Appearance
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:
- Bundling — a single commit that touches multiple units (e.g. two tasks + a plan edit in one commit). This makes
np:undo-taskimpossible: there is no cleangit revertfor "only this one task". - Splitting — a single unit that spans multiple commits (e.g. "part 1 of task N", "part 2 of task N"). This makes
np:undoincoherent: 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
- Reversibility —
np:undo,np:undo-task,np:reset-sliceall rely on the 1:1 commit-to-unit mapping. - Legibility —
git log --onelinereads 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
- Good —
np:undo-task,np:reset-slice, andnp:undoeach map to a well-defined set of commits. - Good —
git log --onelinereads 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-taskhas 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-taskby 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.
