Review only the Python code you changed before pushing
Local Python code reviewer that checks only the files your next commit touches, no daemon or network.
0.3.4Add to Favorites
Why it matters
Avouch helps developers maintain code quality by reviewing only the Python files touched in their next commit, running locally without servers or daemons to catch structural problems against configurable limits before git push.
Outcomes
What it gets done
Parse changed Python files using AST to detect structural problems like bare excepts and excessive parameters
Generate diff-only reports that exclude legacy code you didn't write
Baseline existing findings so only new violations in your changes are flagged
Export findings in JSON, SARIF, or GitHub Actions format for CI/CD integration
Source
Get it from source
Spark does not host a copy of it.
Open sourceReports
Agent outcome reports
No reports yet
Overview
Avouch
Avouch is a local Python code reviewer that uses git and the ast module to check only the files a commit changes against configurable structural limits, with no daemon or network call. Use it right before a git push, in a pre-commit hook on staged changes, or in CI via --all-files, when review feedback should be scoped to what actually changed.
What it does
Avouch is a code reviewer for Python that only looks at the files your next commit touches: it asks git for that list, parses each changed .py file with the standard ast module, and reports structural problems against limits set in avouch.toml. There is no daemon and no network call - three git calls plus the standard library - so it runs in seconds right before git push. Findings are limited to functions and classes whose source span overlaps an added or changed line, so a whole-repo scan never buries a few real findings from your diff under legacy code you didn't write. It checks 20 rules covering things like bare except: clauses, too many parameters, excessive nesting, mutable default arguments, and McCabe complexity, and reports them in a compiler-style output with file:line, rule ID, and message.
When to use - and when NOT to
Use it locally right before git push, staged via --staged in a pre-commit hook, or in CI via --all-files on a fresh checkout (the default diff-only mode finds nothing to review against a clean checkout, since there is no diff yet). It fits teams that want review feedback scoped strictly to what changed, rather than an AI reviewer that spends half its report on legacy code and queues diffs behind everyone else's, which the project's author cites as the reason it exists. It requires Python 3.10+ (for ast.Match and tomllib) and git on PATH, and takes no path argument at all - the review set is always Git-defined, so it is not a tool for reviewing arbitrary files outside a git-tracked project (the --not-git flag exists for that case but walks the working directory instead of using git).
Inputs and outputs
pip install avouch
avouch
avouch init measures a repo's current maxima and writes avouch.toml with a one-unit headroom so the first run passes clean; avouch baseline snapshots current findings to .avouch/baseline.json (fingerprinted by rule, file, name, and line) so later runs only surface new findings, while --no-baseline shows everything. Review scopes are mutually exclusive: default (changed vs HEAD plus untracked), --staged, --changed (diff view with findings pinned inline), --all-files (every eligible file via git ls-files, for CI), and --not-git (walk the working directory without git). Output formats include the default human report, --json (a deterministic schema for gating CI), --format github (inline PR annotations), and --format sarif (for GitHub code scanning). Exit codes are 0 (clean), 1 (findings), 2 (error).
Integrations
A GitHub Actions workflow runs avouch --all-files --format github on pull requests to annotate the diff inline, or --format sarif piped into github/codeql-action/upload-sarif; a pre-commit hook entry runs avouch --staged on every commit. --fix applies safe automatic fixes for two rules today (replacing bare except: with except Exception:, and converting mutable default arguments to a None sentinel), and --watch polls for file changes and re-runs automatically. Licensed under MIT.
Who it's for
Python developers and teams who want a fast, local, diff-scoped structural code review before every push or commit, without the noise of legacy findings or the cost and latency of a hosted AI reviewer.
Source README
avouch
Review the Python you changed, not the Python you inherited.
Avouch is a code reviewer for Python that only looks at the files your next commit touches. It asks git for that list, parses each changed .py with the standard ast module, and reports structural problems against limits you set in avouch.toml.
No daemon, no network, nothing to install alongside it. Run it seconds before git push:
pip install avouch
avouch
Why
Paid AI reviewers wasted half their report on legacy I never wrote and queued my diffs behind everyone else's. I wanted a local, diff-only check that runs before git push, costs nothing, and shows only your changes - so I built Avouch.
- Diff-only: Review set is
git diff HEAD+ untracked, not whole repo. - Local: Three
gitcalls +ast, no server. - Yours: Baseline hides legacy; every finding is attributable to your diff.
Install
Requires Python 3.10+ (ast.Match, tomllib) and git on PATH.
pip install avouch
or from source:
git clone https://github.com/mukundzha/avouch.git
cd avouch
pip install -e .
Both register avouch (avouch.cli:main).
Quick start
Change a file, run it, read the compiler-style report:
$ avouch
AVOUCH · 2 FILES · 4 WARN
────────────────────────────────────────────────
bad.py:1: SCR002: Bare except detected.
│
1 │ def connect(host, port, ...):
│ ^^^^^^^ SCR002
2 │ try:
BY RULE SCR002 Bare except 1
SCR014 Too many parameters 1
PASSED ✓ src/util.py
$ avouch
All clean.
file:line + rule + message, dimmed context, caret under name, BY RULE tally, capped PASSED grid. Duplicate (component, rule) collapses per file.
Exit codes: 0 clean, 1 findings, 2 error. Colors only on TTY; piped is plain.
Usage
Avouch takes no path argument - review set is Git-defined.
avouch --help # all flags
avouch --docs # full docs, no review (TTY browser or piped)
avouch --version # avouch 0.3.4
avouch --watch # watch and re-run on change
Review scopes (mutually exclusive)
Default - Changed vs HEAD + untracked. Best before git push.
Findings are limited to functions and classes whose source span overlaps an added or changed line. File-level findings still apply to the changed file. --all-files and --not-git review complete files.
--list-changed - Print each changed file path, one per line, and exit. Useful for scripts and tooling.
avouch --list-changed
# src/app.py
# tests/test_review.py
--display FILE - Display a file with syntax highlighting, line numbers, and an interactive pager. Press q to quit.
avouch --display src/avouch/cli.py
--changed - Diff view with +/- hunks and findings pinned inline. Local PR sketch.
avouch --changed
# [CHANGED FILES]
# bad.py +2 -1
# ────────────────────
# + def f(a,b,c,d,e,f):
# ^ SCR014
--staged - Only staged (git add). Pre-commit uses this.
avouch --staged
--all-files - Every eligible .py via git ls-files. Use in CI where checkout is clean.
avouch --all-files
avouch --all-files --json
--not-git - Walk CWD for .py without Git. Skips .venv, __pycache__, dist, build, node_modules.
avouch --not-git
avouch --not-git --format github
Without a repo or on clean checkout:
error: no Git repository found
hint: use --not-git
error: nothing to review
hint: nothing changed vs HEAD; use --all-files
Output formats (mutually exclusive)
Default - Human report.
--json - Deterministic {"version":1,"tool":"avouch","violations":[...],"summary":{...}} with rule/severity/message/file/name/kind/line (null for file-level). Same exit codes, gate CI.
avouch --json | jq .summary.total
avouch --all-files --json > report.json
--format github - GitHub workflow commands for inline annotations.
avouch --all-files --format github
# ::warning file=bad.py,line=1,col=5,title=SCR014::Too many parameters...
# ::error for ERROR severity
Use in Actions to annotate PR diff.
--format sarif - SARIF 2.1.0 for code-scanning. Includes tool.driver.rules (20 rules) and physicalLocation spans.
avouch --all-files --format sarif > results.sarif
# upload with github/codeql-action/upload-sarif
--json/--format bypass --quiet and respect baseline filtering.
Diagnostics
--quiet - Only exit code (errors still emit, --json/--format still emit).
--verbose - Step-by-step to stderr: config: <resolved-path>, review set:, analyzing, suppressed, findings:.
--fix - Apply safe fixes before reviewing. Currently replaces bare except: clauses with except Exception: and converts mutable literal/constructor defaults to None sentinel initialization. Combine with any review scope, including --not-git.
--watch - Watch Python files and re-run on change (polling, Ctrl+C to quit). Polls 0.5s (AVOUCH_WATCH_INTERVAL=0.5), snapshots mtime+size of reviewable files + avouch.toml + .avouch/baseline.json; clears screen on TTY and prints ⟳ HH:MM:SS — change detected: path. Works with --all-files / --not-git / --staged / --changed; incompatible with --json / --format / --list-changed / --display. Premium TUI uses full-width block header (┌─ avouch 0.3.4 ─┐).
--ignore-path - Repeatable, component-wise (tests skips tests/ not tests.py). Combined with ignore_paths in avouch.toml.
--select RULES - Review only the comma-separated rule IDs. Repeatable; --ignore RULES is applied afterward.
--ignore RULES - Skip the comma-separated rule IDs for this run without changing avouch.toml.
--docs - Full docs (avouch --docs) - workflow, 20 rules, limits, examples. Works outside repo.
Init & Baseline
avouch init
Measure repo maxima and write avouch.toml with measured+1 headroom so first run is All clean. Rerun recomputes.
avouch init
avouch init --dry-run # preview without writing
Example generated avouch.toml:
ignore_paths = ["tests"]
[limits]
max_parameters = 6
max_file_lines = 450
avouch baseline
Snapshot current findings to .avouch/baseline.json (rule+file+name+line fingerprint; moving a function re-flags). Commit it. Next runs show only new findings.
avouch baseline # snapshot full review
avouch # only new
avouch --no-baseline # show all
avouch baseline # idempotent, recomputes
--verbose shows suppressed N finding(s) by baseline, BY RULE shows (+N suppressed). Malformed/wrong version → error: invalid baseline: exit 2. No file → no suppression. Composes with init.
avouch rule [ID]
Per-rule help from single registry (same source as --docs).
avouch rule # list all: SCR001 async function without await ...
avouch rule SCR002 # show one: name, description, scope, config, Bad/Good, severity
avouch rule SCR014 # too many parameters
Bare avouch rule lists 20 IDs; unknown → error: unknown rule 'FOO' exit 2; works outside repo.
Configuration
avouch.toml is discovered by walking upward from CWD to filesystem root (so tests/ uses repo root), optional/partial, merged over defaults. Malformed/invalid → error: invalid avouch.toml configuration exit 2.
[limits] # thresholds per rule
[rules] # on/off per rule (bool, default true)
ignore_paths = ["tests", "migrations"]
Config file
avouch.tomlin upward-found directory, plain TOML; onlyAVOUCH_FONTenv for terminal font.- Missing/empty → silent defaults, no warning.
Changing a threshold
[limits]
max_parameters = 8 # allow 8 instead of 5
max_file_lines = 2500
Only named keys change.
Disabling a rule
[rules]
nested_function = false # stop SCR015
One-line [rules] is valid.
Rule toggles (all true)
| Key | Default | Rule |
|---|---|---|
async_without_await |
true |
SCR001 |
bare_except |
true |
SCR002 |
max_boolean_conditions |
true |
SCR003 |
detect_duplicateb |
true |
SCR004 |
max_large_comprehensions |
true |
SCR005 |
empty_except |
true |
SCR006 |
max_if_else_chain |
true |
SCR007 |
max_lambda_nodes |
true |
SCR008 |
max_local_variables |
true |
SCR009 |
max_class_lines |
true |
SCR010 |
max_file_lines |
true |
SCR011 |
max_function_lines |
true |
SCR012 |
max_nesting |
true |
SCR013 |
max_parameters |
true |
SCR014 |
nested_function |
true |
SCR015 |
max_return_statements |
true |
SCR016 |
mutable_default_args |
true |
SCR017 |
max_complexity |
true |
complexity |
Limits (all tunable)
| Key | Default | Rule | Meaning |
|---|---|---|---|
max_parameters |
5 | SCR014 | Max params |
max_nesting |
5 | SCR013 | Max depth |
max_function_lines |
300 | SCR012 | Function span |
max_class_lines |
200 | SCR010 | Class span |
max_file_lines |
1000 | SCR011 | File lines |
max_complexity |
40 | - | McCabe |
max_boolean_conditions |
5 | SCR003 | Operands in chain |
max_if_chain |
5 | SCR007 | If/elif length |
max_local_variables |
30 | SCR009 | Assigned names |
max_return_statements |
6 | SCR016 | Returns |
max_lambda_nodes |
10 | SCR008 | Lambda nodes |
max_large_comprehensions |
40 | SCR005 | Comprehension nodes |
Missing limit falls back to hardcoded default - partial never disables.
Ignoring paths
Component-wise: tests skips tests/ but not tests.py; "." skips repo.
avouch --ignore-path PATH(repeatable) orignore_paths = ["tests"]inavouch.toml(must be list).
Combined, de-duplicated, string-based (is_ignored.py), no FS access.
Verifying & errors
avouch --verbose shows config: <resolved-path>, N ignore path(s) or defaults (no avouch.toml).
- Malformed/invalid:
error: invalid avouch.toml configuration: limits.max_parameters must be a positive integer; got 'eight'exit 2. - Unknown keys silently ignored.
--ignore-pathappends; no flag for[limits]/[rules]; applies to all modes; severity fixedWARNING/ERROR.
Example (this repo)
ignore_paths = ["tests"]
[limits]
max_parameters = 5
max_nesting = 5
max_function_lines = 300
max_class_lines = 200
max_file_lines = 1000
max_complexity = 40
max_boolean_conditions = 5
max_if_chain = 5
max_local_variables = 30
max_return_statements = 6
max_lambda_nodes = 10
max_large_comprehensions = 40
[rules]
max_parameters = true
# ... all true
GitHub Actions
Use --all-files in CI (fresh checkout has no diff):
name: Avouch
on: [pull_request, push]
jobs:
avouch:
runs-on: ubuntu-latest
permissions: {contents: read}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with: {python-version: "3.12"}
- run: pip install avouch
- run: avouch --all-files --format github # annotations
# or SARIF:
# - run: avouch --all-files --format sarif > results.sarif
# - uses: github/codeql-action/upload-sarif@v3
# with: {sarif_file: results.sarif}
checkout provides PR code; avouch --all-files --format github annotates diff; permissions: contents: read is enough. Pin avouch==0.3.4.
Why --all-files? Default is changed vs HEAD - fresh checkout is empty (nothing to review). --changed/--staged only make sense locally.
Exit codes 0 clean, 1 findings, 2 error - Actions fails non-zero, so --all-files --format github fails on findings. Fix or ignore_paths.
This repo ships .github/workflows/avouch.yml - enable in Actions, it does pip install -e . and runs --all-files --format github on PR.
Pre-commit
Add to pre-commit so every commit reviews staged changes:
repos: [{repo: https://github.com/mukundzha/avouch, rev: v0.3.2, hooks: [{id: avouch}]}]
# .pre-commit-hooks.yaml: entry avouch --staged, pass_filenames: false
Baselined findings don't fail the hook.
Baseline
avouch baseline # snapshot to .avouch/baseline.json
avouch # only new findings
avouch --no-baseline # show all
Fingerprint rule+file+name+line (moving re-flags). Commit .avouch/baseline.json. --verbose shows suppressed count, BY RULE shows (+N suppressed). Malformed → exit 2.
Other CI
Any CI: pip install avouch → avouch --all-files --json → check exit code.
Configuration
avouch.toml discovered by walking upward from CWD to FS root (so tests/ uses root config), optional/partial, merged over defaults. Invalid limits (must be positive int), rules (bool), ignore_paths (list[str]) → error: invalid avouch.toml configuration exit 2.
[limits]
max_parameters = 8
[rules]
nested_function = false
ignore_paths = ["tests"]
Limits and toggles are all true by default; see avouch --docs for full table. --ignore-path appends to ignore_paths. No env vars.
How it works
cli.py only orchestrates. Pipeline: load_config → git review set → analyzer.analyze_file (ast + walk cache + rules) → baseline.filter → report (or --json/--changed/--quiet). --docs/--version short-circuit.
See avouch --docs for mermaid diagrams and full pipeline.
Layout
avouch.toml .avouch/baseline.json
src/avouch/
cli.py # entry, only orchestrates
baseline.py # snapshot + suppression
git.py # review set
analyzer.py # ast walk, dispatch
report.py # terminal / json / github / sarif / diff
rules/ # one analyze(node,limits) per rule
complexity.py max_nesting.py max_parameters.py ...
utility/
walk.py docs.py is_generated.py is_ignored.py measure.py
config/
default.py loader.py # DEFAULT_LIMITS, load_config
tests/
test_git.py # 74 tests + real git repo
test_init.py # init/baseline
Adding a rule
One file per rule with analyze(node, limits) -> list[issue]:
{"rule": "SCR017", "severity": "WARNING",
"message": "Description (value/limit). Remediation."}
Add toggle in DEFAULT_RULES (+limit in DEFAULT_LIMITS if needed), wire in analyzer.py with guard, add violation + boundary tests. Renderer handles any (severity,message).
Example: mutable_default_args - flags def f(x=[]), suggests None.
Testing
pip install -e . && python -m pytest
86 tests, <1s, no network. Covers git helpers, config validation (upward, limits must be positive int, rules must be boolean, ignore_paths list), nesting/complexity/boolean metrics, rule boundaries, unreadable/syntax errors, report/diff, avouch rule, github/sarif golden, --docs outside repo, real temp git repo. Only subprocess.run mocked.
FAQ
Changed files only? Whole-repo buries your few findings under legacy noise; diff keeps it relevant to next push.
Why git diff HEAD not git diff? Plain diff is unstaged only; HEAD is staged+unstaged plus untracked - nothing missed.
Why AST not regex? Regex can't count cross-line parens, nesting, or def vs call; AST answers exactly.
Exit codes? 0 clean, 1 findings, 2 error. Reviews, doesn't gate - CI can still react.
Network/daemon? No - three git calls + stdlib; runtime ∝ diff, not repo.
Baseline vs ignore_paths? ignore_paths hides files forever; baseline hides findings at snapshot time but re-flags if moved - use baseline for legacy debt.
init vs baseline? init sets limits to measured+1 so first run is All clean; baseline snapshots findings. Compose both for greenfield.
rule vs --docs? Same registry; --docs shows all, avouch rule SCR002 shows one.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.