Skip to content

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, and git log --oneline must 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-msg hooks 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-msg hook 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)

  1. Conventional commit format required — message must match <type>[(<scope>)][!]: <description>.
  2. No bundled scopes — the message body must not contain additional type: ... prefixes on separate lines (the signal of two commits glued together).
  3. "and" heuristic warning — if the subject line contains and connecting two action verbs, a warning is printed but the commit is not blocked.
  4. 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 --oneline becomes 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.

  • Supersedes: nothing (new policy)
  • Related: ADR-0007 (OWASP CI/CD Security Posture) — same hook layer
  • Related: ADR-0006 (env scaffold) — post_gen_project.py extended here to install hooks
  • Implementation: tools/check_atomic_commit.py, .git/hooks/commit-msg, builder/hooks/post_gen_project.py