This release is a pre-release and may not be stable for production use.
Mutineer
Mutineer checks whether agent-written work is really done before it merges. It answers one question: if the agent had got a promised behaviour wrong, would a test have noticed?
It builds on mutmut and adds what agent work needs:
- Diff-scoped mutation testing. Only the functions your branch changed are mutated.
- A rules ledger. Promised behaviour lives in
.mutineer/rules.toml. Tests that prove a rule carry@pytest.mark.rule("SHIP-003"). Every other test stays free to change. - Tamper detection. A rule test that was weakened, skipped, unmarked or deleted without a matching change to the rules file blocks the merge.
- Triage. Gaps in promised behaviour go to the agent; questions about everything else go to a human.
Mutineer is deterministic: no LLM calls.
Install
Mutineer runs inside your project's environment, next to pytest, because mutmut and Mutineer's pytest plugin import your code.
uv add --dev mutineer # or: pip install mutineer
Python 3.11 or newer. mutmut doesn't run on native Windows; use WSL.
Set up
mutineer init
init writes a commented .mutineer/rules.toml and adds mutants/ and
.mutineer/last-run.json to .gitignore. It never overwrites anything.
mutmut needs to know where your code is. In pyproject.toml:
[tool.mutmut]
source_paths = ["src/shop"]
pytest_add_cli_args_test_selection = ["tests/"]
Write down your promises, each with a stable id and the public entry point (the seam) it should be tested through:
# .mutineer/rules.toml
version = 1
[[rule]]
id = "SHIP-003"
statement = "Orders of $50 or more ship free"
seam = "shop.shipping.quote"
Mark the tests that prove a rule, on the test, its class, or the whole module:
import pytest
from shop.shipping import quote
pytestmark = pytest.mark.rule("SHIP-003")
def test_well_over_threshold_ships_free():
assert quote(80) == 0
Rules are never deleted. To drop one, set status = "retired" and keep the entry.
Use
mutineer run # judge the diff from the merge-base to the working tree
mutineer run --format agent # compact JSON for the implementing agent
mutineer recheck --format agent # after the agent adds tests: rerun only its survivors
mutineer rules check # validate rules.toml and list each rule's tests
run compares the working tree (uncommitted and untracked files included) with
git merge-base <base> HEAD. The base is --base, else [tool.mutineer] base, else
main, else master.
| Option | |
|---|---|
--format terminal|markdown|json|agent |
terminal (default), a PR comment, the full verdict, or agent JSON |
--output FILE |
write the report to a file instead of stdout |
--no-mutation |
tamper and rules checks only; no mutmut run, no scores |
--strict |
exit 1 on warn |
recheck --max-rounds N |
round cap (default [tool.mutineer] max_rounds, 3) |
The verdict
Every run ends in one status:
- block: a rule test was tampered with and the rules file doesn't change that rule,
or the test suite fails on its own (including a test file or
conftest.pypytest can't collect). - warn: a mutant in code a rule test covers survives every test, or a warn-level tamper or rules finding.
- pass: none of the above. Questions for a human are listed but never change the status.
Two scores sit beside it:
- rule score: mutants in changed functions that a rule test covers, killed by rule tests alone. The headline number.
- diff score: mutants in all changed functions, killed by any test.
Timeouts and other inconclusive mutants count in neither score. Changed lines mutmut
can't mutate (decorated functions, module-level code, excluded files) are reported as
unmeasured_change and never count as tested.
Who gets what
In a function a rule test covers, each mutant is run twice: against all tests and against rule tests only.
| All tests | Rule tests only | Finding | For |
|---|---|---|---|
| caught | caught | none | |
| caught | survives | guarded_by_impl_only: is this part of the rule? |
human |
| survives | survives | survivor_rule: test the rule through its seam |
agent |
Survivors in code no rule covers become survivor_unowned ("Is this behaviour a
promise?"), and a changed function no test reaches is unreached. Both are for a human.
The agent loop
- The agent runs
mutineer run --format agent. - For each
survivor_rulefinding, it writes a test through the named seam, or says why the mutant can't change visible behaviour. - It runs
mutineer recheck --format agent, which reruns only those mutants with the rule tests and checks for tampering again. - After
max_roundsrechecks with survivors left,recheckexits 3: a human takes over.
recheck exits 4 if there's no previous run, or if the merge-base moved since then; run
mutineer run again in that case.
Exit codes
| Code | Meaning |
|---|---|
| 0 | pass or warn |
| 1 | warn, with --strict |
| 2 | block |
| 3 | recheck round cap reached; needs a human |
| 4 | Mutineer could not run (bad config, unknown base, deleted rule, usage error) |
What Mutineer writes
run and recheck write only to mutants/ (mutmut's working directory) and
.mutineer/last-run.json (the state recheck reads), unless you pass --output.
mutants/ is deleted at the start of every run and recheck. mutmut's cache gives
wrong answers across branches and between Mutineer's two passes, so Mutineer always
starts clean. If you keep your own mutmut results in mutants/, they will be lost; run
mutmut and Mutineer in separate checkouts if you need both.
Your mutmut config ([tool.mutmut], or [mutmut] in setup.cfg) is never edited: Mutineer sets mutmut's scope in memory. The
rule pass adds -p mutineer --mutineer-rule-only to pytest, and leaves any -m of yours
(in addopts or pytest_add_cli_args) alone.
Configuration
[tool.mutineer]
base = "main" # default: main, else master
rules_file = ".mutineer/rules.toml"
test_paths = ["tests"] # where tamper detection looks for tests
max_rounds = 3 # recheck round cap
License
Apache-2.0
Metadata
Release files for mutineer 0.1.0.dev0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mutineer-0.1.0.dev0.tar.gz | 104.9 kB | Without provenance | Details for mutineer-0.1.0.dev0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mutineer-0.1.0.dev0-py3-none-any.whl | Python 3 | none | any | Details for mutineer-0.1.0.dev0-py3-none-any.whl |
Total release size: 176.6 kB
Release files / mutineer-0.1.0.dev0.tar.gz
| Download URL | mutineer-0.1.0.dev0.tar.gz |
|---|---|
| Size | 104.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2d0d0c0a0984d0b00a22303bd15c1654b1f34405991d20e48fe6405c9f06369e
|
|
BLAKE2b-256 checksum How to use checksums |
7b39b77dcf638a2c48ed81bb69e9ea1d4fcbad1ace8b4ccc2713650146e981f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mutineer-0.1.0.dev0-py3-none-any.whl
| Download URL | mutineer-0.1.0.dev0-py3-none-any.whl |
|---|---|
| Size | 71.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
feeb5429b1a386a9746399ca43e81ae0788ce73a00e249c30bc4e0c7c25a4bf7
|
|
BLAKE2b-256 checksum How to use checksums |
5811100a1a70b4015f233109fc83de402ed03889bd80130a7f80ce7a1e09f2d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for mutineer 0.1.0.dev0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.