Skip to content

ADR-0004: Atomic Commit per Unit

  • Status: Accepted
  • Date: 2026-04-14
  • Supersedes: None

Context and Problem Statement

Executor agents must produce a legible, reversible git history. Two anti-patterns destroy that property:

  1. Bundling — a single commit that touches multiple units (e.g. two tasks + a plan edit in one commit). This makes np:undo-task impossible: there is no clean git revert for "only this one task".
  2. Splitting — a single unit that spans multiple commits (e.g. "part 1 of task N", "part 2 of task N"). This makes np:undo incoherent: which commit represents the unit?

The question: what is the commit-to-unit mapping that makes phase-level, plan-level, task-level, and slice-level undo mechanically implementable?

The Rule

Every completed unit (Phase, Plan, Task, Todo, Backlog-move) produces exactly one git commit. A commit never bundles more than one unit. A unit never produces zero or two commits.

This is the atomic-commit-per-unit invariant. It is the property np:undo, np:undo-task / np:reset-slice, and the executor subagent rely on for their implementation.

Milestone exception

A Milestone is one of the six unit-types (ADR-0003) but a Milestone completion is not itself a separate commit. A milestone is represented by an entry in ROADMAP.md; marking it done is an edit to ROADMAP.md which is itself a unit-level commit (with commit-type milestone(…)). There is no "magic milestone commit" separate from the ROADMAP.md edit.

Decision Drivers

  • Reversibilitynp:undo, np:undo-task, np:reset-slice all rely on the 1:1 commit-to-unit mapping.
  • Legibilitygit log --oneline reads like a plan-trace; each line corresponds to one unit completion.
  • Audit — code review can proceed per-unit; reviewers see exactly what one unit changed.

Considered Options

  • One atomic commit per unit — the rule stated above. (CHOSEN)
  • Squash-at-phase-boundary — many small commits during execution, squashed at phase-end.
  • One commit per file change — commit granularity tied to file count, not semantic unit count.
  • No commit discipline — the executor commits whenever it feels like it.

Decision Outcome

Chosen: "One atomic commit per unit", because it is the only option that makes per-unit revert implementable as mechanical git revert <sha> operations. Every other option forces np:undo-task into either "impossible" (squash, no-discipline) or "brittle" (heuristics about "which files belong to task N").

Commit message format

Every unit-producing commit uses the prefix:

<type>(<phase>-<plan>-<task>): <unit title>

Where <type> is the lowercased unit-type name from ADR-0003: phase, plan, task, todo, backlog, or milestone. The <phase>-<plan>-<task> identifier is elided to the granularity of the unit (e.g. a Phase commit uses just phase-03; a Task commit inside Phase 3 Plan 2 Task 4 uses phase-03-02-04 or similar).

Consequences

  • Goodnp:undo-task, np:reset-slice, and np:undo each map to a well-defined set of commits.
  • Goodgit log --oneline reads as a progress report; git log --grep='phase-03' filters one phase cleanly.
  • Good — code review can proceed per-unit.
  • Good — no daemon required to enforce atomicity (ADR-0001).
  • Bad — small units produce many commits. Accepted; modern git tooling handles thousands trivially.
  • Neutral — PR-level squash-merging is compatible, provided per-unit atomic commits are preserved on the feature branch.

Pros and Cons of the Options

One atomic commit per unit — chosen

  • Good — implementable as mechanical revert operations.
  • Good — produces self-documenting git history.
  • Good — well-understood git-discipline pattern with no novel enforcement cost.
  • Bad — commit count grows linearly with plan complexity. Accepted.

Squash-at-phase-boundary — rejected

  • Good — produces a tidy "one commit per phase" history on main.
  • Bad — destroys task-granularity undo: np:undo-task has no commit to revert once the phase is squashed.
  • Bad — crash-recovery loses intermediate state.
  • Bad — a verifier cannot isolate a single task's diff after merge.

One commit per file change — rejected

  • Good — produces the smallest possible commits.
  • Bad — couples commit count to file count, not semantic unit count.
  • Bad — breaks the mental model: readers can no longer equate "one entry in git log" with "one unit in the plan".
  • Bad — commit messages become meaningless ("add line to foo.md") rather than intentional.

No commit discipline — rejected

  • Good — requires the least process.
  • Bad — breaks np:undo-task by construction.
  • Bad — makes per-unit code review impossible.
  • Bad — two executor agents working the same plan at different times produce non-comparable histories.

More Information

  • Related ADR: ADR-0001 — the commit happens in the invoking agent's session, not in a background worker.
  • Related ADR: ADR-0003 — defines the six unit-types this rule binds to.
  • Related ADR: ADR-0005 — commits touch files in a single tree at a time.

CI-gate enforcement of atomic-commit-per-unit is deferred to a later deploy/CI phase. Current enforcement = human review and this ADR as the authoritative reference.