DEV Community

Andre
Andre

Posted on • Originally published at olund.dev

Giving my agent a map of the codebase (drawn partly from its own footsteps)

Originally published at olund.dev.

Watch a coding agent land in an unfamiliar repository and you will see it do
what a new hire does on day one: grep for names, list directories, open
files, and slowly assemble a mental model. Unlike the hire, the agent does
this every session. The model it builds is discarded when the context
window closes, and the next session pays for it again - in tokens, in
latency, and in the wrong turns an incomplete picture produces.

My fix is a per-project code map: a queryable navigation layer each
repository carries, rebuilt incrementally, that a new session receives a
digest of before it reads a single file. The interesting part is not that it
exists - static code intelligence is a mature field - but which three
signals it fuses, because the third one is something most tooling cannot
see at all.

Three signals, one map

Structure is the conventional axis: symbols, calls, and imports parsed
with tree-sitter across whatever languages the repo mixes. This is
deliberately the shallow version of static analysis. Dedicated tools do
deep type resolution far better than I ever will, and competing with them
head-on would be a losing use of my time. Syntactic edges are good enough
for navigation, and the edge table records where each edge came from, so a
deeper per-language analyzer can add resolved edges later without a redesign.

Temporal comes from git: churn, ownership, and co-change - which files
historically change in the same commits. Parsed from plain git log
output; no library dependency, and the repo's whole history is sitting
there with nothing better to do.

Behavioral is the differentiator. My memory system already records
every tool call an agent makes - every read, edit, and write, tagged with
project and session. Rolled up, that log yields a co-touch matrix: which
files did agents actually work on together, in real sessions. Editing
weighs more than reading in the rollup, because changing two files together
is stronger evidence of coupling than looking at them.

The distinction between the axes matters in practice. The call graph tells
you A depends on B - true, and sometimes useless, because half of a
codebase depends on B. The behavioral signal tells you that in the last
thirty sessions, every time A changed, B and C changed too. Those are A's
de facto neighbors: the set you should have open when you touch it. Static
tools cannot compute this because they never see the work. Git co-change
approximates it but only at commit granularity, after the fact, and only
for changes - a session that read four files to safely edit a fifth leaves
no trace in git at all. The agent's own footsteps are the only place this
signal exists.

Cheap at session start, deep on demand

The map is consumed two ways, and the split is deliberate.

The cheap path is a small rendered digest injected into every session's
context at start: the subsystems, a few key files each, one line of purpose,
ordered by community detection over the fused graph. On my main project
right now that is 913 files folded into 33 communities. One refinement
earns its keep daily: the digest's sections are reordered by relevance to
the branch's current task
, which the session-start machinery already knows
from the branch's
current-task pointer. A session opening a
branch about upload quality-control sees the QC subsystem first, not an
alphabetical list.

The deep path is a set of query tools the agent calls mid-session:
neighborhood (structural plus behavioral neighbors of a file), hotspots
(churn-weighted risk), and why - which is my favorite, because it joins
the map to the
documentation standard. Ask why a
file exists and the answer includes the architecture decision records that
cite it, alongside its callers. The decision trail and the call graph hang
off the same index, so "what is this" and "why is it like this" are one
query apart.

The two paths exist because they have different economics. The lay-of-the-
land question benefits from being ambient - answered before the agent
knows to ask, at zero query cost. Graph traversals cannot be pre-rendered
into prose without exploding; they stay behind tools. Pure-filesystem and
pure-query designs both lose to the split.

Staying fresh without a daemon

A navigation layer that drifts from the code is worse than none - an agent
routed by a stale call graph mis-navigates confidently. The freshness
model is a pattern I now reuse everywhere: lifecycle events catch the
common case; a scheduled sweep guarantees convergence.

When an agent edits a file, a post-edit hook re-parses just that file and
updates its rows - sub-second with tree-sitter, and it keeps the index
honest about the thing most likely to be queried next: the code the agent
just changed. But hooks are best-effort by nature. They miss hand edits
made outside a session, deletions, rebases. So a periodic full reindex
sweeps up whatever the hooks missed. Neither mechanism alone is sound: the
hook without the sweep accumulates drift forever; the sweep without the
hook means the map is wrong precisely about the current session's work.
Together they give eventual consistency with a fast path where it matters.

The digest itself is recomputed on the slow cadence, not per-edit - which
subsystems exist and what matters most is stable week to week, and the one
expensive step (an LLM naming the communities) is not worth paying per
keystroke.

What I deliberately did not build

Three rejections shaped the design more than the features did:

  • No language servers, no deep type inference. Spinning up N language servers across every watched project is operationally heavy, and it competes with mature tools on their home turf. The behavioral signal is the moat; syntactic structure is scaffolding for it. The v2 path (per- language deepening adapters) is recorded, provenance-tagged, and unbuilt.
  • No global cross-project graph. Each project's map is self-contained and lives with the project's own data. A cross-project query ("which repos use this pattern") has no consumer yet, and speculative generality would tax every tool signature. When a real consumer shows up, a thin aggregator can be added over the per-project stores.
  • No separate behavior log. The co-touch matrix is a rollup over the event log the memory system already keeps. A dedicated tracking store would duplicate data, lose the joins with session decisions, and double the surface I have to keep redaction-clean.

What transfers

  • Your agent's work history is a first-class code signal. If you run agents against a repo and log their tool calls, you are sitting on coupling data no static analyzer can derive. The rollup is a SQL view, not a research project.
  • Split ambient from on-demand. A small always-injected digest for orientation, tools for depth. Neither substitutes for the other, and the digest is only trustworthy if something recomputes it.
  • Order context by the current task. The same digest reads twice as well when the relevant subsystem comes first. You usually already know the task; use it.
  • Freshness = fast best-effort path + guaranteed slow sweep. Any derived index maintained only by event hooks is quietly wrong; any maintained only by batch is wrong about right now.
  • Route around mature tools, not through them. Tree-sitter-shallow plus a unique signal beats competing with twenty years of type-inference engineering on their terms.

Top comments (0)