diff --git a/.github/workflows/test-tidy-commits.yml b/.github/workflows/test-tidy-commits.yml
new file mode 100644
index 0000000..90b338c
--- /dev/null
+++ b/.github/workflows/test-tidy-commits.yml
@@ -0,0 +1,17 @@
+name: Test tidy-commits
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test-tidy-commits:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Run tidy-commits content checks
+ run: bash tests/tidy-commits-content.sh
diff --git a/README.md b/README.md
index c8e9a59..4389617 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,12 @@ npx skills add akunzai/agent-skills
## Skills
+### [`tidy-commits`](skills/tidy-commits/SKILL.md)
+
+Clean up local git commit history before review or merge. Use it to turn WIP,
+fixup, review-fix, format-only, poorly ordered, unsigned, or poorly messaged
+commits into a clear, verified branch story.
+
### [`agents-md`](skills/agents-md/SKILL.md)
Audit, create, and improve `AGENTS.md` files to give AI assistants persistent project memory.
@@ -33,4 +39,3 @@ to the commands below.
- [`mem-clean`](skills/mem-clean/SKILL.md) — clean expired short-term logs; resolve cloud conflicts
- [`mem-sync`](skills/mem-sync/SKILL.md) — sync daily logs across devices via a per-user branch
-
diff --git a/skills/tidy-commits/SKILL.md b/skills/tidy-commits/SKILL.md
new file mode 100644
index 0000000..2bfc772
--- /dev/null
+++ b/skills/tidy-commits/SKILL.md
@@ -0,0 +1,104 @@
+---
+name: tidy-commits
+description: Use when cleaning up local git commit history before review or merge, especially when a branch has WIP, fixup, review-fix, format-only, poorly ordered, unsigned, or poorly messaged commits.
+---
+
+# Tidy Commits
+
+Tidy an existing branch into a clear, reviewable commit story while preserving the intended final tree.
+
+## Quick start
+
+For local branch history cleanup only — for ordinary new commits use the repo's normal commit workflow.
+
+Inspect state → refuse unclear or unrelated working-tree changes → create a backup ref → plan `base..HEAD` (keep / squash / fixup / reword / reorder / split / drop) → show the plan and exact commands → rebase non-interactively → verify the final tree and tests before any push.
+
+## Preflight
+
+- Determine the base with repo evidence: PR base, `origin/HEAD`, or the user-provided base.
+- Fetch first: `git fetch --prune --all`.
+- Require a clean, understood state. If `git status` shows a rebase, merge, cherry-pick, or mixed unrelated changes, stop and ask.
+- Record current state with `git log --oneline --decorate --stat ..HEAD`, `git diff --stat ...HEAD`, and nearby branch context.
+- Create a backup ref, for example:
+ - `git branch backup/tidy-commits-$(date +%Y%m%d-%H%M%S) HEAD`
+
+## Cleanup plan
+
+Classify each commit before rewriting.
+
+| Commit type | Default action |
+| --- | --- |
+| Cohesive feature/fix/docs/test commit | Keep, maybe reword |
+| `fix`, `fixup`, `review fix`, typo, format-only | Squash or fixup into the commit that introduced the need |
+| Commit in the wrong layer/order | Reorder only when dependencies remain valid |
+| Commit mixes unrelated concerns | Split if needed for reviewability |
+| Debug, temporary, accidental, generated noise | Drop only when final behavior should not include it |
+
+Do not blend unrelated concerns just to reduce commit count. A good stack is a readable story, not necessarily one commit.
+
+## Rewrite
+
+**Collapsing the whole range into one commit?** Skip the todo: `git reset --soft && git commit`. This leaves the final tree staged and re-commits it as a single commit — nothing is replayed, so there are no conflicts (it signs automatically when `commit.gpgsign` is set). Use the rebase todo below only when you need selective fixup, reorder, or split.
+
+Otherwise prefer non-interactive rebase patterns (no editor prompts). Generate the todo oldest-first (the reverse of `git log`), edit the actions and order, then feed it back:
+
+```bash
+git log --reverse --format='pick %h %s' ..HEAD > "${TMPDIR:-/tmp}/tidy-commits-todo"
+# edit that file, then:
+GIT_SEQUENCE_EDITOR="cp ${TMPDIR:-/tmp}/tidy-commits-todo" git rebase -i --update-refs
+```
+
+The first column is the action; lines run top (oldest) to bottom (newest):
+
+```
+pick a1b2c3d Add parser
+fixup f4e5d6a fix typo in parser # folds into the pick above, discards its message
+pick 7890abc Add CLI flag
+edit def1234 Wire CLI to parser # stop to amend, then git rebase --continue
+```
+
+Avoid `reword` in todo files because it opens an editor; use `edit`, then `git commit --amend -m ...` and `git rebase --continue`. Use `GIT_SEQUENCE_EDITOR=:` with `--exec` for bulk mechanical amendments.
+
+Recovery: mid-rebase, `git rebase --abort` restores the pre-rebase state; after a bad finish, `git reset --hard `.
+
+When a branch contains merge commits, ask whether to preserve them with `--rebase-merges` or flatten them. Do not guess.
+
+## Stacked Branches
+
+Before rewriting, detect local branches that point inside the rewritten range. Use `--update-refs` by default so stacked branches follow rewritten commits instead of being orphaned.
+
+Flag branches checked out in another worktree: Git will not move those refs. Report them for manual verification before pushing.
+
+If the repo uses a stacked-PR tool such as `gh stack`, prefer that tool's sync/rebase workflow over hand-editing branch relationships.
+
+## Verification
+
+After rewriting:
+
+- Compare the final tree against the backup ref unless commits were intentionally dropped: `git diff --stat HEAD` and `git diff HEAD`.
+- Show the new story: `git log --oneline --decorate ..HEAD`.
+- Run relevant tests, type checks, linters, or focused reproductions.
+- If branch protection requires verified signatures, check commit signatures with `git log --show-signature ..HEAD` or the repo's GitHub status. Re-sign rewritten commits before pushing when needed.
+
+## Push Safety
+
+Never use plain `git push --force`.
+
+If the branch was already pushed, list every ref that changed and show exact commands first.
+
+```bash
+git push --force-with-lease origin HEAD:
+git push --force-with-lease origin
+```
+
+Ask for confirmation before force-with-lease pushes. Report any local-only backup ref and do not delete it without explicit approval.
+
+## Stop Conditions
+
+Stop and ask when:
+
+- The intended base branch is ambiguous.
+- A commit's purpose cannot be inferred from code, tests, or messages.
+- Conflict resolution requires product judgment.
+- Dropping a commit may change behavior.
+- Another worktree or remote branch would be affected and cannot be verified.
diff --git a/tests/tidy-commits-content.sh b/tests/tidy-commits-content.sh
new file mode 100644
index 0000000..59b87e8
--- /dev/null
+++ b/tests/tidy-commits-content.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+SKILL_DIR="$ROOT_DIR/skills/tidy-commits"
+
+fail() {
+ echo "tidy-commits content check failed: $*" >&2
+ exit 1
+}
+
+[ -d "$SKILL_DIR" ] || fail "skills/tidy-commits directory is missing"
+
+grep -q '^name: tidy-commits$' "$SKILL_DIR/SKILL.md" \
+ || fail "SKILL.md frontmatter must use name: tidy-commits"
+
+grep -q -E 'Use when .*commit history|Use when .*commit stack|Use when .*fixup|Use when .*squash' "$SKILL_DIR/SKILL.md" \
+ || fail "description must trigger on commit history cleanup"
+
+grep -R -q -E 'git status|working tree' "$SKILL_DIR" \
+ || fail "working tree preflight guidance is missing"
+
+grep -R -q -E 'backup (ref|branch|tag)|backup-before' "$SKILL_DIR" \
+ || fail "backup ref guidance is missing"
+
+grep -R -q -E 'fixup|squash|reword|reorder|drop' "$SKILL_DIR" \
+ || fail "commit tidy operations are missing"
+
+grep -R -q -E 'non-interactive|GIT_SEQUENCE_EDITOR' "$SKILL_DIR" \
+ || fail "non-interactive rebase guidance is missing"
+
+grep -R -q -E 'reset --soft' "$SKILL_DIR" \
+ || fail "collapse-all reset --soft shortcut is missing"
+
+grep -R -q -E 'diff .*backup|tree.*identical|final tree' "$SKILL_DIR" \
+ || fail "tree-equivalence verification guidance is missing"
+
+grep -R -q -E -- '--update-refs|stacked branch|checked out in another worktree' "$SKILL_DIR" \
+ || fail "stacked branch/update-refs guidance is missing"
+
+grep -R -q -E 'force-with-lease|never plain --force' "$SKILL_DIR" \
+ || fail "safe force push guidance is missing"
+
+grep -R -q -E 'sign|signature|verified' "$SKILL_DIR" \
+ || fail "commit signing guidance is missing"
+
+echo "tidy-commits content checks passed"