先拦住新增违规,再让 Agent 清理旧账
文章介绍用 ESLint 批量抑制记录历史违规,在启用严格规则的同时约束新增代码。随后按目录把旧问题拆成可验证的小任务交给编程 Agent;该机制仅适用于设为 error 的规则。
AI 深度解读
一条严格的 ESLint 规则,常常不是没人认可,而是一打开就冒出几百个错误,谁也不想接那个横跨全仓库的 PR。作者给出的办法是:先登记历史欠账,让规则立即约束后续代码,再把旧问题按目录拆给 AI 编程代理处理。
这套流程最值得借鉴的,是如何把一次大迁移变成可验收的小任务。不过,它能保证的是检查机制开始运转;类型是否修对、代理有没有绕过规则,还得靠测试和审查。
先让规则生效,历史错误可以分批还
原文以 @typescript-eslint/no-explicit-any 为例。过去团队可能因为存量 any 太多,直接关闭规则,结果新旧问题一起增长。
ESLint 在 v9.24.0,2025 年 4 月 4 日发布的 bulk suppressions,允许把已有违规记录到文件里。配置中先将规则设为 "error",再执行 npx eslint . --suppress-rule @typescript-eslint/no-explicit-any --fix。
这里必须是 "error","warn" 不会被批量抑制。命令生成项目根目录下的 eslint-suppressions.json,按「文件路径、规则、数量」登记旧问题,然后把它提交进仓库。
--fix 会先修复能自动修复的问题。no-explicit-any 在默认选项下不能自动修复,但配置里的其他规则可能改动文件,因此作者要求从干净的工作区执行,并检查 diff。
多个规则可以重复传入 --suppress-rule;--suppress-all 则记录所有处于 "error" 级别的规则。路径相对于工作目录,采用 POSIX 格式;从 v9.27.0 开始,键会排序,减少无关 diff。如果用 --suppressions-location 改存放位置,后续每次运行都要带上它。
先接纳历史欠账,才有机会立即阻止欠账继续增长。
但这里有个边界:抑制记录按文件和规则计数,不绑定具体行。某文件登记了 4 个 any,后来出现第 5 个,ESLint 无法判断哪个是新增的,会把这项规则的全部违规都报出来。

