Inspiration

Most dead-code tools answer "what breaks if I change this." That question is already crowded, I found over a dozen other submissions doing some version of it in this same hackathon. I wanted the narrower question instead: is anyone actually calling this. And specifically, the honest version of that question, where the tool admits when it genuinely can't tell.

A static call graph can't always tell the difference between code that's dead and code that's invoked dynamically: through a decorator-based dispatch table, through a test runner's reflection over class attributes, through inheritance and MRO resolution, or through something entirely outside the indexed scope. Most dead-code tools either over-flag these cases and lose trust the first time they're wrong, or quietly skip the hard parts and only report what's easy. I wanted something that does neither: it tells you exactly what it found, exactly how confident it is, and exactly what it couldn't check.

What it does

It's a GitLab Duo Agent Platform flow. When triggered on an issue or MR, it queries GitLab Orbit's knowledge graph for CALLS and IMPORTS edges on every Definition node in the project, then sorts every finding into one of three buckets instead of a single verdict:

  • Confident — zero incoming call/import edges anywhere, not behind a decorator, not at an external-trigger entry point
  • Uncertain — ambiguous cases the procedure can't fully resolve, like inheritance/MRO dispatch or second-order test-exclusion effects
  • Skipped — decorator-based dispatch, test-framework reflection, or hardware/firmware entry points, explicitly labeled "cannot assess statically" instead of being silently dropped

It's strictly read-only. It never deletes, renames, or modifies a file, and never opens a merge request. It posts one report on the triggering issue and stops.

It also has a fallback mode that turned out to matter a lot during testing (see Challenges below). If Orbit's graph-query tools aren't actually available at runtime, the flow doesn't fail silently or guess. It switches to reading the project's real source files directly, applies the same decorator/test-discovery/inheritance heuristics wherever file-level reading allows it, and opens the report with an unmissable banner stating exactly which tools were missing. Every finding in that mode gets labeled [INFERRED] rather than confident, because file-reading is genuinely weaker evidence than a real call-graph traversal, and the report says so.

How we built it

The core is a project-level SKILL.md that defines the full Orbit query procedure step by step: look up the project's Orbit node, pull every Definition with its file path and type, check incoming edges for each one, and classify into the three buckets above.

Two parts of that procedure came out of real debugging, not guesswork:

  1. The constructor/dunder correction. Early testing showed that __init__, __enter__, __exit__, and similar dunder methods almost always look dead in a naive check, because instantiating a class registers a CALLS edge to the class, not to __init__ itself. I confirmed this directly against a real ~500-line SDK class (EphemeralAgentExecutor) that gets instantiated dozens of times across a full unittest suite. Once I corrected the check to look at the enclosing class's incoming edges instead of the dunder method's own edges, the false positives went away.

  2. Decorator-based dispatch and test-framework discovery. A function registered into a dict via a decorator and called later by string-key lookup is structurally indistinguishable from dead code to a static call graph. Same problem with unittest.TestCase methods, which the test runner discovers through reflection over class attributes, not through any literal call in source. Both get explicitly excluded into the Skipped bucket rather than flagged dead, with the reasoning stated in the report.

I validated all of this against test fixtures I built myself: plain unused functions, cross-file import resolution, an inheritance/MRO case where a method is only reachable through a subclass that doesn't override it, a decorator-dispatch case, and the constructor/dunder case above with the real SDK class and its test suite.

Challenges we ran into

The biggest one wasn't in the procedure logic, it was the platform. Two things came up during live testing.

First, workspace_agent_skills injection was unreliable. The project-level SKILL.md file was correctly placed and readable, but at runtime the flow sometimes received only the skill's manifest entry instead of the actual procedure body, or failed to load it at all. Second: the Orbit graph-query tools, query_graph and get_graph_schema, weren't present in the flow's runtime toolset, even though I'd explicitly declared them in the flow's YAML configuration.

I confirmed Orbit itself was healthy and fully queryable, by running the real SKILL.md procedure directly via glab orbit remote query CLI and getting real CALLS/IMPORTS results back, so the gap wasn't Orbit's health. It turned out to be a per-account setting I hadn't found yet: GitLab ships an "Orbit in GitLab Duo" preference with separate toggles for Agentic Chat, the standalone Orbit Agent, other foundational agents, and Custom Agents specifically. All four were off by default. Enabling Custom Agents gave the flow real tool access immediately.

Once that was on, a second round of testing surfaced two real bugs in the procedure itself rather than the platform: a standalone driver function (use_it, which instantiates a class and calls a method chain to exercise it) was wrongly flagged as confidently dead, since the procedure's "single-definition main file" exclusion didn't generalize to functions shaped like that. And one simple, direct function call was missed entirely in one run, most likely a numeric-ID precision issue I'd separately observed elsewhere in this environment. I fixed both directly in SKILL.md, and a follow-up run confirmed both were corrected.

Rather than block on any of this, I inlined the full procedure directly into the flow's system prompt as a guaranteed fallback, and built a file-based fallback mode for when Orbit tools are genuinely unavailable, with explicit instructions to disclose that loudly rather than quietly substitute a weaker analysis.

Accomplishments that we're proud of

Two early runs against the test project, in fallback mode before I found the account toggle, correctly identified every planted dead-code fixture with cited file and line evidence, correctly excluded the decorator-dispatch and test-discovery cases with real reasoning for each, and correctly downgraded ambiguous cases to Uncertain instead of overclaiming.

I then validated the same procedure two more ways. First via CLI, running the real SKILL.md procedure directly against the live Orbit graph outside the flow entirely, which confirmed every fallback-mode finding and surfaced a new lead, add_temp_file and issue_credential, two SDK methods with no incoming calls that file-reading alone couldn't fully confirm. Then, after fixing the account toggle, through the flow itself: a real run using actual query_graph calls for every classification, which independently re-confirmed add_temp_file and issue_credential as genuinely dead, matching the CLI finding exactly across two separate methods.

That same flow run also caught two real bugs, which I'm honestly more proud of catching than I would be of a flawless first run: a standalone test-driver function got misclassified, and one direct function call got missed. I checked the actual report against the actual source files rather than assuming a graph-backed report is automatically correct, found both issues, fixed the procedure, and a second run confirmed the fixes worked.

What we learned

Disclosing a limitation honestly is more valuable than hiding it. A report that says "I don't know" in the cases where it genuinely doesn't know is more trustworthy than one that guesses everywhere and sounds confident doing it. That principle ended up applying at the platform level too: once I confirmed the Orbit tools genuinely weren't available, the right move was to surface that gap loudly in the report rather than mask it with a silent fallback that looked like the real thing.

What's next for Dead Code Finder

The main gap I described earlier, getting Orbit access into the flow itself, turned out to be a one-toggle fix on my own account, not a platform limitation. That's now resolved and validated end to end. What's left: extend validation past Python to confirm cross-language coverage, add transitive dead-code detection for code only referenced by other dead code, and keep an eye on the numeric-ID precision issue I ran into once, since it could cause a real edge to be missed silently in a larger project where it's harder to spot by inspection.

Built With

  • gitlab-duo-agent-platform
  • gitlab-orbit
  • python
  • yaml
Share this project:

Updates