So a few weeks ago, I did a talk with this company called Ampersand about how I use agents to write code.
Small aside, and maybe this is a bit of a plug, but if you’re building a product that heavily relies on integrations, go use Ampersand. They make that process super easy. And no, they aren’t sponsoring this. I’m making NO money from them lol. They just have an absolutely amazing product.
Anyway! During the talk, I spent a lot of time explaining why I thought monorepos and Markdown files were basically the perfect setup for coding agents. My argument came down to two big advantages.
The first was shared context. When your database, backend, frontend, shared libraries, tests, and infrastructure all live inside the same repository, the agent has one place where it can see how everything connects.
The second was that it becomes much easier to build an entire feature as one coordinated change. If a feature touches the database, backend and frontend, the agent can update all three on the same branch, test everything together and bring the whole thing back as one pull request.
ADVANTAGE 1
One shared codebase
↓
The agent can follow how everything connects
ADVANTAGE 2
One shared source tree
↓
The agent can build and submit one coordinated change
That idea was interesting enough to spark a little discussion in the real world too. Or at least on LinkedIn. Close enough. (Btw, Dion is awesome and has a bunch of cool things to say about software engineering, so you should absolutely check him out.)
I started writing a response to him because I obviously wanted to keep making my case for monorepos. But while I was writing it, I realized those two advantages needed a lot more explanation.
Saying that a monorepo gives an agent “shared context” sounds nice, but what does that actually mean? The agent obviously can’t stuff the entire repository into one prompt. And even if it technically could, dumping tens of thousands of mostly irrelevant files into the context window would be an awful idea.
The same thing was true of the one-PR argument. Sure, keeping everything inside one repository makes it possible to build one coordinated change. But if the repository is enormous, how does the agent find every part of the application that needs to change? How do multiple agents split up the work? And how do they bring their changes back together without stepping all over each other?
So the two advantages were real. I just hadn’t done enough to explain why they were real, how they actually worked or what had to be true for us to get them.
And I figured, hey, this would be pretty cool to dig into properly. I want to take the original argument about shared context and single-PR development, apply a little mathematical rigor to it and make a more scientific case for why monorepos and Markdown files work so well for coding agents.
Along the way, we’re going to get into repository size, search space, module-specific agent chats, context rot, KV-cache reuse and how multiple agents can work across one codebase without creating complete chaos.
Or, put more simply:
ONE MONOREPO
↓
SHARED CONTEXT
↓
FOCUSED AGENT CHATS
↓
MORE EFFICIENT CONTEXT
↓
ONE COORDINATED CHANGE
So yeah, let’s get into it.
Part 1: One Feature Is Never in One Place
Before we dive into the much broader claim that monorepos are all you need, I want to build up to it by starting small. Really small. Like one-feature-request small.
Because the easiest way to understand why the structure of a codebase matters isn’t to begin with some gigantic repository diagram and start throwing around phrases like “shared context.” It’s to follow one tiny change through an actual application and see where it takes us.
That will give us something concrete to work with. Then, once we can actually see the shape of the problem, we can slowly add the diagrams, the agent behavior and eventually the math.
1.1 Let’s Start Really Small
Imagine you’re working on an application that stores customer information.
Nothing too exotic. Each customer has a name, an email address and a phone number. Somewhere in the product, there’s a customer profile page where a user can view and edit all of that information.
Then someone from the product team comes to you with a new request:
Add a billing address to the customer profile.
At first glance, this sounds almost comically easy.
The customer already has a few fields. We’re just adding one more. Somewhere in the codebase, there’s probably a customer object that looks vaguely like this:
CUSTOMER
Name
Email
Phone number
We add a billing address:
CUSTOMER
Name
Email
Phone number
Billing address ← new
Then we put a new text box on the customer profile page, connect it to the new field and move on with our lives.
From the user’s perspective, that really is the entire feature.
Yesterday, the profile page had three pieces of information:
BEFORE
Name
Email
Phone number
Today, it has four:
AFTER
Name
Email
Phone number
Billing address
One new box appeared on the screen. The user can type an address into it, click save and expect that address to still be there when they come back later.
That expectation is doing a lot of hidden work.
The address can’t merely appear on the screen. It has to be stored somewhere. It has to be connected to the correct customer. The application has to know how to read it, change it and send it back to the user. It may need to validate that the address is formatted correctly. Other parts of the application may eventually use it for invoices, taxes or payment processing.
None of that complexity is visible in the little text box.
WHAT THE USER SEES
┌─────────────────────────────┐
│ Billing address │
│ 123 Main Street │
└─────────────────────────────┘
WHAT THE APPLICATION HAS TO DO
Store it
Read it
Update it
Return it
Display it
Validate it
Possibly share it with other systems
This is one of those things that feels obvious once you say it out loud, but it’s extremely important for how we think about coding agents.
A product request is usually described in terms of what the user should experience. The agent, however, has to translate that experience into every piece of code required to make it real.
The user sees one field.
The agent has to find the path that field takes through the application.
And before we can understand why a monorepo might help with that, we first need to see just how far one innocent little billing address can travel.
1.2 The User Asked for One Thing. The Codebase Heard Six.
Let’s follow that billing address into the application.
We’ll do this slowly because the individual steps are pretty simple. The interesting part is what happens when we put all of them next to one another.
First, We Have to Store It
The address needs somewhere to live.
If we want it to survive after the user closes the page, the database needs a place to store it. That might mean adding a column to an existing customer table, creating a separate address table or writing a migration that changes the shape of the data already sitting in production.
For our tiny example, we’ll keep it simple:
CUSTOMERS TABLE
name
email
phone_number
billing_address ← new
Great. The database can now remember a billing address.
Unfortunately, nothing else in the application knows that yet.
Then We Have to Read and Update It
Somewhere in the backend, there’s probably a model that represents a customer.
That model tells the application which customer fields exist and how they should be handled. If the database has a billing_address column but the backend model doesn’t know about it, the value can sit safely in the database while the rest of the application completely ignores it.
So we add the field there too:
BACKEND CUSTOMER MODEL
name
email
phoneNumber
billingAddress ← new
Now the backend can read the address when it loads a customer. It can also update the address when the user changes it.
Two pieces down.
Then We Have to Return It
The profile page still needs some way to receive the address.
That probably happens through an API route. When the frontend asks for Customer 42, the backend needs to include the billing address in its response:
GET /customers/42
{
"name": "Ada Lovelace",
"email": "ada@example.com",
"phoneNumber": "555-0100",
"billingAddress": "123 Main Street"
}
The route that updates a customer also needs to accept the new field. Otherwise, the frontend can display an existing address but can’t save a new one.
That would be a wonderfully useless billing-address feature.
Then We Have to Describe It
The frontend needs to know what it’s receiving.
In a typed application, that usually means updating some shared customer type, API schema or generated client definition. This is the application formally saying, “A customer can now have a billing address, and this is what that value looks like.”
SHARED CUSTOMER TYPE
Customer
├── name
├── email
├── phoneNumber
└── billingAddress ← new
This may seem repetitive. We already added the field to the database and the backend model. Why are we describing it again?
Because each layer has its own job. The database describes what can be stored. The backend model describes what the server understands. The shared type describes what can move safely between different parts of the application.
They’re talking about the same billing address, but they aren’t doing the same thing.
Then We Have to Display It
Only now do we reach the part the user actually asked for.
The customer profile page needs a new input. That input needs to load the existing address, keep track of any edits and send the new value back when the user clicks save.
CUSTOMER PROFILE
Name
┌─────────────────────────────┐
│ Ada Lovelace │
└─────────────────────────────┘
Billing address
┌─────────────────────────────┐
│ 123 Main Street │
└─────────────────────────────┘
[ Save ]
This is the visible feature.
Everything before this point exists to make sure that little box isn’t lying.
Because displaying a text box is easy. Displaying a text box whose contents travel through the frontend, cross the API, reach the backend and safely land in the database is the actual job.
Finally, We Have to Test It
We should probably verify that the whole thing works.
Can we create a customer with a billing address? Can we retrieve it? Can we change it? Does the API return it? Does the form display it? Does clicking save actually preserve it?
Those tests might live in several places:
TESTS
Database migration test
Backend model test
API route test
Frontend component test
End-to-end profile test
We don’t necessarily need every one of those tests in every application. The point is that the feature has behavior at several layers, and each layer gives us a new opportunity to screw it up.
So let’s zoom out.
The original request was five words:
"Add a billing address"
But the implementation has expanded into six concrete jobs:
1. Store the address Database schema
2. Read and update the address Backend model
3. Return the address API route
4. Describe the address Shared type
5. Display the address Frontend component
6. Test the address Tests
The user asked for one thing.
The codebase heard six.
If we pull the feature apart into the pieces it actually touches, this is what we find:
"ADD A BILLING ADDRESS"
│
┌──────────────┬───────┴───────┬──────────────┐
│ │ │ │
▼ ▼ ▼ ▼
DATABASE BACKEND API SHARED
SCHEMA MODEL ROUTE TYPE
│ │ │ │
└──────────────┴───────┬───────┴──────────────┘
│
▼
FRONTEND COMPONENT
│
▼
VISIBLE FEATURE
TESTS CHECK THE ENTIRE THING
This first picture shows us the feature’s reach.
One little product request has already spread into the database, backend, API, shared types, frontend and tests.
But reach is only half the story.
These pieces aren’t merely scattered around the codebase. They also depend on one another. The address has to travel through the application, and every layer along the way needs to understand what it is.
If we rearrange those same pieces around the path the address actually takes, we get a slightly different picture:
"ADD A BILLING ADDRESS"
│
▼
DATABASE SCHEMA
Store it
│
▼
BACKEND MODEL
Read and update it
│
▼
API ROUTE
Return it
│
▼
SHARED TYPE
Describe it
│
▼
FRONTEND COMPONENT
Display it
┌──────────────────────────────────┐
│ TESTS WATCH THE WHOLE PATH │
└──────────────────────────────────┘
The first diagram shows us how far the feature spreads.
The second shows us how all of those pieces connect.
This is the picture I want us to hold onto.
The user experiences one feature, but its implementation is distributed across several connected pieces. And that word, connected, is doing a lot of work.
For a human developer, finding those pieces often relies on experience. You know roughly where the database migrations live, which service owns customer data and where the profile form is hiding. You may even remember that somebody created a second customer type three years ago for reasons no one fully understands.
A coding agent has to reconstruct that same path from the environment we give it. It has to find each relevant piece, understand how the pieces connect and make changes that agree with one another.
Missing one piece doesn’t necessarily produce a nice, obvious failure either. Sometimes the application compiles. Sometimes the form renders. Sometimes the address even appears to save, right up until the user refreshes the page and discovers that it has vanished into the void.
THE FEATURE LOOKS DONE
Form renders ✓
User can type an address ✓
Save button works ✓
Address survives a refresh ✗
Whoops.
So our little billing-address feature has already taught us something important: the difficulty of a change isn’t determined only by how much code the agent needs to write. It also depends on how many connected pieces the agent has to discover.
A feature isn’t really one file that needs editing.
It’s a path through the application.
Next, we need to talk about where the pieces along that path actually live, because the folder tree is about to make this whole thing look much cleaner than it really is.
1.3 The Folder Tree Is Lying to You a Little
So where do all the pieces of our billing-address feature actually live?
Probably in a folder tree that looks something like this:
src/
├── database/
├── backend/
├── api/
├── shared/
├── frontend/
└── tests/
This looks wonderfully organized.
Database code goes in the database folder. Backend code goes in the backend folder. Frontend code goes in the frontend folder. Tests go in the tests folder. Everyone has a home. Nobody is sleeping on the couch.
And if we expand those folders, we might find the six files involved in our feature:
src/
├── database/
│ └── customer.sql
│
├── backend/
│ └── customer.ts
│
├── api/
│ └── customer-route.ts
│
├── shared/
│ └── customer-type.ts
│
├── frontend/
│ └── customer-form.tsx
│
└── tests/
└── customer-profile.test.ts
This view is useful. It tells us where the files are stored, what kind of code each file contains and roughly where we should start looking.
But it hides the most important thing about our feature.
These files aren’t independent just because they live in different folders.
The folder tree organizes them by location:
WHERE THE FILES LIVE
database/
backend/
api/
shared/
frontend/
tests/
The feature connects them by behavior:
HOW THE FEATURE WORKS
customer.sql
↓
customer.ts
↓
customer-route.ts
↓
customer-type.ts
↓
customer-form.tsx
Those are two completely different views of the exact same code.
The first view answers:
Where is this file stored?
The second answers:
What other pieces have to agree with this file for the feature to work?
For a coding agent, the second question is usually much more important.
Imagine the agent starts in frontend/customer-form.tsx. It can see the billing-address input and the function that runs when the user clicks save. That file may point toward shared/customer-type.ts, which describes the customer object. The shared type may point toward the API contract. The API route may call the backend customer model, which eventually reads from the database.
The agent began in one folder, but the feature immediately pulled it through five of them.
STARTING POINT
frontend/customer-form.tsx
│
▼
shared/customer-type.ts
│
▼
api/customer-route.ts
│
▼
backend/customer.ts
│
▼
database/customer.sql
Then, once the implementation changes are complete, the agent still needs to find the tests that verify the path:
FEATURE PATH
database/customer.sql
│
▼
backend/customer.ts
│
▼
api/customer-route.ts
│
▼
shared/customer-type.ts
│
▼
frontend/customer-form.tsx
WATCHED BY
tests/customer-profile.test.ts
Same files. Same folders. Same feature.
We’ve only rearranged the picture.
But that rearrangement reveals something the folder tree couldn’t show us: the relationships between the files.
FOLDER VIEW FEATURE VIEW
database/ database/customer.sql
backend/ ↓
api/ backend/customer.ts
shared/ ↓
frontend/ api/customer-route.ts
tests/ ↓
shared/customer-type.ts
↓
frontend/customer-form.tsx
Shows location Shows connection
So the folder tree isn’t really lying to us. It’s answering a different question.
It gives us a map of the codebase’s physical organization. It does not give us a map of the application’s behavior.
That distinction barely matters when you already know the system. An experienced developer can look at customer-form.tsx and mentally fill in the missing path. They know there must be an API route somewhere. They know that route probably calls a customer service or model. They know the database schema is hiding in another folder. Years of working in the codebase have taught them the connections that the folder tree leaves out.
The agent doesn’t begin with that mental map.
It sees files, folders, names, imports, documentation and whatever else we place inside its environment. From those clues, it has to reconstruct the hidden structure underneath the folder tree.
That means an agent can be standing in exactly the correct folder, looking at exactly the correct file and still have only a small piece of the feature.
WHAT THE AGENT FOUND
frontend/customer-form.tsx
WHAT THE FEATURE NEEDS
database/customer.sql
backend/customer.ts
api/customer-route.ts
shared/customer-type.ts
frontend/customer-form.tsx
tests/customer-profile.test.ts
This is why finding one relevant file isn’t the same as understanding a change.
The agent has to keep asking what that file depends on, what depends on it and where the same idea appears elsewhere in the application. In our example, that idea is a billing address. In a real codebase, it might be a permission, a payment status, an analytics event or one of those booleans named isActive that somehow controls half the company.
The folder tree tells us where the code is stored.
The connections tell us how the software actually works.
And now that we can see those connections, we can finally give this structure a slightly more precise name.
1.4 Okay, Fine. Here Comes a Tiny Bit of Math.
At the end of the last section, we had two ways of looking at the same codebase.
The folder tree told us where the files were stored. The connections told us how the software actually worked.
FOLDER TREE FEATURE CONNECTIONS
database/ customer.sql
backend/ │
api/ ▼
shared/ customer.ts
frontend/ │
tests/ ▼
customer-route.ts
│
▼
customer-type.ts
│
▼
customer-form.tsx
Now I want to make that second picture slightly more precise.
Don’t worry. We aren’t about to turn this into a surprise graduate course in graph theory. We’re basically going to replace the boxes with dots, give the dots a name and congratulate ourselves for doing mathematics.
Let’s start with our six familiar files:
database/customer.sql
backend/customer.ts
api/customer-route.ts
shared/customer-type.ts
frontend/customer-form.tsx
tests/customer-profile.test.ts
For the moment, forget what each file contains. Just represent every file with a dot:
● database/customer.sql
● backend/customer.ts
● api/customer-route.ts
● shared/customer-type.ts
● frontend/customer-form.tsx
● tests/customer-profile.test.ts
Each dot is called a vertex.
A vertex is simply one thing in the system that we care about. In this example, every vertex represents one file. Later, we could choose to represent packages, services, functions or entire modules instead. The math doesn’t particularly care. It just needs us to be clear about what the dots mean.
We’ll give our six vertices short names so we don’t have to keep writing the full paths:
v₁ = database/customer.sql
v₂ = backend/customer.ts
v₃ = api/customer-route.ts
v₄ = shared/customer-type.ts
v₅ = frontend/customer-form.tsx
v₆ = tests/customer-profile.test.ts
So now we have six vertices:
v₁ v₂ v₃ v₄ v₅ v₆
● ● ● ● ● ●
On their own, these dots don’t tell us very much. They’re basically a very abstract folder listing.
The useful part comes when we draw lines between the files that directly rely on one another.
The backend customer model relies on the database schema. The API route relies on the backend model. The shared customer type has to agree with the API contract. The frontend form relies on that shared type. And the profile test checks the behavior created by those pieces.
v₁────v₂────v₃────v₄────v₅
│
v₆
Each line is called an edge.
For this example, an edge means that two files have a direct relationship relevant to the feature. One may import the other, call something inside it, use a type it defines or rely on the behavior it provides. The exact mechanism can change, but the basic idea is the same: those two files can’t be treated as completely independent when we change the billing address.
VERTICES = THE THINGS
● Files
EDGES = THE CONNECTIONS
── Imports
── Function calls
── Type references
── Data contracts
── Test dependencies
Once we have a collection of vertices and edges, we have a graph.
And this is where we finally get our tiny equation:
That looks much fancier than what it means.
G is the graph. V is the collection of vertices. E is the collection of edges connecting them.
Or, in normal-person language:
SOFTWARE GRAPH
The things in the system
+
The relationships between those things
That’s it. We did the math. Everyone can relax.
For our billing-address example, the vertices are the six files we found, and the edges are the direct relationships that make those files part of one working feature.
V = {
customer.sql,
customer.ts,
customer-route.ts,
customer-type.ts,
customer-form.tsx,
customer-profile.test.ts
}
E = {
database ↔ backend,
backend ↔ API,
API ↔ shared type,
shared type ↔ frontend,
frontend ↔ test
}
I’m using the connections in both directions here because, for now, we only care that the relationship exists. We aren’t yet trying to model the exact direction in which data moves or which file imports which. That extra precision can be useful, but we don’t need it to make the argument we’re building toward.
Now let’s zoom out again.
A real application obviously contains more than six files. It may contain hundreds, thousands or millions of vertices, all connected through imports, function calls, schemas, build rules, tests and shared contracts.
If we drew the entire thing, it might look something like this:
ENTIRE APPLICATION
○──○──○────○ ○──○──○
│ │ \ │ / │
○──○────○──○──○──○────○
│ │ │ │
○──○────○ ○──○──○──○
│ │ │
○──○────○────────○──○
Most of that graph has nothing to do with our billing address. The feature probably doesn’t care about the notification system, the analytics pipeline, the admin dashboard or the deeply cursed PDF exporter everyone is afraid to touch.
It only cares about one small region:
ENTIRE APPLICATION
○──○──○────○ ○──○──○
│ │ \ │ / │
○──○────●══●══●══●════○
│ ║ ║
○──○────● ●──○──○
│ │
○──○────○────────○──○
↑
FILES AND CONNECTIONS
TOUCHED BY THE FEATURE
That highlighted region is the part of the application the agent needs to understand for this particular task.
We can give that region a name too. If the feature is called (F), then the portion of the graph relevant to that feature is:
Again, the notation is more intimidating than the idea.
V_F is the collection of files the feature needs to inspect or change. E_F is the collection of relationships between those files that matter for the feature.
V_F = FILES RELEVANT TO THE FEATURE
database/customer.sql
backend/customer.ts
api/customer-route.ts
shared/customer-type.ts
frontend/customer-form.tsx
tests/customer-profile.test.ts
E_F = CONNECTIONS RELEVANT TO THE FEATURE
Database ↔ Backend
Backend ↔ API
API ↔ Shared type
Type ↔ Frontend
Frontend ↔ Tests
This gives us a more precise version of the idea we ended with in the last section:
A feature is a connected region inside a much larger web of code.
The agent’s job isn’t to understand every vertex in the application. That would be absurd, expensive and probably impossible for a large enough codebase.
Its job is to discover the right region.
It needs to find the files that matter, follow the connections between them and avoid wandering into the thousands of nearby files that have absolutely nothing to do with the task.
And now that we have a picture for a feature as a connected region, we can ask the question this entire article has been quietly building toward:
What happens when we draw repository boundaries through it?
1.5 Now Draw Repository Boundaries Through the Graph
At the end of the last section, we had turned our billing-address feature into a small graph:
v₁────v₂────v₃────v₄────v₅
│
v₆
Each vertex represented one file. Each edge represented a direct relationship between two files that had to agree for the feature to work.
More importantly, we learned that this little graph was only one connected region inside a much larger application:
ENTIRE APPLICATION
○──○──○────○ ○──○──○
│ │ \ │ / │
○──○────●══●══●══●════○
│ ║ ║
○──○────● ●──○──○
│ │
○──○────○────────○──○
↑
BILLING-ADDRESS FEATURE
Now let’s change exactly one thing.
We aren’t going to add more files. We aren’t going to make the feature more complicated. We aren’t going to introduce a microservice that somehow needs Kubernetes just to validate a street name.
We’re only going to draw repository boundaries around the files that are already there.
Imagine the company stores its database code in one repository, its backend and API in another, its shared contracts in a third and its web application in a fourth:
REPO A REPO B
DATABASE SCHEMA BACKEND + API
┌──────────────────┐ ┌───────────────────────┐
│ │ │ │
│ customer.sql │──────▶│ customer.ts │
│ │ │ │ │
└──────────────────┘ │ ▼ │
│ customer-route.ts │
│ │
└───────────┬───────────┘
│
▼
REPO C REPO D
SHARED CONTRACTS WEB APPLICATION
┌──────────────────┐ ┌───────────────────────┐
│ │ │ │
│ customer-type.ts │──────▶│ customer-form.tsx │
│ │ │ │ │
└──────────────────┘ │ ▼ │
│ customer-profile.test │
│ │
└───────────────────────┘
The billing-address feature itself hasn’t changed. It still touches the same database schema, backend model, API route, shared type, frontend component and test. The relationships between those pieces haven’t changed either.
All we did was draw boxes around different parts of the graph.
That sounds cosmetic, but it changes the environment the agent has to move through.
Some Edges Now Cross Repository Boundaries
Let’s flatten the picture again so the crossings are easier to see:
REPO A REPO B REPO C REPO D
v₁ │ v₂────v₃ │ v₄ │ v₅
│ │ │ │
│ │ │ v₆
│ │ │
↑ ↑ ↑
REPOSITORY REPOSITORY REPOSITORY
BOUNDARY BOUNDARY BOUNDARY
The edge between (v_2) and (v_3) stays inside Repo B. The edge between (v_5) and (v_6) stays inside Repo D.
But the other relationships now cross repository boundaries:
v₁ │ v₂────v₃ │ v₄ │ v₅
↑ ↑ ↑
CROSS CROSS CROSS
We can describe these crossings with one more small piece of notation, but let’s build it up slowly.
First, we need a way to ask a very simple question:
Which repository contains this file?
Let (r(v)) answer that question.
The (v) represents one of the vertices in our graph, which in this example means one file. The (r) acts like a little lookup function. Give it a file, and it returns the repository where that file lives.
FILE REPOSITORY LOOKUP
v₁ = customer.sql r(v₁) = Repo A
v₂ = customer.ts r(v₂) = Repo B
v₃ = customer-route.ts r(v₃) = Repo B
v₄ = customer-type.ts r(v₄) = Repo C
v₅ = customer-form.tsx r(v₅) = Repo D
v₆ = customer-profile.test.ts r(v₆) = Repo D
Now take any edge in the feature graph. We’ll call its two endpoints (u) and (v):
u ───────── vThose letters aren’t special. They just mean “the file on this end of the relationship” and “the file on the other end.”
We can look up the repository containing each endpoint:
r(u) = repository containing the first file
r(v) = repository containing the second file
If both files live in the same repository, the two lookups return the same answer:
For example, the backend model and API route both live in Repo B:
customer.ts ───── customer-route.ts
r(u) = Repo B
r(v) = Repo B
Repo B = Repo B
No repository crossing.
But if the files live in different repositories, the two lookups return different answers:
For example, the database schema lives in Repo A while the backend model lives in Repo B:
customer.sql ───── customer.ts
r(u) = Repo A
r(v) = Repo B
Repo A ≠ Repo B
Repository crossing.
So this expression:
is just the mathematical version of saying:
The two connected files live in different repositories.
Now we can collect every feature relationship where that condition is true:
I promise that equation is less offensive than it looks. Let’s read it from left to right:
C_F
The repository-crossing relationships
inside the feature
(u,v) ∈ E_F
Take a connected pair of files
from the feature graph
|
Where
r(u) ≠ r(v)
The first file and the second file
live in different repositories
Put together, the equation says:
(C_F) is the set of connected file pairs inside the feature whose files live in different repositories.
Or, even more simply:
(C_F) contains every place where the feature crosses a repository boundary.
For our billing-address feature, those crossing relationships are:
DATABASE ↔ BACKEND
API ↔ SHARED TYPE
TYPE ↔ FRONTEND
So in this toy example:
The vertical bars mean “how many things are in this set.” We found three relationships inside the billing-address feature that cross repository boundaries.
Nothing about the feature became more complicated at the code level. We still have the same six vertices and the same edges. But the agent can no longer move across the entire feature inside one coherent repository.
A Boundary Is More Than a Line on a Diagram
Suppose the agent begins in the web-application repository because that’s where the visible change needs to appear.
It finds customer-form.tsx, follows the customer type and immediately reaches the edge of its current world.
The shared type lives somewhere else.
WEB APPLICATION
customer-form.tsx
│
▼
"Customer type?"
│
▼
┌────────────────────────┐
│ END OF THIS REPOSITORY │
└────────────────────────┘
Now the agent has to discover which repository contains that type, gain access to it and understand how that repository is organized. It may encounter a different set of instructions, a different build system and a different test command.
Then it follows the API contract and crosses another boundary into the backend repository. From there, it discovers that the database schema lives in yet another repository and gets to do the whole little dance again.
Each crossing can introduce another:
Repository to discover
Checkout or workspace to open
Permission boundary to clear
Set of agent instructions to read
Branch to create
Pull request to coordinate
CI system to wait for
Package version that can drift
Not every company has every one of these problems. Some engineering environments make moving between repositories relatively painless. Others turn it into a small administrative side quest involving three Slack messages and someone named Greg who apparently owns the schema registry.
The important point is structural: repository boundaries divide the feature graph into separate working environments.
ONE CONNECTED FEATURE
●──────●──────●──────●──────●
THE SAME FEATURE ACROSS REPOSITORIES
●────│────●──────●────│────●────│────●
↑ ↑ ↑
DISCOVER COORDINATE COORDINATE
ANOTHER REPO ANOTHER REPO ANOTHER REPO
For a human team, some of this coordination is absorbed by habit. The frontend engineer knows who owns the API. The backend engineer knows which schema version the web app is using. The release process may already account for the order in which everything has to ship.
A coding agent has to recover that coordination from the environment. It needs to know that the repositories exist, understand how their versions relate and make sure the change lands in all of them without one side quietly moving ahead of the others.
This is also why the number of crossing edges should be treated carefully.
isn’t a magical equation for the number of minutes, dollars or tool calls a feature will require. One repository crossing might be nearly free in a well-integrated workspace. Another might involve permissions, separate deployments and a versioned package release that makes everyone question their career choices.
What (|C_F|) gives us is a structural count. It tells us how many relationships inside the feature cross repository boundaries. The actual cost of those crossings depends on the company’s tools, permissions, build systems and development workflow.
Still, the structural change is real.
We began with one connected feature graph. Then we drew repository boundaries through it. The feature stayed the same, but the agent’s path through the feature became fragmented.
So now let’s try the obvious experiment.
What happens if we place those same vertices, edges and dependencies back inside one repository?
1.6 Put the Same Feature Back Into One Repository
At the end of the last section, we were left with one connected feature spread across four repositories:
REPO A REPO B REPO C REPO D
v₁ │ v₂────v₃ │ v₄ │ v₅
│ │ │ │
│ │ │ v₆
The files were connected, but the environment was fragmented. Following the billing address from the database to the frontend required crossing three repository boundaries.
So let’s try the obvious experiment.
We’ll take those exact same files and place them inside one repository:
company/
├── database/
│ └── customer.sql
│
├── backend/
│ └── customer.ts
│
├── api/
│ └── customer-route.ts
│
├── shared/
│ └── customer-type.ts
│
├── frontend/
│ └── customer-form.tsx
│
└── tests/
└── customer-profile.test.ts
Notice what we didn’t do.
We didn’t delete the folders. We didn’t mash the database and frontend into one horrifying 80,000-line TypeScript file. We didn’t decide that architectural boundaries are for cowards.
The application still has a database layer, a backend, an API, shared contracts, a frontend and tests. The feature still touches six files, and those files still have to agree with one another.
We changed only the repository boundary around them:
BEFORE
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌──────────┐
│ REPO A │ │ REPO B │ │ REPO C │ │ REPO D │
│ │ │ │ │ │ │ │
│ v₁ │──│ v₂ ── v₃ │──│ v₄ │──│ v₅ ── v₆ │
│ │ │ │ │ │ │ │
└──────────┘ └─────────────┘ └──────────┘ └──────────┘
AFTER
┌──────────────────────────────────────────────────────────┐
│ COMPANY REPOSITORY │
│ │
│ v₁ ────── v₂ ────── v₃ ────── v₄ ────── v₅ │
│ │ │
│ v₆ │
│ │
└──────────────────────────────────────────────────────────┘
Same vertices.
Same edges.
One searchable source tree.
Now Every File Has the Same Repository Label
In the multirepo version, our repository lookup returned several different answers:
r(v₁) = Repo A
r(v₂) = Repo B
r(v₃) = Repo B
r(v₄) = Repo C
r(v₅) = Repo D
r(v₆) = Repo D
That’s why some connected pairs satisfied this condition:
The files on the two ends of those edges lived in different repositories.
In the monorepo version, every lookup returns the same answer:
r(v₁) = Company Repo
r(v₂) = Company Repo
r(v₃) = Company Repo
r(v₄) = Company Repo
r(v₅) = Company Repo
r(v₆) = Company Repo
Or, more compactly:
Now take any edge in the billing-address feature:
u ───────── v
Ask which repository contains each endpoint:
r(u) = Company Repo
r(v) = Company RepoThe answers always match:
That means none of the feature’s edges satisfy the repository-crossing condition anymore.
MULTIREPO
v₁ │ v₂────v₃ │ v₄ │ v₅
↑ ↑ ↑
CROSS CROSS CROSS
MONOREPO
v₁────v₂────v₃────v₄────v₅
│
v₆
No repository crossings
In our simplified example, the set of repository-crossing feature edges is now empty:
And therefore:
That does not mean the feature requires zero work.
The agent still has to update the database schema, backend model, API route, shared type, frontend component and tests. It still has to understand the relationships between them. It can still make a spectacular mess if it changes five pieces and forgets the sixth.
What disappeared are the repository boundaries cutting through that work.
WHAT STILL EXISTS
Six relevant files
Five important relationships
Different modules
Different responsibilities
Different tests
Actual engineering work
WHAT DISAPPEARED
Repository discovery between pieces
Separate checkouts
Cross-repository version drift
Multiple source branches
Multiple coordinated PRs
Repository boundaries inside the feature path
That last point is especially important.
In the multirepo version, completing the feature might require coordinated changes across four source branches and four pull requests. Those changes may need to merge in a particular order. A shared package may need to be published before another repository can consume it. One PR can sit waiting while another repository’s CI decides this is the perfect moment to rediscover flakiness.
In the monorepo version, the same source change can be represented as one coordinated diff:
ONE FEATURE BRANCH
database/customer.sql modified
backend/customer.ts modified
api/customer-route.ts modified
shared/customer-type.ts modified
frontend/customer-form.tsx modified
tests/customer-profile.test.ts modified
↓
ONE PULL REQUEST
That doesn’t guarantee that the change is correct. It does mean the agent can inspect, modify, test and submit the complete source change as one unit.
The database update and frontend update can be reviewed together. The tests can run against the exact versions of the files that will ship together. The pull request itself becomes a visible record of the full path the feature took through the application.
This Is What “Shared Context” Should Mean
This also gives us a much more precise definition of shared context.
Shared context does not mean stuffing the entire repository into one prompt and asking the model to spiritually absorb the company.
It means placing every potentially relevant file inside one environment that the agent can search, navigate and modify.
SHARED CONTEXT DOES NOT MEAN
100,000 files
↓
One gigantic prompt
↓
The context window catches fire
SHARED CONTEXT MEANS
One navigable environment
↓
Search for relevant files
↓
Follow their connections
↓
Inspect only what the task needs
The distinction matters because the model and the agent aren’t the same thing.
The model can only reason directly over the information currently placed inside its context window. The agent, however, can operate tools around that model. It can search the repository, open files, follow imports, inspect tests and gradually bring the right information into the model’s active context.
A monorepo doesn’t give the model instant knowledge of the whole application.
It gives the agent one place where that knowledge can be found.
That is the first major advantage of a monorepo for coding agents. The complete feature graph can live inside one coherent, versioned and searchable environment. When a task crosses from the frontend into the API, backend or database, the agent doesn’t have to leave that environment to keep following the path.
But we should be careful not to celebrate too early.
Putting the entire application in one repository gives the agent one world to explore. It does not tell the agent where, inside that world, the six files relevant to its current task are hiding.
And in a sufficiently large monorepo, that world can be absolutely enormous.
1.7 The Monorepo Gives Us a World, Not an Answer
We ended the last section with what sounded like a fairly clean victory.
Put the whole application in one repository and the agent gets one coherent environment. It can follow the billing address from the frontend to the shared type, through the API and backend, all the way down to the database. It can update every piece on one branch, test the pieces together and submit the entire feature as one pull request.
Lovely.
Unfortunately, we’ve solved one problem by revealing another.
Our billing-address example contained six relevant files. A real monorepo might contain 100,000.
OUR TINY EXAMPLE:
6 files in the repository
6 files relevant to the feature
REAL MONOREPO:
100,000 files in the repository
6 files relevant to the feature
The agent now has access to everything it might need, but it still has to figure out what it actually needs.
That distinction is enormous.
Available Is Not the Same as Relevant
Let’s return to the application graph from earlier.
The billing-address feature was one small connected region inside a much larger web of code:
ENTIRE MONOREPO
○──○──○────○────────○──○──○
│ │ \ │ / │ │
○──○────○──○──○──○────○──○
│ │ │ │ │
○──○────●══●══●══●════○──○
│ ║ ║ │
○──○────● ●──○────○
│ │
○──○────○────○────○──────○
↑
BILLING-ADDRESS FEATURE
Moving everything into one repository places that entire graph inside one searchable environment. It does not magically highlight the six useful vertices in purple and hand the agent a little note saying, “These ones, champ.”
The agent still has to discover the feature subgraph.
It needs to begin somewhere, inspect the surrounding code, follow useful relationships and decide which paths are worth exploring. Some paths will lead toward the database schema or API contract. Others will lead into logging utilities, analytics hooks, translation files or an abandoned experiment from 2021 named customer-v2-final-final.
STARTING FILE
frontend/customer-form.tsx
│
┌─────┼──────────┬────────────┐
▼ ▼ ▼ ▼
Types API Analytics Old component
│ │ │ │
▼ ▼ ▼ ▼
Relevant Relevant Maybe Nope
A monorepo makes all of those paths available.
It does not tell the agent which ones matter.
The Whole Repository Does Not Belong in the Prompt
There’s another easy mistake hiding here.
If shared context means the entire application lives in one repository, you might assume the obvious next step is to put the entire repository into the model’s context window.
Please do not do this.
MONOREPO
████████████████████████████████████████████████
MODEL CONTEXT
████
Even if a model technically supports a gigantic context window, filling it with every file in the company would be wasteful. Most of those files have nothing to do with the current task. They still consume tokens, compete for the model’s attention and make the relevant relationships harder to isolate.
The billing-address feature doesn’t become easier to understand because we also included the recommendation engine, the internal admin dashboard and 4,000 lines of CSS responsible for a button nobody has seen since 2019.
What the model needs is not the entire repository.
It needs the right working set.
AVAILABLE TO THE AGENT
Entire monorepo
100,000 files
Millions of tokens
│
│ Search
│ Navigate
│ Follow connections
▼
PRESENT IN MODEL CONTEXT
Relevant instructions
Relevant documentation
Relevant files
Relevant tests
Relevant history
This is the distinction we need to keep straight:
AVAILABLE TO THE AGENT
≠
PRESENT IN THE MODEL CONTEXT
The repository is the world the agent can explore.
The context window is the small part of that world the model can actively see at one time.
That’s why the agent matters. The model doesn’t sit there with the entire application permanently loaded into its brain. The agent searches, opens files, follows references, runs commands and gradually assembles the context the model needs for the current decision.
One World Is Still Better Than Four
None of this takes away from the argument we just made.
There’s still a major difference between searching one large environment and discovering that the feature continues across several separate ones.
MULTIREPO
Search Repo A
↓
Discover Repo B
↓
Find the API
↓
Discover Repo C
↓
Resolve a package version
↓
Discover Repo D
↓
Coordinate several changes
MONOREPO
Search one environment
↓
Follow the feature graph
↓
Modify the relevant files
↓
Submit one coordinated change
The monorepo keeps the full application graph available without requiring every vertex to be present in the prompt. It gives the agent one coherent place to search, one versioned state to reason about and one source change that can span the entire feature.
That is a real advantage.
But it isn’t the end of the argument.
The monorepo gives us a world, not an answer.
And the larger that world becomes, the more important its internal structure becomes. An agent dropped into 100,000 undifferentiated files isn’t meaningfully oriented just because all 100,000 files happen to share a Git root.
We need a way to divide that world into smaller neighborhoods. We need maps that tell the agent what those neighborhoods contain. And we need a search process that can begin locally, expand when necessary and avoid reading the whole damn repository every time someone asks for a billing address.
That’s where modules and Markdown files enter the story.
Part 2: Turn Modules Into Searchable Neighborhoods
At the end of Part 1, we gave our coding agent something enormously valuable: one coherent world.
The database schema, backend model, API route, shared type, frontend component and tests all live inside the same repository. The agent can search across the entire feature path, follow dependencies in either direction and change everything on one branch. We’ve removed the artificial walls that forced it to keep leaving one workspace and entering another.
There is just one tiny problem.
That world might contain 100,000 files.
THE MONOREPO
100,000 files
Millions of lines of code
Hundreds of packages
Several applications
Years of questionable decisions
One folder named "final-final-v2"
Giving an agent access to the whole application does not tell it where to begin. It merely ensures that the answer exists somewhere in the environment, which is certainly better than hiding one-third of the answer in another repository, but still leaves us with a rather large game of hide-and-seek.
Suppose the agent receives our original request:
"Add a billing address to the customer profile."
Somewhere inside the monorepo are the six files that matter. Surrounding them are authentication flows, payment providers, notification templates, analytics pipelines, internal admin tools, abandoned experiments and approximately 900 files containing the word address for completely unrelated reasons.
MONOREPO
┌──────────────────────────────────────────────────┐
│ │
│ auth/ analytics/ notifications/ │
│ │
│ payments/ internal-tools/ │
│ │
│ ┌──────────────────────┐ │
│ │ CUSTOMER FEATURE │ │
│ │ │ │
│ │ schema │ │
│ │ model │ │
│ │ route │ │
│ │ type │ │
│ │ component │ │
│ │ tests │ │
│ └──────────────────────┘ │
│ │
│ experiments/ search/ reporting/ │
│ │
└──────────────────────────────────────────────────┘
The agent does not need all 100,000 files. It needs a reliable way to find the small connected region relevant to the current task.
This is where modules become useful.
A module is a coherent region of the codebase organized around some responsibility: customers, payments, authentication, notifications, analytics or whatever other nouns your company has converted into software. The exact boundary will vary between applications, but the purpose is the same. It groups code that is more likely to change together and gives both humans and agents a reasonable place to begin looking.
monorepo/
├── customers/
├── payments/
├── authentication/
├── notifications/
├── analytics/
└── shared/
For a billing-address feature, the customers/ module is a much stronger starting point than the root of the repository. The agent may eventually discover that tax calculations live in payments/, address validation lives in shared/ or some cursed legacy export lives in analytics/. Fine. It can leave the module when the evidence tells it to.
The important thing is that it does not have to begin everywhere.
ENTIRE MONOREPO
100,000 searchable files
│
▼
CUSTOMERS MODULE
2,000 likely candidates
│
▼
CURRENT TASK CONTEXT
12 retrieved files
Notice that these are three different spaces.
AVAILABLE SPACE
Everything the agent can access
│
▼
SEARCH SPACE
Where the agent looks first
│
▼
CONTEXT SPACE
What the model is reading right now
The monorepo defines the available space. The module defines the default search space. Retrieval determines the context space.
That distinction is going to do a lot of work for us. A module is not a smaller pile of code that we dump wholesale into the prompt. It is a smaller neighborhood in which the agent begins its search. Most files remain on disk, available if needed but otherwise minding their own business.
2.1 Why a Smaller Search Space Matters
Let’s put some numbers behind the intuition.
Suppose the monorepo contains (N) searchable files. Inside it, the module most likely to contain our feature has (M) searchable files, where:
Searching from the repository root begins with a candidate universe of (N) files. Searching inside the module begins with (M).
If our monorepo contains 100,000 files and the customer module contains 2,000, the initial candidate universe becomes:
WHOLE REPOSITORY
[████████████████████████████████████████] 100,000 files
CUSTOMER MODULE
[█ ] 2,000 files
The percentage reduction is:
So the module-first search begins with 98 percent fewer candidates.
Now, this does not mean every search suddenly becomes exactly 50 times faster. That would be a wonderfully convenient conclusion and also nonsense. Modern code-search systems may use text indexes, symbol tables, abstract syntax trees, dependency graphs, embeddings or approximate nearest-neighbor retrieval. Their actual runtime depends on how those systems are implemented.
The narrower and more defensible claim is that the retrieval system begins with fewer plausible places to look. That can reduce unrelated matches and provide a much stronger prior about where the answer probably lives.
Consider the word address.
Across the entire monorepo, that search might return:
customer billing address
customer shipping address
company office address
email address
IP address
memory address
payment-provider address
browser autocomplete address
analytics event named "address_submitted"
test helper named fakeAddress()
TODO: address this later
A computer will very helpfully inform us that all of these contain the same sequence of letters. Thank you, computer.
Inside the customer module, however, the distribution of results changes. Customer profile schemas, billing fields, shipping information, API serializers and profile tests are much more likely to appear near the top. The module boundary has not solved the task, but it has made the search environment less ambiguous.
We can think of the module as a prior probability over files. Before inspecting any code, the agent starts with the belief that a customer-profile feature is more likely to live in customers/ than in analytics/.
In simplified notation, if (f) is a potentially relevant file and (M) is the most likely module, then:
Here, (U) represents the entire repository. In normal-person language, a randomly selected file from the customer module is more likely to matter to a customer feature than a randomly selected file from the whole company’s codebase.
This is not guaranteed for every file or every task. Cross-cutting features exist. Shared systems exist. Sometimes the codebase has been organized by a raccoon with write access. The point is not that the correct answer must remain inside the initial module. The point is that the module gives the agent a statistically better place to begin.
START IN THE LIKELY MODULE
│
▼
SEARCH LOCAL FILES AND SYMBOLS
│
▼
FOLLOW IMPORTS AND REFERENCES
│
┌─────┴─────┐
│ │
ENOUGH EVIDENCE? │
│ │
Yes ▼ ▼ No
Continue Expand to a
neighboring module
The search can expand outward as the feature graph reveals itself. If the customer model imports a shared address type, follow it. If the API route calls a payment service, inspect it. If the tests reveal an integration with tax calculation, cross that boundary too.
This is a search strategy, not an ownership prison.
But reducing the search space solves only half the problem. Eventually, the agent has to take some of the files it finds and place them into the model’s active context. And this is where the tempting solution becomes: “Well, the model has a giant context window. Why not just give it everything?”
Because, as it turns out, being able to fit something inside a context window does not mean the model will use it equally well.
That brings us to context rot.
2.2 A Large Context Window Is Not Permission to Be Lazy
At the end of the last section, we ran into a very tempting argument.
Modern models can accept enormous context windows. Some can ingest hundreds of thousands or even millions of tokens in a single request. So if our agent has found 2,000 potentially relevant files, why bother filtering them carefully? Why not shove the whole module into the prompt and let the model figure it out?
Because a context window tells us how much text the model can accept. It does not tell us how much text the model can use reliably.
CONTEXT WINDOW SIZE
How much text the model can technically accept
│
▼
CAPACITY LIMIT
CONTEXT QUALITY
How reliably the model can find and use
the right information inside that text
│
▼
PERFORMANCE QUESTION
Those are not the same thing. A moving truck may technically hold everything in your apartment, but that does not mean you should place your passport somewhere inside it and expect to find it quickly at the Canadian border.
The context window is a capacity limit, not a quality guarantee.
This is the idea behind something researchers have started calling context rot: as the amount of input grows, models can become less reliable at finding, interpreting and using the information buried inside it. The model has not crossed its maximum context limit. Nothing has necessarily been truncated. The answer may still be sitting right there in the prompt, surrounded by 80,000 tokens of other stuff, quietly wondering why nobody has come to pick it up.
SHORT, FOCUSED CONTEXT
Task
+
Relevant schema
+
Relevant model
+
Relevant route
+
Relevant test
│
▼
Strong signal
Little distraction
LARGE, NOISY CONTEXT
Task
+
Relevant schema
+
Relevant model
+
Relevant route
+
Relevant test
+
Hundreds of unrelated files
+
Old search results
+
Stale tool output
+
Three abandoned theories
+
A configuration file nobody understands
│
▼
Same answer
Much more haystack
In 2025, Chroma published a large study of context rot covering 18 language models and 194,480 model calls. The researchers varied the length, structure and contents of the input, then measured how reliably the models could recover and use relevant information.
The broad pattern was fairly unpleasant. As the input grew, performance generally became less reliable. The degradation was especially noticeable when the relevant information was surrounded by plausible distractors or when the connection between the question and the answer required some inference rather than a perfect keyword match.
That second point matters enormously for coding agents.
If the task says “add a billing address,” the relevant files may not contain that exact phrase. The database column might be called billing_details. The API serializer might expose invoiceContact. The frontend component might reuse a generic PostalAddressForm. The test might describe the behavior as “persists invoicing information.”
USER REQUEST
"Add a billing address"
│
▼
POSSIBLE IMPLEMENTATION LANGUAGE
billing_details
invoiceContact
PostalAddressForm
invoicing information
customer_location
Real code retrieval is rarely a clean game of finding one unique phrase in a pile of obviously unrelated text. The agent often has to infer relationships between differently named pieces of the system. The more distractors we add, the harder we make that job.
Chroma demonstrated the problem particularly clearly using LongMemEval, a benchmark in which models answer questions about information hidden inside conversation histories. The researchers compared two versions of the same tasks.
In the focused version, the model received only the small portion of history needed to answer the question, averaging roughly 300 tokens. In the full version, it received the entire conversation history, averaging roughly 113,000 tokens. The evidence needed to answer the question still existed in both versions. The difference was how much irrelevant material surrounded it.
FOCUSED INPUT
About 300 tokens
│
├── Relevant evidence
└── Very little unrelated history
│
▼
Model mainly reasons
FULL INPUT
About 113,000 tokens
│
├── Same relevant evidence
├── Unrelated history
├── Plausible distractors
└── Extra conversational debris
│
▼
Model must retrieve and reason
inside the same inference call
Across the model families Chroma tested, performance was consistently better on the focused inputs than on the full histories. The longer prompt contained more information, but the models used the smaller prompt more reliably.
That sounds paradoxical until we separate availability from attention.
MORE INFORMATION AVAILABLE
≠
MORE INFORMATION USED CORRECTLY
A model cannot reason about the useful evidence until it first identifies which evidence is useful. When we provide a focused context, much of the retrieval work has already been performed. When we provide the entire history, the model has to retrieve and reason at the same time. We have quietly turned one problem into two and then acted surprised when performance dropped.
A Tiny Mathematical Model of Context Sludge
We can make this intuition slightly more precise without pretending that language models obey one neat universal law.
Let (C) represent the total number of tokens in the model’s active context. We can divide those tokens into two rough groups:
Here, (R) represents the tokens containing information relevant to the current task. (D) represents everything else: unrelated files, stale tool output, old test logs, abandoned hypotheses and whatever other conversational sediment has accumulated along the way.
Now define the relevance density of the context as:
Since (C=R+D), we can also write:
The Greek letter (\rho), pronounced “rho,” is just a label for the fraction of the context that is relevant. No need to alert the mathematics department.
Imagine that the model needs 5,000 tokens of genuinely useful code and instructions to complete a task. If we give it those 5,000 useful tokens plus another 5,000 tokens of unrelated material, the relevance density is:
Half of the context is useful.
Now keep the useful evidence exactly the same but surround it with 45,000 irrelevant tokens:
Only 10 percent of the context is now relevant.
SAME USEFUL EVIDENCE
5,000 relevant tokens
FOCUSED CONTEXT
██████████░░░░░░░░░░
50% relevant
NOISY CONTEXT
██░░░░░░░░░░░░░░░░░
10% relevant
The model received exactly the same useful information in both cases. Nothing important was deleted, and neither prompt necessarily exceeded the model’s advertised context window. We simply buried the evidence beneath another 40,000 tokens of material the model had to inspect, reject or accidentally become distracted by.
This does not mean model accuracy is numerically equal to (\rho). A context that is 50 percent relevant does not guarantee 50 percent accuracy, and a context that is 10 percent relevant does not cause the model to become exactly five times worse. Models, regrettably, have declined to organize themselves around an equation that convenient.
The ratio measures something narrower: how concentrated the useful evidence is inside the context we constructed. If (R) remains fixed while (D) grows, then (\rho) must fall.
In normal-person language:
If we keep adding irrelevant material without adding useful evidence, the useful evidence occupies a smaller share of the model’s working environment.
The LongMemEval comparison makes the scale of this problem easier to see. The focused inputs averaged roughly 300 tokens, while the full histories averaged roughly 113,000 tokens:
The full inputs were therefore about 377 times longer on average. They still contained the evidence needed to answer the questions, but they forced the models to search through enormously more text before using that evidence.
Again, this does not mean the tasks became exactly 377 times harder. The Chroma study did not establish some universal linear relationship between prompt length and failure. The calculation tells us something simpler: the full-history condition asked the model to perform retrieval across roughly 377 times as much text before completing the same underlying reasoning task.
That is the part our architecture can control.
WITHOUT RETRIEVAL
Large available space
↓
Large active context
↓
Low relevance density
↓
Model retrieves and reasons
WITH RETRIEVAL
Large available space
↓
Small searched neighborhood
↓
Focused active context
↓
Higher relevance density
↓
Model mainly reasons
The goal is not to maximize (\rho) by deleting every token that does not directly contain the answer. Models still need instructions, surrounding code, type definitions and enough neighboring context to understand what the relevant evidence means. A context containing one isolated function may be wonderfully dense and completely useless.
The goal is to remove avoidable noise while preserving the evidence and relationships required for the task.
This gives us a more precise version of the architecture we have been building:
Give the agent broad access to the codebase, but give the model a concentrated view of the current problem.
The monorepo itself does not cause context rot. A repository can contain 100,000 files without harming the model because files sitting on disk consume no context tokens. The problem begins when the agent or its runtime starts loading too much of that repository into the model’s active context.
MONOREPO ON DISK
100,000 files
Large but harmless
Everything remains searchable
│
▼
SEARCH AND RETRIEVAL
Reject unrelated candidates
Follow useful relationships
Select current evidence
│
▼
ACTIVE MODEL CONTEXT
Small
Relevant
Current
Actually useful
This distinction is crucial. We are not trying to make the codebase physically small. We are trying to keep the model’s working view of the codebase focused.
For coding agents, context rot can appear through several related forms of pollution. Relevant files become buried beneath broad search results. Old tool outputs remain after they stop being useful. Earlier versions of files conflict with newer versions. Failed approaches continue occupying tokens. Test logs pile up. The original request receives a smaller and smaller share of the prompt while the conversation slowly becomes an archaeological record of everything the agent has ever considered.
UNMANAGED AGENT CONTEXT
Relevant code
+
Unrelated search results
+
Stale file contents
+
Old test failures
+
Abandoned hypotheses
+
Repeated instructions
+
More unrelated search results
│
▼
IMPORTANT INFORMATION BURIED IN SLUDGE
Module-first search does not magically eliminate this problem. Even a module-scoped agent can accumulate garbage over a long enough session, which is why we will eventually need to talk about persistent chats and compaction. But module scoping prevents a great deal of avoidable pollution before it enters the conversation at all.
Instead of asking the model to excavate six useful files from a landfill we created ourselves, the runtime begins in the most likely neighborhood, searches within a smaller candidate universe and loads only the strongest evidence into context.
WHOLE MONOREPO
Everything remains available
│
▼
MODULE-SCOPED SEARCH
Look in the likely neighborhood first
│
▼
RETRIEVAL
Select the strongest candidates
│
▼
ACTIVE CONTEXT
Only the evidence needed for this task
That leaves us with a clean division of responsibilities. The monorepo provides access. Modules narrow the initial search. Retrieval chooses the files. The active context contains only what the model needs right now.
There is still one missing piece, though. Saying “start in the customer module” is helpful, but the customer module might itself contain 2,000 files, several subsystems and a few architectural decisions that were apparently documented exclusively through interpretive dance.
The agent needs to know what the module owns, where its important entry points live, which tests validate it and which neighboring modules it may need to visit.
In other words, every neighborhood needs a map.
2.3 Every Neighborhood Gets a Map
So far, we’ve made the repository smaller in one very important sense. We’ve stopped treating the entire monorepo as one enormous, undifferentiated blob and started dividing it into modules.
ENTIRE MONOREPO
100,000 files
│
▼
CUSTOMERS MODULE
2,000 files
That’s a huge improvement. But 2,000 files is still 2,000 files.
If I dropped you into an unfamiliar town containing 2,000 buildings and said, “Good news, the address you need is definitely somewhere in this neighborhood,” you probably wouldn’t thank me for my incredible navigational assistance. You’d still want a map.
The agent has exactly the same problem. A module boundary tells it where to search, but it doesn’t necessarily tell it where to begin, which files matter, what the module owns, which paths are dead ends or when the task needs to cross into a neighboring module.
Without that guidance, the agent still has to reconstruct the neighborhood from scratch.
MODULE WITHOUT A MAP
Read directory names
↓
Inspect package manifests
↓
Search for suspiciously relevant symbols
↓
Follow imports
↓
Open tests
↓
Discover three similarly named implementations
↓
Pick one and pray
Sometimes that works. Sometimes the agent edits the deprecated implementation that’s been hanging around since 2022 because nobody was brave enough to delete it.
Software engineering!
What we need is a small piece of durable context that lives beside the code and answers the questions the file tree can’t answer by itself:
What does this part of the repository actually own?
Where should an agent begin for different kinds of work?
Which architectural boundaries matter?
Which neighboring modules might be affected?
How should the agent validate a change?
Which tempting files should it absolutely not touch?
That’s the job of the map.
The Map Should Live Inside the Repository
You could put this information in Confluence. You could put it in Notion. You could put it in a Google Doc called FINAL Architecture v7 ACTUALLY FINAL.
Then six months later, somebody would move a directory, replace the build system and forget that the document existed.
The important difference with a repository map isn’t merely that it’s written in Markdown. It’s that the map lives beside the system it describes.
CODE CHANGE
+
MAP CHANGE
+
CODE REVIEW
+
VERSION HISTORY
=
ONE SHARED SOURCE OF CONTEXT
That gives the map several useful properties. It can be reviewed in the same pull request as the code, evolve on the same branch and be checked by CI to make sure referenced paths and commands still exist. When an agent checks out a particular commit, it also gets the map that belongs to that version of the codebase.
The map doesn’t hover above the repository as corporate mythology. It travels with the code.
Okay, So What Do We Actually Call It?
This is where AGENTS.md enters the story. And I don’t want to just throw that filename at you as if every engineer received a secret memo about it at birth.
An AGENTS.md file is simply a Markdown document containing instructions and navigational context for coding agents. You place it inside the repository so the agent can receive useful guidance before it starts rummaging through the code.
The filename matters because coding tools have begun developing conventions for loading repository instructions into an agent’s context. Cursor supports a root AGENTS.md as a project-instructions format. Claude Code uses CLAUDE.md, but those files can import other files using @path.
So if we want one canonical map instead of maintaining two nearly identical documents that will inevitably begin disagreeing, we can use a tiny adapter:
repository/
├── AGENTS.md
└── CLAUDE.md
# CLAUDE.md
@AGENTS.md
Now the actual guidance lives in one place.
AGENTS.md
│
┌───────────┴───────────┐
▼ ▼
CURSOR CLAUDE CODE
Uses AGENTS.md CLAUDE.md imports
as project guidance the same AGENTS.md
Different tools may discover, combine and prioritize instruction files differently, so this isn’t some universal law of physics. It’s just a useful compatibility pattern.
The important idea is that the repository contains durable architectural guidance, and the coding tool can place that guidance into the context surrounding an agent’s request.
USER REQUEST
"Add international address validation"
+
REPOSITORY GUIDANCE
What the system contains
Where address work begins
Which rules apply
How changes are tested
+
LIVE CODE
Files retrieved during the task
│
▼
AGENT CONTEXT
The user doesn’t need to paste the repository’s architecture into every prompt. The repository carries it.
But there’s another problem hiding here. If the monorepo is huge, one root AGENTS.md can’t explain every module, domain, feature, entry point, dependency and validation command without becoming another gigantic document the agent has to excavate.
So we need maps at more than one level.
The Map Should Work Like a Zoom Lens
Imagine opening Google Maps and immediately seeing every highway, road, driveway, business and bathroom stall in the continental United States. It would be technically comprehensive and completely useless.
A useful map reveals detail gradually.
COUNTRY
↓
STATE
↓
CITY
↓
NEIGHBORHOOD
↓
STREET
Our repository map should behave the same way.
REPOSITORY
↓
MODULE
↓
DOMAIN OR FEATURE AREA
↓
TASK-SPECIFIC FILES
At the top level, the agent doesn’t need to know every file involved in changing a customer’s address. It only needs enough information to route the task toward the Customers module.
Once it enters Customers, it doesn’t need the complete history of the Payments system. It needs to know that address behavior lives in the Address domain and that Payments is one of the downstream consumers.
Once it enters the Address domain, it needs the specific entry points, boundaries and tests relevant to the task. Each level answers a narrower question.
ROOT MAP
Which neighborhood owns this work?
Which rules apply everywhere?
Where are the important exits?
│
▼
MODULE MAP
Which part of this neighborhood owns the behavior?
Which local boundaries matter?
Which adjacent modules depend on it?
│
▼
DOMAIN MAP
Which files should the agent inspect first?
Which tests prove the change works?
Which implementation details matter here?
│
▼
LIVE CODE
What does the system actually do right now?
This is the piece that makes the entire architecture click. The map isn’t one file containing the repository. It’s a hierarchy of increasingly local guidance.
Put the Maps Where the Boundaries Live
Imagine our monorepo is organized like this:
monorepo/
├── AGENTS.md
├── CLAUDE.md
├── customers/
│ ├── AGENTS.md
│ ├── profiles/
│ ├── preferences/
│ ├── events/
│ └── addresses/
│ ├── AGENTS.md
│ ├── create-address.ts
│ ├── update-address.ts
│ ├── validate-address.ts
│ ├── normalize-address.ts
│ ├── address-types.ts
│ ├── adapters/
│ └── tests/
├── payments/
│ └── AGENTS.md
├── tax/
│ └── AGENTS.md
├── authentication/
│ └── AGENTS.md
├── analytics/
│ └── AGENTS.md
└── shared/
└── AGENTS.md
The files form a hierarchy, so the maps follow that hierarchy.
monorepo/AGENTS.md
│
▼
customers/AGENTS.md
│
▼
customers/addresses/AGENTS.md
│
▼
Address implementation and tests
You don’t necessarily need an AGENTS.md in every folder. That would be an excellent way to replace one documentation problem with 4,000 smaller documentation problems.
You add a local map when part of the repository represents a meaningful architectural boundary. That might be a major module, service, package, domain, complicated feature area or subsystem with its own rules and validation process.
MEANINGFUL ARCHITECTURAL BOUNDARY
Owns behavior
Has local rules
Has important entry points
Has meaningful dependencies
Has separate validation
↓
Probably deserves a map
ORDINARY DIRECTORY
Groups a few related files
Has no independent responsibility
Uses its parent's rules
↓
Probably doesn't
The map hierarchy should reflect the architecture, not mechanically mirror every directory. That keeps the maps useful without turning the repository into a Russian nesting doll made entirely of Markdown.
Start With the Repository Map
The root AGENTS.md doesn’t need to explain how invoices are calculated or where customer-address validation occurs. It only needs to route work toward the right neighborhood.
# Repository Guide
## What Lives Here
This monorepo contains the customer-facing application, backend services,
shared packages, internal tools and their tests.
## Start With the Owning Module
- Customer profiles and addresses: `customers/AGENTS.md`
- Invoices and subscriptions: `payments/AGENTS.md`
- Regional tax behavior: `tax/AGENTS.md`
- Authentication and sessions: `authentication/AGENTS.md`
- Product events and reporting: `analytics/AGENTS.md`
- Cross-module primitives: `shared/AGENTS.md`
## Repository-Wide Rules
- Begin in the smallest plausible owning module.
- Follow documented dependencies before searching the entire repository.
- Don't edit generated files directly.
- Keep public type changes backward-compatible unless the task says otherwise.
- Run module validation before repository-wide validation.
## Repository-Wide Validation
- Install dependencies: `pnpm install`
- Type-check affected packages: `pnpm typecheck --filter ...`
- Test affected packages: `pnpm test --filter ...`
- Full validation: `pnpm validate`
Now imagine the user asks:
Add validation for international customer addresses.
The root map doesn’t solve the task. It does something more modest and more useful: it makes the first routing decision.
"Add international customer address validation"
│
▼
ROOT REPOSITORY MAP
│
┌───────────────┼────────────────┐
▼ ▼ ▼
CUSTOMERS PAYMENTS TAX
likely affected possibly affected
owner consumer neighbor
│
▼
BEGIN IN CUSTOMERS
The task may eventually touch Payments or Tax, but it probably shouldn’t begin there. That distinction between “owning module” and “affected neighbor” is extremely important. Without it, every cross-system feature becomes an excuse to search the entire monorepo.
The root map gives the agent a starting neighborhood. It doesn’t build a prison around it.
Then Zoom Into the Module
Inside Customers, we can provide another map:
customers/
├── AGENTS.md
├── profiles/
├── addresses/
├── preferences/
├── events/
└── tests/
# Customers Module
## Responsibilities
- Stores customer profiles
- Creates and updates customer addresses
- Manages customer communication preferences
- Publishes customer lifecycle events
## Does Not Own
- Authentication credentials
- Invoice calculation
- Regional tax rules
- Analytics aggregation
## Start Here
- Customer profile changes: `profiles/`
- Address creation and validation: `addresses/AGENTS.md`
- Communication preferences: `preferences/`
- Customer events: `events/`
## Important Neighbors
- `../payments/` consumes billing addresses.
- `../tax/` uses address jurisdiction during tax calculation.
- `../authentication/` supplies customer identity.
- `../analytics/` consumes customer lifecycle events.
## Validation
- Unit tests: `pnpm test --filter customers`
- Type checking: `pnpm typecheck --filter customers`
- Integration tests: `pnpm test:integration --filter customers`
Now the agent knows that Customers owns the address itself, while Payments and Tax consume some of its output.
AUTHENTICATION
│
supplies identity
│
▼
TAX ◄──────────── CUSTOMERS ────────────► ANALYTICS
│ owns addresses consumes events
│ │
│ │ supplies billing address
│ ▼
└───────────────── PAYMENTS
uses jurisdiction
during calculation
That graph isn’t there to reproduce every import in the repository. The live dependency graph can already do that.
The map records the relationships that change how an agent should reason about the task. If the address type changes, Payments may care. If jurisdiction behavior changes, Tax probably cares. If neither contract changes, the agent may be able to remain entirely inside Customers.
The map shows the exits without demanding that the agent take every exit.
Then Zoom One More Time
The Address domain can have its own local map if it’s complicated enough to deserve one.
# Customer Addresses
## Start Here
- Address creation: `create-address.ts`
- Address updates: `update-address.ts`
- Validation rules: `validate-address.ts`
- Normalization: `normalize-address.ts`
- Shared address types: `address-types.ts`
- External provider integrations: `adapters/`
## Architectural Rules
- Normalize addresses before validation.
- Keep provider-specific response objects inside `adapters/`.
- Don't add tax-jurisdiction logic here.
- Changes to shared address types may affect Payments and Tax.
## Validation
- Unit tests: `pnpm test --filter customer-addresses`
- Contract tests: `pnpm test:contracts --filter customer-addresses`
Now we can see the full search funnel.
100,000 REPOSITORY FILES
████████████████████████████████████████
│
│ Root map routes the task
▼
2,000 CUSTOMERS FILES
█
│
│ Module map selects Addresses
▼
120 ADDRESS FILES
▏
│
│ Domain map identifies entry points
▼
7 STARTING FILES
·
│
│ Live search tests the hypothesis
▼
3 TASK FILES
·
The exact numbers are illustrative. The shape of the process is the point.
REPOSITORY
↓ routing
MODULE
↓ routing
DOMAIN
↓ suggested starting points
LIVE INVESTIGATION
↓
WORKING SET
The map doesn’t replace search. It puts search in the right place.
Two Things Are Happening at Once
There are actually two parallel movements here. The first is easy to see: the searchable universe gets smaller. The second is subtler: the instructions become more specific.
SEARCHABLE WORLD APPLICABLE GUIDANCE
Entire repository Repository-wide rules
↓ +
Customers module Customers responsibilities
↓ +
Address domain Address conventions
↓ +
Relevant files Current task evidence
Or, laid out as one process:
BROAD SEARCH BROAD GUIDANCE
100,000 repository files Repository rules
│ │
▼ ▼
2,000 Customers files Module boundaries
│ │
▼ ▼
120 Address files Domain conventions
│ │
▼ ▼
7 starting files Preferred entry points
│ │
└──────────────┬──────────────────────┘
▼
LIVE INVESTIGATION
│
▼
3-FILE WORKING CONTEXT
This is the core idea of the map hierarchy:
The agent’s searchable world gets smaller while its instructions get more specific.
The model doesn’t need a detailed explanation of address normalization while it’s deciding whether the task belongs to Customers or Payments. And it doesn’t need a tour of the entire monorepo once it’s editing validate-address.ts.
The level of detail should match the level of the decision.
The Search Space Forms a Set of Nested Neighborhoods
We can make that idea slightly more precise.
Let’s give each layer of the search a label:
V₀ is every searchable artifact in the repository.
V₁ is everything inside the selected module.
V₂ is everything inside the selected domain.
Vꜰ is the final set of files that survives the investigation and enters the feature’s working context.
Each set sits inside the one before it. The task files are part of the Address domain, the Address domain is part of Customers and Customers is part of the monorepo.
Visually, the sets sit inside one another:
┌──────────────────── V₀: MONOREPO ────────────────────┐
│ │
│ ┌──────────── V₁: CUSTOMERS ───────────────────┐ │
│ │ │ │
│ │ ┌──────── V₂: ADDRESSES ──────────────┐ │ │
│ │ │ │ │ │
│ │ │ ┌── Vꜰ: TASK FILES ──────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────┘
So:
For our running example:
|V₀| = 100,000 repository files
|V₁| = 2,000 Customers files
|V₂| = 120 Address files
|Vꜰ| = 3 task files
The agent always retains the ability to expand outward, but it begins inside the smallest plausible set. That’s the opposite of treating every file as equally likely from the start.
The Map Creates a Starting Set
The Address map doesn’t immediately identify the final three files. It identifies seven plausible starting points.
Let:
represent the starting files recommended by the Address map.
If the Address domain contains 120 files and the map recommends seven entry points, then:
and:
The starting set therefore contains:
or roughly 5.8% of the domain.
ADDRESS DOMAIN
120 total files
STARTING SET
███████ 7
OTHER AVAILABLE FILES
█████████████████████████████████████████████ 113
That doesn’t mean the other 113 files are irrelevant. It means they don’t all deserve equal attention before the agent knows anything.
BEFORE THE MAP
120 files begin with roughly equal status
AFTER THE MAP
7 files are plausible starting points
113 files remain available if the evidence leads there
That’s a much gentler and more defensible claim than saying the map magically makes search 17 times faster. Search systems may use symbol indexes, dependency graphs, embeddings, lexical search or approximate retrieval, and their actual performance depends on the implementation.
What the map clearly does is supply a stronger prior. It tells the runtime, “These are the places where this kind of behavior usually begins.”
Then Live Search Finds the Winners
Once the agent opens those seven starting files, it can follow imports, inspect callers, read tests and compare the current implementation with the requested behavior. That investigation produces the actual working set.
MAP-PROVIDED STARTING SET
7 plausible files
│
├── Follow imports
├── Inspect callers
├── Read relevant tests
├── Check public types
└── Reject unrelated paths
│
▼
TASK WORKING SET
3 relevant files
We can describe that handoff as:
The arrow is intentionally doing more work than an equals sign. The map doesn’t mathematically determine the final files. It guides an investigation that discovers them.
ADDRESS MAP SUGGESTS
create-address.ts
update-address.ts
validate-address.ts
normalize-address.ts
address-types.ts
adapters/
tests/
│
▼
AGENT INVESTIGATES THE TASK
│
▼
ACTUAL WORKING SET
validate-address.ts
address-types.ts
tests/international-addresses.test.ts
Now those three files can enter active context. The other 117 Address files remain available on disk, as do the other 1,880 Customers files and the other 98,000 repository files.
Nothing has been deleted or hidden forever. The agent has simply stopped carrying the entire city around in its backpack.
Compare That With Blind Exploration
Without maps, the agent may still find the answer. It just has to spend more of the task figuring out where the answer might be.
WITHOUT MAP WITH MAP
Search entire repository Read root map
↓ ↓
Open broad results Enter Customers
↓ ↓
Follow unrelated imports Read Customers map
↓ ↓
Accumulate distractors Enter Addresses
↓ ↓
Discover likely owner Read Address map
↓ ↓
Search again Inspect seven starting points
↓ ↓
Eventually find task files Load three winning files
The right side isn’t guaranteed to be perfect. The map might be incomplete, the task might be mislabeled or the relevant behavior might genuinely cross five modules because somebody made a series of exciting decisions in 2019.
But the guided process begins with an architectural hypothesis. The blind process begins with a file system.
That’s a meaningful difference.
The Instructions Accumulate as the Search Narrows
The agent doesn’t discard the repository map when it enters a module. Broader instructions continue to apply while local guidance adds more detail.
For the address task, the durable context might look like:
REPOSITORY INSTRUCTIONS
+
CUSTOMERS INSTRUCTIONS
+
ADDRESS INSTRUCTIONS
+
CURRENT TASK EVIDENCE
Let’s give each layer of the agent’s context a label:
I₀ is the repository-wide guidance that applies everywhere.
I₁ is the guidance inherited from the selected module.
I₂ is the guidance inherited from the selected domain.
Iₙ is the most local set of instructions that applies to the task.
Eꜰ is the live evidence the agent retrieves while investigating the feature.
The final context combines those increasingly local instructions with the current files, tests and other evidence needed to complete the task.
Then the feature context can be pictured as:
The notation looks more dramatic than the idea is. The agent receives broad rules, increasingly local rules and current evidence.
┌──────────────────────────────────────────────┐
│ REPOSITORY GUIDANCE │
│ │
│ Shared conventions, global boundaries, │
│ repository-wide validation │
├──────────────────────────────────────────────┤
│ MODULE GUIDANCE │
│ │
│ Customers ownership, dependencies, exits │
├──────────────────────────────────────────────┤
│ DOMAIN GUIDANCE │
│ │
│ Address entry points and local rules │
├──────────────────────────────────────────────┤
│ LIVE TASK EVIDENCE │
│ │
│ Retrieved files, tests and current behavior │
└──────────────────────────────────────────────┘
The upper layers should be compact and stable because they’ll be reused frequently. The lower layers can be more specific because they’re only relevant when the agent enters that part of the repository.
And the live code remains the final source of truth.
Maps Also Control How the Search Expands
A task won’t always remain inside its starting domain. Suppose changing the address type breaks a contract consumed by Payments.
The agent now needs to leave Customers, but it shouldn’t respond by throwing away all locality and searching the entire monorepo again. It should follow a documented exit.
CUSTOMERS / ADDRESSES
│
│ Shared billing-address contract changed
▼
PAYMENTS / INVOICES
The expansion becomes:
CURRENT DOMAIN
↓
DOCUMENTED DIRECT NEIGHBOR
↓
NEIGHBOR'S LOCAL MAP
↓
NEIGHBOR'S RELEVANT FILES
Not:
CURRENT DOMAIN
↓
PANIC
↓
SEARCH EVERYTHING
This gives the agent a controlled way to cross boundaries.
TAX
▲
│ jurisdiction contract
│
AUTHENTICATION ───► CUSTOMERS ───► ANALYTICS
│
│ billing-address contract
▼
PAYMENTS
The root map identifies the major neighborhoods. The local maps identify the meaningful roads between them. The code and dependency indexes reveal the current mechanical details of those roads.
We don’t want to reproduce the entire dependency graph in Markdown. We only want to record the edges that change how work should be routed.
We’ll return to that when we talk about showing the exits.
This Is Still One Agent Following the Map
It’s worth drawing one bright line here because we haven’t introduced specialized module agents yet.
Everything we’ve described so far can happen inside a single agent session. The agent reads the repository map, chooses the most likely module, inherits that module’s guidance, narrows into the relevant domain and retrieves the files needed for the task.
ONE AGENT
Reads repository map
↓
Chooses Customers
↓
Reads Customers map
↓
Chooses Addresses
↓
Reads Address map
↓
Retrieves task files
The map is doing two jobs at once. It’s narrowing the agent’s searchable world, and it’s supplying more specific instructions as the agent moves deeper into the repository.
SEARCH GETS NARROWER GUIDANCE GETS MORE SPECIFIC
Entire repository Repository-wide rules
↓ ↓
Customers module Customers boundaries
↓ ↓
Address domain Address conventions
↓ ↓
Task files Current evidence
That’s enough for now. We don’t need multiple agents to make hierarchical maps useful, and we definitely don’t need to introduce some all-seeing orchestrator before we’ve explained what it would actually be orchestrating.
First, we need to finish understanding how maps guide retrieval, control what enters context and expose the paths into neighboring modules. Then, in the next part, we can take the neighborhoods we’ve already created and give each one its own focused agent chat.
A Map Should Point, Not Contain
This gives us the most important design rule for the rest of Part 2:
The Markdown file is a routing map, not the index.
It should contain stable information that changes how an agent navigates:
Responsibilities
Preferred entry points
Architectural boundaries
Important dependencies
Important dependents
Validation commands
Links to deeper documentation
It shouldn’t contain everything the agent could discover from the repository:
Every file
Every symbol
Every import
Every caller
Every test
Every temporary implementation detail
MAP
Small
Stable
Frequently reused
Tells the agent where to look
│
▼
SEARCH AND INDEXES
Large
Dynamic
Derived from the live repository
Find the current candidates
│
▼
WORKING CONTEXT
Smallest relevant evidence set
Used to complete the current task
If we put every mechanical detail into Markdown, the map becomes enormous, expensive to load and stale almost immediately. If we put too little into it, the agent returns to blind exploration.
The sweet spot is durable information that changes where the agent starts, what the module owns, which boundaries apply, where the search may expand and how the change should be validated. Everything else can come from live code, search indexes and dependency graphs.
And now we’ve got the structure needed for the rest of Part 2.
We can look more carefully at why the map is a router instead of an index, how it reduces blind exploration, why only the winning files should enter active context, how the context window becomes an explicit budget, why maps need to show their exits and why they must remain small enough to stay useful.
Then we can deal with the practical questions: How do you build the first map when your repository doesn’t have one? What happens when you’ve just shoved five formerly independent repositories into a monorepo and nobody fully understands the resulting creature? And once the maps exist, how do you keep them alive without appointing some poor engineer as the full-time Minister of Markdown?
Fortunately, the agents can help with that too.
2.4 The Markdown File Is a Routing Map, Not the Index
By the end of the last section, our agent has followed the repository map into the right module, picked up the instructions that apply there and found a small collection of files worth investigating. We’ve gone from an entire monorepo to a manageable working set without pretending the model can psychically identify the correct seven files from 100,000 equally available options.
That sounds a lot like search, but it’s not quite the same thing.
The Markdown map isn’t supposed to contain every file, function, class, import, test and symbol inside the module. If it did, we’d have solved the problem of searching a giant codebase by creating a second, slightly more annoying copy of the giant codebase.
That’d be extremely on-brand for software engineering. It still wouldn’t help.
The map and the index have different jobs.
THE MAP
"Invoice generation belongs to payments."
"Regional tax behavior belongs to tax."
"Billing addresses come from customers."
"Start invoice changes in create-invoice.ts."
"Validate them with the payments integration tests."
THE INDEX
"createInvoice is defined on line 84."
"It's called from these six locations."
"It imports calculateTax from this package."
"These tests reference InvoiceCreated."
"This symbol is re-exported by invoice-service.ts."
The map describes the shape of the neighborhood. The index helps the agent locate exact things inside it.
A modern repository already contains several kinds of machine-readable indexes, even if nobody calls them that. Filenames provide one way to locate code. Text search provides another. Symbol references, import graphs, abstract syntax trees, build metadata and embeddings each give the agent a different view of the same system.
REPOSITORY
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
TEXT INDEX SYMBOL INDEX IMPORT GRAPH
Which files Where's this What depends
contain this? defined or used? on what?
│ │ │
└───────────────────┼───────────────────┘
▼
RETRIEVED EVIDENCE
These indexes are great at answering precise questions. Ask a symbol index where createInvoice is defined and it’ll probably tell you. Ask an import graph which packages depend on payments, and it’ll trace the connections. Ask text search where calculateTax appears, and it’ll happily return 63 matches, including an abandoned migration from 2023 and a test fixture named definitely-do-not-use-this.
What these systems usually can’t tell you is whether payments should own tax behavior in the first place.
An import graph can show that payments calls tax. It can’t reliably explain that regional tax rules must stay inside tax/, that payments can consume the result but shouldn’t duplicate the calculation, and that any change crossing this boundary needs an integration test.
Those aren’t just facts about how the code happens to look today. They’re decisions about how the code is supposed to work.
MACHINE-RECOVERABLE STRUCTURE
payments ───── imports ─────▶ tax
HUMAN-DEFINED INTENT
Payments can request a tax calculation.
Payments shouldn't implement regional tax rules.
Tax owns jurisdiction-specific behavior.
Changes across this boundary need integration tests.
The repository can give us the first relationship. Somebody who understands the architecture has to explain the rest.
That gives us a pretty clean division of labor.
MARKDOWN MAP
Where should the agent begin?
Which module owns the behavior?
Which boundaries shouldn't it cross?
Which neighboring modules might matter?
How should it validate the change?
│
▼
SEARCH AND RETRIEVAL
Which files contain the implementation?
Where are the relevant symbols defined?
Which callers and tests touch this behavior?
What evidence confirms the suspected path?
│
▼
WORKING CONTEXT
Which instructions, files and test results
does the model actually need right now?
The Markdown map handles routing. The repository’s indexes handle retrieval. Then a final selection step decides which results are important enough to put in front of the model.
If we collapse all three into one enormous “just give the agent everything” step, we’ll spend 80,000 tokens teaching it about the analytics dashboard while asking it to fix invoice rounding.
The Map Redistributes the Search
Let’s say the feature request is:
Add VAT to invoices for customers in Germany.
Before the agent reads any repository guidance, several modules look at least vaguely plausible. Customers have countries. Payments creates invoices. Tax handles VAT. Analytics might consume invoice events. Shared probably contains some suspiciously important helper that nobody’s touched since 2022.
The agent has a request, but it doesn’t know where the work belongs yet. Its attention is spread across a pretty large chunk of the repository.
BEFORE ROUTING
customers ██████████
payments ██████████
tax ██████████
analytics ██████████
authentication ██████████
shared ██████████
internal-tools ██████████
No architectural guidance yet.
Everything looks annoyingly plausible.
These bars aren’t benchmark results or literal probabilities coming out of the model. They’re just a picture of uncertainty. Before routing, the agent doesn’t have a strong reason to prefer one neighborhood over another, so its search stays broad.
Then it reads the repository and module maps.
They explain that invoice assembly starts in payments/, regional tax behavior belongs to tax/ and billing jurisdiction comes from customers/. Analytics consumes invoice events but doesn’t own the calculation. Authentication and internal tools have no legitimate reason to join this particular adventure.
The uncertainty hasn’t disappeared. It’s become concentrated.
AFTER ROUTING
customers ███████████████
payments ███████████████████████████
tax ██████████████████
analytics ███
authentication █
shared ████
internal-tools █
Most of the search effort now falls on three modules.
The others haven't disappeared, but they're no longer equal suspects.
That’s what routing actually changes. It moves the agent from a nearly flat search across the repository toward a much smaller collection of likely locations.
SEARCH ATTENTION
BEFORE THE MAP AFTER THE MAP
customers ███████ customers █████████████
payments ███████ payments ████████████████████
tax ███████ tax ███████████████
analytics ███████ analytics ██
authentication ███████ authentication █
shared ███████ shared ███
internal-tools ███████ internal-tools █
HIGH UNCERTAINTY CONCENTRATED UNCERTAINTY
BROAD SEARCH TARGETED SEARCH
The map hasn’t proven that every relevant file lives inside those three modules. It’s simply changed the order in which the agent checks them.
That distinction matters. Routing isn’t a hard filter that makes the rest of the repository disappear. It’s a way to prioritize the most likely starting points while keeping a controlled path outward if the investigation finds something unexpected.
The original feature request can now turn into a much smaller group of searchable questions.
"Add VAT to invoices for customers in Germany"
│
▼
PAYMENTS
Where's the invoice assembled?
Where's tax added to the total?
│
┌──────────┴──────────┐
▼ ▼
TAX CUSTOMERS
Where's German VAT Where does the billing
calculated? country come from?
│ │
└──────────┬──────────┘
▼
VALIDATION
Which integration tests prove
the final invoice is correct?
The map still hasn’t handed the agent the complete implementation. It’s done something more useful: it’s turned one vague request into a sequence of precise questions that the repository’s indexes can actually answer.
If the complete repository is V, the map is M and the task is T, we can describe that first step like this:
ROUTING
(T, M) → C
C is a small candidate region inside V
Search can then inspect that candidate region and its documented neighbors to produce some evidence E:
RETRIEVAL
(T, C, repository indexes) → E
Finally, the runtime selects the part of that evidence the model actually needs, producing its working context W:
SELECTION
E → W
That notation is just a compact version of the picture we’ve already built.
BROAD UNCERTAINTY
│
▼
MAP-GUIDED ROUTING
│
▼
CONCENTRATED SEARCH
│
▼
PRECISE EVIDENCE
│
▼
SELECTED WORKING CONTEXT
The map doesn’t replace search. It changes the shape and order of the search.
Without the map, the agent starts across V, the entire repository. With the map, it starts inside C, follows a few documented connections and expands only when the evidence tells it to. Search still does the detailed work, but it doesn’t have to rediscover the entire architecture every time somebody opens a new chat.
Good Maps Contain Decisions, Not Inventories
This gives us a pretty simple rule for deciding what belongs in AGENTS.md: if the agent can cheaply and reliably recover something from the living codebase, the map probably doesn’t need to repeat it.
A manually maintained list of all 347 files in the payments module will become stale approximately nine minutes after somebody proudly finishes writing it. The same goes for copied symbol tables, exhaustive import lists and giant dependency trees. The repository already knows those things, and it updates them whenever the code changes.
The map should preserve what search can’t reliably figure out on its own: ownership, boundaries, entry points, legitimate neighboring modules, validation paths and links to deeper architectural explanations.
BAD MAP GOOD MAP
347 filenames Ownership
Every exported symbol Entry points
Every internal helper Boundaries
Copied dependency trees Neighboring modules
Pages of implementation detail Validation paths
Facts already visible in code Links to deeper explanations
A useful map is intentionally incomplete. It isn’t supposed to eliminate investigation. It’s supposed to make the investigation start somewhere intelligent.
Think about an actual city map. It doesn’t contain a photograph of every chair inside every building. It shows roads, boundaries, landmarks and connections because that’s what you need to reach the right place.
Once you arrive, you look around.
The Map Controls How Search Expands
The map also needs to tell the agent how to leave the current neighborhood without immediately reopening the entire monorepo.
Our German VAT feature starts in payments/, but the payments map tells us that tax calculations belong to tax/ and billing addresses belong to customers/. Those documented relationships become controlled exits from the module.
PAYMENTS
Invoice assembly starts here
│
┌──────────┴──────────┐
▼ ▼
TAX CUSTOMERS
VAT calculation Billing jurisdiction
Without those exits, the agent has two bad options. It can stay trapped inside payments and duplicate behavior that belongs somewhere else, or it can search the entire repository again whenever it finds an unfamiliar dependency.
The map gives it a third option: expand one documented connection at a time.
UNDIRECTED EXPANSION CONTROLLED EXPANSION
payments payments
│ │
▼ ├── tax/AGENTS.md
search the whole repo └── customers/AGENTS.md
│
▼
welcome back to 100,000 files
This is still one agent following the architecture. We haven’t introduced specialized module agents or an orchestrator yet. That’ll come later, once we’ve established what stable module context looks like and why it’s worth preserving.
For now, the point is simpler: the map doesn’t just shrink the first search. It defines how that search can safely grow again.
Finding Something Doesn’t Mean Loading It
At this point, the agent has a pretty effective navigation system. The map gets it into the right neighborhood, the indexes locate the current implementation and the documented boundaries show which neighboring modules might matter.
That can still produce a lot of evidence. A symbol search might return twelve callers. A text search might return forty tests. An import graph might expose another package that looks vaguely relevant. Computers are magnificent at answering the exact question you asked with far more information than you actually wanted.
FOUND BY SEARCH LOADED INTO CONTEXT
12 callers 2 relevant callers
40 tests 3 representative tests
18 related files 5 necessary files
1 enormous tool result 1 compact summary
So discovery can’t be the final step. Search identifies possible evidence, but the runtime still has to decide what the model actually needs to read.
The map has pointed the agent toward the right neighborhood. The indexes have located the current code inside it. Together, they’ve reduced a massive repository to a small collection of evidence that looks relevant to the task.
But “looks relevant” still isn’t the same thing as “belongs in the prompt.” Every file, instruction, search result and chunk of tool output that enters the model’s active context consumes part of a finite budget.
So the next question isn’t whether the agent can find more information. It’s how much of that information we should actually make the model carry.
2.5 The Context Window Is a Budget
At the end of the last section, our agent had finally found the right neighborhood, followed the relevant roads and pulled back a collection of files that looked useful.
Now we hit the next problem.
Finding information doesn’t make it free.
Every instruction, source file, test, search result, terminal response and chunk of conversation history has to fit inside the model’s context window. That context window may be enormous compared with what models had a few years ago, but it’s still finite. More importantly, everything inside it is competing for the model’s attention.
So the question changes. We’re no longer asking:
Can the agent find this information?
We’re asking:
Is this information valuable enough to occupy part of the prompt?
That’s a much harsher question, which is exactly why it’s useful.
Picture the Context Window Like a Suitcase
Imagine you’re packing for a two-week trip. The airline gives you one suitcase. You can fill it with clothes, chargers and medication, or you can use half of it to transport a bowling ball you found in the garage.
The bowling ball technically fits. That doesn’t make it a good packing decision.
A context window works the same way. The model has a fixed amount of space, and several different things need to fit inside it at once.
TOTAL CONTEXT WINDOW
┌──────────────────────────────────────────────┐
│ SYSTEM PROMPT + TOOL DEFINITIONS │
├──────────────────────────────────────────────┤
│ REPOSITORY + MODULE INSTRUCTIONS │
├──────────────────────────────────────────────┤
│ RETRIEVED CODE + DOCUMENTATION │
├──────────────────────────────────────────────┤
│ CONVERSATION + TOOL HISTORY │
├──────────────────────────────────────────────┤
│ SPACE FOR THE MODEL'S RESPONSE │
└──────────────────────────────────────────────┘
The code isn’t getting a private context window. The repository instructions aren’t getting one either. They’re all sharing the same suitcase with the system prompt, tool definitions, previous messages, search output and whatever the model still needs to generate next.
Let’s call the total context budget B. We can divide what goes inside it into five broad categories:
S = system prompt and tool definitions
I = repository and module instructions
F = retrieved files and documentation
H = conversation and tool history
O = space needed for the model's output
Then the basic constraint is:
S + I + F + H + O ≤ B
That’s the whole game.
The equation isn’t saying every system divides context in exactly this way, and some runtimes apply separate limits or reserve output differently. It’s just giving us a clean mental model: everything we load has a cost, and the total can’t grow forever.
Every Extra File Has an Opportunity Cost
Suppose we’ve got a hypothetical context budget of 128,000 tokens. Before loading any application code, some of that budget is already spoken for.
HYPOTHETICAL 128K CONTEXT BUDGET
System + tools 12,000
Repository instructions 4,000
Conversation history 18,000
Reserved output 14,000
───────
Available for evidence 80,000
That still sounds like a ridiculous amount of room. And honestly, it is. You could fit a small novel in there, which is probably why people look at a large context window and immediately decide that retrieval quality no longer matters.
Then the tools get involved.
EVIDENCE LOADED
Relevant implementation 22,000
Relevant tests 14,000
Useful documentation 6,000
Broad search output 18,000
Possibly related files 15,000
Old tool output 9,000
───────
Total evidence 84,000
We only had 80,000 tokens available.
Nothing especially dramatic happened. Nobody uploaded the entire Linux kernel. We loaded the implementation, some tests, a few documents and several things that looked “potentially relevant,” which is the phrase every bloated prompt uses shortly before becoming unusable.
Now the runtime has to truncate, summarize, compact or throw something away. If it removes the irrelevant search output, great. If it drops an early architectural instruction or compresses the test failure that explains the bug, less great.
WHEN THE BUDGET OVERFLOWS
Keep everything
│
├── Impossible
│
▼
Something has to change
│
├── Truncate older context
├── Summarize earlier evidence
├── Drop retrieved files
├── Reduce output space
└── Start a fresh context
This is why every additional file has an opportunity cost. Loading one more 5,000-token file doesn’t just increase the prompt by 5,000 tokens. It means 5,000 tokens of something else can no longer fit.
Maybe that “something else” is useless terminal output. Fantastic.
Maybe it’s the module boundary that says tax calculations can’t live inside payments. Whoops.
A Bigger Context Window Doesn’t Make Every Token Equally Useful
There’s another problem hiding underneath the raw token limit. Even when everything technically fits, the model still has to identify the useful evidence inside all the junk we gave it.
Imagine that the seven files needed for our VAT feature occupy 20,000 tokens. We can give the model those seven files by themselves, or bury them inside another 80,000 tokens of loosely related code.
FOCUSED CONTEXT
┌──────────────────────────────────────────────┐
│ Instructions │
├──────────────────────────────────────────────┤
│ Relevant payments code │
│ Relevant tax code │
│ Relevant customer code │
│ Relevant tests │
├──────────────────────────────────────────────┤
│ Space for reasoning and output │
└──────────────────────────────────────────────┘
BLOATED CONTEXT
┌──────────────────────────────────────────────┐
│ Instructions │
├──────────────────────────────────────────────┤
│ Relevant payments code │
│ Unrelated subscription code │
│ Relevant tax code │
│ Old migration │
│ Broad search results │
│ Relevant customer code │
│ Analytics consumers │
│ Duplicate tool output │
│ Relevant tests │
│ Abandoned debugging hypothesis │
├──────────────────────────────────────────────┤
│ Less space for reasoning and output │
└──────────────────────────────────────────────┘
The useful evidence hasn’t changed. We haven’t made the model smarter by surrounding it with more text. We’ve just made the signal harder to distinguish from the noise.
That’s what people usually mean when they talk about context rot. It isn’t one clean technical failure with one universal threshold. It’s a loose name for several problems that tend to appear as active context gets larger and messier:
Relevant information gets buried among distractors
Old tool output sticks around after it stops being useful
Earlier file versions conflict with newer ones
Abandoned hypotheses keep occupying space
The current task gets a smaller share of the prompt
Important details may appear where the model uses them less reliably
A larger context window delays some of these problems. It doesn’t magically repeal them.
MORE CONTEXT
More room for useful evidence
+
More room for irrelevant garbage
GOOD RETRIEVAL
More useful evidence
+
Less irrelevant garbage
Those aren’t the same thing.
A 200,000-token prompt containing 150,000 tokens of unrelated code isn’t impressive. It’s just an expensive way to ask the model to play Where’s Waldo with your billing logic.
Relevance Has to Be Worth Its Weight
We can make this slightly more precise.
Suppose every retrieved artifact fᵢ has two properties:
rᵢ = how useful the artifact is for the current task
cᵢ = how many context tokens the artifact costs
What we really care about isn’t relevance by itself. We care about relevance relative to cost.
VALUE PER CONTEXT TOKEN
vᵢ = rᵢ / cᵢ
A 200-token module rule that prevents the agent from putting tax logic in the wrong place may have enormous value. A 12,000-token file that contains one vaguely related helper may have very little.
HIGH CONTEXT VALUE
"Tax owns all jurisdiction-specific calculations."
Cost: tiny
Impact: prevents an architectural mistake
LOW CONTEXT VALUE
12,000-token utility file
Relevant portion: one helper on line 846
Cost: enormous
Impact: mostly makes the prompt heavier
This doesn’t mean a runtime can perfectly score every file before reading it. If it could, retrieval would be a solved problem and we could all go home. The point is that context selection should behave like a budget-allocation problem, not a file-collection contest.
If the selected working set is W, then we want the combined cost of its artifacts to stay within the available evidence budget while maximizing task relevance:
Choose W to maximize useful evidence
subject to:
total token cost of W ≤ available context budget
That’s basically a knapsack problem wearing a GitHub hoodie.
The model wants the most useful collection of evidence it can carry, but every artifact has weight. The goal isn’t to pack the suitcase until the zipper explodes. It’s to pack the things most likely to matter.
Load the Slice, Not Always the Whole File
This also changes how we should think about retrieval. A file may be relevant without every token inside it being relevant.
Suppose invoice-service.ts is 9,000 tokens long, but the feature only touches one public method and two helpers. Loading the entire file may still be necessary if those pieces depend heavily on surrounding state. But if the boundaries are clear, the runtime could begin with the relevant symbols and expand only if something’s missing.
WHOLE-FILE RETRIEVAL
invoice-service.ts
████████████████████████████████████ 9,000 tokens
TARGETED RETRIEVAL
createInvoice()
██████████ 2,200 tokens
calculateTotals()
██████ 1,300 tokens
buildInvoiceLines()
████ 900 tokens
The same principle applies to test output, logs and search results. The model probably doesn’t need 4,000 lines of a failing build. It needs the failure, the relevant stack trace and enough surrounding context to understand what happened.
RAW TOOL OUTPUT
4,000 lines
One useful error
Three warnings repeated 600 times
COMPACT TOOL OUTPUT
Relevant error
Short stack trace
Affected command
Pointer to full logs if needed
We shouldn’t blindly chop everything into tiny fragments either. Code makes sense through relationships, and an isolated function can be misleading without its types, callers or invariants. The right unit of retrieval depends on the task.
The rule is simply this: load the smallest coherent slice that preserves the meaning the agent needs.
The Working Set Should Change as the Agent Learns
The active context also doesn’t need to stay frozen for the entire task.
At the beginning, the agent may need architecture maps, entry points and a few representative files. Once it identifies the actual implementation path, some early search output has done its job and can be summarized or removed. Later, when the agent runs tests, the most useful evidence may become the changed files, the affected callers and the latest failures.
EARLY TASK
Maps
Entry points
Candidate files
Initial search results
│
▼
IMPLEMENTATION
Confirmed files
Relevant interfaces
Nearby tests
Architectural constraints
│
▼
VALIDATION
Changed files
Test failures
Build output
Final dependency checks
Context should behave more like a workbench than a storage unit. You bring forward the tools and parts needed for the current stage. You don’t leave every cardboard box you’ve ever opened sitting in the middle of the floor.
That gives us a healthier context loop:
RETRIEVE
│
▼
READ
│
▼
CONFIRM OR REJECT
│
├── Confirmed → Keep or expand
│
└── Rejected → Summarize or discard
│
▼
UPDATE THE WORKING SET
The map got us into the right neighborhood. Search found the likely evidence. Context management keeps that evidence useful as the task changes.
The Goal Isn’t the Smallest Prompt
There’s an easy way to take this argument too far.
If irrelevant context is bad, then the smallest possible prompt must be best, right?
Not necessarily.
An agent that only sees one function may miss the interface it implements, the downstream consumer it breaks or the architectural rule it violates. Cutting context too aggressively can create a different kind of blindness.
TOO LITTLE CONTEXT TOO MUCH CONTEXT
Missing dependencies Buried dependencies
Missing constraints Distracting constraints
Local correctness only Too much unrelated evidence
Fast but brittle Expensive and confused
│ │
└──────────┬────────────┘
▼
USEFUL WORKING SET
The target isn’t minimum context. It’s sufficient context.
We want enough evidence to understand the change, preserve the architecture and validate the result, but not so much that the actual task gets buried inside the repository’s collected works.
That’s why module maps matter even after retrieval begins. They give the runtime a compact collection of stable instructions that can stay in context while larger, more volatile artifacts move in and out around them.
STABLE CORE
Repository rules
Module ownership
Architectural boundaries
Validation commands
│
▼
DYNAMIC EVIDENCE
Current files
Relevant callers
Active test output
Latest tool results
The stable core keeps the agent oriented. The dynamic evidence changes as the investigation moves forward.
That gives us the basic architecture for a useful prompt:
GOOD WORKING CONTEXT
Small stable map
+
Task-specific evidence
+
Current useful history
+
Enough room to reason and respond
A context window isn’t a dumping ground for everything the agent managed to find. It’s a budget for the information the agent needs right now.
And that gives us a pretty clean system. The map keeps the agent oriented, live search finds the current evidence and the context window carries only what the agent needs for the task in front of it.
There’s just one slightly annoying question left: who the hell is going to build all these maps?
If we’re starting a brand-new repository, we can grow them alongside the code. But if we’ve just dragged twelve existing repositories into one giant monorepo, asking a human to perfectly document the entire system before the agents can help is a wonderful way to make sure nobody ever starts.
Fortunately, the agents can help build the maps too.
2.6 Build the First Map With Agents
Okay, so let’s actually build one.
And let’s not cheat by starting with a tiny repository where every folder has a sensible name, every boundary is clean and the original engineers are still around to explain what the hell they were thinking. Let’s imagine we’ve just consolidated twelve existing repositories into one monorepo. We now have thousands of files, several applications, a small civilization of shared packages, multiple databases, deployment configurations from three different eras and at least one folder that everybody is afraid to delete.
No single person fully understands the resulting system. The frontend team understands the web application, the payments team understands billing and somebody named Kevin apparently understood the deployment pipeline before leaving the company in 2022. The knowledge exists, but it’s scattered across people, code, configuration and years of accumulated engineering debris.
That creates a cold-start problem. We need maps to help agents navigate the repository, but we need someone to navigate the repository before those maps can exist.
Fortunately, the repository already contains most of the raw material.
THE REPOSITORY ALREADY CONTAINS
Files
Imports
Packages
Tests
Build rules
Public APIs
Ownership records
Deployment boundaries
│
▼
STRUCTURAL EVIDENCE
│
▼
NOT AUTOMATICALLY
│
▼
ARCHITECTURAL MEANING
The finished architecture map may not exist yet, but the evidence behind it does. It’s sitting inside package manifests, imports, public interfaces, test suites, deployment files, ownership rules and all the other mildly chaotic debris software teams leave behind while trying to ship things.
The trick is dividing the work correctly. Agents are very good at recovering mechanical facts from a repository. Humans are still better at deciding what those facts mean, which boundaries are intentional and which ones are merely the fossilized remains of a rushed migration.
AGENTS RECOVER THE FACTS
Directories
Packages
Imports
Entry points
Public interfaces
Test commands
Build relationships
Ownership metadata
Deployment configuration
│
▼
HUMANS REVIEW THE MEANING
What each module actually owns
Which boundaries are intentional
Which dependencies should exist
Which entry points developers should use
Which legacy paths should be avoided
Which rules future changes must preserve
That gives us a much more practical goal. We’re not asking an agent to discover the one true architecture and engrave it onto stone tablets before carrying it down from Mount Kubernetes. We’re asking it to gather the evidence, organize that evidence into a useful first hypothesis and make every conclusion easy for a human to inspect.
Start With an Empty Mapping Workspace
Before sending an agent spelunking through the codebase, give its findings somewhere to live. The structure doesn’t need to be elaborate. In fact, elaborate is probably a bad sign at this point because we haven’t learned enough about the repository to deserve an elaborate system.
A reasonable starting structure looks like this:
your-monorepo/
├── AGENTS.md
├── apps/
│ ├── web/
│ └── api/
├── packages/
│ ├── customers/
│ ├── payments/
│ ├── tax/
│ └── authentication/
└── docs/
└── agent-mapping/
├── repository-inventory.md
├── unresolved-boundaries.md
└── module-map-template.md
The root AGENTS.md begins as a temporary set of instructions for the mapping process. The inventory file holds the mechanical picture of the repository. The unresolved-boundaries file collects disagreements and missing information instead of quietly converting guesses into facts. The template gives every module scout the same basic output shape.
At the beginning, the root map can be extremely small:
# Repository Map
This repository is currently being mapped.
## Mapping Files
- `docs/agent-mapping/repository-inventory.md`
- `docs/agent-mapping/unresolved-boundaries.md`
- `docs/agent-mapping/module-map-template.md`
## Mapping Rules
- Cite repository evidence for every architectural claim.
- Separate observed facts from inferred conclusions.
- Do not modify application code during the mapping pass.
- Do not assume every directory is an architectural module.
- Record conflicting evidence as an open question.
- Prefer important direct relationships over exhaustive dependency graphs.
That file isn’t pretending to know the architecture yet. It simply tells every agent how to investigate it. More importantly, it prevents the first agent from deciding that packages/misc/ is a carefully designed bounded context because the folder happened to contain seventeen unrelated utilities and a README from 2019.
The overall workflow is straightforward:
MESSY MONOREPO
↓
STARTER MAPPING FILES
↓
REPOSITORY INVENTORY
↓
MODULE SCOUTS
↓
MAP SYNTHESIS
↓
HUMAN REVIEW
↓
REAL TASK TEST
↓
USEFUL FIRST MAP
Each stage narrows the uncertainty. We begin with the broad physical shape of the repository, investigate likely neighborhoods separately, combine the findings and then test whether the resulting maps help an agent perform actual work.
Ask One Agent to Inventory the Repository
The first agent shouldn’t write the architecture map. It doesn’t know enough yet, and asking for conclusions too early is how we turn folder names into corporate mythology.
Its first job is inventory. Give a repository-level agent a prompt like this:
Inspect this monorepo and build a structural inventory.
Do not modify application code.
Do not write final architecture maps yet.
Identify:
1. Top-level applications, services, packages and shared libraries
2. Package and workspace boundaries
3. Build and deployment configuration
4. Public application entry points
5. Database and schema locations
6. Test suites and validation commands
7. Ownership metadata, including CODEOWNERS
8. Important direct dependency relationships
9. Existing architecture documentation
10. Candidate architectural neighborhoods that deserve deeper investigation
For every claim, cite the exact repository evidence that supports it.
Separate your findings into:
- Observed facts
- Proposed neighborhoods
- Conflicting or ambiguous evidence
- Questions requiring human judgment
- Recommended follow-up investigations
Write the result to:
docs/agent-mapping/repository-inventory.md
This pass should stay relatively mechanical. The agent can discover that packages/payments has its own manifest, exposes a public interface, owns several database migrations and is imported by the API. It shouldn’t immediately declare that payments owns all invoice behavior, because another package called billing-core may be sitting six directories away plotting a jurisdictional dispute.
A directory name by itself is weak evidence. Developers name things badly all the time, especially when they believe the name is temporary. But several independent signals pointing toward the same boundary give us something much more useful.
WEAK EVIDENCE
Folder named payments/
│
▼
"Probably payment-related"
STRONGER EVIDENCE
payments/ directory
+
Dedicated package manifest
+
Public payment interface
+
Payment-specific tests
+
Billing Platform ownership
+
Independent deployment unit
│
▼
"Yep, this is probably a real architectural neighborhood"
The agent isn’t relying on architectural vibes. It’s combining multiple pieces of evidence that would be unlikely to align accidentally. When the folder structure, package system, test suite, ownership metadata and deployment configuration all draw roughly the same boundary, we can investigate that boundary with much more confidence.
The agent also needs an explicit search boundary. Real repositories contain enormous amounts of generated, vendored and temporary material that can consume context without teaching us much about the architecture.
USUALLY INSPECT USUALLY SKIP
Package manifests node_modules/
Public entry points Compiled output
Source imports Generated clients
Tests Coverage artifacts
Deployment configuration Vendored dependencies
CODEOWNERS Lockfile internals
Database schemas Large fixtures and snapshots
Existing architecture docs Temporary build directories
“Usually skip” doesn’t mean the agent can never inspect those areas. If a generated client reveals an important external dependency, it may be relevant. It simply means the agent shouldn’t spend twenty thousand tokens cataloging build artifacts because they happened to be nearby.
There’s also a less glamorous but important safety rule. Repositories often contain secret-bearing configuration, private certificates or credential references. The mapping prompt should tell the agent not to reproduce any of that material.
Do not print, copy or summarize secrets, credentials, private keys or
secret values.
You may record that secret-bearing configuration exists and identify
which module appears to own it, but do not include the sensitive content
in any report or map.
With those guardrails in place, the inventory might produce a first approximation like this:
MONOREPO
apps/
├── web Customer-facing frontend
├── api Public application API
└── worker Background job processing
packages/
├── customers Customer identity and profile behavior
├── payments Payment methods and transaction processing
├── tax Tax calculation and jurisdiction rules
├── authentication Sessions, tokens and access control
└── shared Mixed utilities requiring investigation
infrastructure/
├── database Shared migration tooling
├── deployment Service definitions
└── observability Logging and monitoring configuration
That still isn’t the architecture map. It’s a list of promising neighborhoods and the evidence that made them visible. Now we can stop making one agent carry the entire repository in its head and send smaller agents into each neighborhood.
Send Scouts Into the Candidate Neighborhoods
If one agent reads the entire monorepo sequentially, we’ve recreated the exact context problem we just spent several sections trying to solve. The conversation accumulates directory listings, manifests, search results, code excerpts and unresolved questions until it becomes a digital junk drawer with a very impressive token count.
Instead, use one coordinator to divide the repository into candidate neighborhoods. Then give each neighborhood to a separate scout with a narrow investigation scope.
COORDINATOR
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
CUSTOMERS SCOUT PAYMENTS SCOUT TAX SCOUT
│ │ │
▼ ▼ ▼
Local evidence Local evidence Local evidence
Public surface Public surface Public surface
Dependencies Dependencies Dependencies
Open questions Open questions Open questions
│ │ │
└──────────────────┼──────────────────┘
▼
COORDINATOR SYNTHESIS
Each scout gets the repository inventory, the mapping rules and access to the code. It doesn’t need every finding from every other module. It needs enough orientation to understand where it is, plus permission to follow direct relationships when the evidence leads outside its assigned neighborhood.
For the payments module, the prompt might look like this:
Investigate the candidate payments neighborhood in this monorepo.
Primary scope:
- packages/payments
- directly related configuration, schemas, tests and entry points
- direct dependencies of the payments package
- direct callers of its public interfaces
Determine:
1. What the module appears to own
2. What it clearly does not own
3. Its intended public entry points
4. Its direct dependencies
5. Its direct dependents
6. Database tables, schemas or migrations it controls
7. Relevant tests and validation commands
8. Ownership metadata
9. Architectural constraints already enforced by the code
10. Conflicting evidence or unresolved boundaries
Cite exact files, symbols, manifests, tests or configuration for every
conclusion.
Classify every conclusion as one of:
- OBSERVED: directly supported by repository evidence
- INFERRED: strongly suggested by multiple pieces of evidence
- UNRESOLVED: requires human judgment or additional investigation
Do not write the final AGENTS.md file.
Return a structured scout report for the coordinator.
Do not modify application code.
Do not reproduce secrets or credentials.
This classification matters because models are extremely capable of writing confident sentences about things they mostly inferred. That’s useful when the inference is labeled. It’s less charming when a guess quietly becomes an architectural rule that future agents obey with religious enthusiasm.
A scout report might say that payments owns payment-method storage and transaction execution because the public interface, database migrations and tests all support that conclusion. It might infer that invoice generation belongs elsewhere because the payments package only accepts finalized invoice totals. And it might mark refunds as unresolved because the endpoint lives in the API, the implementation lives in payments and the business rules live in a legacy billing package nobody has emotionally processed yet.
That’s exactly what we want. The scout isn’t required to eliminate ambiguity. It’s required to expose it.
Give Every Map the Same Shape
Before the coordinator turns those reports into Markdown files, we need to decide what a map actually contains. If every scout invents its own format, future agents will spend half their time rediscovering where somebody decided to hide the validation commands.
The root map and local maps serve different purposes. The root map explains the major neighborhoods and the important paths between them. A local map explains how to work safely inside one neighborhood.
ROOT MAP LOCAL MAP
Where are the major systems? What does this module own?
How do they connect? Where should an agent begin?
Which map should I read next? Which interfaces should it use?
What rules apply everywhere? What local rules must it preserve?
A local module map should follow a predictable contract:
# [Module Name]
## Purpose
Explain in two or three sentences why this module exists and which
business capability it represents.
## Owns
List the behaviors, data and decisions controlled by this module.
## Does Not Own
Name nearby responsibilities that belong to other modules.
## Public Entry Points
List the interfaces, exports, APIs or commands other modules should use.
## Important Internal Areas
Provide a short directory-level guide to the important internal regions.
Do not include an exhaustive file tree.
## Direct Dependencies
List the modules and external systems this module calls directly.
## Direct Dependents
List the known applications and modules that call this module directly.
## Data and Schemas
List tables, migrations, events and shared contracts controlled here.
## Architectural Rules
Record the boundaries future changes must preserve.
## Validation
Provide exact commands for tests, type checking, linting and builds.
## Known Traps
Record legacy paths, misleading names and common mistakes.
## Open Questions
List unresolved ownership or boundary questions.
## Evidence
Cite the files, manifests, interfaces, tests and configuration supporting
non-obvious claims.
This isn’t a requirement to fill every heading with three pages of prose. If a module doesn’t own a database schema, say so and move on. Consistent headings matter because they let an agent retrieve the kind of information it needs without rereading the entire document every time.
The most important sections are usually Owns, Does Not Own, Public Entry Points, Architectural Rules and Validation. Together, they answer the five questions an agent needs before touching code:
Where am I?
What belongs here?
What belongs somewhere else?
How am I supposed to enter?
How do I prove I didn't break it?
The Known Traps section is particularly valuable in older repositories because code reveals what exists, but it doesn’t always reveal what should still be used. If legacyTaxClient.ts remains in the repository because three customers haven’t migrated yet, the agent needs to know that new code shouldn’t treat it as the preferred path merely because search found it first.
But the map shouldn’t become a dumping ground for everything an agent discovers. We need one more filter before anything enters it:
If this fact disappeared from the current task, would a future agent still benefit from knowing it?
If the answer is yes, it may belong in the map. If the answer is no, it probably belongs in the task history instead.
BELONGS IN THE MAP DOESN'T BELONG IN THE MAP
Module ownership Current debugging notes
Public entry points Temporary branch state
Architectural boundaries One ticket's implementation plan
Validation commands Raw tool output
Known legacy traps Exhaustive file lists
Important direct relationships Every transitive dependency
Stable operational constraints Unverified speculation
That distinction keeps the maps small and durable. They should carry knowledge that improves many future investigations, not preserve every breadcrumb from one Tuesday afternoon debugging session.
Let the Maps Inherit From One Another
Once the repository has more than one map, the agent needs a simple rule for combining them. The root map provides repository-wide orientation and constraints. The nearest relevant local map adds the guidance specific to the neighborhood where the agent is working.
ROOT AGENTS.md
Repository-wide systems
Global rules
Shared validation
│
▼
packages/payments/AGENTS.md
Payments ownership
Payments entry points
Payments validation
│
▼
packages/payments/providers/AGENTS.md
Provider-specific rules
Only if this area genuinely needs its own map
The agent begins with the root map, then loads the closest relevant local map. More specific guidance can refine general guidance, but it shouldn’t silently contradict it. If the root map says internal package files must never be imported directly, a local map can name the correct public entry point, but it can’t casually announce that direct imports are fine in this neighborhood because vibes.
We also don’t need an AGENTS.md file in every directory. A local map is justified when a neighborhood has distinct ownership, public entry points, validation commands, operational constraints or architectural rules. Creating maps for every folder would produce a second filesystem made entirely of tiny Markdown files, which feels like missing the point with impressive precision.
Turn the Scout Reports Into Actual Maps
Once the scouts finish, the coordinator can compare their reports. This is where separate local investigations become a coherent repository-level picture.
Give the coordinator a synthesis prompt like this:
Synthesize the repository inventory and all module scout reports into a
first set of architecture maps.
Before drafting:
1. Compare overlapping ownership claims
2. Identify missing direct relationships
3. Flag unsupported conclusions
4. Surface disagreements between scouts
5. Add unresolved questions to:
docs/agent-mapping/unresolved-boundaries.md
Then draft:
- A root AGENTS.md for repository-wide orientation
- A local AGENTS.md for each confirmed module
Use the structure defined in:
docs/agent-mapping/module-map-template.md
The root map should include:
- Major architectural neighborhoods
- A short description of what each neighborhood owns
- Direct relationships between major neighborhoods
- Repository-wide rules and validation commands
- Links to local maps
- Known unresolved boundaries
Each local map should include:
- Purpose
- What the module owns
- What it does not own
- Public entry points
- Important internal areas
- Direct dependencies and dependents
- Data and schemas
- Architectural rules
- Exact validation commands
- Known traps
- Evidence for non-obvious conclusions
- Open questions requiring human review
Do not include exhaustive file listings.
Do not include large transitive dependency graphs.
Do not turn unresolved inferences into rules.
Do not modify application code.
The root map should stay compact. Its job is to orient an agent, identify the major neighborhoods and tell it where to look next. If it becomes a complete encyclopedia of the repository, we’ve simply moved the monorepo into Markdown and congratulated ourselves for inventing a slower filesystem.
A first root map might look like this:
# Repository Map
## Applications
- `apps/web` contains the customer-facing frontend.
- `apps/api` exposes the public API.
- `apps/worker` runs asynchronous jobs.
## Core Modules
- `packages/customers` owns customer profiles and identity records.
- `packages/payments` owns payment methods and transaction execution.
- `packages/tax` owns tax calculation and jurisdiction rules.
- `packages/authentication` owns sessions, tokens and access control.
## Important Direct Relationships
- The API calls customers, payments, tax and authentication.
- Payments consumes finalized invoice amounts but does not calculate tax.
- Tax reads customer location data through the customers public interface.
- The worker invokes payments for asynchronous transaction processing.
## Repository Rules
- Use package public entry points instead of importing internal files.
- Run package-local tests before repository-wide validation.
- Record unresolved ownership questions in
`docs/agent-mapping/unresolved-boundaries.md`.
## Local Maps
- `packages/customers/AGENTS.md`
- `packages/payments/AGENTS.md`
- `packages/tax/AGENTS.md`
- `packages/authentication/AGENTS.md`
The local payments map can then be more specific:
# Payments Module
## Purpose
The payments module manages stored payment methods and executes monetary
transactions through external payment providers.
## Owns
- Stored payment methods
- Payment authorization and capture
- Transaction status transitions
- Payment-provider integrations
## Does Not Own
- Invoice construction
- Tax calculation
- Customer identity
- User authentication
## Public Entry Points
- `packages/payments/src/index.ts`
Other modules must import payments through this file.
## Important Internal Areas
- `src/providers/` contains payment-provider implementations.
- `src/transactions/` contains transaction state transitions.
- `src/payment-methods/` manages stored payment methods.
## Direct Dependencies
- `packages/customers`
- `packages/shared/contracts`
## Direct Dependents
- `apps/api`
- `apps/worker`
## Data and Schemas
- `payment_methods`
- `payment_transactions`
Migrations live in `packages/payments/migrations/`.
## Architectural Rules
- Provider implementations remain internal to payments.
- Tax amounts must be supplied by the tax module.
- Other modules may not update payment transaction state directly.
## Validation
- `pnpm --filter payments test`
- `pnpm --filter payments typecheck`
- `pnpm --filter payments lint`
## Known Traps
- `apps/api/src/legacy-payments/` remains for compatibility and must not
be used by new code.
- Refund-policy ownership is not fully resolved.
## Open Questions
- Confirm whether refund orchestration belongs in payments or billing.
## Evidence
- Public exports: `packages/payments/src/index.ts`
- Package boundary: `packages/payments/package.json`
- Tests: `packages/payments/src/**/*.test.ts`
- Ownership: `.github/CODEOWNERS`
Notice what isn’t in there. There’s no exhaustive file tree, no complete import graph and no lovingly detailed explanation of every helper function. The map contains the information an agent needs to enter the neighborhood, make a safe plan and know where the important edges are.
Humans Review the Meaning
At this point, the agents have done most of the tedious exploration. They’ve opened manifests, traced imports, found public interfaces, located tests and compared ownership files. What they haven’t done is earn the right to define the company’s architecture by themselves.
The human review focuses on meaning:
THE AGENT CAN OBSERVE A HUMAN SHOULD CONFIRM
payments owns migrations Should payments own this data?
tax imports customers Is that dependency intentional?
refund code exists in two places Which module should own refunds?
a legacy API is widely imported Should new code keep using it?
two packages deploy together Are they one module or merely coupled?
CODEOWNERS names Billing Platform Is that ownership still current?
This review is much smaller than manually mapping the repository from scratch. The reviewer isn’t wandering through thousands of files hoping the architecture reveals itself through spiritual enlightenment. They’re evaluating specific claims, each attached to concrete evidence.
A practical review checklist looks like this:
HUMAN REVIEW CHECKLIST
[ ] Does each module description match the business meaning?
[ ] Are the proposed ownership boundaries intentional?
[ ] Are public entry points actually the ones we want agents to use?
[ ] Are legacy paths clearly marked?
[ ] Are important direct dependencies missing?
[ ] Did any scout mistake physical proximity for architectural ownership?
[ ] Are validation commands correct and runnable?
[ ] Are unresolved questions still labeled as unresolved?
[ ] Is the root map small enough to load routinely?
[ ] Are local maps detailed enough to guide real work?
The compressed rule is simple:
GENERATE FACTS
+
REVIEW MEANING
=
A USEFUL FIRST MAP
The first map doesn’t need to be perfect. It needs to be accurate enough to guide an agent toward the right neighborhood, honest enough to expose uncertainty and small enough to remain useful inside a real context window.
Tell the Agent How to Use the Maps
Creating the Markdown files isn’t enough. We also need to tell the coding agent when to read them, how to combine them with live repository evidence and what it must understand before editing code.
Otherwise, the maps can sit beautifully formatted in the repository while the agent ignores them and starts searching for filenames containing whatever noun appeared in the ticket. That’s technically a workflow. It’s just not a particularly good one.
For ordinary coding tasks, the root instructions should establish a navigation protocol like this:
Before changing code:
1. Read the root AGENTS.md.
2. Identify the most likely owning module.
3. Read that module's local AGENTS.md.
4. Follow only the direct relationships relevant to the task.
5. Inspect the live code to verify that the map is still accurate.
6. State the proposed change and validation plan.
7. Flag any missing, misleading or outdated map guidance.
Do not modify code until you can explain:
- Which module owns the behavior
- Which public entry point should be used
- Which neighboring modules are affected
- Which architectural rules apply
- Which commands will validate the change
This protocol does two useful things. First, it forces the agent to orient itself before it begins changing files. Second, it makes clear that the Markdown map guides the investigation but doesn’t replace the code itself.
ROOT MAP
+
LOCAL MAP
│
▼
CHOOSE THE RIGHT NEIGHBORHOOD
│
▼
INSPECT LIVE CODE
│
▼
VERIFY THE MAP
│
▼
PLAN AND IMPLEMENT
That verification step matters. A stale map can be more dangerous than no map because it sends the agent confidently toward the wrong answer. The agent should treat the maps as trusted orientation and the repository as current evidence.
Make the Map Prove Itself
Documentation can look extremely convincing while being completely useless. The only reliable test is to give an agent a representative task and watch whether the map helps it find the right path.
Suppose we ask an agent to investigate a bug where California tax is missing from a paid invoice. We don’t ask it to fix the bug yet. We ask it to navigate the system using the new maps and explain where the change probably belongs.
Use the repository and module AGENTS.md files to investigate this task:
"California tax is missing from some paid invoices."
Do not modify code.
Report:
1. The route you followed through the maps
2. The module where you would begin
3. The public entry point you would use
4. The neighboring modules involved
5. The specific files or symbols you would inspect
6. The validation commands you would run
7. Which map guidance influenced each decision
8. Any missing or misleading guidance you encountered
This gives us a concrete test. If the agent starts in payments because the word “paid” appears in the task, the map should redirect it toward tax ownership and then show how tax results reach invoice and payment flows. If it imports an internal tax calculator directly instead of using the public entry point, the local map should catch that too.
ROOT MAP
│
▼
TAX MODULE
│
├── Reads customer location
├── Calculates jurisdiction rules
└── Returns finalized tax amount
│
▼
INVOICE FLOW
│
▼
PAYMENTS RECEIVES FINAL TOTAL
A useful map should help the agent identify that payments processes the final amount but doesn’t decide the tax. That distinction is the entire point of the architecture, and it’s exactly the kind of distinction a raw repository search can miss when several modules contain the word tax.
Run a few tasks like this across different neighborhoods. Choose tasks that cross module boundaries, touch legacy systems and require validation commands. Each failure tells us something specific about the maps.
MAP TEST RESULT WHAT TO DO
Agent finds the right module Keep the route
Agent enters the wrong module Clarify ownership
Agent reads hundreds of files Add a better starting point
Agent uses an internal file Clarify the public entry point
Agent misses a neighbor Add the direct relationship
Agent follows a legacy path Mark the preferred path
Agent can't validate the change Add runnable commands
That’s also how we tune the agent. We don’t keep expanding one enormous master prompt every time something goes wrong. If the correction describes durable repository knowledge, we put it in the smallest relevant map where every future task can use it.
BAD TUNING LOOP
Agent makes mistake
↓
Add more words to giant prompt
↓
Prompt becomes enormous
↓
Nobody knows which instructions matter
BETTER TUNING LOOP
Agent makes mistake
↓
Identify missing repository knowledge
↓
Update the smallest relevant map
↓
Run another representative task
After each test, the agent can perform a short map review:
Review the investigation you just completed.
Identify any durable repository knowledge that was missing, wrong or
unclear in the Markdown maps.
Propose a map update only if the discovery will help future tasks.
Do not add:
- Task-specific debugging details
- Temporary implementation state
- Exhaustive file lists
- Information already obvious from the code
- Speculation that has not been verified
For every proposed update, explain:
1. What the agent previously misunderstood
2. What evidence corrected that misunderstanding
3. Which map should change
4. Why the new guidance will remain useful
Do not update the map until a human has reviewed the proposal.
The human approval requirement matters during the cold-start phase. We want the agent to notice missing guidance, but we don’t want one confused investigation to rewrite the architecture for every future task.
Know When to Stop Mapping
There’s an obvious temptation here to keep investigating until every directory, dependency and architectural mystery has been documented. That sounds responsible right up until the mapping project enters its third month and the agents still haven’t been allowed to change any code.
The first map is finished when it reliably improves navigation. It doesn’t need to explain the entire repository. It needs to give agents enough stable guidance to begin useful work without rediscovering the architecture every time.
THE FIRST MAP IS GOOD ENOUGH WHEN AN AGENT CAN:
[ ] Identify the likely owning module
[ ] Find the intended public entry point
[ ] Name the important direct neighbors
[ ] Avoid known legacy paths
[ ] Find the correct validation commands
[ ] Explain which evidence supports its route
[ ] Surface uncertainty instead of inventing an answer
After a few representative tasks, the first map stops being a plausible document and becomes a tested navigation system. It has shown that it can help an agent move from a vague request to the right neighborhood, the right interface and the right validation path without loading half the company into context.
BUILD THE FIRST MAP
↓
USE IT ON A REAL TASK
↓
OBSERVE WHERE THE AGENT STRUGGLES
↓
CORRECT THE MAP OR PROMPT
↓
RUN ANOTHER TASK
↓
REPEAT UNTIL NAVIGATION BECOMES BORING
And boring is the actual target. The agent should reliably find the right module, use the right interface and run the right validation without performing an archaeological expedition every time somebody changes a button.
That’s enough to solve the cold-start problem. The agents recover the repository’s visible structure, humans correct the architectural meaning and real tasks reveal where the maps are still weak.
Of course, the repository won’t politely freeze once we finish. New modules will appear, interfaces will move, dependencies will change and somebody will eventually add another shared folder despite everything we’ve learned.
So the next problem isn’t how to build the first map.
It’s how to keep the map alive.
2.7 Let Every Task Improve the Map
Imagine that a few weeks have passed since we built the first maps. An agent gets a seemingly ordinary task: add support for partial refunds. It reads the root map, enters the payments module, follows the approved public interface and begins tracing the existing refund flow.
According to the map, refund behavior is split between payments and a legacy billing package. According to the live code, that hasn’t been true for several months. The billing path now forwards everything into payments, and the partial-refund implementation finally removes that last bit of legacy orchestration.
The agent has changed more than a few functions.
THE CODE CHANGED
Refund orchestration moved into payments
+
THE ARCHITECTURE CHANGED
Payments now clearly owns refund behavior
If the pull request updates only the code, the map becomes stale the moment the change merges. The next agent will be told to investigate a boundary that no longer exists, wander into the legacy billing package and spend fifteen minutes rediscovering what the previous agent just learned.
This is how documentation usually dies. Nobody consciously decides that it should become wrong. The code changes on Monday, somebody creates a documentation ticket for Tuesday and that ticket spends the next eleven months enjoying a peaceful retirement at the bottom of the backlog.
So map maintenance can’t be treated as a separate cleanup project. It has to become part of finishing the work itself.
The Task Is Also a Map Test
Every time an agent uses a map, it tests the map against the current repository. The agent begins with the documented route, follows it into the code and discovers whether the map still describes reality.
Most of the time, nothing important will be wrong. The route will lead into the correct module, the public entry point will still exist and the validation commands will work. But when the task reveals a durable mismatch, the agent has found an opportunity to improve the starting point for every task that comes afterward.
MAP VERSION 1
↓
TASK USES THE MAP
↓
LIVE CODE CORRECTS OR EXTENDS THE MAP
↓
MAP VERSION 2
↓
NEXT TASK STARTS WITH BETTER CONTEXT
That creates a useful flywheel. The map guides real work, real work tests the map and the corrected map improves the next round of work. Instead of scheduling another heroic documentation sprint every six months, we let normal development continuously expose the parts of the map that are incomplete or stale.
The agent still shouldn’t freely rewrite architectural guidance whenever it learns something new. It should propose the smallest useful correction, show the evidence behind it and let the appropriate combination of automation and human review decide whether that correction belongs.
AGENT READS THE MAP
↓
AGENT WORKS THE TASK
↓
AGENT DISCOVERS DURABLE STRUCTURE
↓
AGENT PROPOSES A MAP UPDATE
↓
CODE AND MAP LAND TOGETHER
↓
THE NEXT TASK STARTS WITH A BETTER MAP
This is the operating loop that keeps the maps alive. The repository teaches the agent, the agent proposes what future agents should know and the pull-request process prevents one model’s interpretation from quietly becoming constitutional law.
Not Every Discovery Belongs in the Map
An agent working on partial refunds may discover dozens of facts. It might learn which helper calculates refundable balances, which fixture reproduces the bug, which test failed first and which log message was spectacularly unhelpful. Those facts may matter to the current task, but that doesn’t mean they deserve permanent residence in AGENTS.md.
The map should change only when the discovery affects how a future agent should navigate or reason about the module. If payments now owns all refund behavior, that belongs in the map. If invoice 8472 reproduced the bug, future agents probably don’t need that haunting them forever.
BELONGS IN THE MAP STAYS WITH THE TASK
Payments now owns refunds Bug reproduced with invoice 8472
Public entry point moved Temporary debugging logs
New validation command required One test fixture that failed
Legacy billing path deprecated Current branch implementation notes
New direct module dependency Every file touched by the change
Architectural rule changed Raw search and tool output
A simple test handles most cases:
If this fact disappeared with the current task, would a future agent still benefit from knowing it?
If the answer is yes, it may belong in the map. If the answer is no, it should remain in the task history, pull-request discussion or code itself.
There’s also a difference between correcting a misleading map and endlessly expanding a correct one. If the map names the wrong owner or points at a dead entry point, fix it. If the agent merely learns that calculateRefundableAmount() lives on line 184 of a particular file, search can rediscover that whenever it becomes relevant.
A MAP SHOULD PRESERVE
Stable ownership
Preferred entrances
Important boundaries
Direct architectural relationships
Validation routes
Known traps
A MAP SHOULD NOT PRESERVE
Every symbol
Every file
Every search result
Every debugging step
Every detail from every task
Otherwise, the routing layer slowly turns back into the repository, only with worse syntax highlighting.
Some Truth Can Be Checked Automatically
The maps contain two fundamentally different kinds of information. The first kind is mechanical: whether a path exists, which package imports another package, where a manifest lives and whether a validation command still resolves. The repository itself can answer those questions.
The second kind is semantic: what a module is responsible for, which dependency should exist, which path developers ought to prefer and whether a legacy flow is safe for new work. The repository contains evidence about those questions, but it doesn’t contain the final judgment.
This division gives us a much better maintenance system than forcing every line into the same workflow. Mechanical facts can be extracted, checked and refreshed automatically. Architectural intent should remain human-authored or at least human-approved because existence and desirability are very different things.
LIVE CODE AND BUILD METADATA
│
▼
AUTOMATIC EXTRACTION
Packages
Directories
Entry points
Imports
Test commands
Ownership metadata
│
▼
HUMAN INTERPRETATION
Responsibilities
Preferred paths
Forbidden dependencies
Migration status
Architectural intent
│
▼
REVIEWED AGENTS.md
Suppose the agent discovers that packages/payments/src/index.ts moved to packages/payments/src/public.ts. CI can verify that the old path disappeared, the new path exists and package exports now reference it. That’s a fairly mechanical correction.
Now suppose payments begins importing refund policy from billing again. A dependency scanner can prove that the edge exists. It can’t prove that the edge is a good idea, whether the architecture has intentionally changed or whether somebody took a shortcut at 2:00 a.m. and hoped nobody would notice.
That gives us the rule we’ll keep coming back to:
GENERATE FACTS
+
REVIEW MEANING
=
A MAP WE CAN TRUST
Generated facts keep the map connected to the live repository. Human review keeps whatever happens to exist in the repository from automatically becoming whatever ought to exist.
Put the Map Update in the Same Pull Request
Once an agent finishes a task, it should compare the final code against the root and local maps it used. If the architecture didn’t change and the maps remain accurate, wonderful. We don’t need to ceremonially touch a Markdown file just to prove that we believe in documentation.
But if the task changed a public entry point, module responsibility, direct dependency, preferred path or validation command, the smallest relevant map should change in the same pull request. The map update is part of the implementation because future agents will depend on it to understand the implementation.
The practical workflow looks like this:
PULL REQUEST CHANGES A MODULE
│
▼
CI COMPARES CODE WITH AGENTS.md
Do referenced paths still exist?
Do validation commands still resolve?
Did package dependencies change?
Did a public entry point move?
│
┌─────┴─────┐
│ │
NO DRIFT POSSIBLE DRIFT
│ │
▼ ▼
Continue Propose or require
review a map update
│
▼
MODULE OWNER REVIEWS
Is this merely a moved path?
Did module responsibility change?
Is the new dependency intentional?
Should the architectural rule change?
│
▼
CODE AND MAP MERGE
IN ONE CHANGE
The agent begins by reading the applicable maps. During the task, it records any structurally important fact they missed or misstated. Before opening the pull request, it proposes the smallest durable correction and cites the repository evidence behind it.
CI then checks the claims that machines are actually good at checking. It verifies paths, commands, package names, exported entry points, dependency edges and imported instruction files. The relevant module owners review claims about responsibility, intent and permitted relationships.
If the architecture changed, the code and map merge together. If it didn’t, the map stays untouched. This keeps maintenance tied to actual changes instead of producing documentation churn every time somebody renames a local variable.
A map-update backlog is documentation debt wearing a tiny reflective vest. If the architectural change merges today and the map correction becomes a future ticket, the map is already wrong during the exact period when agents are most likely to need it.
There Are Three Different Kinds of Drift
Not every stale map fails in the same way. A deleted file, a new package dependency and a changed business responsibility are all forms of drift, but they require different detection and review systems.
The easiest form is a broken reference.
LEVEL 1: BROKEN REFERENCE
The map points to a path, command or file that no longer exists.
Response:
CI should fail.
If AGENTS.md tells an agent to begin at a file that no longer exists, there isn’t much philosophical debate required. A script can check the reference, discover that it’s dead and block the change until the map points somewhere useful again.
The second form is structural drift.
LEVEL 2: STRUCTURAL DRIFT
Imports, package edges, owners or public entry points changed.
Response:
CI should flag the map for review and may propose an update.
This is slightly more complicated because structural change doesn’t always require a map change. A module may add an incidental dependency that doesn’t belong in the high-level routing layer. But if its public entry point moves or it gains an important direct architectural neighbor, the map probably needs review.
The third form is semantic drift.
LEVEL 3: SEMANTIC DRIFT
The code still exists, but the module's responsibility, preferred path
or architectural intent changed.
Response:
A human owner must decide how the map should change.
This is the dangerous one because every path can still resolve while the guidance has become conceptually wrong. The payments package may still expose a refund interface, but the organization may have decided that billing owns refund policy. No path-existence check can tell us whether that responsibility shift is intentional.
CAN A MACHINE CHECK IT?
"Does this path exist?"
│
▼
YES
"Should this module own refunds?"
│
▼
NOT WITHOUT HUMAN JUDGMENT
Automation can detect evidence of semantic drift. It can notice that responsibilities moved, dependencies changed or several tasks keep correcting the same route. It can even propose revised wording. What it shouldn’t do is silently rewrite architectural intent and merge the result because the prose sounded confident.
Give the Agent an End-of-Task Map Check
The easiest way to make this operational is to add one short review step after the implementation and validation are complete. The agent compares what it learned against the maps it used and proposes only the durable corrections.
Review the task you just completed and compare the final code with the
applicable root and local AGENTS.md files.
Identify any durable repository knowledge that is now missing, wrong or
misleading.
Propose a map update only if the discovery changes:
- Module ownership
- Public entry points
- Direct architectural relationships
- Preferred or forbidden paths
- Stable architectural rules
- Validation commands
- Durable operational constraints
Do not add:
- Task-specific debugging notes
- Temporary implementation state
- Raw tool output
- Exhaustive file or symbol lists
- Facts already obvious from the code
- Unverified speculation
For each proposed update, explain:
1. What changed
2. Which repository evidence proves it
3. Which map should change
4. Why future agents will benefit
5. Whether the update is mechanical or semantic
Do not silently change semantic guidance.
Request review from the relevant module owner.
That prompt gives the agent permission to improve the map without giving it permission to turn every completed task into a memoir. It also forces the agent to distinguish between a mechanical correction that CI can verify and a semantic correction that needs an owner.
The pull request can then include a compact check:
MAP MAINTENANCE CHECK
[ ] Did module ownership change?
[ ] Did a public entry point move?
[ ] Did a direct dependency change?
[ ] Did a preferred or legacy path change?
[ ] Did validation change?
[ ] Did this task reveal misleading guidance?
[ ] If yes, is the smallest relevant map updated?
[ ] Are semantic changes approved by the module owner?
This shouldn’t become another bureaucratic form everyone clicks through without reading. Most pull requests will answer no to most of these questions. Its job is simply to catch the changes that would make future agents start from the wrong assumptions.
Task-Driven Updates Still Need an Occasional Audit
Normal development will keep active modules reasonably healthy because agents repeatedly test those maps against live work. But not every neighborhood receives regular changes, and some forms of drift happen slowly across many pull requests. No individual change looks architectural, yet six months later the map describes a system that quietly stopped existing.
A lightweight scheduled audit can catch those gaps. An agent can periodically verify referenced paths, validation commands, public entry points, package relationships and ownership metadata. It can also flag maps that have grown suspiciously large, local maps that duplicate root guidance and modules that now appear to have distinct responsibilities but still lack their own maps.
PERIODIC MAP AUDIT
Broken paths
Invalid commands
Moved entry points
New package edges
Ownership changes
Contradictory guidance
Bloated local maps
Unmapped architectural neighborhoods
│
▼
MECHANICAL FIX OR HUMAN REVIEW
Semantic claims still need maintainers. A map can pass every automated check while describing a responsibility the team no longer recognizes. The audit should surface that question to the people who own the module, not ask CI to develop organizational intuition.
This doesn’t require a quarterly Map Governance Council with ceremonial robes. A scheduled agent audit, a small report and targeted review from the affected module owners will catch most of the drift that normal task-driven maintenance misses.
The Map Becomes Part of the Repository’s Operating System
At this point, the system is fairly simple. The root map tells the agent where the major neighborhoods are. The local map explains the stable rules inside the relevant neighborhood. Live search provides current mechanical detail, and the work itself reveals when the stable guidance needs to change.
ROOT AGENTS.md
Repository-wide entrance
↓
MODULE AGENTS.md
Stable neighborhood map
↓
LIVE SEARCH AND INDEXES
Current mechanical detail
↓
HUMAN-REVIEWED UPDATES
Maps remain useful instead of becoming archaeology
The Markdown files work precisely because they don’t try to replace the repository. They preserve the small amount of stable knowledge that helps an agent enter the right place, while live tools recover whatever current detail the task actually needs.
Every task then becomes a small feedback loop. Agents use the maps, code tests the maps, CI protects mechanical truth and module owners protect architectural meaning. The result isn’t perfect documentation. It’s something much more useful: a navigation system that improves as the repository evolves.
We’ve now organized the codebase into neighborhoods, given those neighborhoods maps and built a process that keeps the maps alive. But we’re still assuming that one giant agent conversation should travel through all of them.
That’s the next thing we need to fix.
Because once the repository has stable neighborhoods, the obvious move is to give each neighborhood its own agent chat.
Part 3: Give Each Neighborhood Its Own Specialist Agent
We’ve now organized the codebase into neighborhoods, given those neighborhoods maps and built a process that keeps the maps alive. But we’re still assuming that one giant agent conversation should travel through all of them.
That’s the next thing we need to fix.
Because once the repository has stable neighborhoods, the obvious move is to give each neighborhood its own specialist agent.
One Giant Conversation Becomes a Junk Drawer
Imagine opening one agent conversation on Monday morning and asking it to fix a checkout bug. The agent reads the repository map, explores payments, inspects a few tests and eventually makes the change.
Then somebody asks it to update an authentication flow. The same conversation now accumulates identity models, session rules, middleware conventions and whatever horrifying thing your company did with OAuth in 2021.
By Wednesday, the conversation has also visited tax calculation, invoice rendering and the React component responsible for a button that apparently controls all revenue.
The agent technically has more context than it did on Monday. That sounds useful until we look at what the context actually contains.
ONE GIANT AGENT CONVERSATION
Repository instructions
Payments map
Checkout investigation
Refund test output
Authentication map
Session debugging
OAuth decisions
Tax rules
Invoice rendering
Frontend component history
Current task
This isn’t a coherent working memory. It’s an expensive junk drawer.
The problem isn’t only that the conversation gets longer. The useful information for the current task becomes surrounded by unrelated instructions, old searches, test output and decisions from other parts of the application. An agent working on refunds now has to reason around an authentication migration, frontend debugging logs and a detailed argument about tax rounding that seemed extremely important twelve prompts ago.
Context windows can be large, but large doesn’t mean selective. If we keep pouring unrelated work into the same conversation, the model still has to process that material and decide whether any of it matters.
We divided the repository into neighborhoods.
Now we need to divide the work the same way.
Repository Space and Conversation Space Are Different Problems
The Markdown maps from Part 2 reduce the repository search problem. They help an agent begin in the right module, find the approved interface and understand which neighboring systems may matter.
But a good repository map doesn’t automatically create a good conversation.
REPOSITORY PROBLEM
Where should the agent look?
CONVERSATION PROBLEM
What should the agent carry while it works?
Those questions are related, but they aren’t identical. A root map may correctly route an agent into payments while the conversation still contains forty thousand tokens of authentication work, frontend debugging and completed tax tasks.
The next layer of the design is therefore conversational. Instead of sending every task through one enormous working context, we create reusable specialist agents around the same module boundaries we’ve already established.
MONOREPO
├── PAYMENTS
│ ├── AGENTS.md
│ └── Payments specialist
│
├── TAX
│ ├── AGENTS.md
│ └── Tax specialist
│
├── IDENTITY
│ ├── AGENTS.md
│ └── Identity specialist
│
└── FRONTEND
├── AGENTS.md
└── Frontend specialist
These specialists still have access to the monorepo. The payments agent isn’t trapped inside packages/payments and forced to pretend that invoices, taxes and customers don’t exist.
Payments is simply its default neighborhood. It starts with the payments map, searches payments first and crosses a boundary only when the task requires it.
The specialization comes from a better starting point, not artificial blindness.
This Is Something Current Coding Tools Can Actually Do
This isn’t a proposal for some theoretical agent platform that may arrive once the labs finish spending the GDP of a small nation.
Cursor and Claude Code already support most of this structure.
Cursor lets a parent agent delegate work to custom subagents. Each subagent operates in its own context window, can be given its own instructions and tools and returns its result to the parent. Cursor also supports nested AGENTS.md files, so module-specific instructions automatically apply when an agent works inside that part of the repository. Custom specialists can be stored in .cursor/agents/, which means the definitions can live in the monorepo alongside the code they understand. Cursor’s documentation even describes the parent-agent version of this as an orchestrator pattern.
Claude Code supports a similar model through custom subagents in .claude/agents/. Each subagent receives a separate context, can be resumed when continuity matters and can optionally maintain project-scoped memory across conversations. Claude Code reads CLAUDE.md rather than AGENTS.md directly, but its own documentation recommends importing the canonical file from a small adapter:
@AGENTS.md
That lets the repository keep one architectural map while giving Claude Code the entrance it expects. Claude also supports isolated worktrees for subagents, although we’ll save concurrent editing and integration for the next part of the article.
The portable idea is therefore not that every module needs a magical conversation running forever in the background.
It’s simpler than that:
DURABLE MODULE MAP
+
REUSABLE SPECIALIST DEFINITION
+
BOUNDED TASK CONTEXT
The map preserves architectural knowledge. The specialist definition preserves the agent’s role and operating instructions. The task context contains the live investigation and work required right now.
That architecture works whether the tool starts a fresh specialist for each task, resumes a previous specialist or maintains longer-lived module memory.
A Specialist Starts Inside a Smaller Graph
Let’s make the advantage a little more precise.
We can model the repository as a graph:
Here, (V) represents whatever artifacts we’ve chosen to make searchable, such as files, symbols or packages. The edges in (E) represent explicitly defined relationships between those artifacts, such as imports, references or build dependencies.
The meaning of an edge matters. An import graph, a symbol-reference graph and a service-call graph aren’t the same thing. We shouldn’t throw every relationship in the company into one mathematical soup and pretend the notation made it rigorous.
For a payments task, the specialist begins inside a smaller subgraph:
where:
The payments map identifies that local subgraph and the important boundary artifacts connecting it to the rest of the repository. We can represent those boundary artifacts as (B_P).
FULL REPOSITORY GRAPH G
┌──────────────────────────────────────────────────┐
│ │
│ ┌──────── PAYMENTS Gₚ ─────────┐ │
│ │ │ │
│ │ refund service │ │
│ │ payment intents │ │
│ │ ledger adapter │ │
│ │ payment tests │ │
│ │ │ │
│ └───────────┬──────────────────┘ │
│ │ │
│ BOUNDARY ARTIFACTS Bₚ │
│ │ │
│ tax │ invoices identity │
│ │
└──────────────────────────────────────────────────┘
The payments specialist searches (G_P) first. If the task crosses a documented boundary, it follows the relevant edge into tax, invoicing, identity or another neighboring module.
That’s a cleaner search strategy than beginning every task with the entire graph and asking the agent to rediscover the useful neighborhood from scratch.
The Initial Candidate Universe Gets Much Smaller
Suppose the repository contains 120,000 searchable artifacts. Depending on the index, those artifacts might be files, exported symbols, packages or some consistently defined mixture of them.
Let:
be the number of artifacts in the entire repository.
For payments, let:
Suppose the payments neighborhood contains 8,000 artifacts and its documented boundaries expose another 300 plausible artifacts in neighboring modules. The agent’s initial candidate universe becomes approximately:
Compared with the full repository:
The module-scoped starting point removes roughly 93.1 percent of the repository from the agent’s plausible initial candidate universe.
That does not mean the task becomes 93.1 percent faster, cheaper or more accurate. Search systems don’t all scale linearly, agent behavior is messier than a fraction and some tasks will immediately cross module boundaries.
It means something narrower and more defensible: the agent begins with far fewer places that are likely to contain the answer.
GLOBAL START
120,000 plausible artifacts
████████████████████████████████████████
PAYMENTS-SCOPED START
8,300 local and boundary artifacts
███
The entire repository remains available. We’ve only changed where the search begins and what the agent considers likely before evidence tells it to expand.
Context Density Matters More Than Context Size
The repository graph explains where the agent searches. The working context has a similar problem.
Let (T) be the total number of tokens included in a task context, and let (R_P) be the number of those tokens that are relevant to the current payments task.
We can define a rough payments-context density:
This isn’t a standard model-quality metric. It’s a mental model for asking how much of the supplied context helps with the task instead of merely occupying space.
Imagine a giant global conversation containing 80,000 tokens. Only 12,000 of those tokens are meaningfully related to the current payments task:
Only 15 percent of the working context is payments-relevant.
Now imagine giving a payments specialist the same 12,000 useful tokens inside an 18,000-token task context:
The relevant material now accounts for roughly 67 percent of the context.
GLOBAL CONVERSATION: 80,000 TOKENS
Payments-relevant context ██████
Unrelated context ██████████████████████████████████
Context density: 15%
PAYMENTS SPECIALIST: 18,000 TOKENS
Payments-relevant context ███████████████████████████
Other necessary context █████████████
Context density: 67%
A fourfold increase in this made-up density measure doesn’t make the model four times smarter. It doesn’t directly predict accuracy, latency or cost.
It does make the task context more coherent. A larger share of what the model sees concerns the work it’s actually performing, while unrelated investigations remain in their own contexts.
That’s the useful property.
Give the Specialist a Stable Identity
A module specialist shouldn’t be nothing more than an agent with a clever name. Calling something “Payments Agent” doesn’t create specialization any more than naming a folder architecture creates architecture.
The specialist needs a stable operating identity.
PAYMENTS SPECIALIST
Root repository map
+
Payments module map
+
Payments operating instructions
+
Current task
+
Selected live evidence
The root map explains how payments fits into the application. The local map explains what payments owns, which interfaces are public, which paths are preferred and which neighboring modules commonly matter.
The specialist definition explains how the agent should operate inside that neighborhood. The current task provides the changing objective, and live search supplies the exact files, symbols and tests required to complete it.
A practical specialist definition might say:
You are the coding specialist for the payments module.
Begin by reading the repository-level AGENTS.md and the payments
module AGENTS.md. Treat those files as routing guidance, not as
substitutes for inspecting the live repository.
Default to searching and editing inside the payments neighborhood.
Use the documented public entry points and validation commands.
You may inspect any part of the monorepo when the task crosses a
documented boundary or when live evidence suggests the payments map
is incomplete. Do not modify neighboring modules unless the assigned
task requires it.
Before editing:
1. Restate the requested payments behavior.
2. Identify the likely public entry point.
3. List neighboring modules that may be affected.
4. Inspect the live code and tests verifying the behavior.
While working:
- Prefer existing payments conventions.
- Keep changes inside payments when ownership permits it.
- Follow documented dependency rules.
- Treat generated files and mechanical indexes as derived artifacts.
Before finishing:
1. Run the payments validation commands.
2. Check whether public interfaces or neighboring contracts changed.
3. Propose an AGENTS.md update only if the task revealed durable
routing or architectural knowledge.
4. Report files changed, tests run, unresolved risks and any
cross-module follow-up.
In Cursor, that definition can live in a project-level file such as:
.cursor/agents/payments.md
In Claude Code, the equivalent can live at:
.claude/agents/payments.md
The exact frontmatter and tool configuration differ between products, but the architectural role remains the same. The specialist has a stable identity that can be reused whenever work enters the payments neighborhood.
The Map, the Specialist and Live Search Have Different Jobs
It’s tempting to stuff everything the agent might ever need into the specialist definition. That would recreate the giant-context problem at a slightly smaller scale.
The better design separates four kinds of information:
MODULE MAP
Stable architectural guidance
Responsibilities, boundaries, entry points and validation
SPECIALIST DEFINITION
Stable operating behavior
Where to begin, how to expand and what completion requires
LIVE SEARCH
Current repository truth
Exact symbols, paths, imports, callers and implementations
TASK CONTEXT
Temporary working evidence
Plans, logs, diffs, test output and debugging notes
These layers age differently.
Architectural guidance may remain useful for months. A specialist’s operating rules may survive hundreds of tasks. A symbol name could change tomorrow, and a debugging log may become worthless ten minutes after the bug is fixed.
If we preserve all four as though they have equal durability, we eventually produce an agent definition containing half the repository and a small novella about a Redis timeout nobody remembers.
The map should stay compact and architectural. The specialist definition should stay behavioral. Live search should recover current mechanical truth, and temporary evidence should disappear when the task no longer needs it.
Reusable Doesn’t Mean Infinite
A reusable specialist doesn’t need to remember every task it has ever completed.
Both Cursor and Claude Code can resume certain subagent contexts, and Claude Code can give a custom subagent project-scoped memory. Those capabilities are useful when work genuinely continues across several sessions.
They aren’t an invitation to preserve everything forever.
KEEP DURABLY
Module responsibilities
Public entry points
Architectural boundaries
Stable operating instructions
Unresolved architectural decisions
KEEP WHILE THE TASK IS ACTIVE
Current plan
Selected source files
Recent test results
Open debugging evidence
Cross-module questions
DISCARD OR EXTERNALIZE
Completed transcripts
Raw search output
Old test logs
Abandoned hypotheses
Diffs already represented in Git
Facts now captured by code, tests or maps
When a task finishes, its durable result should move into the correct system of record. Code belongs in Git. Behavior belongs in tests. Architectural routing belongs in AGENTS.md. Important decision rationale may belong in an ADR or another reviewed design document.
The specialist should retain identity without hoarding its entire autobiography.
Specialized Without Being Trapped
Module boundaries are defaults, not prisons. A payment can affect taxes, invoices, entitlements, fraud checks and notifications, occasionally before lunch.
The payments specialist therefore needs an explicit escape rule.
START LOCAL
Search payments first
↓
CHECK DOCUMENTED BOUNDARIES
Does the task affect a known neighbor?
↓
FOLLOW THE RELEVANT EDGE
Inspect tax, invoices or identity
↓
EXPAND ONLY AS REQUIRED
Keep unrelated modules out of the working set
The escape rule prevents two opposite failures.
In the first failure, the agent searches the entire repository for every task and loses the benefit of specialization. In the second, the agent treats its module boundary as absolute and makes a locally correct change that breaks a neighboring system.
A useful specialist therefore needs three properties:
CORRECT ROUTING
Begin in the most likely neighborhood
BOUNDED CONTEXT
Carry mostly information relevant to that neighborhood
EXPLICIT ESCAPE ROUTES
Cross boundaries when repository evidence requires it
Remove the first property and the agent starts in the wrong place. Remove the second and unrelated work fills the context. Remove the third and specialization becomes tunnel vision.
The goal isn’t to create several smaller agents that know nothing about one another. It’s to create several focused entrances into one shared system.
One Feature Can Still Cross Several Neighborhoods
Suppose we ask the payments specialist to add tax-inclusive totals to customer invoices. It begins inside payments, but the request immediately touches several neighborhoods.
ADD TAX-INCLUSIVE INVOICE TOTALS
Payments
Determines the transaction amount
│
▼
Tax
Calculates the tax treatment
│
▼
Invoices
Stores and renders the final total
│
▼
Frontend
Displays the result to the customer
The payments specialist can inspect all four modules because they live in the same monorepo. It can follow their maps, inspect their public contracts and report which pieces of the change belong where.
But it shouldn’t quietly absorb all four domains into one giant task context and implement the entire feature itself. The payments agent understands transaction behavior, but it shouldn’t improvise tax policy. The tax specialist understands calculation rules, but it shouldn’t rewrite invoice contracts without involving the module that owns them.
We’ve reduced the amount of context each agent needs to carry, but we’ve distributed the feature across several specialists. Somebody still needs to understand the objective as a whole, determine which modules are involved, order the dependent work and bring the results back together.
ONE LARGE FEATURE
↓
SEVERAL MODULE-SCOPED TASKS
↓
SEVERAL SPECIALIST AGENTS
↓
ONE INTEGRATED CHANGE
Fortunately, Cursor and Claude Code already give the main agent the ability to delegate bounded tasks and collect the results. We don’t need to invent a new species of software robot.
We do need to be careful about what the parent agent owns, what each specialist owns and how their changes are isolated and integrated without turning the branch into a small electrical fire.
That’s the next problem.
Because once each neighborhood has its own specialist, somebody still has to coordinate the crossings.
Part 4: Use a Parent Agent to Coordinate Cross-Module Work
Once each neighborhood has its own specialist, somebody still has to coordinate the crossings.
That sounds like a job for another agent.
Not because adding one more agent automatically fixes everything. If that were true, we could solve software engineering by opening forty Cursor tabs and letting nature take its course.
The parent agent has a specific job. It holds the complete feature objective, identifies which modules are involved, breaks the work into bounded assignments and makes sure the specialists agree on the contracts connecting their changes.
PARENT AGENT
Understands the complete feature
↓
Finds the affected modules
↓
Defines the boundaries between tasks
↓
Delegates bounded work
↓
Collects results
↓
Checks that the pieces still form one feature
The parent isn’t necessarily a different model. In Cursor or Claude Code, it can simply be the primary conversation that receives the user’s request and launches the relevant subagents.
What makes it the parent is the scope of the context it holds.
The specialists understand neighborhoods.
The parent understands the trip.
One Feature, Four Owners
Let’s return to the feature we’ve been following:
"Show tax-inclusive totals on customer invoices"
A naive agent might interpret that as one implementation task. Find the invoice screen, add the tax amount and make the tests green.
But the repository map tells a different story.
CUSTOMER INVOICE TOTAL
Tax rules
│
▼
Payment amount
│
▼
Invoice contract
│
▼
Frontend display
Tax owns the calculation rule. Payments owns the transaction amount. Invoices owns the stored and exposed representation. Frontend owns the final presentation.
The feature is one idea from the user’s perspective, but four responsibilities from the repository’s perspective.
USER REQUEST
"Show tax-inclusive totals"
│
▼
PARENT AGENT READS THE MAP
│
┌───────┼─────────┬──────────┐
▼ ▼ ▼ ▼
TAX PAYMENTS INVOICES FRONTEND
This is where the repository-level map becomes more than onboarding documentation. It gives the parent agent enough architectural information to identify likely owners before anyone begins editing code.
Likely is the important word. The map routes the investigation, but live code still has the final say about current symbols, callers and dependencies.
The Parent Agent Routes Before It Delegates
The parent’s first move shouldn’t be launching four agents.
It should be understanding the shape of the change.
BAD ORCHESTRATION
Receive feature
↓
Spawn many agents immediately
↓
Everybody independently guesses the architecture
↓
Receive several incompatible solutions
BETTER ORCHESTRATION
Receive feature
↓
Read repository map
↓
Identify likely owners
↓
Inspect current boundaries
↓
Define contracts and dependencies
↓
Delegate bounded tasks
The parent begins with the root AGENTS.md, follows the documented module relationships and inspects the live public interfaces between the likely owners. It doesn’t need to read every implementation file. It needs enough evidence to understand how the pieces currently connect.
For the invoice feature, that initial investigation might establish four facts:
1. Tax exports calculateTaxBreakdown()
2. Payments consumes the tax breakdown when finalizing totals
3. Invoices expose MoneyBreakdown through a shared contract
4. Frontend renders the invoice contract without recalculating totals
Those facts define the seams between the tasks. Once the seams are visible, the parent can assign work without asking every specialist to rediscover the complete feature independently.
Decompose Around Contracts, Not Folders
A weak decomposition divides work by file location:
AGENT A
Change files in packages/tax
AGENT B
Change files in packages/payments
AGENT C
Change files in apps/web
That looks organized, but it doesn’t say what each change must accomplish or what the next module can rely on.
A stronger decomposition defines a behavioral responsibility and an output contract.
TAX SPECIALIST
Responsibility:
Return the tax-inclusive breakdown required by invoices
Input:
Existing taxable line items and jurisdiction
Output:
Updated tax breakdown contract
Must preserve:
Current rounding and exemption behavior
PAYMENTS SPECIALIST
Responsibility:
Build the final payable total from the tax breakdown
Input:
Updated tax contract from tax
Output:
Updated payment total consumed by invoices
Must preserve:
Current transaction and currency invariants
The difference matters because modules don’t connect through directory names. They connect through types, APIs, events, database records and other contracts.
The parent agent should therefore divide the feature at those contracts.
MODULE A
Internal implementation
│
▼
PUBLIC CONTRACT
│
▼
MODULE B
Internal implementation
The specialists own the implementation on their side of the boundary. The parent owns the agreement about what crosses it.
Give Every Specialist a Task Packet
A specialist shouldn’t receive the entire parent conversation. That would recreate the context problem we just spent a whole part fixing.
It should receive a compact task packet.
SPECIALIST TASK PACKET
Feature objective
+
Module responsibility
+
Relevant input contract
+
Required output contract
+
Allowed scope
+
Validation commands
+
Expected handoff
For the tax specialist, the assignment might look like this:
FEATURE OBJECTIVE
Show tax-inclusive totals on customer invoices.
YOUR RESPONSIBILITY
Update the tax module so its public breakdown contains the value
payments needs to construct a tax-inclusive invoice total.
BEGIN WITH
packages/tax/AGENTS.md
packages/tax/src/public/calculateTaxBreakdown.ts
CURRENT CONSUMER
packages/payments/src/totals/finalizePaymentTotal.ts
CONSTRAINTS
- Preserve existing jurisdiction and exemption behavior.
- Preserve the documented rounding order.
- Do not change invoice rendering.
- Do not edit payments unless required to update the public contract.
- Report any undocumented consumer you discover.
VALIDATION
Run the tax module tests and type checks documented in AGENTS.md.
RETURN
- Summary of the contract change
- Files changed
- Tests run
- New assumptions or risks
- Any downstream module requiring follow-up
This is much better than telling the specialist, “Please handle the tax part.”
The task packet defines what the specialist owns, what it may assume and what evidence it must return. It also prevents the specialist from expanding into neighboring modules merely because it found something interesting there.
Software agents, much like software engineers, occasionally discover one small problem and return three hours later having redesigned the company.
Boundaries help.
The Work Forms a Dependency Graph
Now we can make the structure mathematical.
Let the cross-module feature be represented as a task graph:
Here:
(J) is the set of jobs
(A) is the set of precedence constraints between those jobs
For our invoice feature, the jobs might be:
where:
(j_t) updates the tax breakdown
(j_p) updates payment totals
(j_i) updates the invoice contract
(j_f) updates the frontend
(j_v) performs end-to-end validation
The precedence constraints might look like this:
TAX
│
▼
PAYMENTS
│
▼
INVOICES
│
▼
FRONTEND
│
▼
END-TO-END TEST
Written as edges:
That particular graph is almost completely sequential. Throwing four agents at it won’t produce a fourfold speedup because each stage depends on the contract produced by the stage before it.
But perhaps the live repository reveals a different structure. Tax and invoice presentation may be independently implementable once the parent defines the new contract in advance:
CONTRACT PLAN
/ \
▼ ▼
TAX FRONTEND
│ │
▼ │
PAYMENTS │
│ │
▼ │
INVOICES ◄──────┘
│
▼
VALIDATION
Now some of the work can happen concurrently.
The graph, not the number of agents, determines the available parallelism.
More Agents Don’t Automatically Mean Less Time
Suppose each job (j) requires an estimated amount of work (w_j). With (m) available workers, the total completion time, often called the makespan, can’t be lower than either of two quantities.
First, it can’t be shorter than the longest dependency chain. That chain is the critical path.
Second, it can’t be shorter than the total work divided among the available workers.
So:
Imagine the work estimates look like this:
Contract planning 1 hour
Tax implementation 3 hours
Payments integration 2 hours
Invoice update 2 hours
Frontend update 3 hours
Final validation 2 hours
The total work is:
With four agents, the total-work lower bound is:
But if the critical path takes ten hours because most tasks depend on earlier results, the feature still can’t finish in less than ten hours.
TOTAL-WORK LOWER BOUND 3.25 hours
CRITICAL-PATH LOWER BOUND 10.00 hours
BEST THEORETICAL MAKESPAN at least 10.00 hours
This is why “just run more agents” is not an orchestration strategy. Parallel workers help only when the task graph contains work that can genuinely happen independently.
The equation doesn’t predict the real completion time. It ignores communication overhead, failed attempts, review, test duration and the minor detail that estimates made before touching the code are often works of fiction.
It does reveal the constraint that matters: dependencies limit parallelism.
Parallelize Discovery Before Parallelizing Edits
There’s an easier place to begin using several specialists: investigation.
Read-only exploration creates far fewer conflicts than concurrent implementation. The parent can ask several specialists to inspect their neighborhoods and return findings before deciding how the code should change.
PHASE 1: PARALLEL DISCOVERY
Tax specialist
Find calculation ownership and public outputs
Payments specialist
Find where tax enters the payable total
Invoice specialist
Find the stored and exposed invoice contract
Frontend specialist
Find where totals are rendered
Those investigations can happen simultaneously because each specialist is reading a different neighborhood and returning structured evidence.
The parent then combines those findings:
SPECIALIST FINDINGS
↓
PARENT SYNTHESIS
↓
CONFIRMED CONTRACT PLAN
↓
IMPLEMENTATION TASKS
This is often safer than allowing four agents to edit immediately. The parent learns the actual dependency graph before committing to a decomposition.
Once the boundaries are confirmed, independent edits can proceed concurrently. Dependent edits should be ordered, and changes touching the same files should usually stay with one specialist.
SAFE TO PARALLELIZE
Independent exploration
Separate test creation
Non-overlapping module changes
Independent verification
USUALLY KEEP SEQUENTIAL
Same-file edits
Schema change followed by consumer update
Contract changes with unresolved shape
Migrations with strict ordering
Tasks requiring constant cross-agent negotiation
Agents don’t make merge conflicts philosophically interesting. They remain merge conflicts.
The Parent Owns the Cross-Module Truth
Each specialist sees a bounded piece of the feature. That’s the entire point.
But it creates a coordination risk. A specialist can produce a perfectly reasonable local change based on an assumption that another specialist doesn’t share.
Consider a simple type:
TaxBreakdown
subtotal
tax
total
The tax specialist may interpret total as the final customer-visible amount. Payments may interpret it as the taxable amount before fees. Invoices may already expose another field called grandTotal, because apparently the English language ran out of nouns.
Every specialist could write locally sensible code and still produce an incoherent feature.
The parent agent must preserve the shared contract:
CROSS-MODULE DECISION
invoiceTotal =
subtotal
+ tax
+ paymentFees
- credits
It then passes the relevant part of that decision to each specialist.
TAX SPECIALIST
Produces subtotal and tax
PAYMENTS SPECIALIST
Adds fees and credits
INVOICE SPECIALIST
Exposes invoiceTotal
FRONTEND SPECIALIST
Displays invoiceTotal without recalculation
The parent doesn’t need every internal implementation detail. It needs the assumptions that cross module boundaries.
That gives us a useful division:
SPECIALISTS OWN
Local implementation
Local conventions
Local tests
Local failure handling
PARENT OWNS
Feature objective
Cross-module contracts
Task dependencies
Shared assumptions
Integration criteria
If a specialist discovers evidence that contradicts the shared plan, it should report the contradiction rather than quietly rewriting the contract.
Specialists Report Evidence, Not Just Confidence
A specialist returning “Done!” is not a useful handoff.
The parent needs enough evidence to understand what changed and whether downstream work can safely proceed.
A good specialist result includes:
RESULT PACKET
Outcome
What behavior now exists
Contract changes
What neighboring modules may rely on
Files changed
Where the implementation landed
Validation
Which commands ran and what passed
Risks
What remains uncertain
Map discoveries
Which durable repository guidance may need updating
For example:
OUTCOME
calculateTaxBreakdown() now exposes taxInclusiveSubtotal.
CONTRACT CHANGE
TaxBreakdown gained taxInclusiveSubtotal: Money.
FILES CHANGED
packages/tax/src/public/TaxBreakdown.ts
packages/tax/src/public/calculateTaxBreakdown.ts
packages/tax/tests/calculateTaxBreakdown.test.ts
VALIDATION
pnpm test --filter tax
pnpm typecheck --filter tax
DOWNSTREAM FOLLOW-UP
Payments must consume taxInclusiveSubtotal when constructing
InvoiceMoneyBreakdown.
MAP DISCOVERY
No map update required. Public ownership and entry point are unchanged.
The parent can now update the payments assignment using concrete output rather than an assumption made before the tax work began.
This is how information should move between specialists. Not by copying entire transcripts, but by passing the contracts, evidence and unresolved questions the next task actually needs.
The Parent Prompt Should Be Explicit Too
The parent agent also needs operating instructions. Otherwise, it may delegate based on whichever folder names sound relevant and declare itself an executive.
A practical parent-agent prompt might look like this:
You are the parent agent for cross-module changes in this monorepo.
Begin by reading the root AGENTS.md and identifying the modules that
may own parts of the requested behavior. Verify those boundaries
against live code before delegating implementation.
For each cross-module task:
1. Restate the complete feature objective.
2. Identify the likely owning modules.
3. Inspect the public contracts connecting those modules.
4. Represent the work as bounded jobs with explicit dependencies.
5. Parallelize only jobs that can proceed independently.
6. Give each specialist the smallest sufficient task packet.
7. Require structured results containing changes, tests, risks and
downstream implications.
8. Re-plan when specialist evidence contradicts the initial map.
9. Keep cross-module contracts and integration criteria in the parent
context.
10. Do not consider the feature complete until end-to-end validation
succeeds.
Specialists may inspect the entire monorepo, but their implementation
scope should remain bounded unless they report that a documented
ownership boundary is wrong.
This is intentionally boring.
Good orchestration is mostly explicit ownership, dependency management and careful handoffs. Adding the word “agent” doesn’t repeal any of those requirements.
Cursor and Claude Code Can Both Run This Pattern
In Cursor, the primary agent can launch custom module subagents, run independent work in parallel and collect the results. The specialists can be defined in the repository and inherit the maps and instructions relevant to their modules.
In Claude Code, the main conversation can invoke custom subagents, resume them when continuity matters and isolate implementation work in worktrees. The experimental agent-team system adds direct communication and a shared task list, but none of that is required for this architecture.
The portable pattern remains:
PARENT AGENT
│
├── Delegates bounded tax task
├── Delegates bounded payments task
├── Delegates bounded invoice task
└── Delegates bounded frontend task
│
▼
SPECIALISTS RETURN RESULTS
│
▼
PARENT UPDATES THE PLAN
The specialists don’t need to communicate directly. The parent carries the cross-module state and passes each specialist the information it needs.
That’s simpler, easier to inspect and supported by both tools.
Coordination Produces Pieces, Not Yet a Feature
At this point, the parent has done the hard conceptual work. It has translated one user request into a dependency graph, assigned bounded responsibilities, preserved the contracts between them and collected evidence from each specialist.
But we still don’t have a finished feature.
We have pieces.
Tax change
+
Payments change
+
Invoice change
+
Frontend change
=
Several individually plausible patches
Those patches may live in one workspace, several worktrees or separate branches. They may pass their module-level tests while still disagreeing at the boundaries. Two specialists may have edited the same shared type, and one may have built against a contract that changed while it was working.
The parent now has to integrate those pieces, resolve conflicts, run cross-module validation and turn the distributed work back into one coherent source state.
That’s where the monorepo gets to perform its final trick.
It lets several specialized agents work on different neighborhoods while still bringing the complete feature back together as one reviewable pull request.
Part 5: Integrate the Work Into One Reviewable Pull Request
At this point, the parent agent has done something genuinely useful.
It has taken one cross-module feature, identified the affected neighborhoods and delegated bounded pieces of work to specialists that understand those neighborhoods. The tax specialist knows how the total should be calculated. The payments specialist knows how that total moves through the invoice system. The frontend specialist knows how it should appear to the customer.
Great.
Unfortunately, three locally correct changes do not automatically add up to one globally correct feature.
The specialists may have made different assumptions about the same interface. One may have renamed a field without telling the others. Another may have implemented against an older version of the shared type. Every module test may pass independently while the complete feature still fails the moment the pieces are connected.
So now the parent agent has to bring everything back together.
Local Success Is Not Global Success
Let’s return to our feature:
"Show tax-inclusive totals on customer invoices"
The parent agent decomposed that request into three bounded pieces:
TAX SPECIALIST
Calculate the tax-inclusive total
and preserve the correct rounding order.
PAYMENTS SPECIALIST
Expose the calculated total through
the invoice response and shared contract.
FRONTEND SPECIALIST
Display the tax-inclusive total
on the customer invoice.
Each specialist can validate its own work inside its own neighborhood.
The tax specialist can prove that the calculation is correct. The payments specialist can prove that the invoice response satisfies its local tests. The frontend specialist can prove that the invoice component renders a supplied value.
But those tests only establish local properties.
TAX TESTS PASS
Tax calculation works
inside the tax module.
PAYMENTS TESTS PASS
Invoice serialization works
inside the payments module.
FRONTEND TESTS PASS
The component renders
the value it expects.
None of that proves the frontend is receiving the value the tax module actually produced.
That requires an integration claim:
Tax calculation
↓
Payments contract
↓
Invoice response
↓
Frontend display
And this is where things can get slightly annoying.
Imagine the tax specialist returns:
{
totalWithTax: 124.99
}
The payments specialist exposes:
{
totalIncludingTax: 124.99
}
And the frontend specialist reads:
invoice.taxInclusiveTotal
All three specialists implemented the same general concept.
They also produced three completely incompatible names for it.
The tax math can be perfect. The invoice serializer can be perfect. The React component can be perfect. The feature still doesn’t work because the contract connecting them is broken.
LOCALLY CORRECT
Tax: totalWithTax
Payments: totalIncludingTax
Frontend: taxInclusiveTotal
GLOBALLY BROKEN
Producer field ≠ Transport field ≠ Consumer field
This is why the parent agent can’t simply collect three “done” messages and call it a day.
It has to inspect the combined source state.
The Feature Is the Union of Its Changes
We can represent each specialist’s contribution as a change to the repository.
Let the tax change be represented by delta T, the payments change by delta P and the frontend change by delta F.
The complete feature change, represented by delta, is the union of all three.
The union symbol means that the complete source change contains everything changed by the tax, payments and frontend specialists.
COMPLETE FEATURE CHANGE
Tax specialist change
+
Payments specialist change
+
Frontend specialist change
=
Combined source change
But simply combining the files doesn’t prove that the result works.
The three local changes also have to remain compatible across every boundary they share. It helps to separate those two requirements mathematically.
First, define local validity as the condition that all three specialist changes work inside their respective modules.
The wedge symbols mean “and.” So local validity requires the tax change to work, the payments change to work and the frontend change to work.
But that still isn’t enough.
Let the tax-to-payments interface be represented by I with a TP subscript, and let the payments-to-frontend interface be represented by I with a PF subscript.
Boundary validity requires both interfaces to remain compatible.
Now we can state the actual condition for a valid feature.
In plain English:
COMPLETE FEATURE IS VALID
All local changes work
AND
All shared boundaries match
That looks like a lot of notation for saying “the pieces have to work individually and fit together.”
But that is exactly the point.
Parallel agents can reduce the time required to produce the pieces. They don’t eliminate the need to prove that the pieces form one system.
The Parent Agent Needs an Integration Workspace
Different agent runtimes can represent delegated work differently.
A specialist might edit the shared working tree directly. It might return a diff. It might create a commit or isolated branch. It might simply report its findings and let the parent make the final changes.
The exact mechanism matters operationally, but the architectural requirement remains the same: the parent agent needs a source state in which it can inspect the complete feature.
SPECIALIST OUTPUTS
Direct edits
or patches
or commits
or implementation instructions
│
▼
PARENT INTEGRATION STATE
│
▼
COMPLETE FEATURE DIFF
This is also why the article’s argument doesn’t depend on Cursor, Claude Code or any other tool supporting one magical built-in “coordinator mode.”
The workflow can be implemented through native subagents, separate agent sessions, isolated branches or plain old task delegation. What matters is that the work is bounded by module, that the outputs return to a parent and that one agent or human is responsible for integrating the complete change.
The parent’s integration workspace becomes the place where local assumptions meet reality.
Integration Has an Order
Not every specialist output should be applied at the same time or in an arbitrary sequence.
Our invoice feature contains dependencies:
TAX CALCULATION
↓
SHARED INVOICE CONTRACT
↓
PAYMENTS RESPONSE
↓
FRONTEND DISPLAY
The frontend depends on the invoice contract. The invoice contract depends on knowing what the tax module produces. That gives the parent agent an integration order.
We can represent “must come before” using the precedence symbol.
This says the tax result must be established before the payments integration can be finalized, and the payments contract must be established before the frontend integration can be finalized.
It does not mean all three specialists must sit around waiting for one another.
The frontend specialist can prepare the display logic while the backend work is underway, especially if the expected contract has already been defined. The payments specialist can update fixtures while the tax specialist finishes the calculation.
The specialists can still work concurrently where the task graph permits it.
SPECIALISTS MAY WORK CONCURRENTLY
Tax Payments Frontend
│ │ │
▼ ▼ ▼
Local work Local work Local work
INTEGRATION STILL FOLLOWS DEPENDENCIES
Tax contract
↓
Payments integration
↓
Frontend consumption
Parallel execution and dependency order are not opposites.
Independent work can run concurrently.
Dependent results still have to be assembled coherently.
Integration Is Where Hidden Assumptions Surface
Suppose the three specialists return their work.
The tax specialist reports:
Added calculateTotalIncludingTax()
Rounding occurs after tax calculation
Tax unit tests pass
The payments specialist reports:
Added totalIncludingTax to InvoiceResponse
Updated invoice serialization
Payments tests pass
The frontend specialist reports:
Added a tax-inclusive total row
Updated the invoice component test
Frontend tests pass
At first glance, everything looks lovely.
The parent agent then inspects the combined diff and discovers that the tax function returns a decimal object while the payments serializer expects a JavaScript number. The frontend assumes the API returns a formatted string.
Everyone worked on the same value, but each module represented it differently.
TAX MODULE
Decimal("124.99")
│
▼
PAYMENTS MODULE
expects number
│
▼
FRONTEND MODULE
expects "$124.99"
This isn’t necessarily a failure by any specialist.
Each agent operated inside a bounded context and followed the conventions visible there. The mismatch only became obvious when the parent compared the boundaries.
That comparison is one of the parent agent’s most important responsibilities.
It should ask:
Does every producer return what its consumer expects?
Did any specialist rename or reshape a shared contract?
Are units, precision and nullability consistent?
Do database, backend and frontend representations agree?
Did two specialists modify the same shared file differently?
Are error behaviors compatible across the boundary?
Did one change invalidate another specialist’s tests or assumptions?
The parent isn’t merely stacking patches.
It is reconciling different models of the system.
Conflicts Are More Than Git Conflicts
Some integration problems are obvious because source control marks them with angry punctuation.
<<<<<<< PAYMENTS CHANGE
totalIncludingTax
=======
taxInclusiveTotal
>>>>>>> FRONTEND CHANGE
Those are almost helpful. At least the repository admits that two changes collided.
The more dangerous conflicts merge cleanly.
The payments specialist may add a required field to a shared type while the frontend specialist continues using a fixture that never includes it. Git sees different files and reports no conflict. The type checker may catch the mismatch, or it may not if the fixture uses a cast.
Similarly, one specialist may change when rounding occurs while another copies a previously rounded value into a new response field. Both edits merge without complaint. The resulting feature quietly returns the wrong total.
TEXTUAL CONFLICT
Two edits touch the same lines.
Source control notices.
SEMANTIC CONFLICT
Two edits encode incompatible assumptions.
Source control smiles and waves them through.
Let the complete set of integration conflicts be represented by C. That set contains both textual conflicts and semantic conflicts.
Textual conflicts occur when changes overlap in the source representation.
Semantic conflicts occur when changes fit together syntactically but disagree about behavior.
Source control can identify much of the first set.
Tests, type checking, static analysis and careful review are needed for the second.
Validation Should Expand With the Source Change
Each specialist begins with local validation because local tests are fast and informative.
That is the right first step. A tax specialist shouldn’t run the entire monorepo after changing one calculation if the tax unit tests can catch the obvious problems in seconds.
But once the changes are integrated, validation has to expand outward.
VALIDATION PYRAMID
1. Changed function
↓
2. Module tests
↓
3. Boundary and contract tests
↓
4. Cross-module feature tests
↓
5. Repository-wide checks
For the invoice feature, the parent might run:
TAX VALIDATION
Does the calculation produce the correct amount?
Does rounding happen in the correct order?
PAYMENTS VALIDATION
Does the invoice response expose the new field?
Does serialization preserve precision?
FRONTEND VALIDATION
Does the invoice render the returned value?
Are loading and missing-value states handled?
CROSS-MODULE VALIDATION
Does a calculated tax-inclusive total travel from
the tax module to the rendered customer invoice?
REPOSITORY VALIDATION
Do type checking, linting and affected integration
tests pass across the complete source state?
We can formalize the expanding validation scope.
Let the changed modules be represented by M with a delta subscript. Let B of those modules represent the affected boundaries, and let D of those modules represent their relevant downstream consumers.
The complete validation scope is the union of all three sets.
Or visually:
VALIDATION SCOPE
Changed modules
+
Affected boundaries
+
Downstream consumers
In plain English, test the changed neighborhoods, the roads connecting them and the places those roads lead.
You don’t necessarily need to test every object in the entire universe.
You do need to test the complete path the feature travels.
The Feature Test Reconstructs the User’s Request
There is a satisfying symmetry here.
We began with one user request:
"Show tax-inclusive totals on customer invoices"
The parent decomposed it into module-level tasks because the implementation was distributed.
Now integration testing has to reverse that decomposition.
USER REQUEST
│
▼
DECOMPOSE INTO MODULE TASKS
│
├── Tax calculation
├── Payments contract
└── Frontend display
│
▼
IMPLEMENT LOCALLY
│
▼
RECOMPOSE INTO ONE FEATURE
│
▼
TEST THE USER-VISIBLE BEHAVIOR
The specialists prove that the pieces work.
The integration test proves that the original request works.
That final test should look less like three implementation details and more like the behavior the user actually asked for:
Given an invoice subtotal of $100
and an applicable tax rate of 8%,
when the customer opens the invoice,
the displayed tax-inclusive total is $108.
This test crosses the same boundaries as the feature.
It doesn’t care which agent wrote which line. It cares whether the system produces one coherent result.
Integration Is the Price of Parallelism
There is another useful way to think about the parent agent’s work.
Delegation can reduce the amount of elapsed time spent on the specialist tasks because some of them can happen concurrently. But the feature still requires decomposition before the specialists begin, integration after they finish and validation after their work is combined.
Let the decomposition time be represented by T with a D subscript, the specialist times by T with tax, payments and frontend subscripts, the integration time by T with an I subscript and the validation time by T with a V subscript.
If the three specialists can work in parallel, the elapsed feature time is approximately:
The maximum appears because the parent must wait for the slowest required parallel task, not add all three specialist times together.
For comparison, a purely sequential workflow would look more like:
This reveals both the advantage and the limit of multi-agent execution.
PARALLELISM CAN REDUCE
Specialist execution time
PARALLELISM DOES NOT REMOVE
Decomposition
Integration
Cross-module validation
Human review
If the parent decomposes the task badly, the specialists create incompatible work or the integration phase becomes a small civil war, the theoretical speedup can disappear.
More agents do not automatically mean less total time.
The architecture works when the module boundaries are clear enough to make specialist work genuinely separable and the parent preserves enough global context to reconnect the results efficiently.
One Feature Should Produce One Reviewable Story
Once the source state is integrated and validated, the parent agent can prepare the pull request.
This is where the monorepo provides another practical advantage. The reviewer can inspect the complete feature as one coordinated diff:
ONE BRANCH
↓
ONE COORDINATED DIFF
↓
ONE DEPENDENCY-AWARE TEST RUN
↓
ONE REVIEWABLE PULL REQUEST
The pull request can show the entire causal chain:
Tax calculation changed
↓
Invoice contract extended
↓
Payments response updated
↓
Frontend display added
↓
Cross-module test proves the complete path
That is much easier to reason about than three disconnected pull requests that happen to depend on one another.
With separate pull requests, a reviewer looking at the frontend may see a new field but not the calculation that produced it. A reviewer looking at payments may see a contract change but not the interface that consumes it. Each pull request can appear reasonable while the combined feature contains a gap.
SEPARATE REVIEW
PR 1: Tax change "Looks fine."
PR 2: Payments change "Looks fine."
PR 3: Frontend change "Looks fine."
Combined behavior: "Why is the total undefined?"
COORDINATED REVIEW
One feature
One dependency chain
One combined diff
One visible behavioral result
One pull request doesn’t mean the review should become a giant unstructured wall of code. The parent agent should organize the description around the same module boundaries used during implementation.
A useful pull-request summary might include:
WHAT CHANGED
Tax
- Added tax-inclusive total calculation.
- Preserved post-tax currency rounding.
Payments
- Added totalIncludingTax to the invoice contract.
- Updated invoice serialization.
Frontend
- Displayed the server-calculated total.
- Added missing-value handling.
CROSS-MODULE VALIDATION
- Tax unit tests pass.
- Payments contract tests pass.
- Frontend invoice tests pass.
- End-to-end invoice total test passes.
- Repository-wide type checking passes.
The implementation remains modular.
The review remains holistic.
One Pull Request Does Not Mean One Atomic Deployment
There is an important qualification here.
A monorepo can represent the complete feature as one atomic source-control revision. The tax, payments and frontend changes can be reviewed, tested and merged into one coordinated repository state.
That doesn’t mean every production service changes at the exact same instant.
ONE SOURCE REVISION
Tax source
Payments source
Frontend source
│
▼
One reviewed and merged commit
POSSIBLY SEPARATE DEPLOYMENTS
Tax service deploys first
Payments API deploys second
Frontend deploys third
Different parts of the repository may still require:
Separate builds
Ordered database migrations
Backward-compatible interfaces
Feature flags
Staged rollouts
Independent deployment and rollback
Compatibility across old and new service versions
Suppose the frontend deploys before the payments API. If the new field is temporarily absent, the frontend shouldn’t collapse into a small emotional crisis.
The source change may therefore include compatibility logic:
const displayedTotal =
invoice.totalIncludingTax ?? invoice.total;
Or the feature may remain behind a flag until every required service is ready.
The precise mechanism depends on the system, but the distinction is essential:
ATOMIC SOURCE STATE
The complete feature is represented
by one coordinated repository revision.
ATOMIC DISTRIBUTED DEPLOYMENT
Every running service changes
at exactly the same moment.
The first is achievable through the monorepo and source-control workflow.
The second is a separate distributed-systems problem.
The accurate claim is that the code representing the full feature can be reviewed, tested and merged as one coordinated source state, even when the resulting services deploy separately.
That is still enormously useful.
It just isn’t magic.
The Parent Agent Is Responsible for the Whole
This gives the parent agent a very different job from the specialists.
The specialists optimize locally. They understand a bounded neighborhood, make focused changes and run the tests that matter inside that module.
The parent protects the feature globally.
SPECIALIST RESPONSIBILITY
Understand one module
Implement bounded change
Run local validation
Report assumptions and results
PARENT RESPONSIBILITY
Reconcile shared contracts
Integrate in dependency order
Detect cross-module conflicts
Run expanding validation
Prepare one coherent review
The parent doesn’t need to redo every specialist’s investigation. That would defeat the purpose of delegation.
But it does need enough repository-level context to understand how the outputs connect. It must know which contracts are shared, which modules consume them and which tests demonstrate the complete user-visible behavior.
This is exactly why the parent began with the root repository map.
The map wasn’t only useful for decomposing the task.
It is also what allows the parent to put the task back together.
The Work Isn’t Done When the Agents Are Done
A collection of agent outputs is not a feature.
A collection of passing module tests isn’t necessarily a feature either.
The work becomes a feature when the individual changes form one compatible source state, the dependency path is validated and a reviewer can understand the complete behavioral story.
SPECIALISTS PRODUCE PARTS
│
▼
PARENT RECONCILES CONTRACTS
│
▼
DEPENDENCY-ORDERED INTEGRATION
│
▼
CROSS-MODULE VALIDATION
│
▼
ONE REVIEWABLE PULL REQUEST
That closes the loop we opened at the beginning of the article.
The user asked for one thing. The codebase turned it into several connected changes. The parent agent decomposed those changes so specialists could work with focused context. Then it recomposed their outputs so the repository once again represented one coherent feature.
But the agents learned things while doing all of this.
They found hidden dependencies, discovered validation commands, clarified ownership boundaries and surfaced rules that weren’t in the original maps. Some of that knowledge should help the next task.
The question is how to preserve it without preserving every search, failure and abandoned idea the agents accumulated along the way.
That is the final problem we need to solve.
Part 6: Preserve Durable Knowledge Without Preserving Every Conversation
By the time our tax-inclusive invoice feature reaches a pull request, the agents have learned quite a lot about the repository. The tax specialist has discovered that currency rounding must happen after tax calculation. The payments specialist has found an undocumented dependency between invoice generation and the shared billing types. The frontend specialist has learned that the invoice component should display the server-calculated value instead of calculating it again in the browser. And the parent agent now knows which sequence of tests validates the complete path across all three modules.
That is useful knowledge. It should absolutely help the next person, or agent, who touches the invoice system.
But the conversations also contain seven failed searches, several obsolete versions of the same files, a test failure caused by a missing comma, two abandoned property names and an increasingly desperate argument with TypeScript. All of that was useful while the agents were solving the task. Almost none of it should become permanent organizational memory.
EVERYTHING THE AGENTS SAW
Stable architectural facts
Accepted decisions
Current validation results
Repeated searches
Obsolete file contents
Abandoned approaches
Fixed errors
Temporary hypotheses
Current task state
The final problem is therefore not figuring out how to preserve every agent conversation forever. That would be easy. We could just keep dumping transcripts into storage until the repository has a charming little digital landfill attached to it.
The real problem is deciding what deserves to survive.
Let’s Start With One Messy Conversation
Imagine that the payments specialist begins with a fairly clean task: expose the new tax-inclusive total through the invoice response. It reads the payments module map, searches for the invoice serializer and initially assumes that InvoiceTotal is the main shared type. Ten minutes later, it discovers that InvoiceTotal is deprecated and the actual contract is InvoiceSummary. It updates the new type, runs the payments tests and gets a serialization failure because the tax module returns a decimal object while the API expects a number. It fixes the conversion, reruns the tests and eventually produces the correct response.
The conversation now contains several versions of reality.
EARLY IN THE CONVERSATION
InvoiceTotal is the active type.
The API probably accepts Decimal.
totalWithTax may be the correct field name.
LATER IN THE CONVERSATION
InvoiceSummary is the active type.
The API requires number serialization.
totalIncludingTax is the accepted field name.
Both sets of statements were useful when they appeared. Only one set is useful now.
That is what makes long-running agent context dangerous. The conversation doesn’t contain one clean model of the repository. It contains a sequence of models, each reflecting what the agent believed at a particular moment. New evidence corrects old evidence, files change, tests invalidate assumptions and completed work makes earlier problems irrelevant. If we preserve the entire sequence without distinguishing current truth from discarded history, the next task inherits all of it.
CONVERSATION OVER TIME
Initial assumption
↓
Search result
↓
Better assumption
↓
Code change
↓
Test failure
↓
Corrected implementation
↓
Validated repository state
The agent needed the whole path to solve the problem. The next agent usually needs the destination.
A Conversation Contains Two Kinds of Information
Now that we have the concrete picture, we can name the two things mixed together inside the transcript. Let the complete conversation history be represented by H. Inside that history, D represents durable information and T represents transient information.
Durable information is the small set of facts that still constrains future work. In our example, that includes the active invoice type, the accepted field name, the serialization boundary, the rounding rule and the validation commands that proved the feature worked. Transient information is the much larger trail of searches, temporary assumptions, obsolete file contents, fixed errors and abandoned implementations the agent used to discover those facts.
DURABLE INFORMATION
InvoiceSummary is the active contract.
The API serializes the total as a number.
The accepted field is totalIncludingTax.
Rounding occurs after tax calculation.
Cross-module invoice tests protect the path.
TRANSIENT INFORMATION
Searches for the deprecated type.
The original Decimal serialization error.
The abandoned totalWithTax property name.
Superseded file contents.
Tests that failed before the final fix.
The distinction isn’t simply “important information” versus “unimportant information.” A stack trace can be extremely important while the agent is debugging and completely useless once the underlying bug is fixed. A search result can determine the next action and become obsolete thirty seconds later. Transient information matters within a particular stage of the task, but durable information continues to matter after that stage ends.
That gives us the actual objective:
PRESERVE
What still changes the correct next action
DISCARD OR SUMMARIZE
What only explains how we reached
the current state
The conversation is a workspace. It is where the agent explores, gets things wrong, corrects itself and gradually constructs a better model of the repository. It should not automatically become the repository’s source of truth any more than every scribble on a whiteboard should become the company’s official architecture documentation.
Long Conversations Quietly Become Their Own Legacy Systems
This gets worse when specialist agents are resumed across several tasks. The first payments task contributes searches, old file reads, an accepted decision and some test output. The second task adds another set of assumptions, corrections and results. By the third task, the conversation contains information produced against several different repository revisions.
LONG PAYMENTS HISTORY
TASK 1
Old searches
Old file contents
Accepted decision
Completed change
TASK 2
New searches
Abandoned approach
Accepted decision
Completed change
TASK 3
Current repository state
Active question
Remaining work
Some continuity is useful. A resumed specialist may already understand the module’s purpose, its important boundaries and the commands used to validate it. But the same history may also contain references to functions that no longer exist, bugs that were already fixed and architectural assumptions invalidated by a later pull request.
The problem is not merely that the conversation gets bigger. The more serious problem is that every piece of information has a different expiration date. A business rule may remain valid for years. A file path may survive for months. A line number may survive until lunch. An error message may stop mattering after the next edit.
DIFFERENT INFORMATION, DIFFERENT HALF-LIVES
Business rule Years
Architectural decision Months or years
Module ownership Months
File path Days or months
Line number Minutes or days
Search result Minutes
Fixed error Seconds
If all of those remain in one undifferentiated history, the agent must repeatedly determine which version of reality deserves to be trusted. Eventually the retained context stops being an advantage and starts becoming archaeology.
Compaction Should Preserve the Current Model, Not the Entire Journey
The obvious response is to compact the conversation. Let C of H represent a smaller version of the original history.
The important idea is that compaction should not behave like a shorter transcript. It should behave like a state transfer. The output needs to tell the next agent what is currently true, what has been decided, what remains unresolved and what work is still active. It does not need to recreate the entire intellectual journey that produced those answers.
LONG CHAT HISTORY
Old searches
+ stale tool output
+ abandoned approaches
+ completed changes
+ current task
│
▼
COMPACTION
Stable decisions
+ unresolved questions
+ current repository state
+ active task
+ important validation results
For our invoice feature, a useful compacted state might look like this:
CURRENT GOAL
Show tax-inclusive totals on customer invoices.
ACCEPTED DECISIONS
Tax is calculated before currency rounding.
The shared invoice type exposes totalIncludingTax.
The frontend displays the server-calculated value.
CURRENT REPOSITORY STATE
Tax, payments and frontend changes are integrated.
Cross-module tests pass at revision 8f32c1.
UNRESOLVED ISSUE
Historical invoices may require a migration.
The migration is outside the current pull request.
VALIDATION
Tax unit tests pass.
Payments integration tests pass.
Invoice UI tests pass.
Repository-wide type checking passes.
That compacted state gives another agent enough information to continue intelligently. It does not preserve the first incorrect property name, the searches that returned nothing or the temporary serialization error, because none of those details should change what the next agent does.
We Can Measure How Much History Survived
Once we think of compaction as a transformation, we can measure how aggressively the history was reduced. Let the size of the original conversation be the number of tokens in H, and let the size of the compacted state be the number of tokens in C of H.
The retained token ratio is:
Suppose the original conversation contains 40,000 tokens and the compacted state contains 4,000.
The compacted history retained ten percent of the original token count.
ORIGINAL HISTORY
████████████████████████████████████████ 40,000 tokens
COMPACTED STATE
████ 4,000 tokens
RETAINED TOKEN RATIO
10%
That sounds efficient, but the number only tells us how much text survived. It doesn’t tell us whether the correct information survived. We could produce a beautifully tiny summary that preserves every failed search, drops the rounding rule and sends the next specialist directly into a financial bug.
So the retained token ratio measures compression.
It does not measure compaction quality.
The Hard Part Is Preserving the Right Ten Percent
To describe quality, return to the set of durable facts D. A useful compacted state should preserve as much of that set as possible.
We can define a conceptual durable-information retention rate:
The denominator represents all durable facts contained in the original conversation. The numerator represents the durable facts that survived into the compacted state. If the original conversation contains ten durable facts and compaction preserves nine, the retention rate is ninety percent.
Now we can see the actual tradeoff.
GOOD COMPACTION
Low retained token ratio
+
High durable-information retention
BAD COMPACTION
Low retained token ratio
+
Missing critical decisions
Ideally, the compacted state becomes dramatically smaller while retaining nearly all of the facts that still constrain future work.
DESIRED DIRECTION
ρ gets smaller
q stays close to 1
This is a conceptual model, not a benchmark we can calculate perfectly from a transcript. Two differently worded statements may represent the same fact, and not every fact has equal importance. Losing the name of a test file is inconvenient. Losing the rule that determines how customer totals are rounded can cost actual money.
A more realistic system would therefore weight critical facts more heavily than minor ones. But the basic principle remains the same: maximum deletion is not the objective. The objective is maximum removal of transient history with minimum loss of durable state.
How Do We Decide What Survives?
The easiest test is to ask whether removing a detail would change the correct next action.
WOULD REMOVING THIS DETAIL CHANGE
WHAT THE NEXT AGENT SHOULD DO?
YES
Preserve it.
NO
Summarize or discard it.
That keeps stable architectural facts, accepted decisions, the current repository revision, important interface changes, unresolved risks, active task state and meaningful validation results. It removes superseded file contents, repeated searches, fixed errors, abandoned hypotheses and routine tool output that can be regenerated from the current repository.
There are exceptions. A rejected approach may deserve to survive if future agents would otherwise repeat an expensive mistake. A fixed error may matter if it reveals a stable constraint. If decimal objects cannot cross the API boundary, the original stack trace can disappear, but the boundary rule should remain.
RAW FAILURE
TypeError in serializer at line 184
while encoding Decimal("124.99")
DURABLE FACT EXTRACTED FROM FAILURE
Invoice API boundaries require
numeric serialization.
This is the 3Blue1Brown-ish move hiding underneath the workflow. We begin with a concrete failure, strip away the incidental details and preserve the structural relationship that explains not only this failure but future failures of the same kind.
The transcript remembers an event.
The durable state remembers the rule.
Some Knowledge Should Leave the Conversation Entirely
Compaction makes a conversation easier to resume, but the knowledge is still trapped inside that conversation. A fresh specialist may not inherit it. A human browsing the repository may never see it. The information can disappear when the session is reset, archived or replaced.
That is fine for temporary task state. It is not fine for stable architectural knowledge.
Consider the most important discovery from our example:
Currency rounding must happen after tax calculation.
That rule constrains every future change involving taxes and invoice totals. If it remains only in a compacted agent history, it is available to one conversation. If it moves into the repository, it becomes available to every human and agent with access to the codebase.
AGENT LEARNS A STABLE FACT
"Currency rounding must happen after tax calculation"
│
▼
Human verifies the rule
│
▼
Store it in the appropriate artifact
│
▼
Future humans and agents can recover it
This is the point where the repository becomes more than the place where code happens to live. It becomes the durable memory layer for what the agents learn.
Put Each Fact Where It Can Be Enforced
Not every discovery belongs in AGENTS.md. Some facts are better expressed in code. Others belong in tests, architecture decision records, pull requests or temporary agent state. The right destination depends on what the fact is supposed to do.
DISCOVERY
│
├── Changes runtime behavior?
│ └── CODE
│
├── Must remain mechanically true?
│ └── TEST OR CI CHECK
│
├── Explains ownership or navigation?
│ └── AGENTS.md
│
├── Records an architectural decision?
│ └── ADR OR DESIGN DOCUMENT
│
├── Matters only to this source change?
│ └── PULL REQUEST
│
└── Matters only while finishing the task?
└── AGENT CONTEXT
The rounding rule should probably be protected by a regression test because violating it changes customer totals. The payments map can explain where the rule is implemented, which module owns it and where the relevant tests live. If the rule exists because of a regulatory requirement or an important architectural tradeoff, the deeper rationale may belong in an architecture decision record.
## Invoice totals
- Tax calculation is owned by `modules/tax`.
- Currency rounding occurs after tax calculation.
- Payments consumes the server-calculated result.
- Cross-module coverage lives in `tests/invoices`.
The test prevents the behavior from silently changing. The map helps future agents find it. The architecture record explains why the team chose it. Each artifact preserves a different layer of the same knowledge.
Stuffing all of that into AGENTS.md would not create better documentation. It would create a very large file that eventually becomes another context problem.
The Repository Has a Durability Ladder
These artifacts provide different levels of protection.
MOST ENFORCEABLE
Code and configuration
Tests and CI checks
Architecture decisions
Module maps
Pull-request history
Compacted agent state
Raw conversation history
LEAST ENFORCEABLE
A rule implemented directly in code shapes the running system. A rule protected by a test fails loudly when violated. A rule stored in a map guides humans and agents but cannot enforce itself. A rule mentioned only in a conversation depends on someone finding the correct transcript and recognizing which sentence still reflects reality.
The lower a stable fact sits on this ladder, the easier it is for that fact to become invisible, stale or contradictory. Durable knowledge should therefore migrate upward until it reaches the strongest artifact appropriate for that kind of information.
TEMPORARY DISCOVERY
Agent conversation
│
▼
Compacted task state
│
▼
Human-reviewed repository knowledge
│
▼
Code, tests, maps or decisions
The conversation is where knowledge is discovered.
The repository is where durable knowledge should eventually live.
The Agent Should Propose. The Repository Should Decide.
There is one fairly important catch: agents can misunderstand architecture. A specialist may infer a rule from one implementation even though the implementation is accidental. It may interpret a temporary workaround as an intentional module boundary. It may confidently update the map with an explanation that sounds excellent and is completely wrong.
So agents should not silently promote every observation into permanent repository doctrine.
Agent discovers a possible stable fact
│
▼
Agent identifies supporting evidence
│
▼
Human or designated owner reviews it
│
▼
Repository artifact is updated
│
▼
Tests and CI validate what can be validated
Mechanical statements can often be checked automatically. CI can verify that a referenced directory exists, that a validation command still runs or that an ownership pattern matches the repository. Architectural meaning is harder. A test cannot reliably determine whether the tax module is supposed to own rounding. That requires someone who understands the intended design.
AUTOMATION PROTECTS
Paths
Commands
Schemas
Types
Tests
Generated indexes
HUMANS PROTECT
Architectural intent
Ownership meaning
Accepted tradeoffs
Business rules
Security boundaries
The agent does the archaeological work. Automation protects mechanical truth. Humans protect semantic truth.
Every Task Should Improve the Starting Point
Once durable discoveries are promoted into the correct repository artifacts, the entire system becomes a feedback loop.
Module map
↓
Agent explores efficiently
↓
Agent discovers stable architecture
↓
Human reviews the discovery
↓
Code, tests or maps improve
↓
Next agent explores more efficiently
The first specialist may need fifteen searches to discover where invoice rounding occurs. Once the module map identifies the owning subsystem and a regression test preserves the rule, the next specialist can begin with that knowledge. It still verifies the current implementation when the task requires mechanical detail, but it no longer has to rediscover the repository’s basic architectural intent from scratch.
That is a much more useful form of memory than one immortal conversation that supposedly remembers everything. Conversations can remain temporary, exploratory and messy. Durable discoveries are extracted, reviewed and moved into artifacts shared by every future agent.
BAD ORGANIZATIONAL MEMORY
One enormous chat
that remembers everything
and slowly becomes wrong
USEFUL ORGANIZATIONAL MEMORY
Temporary agent context
↓
Reviewed discoveries
↓
Code + tests + maps + decisions
↓
Fresh agents recover current knowledge
The repository remembers without requiring the agent to remember.
Close Every Task With a Knowledge Pass
At the end of a task, the parent agent should perform one final knowledge pass before declaring victory. It should identify stable discoveries, separate durable facts from task history, choose the correct repository destination, propose the necessary updates, run the relevant validation and record anything that remains unresolved. Only then should the transient context be compacted or discarded.
TASK CLOSEOUT
1. Identify stable discoveries
2. Separate durable facts from task history
3. Choose the correct repository destination
4. Propose or make the reviewed updates
5. Run the relevant validation
6. Record unresolved issues
7. Compact or discard transient context
For the tax-inclusive invoice feature, that closeout produces several different artifacts because the knowledge has several different jobs.
CODE
Server calculates and returns totalIncludingTax.
TESTS
Rounding-order regression test added.
Cross-module invoice test added.
MODULE MAP
Tax module owns tax calculation.
Payments consumes the calculated result.
Frontend displays the server-provided total.
PULL REQUEST
Explains the coordinated source change and validation.
COMPACTED AGENT STATE
Historical invoice migration remains unresolved.
DISCARDED
Obsolete searches, fixed failures and abandoned patches.
Nothing useful is lost, but not everything is preserved in the same place. The implementation goes into code. Mechanical truth goes into tests. Architectural navigation goes into the maps. Task-specific history stays with the pull request. Unresolved work survives in compacted state. Everything else is allowed to disappear.
That’s not forgetting.
That’s organizing memory.
The Monorepo Becomes the Shared Memory
We started with a tiny feature request that immediately escaped the boundaries of any single file. Following it through the application revealed a dependency graph spanning several modules, which made one globally searchable repository useful. But global access alone wasn’t enough. The repository had to be divided into searchable neighborhoods, those neighborhoods needed compact Markdown maps and each neighborhood benefited from a specialist capable of keeping its working context focused.
Cross-module changes then needed a parent agent that could follow the dependency graph, delegate bounded work and integrate the outputs into one reviewable pull request. Finally, the useful discoveries produced during that work had to flow back into durable repository artifacts instead of remaining trapped inside increasingly ancient chat histories.
ONE MONOREPO
↓
SEARCHABLE MODULE NEIGHBORHOODS
↓
LIVING MARKDOWN MAPS
↓
FOCUSED SPECIALIST AGENTS
↓
PARENT-COORDINATED WORK
↓
ONE REVIEWABLE PULL REQUEST
↓
REVIEWED KNOWLEDGE RETURNS TO THE REPOSITORY
That is the broader argument for monorepos in an agentic world. It is not that we should shove the entire repository into one gigantic prompt and ask the model to figure it out. Please don’t do that. Your context window has suffered enough.
The advantage is that the complete system remains available inside one coherent environment while maps, module boundaries and specialist agents control how much of that environment enters any particular task. The monorepo provides the world. The maps explain the world. The specialists work inside bounded parts of it. The parent coordinates changes across those boundaries. And when the work is finished, the useful discoveries flow back into the repository so the next agent starts from a better map than the last one had.
So yes, monorepos might actually be all you need.
Also Markdown files.
Those too.