CI 仍然执行 npx eslint .。修掉旧问题后,还要运行 npx eslint . --prune-suppressions,清理多余记录;否则 ESLint 默认以退出码 2 报告未使用的抑制项。
v9.28.0 新增的 --pass-on-unpruned-suppressions 可以让这些残留项不影响退出码。作者认为本地可以用,CI 应当关掉,否则账本会逐渐失真。
此外,创建和清理记录只能使用 CLI。Node.js API 到 v10.1.0 才能通过 applySuppressions: true 应用已有记录;编辑器是否停止标红,取决于它的 ESLint 集成有没有启用这个选项。
给代理的任务,应该小到能一次审完
作者没有让代理直接「修复整个仓库」,而是先用 jq 盘点分布。统计单条规则总数时,先设置 RULE=@typescript-eslint/no-explicit-any,再执行 jq --arg r "$RULE" '[.[] | .[$r].count // empty] | add' eslint-suppressions.json。
下面这段按路径前两段汇总,生成从多到少的目录清单:
jq -r --arg r "$RULE" '
[ to_entries[]
| select(.value[$r])
| { dir: (.key | split("/") | .[0:2] | join("/")),
n: .value[$r].count } ]
| group_by(.dir)
| map("\(map(.n) | add)\t\(.[0].dir)")
| .[]' eslint-suppressions.json | sort -rn
重点看 .[0:2]:如果某个目录仍然过大,可以针对该子树改成 .[0:3],继续拆分。合适的粒度,是代理一次能完成、人一次能审完。
每个目录使用相同任务模板,只替换路径:
Fix every @typescript-eslint/no-explicit-any error
in src/billing. Use real types, or unknown plus a
type guard. Do not add eslint-disable comments and
do not add casts whose only job is to silence the
rule. Do not edit files outside src/billing.
Keep runtime behavior the same; run the tests.
When done, run `npx eslint . --prune-suppressions`,
then `npx eslint .` must pass. Commit the code and
eslint-suppressions.json together.
最关键的是禁止靠 eslint-disable 和纯消音用途的类型断言过关。unknown 配合类型守卫,要求代码先检查值,再使用它,比直接把 any 换成一个断言更有约束。
模板还存在一个小歧义:它禁止修改目录外文件,却要求更新根目录的抑制文件。实际使用时,应明确允许修改 eslint-suppressions.json。
多个代理也不应共用同一个工作目录。原文用 git worktree add -b lint/any-billing ../app-any-billing 和 git worktree add -b lint/any-users ../app-any-users,分别建立分支和目录,避免互相覆盖修改。
作者开发的 Hermes IDE 将这套操作包装成任务入口,可以选择 Claude Code、Codex 或其他代理。.hermes/worktree.toml 用 setup = ["npm ci"] 准备依赖,用 done_when = ["npx eslint ."] 设置完成检查。这里的产品介绍有作者自身立场,但底层方法用普通 Git 同样能做。
验收要看代码,也要看欠账是否真的减少
任务完成后,作者要求同时检查源码 diff 和抑制文件:减少的数量,应当能对应到实际修复。如果处理 src/billing,却删掉了 src/users 的记录,就需要追查原因。
--prune-suppressions 会删除不再匹配违规的记录,以及已删除文件的条目。因此,「账本变小」只是线索,不能单独证明修复正确。
作者还给了一个快速检查:运行 git diff main...HEAD | grep '^+.*eslint-disable',寻找新增的禁用注释。但它只能覆盖这一类绕行方式;代理新增的每个 as SomeType,仍要人工确认有没有相应检查。
Hermes 的 Review Desk 可以按轮次或文件查看改动,行内评论会返回给对应代理。这个界面的实际价值,是把代码和抑制文件放在同一次审查里。

如果多个分支修改抑制文件导致冲突,作者建议在双方都只删除记录的前提下,取其中一份,再对合并后的代码运行 npx eslint . --prune-suppressions。这样可以重新生成符合当前代码状态的账本,随后仍应运行完整 lint 检查。
我的判断
我认可它把「规则是否值得启用」和「旧代码何时修完」分开处理。相比一直关闭规则,它能尽早约束后续开发;相比满仓添加禁用注释,它把迁移欠账集中在一个文件里,也更方便盘点。
但作者没有提供代理修复耗时、成功率或人工对照数据。「代理能更快消化清单」在这篇里属于经验判断,不能直接当作效率结论。
更需要留意的是计数机制的局限。同一个文件里修掉一个旧 any,又新增一个 any,总数不变,检查未必能识别。有人重新执行 --suppress-rule、提交更大的抑制额度,CI 也可能通过。
它限制的是违规数量,不能独立证明每一处类型都变得更可靠。
与允许长期保留的局部禁用注释相比,bulk suppressions 更适合团队已经认可、准备逐步清偿的迁移任务。如果规则本身还有争议,先登记几百条欠账,只是推迟讨论。高欠账文件一旦超额就集中报错,也应提前向团队说明,避免干扰无关 PR。
能拿来做什么
- 在现有 TypeScript 前端或 Node 项目里,先选一条团队已经认可的严格规则,登记存量问题,再让 CI 阻止数量继续增长。
- 给 AI 编程代理分配一个可独立审查的目录,明确真实类型、
unknown和类型守卫的使用要求,并把禁止绕过规则写进任务。 - 审查时把源码、测试结果和
eslint-suppressions.json放在一起看,重点检查新增断言、禁用注释以及抑制额度增加。 - 并行处理多个目录时使用独立 worktree;合并后重新清理抑制记录,并运行完整 lint,确认账本与最终代码一致。