Structuring a repo so an agent can navigate it
Six concrete changes that stop your agent burning nine tool calls working out where the routes live.
You ask for a change to the checkout route. The agent lists the repo root, reads package.json, greps for router, opens src/index.ts, follows an import into a barrel file, greps again, reads two more files and finally lands on the handler. Nine tool calls, no code written, and a slice of the session is now spent on directory listings that taught you nothing.
That waste is not the model being slow. It is your repository being unguessable, and you pay the same toll at the start of every single task.
Predictable beats clever
An agent has two ways of finding things: reading files and searching text. No IDE index, no jump-to-definition, no memory of the session you had on Tuesday. Every navigation decision it makes comes from filenames, directory names, and what a grep returns.
That inverts a bit of received wisdom. Structures that are pleasant for a human who has held the codebase in their head for a year β short generic names, a bit of indirection, a utils folder that everyone knows means the payments helpers β are actively hostile to something arriving cold every session. Item returns forty grep hits. CheckoutLineItem returns three, and all three matter.
The test to apply: if the only way to know where something lives is to have been told, you will be telling the agent every session, and paying for it in tool calls and context each time. Related: Context windows for people who ship.
The instruction file is a map, not a manifesto
Your root instruction file β CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, whichever your tool reads β is loaded at the start of a session. It is the only thing you get for free. Most of them are full of tone advice about writing clean code and missing the two things that actually remove tool calls: the exact commands, and where things are.
## Commands
Package manager: pnpm. Never npm or yarn; the lockfile is pnpm's.
- Install: `pnpm install`
- Dev server: `pnpm dev` (port 3000)
- Typecheck: `pnpm typecheck`
- Lint and fix: `pnpm lint --fix`
- Full suite: `pnpm vitest run`
- One test file: `pnpm vitest run src/features/checkout/total.test.ts`
## Where things are
- `src/features/*` β one folder per feature: route, service, schema, tests
- `src/shared/*` β code used by two or more features. Nothing else goes here.
- `src/db/migrations/*` β SQL, applied in filename order. Never edit an applied file.
- `src/generated/*` β codegen output. Do not edit. Exclude from searches.
- `config/` β all environment config. There is no config anywhere else.
The single-test command matters more than the full-suite command. It is the one the agent runs twenty times in a session, and without it the agent will run the entire suite instead, twenty times.
Ten lines of directory map is enough. You are not documenting the repo. You are deleting the first four exploratory tool calls of every task.
Feature folders put everything one task needs in one place
Layer folders β controllers/, services/, models/, types/ β scatter one feature across five directories. A checkout task means opening five folders and working out which files in each are the checkout ones. Feature folders collapse that into one directory read.
src/
features/
checkout/
checkout-route.ts
checkout-service.ts
checkout-schema.ts
checkout-service.test.ts
refunds/
refunds-route.ts
refunds-service.ts
refunds-schema.ts
refunds-service.test.ts
shared/
money.ts
money.test.ts
http-client.ts
db/
migrations/
generated/
api-types.ts
Two details are doing real work there. The test file sits beside the code it tests, so an agent that listed the directory already knows a test exists and what it is called. And the filenames repeat the feature name. checkout-service.ts is more verbose than service.ts. It is worth it, because it is unambiguous in a search result list, unambiguous in a diff, and unambiguous in an editor tab.
One home for shared code, stated explicitly. The sentence in the instruction file above β "code used by two or more features, nothing else goes here" β is the whole policy. Without it you get a helper duplicated in three feature folders, because the agent had no way to know a home existed.
Names that survive a grep
Choose one file naming convention and never mix it. Kebab-case or camelCase; either is fine, both is not. A mixed repo means every filename search needs two guesses, and roughly half of them are wrong. Enforce it with a lint rule so the choice stops being relitigated in review.
Then be suspicious of re-export chains. A barrel that re-exports a barrel means a grep for a symbol lands on a re-export rather than a definition, and the agent spends a tool call per hop following it home.
// src/index.ts
export * from "./lib";
// src/lib/index.ts
export * from "./money";
// src/lib/money.ts <- three hops to reach the actual definition
export function formatMoney(pennies: number): string {
return `Β£${(pennies / 100).toFixed(2)}`;
}
One shallow barrel at a package boundary is fine. Chains inside a package are pure navigation tax.
Generated code needs the same treatment from the opposite direction. Put it in a directory whose name says so, and say in the instruction file that searches should exclude it. Otherwise a grep for a type name returns two hundred hits from a generated API client, the agent reads a handful of them, and the session is now carrying machine output that will never be used.
Two more structural rules. Avoid single files past roughly a thousand lines, because long files get read in parts, and an agent reasoning about a function while unsure what is in the half it did not see is exactly where confident-but-wrong edits come from. And keep configuration in as few places as you can manage: one env file, one config directory, one CI file, all named in the instruction file. Config split across a root file, a nested override and an environment fragment guarantees the agent edits the wrong one.
Let hooks do what the agent will forget
Some tools can run shell commands on lifecycle events, such as after a file write. Formatting and linting belong there rather than in an instruction that asks the agent to remember.
The benefit is not tidiness. It is that a hook holds the convention true without consuming any context at all. "Always run prettier after editing" costs tokens in every session and is followed unevenly at turn forty. A format-on-write hook costs nothing and never forgets. Anything mechanical and checkable should move out of the instruction file and into a hook.
Audit your own repo in ten minutes
Open a fresh session with no prior conversation and ask three questions. Count the tool calls.
- "Where is the code that handles refunds? Do not change anything."
- "What command runs the tests for just that feature?"
- "Where would a helper used by both checkout and refunds go?"
A repo structured for agents answers the first in two or three calls, the second in zero because the instruction file already said, and the third in zero for the same reason. Anything over five calls has located the exact part of your structure that is billing you on every task. Fix that one thing, then run the audit again.
Keep reading
Shipping to production what an agent built
Agent code is correct on the happy path and thin everywhere else, so shipping means checking the parts nobody prompted for.
The retry loop: spotting it and breaking it
Two attempts on the same error is the whole budget, and here is exactly what to do when the second one fails.
When to stop the agent and write it yourself
Six signals that another prompt is wasted, and the skeleton-and-fill pattern that beats both retrying and taking over completely.