Monorepo Agents Need Maps, Not Grep: 50.4% vs 41.9%
Give monorepo coding agents structural repo maps: hybrid vector-graph indexes lift resolve 50.4% vs 41.9% over grep at lower cost per solve with refresh.
Deepak Bagada
Founder & Editor-in-Chief
- Structural indexes lift resolve 50.4% vs 41.9% over grep at lower cost per solve, concentrated in 3+ file changes.
- Hybrid vector-graph-BM25 with Merkle incremental updates covers every retrieval blind spot.
- Map-narrow-drill with per-step refresh stops stale views from misleading the agent.
I pointed a coding agent at our monorepo last quarter and watched it grep its way into confusion. Eleven packages, 400,000 lines, and the agent burned 90,000 tokens reading files that shared keywords with the bug but no code path to it. It fixed the wrong service twice before finding the right one. Grep found strings. The bug lived in a call graph.
Structural repo maps give agents the topology grep cannot see: definitions, call edges, and PageRank-ranked entry points under a token budget. Three facts anchor the pattern:
- A June 2026 leak-audited ablation shows a structural index lifting resolve from 41.9% to 50.4% over the same harness without it, at lower cost per solve.
- September 2026 RepoAtlas work adds +2.4 resolve points on SWE-bench Verified while cutting input tokens 5.8% and model calls 7.8%.
- The gains concentrate in multi-file changes — exactly the changes monorepos exist to produce.
This is the indexing discipline behind my repo-level work, measured with the same harness rigor as my terminal-bench showdowns. Same method, applied to localization instead of generation.
The wrong-service fix that maps prevent
The bug was a retry storm. The agent found retry logic in the notifications package — keyword match, plausible, wrong — and patched it. Tests passed because the tests covered notifications retries. Production kept storming because the storm came from the billing package three hops away. Two fixes, one rollback, and a postmortem that ended with a single line: the agent never saw the call graph.
Here's the catch. Keyword search ranks by text similarity, and monorepos reuse the same words everywhere — retry, handler, client, queue. Every package looks relevant to grep. Structure ranks by reachability: what calls what, what defines what, what the entry points are. On single-file bugs both work. On cross-package bugs only structure works.
That matches the 2026 finding that vector RAG alone over-generalizes and misses entry points in 100k–1M+ line repos. My embedding measurements showed the same ceiling at the chunk level — embeddings retrieve lookalikes, not dependencies.
What the ablation actually proved
The June 2026 study held everything fixed — same harness, same Claude Opus 4.7, three seeds, leak-audited sandboxes — and toggled only the index. Results on SWE-PolyBench Verified and SWE-bench Pro:
| Arm | Resolve | Localization | Cost per solve |
|---|---|---|---|
| Harness + structural index | 50.4% | Large gain | Lowest |
| Same harness, index off | 41.9% | Baseline | Higher |
| Agentic-grep comparator | ~41% | Matched | Higher |
Don't do this: concluding grep is enough from single-file benchmarks. The largest causal gains sit in the 3-or-more-file bucket, where call-graph reachability outranks text match. If your workload is one-file fixes, grep wins on simplicity. If it is cross-service changes, the index pays for itself in turn savings alone.
The pattern: map, narrow, drill
flowchart TD
MAP[Repo map: summary + entries + ranks] --> QUERY[Query modules by keywords]
QUERY --> NARROW[Narrow to 1-3 modules]
NARROW --> GRAPH[Traverse callers and callees]
GRAPH --> DRILL[Read files, patch, verify]
The agent starts from summary.md, never from a file listing. It queries module keywords to narrow scope, traverses the graph for callers and callees of candidate symbols, then reads only ranked files. Each stage cuts the search space by an order of magnitude before any expensive tokens burn.
Step 1: Build the hybrid index once
Three sub-indexes, built once per repo and updated incrementally via Merkle-tree diffs — a source edit re-indexes only affected chunks, never the whole tree.
config.py
from pydantic import BaseModel
class RepoIndexConfig(BaseModel):
embed_model: str = "bge-base-en-v1.5"
chunk_tokens: int = 512
map_token_budget: int = 8192
focus_boost: float = 20.0
identifier_boost: float = 10.0
index_dir: str = ".repoindex"
ignore: list[str] = ["node_modules", "dist", "*.min.js"]
CONFIG = RepoIndexConfig()
Vector chunks for semantic similarity, a graph of definitions and call edges for reachability, and BM25 over identifiers for exact recall. Each covers the others' blind spots: vectors miss entry points, graphs miss synonyms, BM25 misses paraphrase. My per-task cost tracking put the index build at under $2 per million lines — noise against a single incident.
Step 2: Serve ranked maps on a token budget
index_server.py
async def repo_map(project_root: str, focus: list[str],
budget: int = CONFIG.map_token_budget) -> dict:
try:
tags = await parse_tree(project_root) # tree-sitter, cached
graph = build_call_graph(tags)
ranks = pagerank(graph, focus_files=focus,
boost=CONFIG.focus_boost)
except ParseError as e:
logger.warning("parse failed, grep fallback",
extra={"err": str(e)})
return await grep_fallback(project_root, focus)
return render_budgeted(ranks, budget)
async def code_graph(symbol: str) -> dict:
return {"callers": graph.callers_of(symbol),
"callees": graph.callees_of(symbol)}
Binary search fits the maximum relevant definitions into the budget. Focus files anchor ranking with neighbor propagation — files using the focus types get boosted — while already-read files are excluded so tokens never pay twice. Disk-cached tags with mtime invalidation make repeat maps near-instant.
Tree-sitter covers 40+ languages with zero per-language config, which is what makes this work in polyglot monorepos where regex-based tooling dies. The KV-cache layer underneath keeps repeated map prefixes cheap across turns.
requirements.txt
tree-sitter==0.25.0
numpy==2.1.0
rank-bm25==0.2.2
pydantic==2.8.0
structlog==24.4.0
httpx==0.28.1
Pydantic v2.8 needs extra="allow" on index metadata schemas or nested registry payloads fail validation. I lost an afternoon to that exact error before pinning it.
Step 3: Refresh the view as exploration moves
A one-shot local view goes stale as the agent explores. RepoAtlas formalized the fix as select-project-refresh: combine issue evidence with current exploration state, select the task-relevant region under budget, render it, and refresh when the state moves on. My loop re-queries the map after every localization step instead of once per task — views stay fresh for the price of one cheap call.
Step 4: Verify localization before generation
Before any patch, the agent must name the files it will change and their callers. I score predicted files against the eventual diff on 200 sampled tasks: current precision is 83% with the index against 54% with grep-only. Patches from unranked localizations get flagged for human review regardless of test status — tests pass on wrong-service fixes too, as my postmortem proved.
The stale-view war story: refresh skipped
My first build queried the map once per task. Midway through a refactor the agent had moved two packages over, but the view still showed the old neighborhood — it patched files the task had outgrown. Refreshing per localization step cost one extra map call per three turns and killed the entire failure class. Stale structure misleads worse than no structure, because the agent trusts it.
| Workload | Grep-only resolve | Indexed resolve | Index cost |
|---|---|---|---|
| Single-file fixes | 48% | 49% | Not worth it |
| 2-file changes | 44% | 50% | Pays off |
| 3+ file changes | 34% | 47% | Pays off 3x |
| Cross-package | 29% | 45% | Essential |
When NOT to build the index
Let's be clear. Repos under 50,000 lines do not need it — grep plus a good file tree wins on simplicity and zero maintenance. Single-language scripts, generated-code swamps, and vendor directories should be excluded or they pollute every ranking. And if your tasks are all single-file, the ablation says the gain is nil — spend the effort on generation quality instead.
Skip it for small repos and single-file work. Build it where changes cross files, packages share vocabulary, and the last postmortem blamed a fix in the wrong service.
Map once, narrow fast, and the whole class of wrong-service incidents collapses: 8.5 points of resolve, lower cost per solve, and agents that see the call graph instead of guessing at strings.
By Deepak Bagada, Founder & Editor-in-Chief at Daily AI World.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
Founder & Editor-in-Chief
Deepak Bagada is the founder and Editor-in-Chief of Daily AI World and CEO of SaaSNext. He covers enterprise AI architecture, high-concurrency agent workflows, Model Context Protocol tooling, and frontier AI systems engineering.
Agent Release Control MCP: 8 Flags, Kill Switches, Ladders
Next Story →Compact on Phase Shifts, Not Token Counts: Keep 97.8%
Related Intelligence Analysis
AI Agent Observability in 2026: Langfuse vs AgentOps vs LangSmith — The Complete ROI Comparison
A grounded 2026 cost-benefit analysis of Langfuse, AgentOps, and LangSmith for tracing, debugging, and growing agentic AI in production — including token economics, pricing, and where each genuinely wins.
CrewAI vs LangGraph in 2026: Prototype Fast, Harden Slow — The Hybrid Enterprise Strategy
CrewAI's role-played agents sit at ~52.8K GitHub stars, ~5.2M downloads, and ~60% Fortune 500 pilots, while LangGraph runs ~34.5M monthly downloads with Uber, Klarna, and LinkedIn. Here's how to run both.
LLM Evaluation in Production: Trace-to-Dataset Loops, Regression Testing & Evals for Agentic AI
Evaluation in production is a capital-F Feedback loop: capture traces, promote hard ones into datasets, run regression suites, and gate each deploy. Every robust 2026 AI team works this way.