halter
"Halt, there are problems with this." An auditor for a change to a Python tree.
halter takes one git revision (or the working tree) of a Python
repository, runs the tree's own tests and a battery of checks over the
diff against a baseline, and answers accept or refuse with one
line per check. It has no plan, no model and no network: everything it
decides is decided from the tree, the tools it shells out to, and its
own gate code. It is the audit-only release of the saddle harness.
What it audits
For the diff baseline..tree, in this order:
| check | what passes |
|---|---|
syntax |
every changed Python file parses |
ruff |
the diff introduces no new ruff check finding and is ruff format-clean |
tests |
the test command exits 0 |
coverage |
every changed line runs under the tests (changed-line coverage 100%) |
dead-code |
every private definition the diff adds is mentioned somewhere in the tree |
public-deletions |
every public definition the baseline had is still defined |
red-phase |
new tests fail before the change and pass after it; with no test change, coverage plus a mutation kill-rate of at least 85% carries the proof |
assertion-preservation |
pre-existing tests keep their assertions |
mutation |
mutmut mutants on the changed lines are killed by the tests (100% on small samples) |
Four checks that only make sense against a plan (node-scope,
target-scope, property-coverage, requirement-binding) are reported
n/a. Untracked files in the working tree count as part of the change;
mutants/, .coverage, bytecode and tool caches at the tree's top level
do not.
Verdicts are cached under ~/.cache/halter/audit keyed by the tree hash,
the baseline, the test command and a hash of halter's own gate modules
and tool versions, so a second audit of the same tree is served without
re-running anything. --no-cache disables that.
Exit codes
| exit | verdict | meaning |
|---|---|---|
| 0 | accept |
every applicable check passed |
| 1 | refuse |
at least one check failed; the report says which |
| 2 | could not audit | not a git repository, unknown revision or baseline, a linked worktree |
| 3 | nothing to audit |
tree and baseline are identical |
With --json the result dict is printed instead of the text report; the
exit code is the same.
Usage
halter # the working tree of . against HEAD
halter REV # commit REV against REV^, from a fresh clone
halter REV --baseline B # commit REV against B
halter --repo PATH # audit another repository
halter --test-command CMD # default: python -m pytest -q
halter --json # machine-readable result
halter --no-cache # neither read nor write the verdict cache
In REV mode the commit is checked out in a temporary clone, so nothing
uncommitted in the source repository reaches the gates and nothing is
written to it.
Prerequisites
- Python >= 3.12.
- From pip, installed with halter:
coverage,ruff,mutmut(3.x) andpydantic. halter callsruff,coverageandmutmutby name, so the environment that provideshaltermust also put them onPATH(pipx install halterand a plain venv both do). - From the OS:
git, andprlimit(util-linux) for the memory ceiling. - The audited tree's own test command must be runnable from the tree with
the interpreter on
PATH; the default ispython -m pytest -q, so the tree's dependencies andpytestmust be importable there.
Memory ceiling and timeouts
Every subprocess that executes the audited tree's code, that is the test
command and mutmut run, is launched under prlimit --as=6GiB
(RLIMIT_AS, 6 GiB). Code that grows past it gets a MemoryError in its
own process and its tests fail; the audit itself keeps running.
The test command is bounded at 300 s and the mutation run at 600 s of wall clock. A run that exceeds its bound is reported as a failed check, not a crash.
A worked example
A throwaway repository with two commits: n.py returning 1, then a fix
to 2 with a test.
git init example && cd example
printf 'def f():\n return 1\n' > n.py
git add -A && git commit -m baseline
printf 'def f():\n return 2\n' > n.py
printf 'from n import f\n\n\ndef test_f():\n assert f() == 2\n' > test_n.py
git add -A && git commit -m 'fix f'
Audit the second commit:
$ halter HEAD
audit tree d6ec5987d44d baseline b32d48a01db2 surface 374ed4f61324 fresh
PASS syntax 2 file(s) parsed
PASS ruff 2 file(s) clean
PASS tests 'python -m pytest -q' exited 0
PASS coverage every changed line runs
PASS dead-code every private definition added is mentioned elsewhere in the tree
PASS public-deletions every public definition the baseline had is still defined
PASS red-phase fail pre-change, pass post-change
n/a node-scope audit: no plan assigns the diff a node kind
n/a target-scope audit: no plan declares target files
n/a property-coverage audit: no plan pairs a property test with an implementation
PASS assertion-preservation 0 pre-existing test(s) keep their assertions
n/a requirement-binding audit: no plan declares requirements
PASS mutation killed 1 of 1 changed-line mutants (100.0% >= 100.0%)
verdict: accept
$ echo $?
0
Now add an untested module to the working tree and audit that:
$ printf 'def g():\n return 7\n' > m.py
$ halter
audit tree cf04ccbe17e6 baseline 381ea238b0de surface 374ed4f61324 fresh
PASS syntax 3 file(s) parsed
PASS ruff 1 file(s) clean
PASS tests 'python -m pytest -q' exited 0
FAIL coverage no test runs m.py:1, m.py:2
PASS dead-code every private definition added is mentioned elsewhere in the tree
PASS public-deletions every public definition the baseline had is still defined
FAIL red-phase tests unchanged and coverage failed; nothing proves the change
n/a node-scope audit: no plan assigns the diff a node kind
n/a target-scope audit: no plan declares target files
n/a property-coverage audit: no plan pairs a property test with an implementation
PASS assertion-preservation 1 pre-existing test(s) keep their assertions
n/a requirement-binding audit: no plan declares requirements
FAIL mutation killed 0 of 1 changed-line mutants (0.0% < 100.0%) (small sample: 1 mutant(s), all must die): survived 1: m.x_g__mutmut_1; 1 untested (no test runs the mutated function)
verdict: refuse
$ echo $?
1
halter HEAD --baseline HEAD prints verdict: nothing to audit and exits
3; halter no-such-rev prints error: cannot check out revision 'no-such-rev': ... on stderr and exits 2.
Developing
python3 -m venv .venv && .venv/bin/pip install -e . pytest
PATH="$PWD/.venv/bin:$PATH" .venv/bin/python -m pytest -q
The gates shell out to ruff, coverage and mutmut by name, hence the
PATH. The test suite stubs mutmut and never runs real mutants.
License
Copyright (C) 2026 Eliza H
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public
License along with this program. If not, see
<https://www.gnu.org/licenses/>.
Release files for halter 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| halter-0.1.0.tar.gz | 94.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| halter-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 185.8 kB
Release files / halter-0.1.0.tar.gz
| Download URL | halter-0.1.0.tar.gz |
|---|---|
| Size | 94.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
09e9a66578595a0e85fc43ddb70dd88f323810c0b38399a862dbe7c39f858a6f
|
|
BLAKE2b-256 checksum How to use checksums |
1a37931d59dc3a2579cfc182a50d277f1f9a7b5d686a266b7d69464d81c97bf4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / halter-0.1.0-py3-none-any.whl
| Download URL | halter-0.1.0-py3-none-any.whl |
|---|---|
| Size | 91.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0dcb0c238128ed9b0e8ef650834461d6c803d4dc14a2011c56b36631333b1a3d
|
|
BLAKE2b-256 checksum How to use checksums |
899f8597dc4f4b76e5c5d1807123a9ae0d246698a63a917c479a64539a935f07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|