Skip to main content
Subscribe
Front Page / Coding / Deep Dive

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

Deepak Bagada

Founder & Editor-in-Chief

Sep 20, 2026 Published
|
Sep 20, 2026 Updated
|
7 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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 , Founder & Editor-in-Chief at Daily AI World.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
Keyword search ranks by text similarity, and monorepos reuse the same words in every package. Structure ranks by reachability — callers, callees, definitions, entry points. On single-file bugs both work; on cross-package bugs the June 2026 ablation shows the index winning decisively.
Vector chunks for semantic similarity, a definition/call-edge graph for structural reachability, and BM25 over identifiers for exact recall. Each covers the others' blind spots, and Merkle-tree diffs re-index only edited chunks instead of the whole tree.
Start from the summary, query module keywords to narrow to 1–3 modules, traverse callers and callees of candidate symbols, then read only ranked files. Re-query after each localization step so the view never goes stale mid-task.
Under 50,000 lines, grep plus a file tree wins on simplicity. Single-file tasks show nil gain in the ablation. Exclude generated code and vendor directories, and spend the effort on generation quality instead.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc

Cookie & Privacy Preferences

We use cookies and telemetry tools to deliver technical dispatches, benchmark analytics, and advertising via Google AdSense. Review our Privacy Policy.