Zero-config Python quality interlock: lint, format, typecheck, test, coverage, acceptance, audit, deps, architecture, complexity, CRAP, mutation — all behind `interlock <task>` (or `il <task>`).
Project description
interlock
Adopt one Python quality loop across a repository or an organization:
pipx install interlocks # or: uv tool install interlocks
cd your-python-project
interlock doctor # readiness, detected config, blockers, next steps
interlock check # local edit loop
interlock ci # CI parity
interlock 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.interlock] 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
interlock 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 interlock check and CI wiring. If it is blocked, it prioritizes the minimum setup fixes first, such as interlock init, missing paths, unreadable config, unsupported presets, or missing runnable tool resolution.
3. Run Local Checks
interlock 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:
interlock ci
For GitHub Actions, copy this workflow:
name: interlock
on:
pull_request:
push:
branches: [main]
jobs:
interlock:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: 0xjgv/interlock@v1
The reusable action installs interlock, runs interlock 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.
interlock is complementary to LLM-based reviewers such as CodeRabbit, Greptile, or Diamond. They catch style, design, and intent. interlock 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.interlock] or ~/.config/interlock/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.interlock]
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, interlock doctor reports it as an unsupported preset instead of resolving agent-specific defaults.
Configuration
Nothing is required. interlock 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.interlock] in pyproject.toml:
[tool.interlock]
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/interlock/config.toml. - User-global explicit values.
- Project preset defaults from
[tool.interlock]. - Project explicit values.
- CLI flags inside tasks, such as
--min=,--max=,--max-runtime=,--min-score=, and--min-coverage=.
Example user-global config:
# ~/.config/interlock/config.toml
preset = "baseline"
coverage_min = 85
Run interlock help to see the active preset and resolved values.
Run interlock presets to see preset options, their main thresholds, and copyable config.
Run interlock presets set baseline to set a project preset from the CLI.
Stages
| Stage | When | What runs |
|---|---|---|
interlock check |
Local edit loop | fix -> format -> parallel(typecheck, test, acceptance when opted in) -> deps advisory -> cached CRAP advisory or refresh hint -> suppressions |
interlock pre-commit |
Git pre-commit hook | fix/format staged Python files, re-stage, typecheck, tests when source changed |
interlock ci |
Pull requests and protected branches | format-check, lint, complexity, deps, typecheck, coverage, arch, acceptance -> CRAP -> optional mutation |
interlock nightly |
Scheduled jobs | coverage -> mutation, always blocking on mutation_min_score |
interlock post-edit |
Editor/agent hook interface | advisory ruff fix + format on changed Python files |
interlock setup-hooks |
Convenience installer | writes hooks that call interlock pre-commit and interlock post-edit |
interlock clean |
Local cleanup | removes caches, build artifacts, coverage output, mutation state, and __pycache__/ |
interlock pre-commit and interlock post-edit are the stable hook interfaces. interlock 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 withinterlock presets set <preset>.version: print the installed interlock version.
Acceptance Tests
Drop .feature files under tests/features/ and step definitions under tests/step_defs/; interlock acceptance runs them via pytest-bdd and shares coverage with test. Or run interlock 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 interlock ci when a features directory exists. It is opt-in for interlock check via run_acceptance_in_check = true.
Bundled Tool Defaults
When the target project has no config for a given tool, interlock 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 |
interlock deps and interlock 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:
interlock pre-commit
interlock post-edit
Use the convenience installer when you want interlock to write the common hooks:
interlock setup-hooks
It installs:
.git/hooks/pre-commit: runsinterlock pre-commit. Skip withgit commit --no-verifywhen necessary..claude/settings.jsonStop hook: runsinterlock post-editafter Claude Code sessions and preserves existing Stop hooks.
Both reference the Python that installed interlock, so reinstall hooks after switching install locations or interpreters.
Maintainer Release Process
Package identity:
- PyPI distribution:
interlock - import package:
interlock - CLI command:
interlock
Trusted Publishing setup:
- PyPI: owner
0xjgv, repointerlock, workflowrelease.yml, environmentpypi - TestPyPI: owner
0xjgv, repointerlock, workflowrelease.yml, environmenttestpypi - No PyPI API token required.
Release checklist:
- Set
pyproject.tomlversion to0.1.0. - Set
interlock/__init__.py__version__to0.1.0. - Update
CHANGELOG.mdfor0.1.0. - Run
uv run interlock ci. - Run
uv build. - Trigger
releasemanually to publish to TestPyPI. - Create matching tag
v0.1.0. - 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.0.tar.gz.
File metadata
- Download URL: interlocks-0.1.0.tar.gz
- Upload date:
- Size: 54.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bc7ab82afeb526db113b8b454a3f6bedbbcd3fbd2374f9c572aa12b7d036a15
|
|
| MD5 |
69f49b0d99ccf08629f53130424354e2
|
|
| BLAKE2b-256 |
9c2838c02a8c865b8d6ae94e98d40d6e7e537e86a1b4d1cdc011dc499811b1ee
|
Provenance
The following attestation bundles were made for interlocks-0.1.0.tar.gz:
Publisher:
release.yml on 0xjgv/interlock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlocks-0.1.0.tar.gz -
Subject digest:
7bc7ab82afeb526db113b8b454a3f6bedbbcd3fbd2374f9c572aa12b7d036a15 - Sigstore transparency entry: 1384203737
- Sigstore integration time:
-
Permalink:
0xjgv/interlock@608335a00670d0834d2a40659e3a3de7ff1f9ee1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/0xjgv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@608335a00670d0834d2a40659e3a3de7ff1f9ee1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file interlocks-0.1.0-py3-none-any.whl.
File metadata
- Download URL: interlocks-0.1.0-py3-none-any.whl
- Upload date:
- Size: 70.8 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 |
b94e645cfa97e17046ca0714635d8fdd5131cf36e8a096281b1f26161be34208
|
|
| MD5 |
86d602bbc3e2c2c9675039e6ea4b6482
|
|
| BLAKE2b-256 |
2f84863e8a7eafac5976c9b6364b7d1fef67cf5c4d8dae4736c6b62ec32d6f0e
|
Provenance
The following attestation bundles were made for interlocks-0.1.0-py3-none-any.whl:
Publisher:
release.yml on 0xjgv/interlock
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
interlocks-0.1.0-py3-none-any.whl -
Subject digest:
b94e645cfa97e17046ca0714635d8fdd5131cf36e8a096281b1f26161be34208 - Sigstore transparency entry: 1384203862
- Sigstore integration time:
-
Permalink:
0xjgv/interlock@608335a00670d0834d2a40659e3a3de7ff1f9ee1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/0xjgv
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@608335a00670d0834d2a40659e3a3de7ff1f9ee1 -
Trigger Event:
push
-
Statement type: