Independent test inspection / Ruby ≥ 3.4

Make your
tests prove it.

A passing test is a claim. Change the code and see if that claim holds.

Mutineer breaks your Ruby code one change at a time. Your tests should catch it. When they don’t, you get the exact line they missed.

Open source. MIT licensed. Prism + stdlib only.

00 runtime dependencies 20 mutation operators 01 change per mutant JSON evidence for your pipeline
In practice

8,170spec examples, down from 17,702 (−54%)

Stephen Margheim had Opus 5.5 use Mutineer to check that every spec in his team's suite did real work, and refactor the suite. Lines of spec code fell 27%.

Read Stephen's post on X
Before you adopt it

Is it production-ready?

Read this section before you gate an app on Mutineer. Use the Status column to decide. Adopt a row whose Status says supported or serial only. Not supported in the Parallelism column means --daemon cannot run that row.

Mutineer itself needs Ruby 3.4 or later. In-process runs fork, so use Linux or macOS. A plain Ruby project with no database is supported. It runs in parallel on those systems.

RubyDatabaseFrameworkParallelismStatus
3.4 or laterSQLite fileMinitestsupported with --daemonsupported
3.4 or laterSQLite fileRSpecnot supportedserial only
3.4 or laterPostgreSQLMinitestsupported with --daemonsupported
3.4 or laterPostgreSQLRSpecnot supportedserial only
3.4 or laterMySQLMinitestnot supportedserial only
3.4 or laterMySQLRSpecnot supportedserial only
3.4 or laterSQLite in memoryMinitestnot supportedserial only
3.4 or laterSQLite in memoryRSpecnot supportedserial only
3.4 or latermore than one databaseMinitestone workerserial only
3.4 or latermore than one databaseRSpecnot supportedserial only

This matrix is for a Rails app on Ruby 3.4 or later. supported means you can run that cell in parallel. serial only means the safe run uses one worker. not supported means --daemon cannot run that cell. SQLite file means a database file, not :memory:. Minitest on that file can use --daemon. RSpec on that file is serial only. PostgreSQL with Minitest can use --daemon. PostgreSQL with RSpec is serial only. MySQL is serial only. A :memory: database is serial only. An app with more than one database runs one worker. --daemon isolates only the primary connection. --rails without --daemon is serial on every row. A Postgres worker database is copied once, at the start of the run. A test that commits rows outside a transaction leaves them in that worker database. The next mutant on that worker sees those rows. Rails parallel tests have the same ceiling.

An app on an older Ruby uses --test-command. Mutineer still runs on Ruby 3.4 or later. That path is serial. It does not narrow tests by coverage. A crash in the command can count as a kill.

What a release will not break is in STABILITY.md.

How it works

Mutate. Run. Report.

A surviving mutant shows a change your tests did not catch. Inspect it, strengthen the test, and run again. Some changes are equivalent and need no new test.

One change. One missed boundary.COMPARISON
ORIGINALreturn 0 if qty >= 10
MUTANTreturn 0 if qty > 10

Still passes? Your tests missed qty = 10.

Show the test that catches it
assert_equal 0, calculator.discount(10)

The original returns 0 at 10. If the changed method returns a different value, this assertion fails and kills the mutant.

Illustrative example; no Ruby runs in this page.

Mutate

Mutineer parses your source with Prism and applies one change per mutant — flip a < to <=, swap && for ||, delete a statement. Each mutant is re-parsed to confirm it's still valid Ruby.

Run

Every mutant runs your suite in a forked, isolated process — across all your cores. A coverage map means each mutant only runs the test files that actually reach it.

Report

Killed = your tests caught it. Survived = they didn't. You get a mutation score and a unified diff for every survivor, pinned to the exact line. Set a --threshold to fail CI, or --format html for a shareable report. Inspect example report data → in the website theme.

Reading a result24 caught. 1 missed.

96% mutation score. 24 ÷ (24 + 1). An uncovered mutant is excluded from this score.

Inspect a sample report
AI agents & CI

Give your pipeline evidence.

Line coverage tells you which code ran under test. It can't tell you whether a test would notice if that code broke — exactly where AI-generated tests are weakest. Mutineer answers that, programmatically: versioned JSON, mutant ids that survive unrelated edits, structured exit codes, and diff-scoped runs.

Agent inner loop

Close the loop on test quality

An agent writes code and tests, then runs Mutineer on just the diff. Each survivor ships a ready-made diff to feed back: "write a test that fails under this change." Stop when survivors hit zero.

--since origin/main --format json

PR / CI gate

Fail only when a PR makes tests worse

Diff the run against a stored baseline by mutant id. Exit 1 on any new survivor or a score drop — adopt mutation testing on a legacy suite without fixing everything first.

