Adding a strict linter to an existing repository produces four thousand errors and gets reverted. The usual workaround — set everything to warn — enforces nothing, and the count grows quietly.
ever-better is the way around that. It records every violation that exists today as a ceiling: old code is grandfathered, new code is held to the whole rule set, and the ceiling can fall but never rise.
Below is every command, what it does, and the problem it exists for.
npx ever-better <command>
MIT, zero runtime dependencies, Node 20.11+.
diagnose — what is missing, and what each gap costs
ever-better diagnose # read-only
ever-better diagnose --write # also writes QUALITY.md
What it does. Surveys the repository — package manager, language, framework, runtime, ESLint setup, formatter, test runner, dead-code and duplication scans, agent instructions, CI, file sizes — and lists the gaps with the phase each belongs to.
Why. "Add linting" is not a task; it is six of them in an order that matters. Formatting has to land before linting or the first cleanup PR is a diff nobody can read. Type-aware rules cannot run without TypeScript. This prints that order instead of leaving you to discover it.
bootstrap — install the tooling, generate the configs
ever-better bootstrap
ever-better bootstrap --dry-run
What it does. Adds the dependencies, writes eslint.config.mjs, Prettier config, .gitattributes, knip.json, the package scripts CI needs, workflows for three platforms, dependabot.yml, and a secret-scan workflow. Prices each TypeScript strictness flag by enabling it in a temporary config and counting the errors — turning on the free ones, reporting what the rest would cost.
Why. Every rule it writes is an error, not a warning. That is only survivable because of the next command.
freeze — pin today's violations as the ceiling
ever-better freeze
What it does. Runs eslint --suppress-all, which records how many violations each rule has in each file, then stores the totals in .ever-better/state.json.
Baseline pinned: 4,942 violations across 72 rules are now grandfathered.
Why. From this commit on, the 4,942 are silent and violation 4,943 fails the build. You do not have to fix anything today to start enforcing everything tomorrow.
The ratchet itself is ESLint's own bulk-suppressions feature. ever-better is the part around it: knowing which rules to add, keeping the ledger, and failing CI when a number goes up.
It refuses to run twice. A second freeze would grandfather everything added since — which is the one thing the baseline exists to prevent.
check — the gate
ever-better check
What it does. Compares current counts against the ceiling and exits non-zero if anything rose.
8 unsuppressed error(s) — these are new since the baseline:
2 id-length
1 prettier/prettier
Why. A baseline is only a ratchet if something rejects a regression. Without CI it is a note.
Do not pipe it.
ever-better check | tailexits withtail's status and reports a failing gate as success.
prune — lower the ceiling after a fix
ever-better prune
What it does. Removes suppressions for violations that no longer exist, so the recorded ceiling matches reality.
Why. Without it, fixing 100 violations changes nothing: the ceiling still permits them, and they can come back. This is what makes the number monotonic.
next — what to drain first
ever-better next
ever-better next --fan-in
What it does. Ranks the backlog by which edit enforces the most, not by which rule is biggest.
take these first — one or two edits, and the rule is enforced in that file for good:
1 src/browser.js @typescript-eslint/no-require-imports
1 src/common.js no-param-reassign
Why. The ratchet is per file per rule. A file with no suppression left for a rule fails on its next violation, whatever that rule's total is elsewhere — so two violations in one file buy more enforcement than twenty spread across twenty files.
--fan-in adds how many files import each one, because a fix in a widely imported module lands errors in files the diff never opened.
report — what the debt actually looks like
ever-better report
ever-better report --json
What it does. Prints the backlog as a rule × area table, and appends it to $GITHUB_STEP_SUMMARY when that is set.
Why. next answers "which edit enforces the most". This answers "what shape is this repository's debt". And because the generated workflow runs it after check with if: always(), the run where the gate just failed is the run where the backlog is visible — no one has to edit a workflow to get a report.
secrets — scan the history for committed credentials
ever-better secrets
What it does. Runs gitleaks over both the history and the working tree, failing on any finding.
Why both. Either alone passes a repository that is holding a secret. A history scan misses the key you pasted an hour ago and have not committed. A working-tree scan misses the key that was committed and then deleted — which is still in every clone.
This is the one check with no baseline. A leaked key is not debt you ratchet down.
migrate — JavaScript to TypeScript
ever-better migrate # show the plan
ever-better migrate --all # rename everything in one pass
ever-better migrate --file src/foo.js
What it does. Renames with git mv so history follows, and lets the lint fallout land in the ratchet instead of blocking the migration.
Why. Types are the cheapest rule set there is, and the tier that finds the most real bugs cannot run without them. On a 13-year-old JavaScript project, renaming 49 files and changing no logic produced 2,641 type errors — every one of them a question nobody had answered.
emit-diff — prove a refactor changed nothing
ever-better emit-diff --against main
What it does. Compiles the working tree and a git ref, then compares the emitted JavaScript.
Why. Types erase at compile time, so a change that only moves types must produce byte-identical output. That is a proof, in seconds. No amount of test coverage states it as strongly, because tests only cover the paths someone thought of.
catalog — the helpers that already exist
ever-better catalog
What it does. Writes docs/shared-helpers.md: every exported function, grouped by directory, with the first sentence of its doc comment. Point your CLAUDE.md at it.
Why. A linter sees inside one file. Duplication detection only notices copies once they are textually similar, and two independent implementations of the same idea rarely are. Nothing else reports the same function written a sixth time under a sixth name.
status — where you are
ever-better status
What it does. Prints the phase, the frozen date, the current backlog, which rules improved or regressed, and the smallest remaining backlogs.
STALE 100 commits since the diagnosis; re-run diagnose before trusting it
phase drain
backlog 3532
Why. That STALE line matters more than it looks. A diagnosis from a hundred commits ago is a description of a repository that no longer exists.
log — why, not how many
ever-better log --kind drained --rule max-depth "6 violations, 1 real bug"
ever-better log --kind deferred --rule max-lines "router.ts is 1400 lines; its own project"
ever-better log --kind issue --rule no-floating-promises "opened #42 — product decision"
What it does. Records what happened against the current commit — the only command that writes the Work log in QUALITY.md. Everything else records counts.
Why. deferred is the one that earns its keep. It renders as a Carried over checklist stamped with the commit it was seen at, because "router.ts needs splitting" is useless four hundred commits later unless a reader can tell when it was true.
The whole loop, in five commands
The other eight are for when you need them. This is the shape:
npx ever-better diagnose # what is missing
npx ever-better bootstrap # install it
npx ever-better freeze # pin today's count
npx ever-better check # gate CI on it
npx ever-better prune # lower the ceiling as you fix
Or hand the repository to an agent
The commands are the deterministic half — count, record, gate, refuse. The judgement half is eight Claude Code skills that ship with it: which violation is a real bug, when to stop and ask, what deserves an issue rather than a fix.
/plugin marketplace add isamu/ever-better
/plugin install ever-better
Then say clean this repo up. It formats, installs, freezes, and works the backlog down one rule per pull request — fixing violations, extracting the pure functions that make the fixes testable, writing the tests, and lowering the ceiling as it goes.
It is not unattended. Anything it can decide from the code, it decides. Anything it cannot — ambiguous behaviour, a public API change, a refactor that is its own project — becomes an issue with the options written out, and it moves on.
Try it on something you did not write
That is the honest test. Pick a repository you have never read:
git clone --depth 1 https://github.com/debug-js/debug.git
cd debug
npx ever-better diagnose
npx ever-better bootstrap
npx prettier --write .
npx ever-better freeze
Then add a file with a violation and run npx ever-better check. The 72 existing violations stay silent; the new ones fail.
On this particular repository you will also see five (parse error) entries — /* eslint-env */
comments and stale eslint-disable directives that flat config rejects. Those are real, they have
no rule id, and --suppress-all cannot record them, so freeze names the file and line for each.
It is a fair first impression of what the tool does: it tells you where, and it does not pretend the
number is smaller than it is.
Results welcome — docs/RESULTS.md collects what people froze and how much of it turned out to be real defects. It has two rows and both are mine, which is enough to notice a pattern and nowhere near enough to claim one. A run that found zero real bugs is the most useful row that table could get.
→ github.com/isamu/ever-better (MIT)
Top comments (0)