Conventional Commits
Every commit, branch, PR, and issue you name follows the Conventional Commits 1.0.0 spec exactly. Two rules override anything else: each commit is atomic (one logical change), and you verify the message against the spec and the actual diff before it lands.
Exception — keep tool-generated default messages. Merges and similar auto-generated commits keep their default message (Merge branch ..., Merge pull request ..., Revert ...), never rewritten into a Conventional Commits subject. Many tools identify and filter these by their default prefix, so the prefix must stay intact.
The format
<type>[optional scope][optional !]: <description>
[optional body]
[optional footer(s)]
Types — pick the one that matches what the diff actually does:
| type | use for | SemVer |
|---|---|---|
feat |
a new feature | MINOR |
fix |
a bug fix | PATCH |
docs |
documentation only | — |
style |
formatting, no code-meaning change | — |
refactor |
code change that neither fixes a bug nor adds a feature | — |
perf |
a performance improvement | — |
test |
adding or correcting tests | — |
build |
build system or dependencies | — |
ci |
CI configuration | — |
chore |
anything else with no production-code effect | — |
wayfinder |
a wayfinder map ticket/issue | — |
spec |
a spec (e.g. from a wayfinder map via /to-spec) |
— |
A wayfinder map ticket/issue is always wayfinder, and a spec is always spec — the artifact decides the type, overriding what the change itself does (feat, fix, etc.).
Subject-line rules:
typeis required, lowercase, from the table above.scopeis optional: a noun in parentheses naming the affected section —fix(parser):.!immediately before the colon marks a breaking change (MAJOR) —feat(api)!:.- Exactly one colon and one space, then the description.
description: a concise, imperative summary on the same line as the type, led by substance. The type already carries the verb, so cut any leading verb that only echoes it (feat: add…,fix: fix…) or fills space (give…,make…,update…); keep a verb only when it names how the change happens in a way the type can't (refactor(auth): extract token parser).feat(time-tracking): add shift-derived automatic blocking→feat(time-tracking): shift-derived automatic blockingrefactor(seeder): give duty-plan departments unique shifts→refactor(seeder): unique shifts per duty-plan department
Body & footers (only when the change needs them):
- Body: one blank line after the description, then free-form paragraphs.
- Footers: one blank line after the body. Each footer is
Token: valueorToken #value. The token replaces spaces with-(e.g.Reviewed-by,Refs), exceptBREAKING CHANGE. - Breaking change: either
!in the prefix, or a footerBREAKING CHANGE: <description>— the token must be uppercase (BREAKING-CHANGEis synonymous).
Language:
- Always use american english.
Common presets
Reach for these exact messages when the diff matches — they keep recurring commits consistent:
style: lint— the commit contains only linting/formatting changes (no change to code meaning).build(deps): upgrade deps— a broad dependency update touching multiple packages.build(deps): upgrade <package>— a dependency update scoped to one package, e.g.build(deps): upgrade vitest.
A preset applies only when the staged diff is genuinely just that change; if anything else is bundled in, split it out (see step 2 below) rather than stretching the preset.
Making a commit
Survey the whole diff. Run
git statusandgit diff(staged and unstaged). Understand every hunk before writing anything.Split into atomic commits. Group the hunks so each planned commit is a single type + scope + intent. A diff that mixes features, fixes, refactors, or unrelated scopes is not atomic — plan one commit per unit. Done when every hunk is assigned to exactly one planned commit and each planned commit is one type/scope/intent.
Stage and draft, one commit at a time. Stage only that commit's hunks (
git add <paths>, orgit add -pfor partial files), then draft its message in the format above.Verify (below) before committing. Do not run
git commituntil every check passes.Commit with the verified message, then return to step 3 for the next planned commit.
Verify
The feedback run. Re-read the drafted message against both the spec and the staged diff. Every item must pass; if one fails, revise the message (or re-split the commit) and run the checklist again.
- Format: matches
<type>[scope][!]: <description>— lowercase type from the table, one colon + one space. - Type matches reality: the type describes what the staged diff does — not
featfor a refactor, notfixfor a new capability. - Atomic: the staged diff is one logical change; nothing unrelated is bundled in.
- Description: imperative, concise, true to the change, and led by substance — no leading verb that merely echoes the type or fills space.
- Body/footers (if any): blank-line separated; footer tokens well-formed; any breaking change flagged with
!or an uppercaseBREAKING CHANGE:footer. - Authorship: the last footer serves the change, and the message ends there — no tool or agent trailer past it (below).
Branches, PRs, and issues
The same type vocabulary and the subject-line rules apply, and each is verified the same way:
- Branch:
<type>/<kebab-case-summary>—feat/user-export,fix/parser-crash. - PR title: an exact Conventional Commits subject line (
<type>[scope][!]: <description>), because it becomes the squash-merge commit. - Issue title:
<type>[scope]: <description>naming the desired change or the bug.
Authorship
As per the EU AI act you must add this footer to every commit:
Created with AI. Verified by a human.
Co-authored-by: <LLM/Agent name> <LLM/Agent email>
Commit-message formatting
When committing from a shell, pass each paragraph as a separate -m argument. Ordinary quoted \n remains literal text.
git commit \
-m 'type(scope): summary' \
-m 'Body paragraph.' \
-m 'Closes #123'
For exact formatting, use git commit -F <message-file> with real line breaks.
Before pushing, verify:
git show -s --format=%B HEAD | sed -n l
Completion criterion: every intended line appears separately, with no literal \n.