What a graph cannot see
coldstart's gs command builds a real graph off imports and calls, no guessing involved. Building it also showed me exactly where a graph like that runs out of road, and the answer wasn't a parsing gap I could fix.
coldstart’s gs command answers “who calls this” and “who imports this file” from a graph built by actually parsing the code: real imports resolved to real files, real call sites resolved to real function definitions. No inference, no guessing at intent. I trust that graph, and I’ve watched it save turns that would otherwise go into a grep and a manual trace. It’s also taught me exactly where a graph like this, or any graph built by any method, quietly stops being able to help.
Three files, zero edges, one bug
coldstart’s own notebook has a capture step: something fires at the end of a session and writes down what the agent worked out. It runs a little differently depending on the host. Claude Code, Cursor, and Codex each have their own hook mechanism, their own transcript format, their own way of telling a script “the turn just ended.” So there are three scripts: one for each host. None of them imports another. None of them calls another. If you ran any graph over that code, deterministic or LLM-generated, it would report exactly what it looks like: three unrelated files.
They are not unrelated. All three read and write the same marker file in a temp directory, and the exact shape of that filename, which pieces of session and agent identity get folded into it, has to match across all three or the whole mechanism silently breaks. Not at parse time. At runtime, in whichever host happens to run second.
How the coupling actually broke things
This happened more than once, in slightly different ways, across a string of separate fixes. An early version named the marker file using the repository path alone. That worked until a subagent, which shares its parent session’s id, started a capture of its own, and its marker collided with the parent’s, wiping out a worklist (the running checklist of what still needed to be written down) the parent hadn’t finished with. Fixing that meant building the marker’s name from the session id and agent id together, not the repo path alone. Getting that fix into the Claude Code script and not immediately propagating the same key shape into the Cursor and Codex scripts reintroduced a version of the same class of bug in a different host later, because each script was edited as if it were self-contained, which, looking only at its imports, it was.
Across the real history of this feature, that same category of bug resurfaced across roughly a dozen separate fixes before the convention got made explicit and shared instead of copied three times by hand. Each individual fix looked complete, reviewed in isolation, because each script really does read clean on its own. The bug was never inside a script. It was in the gap between them, a gap no graph edge represents because there’s no statement in the code that creates one.
Why no graph catches this, however it’s built
A parser reads your code by building an AST, a tree that represents its syntax, and that tree has no node type for “these two files agree on a string format by convention.” There’s nothing to attach an edge to. An LLM asked to summarize each file in isolation has the same blind spot for a different reason: understanding the coupling requires knowing that a constant in one file has to be checked against assumptions baked into two other files somewhere else in the repo, and that fact doesn’t live in any one file’s text. It lives in the incident history, the sequence of “we fixed it here and broke it there” that only exists in people’s memory of what happened, or in a changelog nobody re-reads before touching that code again.
A smarter graph wouldn’t fix this. Some couplings are structurally invisible to any system that only looks at what’s on the page right now, because the dependency is temporal and conventional rather than syntactic. Two files agreeing to format a string the same way is a real dependency. It just doesn’t show up as an edge, no matter how good your parser or your model is.
What actually caught it
What ended the pattern was writing the incident down the moment it was understood, as a note attached to the files involved, saying plainly that these three scripts share a convention and any change to the marker shape has to be checked against all three. The next time someone, human or agent, opens one of those files, that note is sitting right there, and the fix starts from the actual failure history instead of from three files that each look complete on their own.
The missing edge was a conclusion
None of this argues against building a graph. coldstart’s own gs relies on one, and it’s exactly the right tool for the relationships it can see: real imports, real calls, real reference edges. The argument is narrower than “graphs are insufficient.” It’s that the edge missing here was never a parsing failure at all.
Nobody could have read that convention off the three files, because at the time it was written it wasn’t in them. It came into existence the way most real couplings do: someone changed one script, something broke in another, and the relationship between them was established by that incident rather than by any statement in the code. The edge is the output of an investigation. Regenerating a graph over the same source, however carefully, cannot produce it, because the source is not where it lives.
Which is what the notebook is for here, and the claim is small. It doesn’t discover couplings; it stores the ones that someone already paid to discover, attached to the files they concern, so the next change to that marker shape starts from the failure history rather than from three files that each read clean on their own.
The rule I’d take out of this, whatever you’re building: static structure will show you the relationships the code states. It will never show you the ones the code only assumes. Those are found by breaking something, and they survive only if whoever broke it wrote them down.