ADR-0014: One-Feature-per-Commit Policy Enforced via commit-msg Hook
Status: accepted Deciders: elcubonegro, Claude (Sonnet 4.6) Date: 2026-03-21 Technical Story: Observation that agentic sessions routinely batch unrelated changes into a single commit (e.g., cd95dbc bundled hexagonal architecture refactor, graph-service tests, builder scaffolding, ADRs, Docker Compose, and eval workspaces into one commit), making history unreadable and rollback surgical.
Context and Problem Statement
Agentic CI sessions complete multi-step tasks in a single session and then commit all work at once. This produces commits with 90+ files spanning multiple independent concerns. How do we enforce commit atomicity without blocking legitimate multi-file features?
Decision Drivers
- Git history must be navigable:
git bisect,git revert, andgit log --onelinemust be meaningful. - Agentic sessions are the primary offender — they need machine-enforced constraints, not just guidelines.
- The existing ADR gate (ADR-0007) shows that
commit-msghooks are the right layer for policy enforcement. - Human developers must not be overburdened — the check must have a clear, low-friction bypass for legitimate multi-concern commits.
Considered Options
- Option A: Document policy in AGENTS.md only (honor system)
- Option B:
commit-msghook enforcing conventional commit format + multi-concern detection - Option C: Require one file changed per commit (too restrictive)
Decision Outcome
Chosen option: Option B — commit-msg hook with conventional commit enforcement and multi-concern detection, because:
- Option A already existed implicitly and was ignored (cd95dbc is evidence).
- Option C is unworkable: a single feature legitimately touches multiple files.
- Option B catches the most common failure mode (bundled independent type: scopes in one commit) without blocking normal multi-file features.
The hook is implemented in tools/check_atomic_commit.py and installed as .git/hooks/commit-msg.
Rules enforced (in order)
- Conventional commit format required — message must match
<type>[(<scope>)][!]: <description>. - No bundled scopes — the message body must not contain additional
type: ...prefixes on separate lines (the signal of two commits glued together). - "and" heuristic warning — if the subject line contains
andconnecting two action verbs, a warning is printed but the commit is not blocked. - Bypass — adding
[skip-atomic]to the commit message logs the bypass and allows the commit. Use only for intentional multi-concern commits (e.g., initial project scaffold).
Positive Consequences
- Agentic sessions are forced to stage and commit incrementally or declare a conscious bypass.
git log --onelinebecomes readable.git revert <sha>reverts one concern, not five.- The same tool ships in every generated project via
post_gen_project.py.
Negative Consequences
- Developers must learn conventional commit syntax (mitigated: the error message shows the format).
- Long-running agentic tasks must be broken into multiple commits, which requires planning.
[skip-atomic]can be abused — mitigated by writing every bypass to.atomic-commit-bypasses.log.
Pros and Cons of the Options
Option A — honor system only
- Good, because zero friction.
- Bad, because it was already implicitly in place and ignored (see cd95dbc).
Option B — commit-msg hook
- Good, because it catches the problem at the moment of commit, not in review.
- Good, because it ships into every generated project, making the policy institutional.
- Bad, because it adds a setup step (hook installation) that must be scripted.
Option C — one file per commit
- Good, because maximally atomic.
- Bad, because a feature addition legitimately touches domain, adapter, tests, and ADR simultaneously.
Links
- Supersedes: nothing (new policy)
- Related: ADR-0007 (OWASP CI/CD Security Posture) — same hook layer
- Related: ADR-0006 (env scaffold) —
post_gen_project.pyextended here to install hooks - Implementation:
tools/check_atomic_commit.py,.git/hooks/commit-msg,builder/hooks/post_gen_project.py