assets/schema.json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "inspector-report-v0.8.0",
"title": "Inspector Report",
"description": "Output schema for the inspector agent v0.8.0",
"type": "object",
"required": [
"inspector_version",
"timestamp",
"repo_root",
"areas",
"layer_map",
"architectural_context",
"checks_applied",
"findings",
"summary"
],
"additionalProperties": false,
"properties": {
"inspector_version": {
"type": "string",
"description": "Inspector agent version that produced this report"
},
"timestamp": {
"type": "string",
"format": "date-time",
"description": "ISO8601 timestamp when the report was produced"
},
"repo_root": {
"type": "string",
"description": "Absolute path to the repository root"
},
"areas": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Areas audited, as provided in the prompt"
},
"layer_map": {
"type": "string",
"description": "Architectural layer map committed during Step 0, e.g. 'cmd → engine → protocol → result'"
},
"architectural_context": {
"type": "string",
"description": "One-sentence summary of what orientation found about established patterns"
},
"checks_applied": {
"type": "array",
"items": {
"type": "string",
"enum": [
"dead_symbol",
"layer_violation",
"scope_analysis",
"coverage_gap",
"silent_failure",
"duplicate_semantics",
"cross_field_consistency",
"test_coverage",
"error_wrapping",
"doc_drift",
"interface_saturation",
"panic_not_recovered",
"context_propagation",
"init_side_effects"
]
},
"description": "Check types that were applied in this run"
},
"findings": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"check_type",
"severity",
"confidence",
"file",
"line",
"description",
"tool",
"recommendation"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z_]+:.+:[0-9]+$",
"description": "Deterministic ID: check_type:repo-relative-file:line"
},
"check_type": {
"type": "string",
"enum": [
"dead_symbol",
"layer_violation",
"scope_analysis",
"coverage_gap",
"silent_failure",
"duplicate_semantics",
"cross_field_consistency",
"test_coverage",
"error_wrapping",
"doc_drift",
"interface_saturation",
"panic_not_recovered",
"context_propagation",
"init_side_effects"
]
},
"severity": {
"type": "string",
"enum": ["error", "warning"]
},
"confidence": {
"type": "string",
"enum": ["high", "reduced"],
"description": "high = LSP produced the result; reduced = Grep fallback used"
},
"file": {
"type": "string",
"description": "Repo-relative file path"
},
"line": {
"type": "integer",
"minimum": 1
},
"symbol": {
"type": "string",
"description": "Symbol name if applicable (optional)"
},
"description": {
"type": "string",
"description": "What was found"
},
"tool": {
"type": "string",
"description": "Which tool produced the result, e.g. 'LSP findReferences: 0' or 'LSP unavailable — Grep fallback'"
},
"recommendation": {
"type": "string",
"description": "Concrete recommended fix"
}
}
}
},
"summary": {
"type": "object",
"required": ["total", "by_severity", "by_confidence", "by_check_type", "not_checked"],
"additionalProperties": false,
"properties": {
"total": {
"type": "integer",
"minimum": 0
},
"by_severity": {
"type": "object",
"required": ["error", "warning"],
"additionalProperties": false,
"properties": {
"error": { "type": "integer", "minimum": 0 },
"warning": { "type": "integer", "minimum": 0 }
}
},
"by_confidence": {
"type": "object",
"required": ["high", "reduced"],
"additionalProperties": false,
"properties": {
"high": { "type": "integer", "minimum": 0 },
"reduced": { "type": "integer", "minimum": 0 }
}
},
"by_check_type": {
"type": "object",
"additionalProperties": { "type": "integer", "minimum": 0 },
"description": "Count per check type"
},
"not_checked": {
"type": "object",
"required": ["out_of_scope", "tooling_constraints"],
"additionalProperties": false,
"properties": {
"out_of_scope": {
"type": "array",
"items": { "type": "string" },
"description": "Things excluded by design"
},
"tooling_constraints": {
"type": "array",
"items": { "type": "string" },
"description": "Things that could not be checked due to tooling (LSP unavailable, cross-repo inaccessible, etc.)"
}
}
}
}
}
}
}
references/check-taxonomy.md
# Check Taxonomy
Each check type has a defined tool strategy. Use it rather than improvising.
---
### `dead_symbol`
A symbol is defined but never referenced at runtime.
**Tool strategy:**
1. Locate the symbol definition with Grep to get exact file:line:character
2. **Tier 1A (preferred) — call `mcp__lsp__get_change_impact(changed_files=[file], include_transitive=false)`.** Returns `affected_symbols` with per-symbol `non_test_callers` and `test_callers` counts for all exported symbols in the file at once.
- `non_test_callers == 0 AND test_callers == 0` → dead
- `non_test_callers == 0 AND test_callers > 0` → test-only (warning, not dead)
- Annotate: `[LSP Tier 1A — get_change_impact: N non-test callers, M test callers]`
- If `mcp__lsp__get_change_impact` is unavailable or errors: proceed to Tier 1B.
3. **Tier 1B (fallback) — call `mcp__lsp__get_references` with the exact file_path, line, and character from step 1.** A dead_symbol finding is invalid without this call (either Tier 1A or Tier 1B must succeed).
4. **If `mcp__lsp__get_references` also errors or is unavailable:** fall back to Grep across the full repo — mark finding as `[LSP unavailable — Grep fallback, reduced confidence]`
5. Zero references outside the definition site = dead. Report the LSP result verbatim: `[LSP findReferences: N references]`
**Optional cross-repo extension (`cross_repo_dead_symbol`):** If `consumer_roots` are provided (via `--consumer-repos` flag), after a symbol is classified dead or test-only by Tier 1A/1B, call `mcp__lsp__get_cross_repo_references(symbol_file, line, column, consumer_roots)`. If any references are found in consumer repos, reclassify as live and annotate: `[cross-repo live — N references in consumer repos]`.
**Severity:** warning if the symbol has a comment suggesting future use; error otherwise.
---
### `layer_violation`
A package or module imports something it should not, crossing a boundary in the layer map established in Step 0.
**Tool strategy:**
1. Read the file's import block
2. Check each import against the layer map from Step 0
3. Trace the dependency direction — A imports B is only a violation if B is downstream of A or if A and B are declared peers that should not depend on each other
**Severity:** error if the import creates a cycle or crosses a hard boundary; warning for soft boundary violations.
---
### `scope_analysis`
A function, class, or module is doing too many things. Natural split points exist.
**Tool strategy:**
1. Read the full function/module
2. List each distinct responsibility (I/O, validation, transformation, coordination, etc.)
3. Identify natural split points — places where a sub-function has a clear single purpose
4. Note nesting depth as a signal (3+ levels often indicates bundled concerns)
5. Compare to peer functions sampled in Step 0 — outliers are more meaningful than absolute thresholds
**Severity:** warning if 3–4 responsibilities; error if 5+ or if a single responsibility spans more than ~100 lines and could be independently tested.
---
### `coverage_gap`
A code path, input scenario, or error condition is not handled and will fail silently or produce incorrect behavior.
**Tool strategy:**
1. Read the validation or control flow logic
2. Enumerate the handled cases explicitly
3. Identify what is NOT handled — edge cases, error returns, missing branches
4. Confirm the gap is reachable (not dead code)
**Severity:** error if the unhandled case is reachable from normal inputs; warning if it requires unusual preconditions.
---
### `silent_failure`
An error is caught, logged, or ignored rather than returned or propagated, allowing execution to continue in a bad state.
**Tool strategy:**
1. Read the function's error handling paths
2. Flag any error that is assigned to `_`, logged without return, or used only in a condition that does not abort the function
3. Check if downstream code depends on state that may be invalid due to the suppressed error
**Severity:** error if downstream state is affected; warning if the suppressed error is truly recoverable.
---
### `duplicate_semantics`
Two or more symbols (error codes, types, functions, constants) represent nearly the same concept, creating ambiguity for callers.
**Tool strategy:**
1. Read the definitions of the candidate symbols
2. Compare their descriptions, names, and emission/call contexts
3. Check if callers distinguish between them or treat them interchangeably
**Severity:** warning if the distinction is documented and intentional; error if callers cannot meaningfully distinguish them.
---
### `cross_field_consistency`
Two or more fields in a struct, schema, or configuration must be consistent with each other, but no validation enforces this.
**Tool strategy:**
1. Read the type or schema definition
2. Identify fields that reference each other (by name, by value range, by meaning)
3. Search the validation layer for checks that enforce the relationship
4. If no check exists, confirm by tracing what happens when the fields are inconsistent
5. Use LSP `hover` on related symbols to verify types and constraints
**Severity:** error if inconsistency causes silent data corruption or a runtime panic; warning if it produces a recoverable error.
---
### `test_coverage`
An exported symbol (function, method, type) has no corresponding test.
**Tool strategy:**
1. Enumerate exported symbols in the area with Grep (`^func [A-Z]`, `^type [A-Z]`, etc.)
2. **Tier 1A (preferred) — call `mcp__lsp__get_change_impact(changed_files=[file], include_transitive=false)` per file.** Use the `test_callers` field for each symbol — this includes enclosing test function names, more precise than Grep. Symbols with `test_callers == 0` lack test coverage. Annotate: `[LSP Tier 1A — get_change_impact: M test callers]`. If unavailable, proceed to Tier 1B.
3. **Tier 1B (fallback) — call `mcp__lsp__get_references` on the symbol definition position** to confirm whether test files are callers. Grep misses aliased calls; LSP gives ground truth.
4. **Grep fallback:** if both Tier 1A and Tier 1B are unavailable, search test files (`*_test.go`, `*.test.ts`, `test_*.py`, etc.) for the symbol name with Grep. Mark as `[LSP unavailable — Grep fallback, reduced confidence]`.
5. Flag symbols with zero test references. Report `[LSP findReferences: N references, M in test files]`
**Severity:** error for public API surface (exported and called externally); warning for internal helpers that are exported but only used within the package.
---
### `error_wrapping`
An error is returned without adding context, making the call stack opaque at the call site.
**Tool strategy:**
1. Read error return paths in the function
2. Flag bare `return err` where `err` came from a call into another package
3. Check the wrapping convention established in Step 0 (e.g., `fmt.Errorf`, `%w`, `errors.Wrap`) — apply consistently with what the codebase already does
4. Do not flag: errors already containing context, sentinel errors intended to be passed through, or errors at the top of the call stack (main, handler boundary)
**Severity:** warning — missing context degrades debuggability but does not cause incorrect behavior.
---
### `doc_drift`
A function's documentation no longer matches its signature or behavior.
**Tool strategy:**
1. Read the function signature and its doc comment
2. **REQUIRED — call `mcp__lsp__get_info_on_location` with the exact file_path, line, and character of the function name.** Gives the canonical signature the compiler sees, independent of what the source text says. A doc_drift finding is invalid without this call.
3. If `mcp__lsp__get_info_on_location` is unavailable: fall back to direct Read — mark finding as `[LSP unavailable — Read fallback, reduced confidence]`
4. Compare hover output against the doc comment: parameter names, types, return values, described behavior. Flag mismatches.
**Severity:** warning — doc drift misleads callers but does not cause runtime failures. Error only if the drift describes incorrect error conditions (caller may suppress errors they shouldn't).
---
### `interface_saturation`
An interface or abstract type has too many methods, preventing callers from using it narrowly.
**Tool strategy:**
1. Read the interface definition and count methods
2. **REQUIRED — call `mcp__lsp__get_references` on the type name position to identify all implementors and callers.** Interface saturation findings are invalid without this call.
3. For each caller, check how many methods it actually uses
4. Check if a coherent subset of the methods forms an independently useful contract
**Language patterns:** Go interfaces, Java/C# abstract classes, Python protocols, TypeScript interfaces, Rust traits.
**Severity:** warning if 6+ methods and callers demonstrably use a narrow subset; error if the interface is the only way to reach a core behavior and splitting it would eliminate a forced dependency on unrelated methods.
---
### `panic_not_recovered`
An unhandled crash occurs in a concurrent or long-running context without a recovery mechanism.
**Tool strategy:**
1. Grep for crash-inducing calls:
- Go: `panic(`
- Python: `raise` inside threads/async without try/except
- JavaScript/TypeScript: `throw` inside async functions or Promise callbacks without `.catch`
- Rust: `unwrap()`, `expect()` on `Option`/`Result` in async contexts
2. For each site, check if the enclosing concurrent context has a recovery mechanism
3. Flag sites in goroutines, threads, async tasks, or long-running server loops
4. Do not flag: crashes in main entry points or test helpers
**Severity:** error in goroutines/threads/async contexts; warning in synchronous long-running functions where a caller-level handler may exist.
---
### `context_propagation`
A function receives a cancellation token but creates a fresh root context for callees, breaking cancellation propagation.
**Tool strategy:**
1. Grep for functions accepting a cancellation parameter:
- Go: `ctx context.Context`
- C#: `CancellationToken`
- JavaScript/TypeScript: `AbortSignal`, `signal`
2. Within each, grep for fresh root context construction passed to callees:
- Go: `context.Background()`, `context.TODO()`
- C#: `new CancellationToken()`
- JS: `new AbortController()`
3. Flag cases where fresh root replaces the received token
4. Do not flag: derived contexts created from the received one (e.g., `context.WithTimeout(ctx, ...)`)
**Severity:** warning in general; error on request-handling paths where cancellation is critical for resource cleanup.
---
### `init_side_effects`
A module initializer performs observable side effects — I/O, global mutation, network calls.
**Tool strategy:**
1. Grep for module-level initializers:
- Go: `func init()`
- Python: module-level statements outside `if __name__ == "__main__"`
- JavaScript/TypeScript: module-level code with side effects
- Java/Kotlin: `static {}` blocks
2. Read the body of each initializer
3. Flag: file I/O, network calls, global variable mutation conditioned on external state, process-exit calls
4. Do not flag: registering constants, building lookup tables from compile-time data, pure deterministic assignments
**Severity:** warning — couples test setup to module import order; error if the side effect can fail at import time with no recovery path.
---
### `cross_repo_dead_symbol`
A symbol appears dead within its own repo but may be consumed by external repos that are not indexed by the local LSP server. Only applicable when `--consumer-repos` is provided.
**Tool strategy:**
1. Identify symbols classified as dead or test-only by `dead_symbol` check
2. **REQUIRED — call `mcp__lsp__get_cross_repo_references(symbol_file, line, column, consumer_roots)` for each candidate.** `consumer_roots` comes from the `--consumer-repos` flag (comma-separated list of absolute repo root paths).
3. If any references are returned from consumer repos: reclassify as live. Annotate: `[cross-repo live — N references in consumer repos]`. Remove from dead_symbol report.
4. If zero cross-repo references: confirm dead across all known consumers. Annotate: `[cross-repo verified dead — checked N consumer repos]`.
5. If `mcp__lsp__get_cross_repo_references` is unavailable or errors: skip cross-repo check. Note in "Not Checked — Tooling Constraints" section.
**Severity:** inherited from the underlying dead_symbol finding. Cross-repo verification upgrades confidence, not severity.
**Note:** This check only runs when `--consumer-repos` is provided. It does not appear in reports when the flag is absent.
references/output-format.md
# Output Format
## Default (markdown)
```
## Summary
- Audited: [list of areas]
- Layer map: [the boundary map committed in Step 0]
- Highest severity: [error / warning / none]
- Signal: [one sentence overall assessment]
## [Area Name]
[For each finding:]
**[check_type]** · [severity] · [confidence: high | reduced]
`file:line` · [LSP findReferences: N | LSP hover: confirmed | LSP unavailable — Grep fallback]
What: [what was found]
Fix: [concrete recommendation]
## All Findings
| Severity | Confidence | Check Type | Finding | Location |
|----------|------------|------------|---------|----------|
...
[sorted error → warning, then high confidence → reduced confidence]
## Not Checked — Out of Scope
[Things excluded by design: areas not requested, check types skipped via --checks, etc.]
## Not Checked — Tooling Constraints
[Things that could not be checked due to tooling: LSP unavailable, cross-repo
inaccessible, file not readable, etc. Each entry should state what was attempted
and why it failed.]
```
## JSON (`--json` flag)
```json
{
"inspector_version": "0.8.0",
"timestamp": "<ISO8601>",
"repo_root": "<absolute path>",
"areas": ["<area1>", "<area2>"],
"layer_map": "<committed layer map from Step 0>",
"architectural_context": "<one sentence: what orientation found>",
"checks_applied": ["<check_type>"],
"findings": [
{
"id": "<check_type>:<repo-relative-file>:<line>",
"check_type": "<check type>",
"severity": "error | warning",
"confidence": "high | reduced",
"file": "<repo-relative path>",
"line": 42,
"symbol": "<symbol name if applicable>",
"description": "<what was found>",
"tool": "LSP findReferences: N | LSP hover: confirmed | LSP unavailable — Grep fallback",
"recommendation": "<what to do>"
}
],
"summary": {
"total": 0,
"by_severity": { "error": 0, "warning": 0 },
"by_confidence": { "high": 0, "reduced": 0 },
"by_check_type": { "<check_type>": 0 },
"not_checked": {
"out_of_scope": ["<item>"],
"tooling_constraints": ["<item>"]
}
}
}
```
**Finding ID format:** `<check_type>:<repo-relative-file>:<line>` — e.g. `dead_symbol:pkg/result/codes.go:138`. Deterministic and stable across runs, enabling diff-mode comparison between reports.
**Confidence field:** `"high"` when LSP produced the result; `"reduced"` when Grep fallback was used.
## Persistence (`--output <path>`)
Write the report using the Write tool. Path must be under `docs/inspections/` or end with `-inspection.md` / `-inspection.json`. Format determined by `--json` flag.
The "Not Checked" sections are required in all modes — a clean result is only meaningful if the scope is explicit.
scripts/validate-report
#!/usr/bin/env bash
# validate-report — validate an inspector JSON report against the schema
#
# Usage:
# validate-report <report.json>
# cat report.json | validate-report
#
# Requires: python3 (standard library only — uses jsonschema if available,
# falls back to structural validation otherwise)
#
# Exit codes:
# 0 — valid
# 1 — invalid (errors printed to stdout)
# 2 — usage error or file not found
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCHEMA_FILE="${SCRIPT_DIR}/../assets/schema.json"
if [ ! -f "$SCHEMA_FILE" ]; then
echo "ERROR: schema.json not found at ${SCHEMA_FILE}" >&2
exit 2
fi
# Read input
if [ $# -eq 1 ]; then
INPUT_FILE="$1"
if [ ! -f "$INPUT_FILE" ]; then
echo "ERROR: file not found: ${INPUT_FILE}" >&2
exit 2
fi
INPUT=$(cat "$INPUT_FILE")
elif [ ! -t 0 ]; then
INPUT=$(cat)
else
echo "Usage: validate-report <report.json>" >&2
echo " cat report.json | validate-report" >&2
exit 2
fi
# Validate using Python (input passed via stdin to avoid injection)
echo "$INPUT" | python3 - "$SCHEMA_FILE" <<'PYEOF'
import json, sys
report_text = sys.stdin.read()
schema_path = sys.argv[1]
try:
report = json.loads(report_text)
except json.JSONDecodeError as e:
print(f"INVALID: JSON parse error: {e}")
sys.exit(1)
with open(schema_path) as f:
schema = json.load(f)
errors = []
# Try jsonschema if available
try:
import jsonschema
validator = jsonschema.Draft7Validator(schema)
for error in sorted(validator.iter_errors(report), key=lambda e: list(e.path)):
path = ".".join(str(p) for p in error.path) or "(root)"
errors.append(f" {path}: {error.message}")
except ImportError:
# Fallback: structural validation
required = schema.get("required", [])
for field in required:
if field not in report:
errors.append(f" (root): missing required field '{field}'")
# Check findings structure
for i, finding in enumerate(report.get("findings", [])):
for req_field in ["id", "check_type", "severity", "confidence", "file", "line", "description", "tool", "recommendation"]:
if req_field not in finding:
errors.append(f" findings[{i}]: missing required field '{req_field}'")
if "severity" in finding and finding["severity"] not in ("error", "warning"):
errors.append(f" findings[{i}].severity: must be 'error' or 'warning', got '{finding['severity']}'")
if "confidence" in finding and finding["confidence"] not in ("high", "reduced"):
errors.append(f" findings[{i}].confidence: must be 'high' or 'reduced', got '{finding['confidence']}'")
if "id" in finding:
parts = finding["id"].split(":")
if len(parts) < 3:
errors.append(f" findings[{i}].id: must be check_type:file:line format, got '{finding['id']}'")
# Check summary structure
summary = report.get("summary", {})
for req_field in ["total", "by_severity", "by_confidence", "by_check_type", "not_checked"]:
if req_field not in summary:
errors.append(f" summary: missing required field '{req_field}'")
not_checked = summary.get("not_checked", {})
for req_field in ["out_of_scope", "tooling_constraints"]:
if req_field not in not_checked:
errors.append(f" summary.not_checked: missing required field '{req_field}'")
if errors:
print(f"INVALID: {len(errors)} error(s) found")
for e in errors:
print(e)
sys.exit(1)
else:
version = report.get("inspector_version", "unknown")
finding_count = len(report.get("findings", []))
print(f"VALID: inspector v{version}, {finding_count} finding(s)")
sys.exit(0)
PYEOF
SKILL.md
---
name: inspect
description: Launch a code quality inspector agent to audit defined areas of a codebase. Language-agnostic. Applies a fixed check taxonomy — dead symbols, layer violations, scope overload, coverage gaps, silent failures, duplicate semantics, cross-field consistency, missing tests on exported symbols, unwrapped errors, doc drift, interface saturation, unrecovered panics, context propagation breaks, and init side effects — using LSP-first tool strategies with Tier 1A batch analysis via mcp__lsp__get_change_impact. Returns a severity-tiered findings report with per-finding confidence levels and active LSP tier annotation. Supports --json for structured output, --output for persistence, --checks to target specific check types, and --consumer-repos for cross-repo dead symbol verification. Use when auditing files, packages, or cross-cutting concerns for any of these patterns.
compatibility: Requires an agent runtime that supports subagent delegation and tool use (e.g. Claude Code).
allowed-tools: Agent(subagent_type=inspector), mcp__lsp__start_lsp, mcp__lsp__open_document, mcp__lsp__close_document, mcp__lsp__get_references, mcp__lsp__get_change_impact, mcp__lsp__get_cross_repo_references, mcp__lsp__get_document_symbols, mcp__lsp__get_diagnostics, mcp__lsp__get_info_on_location, mcp__lsp__get_code_actions, mcp__lsp__call_hierarchy, mcp__lsp__go_to_definition, mcp__lsp__go_to_implementation, mcp__lsp__get_server_capabilities
argument-hint: "<path-or-description> [<path-or-description> ...] [--json] [--output <path>] [--checks <type1>,<type2>]"
user-invocable: true
metadata:
schema: assets/schema.json
validator: scripts/validate-report
---
# /inspect — Code Quality Inspection
Launch an inspector agent to audit one or more areas of the codebase.
## Usage
```
/inspect <area> [<area> ...] [--json] [--output <path>] [--checks <type1>,<type2>]
```
Areas can be:
- A file path: `/inspect pkg/result/codes.go`
- A package: `/inspect pkg/engine`
- A description: `/inspect "error handling across the validation layer"`
- Multiple areas: `/inspect pkg/result/codes.go pkg/protocol/validation.go`
**Flags:**
- `--json` — emit structured JSON instead of markdown (machine-readable, enables downstream tooling)
- `--output <path>` — persist report to disk; path must be under `docs/inspections/` or end in
`-inspection.md` / `-inspection.json`. Example: `--output docs/inspections/2026-04-04.md`.
If omitted, defaults to `docs/inspections/<datetime>.md` (e.g. `docs/inspections/2026-04-11T14-32-00.md`).
- `--checks <type1>,<type2>` — apply only the listed check types, skipping others. Example:
`--checks dead_symbol,layer_violation`
- `--consumer-repos <root1>,<root2>` — optional comma-separated list of consumer repo absolute
paths. Enables cross-repo dead symbol verification: symbols classified as dead locally are
checked against consumer repos via `mcp__lsp__get_cross_repo_references` before being reported.
Activates the `cross_repo_dead_symbol` check type.
## What it checks
The inspector applies these checks where relevant — you do not need to specify them:
| Check | What it finds |
|-------|--------------|
| `dead_symbol` | Defined but never referenced (Tier 1A: `mcp__lsp__get_change_impact` batch → high confidence; Tier 1B: `mcp__lsp__get_references` → high confidence; Grep fallback → low confidence) |
| `layer_violation` | Import crosses an architectural boundary |
| `scope_analysis` | Function or module doing too many things |
| `coverage_gap` | Unhandled input, error, or code path |
| `silent_failure` | Error suppressed rather than returned |
| `duplicate_semantics` | Two symbols that mean the same thing |
| `cross_field_consistency` | Related fields with no consistency enforcement |
| `test_coverage` | Exported symbol with no test references (Tier 1A: `mcp__lsp__get_change_impact` test_callers field → more precise than Grep; Tier 1B: `mcp__lsp__get_references`; Grep fallback) |
| `error_wrapping` | Error returned without context (opaque call stack) |
| `doc_drift` | Function documentation no longer matches its signature |
| `interface_saturation` | Interface with too many methods; callers use a narrow subset |
| `panic_not_recovered` | Unhandled crash in a goroutine, thread, or async context |
| `context_propagation` | Function receives a context/token but creates a fresh root for callees |
| `init_side_effects` | Module initializer performs I/O, network calls, or global mutation |
## Execution
Launch the inspector agent with the user's areas as input. Pass the current working
directory as the repo root. **Always set `run_in_background: true`** so the audit runs
asynchronously and the user can continue working while it runs.
**Pre-flight: warm up LSP and ensure permissions.** Background agents cannot receive
interactive permission prompts for MCP tools. Two requirements:
1. The user's global settings must include `mcp__lsp__*` tools in `permissions.allow`
(in `~/.claude/settings.json`). Without this, every LSP call from the background
agent will be denied silently and the inspector will hang.
2. Call `mcp__lsp__start_lsp` in the parent session first, then set the gate flag:
```bash
# 1. Start LSP in the parent session (prompts once for permission — approve it)
mcp__lsp__start_lsp(root_dir="<repo_root>")
# 2. Set the global ready flag so the inspector gate hook passes for background agents
touch /tmp/.inspector-lsp-global-ready
```
```
Launch inspector agent with:
- Areas to inspect: [user's areas]
- Repo root: [resolve the actual repo root from the area path — e.g. if area is /Users/x/code/my-repo/pkg/foo, repo root is /Users/x/code/my-repo]
- Flags: pass through --json, --checks as provided. For --output: if the user provided a path, use it; if omitted, default to `docs/inspections/<YYYY-MM-DDTHH-MM-SS>.md` using the current datetime relative to the repo root
- run_in_background: true
- Instructions: apply the check taxonomy, report findings with severity and file:line citations
- First instruction to agent: DO NOT call mcp__lsp__start_lsp (already running, gate flag is set). Go directly to Step 0 open_document calls, then warm-up check.
```
**Critical: LSP tool usage.** Include this instruction verbatim in the inspector agent's
launch prompt — the agent definition alone is not sufficient:
> **LSP enforcement:** You have two LSP tool surfaces. Use them in this priority order:
>
> **Step 0 — startup sequence (required, do this first, in order):**
>
> 1. **Initialize** pointing at the correct repo root (`start_lsp` is idempotent — safe to call even if already running):
> `mcp__lsp__start_lsp(root_dir="<repo_root>")`
>
> 2. **Open one file per package** you plan to audit. gopls does not index a package
> until at least one file in it is opened. Without this, `get_references` returns
> "no package metadata" for all symbols in that package:
> ```
> mcp__lsp__open_document(file_path="<repo_root>/internal/lsp/client.go", language_id="go")
> mcp__lsp__open_document(file_path="<repo_root>/internal/tools/workspace.go", language_id="go")
> # … one representative file per package being audited
> ```
>
> 3. **Warm-up check (mandatory before trusting zero-reference results):**
> Pick one symbol you know is actively used (e.g. a widely-called function in the
> first package). Call `get_references` on it. If it returns `[]`, the workspace
> is not yet indexed — wait 3–5 seconds and retry. Do not proceed to dead-symbol
> checks until a known-active symbol returns ≥ 1 reference.
>
> **1A. `mcp__lsp__get_change_impact` (Tier 1A — batch, preferred for `dead_symbol` and `test_coverage`):**
> Call once per file; returns all exported symbols with `non_test_callers` and `test_callers` counts.
> Example: `mcp__lsp__get_change_impact(changed_files=["/abs/path/file.go"], include_transitive=false)`
> `non_test_callers == 0 AND test_callers == 0` → dead. `non_test_callers == 0 AND test_callers > 0` → test-only.
> If unavailable or errors: proceed to Tier 1B.
>
> **1B. `mcp__lsp__get_references` (Tier 1B — per-symbol fallback for `dead_symbol`):**
> Call this for per-symbol reference lookups. Returns 1-based locations.
> Example: `mcp__lsp__get_references(file_path="/abs/path/file.go", language_id="go", line=22, column=6)`
> Zero results = dead symbol (high confidence). If the call errors, fall back to option 2.
>
> **2. `LSP` built-in tool (fallback or for hover/other operations):**
> Use for hover, go-to-definition, and as fallback when `mcp__lsp-mcp` is unavailable.
> `LSP(operation="hover", filePath="/abs/path/file.ts", line=14, character=10)`
>
> Do NOT shell out to gopls/rust-analyzer/tsserver via Bash.
> If both LSP surfaces fail, fall back to Grep and annotate as reduced confidence.
The agent works autonomously. When it completes you will be notified — surface the report
directly to the user at that point.
## JSON output and validation
When `--json` is passed, the agent emits a structured report conforming to
[assets/schema.json](assets/schema.json). To validate a report:
```bash
scripts/validate-report report.json
# or: cat report.json | scripts/validate-report
```
Exit 0 = valid, 1 = schema errors, 2 = usage error.