Turn the current git state into one or more well-formed Conventional Commit
messages, each confirmed by the user before the commit lands. Never guess
what changed — scripts/analyze-diff.py is the single source of truth.
Hard rules
- Always run
scripts/analyze-diff.pyfirst. Never read the diff manually, never callgit diff/git status/git ls-filesyourself. The script's output is the only input you use to plan groups and write messages. - Staged wins. If the script reports
State: staged, work with the staged files only. Ignore unstaged / untracked files — the user staged what they meant to stage. - If the script exits non-zero (nothing to commit), stop and tell the user.
- Never commit without explicit user confirmation. Print the message,
wait for a yes/edit/no response, then act. Never pass
--no-verify, never amend, nevergit push. - Never add co-author trailers unless the user explicitly asks.
- Never
git add -A/git add .. Stage exactly the paths for the current group and nothing else.
Process
Step 1 — Inspect state
Run the analyzer:
python3 <skill-dir>/scripts/analyze-diff.py
Two possible states:
State: staged→ skip to Step 3 (message generation for the staged diff).State: unstaged→ continue with Step 2 (propose groups).
Step 2 — Propose commit groups (unstaged state only)
Read Files and propose 1–4 logical commit groups. Apply the heuristics
below. Then:
-
1 group → stage all files with
git add -- <paths>and go to Step 3. Show the user the single-group plan first only if there is visible ambiguity; otherwise stage silently and move on. -
2+ groups → print a numbered proposal like:
Proposed 2 commits: [1] feat(api): <files...> [2] docs: README.md, CHANGELOG.md Confirm? (yes / edit / merge / cancel)Responses:
yes/lgtm→ iterate groups in order (see Sequential loop).edit(or free-form feedback like "move X to group 2", "split group 1") → revise the proposal, re-print, re-prompt.merge→ treat everything as one group; stage all, go to Step 3.cancel/no→ stop. Do not stage anything.
-
More than 4 groups → do not propose. Print the file list grouped by top-level folder and ask the user to narrow scope before rerunning.
Step 3 — Generate the message
Re-run the analyzer if you just staged files, so scope/type signals reflect what is actually staged now. Then:
-
Pick the type from
Suggested type, overriding only when the diff clearly says otherwise:feat— new user-visible capabilityfix— bug fixrefactor— behavior-preserving code changetest— tests onlydocs— documentation onlychore— tooling, deps, meta files
Anything beyond this list (
perf,build,ci,style,revert) or a breaking change → consultreferences/conventional-commits.md. -
Pick the scope from
Likely scopes:- 1 scope → use it as-is (lowercase, short noun).
- 2 scopes touching one logical change → comma-separated
(
fix(api,ui):). - >3 scopes → do not force a scope. See Gotchas below.
- Root-only files → omit scope.
- Generic top-level folder (
src,lib,app) → look one level deeper in the file list for a meaningful scope (e.g.src/api/...→api).
-
Write the subject. Imperative mood, ≤72 chars, no trailing period. "add", not "added" or "adds".
-
Body — only if it earns its keep. Trivial diff → omit. Otherwise 2–5 bullets, one line each, facts only. Do not restate the subject.
-
Footers. Branch name matches
[A-Z]{2,}-\d+→ addRefs: KEY-123. Breaking / revert / closes → see the reference file.
Step 4 — Confirmation loop
Print the message inside one fenced code block and ask
Commit this? (yes / edit / no).
yes/lgtm/ship it/commit→ rungit commit -F -with the message on stdin. Report the resulting hash + subject on one line.- Feedback (any edit, reword, scope change, "shorter", etc.) → revise and loop back to the start of this step. Do not commit until confirmed.
no/cancel/abort→ cancel semantics (below).
Step 5 — Sequential loop (multi-group runs only)
After each commit, print [N/M] committed <hash> <subject> and start the
next group at Step 2 → staging for that group. Re-run the analyzer
between groups. Stop when all groups are committed or the user cancels.
Grouping heuristics
- One logical change spanning multiple folders (a shared type used in
api/+ui/, a rename that spans both) → one group. - Distinct top-level folders with clearly unrelated changes → separate groups.
- Test changes for a same-run feature → keep with the feature. Tests for
pre-existing code → separate
testgroup. - Lockfile-only files (
package-lock.json,yarn.lock,pnpm-lock.yaml,Cargo.lock,poetry.lock,go.sum) → their ownchore(deps): update lockfilegroup. - Docs-only files (README,
docs/,*.md) → their owndocsgroup, unless the docs ship with a feature in the same diff. - Config / meta files (
.github/,.gitignore, tooling configs) → their ownchoregroup unless intertwined with a feature change. - Cap: max 4 groups per run.
Cancel semantics
If the user cancels a message confirmation mid-run:
- Commits already made in this run stay committed — they were confirmed.
- The current group's staged files are unstaged with
git reset HEAD -- <paths>. File contents on disk are never modified. - Remaining groups are not started.
- Print a one-line summary:
Committed N of M groups; group K unstaged and skipped.
Message format
<type>[(scope)]: <short imperative subject>
[optional body — 2–5 bullets, one line each]
[optional footer(s)]
Allowed types: feat, fix, refactor, test, docs, chore. Extend
only via the reference file.
Gotchas
- More than 3 distinct scopes in one group. Do not force
chore(misc):or silently drop the scope. Print the scope list and suggest re-grouping. - Mixed change types in one group (a
feat+ unrelatedfix+docs). Same treatment: name the mix, suggest re-grouping. - Rename-only diff →
refactorwith the new name in the subject. - Breaking change, revert, merge, unfamiliar footer → read
references/conventional-commits.mdand follow its rules. Do not improvise.
Output
Single-group / already-staged run: one fenced code block with the message, one confirmation line. On confirmation, one line with hash + subject.
Multi-group run: the numbered group proposal, then per-group
message-confirmation-commit cycles, with a [N/M] committed ... line after
each. On cancel, the one-line summary described in Cancel semantics.
Nothing else. No essays, no explanations of what changed — the log and the diff already say it.