health-check
Run a comprehensive codebase health check — tests, linting, coverage, complexity, and dependency audit. Compare with previous runs to detect regressions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Run a comprehensive codebase health check — tests, linting, coverage, complexity, and dependency audit. Compare with previous runs to detect regressions.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Run the implement pipeline over multiple backlog tickets in one session. Per ticket: spawn architect → spawn developer → spawn reviewer (the same three-phase pipeline $implement runs), then move to the next. Sequential by default; parallel only when the user explicitly opts in AND the tickets are independent. Reports an aggregated verdict at the end. Use when the user invokes `$batch-implement #N #M #K` or `$batch-implement --status todo`.
Implement a single backlog ticket through a multi-phase pipeline: architect plans (OpenSpec proposal+design+tasks+specs), sr-developer codes in TDD order, sr-reviewer validates (correctness, tests, security, performance). A profile may add custom-* rails. Reads .specrails/local-tickets.json, closes the ticket in place, reports concisely. Use when the user invokes `$implement #N` or `$implement <free-form>`.
Architect role for the specrails implement pipeline. Reads a backlog ticket, surveys the repo, produces (a) an OpenSpec change package under openspec/changes/<slug>/ and (b) a plan artefact under .specrails/agent-memory/explanations/. Does NOT write production code. Invoked by the implement orchestrator via $sr-architect after a spawn_agent / send_message handoff.
Developer role for the specrails implement pipeline. Reads the architect's design + tasks.md and implements them in TDD order: for each task, write a failing test first, run it to confirm it fails, then write the minimum production code to make it pass, then re-run. Reports the files changed. Does NOT review its own work beyond the per-task test cycle. Invoked by the implement orchestrator via $sr-developer.
Reviewer role for the specrails implement pipeline. Validates the entire implementation: the OpenSpec change package (proposal/design/tasks/specs) is well-formed, the developer's code matches the design's public API and invariants, every tasks.md box is ticked, the tests cover every spec scenario, and the project's full test/build suite passes. Writes a confidence-score.json artefact. Does NOT modify the developer's code. Invoked via $sr-reviewer.
sr:batch-implement — Batch implementation orchestrator. Accepts multiple feature references, computes dependency-aware execution waves, invokes sr:implement per wave.
| name | health-check |
| description | Run a comprehensive codebase health check — tests, linting, coverage, complexity, and dependency audit. Compare with previous runs to detect regressions. |
| license | MIT |
| compatibility | Requires git. |
| metadata | {"author":"specrails","version":"1.0"} |
Run a full health check for this project (read from CLAUDE.md or package.json): detect available tools, execute each quality check, compare results against the previous run, detect regressions, compute a health grade, and store a snapshot for future comparison.
Input: $ARGUMENTS — optional flags:
--since <date> — use the report from this date (ISO format: YYYY-MM-DD) as the comparison baseline instead of the most recent--only <checks> — comma-separated subset to run. Valid values: tests, coverage, lint, complexity, deps, perf, static--save — always save the snapshot even when --only is used (default: skip save for partial runs)Parse $ARGUMENTS to set runtime variables.
Variables to set:
COMPARE_DATE — string (ISO date) or empty string. Default: "" (use most recent report).CHECKS_FILTER — array of check names or the string "all". Default: "all".SAVE_SNAPSHOT — boolean. Default: true when CHECKS_FILTER="all", false for partial runs unless --save is present.Parsing rules:
$ARGUMENTS for --since <date>. If found, set COMPARE_DATE=<date>. Strip from arguments.--only <checks>. If found:
<checks> on commas to produce an array.tests, coverage, lint, complexity, deps, perf, static.Error: unknown check "<value>". Valid checks: tests, coverage, lint, complexity, deps, perf, static and stop.CHECKS_FILTER=<validated-array>.SAVE_SNAPSHOT=false (partial run — snapshot may be incomplete).--save. If found, set SAVE_SNAPSHOT=true regardless of CHECKS_FILTER.Print active configuration:
Running checks: <all | comma-separated list> | Static analysis: <enabled|disabled> | Comparing to: <COMPARE_DATE or "latest">
Detect available tools for each check category. Run all detections simultaneously (in parallel). For each category, try tools in the order listed — use the first one found.
If CHECKS_FILTER is not "all", skip detection for categories not in the filter.
For each category, set two variables:
TOOL_<CHECK> — the tool name or command string (e.g., "jest", "eslint")TOOL_<CHECK>_AVAILABLE — boolean (true / false)Detection sequences:
tests: Try in order: jest, vitest, mocha, pytest, go test, cargo test, rspec, dotnet test. If none found, check whether auto-detect from CLAUDE.md or package.json scripts provides a test command — use it as fallback and set TOOL_TESTS="ci-commands".
coverage: Try in order: nyc, c8, pytest-cov, coverage (Python), go test -cover, cargo tarpaulin, lcov.
lint: Try in order: eslint, pylint, flake8, ruff, golangci-lint, rubocop, cargo clippy.
complexity: Try in order: lizard, radon, gocyclo, plato. If none found, set TOOL_COMPLEXITY_AVAILABLE=false — complexity will be estimated from linter output if lint ran.
deps: Try in order: npm audit, pip-audit, govulncheck, cargo audit, bundle audit.
perf: Look for a performance entry point at these paths in order:
scripts/perf.shscripts/benchmark.sh"perf" or "benchmark" script key in package.jsonperf or benchmark target in MakefileIf found, set TOOL_PERF=<path-or-command> and TOOL_PERF_AVAILABLE=true. Otherwise set TOOL_PERF_AVAILABLE=false.
Detection summary table (print after all probes complete):
| Category | Available | Tool |
|------------|-----------|-------------------|
| tests | yes/no | <tool or N/A> |
| coverage | yes/no | <tool or N/A> |
| lint | yes/no | <tool or N/A> |
| complexity | yes/no | <tool or N/A> |
| deps | yes/no | <tool or N/A> |
| perf | yes/no | <tool or N/A> |
| static | yes | ai-analysis |
The static check is always available (AI-assisted analysis requires no external tools).
Read .claude/health-history/ to find the comparison baseline.
Variables to set:
PREV_REPORT_PATH — absolute file path or nullIS_FIRST_RUN — booleanPREV_REPORT — parsed JSON object or nullLogic:
Check whether .claude/health-history/ exists and contains .json files.
IS_FIRST_RUN=true, PREV_REPORT_PATH=null, PREV_REPORT=null. Print: First run — no previous report found. Regression comparison is not available. Proceed.If reports exist and COMPARE_DATE is empty: select the most recently modified .json file.
If COMPARE_DATE is set: find the report whose filename date component is closest to COMPARE_DATE (without exceeding it). If no report matches within 7 days, print: Warning: no report found near <COMPARE_DATE>. Falling back to most recent. Then use the most recent.
Set IS_FIRST_RUN=false, PREV_REPORT_PATH=<path>, load file content into PREV_REPORT.
Print one line:
Baseline: first run (no comparison)Comparing to: <YYYY-MM-DD> (<short-sha from filename>)Run checks sequentially in this order: tests, coverage, lint, complexity, deps, perf. Sequential execution avoids resource contention that would skew timing and coverage metrics.
For each check, follow this pattern:
Skip condition: If TOOL_<CHECK>_AVAILABLE=false OR check is excluded by CHECKS_FILTER:
RESULT_<CHECK> = { status: "skipped", tool: null, metrics: null }<check>: SKIPPEDRun: Execute the tool with the command shown below. If the tool exits non-zero: set status: "fail", capture the error message, record whatever partial metrics are available, and continue — do NOT abort the command.
Store: Set RESULT_<CHECK> to a structured object with status, tool, and metrics fields.
Run command (first tool that matches):
jest: jest --json 2>/dev/null — parse JSON stdout for numPassedTests, numFailedTests, numPendingTests, testResults[].durationvitest: vitest run --reporter=json 2>/dev/null — parse JSON for equivalent fieldsmocha: mocha --reporter json 2>/dev/null — parse stats objectpytest: pytest --tb=no -q 2>&1 — extract pass/fail/skip counts from summary linego test: go test ./... -v 2>&1 — count --- PASS, --- FAIL, --- SKIP linescargo test: cargo test 2>&1 — parse test result: summary linerspec: rspec --format json 2>/dev/null — parse JSONdotnet test: dotnet test --logger "console;verbosity=normal" 2>&1 — extract summaryci-commands fallback: run auto-detect from CLAUDE.md or package.json scripts and extract pass/fail counts from output using best-effort parsingMetrics to extract: tests_total, tests_passed, tests_failed, tests_skipped, pass_rate (0.0–100.0), duration_seconds.
Set RESULT_TESTS.
Run command:
nyc / c8: nyc report --reporter=text-summary 2>/dev/null or c8 report --reporter=text-summary 2>/dev/null — parse "Statements" or "Lines" coverage percentagepytest-cov: re-run as pytest --cov --cov-report=term-missing -q 2>&1 or read .coverage via coverage report 2>/dev/nullcoverage (Python): coverage report 2>/dev/null — extract TOTAL line percentagego test -cover: go test -cover ./... 2>&1 — extract coverage: N.N% from each package, compute meancargo tarpaulin: cargo tarpaulin --out Stdout 2>/dev/null — extract coverage percentagelcov: lcov --summary coverage.info 2>/dev/null — extract lines-found/lines-hitMetrics to extract: coverage_pct (float), coverage_type ("line" / "branch" / "statement").
Set RESULT_COVERAGE.
Run command:
eslint: eslint . --format json 2>/dev/null — count severity: 2 (errors) and severity: 1 (warnings) across all results; count files analyzedpylint: pylint --output-format json <src-dir-or-.> 2>/dev/null — count messages by type (error, warning)flake8: flake8 --format default . 2>&1 — count lines with E prefix (errors) vs W prefix (warnings)ruff: ruff check --output-format json . 2>/dev/null — parse JSON array, count by severitygolangci-lint: golangci-lint run --out-format json 2>/dev/null — count issues by severityrubocop: rubocop --format json 2>/dev/null — parse offenses by severitycargo clippy: cargo clippy --message-format json 2>/dev/null — count "level":"error" and "level":"warning" messagesMetrics to extract: lint_errors, lint_warnings, lint_files_checked. Compute lint_score = max(0, 100 - lint_errors * 5 - lint_warnings * 1).
Set RESULT_LINT.
If TOOL_COMPLEXITY_AVAILABLE=false:
RESULT_LINT is available (lint ran and has output): set complexity_source: "estimated" — use Claude's reasoning to estimate complexity signals from lint output (e.g., complexity-related lint rules fired). Set numeric metrics to null.complexity_source: "unavailable", all metrics null, status: "skipped".Run command (if tool available):
lizard: lizard . --csv 2>/dev/null — parse CSV, compute average CCN, max CCN, count functions with CCN > 10radon: radon cc . -a -j 2>/dev/null — parse JSON, use average_complexity field; count items with complexity > 10gocyclo: gocyclo -over 10 . 2>/dev/null and gocyclo -avg . 2>/dev/null — extract average and functions exceeding thresholdplato: plato -r -d /tmp/plato-report . 2>/dev/null && cat /tmp/plato-report/report.json — extract summary.average.maintainabilityMetrics to extract: avg_cyclomatic_complexity (float), max_cyclomatic_complexity (int), high_complexity_functions (int, count with CCN > 10), complexity_source ("measured" / "estimated" / "unavailable").
Set RESULT_COMPLEXITY. Status is "measured" when a tool ran, "estimated" when inferred, "skipped" when neither is possible.
Run command:
npm audit: npm audit --json 2>/dev/null — parse vulnerabilities object; count by severity fieldpip-audit: pip-audit --format json 2>/dev/null — parse JSON array; count by fix_versions presence and severitygovulncheck: govulncheck ./... 2>&1 — extract vulnerability counts from summary; if no JSON flag, use Claude's reasoning on text outputcargo audit: cargo audit --json 2>/dev/null — parse vulnerabilities.list, count by advisory.severitybundle audit: bundle audit check 2>&1 — parse output for Insecure Source and Unpatched versions; classify by severity from advisory textMetrics to extract: vuln_critical, vuln_high, vuln_moderate, vuln_low, vuln_total.
Set RESULT_DEPS.
If TOOL_PERF_AVAILABLE=false: set RESULT_PERF = { status: "skipped", tool: null, metrics: null }, print perf: SKIPPED, continue.
Run the detected entry point. After it completes, attempt to parse its stdout for these standard keys:
p50, p50_ms, median_ms → perf_p50_msp95, p95_ms → perf_p95_msp99, p99_ms → perf_p99_msperf_customIf the script output does not contain recognizable keys, set all latency fields to null and store the raw output in perf_custom.raw.
Metrics to extract: perf_p50_ms, perf_p95_ms, perf_p99_ms, perf_custom.
Set RESULT_PERF.
Phase 3 summary (print after all checks):
tests: <PASS|FAIL|SKIPPED> (<tool>)
coverage: <PASS|FAIL|SKIPPED> (<tool>)
lint: <PASS|FAIL|SKIPPED> (<tool>)
complexity: <MEASURED|ESTIMATED|SKIPPED> (<tool>)
deps: <PASS|FAIL|SKIPPED> (<tool>)
perf: <PASS|FAIL|SKIPPED> (<tool>)
Perform static code inspection to detect issues that tools cannot surface: missing documentation, broken imports, and unused exports. These checks are language-aware and use AI-assisted code reading rather than external tooling.
Always exclude the following directories from analysis:
node_modules/, .git/, dist/, build/, vendor/, .claude/, coverage/For each finding, record a structured object:
{ severity: "critical"|"warning"|"info", check: string, file: string, line: number, description: string, action: string }
Set STATIC_FINDINGS = [] before starting. Append each finding to this list.
Scan public-facing code units (exported functions, classes, interfaces, modules) for missing documentation comments.
Language-specific rules:
export keyword. Check whether a JSDoc block (/** ... */) appears immediately above the declaration. Missing JSDoc on exported symbols = finding.# @param, ##, or any comment block) immediately above the definition. Missing comment on public methods in lib/ = finding.// <ExportedName>) immediately above. Missing comment on exported symbols = finding.Severity assignment:
critical: Main entry points (e.g., index.ts, main.go, app.rb) with no module-level documentationwarning: Exported/public function or class with no documentationinfo: Non-exported function over 20 lines with no documentationScan all source files matching the detected language(s). Skip test files (*.test.*, *.spec.*, _test.go, spec/**).
Append each finding to STATIC_FINDINGS.
Detect import/require statements that reference non-existent paths. Focus on local relative imports (skip node_modules, stdlib, installed packages — these are validated by the runtime).
Language-specific rules:
import ... from '...' and require('...') statements. For each import path starting with . or /:
.ts, .tsx, .js, .jsx, /index.ts, /index.js).critical finding.from . import, from .. import, and import X with relative markers. Resolve relative paths from the package root. If the resolved module file does not exist: add a critical finding.require_relative '...' statements. Resolve paths from the source file's directory (try .rb extension if missing). If the resolved path does not exist: add a critical finding.Severity: Always critical — a broken import is a runtime error.
Append each finding to STATIC_FINDINGS.
Detect exported symbols that are never imported anywhere else in the codebase. Limit to the project source — do not flag exports intended as a library's public API (i.e., symbols re-exported from a barrel file like index.ts).
Language-specific rules:
export const, export function, export class, export type, export interface, export default across all non-test .ts/.tsx/.js files.warning (may be an intentional API export) unless the file is not index.ts and not under src/ root — in that case severity is info.__all__ or public top-level definitions (non-underscore).info finding.lib/. Search for require or direct references elsewhere. If a class/module in lib/ is never referenced outside its own file: add a warning finding.Severity: warning for clearly unexported contexts; info when the symbol could be part of a public API.
Append each finding to STATIC_FINDINGS.
After all three static checks complete, print:
documentation: <N findings> (<N critical, N warning, N info>)
broken_imports: <N findings> (<N critical>)
unused_exports: <N findings> (<N warning, N info>)
Static findings total: <N>
Using all RESULT_<CHECK> values and PREV_REPORT (if IS_FIRST_RUN=false), compute the final health report object.
For each numeric metric, compute delta = current_value - prev_value. If IS_FIRST_RUN=true, set all deltas to "N/A (first run)".
Delta notation convention:
A regression is triggered when any of the following thresholds is crossed vs. the previous report:
| Check | Threshold |
|---|---|
| tests | pass_rate drops by more than 1% |
| coverage | coverage_pct drops by more than 2 percentage points |
| lint | lint_errors increases vs. previous |
| lint | lint_score drops by more than 5 points |
| complexity | high_complexity_functions increases vs. previous |
| deps | vuln_critical increases vs. previous |
| deps | vuln_high increases vs. previous |
| perf | perf_p50_ms increases by more than 10% vs. previous |
If IS_FIRST_RUN=true: set REGRESSIONS=[] (no regression detection possible on first run).
Build REGRESSIONS as a list of objects: { check, metric, previous, current, delta }.
Evaluate criteria in order from F to A; assign the first grade whose criteria are met:
| Grade | Criteria |
|---|---|
| F | Test suite fails to run (RESULT_TESTS.status = "fail") OR pass_rate < 50% |
| D | Multiple regressions detected (len(REGRESSIONS) >= 2) OR pass_rate < 80% OR vuln_critical > 2 |
| C | One regression detected OR pass_rate 80–89% OR vuln_critical > 0 |
| B | No critical regressions. Any one of: pass_rate 90–94%, OR coverage_pct 70–79%, OR lint_errors 1–5, OR vuln_high <= 2 |
| A | No regressions. pass_rate >= 95%. coverage_pct >= 80% (if measured). lint_errors == 0 (if measured). vuln_critical == 0 AND vuln_high == 0 (if measured). |
When IS_FIRST_RUN=true, regressions cannot be detected — base the grade on absolute metric thresholds only (no regression criteria apply).
When a check is SKIPPED, omit its metric from grade criteria (do not penalize for unavailable tools).
Build HEALTH_REPORT as a structured object matching the JSON storage schema exactly:
HEALTH_REPORT = {
schema_version: "1",
project: "this project (read from CLAUDE.md or package.json)",
timestamp: <ISO 8601 current datetime>,
git_sha: <full SHA from `git rev-parse HEAD` or "unknown">,
git_short_sha: <7-char SHA from `git rev-parse --short HEAD` or "unknown">,
git_branch: <branch from `git rev-parse --abbrev-ref HEAD` or "unknown">,
checks: {
tests: { status, tool, metrics: { tests_total, tests_passed, tests_failed, tests_skipped, pass_rate, duration_seconds } },
coverage: { status, tool, metrics: { coverage_pct, coverage_type } },
lint: { status, tool, metrics: { lint_errors, lint_warnings, lint_score, lint_files_checked } },
complexity: { status, tool, metrics: { avg_cyclomatic_complexity, max_cyclomatic_complexity, high_complexity_functions, complexity_source } },
deps: { status, tool, metrics: { vuln_critical, vuln_high, vuln_moderate, vuln_low, vuln_total } },
perf: { status, tool, metrics: { perf_p50_ms, perf_p95_ms, perf_p99_ms, perf_custom } }
},
static_findings: <STATIC_FINDINGS array — each: { severity, check, file, line, description, action }>,
grade: <"A"|"B"|"C"|"D"|"F">,
regressions: <REGRESSIONS array>,
comparison_report: <PREV_REPORT_PATH basename or null>
}
Render the health report to the terminal using Markdown formatting:
## Codebase Health Report — this project (read from CLAUDE.md or package.json)
Date: <ISO date> | Commit: <git_short_sha> | Compared to: <previous report date or "first run">
Overall Grade: <A/B/C/D/F> (<one-line summary>)
### Test Suite [<PASS/FAIL/SKIPPED>]
Tests: N passed, N failed, N skipped (N total)
Pass rate: N% <delta: (+N%) or (-N%) or N/A (first run)>
Duration: Xs
### Code Coverage [<PASS/FAIL/SKIPPED/ESTIMATED>]
Coverage: N% <delta vs previous>
Type: line/branch/statement
### Linting [<PASS/FAIL/SKIPPED>]
Score: N/100 <delta vs previous>
Errors: N Warnings: N
### Complexity [<MEASURED/ESTIMATED/SKIPPED>]
Avg CCN: N Max CCN: N
High-complexity functions: N (>10 CCN) <delta vs previous>
### Dependencies [<PASS/FAIL/SKIPPED>]
Vulnerabilities: N critical, N high, N moderate, N low
### Performance [<PASS/FAIL/SKIPPED>]
p50: Nms p95: Nms p99: Nms <delta vs previous>
---
Regressions detected: N
<if N > 0, list each:>
- <check>: <metric> changed from X to Y (<delta>)
<if N == 0:>
No regressions detected.
---
### Static Analysis Findings [<N total: N critical, N warning, N info>]
<if STATIC_FINDINGS is empty:>
No static findings detected.
<if STATIC_FINDINGS is non-empty:>
| Severity | Check | File | Line | Finding | Action |
|----------|-------|------|------|---------|--------|
<for each finding in STATIC_FINDINGS, sorted by severity (critical first, then warning, then info):>
| <CRITICAL|WARNING|INFO> | <check> | <file> | <line> | <description> | <action> |
<if any critical findings:>
#### Critical Findings (require immediate attention)
<for each critical finding:>
**[CRITICAL] <check>** — `<file>:<line>`
> <description>
> **Action:** <action>
<if any warning findings (limit to top 5 by file breadth — prefer findings spread across more files):>
#### Warnings (recommended fixes)
<for each warning finding, up to 5:>
**[WARNING] <check>** — `<file>:<line>`
> <description>
> **Action:** <action>
<if more than 5 warning findings:>
> ... and N more warnings. Run with `--only static` to see all.
<if any info findings:>
#### Info (low-priority improvements)
Summary: N info-level findings across N files. Run with `--only static` to see full list.
For delta display: wrap positive deltas on error/failure metrics in (+N) to indicate regression; wrap negative deltas on pass-rate/coverage in (-N%) styled as improvement. For terminal rendering, use plain notation — the sign alone conveys direction.
Only store if SAVE_SNAPSHOT=true.
<YYYY-MM-DD>-<git_short_sha>.json where the date is today's ISO date. If git is unavailable, use <YYYY-MM-DD>-unknown.json..claude/health-history/ if it does not exist (idempotent — no error if already present).HEALTH_REPORT serialized as JSON to .claude/health-history/<filename>.Stored: .claude/health-history/<filename>After writing (or after checking the directory if SAVE_SNAPSHOT=false), count .json files in .claude/health-history/. If count > 30, print:
Note: .claude/health-history/ has N reports. Consider pruning old ones with:
ls -t .claude/health-history/ | tail -n +31 | xargs -I{} rm .claude/health-history/{}
Check whether .claude/health-history appears in .gitignore (if .gitignore exists). If it does not appear, print:
Tip: health history reports are local artifacts. Add to .gitignore:
echo '.claude/health-history/' >> .gitignore