Zero-config Python quality interlocks: lint, format, typecheck, test, coverage, acceptance, audit, deps, architecture, complexity, CRAP, mutation — all behind `interlocks <task>` (or `il <task>`).
Project description
interlocks
Adopt one Python quality loop across a repository or an organization:
pipx install interlocks # or: uv tool install interlocks
cd your-python-project
interlocks doctor # readiness, detected config, blockers, next steps
interlocks check # local edit loop
interlocks ci # CI parity
interlocks bundles ruff, basedpyright, pytest, pytest-bdd, coverage, mutmut, deptry, import-linter, pip-audit, and lizard behind one CLI. New repositories can start with auto-detected paths and bundled tool defaults; mature repositories can opt into named presets or explicit [tool.interlocks] thresholds when they need stronger gates.
First-Run Adoption Loop
1. Install
pipx install interlocks
# or
uv tool install interlocks
Every underlying tool ships with the CLI. No per-project dev dependency list is required just to try the standard loop.
2. Diagnose Readiness
cd your-python-project
interlocks doctor
doctor is the safe first command. It performs static local inspection only: nearest pyproject.toml, detected source/test/features paths, runner, invoker, active preset, resolved gate values, PATH visibility, blockers, warnings, and shortest next steps. It does not run tests, typecheck, coverage, mutation, dependency audit, or network checks.
If the repository is ready, doctor points you at interlocks check and CI wiring. If it is blocked, it prioritizes the minimum setup fixes first, such as interlocks init, missing paths, unreadable config, unsupported presets, or missing runnable tool resolution.
3. Run Local Checks
interlocks check
check runs the local edit loop: fix, format, typecheck, tests, optional acceptance tests, advisory dependency hygiene, cached CRAP feedback when fresh coverage exists, and the suppressions report. It is the command to run after edits before pushing.
4. Wire CI
The direct CI command is:
interlocks ci
For GitHub Actions, copy this workflow:
name: interlocks
on:
pull_request:
push:
branches: [main]
jobs:
interlocks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: 0xjgv/interlocks@v1
The reusable action installs interlocks, runs interlocks ci, and writes a concise GITHUB_STEP_SUMMARY when GitHub provides the summary file. The action does not duplicate lint, typecheck, coverage, CRAP, dependency, architecture, acceptance, or mutation logic; the CLI remains the source of truth.
Why This Matters for AI-Authored Code
When agents write most of the PRs, human review stops being the quality floor. Deterministic gates become the part that scales:
crapcatches complex code the agent shipped without matching tests.mutationcatches tests the agent wrote that do not actually test the code.coverageand complexity trends feed drift telemetry — signal that agent output is regressing before users notice.trustcombines those into one actionable report, so reviewers (human or LLM-based) have a stable ground truth.
interlocks is complementary to LLM-based reviewers such as CodeRabbit, Greptile, or Diamond. They catch style, design, and intent. interlocks catches what is machine-verifiable: complexity, coverage, mutation survival, dependency hygiene, architectural drift. Runs in seconds, same command locally and in CI.
Adoption Presets
Presets are optional defaults under [tool.interlocks] or ~/.config/interlocks/config.toml. Explicit values in the same layer override preset defaults, so you can manually tune thresholds in pyproject.toml after choosing a preset.
[tool.interlocks]
preset = "baseline" # "baseline" | "strict" | "legacy"
baselinelowers first-adoption friction: advisory CRAP, relaxed thresholds, mutation off in CI, acceptance off incheck.strictis for mature repositories: stronger thresholds, blocking CRAP and mutation, mutation in CI, acceptance incheck.legacyis for ratcheting existing repositories: very permissive thresholds, advisory gates, mutation off in CI.
agent-safe is intentionally unsupported. If configured, interlocks doctor reports it as an unsupported preset instead of resolving agent-specific defaults.
Configuration
Nothing is required. interlocks walks up from CWD to the nearest pyproject.toml and auto-detects:
- project root: first directory with
pyproject.toml - test runner: pytest if pytest config/deps/imports are present, otherwise unittest
- test dir: first existing of
tests/,test/,src/tests/ - source dir: build-backend declarations, package layouts,
src/<pkg>, top-level packages, or the project root - test invoker:
uv runwhenuv.lockexists, elsepython -m - features dir: first existing of
tests/features/,features/,<test_dir>/features/
Override anything via [tool.interlocks] in pyproject.toml:
[tool.interlocks]
preset = "baseline"
# Paths / runners
src_dir = "mypkg"
test_dir = "tests"
test_runner = "pytest" # "pytest" | "unittest"
test_invoker = "python" # "python" | "uv"
pytest_args = ["-q", "-x"]
# Thresholds
coverage_min = 80
crap_max = 30.0
complexity_max_ccn = 15
complexity_max_args = 7
complexity_max_loc = 100
mutation_min_coverage = 70.0
mutation_max_runtime = 600
mutation_min_score = 80.0
# Gate behavior
enforce_crap = true
run_mutation_in_ci = false
enforce_mutation = false
mutation_ci_mode = "off" # "off" | "incremental" | "full"
mutation_since_ref = "origin/main"
# Acceptance
acceptance_runner = "pytest-bdd" # "pytest-bdd" | "behave" | "off"
features_dir = "tests/features"
run_acceptance_in_check = false
Precedence, lowest to highest:
- Bundled dataclass defaults.
- User-global preset defaults from
~/.config/interlocks/config.toml. - User-global explicit values.
- Project preset defaults from
[tool.interlocks]. - Project explicit values.
- CLI flags inside tasks, such as
--min=,--max=,--max-runtime=,--min-score=, and--min-coverage=.
Example user-global config:
# ~/.config/interlocks/config.toml
preset = "baseline"
coverage_min = 85
Run interlocks help to see the active preset and resolved values.
Run interlocks presets to see preset options, their main thresholds, and copyable config.
Run interlocks presets set baseline to set a project preset from the CLI.
Stages
| Stage | When | What runs |
|---|---|---|
interlocks check |
Local edit loop | fix -> format -> parallel(typecheck, test, acceptance when opted in) -> deps advisory -> cached CRAP advisory or refresh hint -> suppressions |
interlocks pre-commit |
Git pre-commit hook | fix/format staged Python files, re-stage, typecheck, tests when source changed |
interlocks ci |
Pull requests and protected branches | format-check, lint, complexity, deps, typecheck, coverage, arch, acceptance -> CRAP -> optional mutation |
interlocks nightly |
Scheduled jobs | coverage -> mutation, always blocking on mutation_min_score |
interlocks post-edit |
Editor/agent hook interface | advisory ruff fix + format on changed Python files |
interlocks setup-hooks |
Convenience installer | writes hooks that call interlocks pre-commit and interlocks post-edit |
interlocks clean |
Local cleanup | removes caches, build artifacts, coverage output, mutation state, and __pycache__/ |
interlocks pre-commit and interlocks post-edit are the stable hook interfaces. interlocks setup-hooks is a convenience command that installs a git pre-commit hook and merges a Claude Code Stop hook; rerunning it is idempotent.
Tasks Reference
Correctness:
fix/format: ruff lint-fix and format, mutating files.lint/format-check: read-only equivalents for CI.typecheck: basedpyright.test: pytest or unittest, auto-detected.acceptance: Gherkin via pytest-bdd or behave.
Hygiene:
audit: pip-audit CVE scan.deps: deptry unused, missing, and transitive import checks.arch: import-linter contracts; default contract forbids source importing tests.
Advanced gates:
coverage --min=N: coverage.py with fail-under.--min=Noverridescoverage_min.crap --max=N [--changed-only]: CRAP complexity x coverage gate. Blocking depends onenforce_crap.mutation --max-runtime=N [--min-coverage=N] [--min-score=N] [--changed-only]: mutmut. Advisory unlessenforce_mutation = trueor--min-score=is passed.trust [--refresh] [--no-trend]: actionable trust report combining coverage, CRAP, mutation, suspicious-test AST inspection, recent git diff, and next actions.--refreshruns coverage first with--min=0.
Scaffolding:
init: writes a greenfieldpyproject.toml,tests/__init__.py, andtests/test_smoke.py; refuses to overwrite.init-acceptance: writes a working pytest-bdd example undertests/features/andtests/step_defs/; refuses to overwrite.
Utility:
doctor: adoption diagnostic. Exempt from thepyproject.tomlpreflight gate.help: command list plus detected paths, active preset, and thresholds.presets: show preset options, current values, copyable config, and set a project preset withinterlocks presets set <preset>.version: print the installed interlocks version.
Acceptance Tests
Drop .feature files under tests/features/ and step definitions under tests/step_defs/; interlocks acceptance runs them via pytest-bdd and shares coverage with test. Or run interlocks init-acceptance for a working example.
Runner detection order:
acceptance_runnerin config ("pytest-bdd","behave", or"off").- Behave layout:
features_dir/steps/plusfeatures_dir/environment.py. behavedeclared as a dependency but notpytest-bdd.- Default to pytest-bdd.
Acceptance always runs in interlocks ci when a features directory exists. It is opt-in for interlocks check via run_acceptance_in_check = true.
Bundled Tool Defaults
When the target project has no config for a given tool, interlocks injects its bundled default.
| File | Consumed by | Detected via | Injected flag |
|---|---|---|---|
ruff.toml |
fix, format, lint, format-check |
[tool.ruff], ruff.toml, .ruff.toml |
--config |
pyrightconfig.json |
typecheck |
[tool.basedpyright], pyrightconfig.{json,toml} |
--project |
coveragerc |
coverage |
[tool.coverage.*], .coveragerc |
--rcfile= |
importlinter_template.ini |
arch |
[tool.importlinter], .importlinter, setup.cfg |
formatted tempfile plus --config |
bdd_example.feature |
init-acceptance |
none | direct copy |
bdd_test_example.py |
init-acceptance |
none | direct copy |
bdd_conftest.py |
init-acceptance |
none | direct copy |
scaffold_pyproject.toml |
init |
none | read plus {project_name} substitution |
scaffold_test_example.py |
init |
none | direct copy |
interlocks deps and interlocks mutation ship no bundled fallback: deptry applies its built-ins, and mutmut reads the project's pyproject.toml.
Hooks
Use the stable hook interfaces directly when integrating with your own hook manager:
interlocks pre-commit
interlocks post-edit
Use the convenience installer when you want interlocks to write the common hooks:
interlocks setup-hooks
It installs:
.git/hooks/pre-commit: runsinterlocks pre-commit. Skip withgit commit --no-verifywhen necessary..claude/settings.jsonStop hook: runsinterlocks post-editafter Claude Code sessions and preserves existing Stop hooks.
Both reference the Python that installed interlocks, so reinstall hooks after switching install locations or interpreters.
Maintainer Release Process
Package identity:
- PyPI distribution:
interlocks - import package:
interlocks - CLI command:
interlocks
Trusted Publishing setup:
- PyPI: owner
0xjgv, repointerlocks, workflowrelease.yml, environmentpypi - TestPyPI: owner
0xjgv, repointerlocks, workflowrelease.yml, environmenttestpypi - No PyPI API token required.
Release checklist:
- Set
pyproject.tomlversion to the next release. - Set
interlocks/__init__.py__version__to the same release. - Update
CHANGELOG.mdfor the release. - Run
uv run interlocks ci. - Run
uv build. - Trigger
releasemanually to publish to TestPyPI. - Create matching
vX.Y.Ztag. - Push tag.
- Confirm PyPI release, GitHub release assets, and attestations.
See CHANGELOG.md for release history.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file interlocks-0.1.1.tar.gz.
File metadata
- Download URL: interlocks-0.1.1.tar.gz
- Upload date:
- Size: 54.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f66fa5a4a35fd8262ad854ac2c3e3c421111a76753616daeec077f3abc54c7ef
|
|
| MD5 |
c0a48f1c89b7380d03023ef7342b3f9f
|
|
| BLAKE2b-256 |
ed32b19034bc93969d535d9e1577bbac48d412b236f8fb25fee06df1efba5719
|
Provenance
The following attestation bundles were made for interlocks-0.1.1.tar.gz:
Publisher:
release.yml on 0xjgv/interlocks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlocks-0.1.1.tar.gz -
Subject digest:
f66fa5a4a35fd8262ad854ac2c3e3c421111a76753616daeec077f3abc54c7ef - Sigstore transparency entry: 1393037380
- Sigstore integration time:
-
Permalink:
0xjgv/interlocks@a8694e047bbb640951217ddef40264e7538e5c90 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/0xjgv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a8694e047bbb640951217ddef40264e7538e5c90 -
Trigger Event:
push
-
Statement type:
File details
Details for the file interlocks-0.1.1-py3-none-any.whl.
File metadata
- Download URL: interlocks-0.1.1-py3-none-any.whl
- Upload date:
- Size: 71.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
771dcc30e01adc27764cc764c8970f642185009841736e1f4b18dc99cebb8906
|
|
| MD5 |
b6d9d8fbf7aef4a4ca81f7ef7134ba45
|
|
| BLAKE2b-256 |
120666dc2f36636947b59efd91669547cd92dc442f20274d5596cf1a52748a38
|
Provenance
The following attestation bundles were made for interlocks-0.1.1-py3-none-any.whl:
Publisher:
release.yml on 0xjgv/interlocks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlocks-0.1.1-py3-none-any.whl -
Subject digest:
771dcc30e01adc27764cc764c8970f642185009841736e1f4b18dc99cebb8906 - Sigstore transparency entry: 1393037412
- Sigstore integration time:
-
Permalink:
0xjgv/interlocks@a8694e047bbb640951217ddef40264e7538e5c90 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/0xjgv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a8694e047bbb640951217ddef40264e7538e5c90 -
Trigger Event:
push
-
Statement type: