Skip to content

10 min read

Nearest-file and cumulative discovery predict opposite outcomes

On this page

The platform monorepo — one repository holding dozens of packages that are built, versioned and published separately — carries a rule at its root: every change that touches user-visible behaviour ships with a changeset, because the release tooling builds the changelog from those files and a missing one silently drops a line from the notes. Underneath it, packages/internal-tools/ carries its own instruction file saying that the package is never published to the registry and changesets should be skipped. Maya worked almost exclusively in that package for two sprints, and of the fourteen pull requests she opened from it, six arrived with a .changeset/tidy-ants-repeat.md file the agent had written without being asked. She deleted them, filed a ticket against the platform team titled "nested config ignored intermittently", and went back to work.

The interesting word in that title is "intermittently". A configuration system that reads the nearest file and stops is deterministic with respect to this question: either the root rule is in front of the model or it is not, and the answer does not change between Tuesday and Thursday on the same checkout. Six out of fourteen is not what a file being ignored looks like. It is what two rules being present at once looks like, with nothing in either of them saying which one governs, and the model resolving the collision freshly on every run.

Behind Maya's collision is the arrangement this topic is about. A session does not read one instruction file; it reads whatever set of them the program running the agent — the CLI or IDE extension, which everything below calls the host — decides to assemble, and in a repository like platform that set is drawn from four places at once: a personal file in the engineer's home directory, a policy the company distributes to every machine, the file at the repository root, and a file inside whichever package is being worked on. Scoping is the question of which of those texts a given session picks up and which code each one was written to govern, and it exists so that a package with a genuine local convention can state it in one place instead of relying on everyone remembering it. Precedence is the contract that says which text governs when two of them ask for incompatible things, and it exists because prose carries no rule for its own resolution; where nothing in the system settles a conflict, the model settles it while generating, which is another way of saying it settles differently on different runs. You meet both the moment a second file appears — the first package that wants its own testing rule, the first week at a company that ships a central policy to every laptop — and Maya met them without knowing the arrangement had a shape, which is why her ticket asks a question the platform team cannot answer.

Two models, two predictions

The instruction files described in 12.7 have to be found before they can be applied, and the finding is where the two models diverge.

Under nearest-file discovery, the host walks up from the working directory until it finds an instruction file, loads that one, and stops. This is the model most engineers assume, because it is how a lot of neighbouring tooling behaves. Applied to Maya's situation, it predicts that the root changeset rule never enters the session at all when she starts inside packages/internal-tools/, and the agent adds changesets on zero of fourteen PRs. It also predicts a second, stranger thing that nobody usually thinks through: when Maya works from the repository root and edits a file inside internal-tools/, the local rule is invisible and the root rule applies, so the same file gets different treatment depending on where the terminal was.

Under cumulative discovery, the host collects every instruction file from the repository root down to the working directory and concatenates them, root first. Both rules are in context. Neither says anything about the other. The model reads "always add a changeset for user-visible changes" and then reads "this package is not published; skip changesets", and it has to decide whether the second is an exception to the first, a contradiction of it, or a note about a different situation. That decision is a generation, so it varies, and roughly-half-the-time is exactly the shape of it.

There is a third arrangement that shows up in practice and confuses the diagnosis, which is discovery that is cumulative but lazy. The root file loads at session start; a nested file loads the first time the agent reads or writes a file underneath that directory. Behaviour then depends on the order of operations inside the session. If Maya's first request sends the agent to read packages/internal-tools/src/index.ts, the local rule arrives early and tends to hold. If her first request has the agent grep across the whole repository and touch three other packages before it gets to internal-tools/, the root rule has been sitting alone in context for twenty turns before the local one shows up.

Nesting also rarely stops at two levels. The same repository has a third file under packages/internal-tools/scripts/, four lines long, saying that everything beneath it is one-off maintenance tooling that does not need tests. Under nearest-file discovery that four-line file becomes the entire instruction set for anyone whose terminal is open in scripts/, which means the repository's rule about never committing credentials to source is also absent, and nobody thought to restate it there because nobody imagined a four-line file displacing everything above it. Under cumulative discovery the session carries all three files, roughly 340 lines of instruction text between them, holding the root's testing policy, the package's changeset exemption and the script directory's test exemption at once, two of the three phrased as unqualified imperatives. The number of pairs that can disagree grows with the square of the file count, so adding a third file did not increase the ambiguity by half, it roughly tripled it.

Discovery modelWhat is in context inside packages/internal-tools/Predicted changeset rateSignature in the wild
Nearest-fileLocal rule only0 of 14Behaviour flips when you cd to the repo root
CumulativeRoot rule and local rule, root firstAnything between 0 and 14, unstableSame commit, same prompt, different answers
Cumulative, lazyRoot rule always; local rule after the first touchLow when the task starts local, high when it starts wideCorrelates with task shape, not directory

Maya's six-of-fourteen rules out nearest-file discovery on its own. Distinguishing the two cumulative variants takes one more observation, which is whether the failures cluster on the wide-ranging tasks, and hers did: five of the six changeset-adding runs began with a repo-wide search.

Where "nearest wins" came from

The assumption is not stupid; it is transferred from systems where it is genuinely true, and where the reader has spent years having it be true. Git resolves core.autocrlf by reading system, global and local configuration in order and letting the last writer of that key win:

