# Reapr: mutation testing for your AI agents
> Reapr runs your own StrykerJS or cargo-mutants config on the lines a pull request changed and reports the mutation score of the diff and every surviving mutant, for people and for agents.
Source: https://reapr.dev/
Kill the mutants your tests miss. Reapr works the same for code an agent wrote and code a person wrote.
## What it is
- `rpr`, a command-line tool (version 0.4.0, Linux x86_64 and aarch64; macOS and Windows are planned).
- [reapr-corp/run](https://github.com/reapr-corp/run), a GitHub Action that runs `rpr` on every pull request and writes a job summary.
- [mutation-audit](https://github.com/reapr-corp/reapr/blob/main/plugins/reapr/skills/mutation-audit/SKILL.md), an agent skill that turns surviving mutants into tests, with hard rules: no production-code changes, no config or threshold edits, no deleted tests.
- Engines: StrykerJS (JavaScript, TypeScript) and cargo-mutants 26.2 to 27.x (Rust) are supported. PIT, Infection, mutmut and Stryker.NET are planned.
## Install and run
```sh
curl -LsSf https://github.com/reapr-corp/reapr/releases/latest/download/rpr-installer.sh | sh
rpr run --diff origin/main # human summary
rpr run --diff origin/main --min-score 80 # exit 1 below 80%
rpr run --diff origin/main --json # one JSON document, for CI and agents
```
Needs git and the engine: StrykerJS with a config file (Node.js 20.10 or later), or cargo-mutants 26.2 to 27.x.
## How it works
1. `git diff ...HEAD` gives the changed line ranges.
2. Your config's `mutate` globs pick the files. A wrapper config imports yours and overrides only `mutate`, `reporters` (adds `json`) and `jsonReporter.fileName`.
3. Stryker runs. Reapr keeps the mutants inside the changed ranges, computes the score (detected / (detected + undetected)) and writes a run record to `.git/reapr/runs//`.
## Reference measurement
One package of a pnpm + Turborepo monorepo (~55k files incl. build caches), StrykerJS 9.6.1, Vitest 4, medians of 5 runs on a loaded 22-thread Linux machine ([bench report](https://github.com/reapr-corp/reapr/blob/main/bench/results/2026-11-02-turborepo-monorepo.md)):
- About 85% of a run with 3 mutants passed before the first test ran.
- Adding `.turbo` to Stryker's `ignorePatterns` took a plain Stryker run from 38.4 s to 12.5 s with identical results. That is a Stryker config change, not a Reapr feature, and it applies to Turborepo repos with a local cache inside the repository.
- With `incremental: true`, Stryker's report showed 87.0% for the package; the diff's own 3 mutants were all killed (100%).
## Pricing
The CLI, the Action, the engine adapters and the skill are free and open source (FSL-1.1-ALv2 for the core and CLI, MIT for the rest). Reapr Cloud, a hosted layer, is planned and not built: https://reapr.dev/cloud
Source: https://github.com/reapr-corp/reapr
## Frequently asked questions
### What is mutation testing?
A mutation engine changes your code on purpose, one small change at a time (for example > becomes >=), and runs your tests against each change. If a test fails, the mutant is killed. If every test still passes, the mutant survived: a change in behavior that no test catches. The mutation score is the share of mutants your tests detect.
### How is mutation testing different from code coverage?
Coverage records which lines ran during the tests. The mutation score records whether a test fails when those lines change. A line can be 100% covered by a test that asserts nothing, and a mutant on that line survives.
### How do I run Stryker only on the lines I changed?
StrykerJS accepts line ranges in mutate, such as src/app.ts:5-7. Reapr builds those ranges from git diff against the merge base, keeps the files your own mutate globs select, and runs Stryker with your config: rpr run --diff origin/main.
### Does Reapr change my Stryker config?
No. Reapr never edits your config file. For each run it writes a wrapper config that imports yours and overrides three keys: mutate (the changed ranges), reporters (adds json) and jsonReporter.fileName. Test runner, thresholds, timeouts, checkers and plugins stay as you wrote them.
### Does it work with Jest, Vitest and monorepos?
Reapr runs whatever test runner your StrykerJS config uses, including Jest and Vitest, and it runs cargo-mutants for Rust projects. In a monorepo, run rpr with --project and --config per package, or list one config per package in the GitHub Action's configs input. Reapr runs on Linux (x86_64 and aarch64) today; macOS and Windows are planned.
### Is Reapr free?
Yes. The rpr CLI, the GitHub Action, the engine adapters and the agent skill are free and open source: FSL-1.1-ALv2 for the core and CLI (each release becomes Apache 2.0 after two years) and MIT for the adapters, the Action and the skill. A hosted layer, Reapr Cloud, is planned as a paid product and is not built yet.
---
# Reapr mutation-audit agent skill
> Your agent writes tests that pass. This skill makes them catch bugs. mutation-audit runs Reapr on your diff, writes one focused test per surviving mutant, and reruns until each one is killed. It never changes your production code.
Source: https://reapr.dev/skill
## What it does
The skill runs `rpr run --diff --json`. For each surviving mutant it states the behavior the mutant changes, writes one focused test in the repo's conventions, runs the test, then reruns rpr and keeps the test only when the mutant is killed. Equivalent mutants, which no test can kill, are reported instead of getting a forced test. When the intended behavior is unclear, it asks the human instead of writing a test that pins a possible bug. It also lists tests that cover the diff but kill no mutant, and checks the diff for edge cases mutation operators do not produce.
## Requirements
- rpr, Linux x86_64 or aarch64 (macOS and Windows are planned):
```sh
curl -LsSf https://github.com/reapr-corp/reapr/releases/latest/download/rpr-installer.sh | sh
```
- A mutation engine: StrykerJS with a config file (JavaScript, TypeScript; Node.js 20.10 or later), or cargo-mutants 26.2 to 27.x (Rust).
## Install in Claude Code
1. Install the plugin:
```text
/plugin marketplace add reapr-corp/reapr
/plugin install reapr@reapr
```
On Claude Code 2.1.275 or later, one step does both: `/plugin install reapr --marketplace reapr-corp/reapr`
2. Run `/reapr:setup`. It checks rpr, the engine and its config, measures the baseline score, and offers to add the CI job and the agent rule, asking before every change.
3. Run `/reapr:mutation-audit`, or ask Claude to check the tests on the current branch.
## Install in other agents (Cursor, Codex, Gemini CLI and others)
1. Install rpr with the command above.
2. Give the agent the rules below (the same file is at https://raw.githubusercontent.com/reapr-corp/reapr/main/plugins/reapr/AGENTS.md). It links the full workflow in SKILL.md: https://github.com/reapr-corp/reapr/blob/main/plugins/reapr/skills/mutation-audit/SKILL.md
3. Ask the agent to check the tests on the current branch.
The contents of AGENTS.md:
````markdown
# Reapr: instructions for coding agents
For any agent (Cursor, Codex, Copilot, Aider and others) asked to check, strengthen or review the tests of a
branch or pull request. Point the agent at this file or copy it into the agent's instructions.
The skill runs Reapr (`rpr run --diff --json`, or `rpr run --all --json` for a whole-codebase audit) and
uses the surviving mutants as evidence: one focused test per survivor, confirmed killed by a second run. It flags
tests that cover the scope but kill nothing, then audits the code for edge cases that mutation operators do not
represent.
## Hard rules
- Never change production code, thresholds or engine config, and never create an engine config. If rpr, the
engine or a StrykerJS config is missing, follow Setup below (`npx stryker init` is the human's to run).
Rust projects use `--engine cargo-mutants`, whose config is optional; it has no thresholds, so only
`--min-score` (the human's choice) gates it.
- When the intended behavior is ambiguous (the code disagrees with docs, types, names, callers, specs or an
existing test, or nothing specifies it), ask the human. Do not write a test that pins the current behavior.
- A test is done only when a later `rpr` run (the same `--diff ` or `--all` command as the first) shows
it kills a mutant. Engine-only checks (`cargo mutants -f `) are for iterating, not for the report.
Equivalent mutants are listed for the human with the input domain that makes them equivalent, not forced.
- Never delete tests; change existing tests only with the human's OK. One behavior per test, in the repo's
conventions, inside the diff scope. Rust tests go in the mutated package; commit them before re-running
`rpr --diff`, or the scope shifts.
- Do not change process-wide state (env vars, cwd, PATH) in tests; use a child process or a project-local fake.
- Generate one UUID v4 per session (`uuidgen` or `node -e 'console.log(crypto.randomUUID())'`) and prefix
every `rpr` command with `RPR_INVOKED_BY=mutation-audit RPR_SESSION_ID=`. It only tags Reapr's
anonymous telemetry and changes nothing when `DO_NOT_TRACK` is set.
## Setup (first use)
Ask before every step that changes anything. 1. `rpr --version`, else offer the installer
(`curl -LsSf https://github.com/reapr-corp/reapr/releases/latest/download/rpr-installer.sh | sh`). 2. Detect
StrykerJS (package.json with vitest/jest, `stryker.*config*` files, per package in monorepos) or cargo-mutants
(`Cargo.toml`); both → `--engine`. 3. cargo-mutants installed (`cargo install --locked cargo-mutants`); for
StrykerJS a config must exist: never write one, ask the human to run `npx stryker init`, and stop until it
exists. 4. Baseline: `RPR_INVOKED_BY=setup RPR_SESSION_ID= rpr run --all --json` per config; report score,
survivors per file and time. 5. Optionally, with consent, a `reapr-corp/run@v1` pull-request job and an agent
rule. Full steps: [skills/setup/SKILL.md](skills/setup/SKILL.md).
## Workflow
Follow the full workflow in [skills/mutation-audit/SKILL.md](skills/mutation-audit/SKILL.md). Ignore its YAML
header; where it says `/reapr:setup`, use the Setup section above. `${CLAUDE_SKILL_DIR}` in it means the
`skills/mutation-audit` folder next to this file.
````
## What it never does
- Change production code to kill a mutant. If a test shows the code is wrong, it reports the bug and stops.
- Lower a threshold, or change Reapr's flags to move the score.
- Edit your engine config, or invent one when the repo has none.
- Rewrite or delete an existing test without asking you.
## Results
- On Reapr's own Rust codebase, the skill raised the mutation score from 89.51% to 94.65%: 23 surviving mutants killed by 8 new tests, with no production code changed (https://github.com/reapr-corp/reapr/pull/30).
- In the skill's evals (four prompts on a TypeScript + Vitest + StrykerJS fixture, each run with and without the skill), 21 of 21 rule checks passed with the skill and 14 of 21 without it. Without the skill, the agent wrote its own Stryker config when the repo had none, and rewrote existing tests without asking (https://github.com/reapr-corp/reapr/pull/14, https://github.com/reapr-corp/reapr/blob/main/plugins/reapr/skills/mutation-audit/evals/evals.json).
## Frequently asked questions
### Does it work without Claude Code?
Yes. Claude Code installs it as a plugin. Cursor, Codex, Gemini CLI and other agents can follow the same workflow: point them at AGENTS.md in the reapr repository, which has the hard rules and links the full SKILL.md.
### Does it send my code anywhere?
No. The skill runs rpr and your own test suite on your machine. rpr sends one anonymous usage event per run, with counts and durations and never code, paths or names, and DO_NOT_TRACK=1 turns it off. Every property is listed at reapr.dev/telemetry.
### Does it change my Stryker or cargo-mutants config?
No. It never edits or creates an engine config, never lowers a threshold and never changes Reapr's flags. It adds test files, and it changes an existing test only after you say yes.
### What if my repo has no Stryker config?
The skill stops and tells you to create one with npx stryker init. It does not write one itself: the config decides the test runner, what is mutated and the thresholds, and those are your team's choices.
### Does it work with Rust?
Yes. rpr supports cargo-mutants (26.2 to 27.x), and the skill runs it with the project's own .cargo/mutants.toml when there is one. On Reapr's own Rust code it raised the mutation score from 89.51% to 94.65% with 8 new tests.
---
# How to speed up a slow Stryker run
> Where a small StrykerJS run spends its time, and the ignorePatterns, checker, logLevel and concurrency settings that change it.
Source: https://reapr.dev/guides/speed-up-stryker
A mutation run on a small diff should take seconds of test time. In practice it often takes most of a minute,
and almost none of that is tests. This guide shows where the time went in one measured case and which Stryker
settings changed it.
## The measured case
The numbers come from Reapr's [bench report](https://github.com/reapr-corp/reapr/blob/main/bench/results/2026-11-02-turborepo-monorepo.md):
one package of a pnpm + Turborepo monorepo (~55k files incl. build caches), StrykerJS 9.6.1, Vitest 4, a diff
that produced 3 mutants, and medians of 5 runs per variant on a 22-thread Linux machine. Every run was under
load from other jobs, so treat absolute seconds as inflated. The comparisons between variants are what matter.
## Where the time goes
A median run took 54.0 s. About 85% of it happened before the first test ran:
| Phase | Median | Share |
|---|---|---|
| Start, load the config, walk 55,389 files | 18.0 s | 33% |
| Instrument and preprocess | 4.1 s | 8% |
| Checker, sandbox copy, test runner startup | 24.8 s | 46% |
| Initial test run (83 tests) | 1.4 s | 3% |
| Testing the 3 mutants | 2.7 s | 5% |
| Reporters and temp-dir cleanup | 1.7 s | 3% |
Testing, the initial run plus the mutants, was about 8% of the wall time. Before you tune test speed, look at
startup.
## Keep build caches out of the sandbox
Stryker copies your project into a sandbox directory before it runs anything. This finding applies to
Turborepo repos with a local cache inside the repository (`.turbo/cache`); not every monorepo has one, and a
review of 8 open source Stryker repositories found none. In the measured package, `.turbo/cache` was 53,832 of
the 55,390 files copied (97%) and 1,277.6 MB of the 1.288 GB (99.2%). The config's `ignorePatterns` did not
exclude it.
Adding `.turbo` to `ignorePatterns` took a plain Stryker run from **38.4 s to 12.5 s** (−67%), with identical
results. CPU time fell from 62.8 s to 19.8 s and peak memory from 607 MB to 399 MB.
```js
// stryker.config.mjs
export default {
// ...your existing config
ignorePatterns: [".turbo"],
};
```
Stryker 9.6.1 and 10.x always ignore `node_modules`, `.git`, `*.tsbuildinfo`, `/stryker.log`, `.next`, `.nuxt`
and `.svelte-kit`. Anything else in the project directory is copied. Other large directories that your tests never read are candidates too, but do not
ignore files your test runner needs, such as its config or fixtures.
Stryker prints how many files it found at the default log level, in a line like
`ProjectReader Found N of M file(s)`. If M is far above the number of source files you expect, list the largest
directories:
```sh
du -sh .[!.]* * 2>/dev/null | sort -h | tail
```
## Point the TypeScript checker at the files you mutate
The TypeScript checker builds a TypeScript program to discard mutants that do not compile. In the measured
package it cost about 3.2 s and checked nothing. The tsconfig it used had no `include`, so it covered only
three shared Vitest config files. The mutated package was outside that program, and the checker let every
mutant through.
Check what your checker's tsconfig covers:
```sh
npx tsc --listFilesOnly -p tsconfig.json | grep -v node_modules
```
If the files you mutate are not in the list, point `tsconfigFile` at a tsconfig that includes them, or remove
the checker if you do not need compile-error filtering:
```js
export default {
checkers: ["typescript"],
tsconfigFile: "tsconfig.json", // must include the files you mutate
};
```
## Use debug logging only to diagnose
`--logLevel debug` is useful to find which phase is slow, but it is not free. In the measured case it added
21 s to the median wall time (59.4 s against 38.4 s at the default level). In one run it held Stryker's event
loop for about 4 s while it printed the list of 55k files.
## Do not raise concurrency for small diffs
With 3 mutants, `--concurrency 8` made the run slower, not faster. Starting four TypeScript checkers in
parallel took 8.9 s against 2.9 s, and mutant testing saved only 1.1 s. The phases added up to 5.7 s more. A
bigger pool pays off when there are many mutants to spread across it.
## About coverageAnalysis
`coverageAnalysis` decides which tests run against each mutant. With `perTest`, the default since Stryker 5,
Stryker runs only the tests that cover the mutant. With `off`, it runs every test for every mutant. Stryker's
docs say it does not change the score. The Vitest runner ignores the setting and always uses `perTest`, so for
Vitest projects there is nothing to tune here. For Jest, Mocha or Jasmine, keep `perTest` unless your tests
depend on each other's order.
## What Reapr does here
Reapr runs your Stryker config as written and does not change any of these settings. In the measured runs,
Reapr's own work outside the Stryker process took 1.7 s. It records the engine time, CPU time and peak memory
of every run in `.git/reapr/runs//run.json`, so you can compare a config change against the runs before
it.
## Frequently asked questions
### Why is Stryker slow even when only a few mutants run?
Most of the time goes to startup, not to tests. In a measured run with 3 mutants, about 85% of the wall time passed before the first test ran: walking the project, instrumenting files, copying the sandbox and starting the checker and test runner.
### What does ignorePatterns do in Stryker?
It lists files and directories that Stryker does not copy into its sandbox. Stryker 9.6.1 and 10.x always ignore node_modules, .git, *.tsbuildinfo, /stryker.log, .next, .nuxt and .svelte-kit. Other caches, such as Turborepo's local cache in .turbo, are copied unless you add them.
### Does coverageAnalysis change the mutation score?
No. Stryker's docs say coverage analysis is a performance optimization that does not influence the score. The default is perTest, and the Vitest runner always uses perTest whatever you set.
---
# Mutation testing on pull requests
> Why mutation testing belongs on the pull request diff, and how to scope a StrykerJS run to it, with or without Reapr.
Source: https://reapr.dev/guides/mutation-testing-pull-requests
Code review asks one question about tests: do they protect the change? Coverage cannot answer it. A mutation
run can, if it runs on the change and finishes while the pull request is open.
## Why scope to the diff
A mutation engine changes your code on purpose, one small change at a time (a mutant), and runs your tests
against each change. Run on a whole project, that is thousands of mutants. A signal that takes 40 to 90 minutes
ends up in a nightly job, where nobody connects a surviving mutant to the change that caused it.
Scoped to the lines a branch changed, the run covers only the code under review, and its result (a score and a
list of surviving mutants) belongs in the pull request, next to the code it is about.
## Without Reapr: StrykerJS and git
StrykerJS accepts line ranges in `mutate`:
```sh
npx stryker run --mutate "src/app.ts:5-7,src/util.ts:12-30"
```
The ranges come from git. Compare against the merge base, with no context lines:
```sh
git diff --unified=0 origin/main...HEAD -- 'src/**/*.ts'
```
Each hunk header such as `@@ -10,2 +12,4 @@` means lines 12 to 15 in the new file. Turn the headers into
`path:start-end` ranges and pass them to `--mutate`. Three things to handle yourself:
- **Your mutate globs.** A list of ranges replaces your config's `mutate` patterns, so test files and other
excluded paths in the diff get mutated unless you filter them out first.
- **Deleted lines.** A hunk that only deletes lines has no new lines to mutate. Skip it.
- **Incremental mode.** With `incremental: true`, Stryker's report keeps results from outside your ranges, so
its score is no longer the score of the diff. See
[why Stryker's incremental score is package-wide](/guides/stryker-incremental-score).
## With Reapr
Reapr does those steps and nothing else. It reads the diff from the merge base, keeps the ranges whose files
your own `mutate` patterns select (matched the way Stryker matches them), runs Stryker with your config, and
scores only the mutants inside the changed ranges:
```sh
rpr run --diff origin/main # human summary
rpr run --diff origin/main --min-score 80 # exit 1 below 80%
rpr run --diff origin/main --json # one JSON document, for CI and agents
```
The wrapper config Reapr writes imports yours and overrides three keys: `mutate`, `reporters` (adds `json`)
and `jsonReporter.fileName`. Your test runner, thresholds, timeouts, checkers and plugins stay as you wrote
them.
## On every pull request
The [reapr-corp/run](https://github.com/reapr-corp/run) action installs `rpr`, runs it against the pull
request's base branch, and writes a job summary with the score, counts, survivors and warnings:
```yaml
on: pull_request
jobs:
rpr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # full history, so origin/...HEAD has a merge base
- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
- run: npm ci
- uses: reapr-corp/run@v1
with:
min-score: 80
```
In a monorepo with one Stryker config per package, list them in `configs`, one per line. Every config runs,
and the summary combines them. A package the diff does not touch reports `nothing to mutate`.
## What it costs
Scoping removes mutants, not startup. In Reapr's [bench report](https://github.com/reapr-corp/reapr/blob/main/bench/results/2026-11-02-turborepo-monorepo.md),
a diff with 3 mutants still took a median of 54.0 s, and about 85% of that passed before the first test ran.
[How to speed up a slow Stryker run](/guides/speed-up-stryker) covers the settings that changed it.
## Frequently asked questions
### How do I run Stryker only on the lines a pull request changed?
StrykerJS accepts line ranges in mutate, for example src/app.ts:5-7. Get the changed ranges from git diff --unified=0 against the merge base and pass them with --mutate. Reapr does this for you with rpr run --diff origin/main and keeps only the files your own mutate globs select.
### Why not run mutation testing on the whole project in CI?
A full run on a real codebase can take 40 to 90 minutes, which ends up in a nightly job. A run scoped to the diff tests only the code under review, so its result belongs in the pull request.
### Can I fail a pull request on a low mutation score?
Yes. rpr run --diff origin/main --min-score 80 exits with code 1 when the diff's score is below 80%, and the reapr-corp/run GitHub Action takes the same min-score input. Your config's own thresholds.break also works.
---
# Why Stryker's incremental score covers the whole package
> Why StrykerJS with incremental true and --mutate ranges reports a package-wide score, and how Reapr gets the score of the diff.
Source: https://reapr.dev/guides/stryker-incremental-score
You run Stryker on the lines you changed and it reports a score for hundreds of mutants you never touched. That
is not a bug in your ranges. It is how incremental mode builds its report.
## What happens
With `incremental: true`, Stryker keeps the results of earlier runs in an incremental file and reuses them.
When you also pass `--mutate` with line ranges, it tests the mutants in those ranges and carries the earlier
results for everything else into the same report. Stryker's
[incremental docs](https://stryker-mutator.io/docs/stryker-js/incremental/) say so directly: mutants not in
scope are not removed from the full mutation report.
The score at the bottom of that report is then the score of the package, mixed from today's run and older
ones.
## A measured example
In Reapr's measurements on one package of a pnpm + Turborepo monorepo (~55k files incl. build caches,
StrykerJS 9.6.1, Vitest 4), a plain `npx stryker run --mutate ` on the diff's ranges reported
**87.0%** with 38 survivors, across the package. The diff itself had 3 mutants, all killed: **100%**. Both
numbers were correct; they answer different questions. Source: the
[README's measurements](https://github.com/reapr-corp/reapr#measurements) and the
[bench report](https://github.com/reapr-corp/reapr/blob/main/bench/results/2026-11-02-turborepo-monorepo.md).
A reviewer who reads 87.0% thinks the change left 38 gaps. It left none.
## How to get the score of the diff
Filter the JSON report to the mutants inside the changed ranges, then compute the score over them:
- detected = Killed + Timeout
- undetected = Survived + NoCoverage
- score = detected / (detected + undetected) × 100
CompileError, RuntimeError, Ignored and Pending do not count. This is the
[mutation-testing-metrics](https://stryker-mutator.io/docs/mutation-testing-elements/mutant-states-and-metrics/)
definition Stryker uses, so the result compares one to one with Stryker's own numbers.
"Inside the range" needs care. Stryker counts a mutant as in a range when it starts on or after the first
column of the range's first line and ends on or before its last line, comparing line and column together. Use
the same rule, or your count will drift from Stryker's at the edges.
## How Reapr does it
`rpr run --diff origin/main` reads the diff, runs Stryker on those ranges with your config (incremental
settings included, untouched), and filters the report with Stryker's containment rule. Results that Stryker
carried over from outside the diff are excluded, and a warning says how many:
```text
warning: stryker report carried 12 results from outside the diff scope (incremental); excluded from the diff score
```
The score, the counts and the survivors in Reapr's output are for the diff only. Stryker's unfiltered report
is still in the run record, `.git/reapr/runs//mutation.json`, if you want the package view too.
## A related trap: bail with Vitest 4
In the same earlier measurements, Vitest 4 with bail on counted some killed mutants as survived: the score was
87.8% with bail on and 98.0% with `disableBail: true`, on the same code. The effect depends on the setup: an
independent check on another Vitest 4 repository saw no difference. It can happen, so check it on yours: run once
with `disableBail: true` and compare the scores. Reapr warns when your config uses the Vitest runner with Vitest
4 or later and `disableBail` is not `true`. It only warns; whether to set `disableBail: true` is your call.
## Frequently asked questions
### Why does Stryker report hundreds of mutants when I only mutated a few lines?
With incremental true, Stryker reuses results from its incremental file and carries results from outside the --mutate ranges into the report. Its docs say mutants not in scope are not removed from the full report. The score in the report is then for the package, not for your lines.
### How do I get the mutation score of only the changed lines?
Filter the report to the mutants whose location falls inside the changed ranges and compute detected / (detected + undetected) over them. Reapr does this with rpr run --diff, using Stryker's own containment rule, and warns how many carried-over results it excluded.
### Should I turn off incremental mode?
Not necessarily. Incremental mode is a cache and can save time on repeat runs. The problem is only the score in the report. Read the score of the diff from a tool that filters it, or filter the JSON report yourself.
---
# Mutation testing for AI-generated code
> Why coverage cannot check tests that an agent wrote, how surviving mutants can, and the rules Reapr's mutation-audit agent skill follows.
Source: https://reapr.dev/guides/mutation-testing-ai-generated-code
Teams now ship a lot of code that an agent wrote, often with tests the same agent wrote. The tests pass and
coverage is high. Neither fact says the tests would catch a bug.
## Coverage measures execution, not verification
Line coverage records which lines ran while the tests ran. It cannot tell a test that checks the result from
one that only calls the function:
```ts
it("computes the shipping fee", () => {
expect(shippingFee({ total: 120 })).toBeDefined();
});
```
That test covers every line of `shippingFee`. Change `total > 100` to `total >= 100`, or return `0` instead of
the fee, and it still passes.
## What a mutation score measures
A mutation engine such as StrykerJS changes the code on purpose, one small change at a time: `>` becomes `>=`,
a condition becomes `true`, a return value changes. Each change is a mutant. If a test fails, the mutant is
killed. If every test still passes, the mutant survived, and you have a concrete change in behavior that no
test catches.
The score is detected / (detected + undetected): killed and timed-out mutants over those plus the survived and
uncovered ones. It is the share of the changes your tests would notice.
## Why this fits agents
A surviving mutant is a precise, checkable task: this line, this change, no test failed. Reapr prints each one
in its JSON output:
```json
{ "file": "src/a.ts", "line": 10, "column": 3, "mutator": "ConditionalExpression",
"replacement": "false", "status": "Survived" }
```
An agent can write a test for that exact behavior and prove it worked: run Reapr again and check that the
mutant is now killed. The evidence comes from the engine, not from the agent's own judgment.
## The rules that keep it honest
Every shortcut that raises the score can make the tests worse. Reapr's
[mutation-audit agent skill](https://github.com/reapr-corp/reapr/blob/main/plugins/reapr/skills/mutation-audit/SKILL.md)
writes these down as hard rules:
- Never change production code to kill a mutant. If a test shows the code is wrong, report it.
- Never edit engine config, thresholds or Reapr flags to change the result, and never create an engine config.
- Never delete or weaken tests. Change an existing test only with the human's OK.
- One behavior per test, in the repo's conventions, inside the diff scope.
- Assert intended behavior, not current behavior. When the code disagrees with the docs, types, names or
callers, ask the human instead of writing a test that pins a possible bug.
- A test is done only when a later Reapr run shows that it kills a mutant. Equivalent mutants, which no test
can kill, are listed for the human instead of getting a forced test.
The skill also lists tests that cover the diff but kill no mutant, and then checks the diff for edge cases that
mutation operators do not produce: boundaries, empty input, error paths, time zones and numeric precision.
## What the skill's evals showed
The skill was tested on a throwaway TypeScript + Vitest + StrykerJS repo, with and without the skill
([pull request #14](https://github.com/reapr-corp/reapr/pull/14)). On a weak-boundary fixture, the run with the
skill took the score from 61.90% to 90.48% and passed 7 of 7 checks. The run without it reached a higher score
but rewrote an existing test without asking. With no Stryker config in the repo, the skill stopped and pointed
to `npx stryker init`. Without the skill, the agent wrote its own config and then changed tests.
## Try it
The skill has its own page with the install commands and results: [reapr.dev/skill](/skill).
```sh
curl -LsSf https://github.com/reapr-corp/reapr/releases/latest/download/rpr-installer.sh | sh
rpr run --diff origin/main --json
```
In Claude Code, install the skill with `/plugin marketplace add reapr-corp/reapr` and
`/plugin install reapr@reapr`, then ask it to check the tests on your branch. For other agents, point them at
[AGENTS.md](https://github.com/reapr-corp/reapr/blob/main/plugins/reapr/AGENTS.md).
## Frequently asked questions
### Is code coverage enough to trust AI-written tests?
No. Coverage records which lines ran during the tests. A test that calls a function and asserts nothing useful still covers every line it runs. A mutation score checks whether a test fails when the code changes, which is what a test is for.
### How can an agent use mutation testing?
Run rpr run --diff origin/main --json. Each surviving mutant has a file, line, column, mutator and replacement. The agent writes one focused test per survivor, runs Reapr again, and keeps the test only if the mutant is now killed.
### Should an agent change code to kill a surviving mutant?
No. A surviving mutant is evidence about the tests. If a new test shows the code is wrong, that is a bug for a human to decide on. Reapr's mutation-audit skill never changes production code, engine config or thresholds.
---
# Telemetry
`rpr` sends one anonymous usage event per `rpr run`. It is on by default and easy to turn off.
## Why
To learn how Reapr is used in practice: which outcomes are common, how long runs take, how large diffs are, which
warnings and errors people hit, and on which platforms and CI systems. That decides what gets fixed and built
next. The event holds counts, durations and fixed categories only, never your code or anything that names you,
your project or your machine.
## How it works
- One event, `run_completed`, per `rpr run`, also when the run ends in an error (after the arguments were parsed).
`rpr --help`, `rpr --version`, invalid arguments and `rpr telemetry` send nothing.
- It is sent only after the output is printed, so it never delays or changes it. Nothing happens while the engine
runs. stdout is untouched: with `--json` the JSON document is still the only thing on stdout.
- It is sent by running `curl` (a `POST` with the JSON on curl's stdin). `rpr` waits at most 1.5 seconds, then
kills curl. Network errors are ignored. If `curl` is not on `PATH`, nothing is sent.
- The first time telemetry is active on a machine, `rpr` prints this notice once, on stderr:
```
rpr collects anonymous usage data (no code, paths or names) to improve the tool. Disable: rpr telemetry disable or DO_NOT_TRACK=1. Details: https://github.com/reapr-corp/reapr/blob/main/TELEMETRY.md
```
- Only when the event is sent, `rpr` runs `git rev-parse --is-shallow-repository` and
`git rev-list --max-parents=0 HEAD` in the project directory for `project_id` (below), each cut off after 1
second. Opted out, it runs nothing.
- In CI (any of `GITHUB_ACTIONS`, `GITLAB_CI`, `CIRCLECI`, `BUILDKITE`, `JENKINS_URL` or `CI` is set), nothing is
written to disk: each run uses a fresh random id and prints the notice (one stderr line).
Builds without a project key (every build before the key is set in the source) send nothing, print no notice and
write no state file.
## Where it goes
[PostHog](https://posthog.com) US cloud (`https://us.i.posthog.com/i/v0/e/`). Events are sent with
`$process_person_profile: false`, so PostHog creates no person profile. Retention: PostHog's default. The
project API key in the binary is public by design: it can only write events, not read them.
## What is sent
The event, exactly as sent:
```json
{
"api_key": "",
"event": "run_completed",
"distinct_id": "d211dd4c-b6f3-4622-b367-f3643505abc0",
"timestamp": "2026-11-02T17:20:00Z",
"properties": { "...": "below" }
}
```
`distinct_id` is the install id: a random UUID v4 created on the first run and kept in the state file (in CI, a
new one per run). It is not derived from anything about you or the machine. `timestamp` is RFC 3339 UTC.
| Property | Example | Meaning |
|---|---|---|
| `rpr_version` | `"0.1.0"` | The `rpr` version. |
| `os`, `arch` | `"linux"`, `"x86_64"` | Rust's `std::env::consts`; not the CI's `RUNNER_OS`. |
| `ci` | `true` | Whether a CI variable listed above is set. |
| `ci_provider` | `"github_actions"` | `github_actions`, `gitlab`, `circleci`, `buildkite`, `jenkins`, `other` (only `CI` is set) or `null` (not CI). From which variables are set, never their values. |
| `runner` | `"github_hosted"` | `local` when not in CI; `github_hosted` or `self_hosted` from GitHub's `RUNNER_ENVIRONMENT`; else `null`. |
| `trigger` | `"pull_request"` | What started the run. `local` when not in CI. In GitHub Actions, `GITHUB_EVENT_NAME` when it is `pull_request`, `pull_request_target`, `push`, `schedule`, `workflow_dispatch` or `merge_group`, else `other`. In GitLab CI, `CI_PIPELINE_SOURCE` mapped: `merge_request_event` → `pull_request`, `push`, `schedule`, `web` → `manual`, else `other`. `null` on other CI providers. Why: the share of runs that gate pull requests, Reapr's main use. |
| `via_action` | `true` | `RPR_VIA_ACTION` is `1`, which the [GitHub Action](https://github.com/reapr-corp/run) sets. Why: Action runs vs direct CLI runs in CI. |
| `agent` | `"claude_code"` | The coding agent running `rpr`, from which variables are set (never their values): `claude_code` (`CLAUDECODE`), `cursor` (`CURSOR_AGENT`), `codex` (`CODEX_THREAD_ID`, `CODEX_SANDBOX` or `CODEX_SANDBOX_NETWORK_DISABLED`), `gemini_cli` (`GEMINI_CLI`); `null` otherwise (a person, or an agent without a documented marker, such as the GitHub Copilot coding agent). Claude Code's IDE extensions also set `CLAUDECODE` in their integrated terminals. Why: runs by agents vs by people. Sources below. |
| `invoked_by` | `"mutation-audit"` | `RPR_INVOKED_BY`, set by the tool that ran `rpr`: `mutation-audit` or `setup` (Reapr's agent skills), `other` for any other value (never the value itself), `null` when unset. Why: score change per agent-skill session, and how many setups reach a baseline. |
| `session_id` | `"6f1c2a9e-3b7d-4e21-9c55-0a8d7e4b12f3"` | `RPR_SESSION_ID`: a random UUID v4 the skill generates once per session (`uuidgen`), so the runs of one session can be grouped and its first and last score compared. Sent only if it is a UUID v4, else `null`. Not derived from anything. Why: score change per agent-skill session. |
| `first_run` | `true` | `true` on the run that created the state file (a new install), `false` afterwards, `null` when nothing persists (CI, or no config dir). Why: new installs per week and how many come back. |
| `build` | `"release"` | `debug` for a debug build (`cargo build`, `cargo run`), else `release`. Why: development runs are filtered out of every chart. |
| `project_id` | `"cad1fb6d3fe90e6cebcffe875d929949"` | Anonymous project fingerprint (recipe below); `null` outside a git repository, in a shallow clone, or when git fails or takes over a second. Why: active projects per week, without knowing which projects they are. |
| `cpu_count` | `4` | Available parallelism. |
| `flags` | `["json", "min_score"]` | Names of the optional flags given: `all`, `json`, `min_score`, `config`, `engine`, `project`. Never their values. `--diff` is not listed. |
| `scope_mode` | `"diff"` | `diff` (`--diff`) or `all` (`--all`, a whole-project run). Why: how often Reapr is used to audit a whole codebase instead of a diff. |
| `engine` | `"stryker"` | Engine id: `stryker` or `cargo-mutants`; `null` on errors. |
| `engine_version` | `"9.6.1"` | The engine's version; `null` when unknown, on errors, or when it has any character other than letters, digits, `.`, `+` or `-`. |
| `outcome` | `"passed"` | `passed`, `min_score_failed`, `engine_gate_failed`, `nothing_to_mutate` or `error`. |
| `error_kind` | `"git"` | On errors: `config_missing`, `no_engine`, `ambiguous_engine` (several engines detected, no `--engine`), `git`, `engine_process`, `parse`, `io`, `args` (invalid `--project` or `--config` path) or `other`. A fixed kind, never the error message. `null` otherwise. |
| `warning_kinds` | `["vitest_bail"]` | Kinds of the warnings printed: `vitest_bail`, `incremental_carried_over`, `git_metadata`, `engine_version_unsupported` (the engine version is outside the adapter's supported range), `other`. Never their text. |
| `duration_ms_total` | `90123` | Wall time of `rpr`, from start to the printed output. |
| `duration_ms_engine` | `83456` | Engine wall time (`duration_ms` in the JSON output); `null` on errors. |
| `mutants_in_scope` | `16` | Mutants in the diff scope (sum of `counts`). |
| `counts` | `{"killed": 14, "survived": 2, ...}` | Mutants per status, as in the JSON output. |
| `score` | `87.5` | Mutation score, rounded to one decimal; `null` when there is none. |
| `survivors` | `2` | Number of surviving mutants. Never their files, lines or code. |
| `ranges_bucket`, `files_bucket` | `"6-20"`, `"2-5"` | Changed ranges and files in the diff, as a bucket: `0`, `1`, `2-5`, `6-20`, `21-100`, `101+`. |
| `carried_over_excluded` | `"0"` | Results an incremental report carried from outside the diff and Reapr excluded, bucketed the same way. |
| `engine_cpu_ms` | `280000` | CPU time (user + system) of the engine's process tree; `null` where it is not measured. |
| `engine_max_rss_kb` | `812000` | Peak RSS of the engine's largest process, in KiB; `null` where it is not measured. |
| `$process_person_profile` | `false` | Tells PostHog not to create a person profile. |
Coding agent markers, as each vendor documents them:
- Claude Code: `CLAUDECODE`, "Set to `1` in subprocesses Claude Code spawns"
([docs](https://code.claude.com/docs/en/env-vars)).
- Cursor: `CURSOR_AGENT`, "to detect when Cursor is running" ([docs](https://cursor.com/docs/agent/terminal)).
- Codex CLI: `CODEX_THREAD_ID`, `CODEX_SANDBOX`, `CODEX_SANDBOX_NETWORK_DISABLED`, set on spawned commands in
its official source ([exec_env.rs](https://github.com/openai/codex/blob/main/codex-rs/core/src/exec_env.rs),
[spawn.rs](https://github.com/openai/codex/blob/main/codex-rs/core/src/spawn.rs)). Not `CODEX_HOME`, a user
setting.
- Gemini CLI: `GEMINI_CLI`, set to `1` for shell commands
([docs](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/shell.md)).
### `project_id`
The hex SHA-256 of `reapr-telemetry-v1:` followed by the repository's root commit shas (`git rev-list
--max-parents=0 HEAD`, sorted, joined with `,`), truncated to its first 32 hex characters. You can compute it:
```sh
printf 'reapr-telemetry-v1:%s' "$(git rev-list --max-parents=0 HEAD | sort | paste -sd, -)" | sha256sum | cut -c1-32
```
The same repository gives the same id on every machine and clone, so runs of one project can be counted
together; the sha itself is not sent and cannot be read back from the hash. The limitation: for a public
repository, someone who computes the same hash for a known repository could confirm it uses Reapr; nothing else
about the project is revealed. A shallow clone sends `null`: its root is the shallow boundary, a different commit
on every run. Turn telemetry off to send nothing at all.
On errors, the run-result properties (`engine` through `engine_max_rss_kb`, except `outcome`, `error_kind` and
`duration_ms_total`) are `null` or empty. There is no `configs` property: `rpr` runs one config per call (the
GitHub Action's `configs` input calls it once per config).
## What is never sent
Code, mutants, file paths, file or repository names, URLs, branch names or the `--diff` base, commit shas (only
the `project_id` hash of the root commits), emails, commit trailers, git config, the contents of your engine
config, error or warning messages (only their kind), usernames, hostnames, runner names, and the values of
environment variables (except `trigger`'s allow-listed event names and `RPR_VIA_ACTION=1`). The event has no IP address field;
PostHog, like any server, sees the IP address the request comes from.
A unit test builds an event from a run full of paths, emails and shas and fails if any value contains `/`, `\`,
`@` or 32 or more hex characters in a row, except `project_id`, which must be exactly a 32-character hex hash.
## How to turn it off
In precedence order:
1. `DO_NOT_TRACK` set to any non-empty value other than `0` turns it off ([consoledonottrack.com](https://consoledonottrack.com)).
2. `RPR_TELEMETRY=0` (or `false`, `off`, `no`, any case) turns it off; `RPR_TELEMETRY=1` (or `true`, `on`, `yes`)
turns it on. Other values are ignored.
3. `rpr telemetry disable` or `rpr telemetry enable` saves the setting.
4. Otherwise it is on.
`rpr telemetry` (or `rpr telemetry status`) prints whether it is on and why, the install id and the state file
path. It never sends anything.
In GitHub Actions, set it for the job:
```yaml
env:
DO_NOT_TRACK: 1
```
## See the payload
`RPR_TELEMETRY_DEBUG=1` prints the exact JSON that would be sent on stderr, on one line, and sends nothing:
```sh
RPR_TELEMETRY_DEBUG=1 rpr run --diff origin/main --json > /dev/null
```
## State file
`${XDG_CONFIG_HOME:-$HOME/.config}/reapr/telemetry.json`:
```json
{
"install_id": "d211dd4c-b6f3-4622-b367-f3643505abc0",
"enabled": null,
"notice_shown": true
}
```
`enabled` is `null` until you run `rpr telemetry enable` or `disable`. Delete the file to get a new install id.
If the file cannot be read or written, `rpr` behaves as if it were missing and never fails because of it.
Source of truth: https://github.com/reapr-corp/reapr/blob/main/TELEMETRY.md
---
# Privacy
> What reapr.dev and the Reapr Cloud waitlist collect, where it is stored, how long, and how to ask for deletion.
Source: https://reapr.dev/privacy
This notice covers reapr.dev and the Reapr Cloud waitlist. The `rpr` command-line tool has its own notice: see
[Telemetry](/telemetry).
## The website
reapr.dev sets no cookies and uses no local storage. Fonts and images are served from reapr.dev itself. Two
third-party scripts can load, both from Cloudflare: the Web Analytics script, only when your browser does not
ask for privacy (see below), and the Turnstile spam check, only on pages with the waitlist form.
The site is hosted on Cloudflare Workers. Like any web host, Cloudflare receives the IP address and request
details of each visit in order to deliver the page, under
[Cloudflare's privacy policy](https://www.cloudflare.com/privacypolicy/).
## What the site measures
Two things, both without cookies, and neither tied to a person.
**Page views, with Cloudflare Web Analytics.** It counts views in aggregate: the page, the referrer, the
browser and device type, the country, and page-load timings. It sets no cookies and keeps no state in your
browser. Script: `static.cloudflareinsights.com`, reporting to `cloudflareinsights.com`.
**Five product events, sent through reapr.dev itself.** The page posts them to `/api/event` on reapr.dev. The
site's Worker drops anything not on this list, then forwards the event to PostHog (US cloud):
| Event | Properties | When |
|---|---|---|
| `install_copied` | `command`: `install` or `run` | You copy the install or run command. |
| `cta_clicked` | `cta` (which button), `page` (the path) | You click a main call-to-action button. |
| `guide_viewed` | `slug` (which guide) | You open a guide. |
| `skill_install_copied` | `which` (which command or text) | You copy an install command or the agent prompt on [/skill](/skill). |
| `waitlist_submitted` | `interests` (the boxes you ticked) | You join the waitlist. Never your email. |
Each event gets a new random id, so events cannot be linked to each other or to you. PostHog is told not to
create a person profile and not to look up a location. PostHog receives the event from Cloudflare's servers,
not from your browser, so it never sees your IP address. The Worker stores nothing about the event.
**How to opt out.** Turn on Global Privacy Control or Do Not Track in your browser. With either one set, pages
load no analytics script and send no events, and the Worker discards events and does not report waitlist
sign-ups. A content blocker that blocks `static.cloudflareinsights.com` and `/api/event` has the same effect.
## The Reapr Cloud waitlist
When you join the waitlist on [/cloud](/cloud), Reapr stores:
- your email address,
- the interests you ticked,
- the time you signed up, and the time of your last change if you sign up again (UTC),
- a keyed hash of your IP address.
**Why:** to email you when Reapr Cloud opens, and to learn which features matter most. The IP hash exists only
to limit how many sign-ups one address can send per minute, against spam. It is an HMAC-SHA-256 with a secret
key, so it cannot be turned back into your IP address without that key. Your IP address itself is never stored.
**Spam check:** the waitlist form, and only the form, uses
[Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/). It runs in your browser when the form
loads and gives the form a one-time token, which reapr.dev checks with Cloudflare when you submit. Turnstile
sets no cookies for this. What Cloudflare processes is described in its
[Turnstile Privacy Addendum](https://www.cloudflare.com/turnstile-privacy-policy/). Turnstile needs JavaScript,
so the form cannot be sent without it.
If you sign up again with the same email, the interests you tick are added to the ones you ticked before, and
the time of the change is stored next to the first sign-up time.
**What we do not do:** sell, share or rent the list, send it to a mailing service, or use it for anything other
than Reapr Cloud announcements.
**Where:** in Cloudflare Workers KV, on Reapr's Cloudflare account.
**How long:** until the list is no longer needed for Reapr Cloud announcements, or until you ask to be removed,
whichever comes first. Rate-limit counters are kept by Cloudflare's rate limiting for one minute and are not
stored with your entry.
## How to ask for access or deletion
There is no email address for Reapr. To see or delete your waitlist entry, open a
[private report on GitHub](https://github.com/reapr-corp/reapr/security/advisories/new) with the email address
you signed up with. Only the maintainers can read it. For questions about this notice, use
[GitHub Discussions](https://github.com/reapr-corp/reapr/discussions).
## Changes
Changes to this notice are published on this page, with the date below.