Skip to main content
Pre-release

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.py pytest 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

  1. The agent runs mutineer run --format agent.
  2. For each survivor_rule finding, it writes a test through the named seam, or says why the mutant can't change visible behaviour.
  3. It runs mutineer recheck --format agent, which reruns only those mutants with the rule tests and checks for tampering again.
  4. After max_rounds rechecks with survivors left, recheck exits 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)

Source distribution for mutineer 0.1.0.dev0
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)

Table of built distributions (wheels) for mutineer 0.1.0.dev0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page