bash
$ git config --list --show-origin | grep core.autocrlf
file:/opt/homebrew/etc/gitconfig        core.autocrlf=input
file:/Users/maya/.gitconfig             core.autocrlf=input
file:.git/config                        core.autocrlf=false
$ git config --get core.autocrlf
false

Three files mention the setting and exactly one value survives, because core.autocrlf is a key and a key holds one value. EditorConfig walks up the tree and stops at root = true. ESLint's flat config applies later entries over earlier ones for matching files. tsconfig.json extends a base and overrides named compiler options. Kubernetes merges kubeconfig files with a documented precedence per field. Every one of these systems has an identifier that lets a later definition find and displace an earlier one.

Prose has no identifier. "Always add a changeset for user-visible changes" and "this package is not published; skip changesets" are two strings that share the word changeset and nothing else structural. There is no mechanism by which the second can be recognised as being about the same key as the first, because there is no key. Even a host that intends nearest-file semantics can only implement them at the granularity of whole files, and the moment a host decides that nested files should extend rather than replace — which is what most teams want, since a package-level file that erased the repository's testing and security conventions would be a worse default — the contradiction is structurally unavoidable and lives in the text rather than in the loader.

This is why the shallow diagnosis is expensive. Reading Maya's ticket as "the nested file is nearer, so it should win, so the tooling is broken" produces an upstream bug report, a week of waiting, and a reply explaining that discovery is cumulative by design. The behaviour she saw is consistent with the tool doing exactly what its documentation says. In the version of this that actually happened on a team I worked with, the ticket was open for nine days, two engineers wrote a wrapper that stripped the root file before launching the agent inside that package, and the wrapper was still in the repository four months later, quietly suppressing a security instruction that had been added to the root file in the interim. The cost of the wrong model is rarely the wrong answer on the day; it is the workaround that outlives the misunderstanding.

The check that takes five minutes

Before arguing about which model your host implements, make it tell you. The technique is to plant recognisable strings and then ask what arrived.

markdown
<!-- platform/CLAUDE.md -->
## Release process

Every user-visible change ships with a changeset. Run `pnpm changeset` before
opening the PR.

Configuration marker: ROOT-7F3A.
markdown
<!-- platform/packages/internal-tools/CLAUDE.md -->
## This package

`internal-tools` is never published to the registry. Skip changesets.

Configuration marker: TOOLS-B241.

Then start a session with the working directory set to packages/internal-tools/ and ask a question with no engineering content at all:

text
Do not read or edit any files. List every instruction file currently in your
context, in the order you received them, and quote the "Configuration marker"
line from each one.

Three answers are possible, and each one is a different world. Only TOOLS-B241 means nearest-file discovery, and Maya's ticket is then a real bug worth escalating. Both markers, root first, means cumulative discovery and the intermittency is expected. Only ROOT-7F3A means the nested file was never discovered at all, usually because the host looks for a different filename in subdirectories than it does at the root, or because discovery is anchored to the repository root rather than the working directory. Repeat the same probe from the repository root to see whether the answer moves, which separates working-directory-anchored discovery from checkout-anchored discovery.

The probe answers a binary question — what was loaded — and Maya's complaint was about a rate, so it is worth spending another few minutes measuring the rate properly. Run the real task from a fresh session five times, with the same prompt and the same working directory, and count how many runs produce a changeset. A cumulative loader with two contradicting rules typically lands somewhere between two and four out of five; three probe runs that all agree, on a question the model has genuine latitude over, is weak evidence that something other than the model is deciding. Doing this before you write the ticket also gives whoever reads it a number rather than the word "sometimes", and "3 of 5 on a1b2c3d, prompt attached" is a reproduction where "sometimes" is a conversation.

Two cautions on this probe. The model is reporting on its own context, and a model that has been handed no instruction files at all will sometimes produce a plausible-sounding list anyway, so a marker string that appears nowhere in your files is a signal that you are reading a hallucination rather than an inventory. And the probe tells you what was loaded, which is not the same as what governed — a rule can be present and lose. Chapter 2 replaces this with the host's own view of the assembled configuration, which does not depend on the model's cooperation and takes about the same five minutes.

What knowing the model does not give you

Identifying discovery as cumulative explains Maya's six of fourteen and settles nothing about what to do next. The obvious repair — rewriting the local file as "the root's changeset rule does not apply here" — is better than what she had, because it converts two rival statements into one statement with a named exception, and chapter 5 takes up when that framing is the right resolution and when the honest answer is that one of the two files should stop mentioning the subject entirely.

Discovery also says nothing about the layers that live outside the repository. Everything above concerns files inside one checkout, which is the tractable part of the problem, because anyone can read them and everyone sees the same bytes. A personal instruction file in a home directory applies to every session that person runs, appears in no repository, and is invisible to the two colleagues comparing notes on the same commit. That layer breaks the assumption that a shared checkout implies shared behaviour, and it is the reason the diagnostic in the next chapter starts from what a specific session assembled rather than from any file on disk.

Scoping and precedence — read the current version at https://daviddominguez.dev/learn/en/scoping-and-precedence/nearest-file-and-cumulative-discovery-predict-opposite-outcomes

Back to top