AGENTS.md/CLAUDE.md 中的路径和引用会悄悄失效,agent 仍会信以为真导致破坏。reflint 通过磁盘校验路径引用、链接有效性、npm run 与 package.json 的一致性来自动发现问题。
AGENTS.md 和 CLAUDE.md 会悄然腐坏。你写"setup is npm run build"、"the entry point is src/index.ts",有人重命名了一个脚本,指令就静悄悄地变成了谎言。Agent 相信了它,然后搞坏了东西。
于是我写了 reflint:一个完全忽略措辞和风格的 linter,只检查一件事——引用还能不能解析?反引号路径对磁盘、markdown 链接目标对仓库、npm run <script> 对 package.json。零依赖、语言无关、在 CI 里 exit 1。
这就是开场段落。剩下的内容是把它发布之后发生的事。
我用 reflint 对 118 个公开仓库(都有 AGENTS.md 或 CLAUDE.md)进行了检查,对比修复前后的版本。发现的问题大部分是我自己造成的。
reflint 有 --code-blocks。帮助文本说它让工具"also check paths inside fenced code blocks",言下之意是默认跳过它们。几个月前我写了一篇文章,大意是:开启这个选项后它会检查代码块里的裸路径。
在 118 个仓库里,这个 flag 改变结果的次数是零。
原因是接线错误。四个扫描器里只有最后一个参考了"是否在栅栏内"状态。npm run 扫描器、反引号引用扫描器和 markdown 链接扫描器从不看这个——它们默认就读取栅栏内容,所以打开这个 flag 什么也加不了。这个 flag 是死的。
我把三个扫描器都移到了栅栏检查内部。在同样的 118 个仓库上,现在有 10 个的结果会因为这个 flag 而改变。
AGENTS.md:3 `pnpm -r` — no script "-r" in package.json
AGENTS.md:4 `pnpm --filter` — no script "--filter" in package.json
AGENTS.md:5 `pnpm -w` — no script "-w" in package.json
脚本名正则的字符集包含了 -,所以 pnpm 后面紧跟的 flag 被当成了脚本名。39 个脚本类发现里有 16 个是这种情况。在任何使用 pnpm workspace 的仓库里,正确写的文档都会被标红。
这些形式是针对每个 workspace 包运行的,所以不该拿根目录的 package.json 来检查它们。reflint 现在直接跳过整个调用。
栅栏状态是用显而易见的 inFence = !inFence 切换来追踪的。这在演示栅栏本身时就会出问题——用一个更长的栅栏把栅栏标记包起来:标记数量变成奇数,下面的一切永远卡在"在栅栏内"状态。
118 个仓库里有一个——一份 3,461 行的 AGENTS.md——恰好就是这么做的。标题和正文被当成栅栏内容处理了。
结合修复 #1,这是危险的一个。默认跳过栅栏内容,所以从那个放错的标记往下,reflint 什么都不检查,exit 0,然后打印 reflint: all references resolve。
对于一个 linter 来说这是最糟糕的失败方式,而我已经在 0.9.2 经历过一次了。那时候通过 npm i -g 或 npx 安装的 CLI exit 0 后根本没运行,因为入口点检查把 process.argv[1] 和 import.meta.url 比较,而这两种方式通过符号链接时永远不匹配。"没发现问题"和"根本没跑"无法区分——对用户如此,对 CI 也是。
关闭规则现在用 CommonMark 的规则:相同字符、长度至少等于开头、后面没有信息字符串。
这些修复每一个都创造了默认不检查的区域。如果发现数变少了,用户无法判断是问题被修复了还是工具停止扫描了。这是同一层级的失败,往上走了一层。
所以 reflint 现在总是报告它跳过了什么:
reflint: 2 broken references (3 inside code blocks, not checked — run with --code-blocks)
reflint: all references resolve (1 inside code blocks, not checked — run with --code-blocks)
JSON 输出新增了一个 skipped 字段。HTML 注释不计入——它们是被禁用的文本,不是 --code-blocks 会恢复的内容。
在 118 个仓库里:208 个发现变成了 185 个。消失的 23 个都是噪音,没有出现新的发现。
坦诚地说,同一版本里另外两个修复——HTML 注释和缩进代码块——在这个语料库里出现次数为零。它们可复现,我修了,但没有移动任何数字。上面三个修了。
审计之后的那个 release 还修了别的东西。Agent 指令文件里满是禁止性语句:
Never run npm run release; releases are performed by a human.
→ `npm run release` — no script "release" in package.json
Never read or execute `scripts/deprecated.sh`; it was removed after the migration.
→ reference `scripts/deprecated.sh` does not exist
这两份文档都写得正确。如果你明确告诉 agent 不要使用某个已经被移除的东西,它当然不存在——那是这句话的全部意义。然而写它的人反而是唯一被报警的人。
禁止行不再被扫描为引用,而且这个检查在栅栏外运行,所以 block 里面的 # never edit this comment 不会静默地把下面的真实引用排除掉。
我在三个 linter 里都遇到了完全一样的形状。文本规则看到的是提及,不是执行。一个东西越危险,仔细的作者越可能点名它来声明、禁止或约束它——所以误报集中在写最好文档的人身上。
在整个过程中,测试套件在所有修复之前就是绿的。
--code-blocks 有测试。它们断言开启后第四个扫描器会检查栅栏路径。它们没断言其他三个在跳过它们。栅栏追踪有测试,但没有一份测试输入是奇数标记数的文档。
我写的测试只覆盖了我设想过的崩溃方式。118 个仓库的他人 AGENTS.md 覆盖了我没设想过的。直到我对着它们跑了一遍,我以为我的工具能正常工作。
npx @hyuga/reflint # auto-detects AGENTS.md / llms.txt / CLAUDE.md
npx @hyuga/reflint --code-blocks # also check inside fenced blocks (this works now)
npx @hyuga/reflint --since main # only files changed against a git ref
npx @hyuga/reflint --format json
在 CI 里,这才是重点:
name: reflint
on: [push, pull_request]
jobs:
reflint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hyuga611/reflint@v1
发现的问题会作为内联 PR 注释展示。我不得不修正自己 README 的一处错误:GitHub 在你把这个 check 标为 required 之前不会阻止合并。在那之前它只是一个红叉,任何人都可以点过去忽略它。
Repo: https://github.com/hyuga611/reflint — 变更日志按 release 撰写,包括我犯错的那几个。