code-health · diff

git:20260622.65ad1f6 to git:20260622.269c6b0

154 added, 45 removed. Audit A to A.

---
name: code-health
description: >
- Run a static-analysis pass on a Python project — detect the installed
- linter (ruff), drive it 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. ruff is
- resolved on PATH or run ephemerally via uvx / pipx — it installs nothing.
+ Run a static-analysis pass on a project — detect the ecosystem (Python or
+ Node), drive its linter (ruff / eslint, plus advisory prettier/complexity
+ 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 linter and runs it, normalizing the result so callers
- can branch deterministically. For this slice the scope is **Python + ruff**:
+ 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 an **advisory** secondary signal.
- - Resolves a linter in priority order: a `.jig/lint-command` override →
- `ruff` on `PATH` → `uvx ruff` (ephemeral) → `pipx run ruff` (ephemeral).
- - Invokes it with `ruff check --output-format=json <dir>` and parses the
- result into a **tight summary** — a findings count + the top rule codes,
- not ruff's raw dump (per spec 057's "tight envelope, not a transcript").
- - Normalizes the exit code:
+ - 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: …").
+ - **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, OR the resolved tool failed to start
- - Degrades gracefully: when no linter and no ephemeral runner are available,
- it exits `2` with a one-line recommendation (`no Python linter found —
- install ruff or run via pipx`) — never a stack trace.
+ - `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` run ruff ephemerally only if those
- launchers are already on `PATH`.
+ 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 linter name (`ruff`, `uvx ruff`, or `pipx run ruff`).
- - Exit `2` with a recommendation on stderr if nothing resolves.
+ - 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 linter via the same logic as `detect`.
- - Prints a tight summary (count + top rule codes) to stdout.
- - Exit code is normalized (`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 (install
- ruff or wire `.jig/lint-command`), not "the code is clean".
+ - 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 / 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** and takes
- priority over all auto-detection — the same semantics as `tdd.py`'s
- `.jig/test-command`. Useful for a project whose linter isn't ruff
- (e.g. `flake8 src` or `pylint mypkg`).
+ 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 stdin). 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
- - **Scope is Python + ruff (slice 060-01).** Node (eslint/prettier),
- complexity, duplication, a dedicated code-health reviewer pass, and the CI
- dogfood are later slices (060-02..05). A non-Python project with no
- `.jig/lint-command` override degrades to the recommendation.
- - **Exit `1` vs `2`.** Exit `1` means ruff ran and found issues — inspect
- the summary. Exit `2` means no linter was resolvable or the resolved tool
- failed to start (an environment issue) — don't conflate them with clean.
- - **Ephemeral runs need a network/cache.** `uvx ruff` / `pipx run ruff`
- fetch ruff on first use. If neither the binary nor a launcher is present,
- the skill recommends rather than failing opaquely.
- - **Tight summary, not the raw dump.** `check` parses ruff's JSON into a
- count + top codes; it does not echo the full ruff output. Re-run ruff
- directly when you need every finding's location.
+ - **Scope is Python (ruff, + advisory complexity) 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 (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
+ duplication 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` /
+ `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 (no detector)` (for 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.