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:
- 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.
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.