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

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 pointer file described in the previous post. 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 from the previous post. 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:

What transfers

This is the fourth post about the same small stack, and the pattern holds: the memory system logs the footsteps, the presence layer keeps parallel sessions legible, the docs standard preserves the why, and the code map fuses all three into the thing an agent actually needs on arrival - a sense of place.