Run a static-analysis pass on a project — detect the ecosystem (Python or Node), drive its linter (ruff / eslint, plus advisory pyright/complexity/ prettier and a cross-ecosystem duplication signal) via the `health.py` helper, and act on the normalized exit code (0 clean / 1 findings / 2 no-linter). Auto-triggers when you say lint this, check code health, run the linter, ask is this code clean, ask any lint issues, or want a static analysis pass. Tools are resolved on PATH or run ephemerally ...
Scanned 9/4/2026
Install to Claude Code
npx -y skills add ramboz/jig --skill code-health --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Code Health?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ramboz-code-health-157838ad)More formats (shields.io, HTML) on the badges page.
---
name: code-health
description: >
Run a static-analysis pass on a project — detect the ecosystem (Python or
Node), drive its linter (ruff / eslint, plus advisory pyright/complexity/
prettier and a cross-ecosystem duplication signal) via the `health.py`
helper, and act on the normalized exit code
(0 clean / 1 findings / 2 no-linter). Auto-triggers when you say lint this,
check code health, run the linter, ask is this code clean, ask any lint
issues, or want a static analysis pass. Tools are resolved on PATH or run
ephemerally via uvx / pipx / npx — it installs nothing.
Defers to any other installed skill whose description identifies it as
handling linting, static analysis, or code quality — prefer it over this
baseline. Do not use for running tests (use `/jig:tdd-loop`), for
security review (use `/jig:security-review`), for spec-compliance review
of a finished slice (use `/jig:independent-review`), or for general PR
craft review (use `/jig:pr-review`).
user-invocable: true
---
> Spec 060 introduced `code-health` as the **static-analysis sibling of
> `tdd-loop`**, under [ADR-0017](../../docs/decisions/adr-0017-scaffolded-code-health.md)'s
> "detect the language → drive its blessed tools → normalize → degrade
> gracefully" framing. Like `tdd.py`, the deterministic detection +
> subprocess invocation live in `health.py`; this SKILL.md drives the
> judgment layer. If another installed skill's description identifies it as
> handling linting / static analysis / code quality, the Claude Code skill
> router prefers it — the deferral is **category-based**.
## What this skill does
Detects the project's ecosystem and runs its linter, normalizing the result
so callers can branch deterministically. Ecosystem detection is
**table-driven** — each ecosystem (Python, Node) is a data-structure entry,
so adding a language is an entry, not a control-flow fork. Current scope:
**Python (ruff) + Node (eslint)**, each with **advisory** secondary signals.
- A `.jig/lint-command` override always wins and **bypasses ecosystem
detection entirely** (honored verbatim — same semantics as `tdd.py`'s
`.jig/test-command`).
- Otherwise detects the ecosystem by marker files (`pyproject.toml` / `*.py`
for Python; `package.json` for Node) and resolves its primary linter:
- **Python** — `ruff` on `PATH` → `uvx ruff` → `pipx run ruff` (ephemeral),
invoked as `ruff check --output-format=json <dir>`.
- **Node** — `eslint` on `PATH` → `npx eslint` (ephemeral), invoked as
`eslint --format json <dir>`.
- Parses the result into a **tight summary** — a findings count + the top
rule codes, not the raw dump (per spec 057's "tight envelope, not a
transcript").
- Adds an **advisory** dimension that is *reported, not gating* (it never
changes the exit code):
- **Python — complexity:** an advisory ruff probe with
`--select C901,PLR0911,PLR0912,PLR0913,PLR0915` surfaces a per-function
complexity signal ("complexity: N function(s) over threshold; top: …").
- **Python — type checking:** an advisory `pyright --outputjson` probe
resolves `pyright` on `PATH`, then `uvx pyright`, then `pipx run pyright`.
Type diagnostics are summarized as a count + representative rules
("pyright: N type diagnostic(s); top: …"). If no type-checker resolves,
it emits `pyright: skipped (no type-checker) …`. Like every advisory
signal, it is reported, never gating.
- **Node — formatting:** an advisory `prettier --check` probe surfaces
files that need formatting ("prettier: N file(s) need formatting").
- **Cross-ecosystem — duplication:** an advisory probe (run for BOTH
Python and Node) reports copy/paste duplication. It is **native-first**
(an explicit extension point for a future per-ecosystem native
duplication tool — currently empty, since no jig ecosystem ships a
distinct native detector), falls back to an ephemeral **`npx jscpd`**
when `npx` is on `PATH` (the Node analogue of `pipx run`, works on any
language, installs nothing), and otherwise emits
`duplication: skipped (no detector) — install a duplication tool or Node
(npx jscpd) to enable`. When it runs, the summary is a tight
percentage + the top clones as `file:line`
("duplication: 4.2% (12 clones); top: foo.py:10, bar.py:88") — never the
raw jscpd log. Like the other advisory signals it is **reported, never
gating** (it cannot change the exit code).
- Normalizes the **primary** linter's exit code:
- `0` — clean (no findings)
- `1` — findings exist (the linter ran and reported issues)
- `2` — no linter resolvable, no recognized ecosystem, OR the resolved tool
failed to start
- Degrades gracefully (AC4), never a stack trace:
- **no markers** → exit `2` + "no recognized ecosystem (Python/Node) found
— set .jig/lint-command to run your linter".
- **one ecosystem, no resolvable linter** → exit `2` + an
ecosystem-specific recommendation (ruff/pipx for Python; eslint/npx for
Node).
- **mixed (2+ ecosystems)** → exit `2` + a recommendation naming the
detected ecosystems and pointing at `.jig/lint-command` to disambiguate.
It **installs nothing** — `uvx` / `pipx` / `npx` run the tools ephemerally
only if those launchers are already on `PATH`.
## Helper invocations
Two subcommands mirror `tdd.py`: `detect` reports which linter resolves, and
`check` runs it.
### Detect the linter
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/code-health/health.py" detect [target]
```
- `target` defaults to `.` when omitted.
- Stdout: the resolved primary linter name across ecosystems (`ruff`,
`uvx ruff`, `pipx run ruff`, `eslint`, or `npx eslint`).
- Exit `2` with a recommendation on stderr if nothing resolves (no recognized
ecosystem, no resolvable linter, or a mixed project needing disambiguation).
### Run the lint pass
```bash
python3 "${CLAUDE_PLUGIN_ROOT}/skills/code-health/health.py" check [target]
```
- Auto-resolves the ecosystem's linter via the same logic as `detect`.
- Prints a tight summary (count + top rule codes) to stdout, plus any
**advisory** lines (Python complexity / Python pyright / Node prettier /
cross-ecosystem duplication) — advisory lines are reported but never
change the exit code.
- Exit code is normalized off the **primary** linter (`0` clean / `1`
findings / `2` no-linter) per the table above. Branch on it
deterministically — exit `1` means inspect the summarized findings; exit
`2` means the tool couldn't even start, no ecosystem was recognized, or a
mixed project needs `.jig/lint-command` disambiguation — not "the code is
clean".
### Override the auto-detection
Create `<target>/.jig/lint-command` with the first non-blank, non-comment
line being the exact command to run. It is honored **verbatim**, takes
priority over all auto-detection, and **bypasses ecosystem detection
entirely** — the same semantics as `tdd.py`'s `.jig/test-command`. Useful for
a project whose linter isn't ruff/eslint (e.g. `flake8 src` or `pylint
mypkg`), or to disambiguate a mixed Python+Node repo. (This is how jig's own
CI is unaffected — jig commits a `.jig/lint-command`.)
## When NOT to use
- **Running tests** — that's `/jig:tdd-loop` (`tdd.py`). Static analysis and
the test loop are different cadences.
- **Security review** — that's `/jig:security-review`; this skill is about
lint / style / correctness signals, not vulnerabilities.
- **Spec-compliance review** of a finished slice — `/jig:independent-review`.
- **General PR craft review** — `/jig:pr-review`.
- **Pure-documentation edits** that touch no code.
## Relationship to other skills
`health.py` is the static-analysis sibling of `tdd-loop`'s `tdd.py` — same
detect → drive → normalize → degrade shape, same `.jig/*-command` override
idiom, same `0 / 1 / 2` exit contract. Per [ADR-0002](../../docs/decisions/adr-0002-contracts-stays-deferred.md)
the shared idioms (`_read_text_safe` / `_custom_command_file` /
`_parse_custom_command`) are **inline-mirrored**, not extracted into a
`_common` module — this is only the second helper of its kind, and the two
have independent lifecycles. The deliberate duplication is noted in
`health.py`'s module docstring (exactly as `tdd.py` documents its own
duplication of `scaffold.py`).
## The code-health review pass (slice 060-05)
Beyond the `health.py` runner, jig wires a distinct **code-health review
pass** into the post-implementation flow (alongside compliance / craft /
arch). The layering ([ADR-0017](../../docs/decisions/adr-0017-scaffolded-code-health.md)):
the **spine runs the tool** (`health.py`), and a **read-only `reviewer`
subagent judges its tight summary** — rendering the judgment a static tool
*can't*: is reported duplication within the [ADR-0002](../../docs/decisions/adr-0002-extract-helper-on-third-caller.md)
inline-mirror budget (two callers may mirror; a third triggers an extract)?
is a flagged complex function inherent or fixable? are the lint findings
worth blocking on?
- **The reviewer never runs `health.py`.** It is read-only
(Read/Glob/Grep, no Bash). The orchestrator / CI runs `health.py`,
captures the tight summary, and feeds it into the prompt via
`review.py code-health … --summary-file <path>` (or `--summary-file -`
to pipe it in — stdin is never read implicitly, bug 017). The reviewer
judges the summary, never raw logs.
- **The pass is GATED, not always-on.** It runs only when a slice's
frontmatter declares `code_health_review: true` — exactly mirroring how
`arch_review: true` gates the arch pass. **Why gated:** ADR-0017 flags
the per-slice review cost (the spec 055/057 context-cost discipline —
every pass adds orchestrator turns + a subagent), and recommends gating
it like arch-review rather than spending it on every slice. The flag
defaults off, so existing slices are unaffected; a slice author opts in
when a change is duplication-/complexity-heavy enough to warrant the
judgment.
- **Evidence + block rule.** The verdict is recorded as
`docs/specs/NNN-slug/reviews/slice-NN-code-health.md` ([ADR-0014](../../docs/decisions/adr-0014-review-evidence-model.md)
evidence model). `[blocker]`-tagged findings block the `REVIEWED`
transition; `[nit]`-tagged findings become reconciliation-log items —
the same rule as the craft/arch passes. `workflow.py transition`
requires the `code-health` verdict for `REVIEWED`/`DONE` iff the flag is
set. Query the flag with
`workflow.py code-health-review-needed <spec.md> <slice>`.
See `skills/spec-workflow/SKILL.md` § "After implementation" for the full
four-pass orchestration recipe.
## Gotchas
- **A configured `review.code_health_skill` (scaffold.json) is honored only in
the orchestrated code-health pass, not on interactive `/jig:code-health`.**
Spec 096-01 / ADR-0040 D1 makes `code_health` one of the three extensible
categories, so a project can name a richer code-health reviewer in
`scaffold.json`; that key is read by `review.py` when it builds the
**code-health pass** prompt for the spec-workflow reviewer subagent (before
096-01 that builder had no richer dispatch at all). It is **not** consulted on
this interactive invocation. Config honoring on orchestrator-invoked surfaces
is a tracked follow-up (ADR-0040 OQ1).
- **Scope is Python (ruff, + advisory complexity and pyright) and Node
(eslint, + advisory prettier --check), plus a cross-ecosystem advisory
duplication signal (`npx jscpd`).** The dedicated code-health reviewer pass (slice
060-05) is now live — see "The code-health review pass" above; the Tier-2
scaffold-the-floor work (slice 060-06) is **DEFERRED**. An unrecognized
ecosystem with no `.jig/lint-command` override degrades to a recommendation.
- **Advisory ≠ gating.** The complexity and pyright (Python), prettier
(Node), and duplication (cross-ecosystem) signals are reported in the
summary but **never** change the exit code — the exit code is driven solely by the
primary linter (ruff / eslint). A clean ruff run with complexity or
type findings still exits `0`.
- **Duplication is honest about being unavailable.** Unlike complexity /
prettier (which stay silent when their tool isn't present), the duplication
probe emits `duplication: skipped (no detector) …` when neither a native
tool nor `npx` is available — so a reader knows the dimension was *not
measured* rather than *measured clean*. It writes jscpd's JSON report to a
temp dir outside the project (read back, then removed) so it never pollutes
your tree, and runs jscpd without `--threshold` so jscpd itself never exits
non-zero (advisory, not gating).
- **The override path runs no advisory probes** (including duplication) — it
honors `.jig/lint-command` verbatim without ecosystem detection, so jig's
own dogfood CI (which sets an override) is unaffected.
- **Mixed repos degrade, they don't guess.** If both `pyproject.toml` (or
`*.py`) and `package.json` are present, `check` exits `2` and asks you to
set `.jig/lint-command` to disambiguate — it never picks one for you.
- **Exit `1` vs `2`.** Exit `1` means the linter ran and found issues —
inspect the summary. Exit `2` means no linter was resolvable, no ecosystem
was recognized, a mixed project needs disambiguation, or the resolved tool
failed to start (an environment issue) — don't conflate any of those with
clean.
- **Ephemeral runs need a network/cache.** `uvx ruff` / `pipx run ruff` /
`uvx pyright` / `pipx run pyright` / `npx eslint` / `npx prettier` /
`npx jscpd` fetch the tool on first use. If neither the binary nor a
launcher is present, the skill recommends (for the primary linter) or
reports `skipped` (for pyright/duplication) rather than failing opaquely.
- **Tight summary, not the raw dump.** `check` parses the linter's JSON into
a count + top codes; it does not echo the full tool output. Re-run the
linter directly when you need every finding's location.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!