ADR 0034: AGENTS.md is an untracked build artifact, guarded by a currency check
Amendments.
- Vocabulary: current pointers use
done(formerlyfinish),accept(formerlygraduate), andAgent file(formerlyCompiled agent file); the decision and reasoning are unchanged.- ADR 0128 — tracking reversed: the tracking default is reversed — the Agent files (canonical and mirrors) are now tracked by default, and the managed ignore block enumerates only materialized/local paths. This record's surviving contributions are the banner removal, the stateless currency check, and the tracked-artifacts guard (now scoped to the enumerated block).
Status: accepted; the tracking default is superseded by ADR 0128 (the currency check and tracked-artifacts guard remain in force); supersedes the tracking decision in — and consolidates — ADR 0032 (whose surviving contribution, the @AGENTS.md pointer, is described in §1–2 below), and revises the tracked flag from ADR 0031. Builds on the orientation verb from ADR 0033.
Context
§discern refresh compiles one body — [built-in base] + [a section per enabled feature] + [your sources] — and writes it to every provider file in [guidance].agents. Two things sat on top of that body to discourage edits:
- A do-not-edit banner at the top of every generated file (an HTML comment — "GENERATED by discern refresh — do NOT edit this file"), and a second, longer banner inside the
CLAUDE.mdpointer. AGENTS.mdwas git-tracked (ADR 0031/0032: "the one tracked file, so guidance changes show in review"), with a CIgit diff --exit-codecatching a stale copy.
Both exist for one reason: coding agents reflexively edit their own instruction file, and an edit to a generated file is silently overwritten on the next refresh — lost unless the agent knew to put it in the source instead.
Two problems surfaced:
- The banner burns prime context for no one who benefits. Claude Code strips HTML comments before the model sees them, so the banner is invisible to the agent it most needs to deter — while Codex and most other agents receive it at the top of the file, where attention is most valuable. The banner is also redundant: the always-included
base.mdalready carries the never-hand-edit guidance in the body, visible to every agent (it is prose, not a comment). - Tracking a derivative invites the confusion it was meant to prevent. The meaningful, reviewable artifact is the source (
guidance.md+ the config), which is already tracked.AGENTS.mdis a pure function of those; committing it adds a second copy that drifts, shows up in every guidance diff twice, and still does not stop an agent from editing it.
A "don't edit me" rule is better enforced than asserted. And the enforcing check is not really about hand-edits at all — it is the general property "the generated files match what the sources would produce right now," which also catches the far more common case: you edited guidance.md or flipped a [features]/config value and forgot to refresh.
Decision
§1. Drop the do-not-edit banners. The compiled body starts at its first real heading; the CLAUDE.md pointer is just @AGENTS.md. The in-body "never hand-edit" section in base.md carries the message to every agent, Claude included.
2. AGENTS.md is untracked by default. All Agent files (AGENTS.md, CLAUDE.md, GEMINI.md) are gitignored — AGENTS.md is a build artifact like the rest. The seed .gitignore ignores it; a schema-8→9 migration adds the ignore to existing installs and notes the one-time git rm --cached AGENTS.md. The Provider.guidanceFile.tracked flag — whose only functional job was naming the canonical pointer target — becomes canonical, decoupling "the full-body file other mirrors import" (still AGENTS.md) from git-tracking (now uniform: none). The @AGENTS.md import resolves a local file regardless of git status, so the pointer mechanism is unchanged.
3. A "generated artifacts are current" check, surfaced at two strengths. renderAgentFiles computes the expected content of each agent file; it is the single source both the writer (compileGuidelines) and the checker (checkGuidanceCurrent) use, so the check can never disagree with what refresh writes. The check is stateless — recompile in memory, compare to disk — so there is no stored hash to keep in sync. It distinguishes:
- stale — the file exists but its bytes differ from the recompiled body;
- missing — the file is absent (the expected state of an untracked artifact on a fresh checkout).
It is surfaced as:
discern status— an advisory hint on either condition (read-only observation; never blocks).discern done— a built-in gate step (aguidance-check, dispositiongate, like the merge check) that blocks onstaleonly, gated on theguidancefeature. Amissingfile is not a failure: an untracked artifact is legitimately absent on a fresh clone, so blocking it would break first-checkout CI for every consuming project. The failure carries aDiagnosticwhosereproduce_cmdisdiscern refresh, a capped diff of what refresh would change, and the redirect: edits belong in your[guidance].sources, not the generated file.
4. A "generated/local artifacts stay untracked" check. The currency check catches byte drift, but it cannot catch a byte-current artifact that was forced into Git with git add -f. discern status therefore reports tracked_ignored_artifacts for any tracked path matched by discern's own generated/local ignore model, and discern done blocks with failed_stage: "tracked_artifacts". The diagnostic tells the agent to remove the paths from the index with git rm -r --cached -- <path...>, then run discern refresh. The detector is derived from the same provider-registry artifact set and canonical .gitignore block as setup/upgrade, so a new generated file or skills directory auto-enrols.
Consequences
§- Prime context reclaimed. The most valuable tokens now open with real guidance, not a deterrent the deterred agent can't even see.
- The source is the reviewable unit. Guidance changes show up in review as
guidance.md/config diffs — the thing a human should actually read — instead of as a regeneratedAGENTS.md. The derivative no longer churns the tree. - The deterrent is now real and self-explaining. An agent that edits
AGENTS.mdis told, atdonetime, exactly what it would lose (the diff) and where the edit belongs. The same check doubles as the guard that config and guidance edits were actually compiled. - CI shifts off the tracked-file trick. The
git diff --exit-codeguard for a staleAGENTS.md(ADR 0032) is replaced by thedonecheck. Because the check blocks onstaleand notmissing, a fresh checkout with noAGENTS.mdstays green; drift is caught the moment the file exists and disagrees. - Forced tracking is self-healing. If an agent overrides the ignore block and stages a generated/local artifact anyway, the next
statusnames it and the nextdonerefuses to bless the branch until the index is repaired. - One self-healing path.
renderAgentFilesbeing the single renderer means a future change to the compile (a new feature section, a provider) is reflected in the check automatically. - Extended by ADR 0035. The compile now templates the built-in sections against a context derived purely from committed config; routing it through the same
renderAgentFileskeeps this currency check authoritative, and the config-only context is what keeps the recompile deterministic.
Alternatives considered
§- Keep the banner, just shorten/relocate it. Rejected: any banner in the generated file is still wasted on the agents that can see it and invisible to the one that can't, and
base.mdalready says it in-band. - Keep
AGENTS.mdtracked; add only the check. Viable for a project that wants the compiled body visible in its own diffs — tracking is a per-project.gitignorechoice, so such a project simply does not ignoreAGENTS.md. Rejected as the shipped default, and not taken by discern itself (it dogfoods the untracked default): tracking a derivative is the confusion this removes, the source (guidance.md/built-ins) is already the reviewable diff, and the currency check makes the tracked-file guard redundant. Untracking also stops a regeneratedAGENTS.mdfrom dirtying the tree every time a worktree refreshes. - Warn only in
status, but letdonepass. Rejected: status is the right orientation surface for the next agent, but the branch should not be able to accept with a known generated/local artifact in history. The gate is the durable guardrail. - Block
doneonmissingtoo. Rejected: an untracked artifact is expected to be absent on a fresh checkout, so blocking would red-light first-run CI for every consumer.statusstill surfacesmissingadvisorily, and the normal flows (worktree create, init, upgrade) all runrefresh, so a working tree has the file. - Auto-heal: run
refreshin thefixstage. Rejected: silently regenerating would erase a deliberate hand-edit before anyone saw it — the opposite of the rescue. The check shows the diff first and lets the agent move the intent to the source. - Store an expected hash and compare. Rejected: a second source of truth to keep in sync. Recompile-and-compare is stateless and always correct.