Making an Agent Work on a Large Monorepo
Agents that shine on a 40-file project invent a map when the repo has 4,000 packages. The fix is navigation aids and scope, not a bigger window.
Your agent renamed a prop in packages/ui/src/Button.tsx and the build broke in four apps you have never opened. Nothing about that is careless. The agent could see the file it was editing and perhaps a dozen others, and from those dozen it built a theory of the repository that happened to be wrong.
That is the monorepo problem in one sentence. It is not that the agent is worse at large codebases. It is that it cannot hold the map, so it invents one, and an invented map is confidently wrong in exactly the places that hurt.
Why 4,000 packages breaks what worked at 40
On a small project the agent effectively reads everything. It knows where the config lives because it saw the config. It knows there is one HTTP client because it saw the one HTTP client. Correctness comes for free from coverage.
Past a few hundred files that stops. The agent now works from a sample, and it extrapolates from that sample the way anyone would. It sees apps/web and packages/ui and assumes the other 300 directories follow the same shape. It reads one tsconfig.json and assumes the rest match. It finds a Button and assumes it is the Button.
The instinct at this point is to reach for more context. A bigger window, a retrieval step that pre-loads thirty candidate files, a summary of every package pasted into the system prompt. This helps less than you expect and costs more than you want. Reading more of a repository is not the same as knowing how it is organised. The missing thing is not volume, it is orientation.
A repo map at the root
Write one file at the root that says what lives where and what is off limits. Keep it to a screen. This is the highest-value artefact in the whole setup and it takes twenty minutes.
# Repo map
pnpm workspace + Turborepo. Node 20. Package manager is pnpm. Never npm, never yarn.
## Where things live
- `apps/web` — customer Next.js app. Routing and page composition only, no business logic.
- `apps/admin` — internal console. Same stack, different auth provider.
- `packages/ui` — the only place shared React components live.
- `packages/api-client` — generated from OpenAPI. Do not edit by hand.
- `packages/db` — Prisma schema and migrations. Schema changes need a migration.
- `services/*` — deployable Go services. Separate toolchain, separate CI.
- `vendor/` — third-party source we patch. Treat as read-only.
## Off limits
- `packages/api-client/src/generated/**` — regenerate with `pnpm gen:api`
- `**/*.pb.go`, `**/*.gen.ts`
- `pnpm-lock.yaml` — only ever changed by running pnpm itself
## Commands (always scoped, never repo-wide)
- test: `pnpm --filter <pkg> test`
- typecheck: `pnpm --filter <pkg> typecheck`
- lint: `pnpm --filter <pkg> lint`
Notice what is not in there. No architecture philosophy, no history, no list of every package. A map that takes four minutes to read gets skimmed, and a skimmed map is the same as no map.
Per-package context that loads only where it applies
The root map handles orientation. Everything specific goes in a context file inside the package itself, so packages/billing/CLAUDE.md explains that money is stored in integer minor units, that Invoice is the aggregate root and nothing outside the package constructs one directly, and that the Stripe webhook handler must stay idempotent. That file is irrelevant noise when the agent is working in apps/admin, and most harnesses will only pull it in when work happens in that subtree. This is the mechanism that makes a large repo tractable: total documented context can be large as long as the context loaded per task stays small.
Scope the working directory
Start the session in packages/billing, not at the root. The effect is larger than it sounds. Search defaults narrow, so rg "TODO" returns eleven results rather than four thousand. Relative paths become unambiguous. The agent's sense of the scale of the problem matches the actual scale of the problem.
If the change genuinely spans packages, do it as two sessions rather than one wide one. That is the workflow that holds up: plan at the repo level, execute at the package level.
Make the feedback loop cheap
An agent that cannot run its own change learns nothing from making it. Worked example, with the assumptions stated so you can substitute your own: suppose a cold repo-wide turbo build takes 11 minutes, and a filtered build of one package plus its six dependencies takes 40 seconds. A typical agent session goes through twenty edit-and-verify cycles. Repo-wide that is 220 minutes, three hours and forty minutes of waiting. Filtered it is 800 seconds, thirteen minutes and twenty seconds. Same twenty cycles, same quality of feedback, and the difference is entirely in which command you told the agent to run.
So the per-package test, lint and typecheck scripts are not tidiness. They are the difference between an agent that iterates and an agent that guesses and hands you the guess.
Generated code will be "fixed"
Generated files look broken to a model. Odd formatting, disabled lint rules, thousand-line unions, comments that do not match the code. The agent finds one, decides it is a mess, tidies it, and the tests pass because the tidying was semantically fine. Then somebody runs pnpm gen:api and the work vanishes, or worse, does not vanish and now drifts from the schema.
Exclude generated directories in the ignore file the agent respects, list them in the root map under "off limits", and put a @generated header at the top of each file. Three cheap signals beat one good one, because you only need any of them to land.
Six packages export a Button
At scale you will have packages/ui, packages/admin-ui, packages/billing/components, a legacy packages/design-system and two apps all exporting something called Button. The agent picks one. It picks by proximity and by what appeared in context, not by which one you meant, and it will be right often enough that you stop checking.
You cannot prompt your way out of this. A prompt is advice given once per turn against a name collision that exists on every turn. What works is making the collision impossible: one canonical Button in packages/ui, everything else renamed to AdminActionButton or InvoiceDownloadButton, and internal components kept out of the package's exports map so they are not importable at all. Ownership encoded in the name survives context compaction. Advice does not.
Boundaries need a lint rule
The agent will add import { db } from "@acme/db" inside packages/ui without hesitating, because it solves the immediate problem and nothing in the file says otherwise. Layering rules that live in a wiki are not enforcement. Put them in eslint, either no-restricted-imports with path patterns per workspace or Nx's module boundary rule if you are on Nx, and make sure the per-package lint script runs them. The point is that the violation surfaces inside the agent's own loop, forty seconds later, where it can fix it, rather than in CI twenty minutes later where you have to.
Lockfiles and vendored code deserve the same treatment for a duller reason: a 30,000-line pnpm-lock.yaml read into context is pure cost with no information. Exclude it, exclude vendor/, and state in the map that dependency changes happen by running pnpm.
Plan high, execute low
The shape that works: one planning pass at the root with the map loaded, producing a written plan naming the packages that change, the order, and the public API delta at each boundary. Then one execution session per package, scoped to that directory, with the plan file as its brief. The plan file is the thing that carries context across sessions, which means it needs to be specific enough to act on without re-derivation.
None of this is agent-specific, and that is the part worth sitting with. Every fix here, the map, the scoped commands, the naming convention, the boundary lint, is something a new engineer on their second week also needed and probably asked for in Slack. The agent is the colleague who never asks, so the repository has to answer in advance.
Keep reading
Choosing a Model for the Task: When Cheap and Fast Wins
A task-by-task guide to when a cheap fast model wins, when it costs you an hour of cleanup, and how to test the trade-off on your own repo.
Cutting Your Token Bill Without Cutting Quality
Most token waste is context hygiene, not model choice. Where the spend actually goes, and the session habits that cut it without hurting output.
Measuring Whether the Agent Is Actually Making You Faster
Agents feel fast, and feeling fast is not the same as being fast. A two-week log, honest arithmetic, and why rework rate is the number that matters.