Across 306 total functions in wshobson/agents, 58 score in the critical band — and the five highest activity-risk functions are all in the tools/ layer, all in the fire quadrant, meaning structural complexity and active commit churn are colliding in the same code right now. I would start with check_stale_artifacts in tools/doc_gardener.py: a cyclomatic complexity of 117 combined with 8 commits in the last 30 days is the clearest live regression risk in the repository. The top-ranked function by activity_risk, parse_frontmatter in tools/adapters/base.py, scores 16.73 — a number driven not just by its structural shape but by the fact that it was touched 2 times in the last 30 days while carrying 57 independent execution paths and calling 25 distinct functions.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Repository Overview
At the time of analysis (commit cc37bfd), I evaluated 306 functions across wshobson/agents. 58 landed in the critical band and 94 are in the fire quadrant — actively changing code with high structural complexity. That combination is where regressions get introduced, not just where debt accumulates.
306 functions analyzed
The antipattern picture reinforces the urgency. Exit-heavy functions — those with many distinct return or exit paths — are the most common pattern across the top hotspots, appearing alongside god functions and complex branching at nearly every rank.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Complex Branching×5Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Deeply Nested×3Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.
Top 5 Hotspots
| Rank | Function | File | Risk Score | Band | CC | ND | FO | Touches (30d) |
|---|---|---|---|---|---|---|---|---|
| 1 | parse_frontmatter | tools/adapters/base.py | 16.73 | critical | 57 | 4 | 25 | 2 |
| 2 | check_stale_artifacts | tools/doc_gardener.py | 16.71 | critical | 117 | 5 | 36 | 8 |
| 3 | uninstall | tools/install_copilot.py | 15.65 | critical | 30 | 5 | 16 | 3 |
| 4 | main | tools/generate.py | 15.24 | critical | 50 | 2 | 32 | 6 |
| 5 | validate_opencode | tools/validate_generated.py | 15.06 | critical | 50 | 5 | 22 | 3 |
parse_frontmatter — base.py
By name and location, parse_frontmatter is responsible for extracting structured metadata from document headers — a foundational operation for the adapter layer in tools/adapters/. Everything downstream that consumes agent definitions likely depends on what this function produces, which makes its structural shape a coupling problem first and a complexity problem second.
A cyclomatic complexity of 57 means there are 57 independent execution paths through this function. Each path is a required test case and a potential place where a caller’s assumptions about the return value diverge from what the function actually produces. In Python, where return types are not enforced at the boundary, that divergence tends to surface at runtime rather than at review time.
The fan-out of 25 distinct callees is the other structural problem. In Python’s dynamically typed runtime, those 25 call sites represent coupling that the type system won’t catch — if any callee’s interface shifts, parse_frontmatter absorbs the breakage silently until a test or a production failure surfaces it. Combined with the hub_function and god_function patterns, this function is both an orchestrator and a monolith: it does too much and touches too many things.
The exit_heavy pattern means there are multiple return paths scattered through those 57 branches. Each exit point is a place where a caller might receive a different shape of output depending on which path fired — that’s difficult to test exhaustively and easy to misread during review.
With 2 commits in the last 30 days (the most recent 20 days ago), this function is actively being changed. An activity-weighted risk score of 16.73 is the highest in the repository. My recommendation: extract the distinct parsing concerns — detecting frontmatter boundaries, tokenizing fields, applying defaults, and validating structure — into separate functions. That alone would bring CC below 15 and make each exit path testable in isolation. PR review comments on this file suggest reviewers are already flagging concerns; the complexity is visible to the team.
check_stale_artifacts — doc_gardener.py
This is the single most structurally complex function in the top five. A cyclomatic complexity of 117 is extreme — by comparison, CC 30 is already a strong refactoring signal, and CC 100+ means the function has accumulated so many conditional branches that no single engineer can hold its full behavior in working memory. From the name, this function likely traverses documentation artifacts and determines which ones are outdated relative to some reference state — a task that naturally accumulates conditionals as edge cases are handled.
The max nesting depth of 5 compounds that problem. At five levels deep, the innermost logic is reasoning about state that was established four conditional layers up. In Python, where indentation is semantically meaningful, deeply nested code is especially hard to extract safely — any refactor has to account for variables that are in scope only because of the outer conditions.
Fan-out of 36 is the highest in the top five. That means 36 distinct functions are called from inside this one. In a Python codebase, where duck typing means many of those call targets may accept — and return — different types depending on runtime context, the coupling here is wider than the raw number suggests.
What makes this a live-fire concern rather than just a cleanup note: 8 commits touched this function in the last 30 days. That is the highest touch count of any function in the top five. Historically, one in three commits to this file has been tagged as a bug fix — supporting context that the complexity is translating into rework, though I can’t attribute that directly to this function alone. Two authors have been active on this file in the last 90 days, which adds coordination overhead on top of the structural complexity.
I would prioritize this function for decomposition before any other in the repository. The immediately actionable step: identify the top-level artifact categories check_stale_artifacts handles and extract each into its own function. Even splitting the function into three similarly-sized pieces would cut CC roughly to 40 each — still high, but reviewable and testable.
uninstall — install_copilot.py
Uninstall routines accumulate complexity naturally — they have to reverse a sequence of operations that may have partially succeeded, handle missing files or registry entries, and exit cleanly regardless of how far the original installation got. A cyclomatic complexity of 30 at nesting depth 5 is consistent with that pattern, and it puts this function right at the threshold where manual reasoning about all paths becomes unreliable.
The exit_heavy and deeply_nested patterns together are the specific risk here. Multiple early-return paths nested five levels deep means that certain cleanup steps may be skipped depending on which branch fires. That kind of silent skip is hard to detect in review and harder to reproduce in tests, because the triggering condition is often a file-system or environment state that’s difficult to simulate.
Three commits in 30 days, with the most recent 16 days ago, puts this in live-fire territory. One in three of the 6 total commits to this file has been tagged as a bug fix — uninstall paths are a common source of edge-case bugs in tooling scripts, and the historical signal is consistent with that. A companion function _is_relative_to in the same file is in the watch quadrant (touched once in 30 days, low structural complexity), which tells me the surrounding file is active but the structural weight is concentrated in uninstall itself.
My recommendation: extract the per-artifact cleanup operations into individual functions, each responsible for one reversible step, and let uninstall become an orchestrator that calls them in sequence. That restructuring would flatten the nesting, reduce CC significantly, and make each cleanup step independently testable.
main — generate.py
main in a generator script is almost always an orchestrator — it parses arguments, dispatches to subsystems, handles errors, and exits. The nesting depth of 2 confirms that: the control flow is relatively flat, which is what you’d expect from a function that calls out to other things rather than embedding logic inline. But a cyclomatic complexity of 50 with fan-out of 32 means it’s doing an enormous amount of dispatching, with 50 distinct paths through the decision tree and 32 different callees.
This is the hub_function-adjacent profile: low nesting but very high fan-out and CC. In Python, a main that calls 32 functions is a function where adding or changing any one of those 32 callees requires the author to understand how that callee fits into all 50 possible execution paths. That’s a disproportionate cognitive load for what should be a thin entry point.
The external signals on this file are the strongest in the top five. Across 14 total commits, nearly half have been tagged as bug fixes, and at least one change was reverted. I want to be careful not to overstate this — these are file-level signals and don’t prove main was the defective function in each case. But the pattern is consistent with a function that’s been patched repeatedly because its full behavior is hard to reason about from a single read.
Six commits in the last 30 days is high churn for a single orchestrating function. My recommendation: identify the major dispatch branches — argument parsing, validation, generation, output handling, error reporting — and extract each into a named function. The goal is a main with CC under 10 that reads like a table of contents for the script’s behavior.
validate_opencode — validate_generated.py
validate_opencode almost certainly validates generated output specific to the opencode tool integration — checking structure, required fields, format conformance, or semantic correctness of whatever tools/generate.py produces. Validation functions tend to grow by accretion: each new case that needs checking adds a branch, and over time CC drifts upward without any single addition feeling large.
A cyclomatic complexity of 50 at nesting depth 5 is the same CC as main but with significantly deeper nesting — that combination is harder to reason about than flat high-CC code, because the logic at the inner levels depends on the state established by the outer conditions. The complex_branching and deeply_nested patterns both fire here, and god_function and long_function indicate the validation logic hasn’t been decomposed into smaller, named checks.
Across 17 total commits — the highest count of any file in the top five — nearly 30% have been tagged as bug fixes, and at least one change was reverted. Two authors have been active on this file in the last 90 days. Again, these are file-level signals, but they suggest the validation logic has been a recurring source of rework.
Three touches in 30 days with the last change 18 days ago keeps this in the fire quadrant. My recommendation: decompose validate_opencode into a set of single-responsibility validators — one per field or structural concern — and have the parent function aggregate their results. That pattern reduces CC in the parent to roughly the number of validators called, makes each validation rule independently testable, and means a new validation requirement adds one new function rather than another branch in an already complex function.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 6 |
complex_branching | 5 |
god_function | 5 |
long_function | 4 |
deeply_nested | 3 |
hub_function | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
Reproduce This Analysis
git clone https://github.com/wshobson/agents
cd agents
git checkout cc37bfdd292ce520ba1c44df7a3a70d5f8137236
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →