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/--allno longer exist onmadt_commit,madt_finish, ormadt_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_commitpath is unchanged and is NOT a staging mode.no_committellsmadt_finishthe 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 viamadt_commit files=[...](or plain committed history); it stages nothing, so blanket staging never had a role there. Likewise thebranch=(no-checkout) finish form is unaffected. - Pathspec isolation still holds where it already did. For a foreign linked
worktree,
madt_commitremains pathspec-isolated to exactly the namedfiles(git’s owncommit -- <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=truestaging: the same-repo-worktree “includingall=truestaging” parenthetical, themadt_addall=truerefusal, and themadt_commit“bothall=trueand the implicit commit-what’s-already-staged mode are refused” clause. Read them as: declaredfiles=[...]is the only staging form; there is noall=trueto refuse. The pathspec-isolation guarantee for foreign targets survives verbatim. - ADR 0019 — the linked-worktree carve-out line “
madt_commitstill requiresfiles=(noall=true)”. Read it as:madt_commitrequiresfiles=[...]; 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, andmadt_addshare 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=truegets a contract error that names the new path (declarefiles=[...], or useno_commitfor 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 ofall=trueguidance 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=truebut 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=trueprose. 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.