Teaching a Checker to Fix Its Own Findings
A three-tier autofix engine โ mechanical edits, a local model, and a gate that contains any bad fix
A linter that only reports problems is doing half a job. It hands you a list of line numbers and a stern message, and then leaves you to make the same edit forty times by hand. For a small codebase that is fine. For a repo where a single rename refactor can break a hundred and thirty import sites in an afternoon, the "go fix these yourself" model is the bottleneck.
NAOMS has its own static analyzer โ a built-in checker with a growing catalogue of rules that encode the project's hard constraints: no quiet direct writes to the ledger, no forge-host names in tracked files, no floating promises, no doc comment that accidentally terminates itself. By early April the catalogue had real teeth and a real problem: the big rename sweeps of the week were generating violations faster than anyone could hand-fix them. The checker was right about everything and useless about all of it.
On 2026-04-10 we shipped the answer: a checker that fixes what it finds. Not all of it โ that would be a lie, and the lie is the interesting part โ but a deliberately tiered slice of it, with a validation gate that treats every machine-made edit as guilty until proven innocent.
To understand why this had to be tiered rather than a single "autofix everything" flag, it helps to look at the shape of the findings themselves. Run the checker against a tree mid-refactor and you get three qualitatively different kinds of violation. The first kind has exactly one correct fix and a machine can compute it from the finding alone. The second kind has a small, bounded set of plausible fixes and choosing between them takes a sentence of judgment โ the sort of judgment a model is good at and a regex is not. The third kind requires understanding the program's intent and belongs to a human. A single autofix flag treats all three the same and is therefore wrong about two of them. The tiers exist because the findings are not homogeneous, and pretending they are is how autofixers earn their bad reputation.
The honest framing first
Before the architecture, the boundary. An autofixer that silently rewrites your source and trusts itself is a footgun pointed at the main branch. The design question is not "can a machine fix this finding" โ for many findings it obviously can โ but "what happens when the machine is wrong, and how small can we make the blast radius." Everything below is in service of that second question.
Tier 1 โ mechanical
The first tier is pure text transformation: deterministic, reversible, no model in the loop. Stale file-header comments, a missing error check appended to a query result, an import that needs to switch to a renamed alias. These are findings where the fix is a function of the finding โ given the violation, the correct edit is computable. The commit that landed the per-commit hook wired this directly into the commit path: opt in with an environment flag, and the boring violations never reach a human.
The clearest demonstration came the next day, when one mechanical pass rewrote stale headers across 228 package files in a single commit. No model, no ambiguity, no review fatigue โ exactly the work a human should never have to do by hand.
The property that makes the mechanical tier safe is that every transformation is a pure function: the same finding on the same source always produces the same edit, and the inverse is computable, so a mechanical fix is always reversible. That determinism is what lets it run inside the pre-commit hook without a review step. You are not trusting a machine's judgment; you are trusting that string replacement does what string replacement does. The interesting design work here is restraint โ keeping rules out of this tier the moment their fix stops being a pure function of the finding. The day a "mechanical" rule needs to look at a second file to decide its edit, it is no longer mechanical, and quietly leaving it there is how a deterministic autofixer starts making non-deterministic mistakes.
Tier 2 โ model-assisted, locally
Some findings are not mechanical. They need a small amount of judgment: which of three call sites is the real one, how to phrase a replacement that reads naturally, where to thread a new argument through an existing signature. For these, the second tier reaches for a local model.
That word โ local โ is the whole point. NAOMS runs its sub-agent work on on-device models, not a hosted API, and the autofix engine is no exception. The model never leaves the machine; the prompt templates are checked into the repo; the thirteen rules that opt into model assistance are explicit. There is no scenario where fixing a lint finding ships your source tree to someone else's datacenter.
Choosing which local model was not a guess. The work was preceded by a careful evaluation: seven candidate models, thirteen representative findings, two hundred and seventy-three runs to see which model produced edits that survived the gate. The model choice is an empirical result, not a vibe.
Tier 3 and the gate that makes the whole thing safe
Here is the part that turns a clever idea into something you can point at the main branch.
A naive autofixer runs all its edits, then validates once at the end. If anything is broken, you revert the batch and you have lost every good fix along with the one bad one โ and worse, you have no idea which fix was the culprit. That is the failure mode that keeps autofix out of most codebases. People try it, one model edit corrupts a file, the whole run rolls back, and they conclude machines can't be trusted with source.
The fix is to make the unit of trust the file, not the run. Each proposed edit is applied to a single file, the file is re-validated in isolation, and only if it still parses and still passes does the edit stick. A bad model suggestion reverts that file and nothing else. The good fixes in the same run are untouched. The blast radius of a wrong machine edit is one file, caught immediately, with the finding left open for a human โ which is exactly the right outcome.
This is the structural answer to the safety question from the top. The tiers decide how a fix is generated โ mechanically, with a local model, or not at all. The per-file gate decides whether any individual fix is allowed to survive. The two are orthogonal, and keeping them orthogonal is what lets you turn the model tier on without holding your breath.
It is worth being precise about what "re-validated in isolation" buys you, because the naive objection is that a per-file gate can't catch breakage that spans files. That is true, and it is exactly why the gate is paired with the existing whole-tree check that runs afterward anyway. The per-file gate's job is narrower and more valuable: it catches the local catastrophes โ the edit that made a file stop parsing, the replacement that dropped a brace, the model output that hallucinated a token โ at the moment they happen, before they can contaminate the rest of the run or the rest of the diff. Cross-file regressions are still the whole-tree checker's responsibility on the next pass. The two gates compose: fast, local, per-edit rejection in the inner loop; slow, global verification in the outer one. Neither alone is sufficient, and conflating them is the mistake that makes people validate once at the end and lose everything when one edit goes bad.
There is a quieter benefit, too. Because each surviving fix has individually passed re-validation, the report the engine emits is trustworthy at the line level: it does not merely say "I changed forty files," it says "here are the thirty-seven findings I fixed and the three I could not, with the reason." A report you can trust file-by-file is a report a human can skim instead of audit, and that difference โ skim versus audit โ is the entire return on the per-file gate's complexity.
That same day the engine grew a machine-readable report and an agent-callable interface, so the autofixer is itself addressable by other agents: a sub-agent can ask the checker to fix what it can and report precisely what it could not, machine to machine, with the human only in the loop for the genuinely hard residue.
Why the rules are the schema
One more piece of the design deserves attention, because it is what keeps the whole thing from rotting. Each rule is a self-contained unit: it knows how to detect its violation, and it declares which tier โ if any โ knows how to fix it. A rule is not a global switch in some autofix module that has to be kept in sync with the detector; the detector and its fix strategy live together. When a later polish pass added a new doc-comment rule โ born directly out of the "a stray comment terminator breaks the build" class of bug โ it shipped its detection and its tier assignment in the same commit. Adding a rule does not mean editing the autofixer; the autofixer reads the rule's declared capability and acts on it.
This is what makes the system extensible without becoming dangerous. A new rule author decides, at authoring time and in one place, whether their finding is mechanically fixable, model-fixable, or human-only. If they are unsure, the safe default is human-only โ the finding gets reported and nothing is auto-edited. You opt a rule into automation deliberately; you never opt it in by accident. For a checker that runs in the pre-commit path of a repo with a hundred-plus concurrent agents, "you never automate by accident" is not a nicety. It is the property that lets you sleep.
It also explains the relationship to the broader zero-violations push. That push is not "run the autofixer once and declare victory." It is the slow, rule-by-rule campaign of driving each violation category to zero โ and for each category, the question is the same: can this be mechanically fixed, model-fixed, or does it need a human. The autofixer is the lever; the campaign is the work of pulling it, category by category, and that work was still in progress as this shipped.
What this is not
It is not magic, and the design is careful to never pretend otherwise. The model tier covers thirteen rules, not all of them. The mechanical tier covers the boring deterministic ones. Everything outside those two sets still lands on a human's desk โ and that is the honest line. The zero-violations push was, as of this writing, still in progress, not finished; the autofixer is the tool that makes that push tractable, not a button that completes it. A companion polish commit added a rule โ more findings to fix โ on the same day, because a checker that fixes its own findings is only worth building if you keep teaching it new things to find.
The lesson generalizes past this repo. If you maintain a checker, the highest- leverage feature you can add is not another rule โ it is the ability to fix the rules you already have, tiered by how much judgment the fix requires, gated so that being wrong about one file never costs you the others. Report-only is half a job. Fix-and-prove-it is the other half.
Written by AI agents from real project logs; owned and edited by Mujo.