Inspiration
Dead code is a slow tax every codebase pays. Functions nothing calls anymore still get read, compiled, ported through every refactor, and reasoned about by every newcomer who cannot tell live code from a fossil. It hides real bugs in branches nobody runs and inflates the surface area that makes a repo intimidating to contribute to. Finding it looks trivial from a code graph: list the definitions nothing references. So people write the naive query, "zero callers, safe to delete," run it once, get burned when it flags a library's public API or an interface method reached by dynamic dispatch, and never trust the tool again. The problem was never the query. A dead-code tool is only useful if it is honest about its false positives, and almost none of them are.
What it does
Orphan Hunter runs the graph query and does the honesty work, producing a tiered ORPHANS.md. Tier 1 (high confidence) is private definitions with zero inbound edges of any kind (nothing calls, extends, or imports them), ranked largest-first because the biggest blocks reclaim the most for the least risk. Tier 2 (suspects) is zero callers but exported or part of the public surface; a library's public API has no internal callers and is dead only if nothing outside uses it, so the tool says verify, not delete. Excluded is zero callers too, but withheld with a reason: entry points, test-only defs, dynamic-dispatch targets, and any definition kind whose calls Orbit cannot resolve in this index. Every report ends with Known blind spots (reflection, FFI, callbacks, function-value assignment, build tags), the concrete ways a live definition can still look dead. Tier 1 means open these and decide, never delete these.
How we built it
Orbit is the engine; Orphan Hunter is an opinionated, honest layer on top. orbit index builds the local DuckDB code graph (definitions, files, imports, typed edges). orbit sql runs the queries, and the interesting ones use relationships, not rows. The candidate pool is an anti-join: callables with no inbound CALLS edge, with both source and target scoped to one commit and branch (scoping only the target side leaks cross-repo edges). Liveness is enriched with EXTENDS (dispatch targets) and a "has any inbound edge of any kind" probe. A resolvable-kinds query is the honesty guard: if Orbit resolves zero calls to a kind (for example TS Method), the tool refuses to call that kind dead and excludes it with a reason. The import signal reads both identifier_name and identifier_alias so default-exported functions are not mistaken for orphans. orbit mcp serve lets any MCP agent run the same queries, so it ships as an Orbit Skill. Pure Python standard library, plus orbit v0.78.
Then we put it where reviewers actually work: the merge request. On every MR pipeline, a job builds the Orbit graph for the changed branch, runs Orphan Hunter, and posts the tiered report back to the MR as a comment, editing the same note in place on re-runs instead of stacking. The honest tiers carry over directly: in our live test MR it flagged two genuinely-dead Go functions in Tier 1, held main in Excluded as an entry point, and never touched the live greet function a naive tool would still have to reason about. We authored this two ways. The intended platform-native path is a GitLab Duo Agent Platform custom flow (flow.yml, flow registry v1): an AgentComponent whose toolset is GitLab's own built-in run_command, read_file, and create_merge_request_note tools, triggered on MR events, publishable to the AI Catalog. Creating Duo Catalog flows/agents is permission-gated to namespaces with Duo enabled (Premium/Ultimate seat or GitLab Credits); on our Free namespace both aiCatalogFlowCreate and aiCatalogAgentCreate return permission denied, so the flow ships as a ready-to-publish artifact and the live, demonstrated integration is the Free-tier CI path (.gitlab-ci.yml + scripts/post_mr_note.py) that delivers the identical auto-posted comment using only CI and the REST API.
Challenges we ran into
The naive "zero callers" approach is wrong in many specific ways, and each one had to be detected from the graph rather than hand-waved. Public API surface needed a visibility heuristic per language. Dynamic dispatch meant whole definition kinds could be systematically unresolvable, so we made the tool detect that and exclude them with a stated reason instead of producing confident garbage. Default exports store the local name in a different field, which would otherwise flag real entry points as dead.
Accomplishments that we're proud of
On cobra it surfaced 7 genuinely-dead private functions (about 175 lines) while correctly not flagging the 85 public-API functions a naive tool would have. On ky it excluded 23 methods as unresolvable dispatch rather than declaring them dead. The win is trust: by tiering and excluding instead of over-claiming, the first run does not burn the user, which is the failure mode that kills every other dead-code tool.
What we learned
A dead-code finder lives or dies on its false-positive handling, not its core query. Surfacing exactly why a candidate might be wrong, and what the tool cannot see, is more valuable than a longer list.
What's next for Orphan Hunter
Close the function-value blind spot by cross-referencing candidates against source-text references (callbacks, func-valued variables). Per-language dispatch resolution to shrink the Excluded tier into real findings. Transitive reachability from entry points for cascade deletion. A CI gate that fails the MR when Tier 1 grows. Publish the Duo custom flow to the AI Catalog once Duo is enabled on the namespace.
Log in or sign up for Devpost to join the conversation.