| name | risk-assessment |
| description | Assess project/domain health — scan risk zones across dimensions (bugs, hotspots, tech debt). Use when asked evaluate risks, find problematic areas, assess code health, identify zones needing attention — NOT for specific bug symptoms (use bug-hunt instead) |
| argument-hint | [scope — domain, subsystem, or 'whole project'] |
Risk Assessment
Phase Order (MANDATORY — do not skip any phase)
🛑 STOP — read all 5 phases before scanning.
- Phase 1 — SCAN with primary preset
- Phase 2 — SCAN with stratified second preset (MUST run, even if Phase 1
found enough hits — single-preset risk maps are biased)
- Phase 3 — MERGE results with negativeIds dedup (MUST use negativeIds, NEVER
manual filtering)
- Phase 4 — CLASSIFY into severity tiers
- Phase 5 — REPORT pair-diagnostics (MUST surface signal pairs, single-signal
reports are misleading)
Top Anti-patterns (read before scanning)
- Using bug-hunt for risk assessment. bug-hunt finds ONE root cause. This
skill scans the risk surface.
- Single unfiltered scan for broad scope. Dominant-churn domain takes 100%
of slots. Always run stratified second scan with
!**/dominant/**.
- Classifying from a single signal. "High churn" alone implies no class.
Check companion signals (
imports, bugFixRate, ageDays, blockPenalty)
before picking a label. See references/signal-interpretation.md.
Full list: references/anti-patterns.md.
Multi-dimensional risk scan via rank_chunks × 4 rerank presets, cross-referenced
by overlap count. Semantic/hybrid search resolves intent-based scopes.
Rules
- Execute YOURSELF — no subagents.
- No
git log, git diff, git blame — overlay has git signals.
- No built-in Search/Grep for code discovery — use tea-rags tools only.
- Search results contain code when metaOnly=false. Evaluate from search
results BEFORE any Read or navigation.
- Partial reads only.
Read(path, offset=startLine, limit=endLine-startLine) using coordinates
from search results. Never read full files.
- Minimize tool calls. Batch: all rank_chunks one message, all Critical
UUIDs one find_similar, all symbol names one hybrid_search. Target: ≤12 calls
domain scope, ≤16 broad scope.
Flow
0. SCOPE RESOLUTION → pathPattern + scopeType
1. SCAN → rank_chunks × 4 presets (parallel)
2. MERGE → cross-reference by relativePath, assign tiers
3. EXPAND → find_similar from Critical only
4. ENRICH → partial Read + test coverage + structural axis (codegraph) + classify
5. OUTPUT → top-10 risk map + structural risks
Phase 0: SCOPE RESOLUTION
Translate $ARGUMENTS into pathPattern and scopeType.
Shortcut: pathPattern provided directly as argument (e.g., delegated from
explore PG-2) → use as-is, scopeType = "domain", skip resolution.
$ARGUMENTS describes...
├─ Broad ("whole project", "all code", no specific area)
│ → pathPattern = none
│ → scopeType = "broad"
│
├─ Domain/directory ("ingest domain", "explore/", "adapters")
│ → pathPattern = "**/ingest/**"
│ → scopeType = "domain"
│
└─ Intent/concept ("enrichment pipeline", "error handling")
→ ONE search call (concept/behavior → semantic_search,
named symbol + context → hybrid_search):
query = extracted concept, language = primary, limit = 10
→ extract directory prefixes from result relativePaths:
- Shared prefix → "**/enrichment/**"
- 2-3 clusters → "{**/dir1/**,**/dir2/**}"
- Scattered → no pathPattern (scan everything)
→ scopeType = "intent"
pathPattern rules: Never brace full file paths with slashes — breaks
picomatch. Always extract directory-level prefixes.
One call only. Scope resolution not exhaustive.
Phase 1: SCAN
Run rank_chunks × 4 presets. All 4 calls in ONE message (parallel).
| Preset | Surfaces |
|---|
bugHunt | Burst activity + volatility + bug fix rate |
hotspots | Chunk-level churn + burst + instability |
techDebt | Old + churny + bug-prone + dense code |
dangerous | Bug-prone + volatile + single-owner (bus factor) |
Codegraph transparency: when codegraph active (prime ## Enrichment lists
codegraph.symbols), techDebt and dangerous already absorb structural
signals via reranker override — no parameter change, risk map sharpens
automatically. Explicit structural axis (blast-radius hubs + cycles) added in
Phase 4, not here.
Parameters per call:
rank_chunks:
path: <project>
rerank: <preset>
language: <primary language> ← omit on polyglot codebases
pathPattern: <from Phase 0>
metaOnly: false ← REQUIRED (content needed for EXPAND)
limit: 10
Polyglot: 2+ languages each >10% chunks → omit language filter. Group by
language in OUTPUT.
Domain-stratified scanning (broad scope only):
Unfiltered rank_chunks returns results dominated by highest-churn domain.
Other domains invisible regardless of actual risk.
After first scan (4 presets × no pathPattern):
1. Identify dominant domain:
Count unique relativePath directory prefixes across all results.
The domain with the most slots is dominant.
2. ALWAYS run second scan (broad scope):
4 presets × pathPattern = "!**/dominant-domain/**"
Same parameters, same limit.
Feed both scans into Phase 2 MERGE.
Doubles scan calls for broad scope (8 instead of 4), but guarantees every domain
gets representation. Cost acceptable: rank_chunks is scroll operation, not
vector search. No threshold — always run both scans.
Empty results: preset returns 0 → exclude from overlap count. N = presets
with results (may be < 4).
Pagination: Stop conditions per-preset:
- Gradient drop > 2× average adjacent gap → stop
- < 3 new unique files on page → stop
- Hard cap: 3 pages (offset 0, 10, 20)
One page usually sufficient.
Phase 2: MERGE
Cross-reference by relativePath (primary key). Within same file, chunks
overlap if [startLine, endLine] ranges intersect >50%.
| Overlap | Tier | Meaning |
|---|
| N/N | Critical | All active dimensions converge |
| N-1/N | High | Strong multi-signal risk |
| N-2/N | Medium | Two signals, may be expected |
| 1/N | — | Single signal — exclude from output |
Sorting within tier: Label severity scores: critical/extreme = 4,
concerning/erratic/high = 3, typical = 1, healthy/low/stable = 0. Sum
across all overlay signals. Sort descending.
Healthy demotion: candidate's bugFixRate healthy across ALL presets that
found it → demote one tier (Critical → High, High → Medium). High overlap +
healthy bugFixRate = active development churn, not risk.
Zero Critical/High: Skip EXPAND + ENRICH. Output Medium candidates + "No
critical risks found. Codebase appears healthy by multi-signal analysis."
Phase 3: EXPAND
find_similar from Critical (N/N) candidates only.
Negative contrast (healthy-demoted as negativeIds):
Phase 2 MERGE produces healthy-demoted candidates: high preset overlap but
healthy bugFixRate. Structurally similar to Critical candidates but
well-maintained — exact opposite of antipatterns. Use as negative examples to
sharpen find_similar toward risky code:
find_similar vector direction:
positive = Critical candidates (buggy, churny, oversized)
negative = healthy-demoted from MERGE (active but clean)
→ result space shifts AWAY from "active development" TOWARD "antipattern"
Collect negativeIds from ALL healthy-demoted candidates in Phase 2 (any tier).
No healthy-demoted candidates → skip negativeIds.
Batch expansion — pass ALL Critical chunk UUIDs in one call:
find_similar:
positiveIds: [<all Critical UUIDs>] ← batch, not per-candidate
negativeIds: [<demoted UUIDs>] ← healthy-demoted from MERGE
path: <project>
limit: 10
rerank: bugHunt ← surface risky similar, not just similar
pathPattern: <see scope rules>
Two-pass for broad scope only:
- Pass 1 (in-domain): no pathPattern → 1 call
- Pass 2 (cross-domain):
pathPattern = "!**/dominant-domain/**" → 1 call
Domain/intent scopes: Pass 1 only (same pathPattern as Phase 0). Total: 1 call.
Label results as "Related risk" (pass 1) or "Cross-domain risk" (pass 2).
Scope rules embedded in two-pass description above.
Filter by overlay: Include only results with concerning+ signals (bugFixRate
concerning+, OR churnVolatility erratic+, OR blameContributorCount = 1 —
single live-line owner). Healthy overlay → ignore.
Add qualifying results as "Related risk" under parent Critical candidate.
Phase 4: ENRICH
For Critical and High candidates (typically 5-10 chunks):
1. Code review — Content in results (metaOnly=false). Read only when
surrounding context needed. Use chunk coordinates.
2. Test coverage check — ONE hybrid_search, all Critical/High symbol names
joined as query, pathPattern targeting the project's test directory
convention, metaOnly=true. BM25 catches exact symbol names in test files. One
call covers all candidates.
- Symbol absent from results → "untested risk zone"
- Symbol present → note test path (do NOT read test content)
3. Decomposition check — Run rank_chunks decomposition preset, scoped to
same pathPattern. Cross-reference Critical/High candidates by relativePath. Risk
candidate also decomposition candidate (methodLines label = high+ from labelMap)
→ add "Oversized" classification. NOT a 4th MERGE preset — decomposition
measures size, not risk. Post-filter on already-identified risk zones.
3b. Structural amplifier + cycles (codegraph axis). ONLY when prime shows
codegraph.symbols under ## Enrichment. Line absent → graph tools not
registered — skip, note structural risk not assessed (never claim "no cycles" /
"no hubs"). See search-cascade "Graph navigation" for off-routing.
- Blast-radius amplifier (
architecturalHub). Run rank_chunks
rerank="architecturalHub" scoped to same pathPattern. Cross-reference
resulting isHub=true / high-fanIn files with Critical/High candidates by
relativePath. Risk candidate ALSO a hub = blast-radius hub — escalate
(tag Risk Type, sort to top of its tier): change there ripples across many
dependents. Amplifier on already-identified risk, NOT a 5th MERGE preset —
clean high-fanIn hub with healthy git signals is backbone, not risk.
- Cycles (
find_cycles). Run find_cycles scope=file pathPattern=<scope>.
Circular dependencies = structural risk churn presets cannot see. Noise guard:
20 cycles unscoped → narrow by subdomain. Empty result with codegraph ON =
valid "no cycles (DAG)". Surface findings in OUTPUT Structural risks
section.
4. Risk classification — from overlay labels + tier + test coverage.
BEFORE picking a class, consult pair diagnostics. Single overlay signals
ambiguous. references/signal-interpretation.md gives pair/triple rules that
disambiguate patterns (god module vs bug attractor, healthy owner vs toxic silo,
active development vs coupling, legacy minefield vs proven stable). Read
whenever overlay shows more than one strong signal.
Key disambiguators (always check before classifying):
imports (fan-in, file-level) separates coupling (high) from bug attractor
(low). Codegraph on → prefer real fanIn / isHub / transitiveImpact
signals over imports proxy — they measure call/import edges, not raw
import-line count. See signal-interpretation "Structural signals" +
blast-radius-hub / cyclic-coupling patterns.
bugFixRate separates healthy (stable) from fragile (unstable)
ageDays inverts churn meaning (old+churn = minefield, young+churn = feature)
blameDominantAuthorPct alone does NOT mean silo; pair with bugFixRate or age
recentDominantAuthorPct = activity concentration (who's committing lately),
NOT who owns live code — only blame* speaks to ownership
- path heuristic (
dto/, schema/, generated/) flags boilerplate churn
File × chunk refinement. File-level signals point to which file. Chunk-level
signals (chunk.bugFixRate, chunk.ageDays, chunk.relativeChurn,
chunk.blameContributorCount, chunk.recentContributorCount) point to which
method inside. Overlay shows both → chunk-level locates exact problem:
- Coupling point → find chunk with highest
chunk.recentContributorCount
(recently-touched-by-many — overloaded API)
- Knowledge silo zoom-in → find chunk with
chunk.blameContributorCount = 1
(single live-line owner of a method inside a shared file)
- Legacy minefield → find chunk with highest
chunk.bugFixRate + chunk.relativeChurn
- Bug attractor → find chunk with highest
chunk.bugFixRate
- Fossil vs active legacy →
chunk.ageDays inside old file
See references/signal-interpretation.md § "Method-level (chunk) pair
diagnostics" for the full table.
See references/classification-tiers.md
for the full 13-tier table.
Phase 5: OUTPUT
Top-10 risk map, sorted by tier → severity sum.
Risk Assessment: [scope]
Scanned: [N chunks across M presets], [K unique files]
## Critical (N/N presets)
| # | Symbol | File:Line | Risk Type | Key Signals | Tests |
|---|--------|-----------|-----------|-------------|-------|
| 1 | symbol() | file.ts:42 | Bug magnet, Untested | bugFix:58% concerning churn:high | none |
Related risks (find_similar):
- relatedFn() file.ts:85 — same pattern [bugFix:50% concerning]
## High (N-1/N presets)
| # | Symbol | File:Line | Risk Type | Key Signals | Tests |
|---|--------|-----------|-----------|-------------|-------|
| 2 | symbol() | file.ts:149 | Fragile | volatility:erratic burst:extreme | 776 LOC |
## Medium (N-2/N) — [count] candidates
Count only. "Show medium risks" to expand.
## Structural risks (codegraph axis)
Only when codegraph is active. Omit the whole section (or state "structural risk
not assessed — codegraph off") when prime has no `codegraph.symbols`.
- **Blast-radius hubs** — Critical/High candidates that are also `isHub` /
high-`fanIn`: `symbol() file.ts:line — fanIn:N, blast-radius`.
- **Cycles** — from `find_cycles`: `a.ts → b.ts → a.ts` (or "no cycles — DAG").
## Summary
- Critical zones: [count] — require immediate attention
- High zones: [count] — schedule for review
- Test gaps: [count] untested files among Critical/High
- Dominant risk type: [most common classification]
- Recommendation: [one-sentence next step]
Label mapping: Use labelMap from get_index_metrics (session start). Show
raw value + label: bugFix:58% concerning.