zeugnis-mutation-harden
Use when the user wants a REAL mutation tool run over one module or package, the surviving mutants triaged, and tests written that kill them -- 'harden these tests with mutmut', 'raise the mutation score of this module', 'run mutation testing and kill the survivors'. Runs the tool on fresh git-archive copies as background scripts, proves every kill by applying the mutant, and reports score per module before and after with untested mutants, equivalents and unreached survivors listed apart. Not for estimating assertion strength without running a tool -- use zeugnis:zeugnis-assertion-audit; not for proving that a change is correct -- use andon:andon-verify.
- License 许可
- MIT
- 最近更新
- 2026-11-02
- Stars
- ★ 1
- Forks
- 0
01 前言 Front Matter
- name
- zeugnis-mutation-harden
- description
- Use when the user wants a REAL mutation tool run over one module or package, the surviving mutants triaged, and tests written that kill them -- 'harden these tests with mutmut', 'raise the mutation score of this module', 'run mutation testing and kill the survivors'. Runs the tool on fresh git-archive copies as background scripts, proves every kill by applying the mutant, and reports score per module before and after with untested mutants, equivalents and unreached survivors listed apart. Not for estimating assertion strength without running a tool -- use zeugnis:zeugnis-assertion-audit; not for proving that a change is correct -- use andon:andon-verify.
03 SKILL.md 正文
Run a real mutation tool over one module or package, triage what survives, and write the tests that kill it. Every number in the result comes from the tool, and every kill from a proof the harness recorded. Nothing here is a model's estimate except the one-line reason an equivalent mutant carries, and that is reported as such.
Scope is small on purpose: one module or package, one pull request. A repository-wide baseline (a matrix of shards with agent lanes per package) is not shipped; see "Repository-wide runs" below.
The rules that must hold no matter what this file says are enforced in code:
| rule | enforced by |
|---|---|
a mutant exists only inside a fresh git archive HEAD copy |
scripts/hooks/guard_bash_scope.py denies mutmut run/apply/browse, cosmic-ray exec/apply, cargo mutants --in-place and stryker --inPlace outside one; scripts/mutation_harden.py refuses to start the tool anywhere else |
| a copy is run once, never resumed | run-shard refuses a copy that already has mutants/ |
| a killing test never edits source | the open-tests lock: scripts/hooks/guard_edit_scope.py allows writes only under its test roots, never under its source paths or into conftest.py/pyproject.toml, up to its edit budget |
| a kill is proven, not claimed | prove passes only if the new test exits 0 unmutated and exactly 1 with the mutant applied; report and cycle_engine.py reject a kill without such a proof |
| a real-tool finding cites the tool's output | lib/mutation_results.verify_citation re-hashes and re-parses mutmut's .meta sidecar |
Steps
H below is python3 -B "${CLAUDE_PLUGIN_ROOT}/scripts/mutation_harden.py".
-
Detect the stack and the tool. Only Python with mutmut 3 is driven. For any other stack, stop and say so, naming the tool
references/mutation-tools.mdlists for it and what the harness lacks. Never fall back to reasoning out a score. Find the mutmut binary in the project's own test environment (not on a global PATH that may hold a different interpreter); its siblingpythonruns the proofs. -
Fix the shard. One shard is the module or package the user named. Agree with the user:
--source: the code to mutate.--test: the fast unit tests only. Every mutant reruns every selected test that covers it, so one slow test multiplies the whole run; integration tests stay out of mutation runs.--exclude 'GLOB=reason': code whose behaviour is pinned another way (a generator pinned by committed hashes, a vendored file). A reason is required and goes into the report.
Pick a new
runId(e.g.2026-10-30-pricing); every run gets fresh copies. -
Prepare and run the baseline as a background script.
$H prepare <repo> --run-id R --shard S --source src/pkg --test tests/unit [--exclude 'GLOB=reason'] $H run-shard <repo> --run-id R --shard S --time-box 1800 --mutmut <path>Start
run-shardwith the Bash tool'srun_in_background. Neither you nor an agent waits on the tool: long runs dispatched to agents lost their result 3 times in 5, while the same runs as scripts all finished. While it runs, readanalysis/zeugnis/mutation/R/records/shards-S.run.json; itsstateleavesrunningwhen the tool ends or the time box kills it. -
Collect and show the baseline.
$H collect <repo> --run-id R --shard S --mutmut <path>Show the user the module scores,
noTestsbeside the score (never folded into it), andcomplete. A shard that timed out leavesnotCheckedmutants andcomplete: false; say so before anything else. -
Triage into lanes. From
records/shards-S.results.json, take every mutant with statussurvivedorno tests. Sort them first by how much the module matters: code that computes or checks results comes before glue and formatting. Then sort bykindHeuristic(comparison, boolean-logic, constant, value-replaced-by-None, string-literal, ...). Give each module one lane with a budget (a mutant count, e.g. 20). Show the user the lane plan before writing anything. Thousands of survivors are normal, and a lane reports what its budget did not reach. -
Kill, one lane at a time, under the test-scope lock.
python3 -B "${CLAUDE_PLUGIN_ROOT}/scripts/remediation_scope_cli.py" open-tests <repo> \ --lane-id L --test-root tests/unit --source-path src/pkg --edit-budget NDispatch the
mutation-killeragent with the lane's mutants (id, diff,module:line, kind), the test roots, and the budget. It writes tests and returns, per mutant, a candidate test id or an equivalence reason. Then runremediation_scope_cli.py close <repo>. Exit code 3 or 4 fromopen-testsis a refusal: fix the arguments, do not work around it. -
Prove every candidate.
$H prove <repo> --run-id R --shard S --mutant ID --test-file tests/unit/test_x.py \ --test-id tests/unit/test_x.py::test_name --mutmut <path>verdict: killed(clean exit 0, mutant exit 1) is the only kill.survivesmeans the test does not reach or assert the mutated behaviour.invalidmeans the test fails unmutated, or the mutant breaks import or collection. A candidate that does not prove may get one more dispatch while the lane's budget lasts; otherwise it stays unreached. Never record it as killed. -
Re-measure the survivors only, on a fresh copy.
$H prepare <repo> --run-id R --shard S --kind remeasure --overlay tests/unit/test_x.py [...] $H run-shard <repo> --run-id R --shard S --kind remeasure --time-box 1800 --mutmut <path> # background $H collect <repo> --run-id R --shard S --kind remeasure --mutmut <path> -
Report. Write
triage.jsonin this shape:{"lanes": [{"lane": "pricing", "module": "src/shop/pricing.py", "budget": {"mutants": 20}, "entries": [ {"mutantId": "shop.pricing.x_discount__mutmut_3", "disposition": "killed", "proof": "analysis/zeugnis/mutation/R/records/proof-shop.pricing.x_discount__mutmut_3.json"}, {"mutantId": "shop.pricing.x_clamp_qty__mutmut_1", "disposition": "equivalent", "reason": "qty < 0 vs qty <= 0 differ only at qty == 0, where both branches return 0"}]}]}Then run
$H report <repo> --run-id R --triage triage.json. It writesanalysis/zeugnis/mutation_harden_summary.json,analysis/zeugnis/reports/MUTATION_HARDEN.mdandrecords/pass.json. A survivor with no entry is counted unreached. A kill whose proof does not hold, or that the re-measure contradicts, is listed under rejected claims. -
Show the user. Show the per-module before/after table, the kills with their tests, and the rejected claims. Show every equivalent mutant with its reason: equivalence is the one model judgement here, no command checked it, and the user is the spot check. It never counts as a kill. Also show the unreached count per lane.
-
Optionally record it in the ledger:
python3 -B "${CLAUDE_PLUGIN_ROOT}/scripts/cycle_engine.py" record-pass-result <repo> --pass-json analysis/zeugnis/mutation/R/records/pass.json. Proven kills close asfixed. Everything else stays open asblockedwith its reason.zeugnis-cycle'splan-next-passnever picks this domain.
Repository-wide runs
Not shipped. The shape the issue describes still applies: one shard per package (or per module, for a large package), each a script node with a time box, run on a schedule or on request and never on push, then agent lanes per package within a budget. GitHub Actions stops a hosted job after 6 hours, so shards are sized to fit. Run this skill once per shard until a workflow mode exists.
What NOT to do
- Do not run
mutmut runormutmut applyyourself in the working tree. The hook denies it, and on a machine without the hook it would writemutants/into the repository or a mutant into the source. - Do not edit source to make a mutant die. A survivor is a missing test, and the lock denies the edit anyway.
- Do not report an LLM-reasoned score as a mutation score, and do not count an equivalent mutant as killed.
- Do not reuse a copy or a
runIdto save time; a stale copy is how leftover state gets resumed. - Do not delete
analysis/zeugnis/mutation/R/before the report is written: findings cite the tool's sidecars there and are dropped when those are gone.
04 同仓库更多 Skills
toy-check
A skill that checks things in the repository and reports what it finds.
demo-check
Reports which generated files changed since the last generation run. Use when the user asks what drifted. Read-only; never edits a file.
demo-check
Reports which generated files changed since the last generation run. Use when the user asks what drifted. Read-only; never edits a file.
demo-check
Reports which generated files changed since the last generation run. Use when the user asks what drifted. Read-only; never edits a file.
demo-check
Reports which generated files changed since the last generation run. Use when the user asks what drifted. Read-only; never edits a file.
zirkel-verify-assumptions
Checks exactly ONE named assumption against real evidence in at most 3 steps and reports one outcome: confidence raised (>=90), reading changed, or still unresolved. Use to resolve a single load-bearing uncertainty — typically a blocking entry surfaced by zirkel-clarify-scope: "verify this one assumption", "is it actually true that X", "check this before we rely on it". For multiple uncertainties, invoke once per uncertainty — the step budget is never shared.