Conventional Commits solved a real problem. It gave teams a small shared grammar for a part of software development that had mostly been left to taste:
feat(auth): support passkeysThat line is easy to write, easy to scan, and easy to parse. It is a much
better foundation than histories full of changes, updates, and fix stuff.
It can drive changelogs, release automation, and navigation through an
unfamiliar codebase.
But its greatest strength became its biggest constraint. Conventional Commits uses the type to answer two different questions:
- What kind of change is this?
- What release consequence does this commit request?
Those questions correlate often enough that the shortcut feels natural.
feat maps to a minor release. fix maps to a patch release. A ! or
BREAKING CHANGE maps to a major release.
Correlation is not identity. Once commit messages become release inputs, the shortcut starts turning engineering judgment into taxonomy.
The header is worth keeping. When commit messages also feed release tooling, the requested impact needs a field of its own.
What Conventional Commits got right
The Conventional Commits 1.0.0 specification is deliberately small. Its basic shape is familiar now:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Three choices are worth preserving.
First, the summary remains useful in Git logs, hosting platforms, release tools, and terminal interfaces. The format puts its strongest human signal on the first line.
Second, it leaves the body available for reasoning. A good commit can remain brief when the diff is self-explanatory and become detailed when context would otherwise disappear.
Third, it creates an automation seam without turning the whole message into a configuration file. A person can write a valid message without a form, editor plugin, or generator.
Intent Commits preserves all three. The disagreement is not with structured commits. It is with the meaning assigned to that structure.
A type cannot carry all this meaning
Consider a refactor:
refactor(money): replace decimal strings with Money valuesWhat is its release impact?
If Money is entirely internal, perhaps none. If it fixes observable rounding
errors without changing the public API, perhaps patch. If callers receive a
new Money value in place of a string, it is probably major. The word
refactor cannot indicate which one occurred.
The same ambiguity appears with the types that Conventional Commits does map.
A fix can break consumers when the old behavior became a de facto contract.
A feat can be internal, experimental, disabled, or limited to an unreleased
surface. A documentation correction can reveal that users have been relying
on a dangerous interpretation and need to change their configuration.
None of this means the types are bad. feat, fix, docs, and refactor are
useful descriptions of intent. They help a reviewer understand the nature of
the work and help a reader navigate history.
They are simply not sufficient release inputs.
When a type must stand in for release impact, authors can choose a label for the version bump they want rather than the work they performed. If they keep the truthful type, release tooling can produce a consistent but contextually wrong result.
The problem is not missing vocabulary. Adding more types only creates a larger guessing table.
Breaking changes have two spellings and too little guidance
Conventional Commits provides two ways to mark a breaking change:
feat(api)!: remove token authenticationor:
feat(api): remove token authentication
BREAKING CHANGE: token authentication has been removedIt even permits both at once. This is convenient for adoption, but awkward for a protocol. Two canonical spellings create extra parser states and linting rules while leaving repositories to select a preferred style.
More importantly, identifying a break is only half the information a consumer needs. “Token authentication has been removed” describes the event. It does not say whether OAuth client credentials replace it, whether existing tokens continue to work during a transition, or whether there is no migration path.
A breaking change should carry a consequence and an instruction:
refactor(auth): remove API token authentication
Impact: major
Migration: replace API tokens with OAuth client credentialsThat is slightly more work for the author and substantially less work for everyone adopting the release.
“Similar to Git trailers” is not a complete contract
Conventional Commits was right to put structured metadata at the end of a
message. Git already has a trailer convention and provides
git interpret-trailers to manipulate it.
The problem is the phrase “similar to.” Similar formats are where parsers diverge. Case sensitivity, continuation lines, duplicate tokens, whitespace, unknown fields, and paragraph boundaries all become implementation choices. One tool preserves metadata that another silently rewrites.
Intent Commits makes Git-compatible trailers the structured layer. The body remains prose for people. Automation reads declared fields from the trailer block:
Impact: patch
Refs: DEVELOP-123
Reviewed-by: Ada LovelaceThe specification owns a small interoperable set: Impact, Target-Impact,
Migration, Target-Migration, Reverts, Affects, and Changeset.
Projects remain free to add fields such as Refs or Security, and tools must
preserve fields they do not understand.
A protocol should standardize the information that must travel between tools without trying to own every useful piece of repository metadata.
Reverts are changes, not arithmetic
Conventional Commits deliberately leaves revert behavior to tool authors. That is understandable because reverts are messy, but the absence becomes visible as soon as commits drive releases.
Suppose a minor feature is released and reverted a week later. Does the revert subtract a minor increment from history? No. The earlier release still contained the feature. The revert is a new change with a new compatibility effect.
It may be a patch that restores expected behavior. It may itself be breaking because consumers already adopted the feature. A partial revert may leave half the original contract intact.
Intent Commits therefore treats a revert like any other present-tense change:
revert(checkout): restore synchronous payment capture
Impact: patch
Reverts: 7f83d12a96d4Reverts provides the graph relationship. Impact describes what applying
this commit means now. Tooling is explicitly forbidden from pretending that
release history can be balanced like a ledger.
Squashing changes the unit of meaning
A repository can accept ordinary branch commits and produce one structured message when a pull request is squashed. That workflow reveals another missing rule: metadata from the source commits cannot simply be concatenated.
A branch might add an API, replace it during review, fix two mistakes, and end with one small backwards-compatible capability. The intermediate commits tell the story of development. The squash commit describes the resulting change that lands.
Intent Commits requires the squash author to resolve that result deliberately. The highest impact among the intermediate commits is not automatically correct; changes that cancel before landing do not affect consumers. The final message must classify the final diff.
The requirement becomes necessary at the boundary between tools, where unwritten expectations stop being universal.
The Intent Commits model
The Intent Commits Specification separates a message into layers with different owners:
| Layer | Question | Owner |
|---|---|---|
| Type | What kind of work is this? | Intent Commits plus project vocabulary |
| Scope | What primary boundary changed? | Project profile |
| Description | What state does the commit produce? | Author |
| Body | Why was this change necessary? | Author |
| Impact | What release consequence is requested? | Author or project profile |
| Migration | What must a consumer do? | Author |
| Other trailers | What additional systems need metadata? | Project policy |
The common header barely changes:
feat(auth): support passkeysWhat changes is the refusal to guess. In Intent Commits, that type does not
inherently mean minor. A release-ready version declares the impact:
feat(auth): support passkeys
Impact: minorA project can preserve the familiar shortcut by publishing a profile:
types:
feat:
default-impact: minor
fix:
default-impact: patch
docs:
default-impact: none
refactor: {}This is not the same coupling in a different file. The distinction is that the
mapping is now visible project policy, not a universal claim about what feat
means. A project can leave refactor without a default because its impact
requires judgment.
Impact is the explicit version-bump override
Impact is not merely a description of risk. It is the explicit version-bump
signal consumed by release automation.
That means the author can keep the truthful type while requesting a different release class from its usual default:
fix(reporting): expose corrected historical totals
Impact: minorThe work is still a bug fix. Perhaps the corrected totals are important enough
to announce as a new minor release, or the project deliberately groups data
corrections that way. There is no reason to mislabel it as feat merely to
coerce the release tool.
The same rule works for a major release:
fix(parser): reject ambiguous signed documents
Impact: major
Migration: remove duplicate keys before parsing documentsAn explicit Impact trailer takes precedence over a project profile’s type
default. Impact: major also requires migration guidance. The specification
does not decide whether the author’s release judgment is correct, but it makes
the judgment visible, reviewable, and deterministic.
That requested impact is still not the complete release decision. Several
commits may form one change, or one atomic commit may affect release units with
different schedules. Intent Changesets
can own that larger decision, while the
changelog owns the audience-facing release history.
Impact preserves useful atomic evidence; it does not make a commit message
the entire release model.
Scope must identify the release unit in a monorepo
Scope becomes more important as a repository grows. In a monorepo, auth,
http, or refunds may exist in several applications. A locally meaningful
name can be globally ambiguous.
Imagine a Go repository with four independently deployed applications:
apps/gateway
apps/billing
apps/worker
apps/adminA fix confined to the worker should identify the worker:
fix(apps/worker): retry interrupted settlement jobs
Impact: patchIf narrower context matters, the scope can be hierarchical while keeping the release unit first:
fix(apps/billing/refunds): reject duplicate credit notes
Impact: patchfix(refunds) is not good enough when several applications have refund logic.
The scope should let a person or release tool connect the commit to an
application without reading the diff or guessing from repository structure.
Shared changes need more context. Intent Commits keeps one primary scope and
uses repeatable Affects trailers for additional release units:
feat(shared/auth): rotate service credentials without downtime
Impact: minor
Affects: apps/gateway
Affects: apps/workerThe standardized Project Profile defines canonical components and streams. If targets need different bumps, Intent Commits uses portable qualified fields:
Target-Impact: apps/gateway=minor
Target-Impact: apps/worker@lts=noneTarget-Migration follows the same selector grammar. An exact stream value
wins over a release-unit value, which wins over the common Impact or
Migration. These target differences do not require a repository-specific
trailer dialect, and tools must not silently apply the largest bump to every
application.
Scope may be syntactically optional, but a monorepo commit is not release-ready if the affected release unit cannot be resolved.
Unspecified does not mean none
Making Impact mandatory on every commit would maximize explicitness and make
the format tedious for repositories that already have sensible defaults.
Making it optional and treating absence as none would be worse: forgotten
metadata would silently suppress releases.
Intent Commits uses a third state: unspecified.
If a message has no Impact trailer and its project profile provides no
default, release tooling does not know the impact. It must not invent one from
the type, and it must not silently interpret uncertainty as no impact. It can
reject the commit, ask a human, or keep it out of a release until classified.
That gives repositories a gradual adoption path without making ambiguity look like certainty.
Three kinds of conformance
“This is a valid conventional commit” can mean several things. Sometimes it means a parser recognized the header. Sometimes it means the standard fields are semantically correct. Sometimes it means a repository accepts the type and scope. Those are separate claims.
Intent Commits names them:
- Intent Commits Core means the message structure and standard fields are valid.
- Intent Commits Release Ready means the effective impact is known under an identified project profile.
- Intent Commits Project means the message also satisfies that project’s types, scopes, and additional policy.
A validator can now report something useful: malformed syntax, invalid Intent Commits metadata, an unknown project scope, or an unspecified impact. “Invalid commit” is rarely enough information to fix one.
Style is not interoperability
Teams should care about readable descriptions. Imperative subjects, short first lines, and bodies that explain why the change exists make a history better for humans.
They do not make a message more parseable.
Intent Commits deliberately leaves capitalization, terminal punctuation, imperative mood, and maximum line length to project profiles. A base protocol should not reject a machine-readable commit because its author wrote “adds passkeys” instead of “add passkeys.” That is a style violation at most, not a failure of interoperability.
Separating these layers also improves tooling. A parser can implement Intent Commits without becoming an English-language writing critic. A project linter can add the exact editorial policy its maintainers value.
Keep Conventional Commits when impact lives elsewhere
A repository does not need another commit protocol merely because it uses Conventional Commits. If messages exist for history navigation while a pull request, changeset, or release review records impact, the existing header can keep doing that smaller job. Release automation can ignore type-to-version mappings instead of adding trailers to every commit.
Intent Commits earns its cost when several people or tools must exchange the requested impact, migration guidance, affected release units, and uncertainty through the commit itself. The decision reverses when that information already has one authoritative home and commit-level duplication would create drift.
AI needs the protocol and the uncertainty rules
An AI tool can perform repetitive work such as drafting a commit message from a diff.
A model can produce plausible structure while filling missing context with a confident guess. A precise protocol makes those missing decisions visible.
“Write a conventional commit” is therefore an inadequate instruction. It does
not provide the repository’s type vocabulary, monorepo scopes, release policy,
or standard for uncertainty. The model may invent an issue number, call an API
break a refactor, omit an affected application, or choose patch because most
files look small.
An AI commit generator should receive four things:
- the exact staged or otherwise selected diff;
- the Intent Commits version;
- the repository’s Intent Commits project profile; and
- an instruction to stop and ask when scope, impact, or migration is unknown.
The specification includes a reusable prompt. Its essential constraint is:
Do not invent an issue reference, scope, affected component, release impact,
or migration path. If any required decision cannot be determined, return
NEEDS_INPUT followed by the unresolved decisions.Without the stop condition, a generator can convert missing evidence into
Impact: none. It may suggest an impact, but the suggestion must remain
distinguishable from a validated declaration.
AI can also act as a useful second reviewer. It can notice that a commit marked
Impact: none changes public interfaces, migrations, or several applications.
Those observations are warnings, not proof. Diff size and file count are weak
proxies for compatibility: a one-line change can be major, while a generated
ten-thousand-line change can have no consumer impact.
AI may draft and challenge the message, but it must obey the same evidence boundaries as the engineer.
Adopting Intent Commits without rewriting history
Intent Commits is designed as an evolution, not a flag day. Most Conventional Commit headers already fit the Intent Commits grammar. Bodies and ordinary trailers remain in the same place.
A practical migration can happen in stages:
- Keep the existing
type(scope): descriptionheaders. - Add
Impactto commits whose release consequence is not captured safely by current type mappings. - Add
MigrationwheneverImpact: majoris declared. - Replace
BREAKING CHANGEand!in newly authored messages. - Publish a project profile with accepted types, scopes, and defaults.
- Require Release Ready conformance at the merge boundary.
The header remains valid Conventional Commits syntax. Existing tools may still
infer releases from feat and fix, so release automation must be updated
before Intent Commits becomes authoritative. Compatibility of syntax does not
mean compatibility of release semantics.
Old history should remain old history. A migration tool can translate obvious
breaking markers to Impact: major, but it cannot manufacture useful migration
instructions or recover context the original message never contained.
What Intent Commits does not try to solve
A commit-message specification should resist becoming a development process. Intent Commits does not dictate trunk-based development, Git Flow, squash merging, release cadence, or whether every local work-in-progress commit must conform.
It does not define one perfect taxonomy. A Laravel monolith, a Go library, a mobile application, and an infrastructure repository do not share the same useful set of types and scopes.
It also does not promise that a declared impact is correct. Structured metadata can make a decision explicit and reviewable; it cannot make the decision for the team. Tests, API comparison, human review, and release policy still matter.
The aim is narrower: when a person or tool makes that decision, the message has one clear place to carry it.
A specification should expose uncertainty
The deeper problem with Conventional Commits is not that feat maps to minor
or fix maps to patch. Those are useful defaults for many projects.
The problem is presenting defaults as semantics.
Good protocols preserve distinctions that downstream consumers need. The kind of work, its compatibility effect, and its migration path are related, but they are not interchangeable. Once they are separate fields, teams can review them separately, tools can report uncertainty honestly, and release automation can operate on declared policy instead of folklore.
That is what Intent Commits is intended to provide: a readable history for people, a deterministic protocol for tools, and enough room for engineering judgment to remain visible.
The complete normative contract is published as the stable Intent Commits Specification 1.0.0. Its next useful pressure test is not whether the examples look tidy; it is whether independent parser and release-tool implementations reach the same result on difficult histories.