Dead Code Finder
A GitLab Duo agent flow that finds genuinely unused code, and is explicit about what it can’t prove
Built for the GitLab AI Hackathon (Transcend)
What was the problem?
Most tools that check whether code is “dead” either declare something safe to delete with no real proof, or flag things as unused that are actually just being called in an unusual way; neither answer is trustworthy enough to act on.
What did I make?
I built a tool that checks a codebase’s real call graph (actual proof of what calls what) and sorts every result into one of three honest buckets: confident it’s unused, genuinely uncertain, or “can’t tell statically, here’s why.” It never deletes or changes anything itself; it only reports what it found and how sure it is.
Why is that useful?
A tool that always says “yes, delete it” isn’t trustworthy, and a tool that hedges on everything isn’t useful either. Separating “confident” from “uncertain” from “can’t check this” means a developer can actually trust the confident findings instead of double-checking everything by hand.
What is still unproven?
It’s been validated on Python code specifically, using cases planted on purpose to be hard: inheritance, decorators, test-framework tricks. It can’t see code referenced only from non-code files like YAML or CI configs, and it has no way to tell something genuinely abandoned apart from something just new and not wired up yet.
The problem
- Most impact-analysis tools answer "what breaks if I change this." Nobody had a good answer to the narrower question, "is anyone calling this at all," with an honest confidence level attached instead of a blanket yes/no.
- Naive static analysis over-claims in both directions: it either declares something "safe to delete" with no real proof, or produces false positives on legitimate-but-unusual call patterns like decorator dispatch, test-framework reflection, and inheritance/MRO.
The approach
- Built as a GitLab Duo Agent Platform flow that queries Orbit’s real static call graph for CALLS/IMPORTS edges on every Definition node in the project, rather than pattern-matching source text.
- Every finding lands in one of three honest buckets instead of a binary: Confident (no incoming call/import edges anywhere), Uncertain (ambiguous cases like inheritance/MRO dispatch), or Skipped (decorator-based dispatch, test-framework reflection, hardware entry points, explicitly flagged as "cannot assess statically," never silently dropped).
- A hard behavioral constraint by design: the flow never deletes, renames, or modifies a file, never opens a merge request, and never says "safe to delete", only "no reference found in the static call graph, here is exactly what was checked."
- Validated against a deliberately planted set of dead-code fixtures covering the genuinely hard cases: cross-file import resolution, inheritance/MRO dispatch, decorator-based dispatch, constructor/dunder handling on a real ~500-line class, and test-framework reflection discovery.
What broke (and how it got fixed)
- The platform’s own skill-injection mechanism was unreliable: the flow’s procedure file was correctly placed in the project, but at runtime sometimes arrived as just its manifest entry instead of the full procedure body. Fixed by inlining the full procedure directly into the system prompt as a guaranteed fallback, while still preferring the real injected skill whenever it loaded correctly.
- The Orbit graph-query tools were declared in the flow’s own configuration but came back unavailable at runtime, traced to an account-level GitLab Duo preference ("Custom Agents") that was off by default, not a platform or flow bug. Rather than silently degrading or guessing, the flow was built to detect the missing tools explicitly and switch to a clearly labeled file-based fallback mode, tagging every finding made that way as `[INFERRED]` since file-reading is weaker evidence than a real graph traversal.
- After the account toggle was fixed and the flow ran against the real graph, comparing its output to the actual source surfaced two genuine logic bugs: a standalone driver function that exercises an inheritance chain was wrongly bucketed as confidently dead (the exclusion rule for driver functions didn’t generalize to it), and a real, simple, single-hop call was missed by the graph traversal, traced to a separate numeric-ID precision issue and fixed with a single-ID re-check whenever an expected edge unexpectedly came back empty.
Outcome
- Before finding the account toggle, the file-based fallback mode’s guesses were independently checked against the real Orbit graph via the GitLab CLI, run completely outside the flow; every finding held up, including correctly telling apart a method reachable only through inheritance from one nobody calls at all.
- After both logic bugs were fixed, a second run against the real graph correctly reclassified both cases and exactly matched the earlier CLI-verified findings.
- The fallback mode’s disclosure habits (the labeled banner, the `[INFERRED]` tagging) were kept in the flow even after the underlying toggle was fixed, since skill injection can fail independently of Orbit access; treated as a standing resilience pattern, not a one-off patch.
Stack
- Platform
- GitLab Duo Agent Platform: a custom "flow"
- Data source
- GitLab's own Orbit static call graph (query_graph / get_graph_schema)
- Fallback tools
- list_repository_tree / get_repository_file / find_files / blob_search
- Independent verification
- glab CLI (glab orbit remote query), run outside the flow
Honest scope
A hackathon submission validated against Python specifically; other languages aren’t independently verified. It can’t detect transitively dead code (referenced only by other dead code), can’t see usage outside the project’s own indexed scope, can’t see references from non-code files like YAML or CI templates, and has no commit/MR age data available to distinguish "abandoned" from "new and not yet wired up."
Have a technical problem, AI or otherwise, that needs a second pair of eyes?
Let's talk.