Purpose
Establish the four things /mutation-testing:audit cannot infer safely at run time: which tool,
which diff target, which operators, and where arid-node suppressions live.
Configuration is required rather than optional here, which differs from a plugin whose config merely
speeds up inference. A mutation run drives the project's own test runner against deliberately broken
source; guessing the tool or the diff target either does nothing or does something expensive.
Check-centric per the uniform setup contract (docs/PLUGIN-PHILOSOPHY.md
"Setup is explicit and repeatable" in the marketplace repository): check inspects read-only;
apply interviews and writes, then re-runs check. No argument or check runs the check; apply
runs the check first, then the write flow.
The config merge model
.claude/mutation-testing.md resolves across the documented cascade layers. User-global
(~/.claude/mutation-testing.md) → team (${CLAUDE_PROJECT_DIR}/.claude/mutation-testing.md) →
local overlay (.claude/mutation-testing.local.md), later layers overriding scalars and unioning
lists. This skill writes only the team file; a higher overlay is the user's to change. Report
explicitly when an overlay changes the team file's effect, and warn when a layer cannot be read,
rather than silently presenting the team file as effective.
The arid-node suppression record is a separate surface, .claude/mutation-testing-arid.md,
shaped by the finding-suppression convention. Keeping it separate from the config keeps the config
reviewable: a config diff is a policy change, a suppression diff is an accepted finding.
Its layering is not the config's, and the difference is the whole point. The config's later
layers override; the suppression record sits in the cascade's policy-floor precedence-inversion
class, so on a conflict for the same finding_id the team layer wins, and a personal-layer
entry for an id the team layer does not carry does not suppress at all. It is reported
personal-only, not applied. A personal layer is a draft surface here. Treating it as "layered the
same way" would let one developer silently hide a finding the team never accepted. The full
contract, including id derivation and the four dispositions, is owned by the audit skill's
${CLAUDE_PLUGIN_ROOT}/skills/audit/context/suppression.md.
check (read-only)
Report a PASS / FAIL / INFO table with one remediation line per FAIL. Modify nothing, and do not
run a mutation analysis. That is /mutation-testing:audit.
Ecosystem detected. Identify the stack from manifests that actually exist (package.json,
*.csproj/*.sln, pom.xml/build.gradle, composer.json, pyproject.toml). Report what was
found and which mutation tool it implies. No recognized ecosystem → INFO naming the manual path
in the principles skill's tooling.md, not FAIL.
Tool present and runnable. Invoke the tool's own version command. Absent → FAIL with the
install line for this ecosystem. Present but erroring → FAIL quoting the error; a tool that
installs but cannot start is the common case and is worth distinguishing.
Test runner supported. Confirm the project's runner is one the tool drives. Report the
detected runner and the tool's support status. Unsupported → FAIL; this blocks every later step,
so report it before anything about scores.
Baseline suite is green. Run the project's test command once, unmutated, and report the
result. Red → FAIL. A red baseline kills every mutant and reports a perfect score, so no mutation
result is meaningful until this passes. Record the wall-clock; it is the input to the timeout
setting and to whether diff-scoping is sufficient.
Known flakiness. Ask, and check for a documented flaky-test list or retry configuration.
Flakiness present → INFO with the consequence stated plainly: mutants killed by a flaky failure
inflate the score by an unknown margin.
Config presence (effective, across layers). Report the merged result and which layer
contributes what: tool, diff target, operator set, timeout, paths to mutate. Nothing readable →
FAIL; unlike an inference-speeding config, this one is required.
Diff target resolves, the configured target must resolve in this repository
(git rev-parse --verify <target>). Unresolvable → FAIL naming it; a stale default here silently
scopes a run to nothing or to everything.
Suppression record. Report presence and entry count of .claude/mutation-testing-arid.md
across layers. Absent is a valid state (no suppressions) → INFO. Present → validate every
entry against the full contract in
${CLAUDE_PLUGIN_ROOT}/skills/audit/context/suppression.md, not a subset of it.
Grade the entries with the lint rather than deriving anything by hand, passing every layer that
exists:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/suppression-lint.sh" .claude/mutation-testing-arid.md
It prints record <path> and then one line per entry, and exits 0 when every entry passes, 1
when any entry fails, and 2 when it cannot grade a record at all: no top-level suppressions:
mapping, an unreadable node-kind table, or no digest tool. An absent record and an empty
suppressions: mapping are both exit 0. Its three failure verdicts are FAIL conditions here; the
two items after them are the probe's own, and the lint decides neither:
malformed <id>. A required key is missing (check, claim, sites with a surface and
an anchor/v<N>, reason, date), date is present but is not a real calendar day written YYYY-MM-DD,
or claim is not the canonical form. FAIL, naming the entry and the key the lint names. A partial parse is not offered: an entry with sites,
reason, and date but no check or claim is malformed and must not be reported as usable,
because the audit would otherwise suppress a mutant on a location match the contract never
authorized.
mismatch <id>. The entry's (check, claim, sites) do not hash to the key it is filed
under, and the line names the id they do hash to. FAIL. This is the case a hand-edited
constituent beside a stale key produces, and it silently stops suppressing if unchecked.
unknown-kind <id>. claim binds arid(kind=<node-kind>) with a kind that is not a
member of the enumerated table in
${CLAUDE_PLUGIN_ROOT}/skills/principles/reference/scaling-and-suppression.md
("The node-kind vocabulary"), which the lint reads at run time. FAIL, reporting the accepted
kinds the lint prints beneath the failing entries. Membership is the test, not shape: a
single-word kind that is not in the table is indistinguishable from prose that happens to be
one word, and passing it would make every suppression self-justifying.
- Personal-only entries. Any id present in a
.local.md layer but absent from the team layer
is reported personal-only, not applied, with promotion to the team layer named as the remedy.
This is INFO, not FAIL: the entry is legal, it simply does not suppress.
- Report the contributing layer for every entry. A malformed layer degrades soft. Report it and
continue, never fail the whole read.
Tracked, not ignored. Both the config and the suppression record must be committed to be
team-shared, and not ignored is only half of that. Run both probes per file:
git ls-files --error-unmatch <path> (is it tracked?) and git check-ignore -v <path>
(is it ignored?). Untracked → FAIL, naming the git add that fixes it; ignored → FAIL with the
matching pattern. Checking only check-ignore is the trap: immediately after apply writes them
the files are untracked but not ignored, so check-ignore is silent and the probe would report
success on two files no teammate will ever receive. The .local.md overlays are expected to be
both untracked and ignored. INFO, not FAIL.
apply (idempotent)
Run check, then interview and write. Proceed non-interactively where the repository makes a value
unambiguous; ask only where the answer is genuinely the user's.
- Read the effective config first, across all layers, and summarize it before proposing
changes. Nothing is dropped without the user confirming.
- Detect and propose the tool. From the ecosystem, name the tool and the exact install command,
and ask before installing anything. Installing a dependency into the consumer's project is the
user's decision, not this skill's. Propose, never install unprompted.
- Settle the diff target. Default to the repository's own default branch as resolved from
origin/HEAD, not a hardcoded main or master. Confirm it resolves.
- Settle the operator set. Default to the tool's defaults, and say why: optional and
experimental operators raise the mutant count and the unproductive rate together. Offer the
narrowed set only if the user asks for a cheaper run.
- Settle the timeout from the measured baseline suite time in
check, not from a guess.
- Settle the mutate paths. Propose source roots, excluding generated code, vendored
directories, and test code itself. Mutating tests measures nothing.
- Write the config following
${CLAUDE_PLUGIN_ROOT}/skills/setup/templates/config-template.md.
- Create the suppression record empty, with its header comment and an empty
suppressions:
mapping, a mapping, never a list, since a list is taken whole and one personal entry would
discard the team's entire accepted set. An empty record makes the first suppression an edit to a
reviewed file rather than the creation of a new one.
- Stage both files for commit and say so. They are team-shared surfaces; leaving them untracked
is the failure probe 9 exists to catch, and
apply is where it is cheapest to fix. Offer the
git add; never commit on the user's behalf.
- Verify after remediation. Re-run the
check probes on what was written, including both
halves of probe 9, tracked and not-ignored, then offer the overlay convention: personal
config overrides in .claude/mutation-testing.local.md, and recommend the recursive
.claude/**/*.local.* line in .gitignore if not already covered. State plainly that the same overlay pattern does not
give personal suppressions effect: an arid entry in a .local.md is a draft until promoted to
the team layer.
Re-running apply after everything passes changes nothing and reports "already configured".
Output
A tracked .claude/mutation-testing.md and .claude/mutation-testing-arid.md in the consuming repo,
plus a one-paragraph summary of what was written, the measured baseline suite time, and how to
re-run this setup to reconfigure.
What this skill does NOT do
- Run a mutation analysis. That is
/mutation-testing:audit. check runs the unmutated suite
once to establish the baseline, and nothing more.
- Install anything without asking.
- Write the plugin cache, Claude Code user settings, or
pluginConfigs.
- Write machine-local state. Configuration lives in the consumer's tracked files, never in the
plugin directory or the plugin data directory.
- Set a score threshold. The config template deliberately has no such field; see the
principles
skill's scaling-and-suppression.md for why gating on the score inverts the incentive.
1---2name: setup-493description: Verify and configure the mutation-testing plugin for this repository. check inspects the ecosystem's mutation tool (installed, runnable, supported test runner), the baseline suite's health, and the tracked .claude/mutation-testing.md config across its merge layers, read-only; apply detects the ecosystem, installs or names the tool, interviews for the diff target and operator set, and writes the config plus an empty arid-node suppression record. Use when: 'set up mutation testing', 'is mutation testing configured', 'which mutation tool for this repo', 'mutation-testing setup', or the audit skill reports missing config or an unavailable tool. Re-runnable. Safe to invoke again to reconfigure.4---56## Purpose78Establish the four things `/mutation-testing:audit` cannot infer safely at run time: **which tool**,9**which diff target**, **which operators**, and **where arid-node suppressions live**.1011Configuration is required rather than optional here, which differs from a plugin whose config merely12speeds up inference. A mutation run drives the project's own test runner against deliberately broken13source; guessing the tool or the diff target either does nothing or does something expensive.14Check-centric per the uniform setup contract (`docs/PLUGIN-PHILOSOPHY.md`15"Setup is explicit and repeatable" in the marketplace repository): `check` inspects read-only;16`apply` interviews and writes, then re-runs `check`. No argument or `check` runs the check; `apply`17runs the check first, then the write flow.1819## The config merge model2021`.claude/mutation-testing.md` resolves across the documented cascade layers. User-global22(`~/.claude/mutation-testing.md`) → team (`${CLAUDE_PROJECT_DIR}/.claude/mutation-testing.md`) →23local overlay (`.claude/mutation-testing.local.md`), later layers overriding scalars and unioning24lists. This skill writes only the **team** file; a higher overlay is the user's to change. Report25explicitly when an overlay changes the team file's effect, and warn when a layer cannot be read,26rather than silently presenting the team file as effective.2728The arid-node suppression record is a **separate** surface, `.claude/mutation-testing-arid.md`,29shaped by the finding-suppression convention. Keeping it separate from the config keeps the config30reviewable: a config diff is a policy change, a suppression diff is an accepted finding.3132**Its layering is not the config's, and the difference is the whole point.** The config's later33layers override; the suppression record sits in the cascade's policy-floor precedence-inversion34class, so on a conflict for the same `finding_id` the **team** layer wins, and **a personal-layer35entry for an id the team layer does not carry does not suppress at all**. It is reported36`personal-only, not applied`. A personal layer is a draft surface here. Treating it as "layered the37same way" would let one developer silently hide a finding the team never accepted. The full38contract, including id derivation and the four dispositions, is owned by the audit skill's39[`${CLAUDE_PLUGIN_ROOT}/skills/audit/context/suppression.md`](../audit/context/suppression.md).4041## `check` (read-only)4243Report a PASS / FAIL / INFO table with one remediation line per FAIL. Modify nothing, and do **not**44run a mutation analysis. That is `/mutation-testing:audit`.45461. **Ecosystem detected**. Identify the stack from manifests that actually exist (`package.json`,47 `*.csproj`/`*.sln`, `pom.xml`/`build.gradle`, `composer.json`, `pyproject.toml`). Report what was48 found and which mutation tool it implies. No recognized ecosystem → INFO naming the manual path49 in the `principles` skill's `tooling.md`, not FAIL.502. **Tool present and runnable**. Invoke the tool's own version command. Absent → FAIL with the51 install line for this ecosystem. Present but erroring → FAIL quoting the error; a tool that52 installs but cannot start is the common case and is worth distinguishing.533. **Test runner supported**. Confirm the project's runner is one the tool drives. Report the54 detected runner and the tool's support status. Unsupported → FAIL; this blocks every later step,55 so report it before anything about scores.564. **Baseline suite is green**. Run the project's test command once, unmutated, and report the57 result. Red → FAIL. A red baseline kills every mutant and reports a perfect score, so no mutation58 result is meaningful until this passes. Record the wall-clock; it is the input to the timeout59 setting and to whether diff-scoping is sufficient.605. **Known flakiness**. Ask, and check for a documented flaky-test list or retry configuration.61 Flakiness present → INFO with the consequence stated plainly: mutants killed by a flaky failure62 inflate the score by an unknown margin.636. **Config presence (effective, across layers)**. Report the merged result and which layer64 contributes what: tool, diff target, operator set, timeout, paths to mutate. Nothing readable →65 FAIL; unlike an inference-speeding config, this one is required.667. **Diff target resolves**, the configured target must resolve in this repository67 (`git rev-parse --verify <target>`). Unresolvable → FAIL naming it; a stale default here silently68 scopes a run to nothing or to everything.698. **Suppression record**. Report presence and entry count of `.claude/mutation-testing-arid.md`70 across layers. Absent is a valid state (no suppressions) → INFO. Present → validate **every**71 entry against the full contract in72 [`${CLAUDE_PLUGIN_ROOT}/skills/audit/context/suppression.md`](../audit/context/suppression.md), not a subset of it.7374 Grade the entries with the lint rather than deriving anything by hand, passing every layer that75 exists:7677 ```bash78 bash "${CLAUDE_PLUGIN_ROOT}/scripts/suppression-lint.sh" .claude/mutation-testing-arid.md79 ```8081 It prints `record <path>` and then one line per entry, and exits 0 when every entry passes, 182 when any entry fails, and 2 when it cannot grade a record at all: no top-level `suppressions:`83 mapping, an unreadable node-kind table, or no digest tool. An absent record and an empty84 `suppressions:` mapping are both exit 0. Its three failure verdicts are FAIL conditions here; the85 two items after them are the probe's own, and the lint decides neither:86 - **`malformed <id>`**. A required key is missing (`check`, `claim`, `sites` with a `surface` and87 an `anchor/v<N>`, `reason`, `date`), `date` is present but is not a real calendar day written `YYYY-MM-DD`,88 or `claim` is not the canonical form. FAIL, naming the entry and the key the lint names. A partial parse is not offered: an entry with `sites`,89 `reason`, and `date` but no `check` or `claim` is malformed and must not be reported as usable,90 because the audit would otherwise suppress a mutant on a location match the contract never91 authorized.92 - **`mismatch <id>`**. The entry's `(check, claim, sites)` do not hash to the key it is filed93 under, and the line names the id they do hash to. FAIL. This is the case a hand-edited94 constituent beside a stale key produces, and it silently stops suppressing if unchecked.95 - **`unknown-kind <id>`**. `claim` binds `arid(kind=<node-kind>)` with a kind that is not a96 **member of the enumerated table** in97 `${CLAUDE_PLUGIN_ROOT}/skills/principles/reference/scaling-and-suppression.md`98 ("The node-kind vocabulary"), which the lint reads at run time. FAIL, reporting the accepted99 kinds the lint prints beneath the failing entries. Membership is the test, not shape: a100 single-word kind that is not in the table is indistinguishable from prose that happens to be101 one word, and passing it would make every suppression self-justifying.102 - **Personal-only entries**. Any id present in a `.local.md` layer but absent from the team layer103 is reported `personal-only, not applied`, with promotion to the team layer named as the remedy.104 This is INFO, not FAIL: the entry is legal, it simply does not suppress.105 - Report the contributing layer for every entry. A malformed layer degrades soft. Report it and106 continue, never fail the whole read.1079. **Tracked, not ignored**. Both the config and the suppression record must be committed to be108 team-shared, and *not ignored* is only half of that. Run **both** probes per file:109 `git ls-files --error-unmatch <path>` (is it tracked?) and `git check-ignore -v <path>`110 (is it ignored?). Untracked → FAIL, naming the `git add` that fixes it; ignored → FAIL with the111 matching pattern. Checking only `check-ignore` is the trap: immediately after `apply` writes them112 the files are untracked but not ignored, so `check-ignore` is silent and the probe would report113 success on two files no teammate will ever receive. The `.local.md` overlays are expected to be114 both untracked and ignored. INFO, not FAIL.115116## `apply` (idempotent)117118Run `check`, then interview and write. Proceed non-interactively where the repository makes a value119unambiguous; ask only where the answer is genuinely the user's.1201211. **Read the effective config first, across all layers**, and summarize it before proposing122 changes. Nothing is dropped without the user confirming.1232. **Detect and propose the tool.** From the ecosystem, name the tool and the exact install command,124 and ask before installing anything. Installing a dependency into the consumer's project is the125 user's decision, not this skill's. Propose, never install unprompted.1263. **Settle the diff target.** Default to the repository's own default branch as resolved from127 `origin/HEAD`, not a hardcoded `main` or `master`. Confirm it resolves.1284. **Settle the operator set.** Default to the tool's defaults, and say why: optional and129 experimental operators raise the mutant count and the unproductive rate together. Offer the130 narrowed set only if the user asks for a cheaper run.1315. **Settle the timeout** from the measured baseline suite time in `check`, not from a guess.1326. **Settle the mutate paths.** Propose source roots, excluding generated code, vendored133 directories, and test code itself. Mutating tests measures nothing.1347. **Write the config** following135 [`${CLAUDE_PLUGIN_ROOT}/skills/setup/templates/config-template.md`](templates/config-template.md).1368. **Create the suppression record empty**, with its header comment and an empty `suppressions:`137 mapping, a **mapping, never a list**, since a list is taken whole and one personal entry would138 discard the team's entire accepted set. An empty record makes the first suppression an edit to a139 reviewed file rather than the creation of a new one.1409. **Stage both files for commit and say so.** They are team-shared surfaces; leaving them untracked141 is the failure probe 9 exists to catch, and `apply` is where it is cheapest to fix. Offer the142 `git add`; never commit on the user's behalf.14310. **Verify after remediation.** Re-run the `check` probes on what was written, including both144 halves of probe 9, tracked *and* not-ignored, then offer the overlay convention: personal145 config overrides in `.claude/mutation-testing.local.md`, and recommend the recursive146 `.claude/**/*.local.*` line in `.gitignore` if not already covered. State plainly that the same overlay pattern does **not**147 give personal suppressions effect: an arid entry in a `.local.md` is a draft until promoted to148 the team layer.149150Re-running `apply` after everything passes changes nothing and reports "already configured".151152## Output153154A tracked `.claude/mutation-testing.md` and `.claude/mutation-testing-arid.md` in the consuming repo,155plus a one-paragraph summary of what was written, the measured baseline suite time, and how to156re-run this setup to reconfigure.157158## What this skill does NOT do159160- Run a mutation analysis. That is `/mutation-testing:audit`. `check` runs the *unmutated* suite161 once to establish the baseline, and nothing more.162- Install anything without asking.163- Write the plugin cache, Claude Code user settings, or `pluginConfigs`.164- Write machine-local state. Configuration lives in the consumer's tracked files, never in the165 plugin directory or the plugin data directory.166- Set a score threshold. The config template deliberately has no such field; see the `principles`167 skill's `scaling-and-suppression.md` for why gating on the score inverts the incentive.