Maintainability Agent
A deterministic, offline maintainability audit whose output is a bounded work order for an AI coding agent — a copy-paste prompt, per finding, that says fix exactly these and refactor nothing else. Chat-primary; CLI for CI. Version 1.10.1.
Languages parsed: Python, Java, C, C++, C#, Fortran (free-form and fixed-form), and the JS/TS family — each by a scanner written for it, and measured with that language's own reading of what a branch is. What that means per language.
pip install maintainability-agent # CLI + library
pip install "maintainability-agent[mcp]" # optional local MCP server for chat / IDE hosts
cp -r skills/maintainability-agent ~/.claude/skills/ # Claude Code slash command
Executive summary
Agents write code faster than anyone can review it. Not measurably worse code — this project tested that claim about itself and retracted it — just more, arriving faster than trust can accumulate. Linters and quality dashboards catch some of the resulting slop. None of them ship the one thing that actually closes the loop: a bounded prompt back to the agent, scoped to the findings, that forbids unbounded rewrites.
That prompt is the product. Everything else — the scanner, the ISO/IEC 25010-inspired 0–5 score, the analyzer pool, the semantic and economic signals — exists to aim it. Remove the prompt and what's left is a worse version of tools that already ship.
What 1.0 guarantees:
- Deterministic and local. Same tree, config, pinned analyzer versions and history in → same evidence, findings and score out. The analysis performs no network access and invokes no LLM, and this agent does not transmit your source. Third-party analyzers it may spawn (eslint, jscpd, lizard) are not network-sandboxed — this process does not police whether they phone home; install them yourself for an air-gapped run.
- One uniform rubric, readable in source, applied to every repository — so "better" and "worse" are not an argument. Calibrated against a query-selected corpus of mature open-source projects; the corpus median earns a B, and A+ is gated, not averaged.
- Honest about evidence. A score is withheld when too little was examined
to support one — a
--changed-onlydiff is not a repository grade, and a shallow clone is not an A. Every reported value names what measured it. - The complete work order is the report; chat/CLI is a bounded UI. The HTML/Markdown report carries the entire backlog with a deterministic copy-paste prompt for each item. The chat surface stays a tight summary so a host's payload cap can never truncate the prompt.
- One setup, three transports. Chat, MCP, and an interactive CLI TTY ask the same first-run questions. A surface that asks a subset is a bug.
Governing intent lives in docs/product-intent.md — that document is authoritative, and this README defers to it wherever the two differ.
Why this exists
The ratio of code-written to code-reviewed has collapsed. Unmaintainable code that used to accumulate over years can now accumulate in an afternoon: duplicated helpers, oversized files, speculative abstractions — the same slop hand-written codebases always accrued, now at machine speed.
The same speed is the way out. An agent pointed at specific, deterministic findings can fix them at the rate they appear. The loop this tool closes:
- Measure pressure points deterministically, with no LLM involved.
- Score with one uniform standard, so the verdict is not a debate.
- Emit a prompt scoped to those findings only, with explicit instructions not to refactor beyond them.
- Hand it to the agent. Review a scoped diff instead of a speculative rewrite.
Step 3 is the product; steps 1 and 2 are in service of it. Every other tool in this space stops at "here's a list of findings."
Where the model stops: the agent authors, no model runs in CI, and the gate holds a fixed oracle — a rate computed by code, not a model's opinion. A model-judged gate degrades from right by construction to right most of the time; this one cannot, because nothing in the score is inferred.
And the limit: step 3 bounds the agent by instruction. Nothing verifies the returned diff stayed inside the work order, and a finding can still be made to disappear by suppressing it. The three checks that would make the bound enforced rather than asked for are named, with the failure each closes, in the roadmap.
One pre-registered experiment has tested the bounded prompt. Generic prompting made 2 of 6 repositories worse; bounded prompting made 1 of 6 worse and improved 5 of 6, under this tool's own finding count. The registered hypothesis was narrower diffs, which did not hold, so the registered verdict stands at INCONCLUSIVE. Method, limits and raw data: docs/studies.md.
Who it's for
- Teams running AI agents in the dev loop, tired of unbounded cleanup PRs, who want a CI gate that actively constrains follow-up scope.
- Repos that want a maintainability gate without a SaaS analyzer or shipping code to a third party.
- Solo devs who want a single deterministic audit to pin in a Makefile, pre-commit, or local CI script.
The road to 1.0
1.0 is not a rewrite. It is the line drawn under a long arc of subtraction — what remained after every claim the project could not stand behind was removed. The arc is the credibility:
0.5.0 — the scoring engine was rebuilt. The old model counted findings absolutely, so it graded repo size, not maintainability: it scored Django, pytest, black, tornado, httpx, lodash, svelte, fastapi all at 0.0 / F while a 53-file toy repo scored 4.6 / A. Scores became rates, normalized per dimension against what real code carries and calibrated so the corpus median earns a B. See docs/standard.md.
0.6.0 — a near-duplication finding that pairs the copies, naming the declaration to reuse (
toAtomicAmountatTradeTicket.tsx:862already does this), so renaming can't hide clone-instead-of-reuse. Useful on its own terms — and explicitly not evidence about who wrote the code.
Retracted: that near-duplication distinguishes AI-written code. 0.6.0 had called it "the first signal that separates AI-written applications from mature human-written OSS." Re-run against a control matched on age, popularity and language, the near-duplication gap is not significant (p = 0.546), and no other metric earns the claim either. The honest summary is this design could not measure a difference, not there is no difference. See docs/studies.md.
0.7.0 — the evidence model. The score is withheld when the evidence cannot support one; a diff is not a repository grade. External analyzers became the primary evidence where they measured a full concept set, with the built-in detectors as the fallback and disagreement widening the range rather than being averaged. The grade is gated and banded from the evidence floor, so withholding evidence can never buy a better letter.
0.8–0.9 — one setup, chat-first. Chat / MCP became the primary surface, the CLI the automation door, and the three transports converged on a single question set. A run of chat-surface wiring defects — and their closing tests — is recorded in docs/defect-register-chat-surface.md.
1.0.0 — acceptance and the complete work order. A real-repo acceptance round on a mixed Python/TypeScript codebase hardened the last edges: the report now carries the entire backlog with a per-item copy-paste prompt, charts were rebuilt for legibility, reconfigure stopped destroying hand-tuned config, and TypeScript semantic coverage learned to find workspace projects and a locally-installed compiler. See the changelog.
The through-line: this repository runs the tool against itself in CI and checks the report in (docs/self-audit.md). An earlier revision of the self-audit table advertised 5.0/A+ after the codebase had drifted to a B; a hostile audit caught the stale claim — precisely the failure mode this tool exists to catch.
Install
python3 -m pip install maintainability-agent
maintainability-agent --root . --config maintainability-agent.json
Or run from a source checkout without installing:
python3 -m maintainability_audit --root . --config maintainability-agent.json
For an editable dev install and the full local-verification sequence, see CONTRIBUTING.md. Upgrading from 0.x? The scale and the evidence model changed on the way to 1.0 — see docs/migration-1.0.md.
Primary Surface: Chat / MCP
Drive the local MCP process from an IDE assistant or chat host. Call
audit_repository; unset action never audits. An unconfigured repository
(no repository maintainability-agent.json and no user config) returns
setup_needed and audit_ran: false — structured setup choices for analyzer
policy, history consent, economics, test-suite execution, and presentation, and
no report. A configured repository returns choice_needed (run or
reconfigure), also without a report. Answering setup does not start an audit.
action="run" returns the report and its bounded remediation prompt to the
conversation. record_history=None follows the persisted first-run history
consent and always appends to an existing history, while an explicit true
or false wins.
python3 -m pip install "maintainability-agent[mcp]"
maintainability-agent mcp --allow-root /absolute/path/to/repository
Presentation is exactly three choices — chat, a Markdown file, or a single-file HTML report — and where to save a file is asked only after a file format is chosen. No report file is written without that choice. See chat workflow help and IDE and agent integration.
Automation / CI: CLI
Use the CLI for scripts, repeatable automation, and CI gates. Copy the example
config to your repo root as maintainability-agent.json, then:
maintainability-agent \
--config maintainability-agent.json \
--fail-on-gate \
--output maintainability-report.md \
--prompt-output maintainability-remediation-prompt.md \
--comment-output maintainability-pr-comment.md \
--sarif-output maintainability.sarif
--fail-on-gate fails CI on hard gates only (a missing README, an undocumented
test command, a breached threshold) — never on a letter grade, and never on a
withheld estimate. For PR work, add --changed-only main...HEAD to audit the
diff; on a change too small to support a rate, the estimate and grade are
withheld and the scope is named as the reason.
What it analyzes
The deterministic scanner reads code from your repo (no LLM calls) and produces signals on:
- largest files (configurable warn/fail thresholds)
- function size and complexity — exact ranges for Python via
ast, brace-bounded for JS/TS/JSX/TSX/HTML — plus cognitive complexity (nesting-weighted reading cost) - class size, against its own budget (
max_class_lines) - duplicate blocks, and near-duplicate declarations compared structurally so renaming can't hide a copy, each paired with the original to reuse
- unreferenced private declarations (debris nothing can reach)
- competing libraries for one concern (two HTTP clients, two validators)
- configurable risk patterns (
eval(,exec(, TODO/FIXME, custom regex) - expected files / commands / clean-worktree (opt-in hard gates)
- TypeScript semantic facts from a recorded analysis or an
already-installed
tsc, including workspace projects (ADR 003) - test effectiveness from an opted-in suite run and parsed coverage (Class 5, default off — the one place the agent may execute the tree)
- an ISO/IEC 25010-inspired 0–5 estimate per category, and a verified grade — or a disclosed withholding when the evidence is thin.
The analyzer is intentionally conservative and under-reports rather than
over-reports: an unrecognized declaration costs one missed finding, never a
cascade of false ones. Pair it with native tools (ESLint, Ruff, Radon, Semgrep,
SonarQube, Qlty) rather than replacing them — their SARIF folds in via
--sarif-input. Accuracy and limits: docs/language-support.md.
Language support
Be clear-eyed about this: the tool does not support every language equally, and on an unrecognized language it will quietly under-report rather than fail. Coverage comes from two layers.
Eight languages are parsed as of 1.6.0: Python (1.0), Java (1.0), C (1.1), C++ (1.2), C# (1.3), Fortran (free-form 1.4, fixed-form 1.6), the JS/TS family, and HTML. Each has a scanner written for it and a documented list of what it misses — a language is claimed here only when both exist.
Built-in scanner (always on, no dependencies) — reads function/class declarations, sizes and complexity for a fixed set of languages, and only these:
| Language | How it's measured | Fidelity |
|---|---|---|
Python (.py) |
ast — exact end_lineno |
Exact |
Java (.java) |
dedicated brace-bounded scanner | Bounded; under-reports some constructs |
C (.c, .h) |
dedicated brace-bounded scanner — functions, struct/enum/union |
Bounded; prototypes and macros are not declarations |
C++ (.cpp, .hpp, .cc, .cxx, .hh) |
dedicated brace-bounded scanner — functions, class members, namespaces, templates | Bounded; bodyless declarations are not definitions |
C# (.cs) |
dedicated brace-bounded scanner — methods, constructors, class/interface/struct/record/enum |
Bounded; properties are not declarations |
Fortran, free-form (.f90, .f95, .f03, .f08, .F90, .F95, .F03, .F08, .pf) |
dedicated keyword-bounded scanner — modules, subroutines, functions, derived types | Bounded by end; measured with Fortran's own branch and nesting reading |
Fortran, fixed-form (.f, .for, .ftn, .F, .FOR, .FTN) |
the same scanner over card-column source; continuations joined, labelled DO loops understood |
Bounded by end or by the loop's label |
JS / TS / JSX / TSX (.js, .jsx, .mjs, .cjs, .ts, .tsx) |
brace/paren depth over a masked copy | Bounded by the declaration's own braces |
HTML (.html) |
same brace scanner (inline <script>) |
Bounded |
| TypeScript (semantic) | a recorded analysis or a locally-installed tsc, workspace projects included |
Type-level facts; unknown when no checker is present |
Any language not in that table — Go, Rust, Ruby, PHP, Kotlin, Swift, and the rest — is not parsed for declarations by the built-in scanner. Its files still count toward repo size, but the built-ins produce no function-size, complexity, duplication or dead-code findings for them, and the estimate leans on whatever evidence is available (which is why the report discloses its evidence tier and can withhold the grade).
Which analyzer covers which language (the opt-in pool, when installed):
| Language | Built-in scanner | External analyzer |
|---|---|---|
| Python | ast, exact |
ruff, radon, mypy, vulture, complexipy, interrogate, pydocstyle, pylint, cohesion |
| Java | dedicated scanner | lizard, PMD, Checkstyle, SpotBugs |
| C / C++ / C# | dedicated scanners | lizard, multimetric |
| Fortran (free- and fixed-form) | dedicated scanner | fortitude — 100+ rules; lizard — complexity, NLOC, params |
| JS / TS / JSX / TSX | brace scanner | ESLint, lizard, jscpd |
Fortran reached parity in 1.6.0. lizard measures it — this project's
catalog row for lizard was stale and said otherwise, so lizard came out
not-applicable on every Fortran repository and never ran — and
fortitude adds 100+ lint rules beside it. A lint now fails the build if
any parsed language has no analyzer that measures complexity.
External analyzer adapters (opt-in pool) — this is how coverage extends beyond the built-in set. When you enable the analyzer pool, the tool shells out to mature analyzers and folds their output in through per-tool adapters: lizard (cyclomatic complexity across ~a dozen languages), jscpd (cross-language duplication), ESLint (JS/TS), PMD / SpotBugs (JVM), and others in the catalog. These run only when selected and installed (acquisition is opt-in and off by default), and where they measured a full concept set they become the primary evidence, with the built-ins as the fallback.
So: first-class today is Python (and TypeScript for semantics); Java, C, C++, C#, free-form Fortran and the JS/TS family are bounded-but-real; every other language is only as covered as the external analyzer you point at it — and with no analyzer, it is under-reported by design. The per-language accuracy, the exact parsed set, and the known limitations are documented in docs/language-support.md; the adapter catalog is in docs/adapters.md.
What it produces
Any combination of: maintainability-report.md (or a single-file HTML report),
maintainability-remediation-prompt.md (the bounded prompt),
maintainability-pr-comment.md, maintainability.sarif (2.1.0, for GitHub
Code Scanning), maintainability-baseline.json (for --fail-on-new
incremental adoption), and per-tool agent instruction files
(AGENTS.md, CLAUDE.md, Cursor/Copilot/Windsurf rules) via
--init-agent-standards.
The report is the complete work order. The HTML and Markdown reports carry the whole backlog — every finding with a self-contained, deterministic copy-paste prompt telling the agent to fix only the listed items, keep the patch small, preserve architecture and behavior, add tests where behavior changes, and report false positives instead of rewriting blindly. Chat and CLI render a bounded view — a summary plus the top items and a pointer to the report — so a payload cap can never truncate the prompt.
Scoring standard
Based on ISO/IEC 25010 maintainability — modularity, reusability,
analyzability, modifiability, testability. Scores are rates calibrated
against real code, not counts: every pressure is normalized against the
median a pinned 40-repo corpus of mature projects (django, angular,
transformers, webpack, vite, playwright, …) actually carries, so 2.5x means
"two and a half times what well-maintained real code shows." The corpus median
earns a B; A+ is gated, requiring every dimension clean.
The corpus is selected by query, not taste — stars:>3000 created:<2021-01-01 pushed:>2026-01-01 across Python, TypeScript and
JavaScript, then filtered to repositories that contain code. The calibration is
reproducible, not asserted: python3 tools/calibration/measure.py --check
re-measures and fails on drift, and tests/test_calibration_corpus.py
re-derives the constants offline from checked-in measurements. See
docs/standard.md.
Self-audit
This repo eats its own dogfood — the tool runs against this codebase in CI, and a report is checked in at docs/self-audit.md, stamped with the exact source commit it was generated against (a provenance record, not a claim about HEAD). These figures mirror that stamped report row for row:
| Metric | Value |
|---|---|
| Maintainability estimate | 4.1 / 5 |
| Verified grade | B |
| Files scanned | 391 |
| File warnings | 120 |
| File failures | 0 |
| Function warnings | 65 |
| Function failures | 0 |
| Duplicate blocks | 0 |
| Risk findings | 0 |
| Hard gate failures | 0 |
Yes, a B — demoted from the A band because warning rates exceed the
A-grade ceilings, against thresholds this repo sets stricter than the shipped
defaults. The grade is gated (A+ needs every dimension clean; a
repo with production code and zero test files cannot earn an A-grade) and banded
from the evidence floor, so withholding evidence never buys a better letter.
Every threshold gate — file, function, duplication — is opted on for this
repo's own CI, against thresholds stricter than the shipped defaults, so
drifting below the bar fails the build rather than the README. (CI note:
actions/checkout defaults to fetch-depth: 1, which hides history and costs
roughly a grade — use fetch-depth: 0.)
Platform support
POSIX. Linux and macOS are what this has run on; Windows is untested and not
claimed. That is a statement about evidence, not a refusal: CI runs Linux,
development runs macOS, and the test suite creates symlinks with no platform
guard (needing Developer Mode on Windows), so it never reaches the point of
telling anyone whether the product works there. Adding windows-latest to CI
and fixing what falls out is how that changes.
Invokable skill / slash command
This repo ships a portable skill under
skills/maintainability-agent/ so
/maintainability-agent is one keystroke away. Keep the installed copy in sync
— a drifted skill teaches agents a dead workflow:
maintainability-agent --install-skill # writes ~/.claude/skills
Re-run after every upgrade; a differing installed copy is refused with the
list of differences (--force-skill to overwrite).
| Host | Install destination | Invocation |
|---|---|---|
| Codex / OpenAI | via skills/maintainability-agent/agents/openai.yaml |
per Codex's skills convention |
| Claude Code | skills/maintainability-agent/ → ~/.claude/skills/maintainability-agent/ (or repo .claude/skills/) |
/maintainability-agent |
| GitHub Copilot (VS Code) | skills/maintainability-agent/copilot/maintainability-agent.prompt.md → <repo>/.github/prompts/ |
/maintainability-agent in Copilot Chat |
For always-on guidance instead of an invokable skill, use
--init-agent-standards (see docs/ide-agent-integration.md).
GitHub Action
This repo ships action.yml, usable as a composite action:
- uses: marshallguillory86/maintainability-agent@v1.0.0
with:
config: maintainability-agent.json
changed-only: main...HEAD
fail-on-gate: "true"
Or copy .github/workflows/maintainability.yml into the target repo. For repos
not on GitHub Actions, examples/local-ci.sh enforces coverage and writes
coverage.xml.
Documentation
Start with the documentation index, which states each document's genre and what it is allowed to assert.
Governing — Product intent (authoritative: promises, and what it must never claim) · Architecture (layers, enforced invariants, known debt) · Philosophy (why AI-specific: volume, not pathology) · Decision register including ADR 001 (evidence and verification).
Reference — Maintainability standard · Studies and measured results · Report contract · CLI reference · Config schema · Language support · Analyzer adapters · IDE and agent integration · Chat workflow help · PR and baseline workflows · Roadmap · Changelog.
Running tests
PYTHONPATH=src python3 -m pytest
The full local verification that matches CI — ruff, pip-audit, the 92% coverage gate, and the self-audit — is in CONTRIBUTING.md.
Get in touch
- Bugs / features / questions — open a GitHub Issue.
- Discussion — the Discussions tab.
- Security — see
SECURITY.mdand the private advisory flow. Do not post vulnerabilities in public issues.
Support this work
This is a single-maintainer, MIT-licensed project — free to use, and built on a lot of unpaid hours. If it saves you or your team time, please consider sponsoring its continued development:
Sponsorship is entirely optional and never gates a feature, a fix, or support — the whole tool stays free and open. It just helps keep the work going.
Acknowledgements
- Miles Parker — identified the friction-signal gap: that the "this keeps fighting me" signal a maintainer accumulates over months is exactly the evidence an LLM cannot hold across sessions, and that a tool positioned between the two should carry it. A contribution of insight rather than code, and it changed what this project is for.
License
MIT
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 maintainability_agent-1.10.1.tar.gz.
File metadata
- Download URL: maintainability_agent-1.10.1.tar.gz
- Upload date:
- Size: 896.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b84b8c811024ca477864173aa61da6870c848ba7c90bfd23082eca9ab931a90
|
|
| MD5 |
ec04912eadd4c1714b3b371c86a02333
|
|
| BLAKE2b-256 |
ae2b7c5eb6326c2e2e612592014a45731a39876b7ae880b3c7ca8376812d72b7
|
Provenance
The following attestation bundles were made for maintainability_agent-1.10.1.tar.gz:
Publisher:
release.yml on marshallguillory86/maintainability-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maintainability_agent-1.10.1.tar.gz -
Subject digest:
6b84b8c811024ca477864173aa61da6870c848ba7c90bfd23082eca9ab931a90 - Sigstore transparency entry: 2701921471
- Sigstore integration time:
-
Permalink:
marshallguillory86/maintainability-agent@08b59cae6e32746b33354d8acb47e78922097264 -
Branch / Tag:
refs/tags/v1.10.1 - Owner: https://github.com/marshallguillory86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@08b59cae6e32746b33354d8acb47e78922097264 -
Trigger Event:
push
-
Statement type:
File details
Details for the file maintainability_agent-1.10.1-py3-none-any.whl.
File metadata
- Download URL: maintainability_agent-1.10.1-py3-none-any.whl
- Upload date:
- Size: 472.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d63918a244c72cda8a6988bb7a892406b7cc0301e6af77efa2cb78c79015a79d
|
|
| MD5 |
385f4b4f61c86f8e7a9db280076f2f47
|
|
| BLAKE2b-256 |
42fb60ea505c21977d82e7cb77c1ae3e9fbcfd90b4ab00d7df2cc25784e889a1
|
Provenance
The following attestation bundles were made for maintainability_agent-1.10.1-py3-none-any.whl:
Publisher:
release.yml on marshallguillory86/maintainability-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
maintainability_agent-1.10.1-py3-none-any.whl -
Subject digest:
d63918a244c72cda8a6988bb7a892406b7cc0301e6af77efa2cb78c79015a79d - Sigstore transparency entry: 2701921708
- Sigstore integration time:
-
Permalink:
marshallguillory86/maintainability-agent@08b59cae6e32746b33354d8acb47e78922097264 -
Branch / Tag:
refs/tags/v1.10.1 - Owner: https://github.com/marshallguillory86
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@08b59cae6e32746b33354d8acb47e78922097264 -
Trigger Event:
push
-
Statement type: