Index

Intent Commits: Commit Types Are Not Release Decisions

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 passkeys

That 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:

  1. What kind of change is this?
  2. 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 values

What 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 authentication

or:

feat(api): remove token authentication

BREAKING CHANGE: token authentication has been removed

It 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 credentials

That 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 Lovelace

The 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: 7f83d12a96d4

Reverts 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:

LayerQuestionOwner
TypeWhat kind of work is this?Intent Commits plus project vocabulary
ScopeWhat primary boundary changed?Project profile
DescriptionWhat state does the commit produce?Author
BodyWhy was this change necessary?Author
ImpactWhat release consequence is requested?Author or project profile
MigrationWhat must a consumer do?Author
Other trailersWhat additional systems need metadata?Project policy

The common header barely changes:

feat(auth): support passkeys

What 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: minor

A 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: minor

The 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 documents

An 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/admin

A fix confined to the worker should identify the worker:

fix(apps/worker): retry interrupted settlement jobs

Impact: patch

If narrower context matters, the scope can be hierarchical while keeping the release unit first:

fix(apps/billing/refunds): reject duplicate credit notes

Impact: patch

fix(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/worker

The 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=none

Target-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:

  1. the exact staged or otherwise selected diff;
  2. the Intent Commits version;
  3. the repository’s Intent Commits project profile; and
  4. 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:

  1. Keep the existing type(scope): description headers.
  2. Add Impact to commits whose release consequence is not captured safely by current type mappings.
  3. Add Migration whenever Impact: major is declared.
  4. Replace BREAKING CHANGE and ! in newly authored messages.
  5. Publish a project profile with accepted types, scopes, and defaults.
  6. 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.