code-health · git:20260916.dc8d453 · 2026-09-16 · sha256 190c161a6e9d29d6
code-health git:20260916.dc8d453A
Immutable. This exact content is served forever at /api/v1/blob/190c161a6e9d29d6.
---
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 ".github/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 ".github/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.