From jig
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`).
How this skill is triggered — by the user, by Claude, or both
Slash command
/jig:code-healthThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Spec 060 introduced `code-health` as the **static-analysis sibling of
Spec 060 introduced
code-healthas the static-analysis sibling oftdd-loop, under ADR-0017's "detect the language → drive its blessed tools → normalize → degrade gracefully" framing. Liketdd.py, the deterministic detection + subprocess invocation live inhealth.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.
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.
.jig/lint-command override always wins and bypasses ecosystem
detection entirely (honored verbatim — same semantics as tdd.py's
.jig/test-command).pyproject.toml / *.py
for Python; package.json for Node) and resolves its primary linter:
ruff on PATH → uvx ruff → pipx run ruff (ephemeral),
invoked as ruff check --output-format=json <dir>.eslint on PATH → npx eslint (ephemeral), invoked as
eslint --format json <dir>.--select C901,PLR0911,PLR0912,PLR0913,PLR0915 surfaces a per-function
complexity signal ("complexity: N function(s) over threshold; top: …").prettier --check probe surfaces
files that need formatting ("prettier: N file(s) need formatting").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).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 start2 + "no recognized ecosystem (Python/Node) found
— set .jig/lint-command to run your linter".2 + an
ecosystem-specific recommendation (ruff/pipx for Python; eslint/npx for
Node).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.
Two subcommands mirror tdd.py: detect reports which linter resolves, and
check runs it.
python3 "${CLAUDE_PLUGIN_ROOT}/skills/code-health/health.py" detect [target]
target defaults to . when omitted.ruff,
uvx ruff, pipx run ruff, eslint, or npx eslint).2 with a recommendation on stderr if nothing resolves (no recognized
ecosystem, no resolvable linter, or a mixed project needing disambiguation).python3 "${CLAUDE_PLUGIN_ROOT}/skills/code-health/health.py" check [target]
detect.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".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.)
/jig:tdd-loop (tdd.py). Static analysis and
the test loop are different cadences./jig:security-review; this skill is about
lint / style / correctness signals, not vulnerabilities./jig:independent-review./jig:pr-review.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
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).
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):
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
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?
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.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.docs/specs/NNN-slug/reviews/slice-NN-code-health.md (ADR-0014
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.
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.0.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)..jig/lint-command verbatim without ecosystem detection, so jig's
own dogfood CI (which sets an override) is unaffected.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.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.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.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.npx claudepluginhub ramboz/jig --plugin jigGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.