--baseline prior.json --threshold 90

GitHub Action

One step in your workflow

A composite action wraps the CLI: point it at your sources, a baseline, and a threshold. Reads the diff, posts the report, sets the exit code.

uses: davidteren/mutineer@v1

Why Mutineer

Small tool. Exacting work.

Prism and the standard library provide the runtime. Test-framework changes stay in disposable child processes; Rails apps can use the optional --rails or --boot entry point.

Zero runtime deps

Prism ships with Ruby ≥ 3.4 and everything else is stdlib. Nothing to pin, nothing to break on upgrade.

Valid mutants only

Every mutation is re-parsed before it runs. Syntactically broken mutants are skipped, not counted against your score.

Fork isolation

Each mutant runs in its own forked process on Linux & macOS — no state bleed between runs, parallel by default.

Coverage-mapped

A per-mutant coverage map runs only the test files that reach the mutated line. Digest-keyed cache skips unchanged work.

CI & agent ready

Stable, sorted --format json with a versioned schema and mutant ids that survive unrelated edits. --threshold and --baseline gate the build.

Rails-aware

--rails boots config/environment once, then forks per mutant with fork-safe ActiveRecord — dogfooded against a real Rails app.

Operators

Twenty ways to break your code.

Five run by default. Fifteen Tier-2 operators are off until you ask for them with --operators or .mutineer.yml — they're noisier, but they catch deeper gaps.

Default set keeps survivor output focused; opt into Tier 2 for a harder pass.
OperatorMutation ruleTier
comparison< ↔ <= , > ↔ >= , == ↔ !=default
arithmetic+ ↔ − , * ↔ / , % → * , ** → *default
boolean_connector&& ↔ ||default
boolean_literaltrue ↔ false , nil → truedefault
statement_removalreplace a non-final statement with nildefault
return_nilreplace a return / final expression with niltier 2
literal_mutationinteger → 0, 1, n+1 ; string → emptytier 2
condition_negationwrap if/unless/ternary condition in !( … )tier 2
string_literalnon-empty string → "" ; "" → "mutineer"tier 2
regexdrop ^ / $ anchors ; + ↔ *tier 2
collection_methodmap ↔ each , all? ↔ any? , first ↔ last , min ↔ max , select ↔ rejecttier 2
safe_navigation&. → .tier 2
range.. ↔ ...tier 2
negation_removal!x , not x → xtier 2
chain_linkdrop one call from a chain: a.b.c → a.ctier 2
operand_removala && b → a , btier 2
array_literal[a, b] → []tier 2
condition_trueif / elsif / unless / ternary / modifier / case-in guard condition → truetier 2
condition_falseif / elsif / unless / ternary / modifier / case-in guard condition → falsetier 2
operator_assignment+= ↔ −= , *= ↔ /= , %= → *= , **= → *= (not ||= , &&=)tier 2

Run mutineer --list-operators to see the live set for your installed version.

Mutineer and Mutant

Clean-room, not a fork.

Mutineer was written from scratch. It contains no code from Mutant or any other mutation-testing tool. Mutineer’s own code is licensed under MIT.

Mutant is the established tool, with a deeper operator set. Mutineer trades that depth for an MIT license, no dependencies, and a JSON contract for CI and agents.
AspectMutineerMutant 0.17
LicenseMIT, for every useFree for open source; commercial use needs a paid subscription
Runtime dependenciesNone (Prism + stdlib)parser, unparser, sorbet-runtime and others
Ruby3.4 and later (older app Rubies via --test-command)3.3 and later
Test frameworksMinitest and RSpecRSpec, Minitest and Test::Unit
What you mutateFiles, or one method with --onlySubjects named by expression

Checked against Mutant 0.17 on 2026-10-06. Full comparison and when to pick each: README.

Get started

Put your tests to the test.

Ruby ≥ 3.4 is the only requirement. Point Mutineer at your source and the tests that cover it.

Install

$ gem install mutineer

Run against a file and its test

$ mutineer run lib/foo.rb \ --test test/foo_test.rb \ --threshold 90

Gate a PR against a baseline

$ mutineer run app/ \ --since origin/main \ --baseline .mutineer/baseline.json

Preview mutations without running tests

$ mutineer run --dry-run lib/foo.rb

Configure once in .mutineer.yml

# .mutineer.yml operators: - comparison - arithmetic - return_nil threshold: 85 jobs: 8

Select JSON output on the command line:

$ mutineer run lib/foo.rb --test test/foo_test.rb --format json

Key flags: --test FILE, --threshold, --since, --baseline, --format human|json|html, --output FILE, --jobs, --rails, --daemon, --timeout SECONDS, --dry-run, --matrix. Typed flags override .mutineer.yml. Full list: mutineer --help or the README.