ADR 0024 — Declared-files staging: a commit's file set must be named, never swept

  • Status: Accepted
  • Date: 2026-07-10
  • Tracking: legacy tracker 1981 (records the legacy tracker 1964 / PR legacy tracker 1977 contract as a first-class decision)

Context

madt_commit and madt_finish (and madt_add) once offered a blanket-staging mode — all=true on the MCP surface, -A/--all on the CLI — that ran the equivalent of git add -A before committing: stage every modified, deleted, and untracked path in the worktree, then commit the lot. It sat beside the declared form (files=[...] on MCP, --files on CLI), which stages and commits only the named paths.

Blanket staging is a foot-gun whose blast radius is the whole worktree. The 2026-07-05 incident (legacy tracker 1959, legacy tracker 1962) is the worst case realized: a madt_finish all=true silently swept a 30 MB compiled madtea-dev binary from the repo root into the legacy tracker 1951 PR, poisoning that PR’s history and forcing a supersede (PR legacy tracker 1956 → legacy tracker 1961). Nothing in the flow asked “did you mean to commit this?” — all=true answered “yes, all of it” on the agent’s behalf. legacy tracker 1962 originally proposed a binary/large-artifact staging guard as the fix; the owner ruling went further, at the root: remove the sweep, not just its worst symptom.

The gap this ADR fills: legacy tracker 1964 / PR legacy tracker 1977 removed all=true (and legacy tracker 1995 removed the same mode from madt_add), but the two accepted ADRs whose prose leaned on that parameter — ADR 0007’s legacy tracker 1862 cross-repo amendment and ADR 0019’s linked-worktree carve-out — kept describing it in the present tense. ADRs are immutable point-in-time records, so rather than retro-edit their bodies, this record states the surviving contract in full and formally supersedes their staging sections, with pointer notes left on both (legacy tracker 1981).

Decided 2026-07-05 (legacy tracker 1964): “If the agent doesn’t want to declare each file that should be part of the commit, then it shouldn’t commit.” Blanket staging is removed entirely, on both surfaces, and the decision was explicitly confirmed to cover local checkpoint commits, not just the irreversible finish flow.

Decision

A commit’s file set must be declared. Staging IS the declaration; there is no sweep.

  • files=[...] (MCP) / --files (CLI) is the only stage-and-commit path. The paths a commit will contain are named by the caller. Naming a directory is itself a deliberate known-scope declaration (its current contents), not a wildcard — the point is that the caller chose a bounded, stated scope, not that it typed every leaf.
  • all=true / -A / --all no longer exist on madt_commit, madt_finish, or madt_add, on either surface. Passing them yields a helpful contract error naming the declared-files path — never silent acceptance, and never a silent no-op.
  • The no_commit path is unchanged and is NOT a staging mode. no_commit tells madt_finish the working tree is already committed — push / open-PR / merge the existing commits without staging or creating any new commit. It is the completion path for work already checkpointed via madt_commit files=[...] (or plain committed history); it stages nothing, so blanket staging never had a role there. Likewise the branch= (no-checkout) finish form is unaffected.
  • Pathspec isolation still holds where it already did. For a foreign linked worktree, madt_commit remains pathspec-isolated to exactly the named files (git’s own commit -- <files>), so it cannot sweep in something already staged in that index. Declared files are the floor everywhere; the foreign case adds isolation on top.

This is a contract about intent: a commit contains what its author named, and nothing the author did not name. There is no path by which madtea stages a file the caller did not choose.

Supersession

This ADR formally supersedes the staging portions of two earlier records. Their decisions otherwise stand unchanged — legacy tracker 1964 removed a parameter their prose referenced, it did not overturn what they decided (structural fail-closed multi-repo; foreign-primary immutability). Only the staging descriptions are superseded:

  • ADR 0007 (legacy tracker 1862 amendment) — every mention of all=true staging: the same-repo-worktree “including all=true staging” parenthetical, the madt_add all=true refusal, and the madt_commit “both all=true and the implicit commit-what’s-already-staged mode are refused” clause. Read them as: declared files=[...] is the only staging form; there is no all=true to refuse. The pathspec-isolation guarantee for foreign targets survives verbatim.
  • ADR 0019 — the linked-worktree carve-out line “madt_commit still requires files= (no all=true)”. Read it as: madt_commit requires files=[...]; blanket staging no longer exists to be excluded.

A pointer note to this ADR is added to both (bodies otherwise preserved, per the ADR immutability convention).

Consequences

  • The legacy tracker 1959 incident is contract-impossible. Sweeping an unnamed 30 MB artifact into a commit now requires naming it in files=[...] — which is exactly the intent check that was missing. This substantially supersedes legacy tracker 1962’s acceptance criterion 4 (the proposed large-artifact staging guard): with declaration, an unwanted artifact cannot be staged without being typed.
  • One staging vocabulary everywhere. madt_commit, madt_finish, and madt_add share a single answer to “what goes in?”: the declared paths. No surface, foreign or same-repo, has a blanket mode.
  • Migration is a loud error, not a surprise. An agent that previously ran finish all=true gets a contract error that names the new path (declare files=[...], or use no_commit for already-committed work) rather than a silent behavior change.
  • No shipped string recommends blanket staging. Help topics, error/steering text, MCP server instructions, docs/COMPARISON.md, and the regenerated reference docs were swept clean of all=true guidance under legacy tracker 1964.

Alternatives considered

  • A binary / large-artifact staging guard (legacy tracker 1962’s original criterion 4). Rejected as the primary fix: it treats a symptom (large files) of the real defect (staging files nobody named). A guard tuned to size still sweeps in small unwanted files, and every threshold is a new argument. Declaration addresses the whole class. (The guard idea is not resurrected here; declared files subsume it.)
  • Keep all=true but gate it behind a confirmation. Rejected: a confirmation the agent answers itself is the same rubber-stamp that let the 30 MB binary through. The owner ruling is that the declaration is the consent — there is nothing left to confirm once files are named.
  • Retro-edit ADRs 0007 and 0019 to drop the all=true prose. Rejected: ADRs are immutable point-in-time decision records (reinforced by the legacy tracker 1963 rename epic, which preserves ADR bodies verbatim). The convention is to write a superseding record and leave a pointer note — which is what this ADR is.

Relates to ADR 0007 (structural fail-closed multi-repo) and ADR 0019 (foreign-primary checkouts immutable), whose staging sections it supersedes.

Canonical source: docs/adr/0024-declared-files-staging-contract.md in the madtea repo.