nuhuh
v0.1.10
Published
Your agent said Done. nuhuh runs the experiment with fresh tests, real exit codes and actual files, then hands back a receipt it wrote, not one your agent dictated.
Maintainers
Readme
编码智能体几乎每次都用同一句话收尾。"完成了!所有测试都通过。" 有时这是真的。但研究者实测发现,在自我评分的失败运行中, 75.8% 仍然宣称成功。 而且假完成很少出现在 diff 里。它们藏在从未运行过的测试里,从未设置过的 环境变量里,返回 500 的接口里。
nuhuh 不读 diff,也不问模型的意见。它把智能体最后一条消息里的每个声明拿出来, 重新对现实执行一遍。在干净的进程里跑完整测试套件,跑构建,查磁盘上的文件, 调本地接口。然后打印一张 nuhuh 自己写的收据,而不是智能体口述的那张。
🧾 receipt
✅ src/login.ts
src/login.ts exists (33 bytes)
❌ src/login.test.ts
src/login.test.ts does not exist
❌ All tests pass.
ran `npm test` fresh, exit 1 ("Tests: 1 failed, 3 passed")
✅ The build succeeds.
ran `npm run build` fresh, exit 0
2 of 4 claims verified, 2 failed.10 秒上手
npx nuhuh demo # 看它当场抓住一个排练好的假"完成"。零配置,不碰任何东西
npx nuhuh # 在任何项目里,检验你最近一次会话的真实"完成"nuhuh 读取磁盘上已有的 Claude Code 会话日志,并以 Codex rollout 作为回退。 它从最后一条消息中提取完成声明,逐条对照你的工作树验证。MIT 许可,不需要账号, 不需要 API key,没有任何模型调用。任何数据都不会离开你的机器。
门禁模式,让"完成"不再是一种感觉
npx nuhuh init这会安装一个 Stop 钩子。此后每当智能体想要收工,
- nuhuh 从它的最后一条消息中提取声明
- 逐条做实验(新鲜的测试运行、构建、文件、接口、env)
- 只要有假声明就拒绝"完成",并把失败证据原样喂回给智能体,让它回去继续干
- 弹回 3 次后不再争论,把收据交给人类
你不用再当那个在智能体发誓测试通过之后又亲自重跑一遍的人。
nuhuh log 会把门禁实际做过的事一行一条列出来。
用 nuhuh uninit 卸载,用 NUHUH_OFF=1 临时暂停。
默认情况下绿色回执静默通过。设置 NUHUH_RECEIPT=always 后,成功回执也会
打印到会话里,让智能体永远学不会「一句自信的话就够了」。
NUHUH_STRICT=1 更进一步,会弹回不含任何可验证声明的完成宣言并要求证据。
零声明的「完成了!」是我们实测中最常见的虚假完成类型。
委托得越多,越需要它
我们自己的基准显示,前沿模型几乎不会谎报完成(102 次运行 0.0%)。虚假完成来自 更小更便宜的模型(Haiku 4.5 6.0%,Codex 2.1%)。而智能体工作流正朝那个方向走: 一个强编排者把活委托给便宜的工作模型,批量任务在跑,没人盯着的 CI 在跑。 门禁就是委托车道的安全带。便宜的活用机器验证,昂贵的模型留给判断。
它检查什么
| 智能体说 | nuhuh 做 |
| --- | --- |
| "所有测试都通过" | 在干净进程里重新跑完整套件,读退出码而不是文风。还会记录会话新加的 .skip、.only 或 xit,因为不再运行的测试永远不会失败 |
| "构建成功" | 运行构建脚本,退出码说了算 |
| "我创建了 src/x.ts" | 检查文件是否真的存在 |
| "我删除了 legacy.js" 或 "不存在 X" | 检查它是否真的不在了 |
| "localhost:3000 的接口能用" | 真的去调用它(永远只探测本地主机) |
| "我在 .env 里设置了 DATABASE_URL" | 只检查键是否存在,值永远不会进入收据 |
| "我提交了改动" | 读取 git,若被追踪的文件仍有未提交的改动则判定声明失败 |
声明匹配支持英语、韩语、日语和简体中文。模式是一个数据文件, 所以增加语言是一个 PR,不是一个 fork。
为什么不找另一个模型来审查
流行的答案是再挂一个 LLM 当对抗审查员。它有三个测试运行器没有的问题。
| | 第二个 LLM 审查员 | nuhuh | | --- | --- | --- | | 每次检查的代价 | 每次弹回都是一整轮审查的 token | 零 | | 判定 | 意见,在这类失败上只有 AUROC 0.54 到 0.65 | 退出码,完全确定性 | | 知道何时收手 | 不知道,审 25 轮还能挑出新毛病 | 知道,声明要么验证通过要么不通过,同样的失败重复出现就终止循环 |
裁判模型失败是因为它们"依赖自信的收尾语气这类表面完成信号,而不是经过验证的 状态变化"。测试运行器检测失败套件的能力是 1.0。nuhuh 就是一个穿着 Stop 钩子 的测试运行器。
读 diff 的方案有相反的盲区。把 diff 当作事实的审查者看不到 diff 之外的遗漏, 比如没设的环境变量、没跑的迁移、没在监听的服务。而这些恰恰是 nuhuh 去戳的声明。
False Done Rate 基准测试
bench/ 里有一个持续增长、可复现的基准,按 harness 测量"完成"有多经常是假的,
以及其中 nuhuh 抓住了几个、漏掉了几个。地面真值是一组完全不知道 nuhuh 存在的
确定性 check.sh 脚本,所以这个基准也能暴露 nuhuh 自己的盲区。
它已经做到了。第一次真实运行就抓到 nuhuh 的四类误伤(把行号引用当路径、把代码
标识符当路径、删除对象归属错误、把写在 .env.example 里的键冤枉了)。每一类
现在都是永久回归测试。方法论和诚实的局限见 bench/README.md。
它不做什么
- 它无法告诉你代码写得好不好。它告诉你智能体说的和你的机器做的 是否一致。这是一个更小、但可检验的命题。
- 没有安全手段检查的声明会被标为
⚠️ unverifiable,永远不会是failed。 超时什么也证明不了,永远不按失败处理。这个工具宁可漏报也不冤枉, 因为一次冤枉毁掉信任,一次漏报只损失一次检查。 - 它永远只探测 localhost,只读项目内部,只运行项目自己清单里定义的命令, 绝不运行来自智能体文本的命令。
相关项目
- taskmaster 让智能体一直干到它说完成为止,那个完成标记是被信任的。nuhuh 不信任任何它能重新执行的东西。
- tdd-guard 和 probity 在编辑过程中强制流程(测试先行)。nuhuh 在"完成"时刻检验结果。两者可以搭配使用。
- agent-done-or-not 记录智能体选择包装的命令的收据。nuhuh 不需要智能体配合,它从自然语言中提取声明并重新执行。
- backcheck 和 agent-receipts 审计转录里说发生了什么。nuhuh 检验现在什么是真的。
- Claude Code 自带的
/verify把 diff 当作事实并明确不运行测试。nuhuh 为 diff 之外的 bug 而存在。
它会在你的机器上运行什么
任何会执行命令的工具都值得在安装前审计一遍,所以这里是完整清单。
- 它唯一会执行的命令是你项目自己清单里的 test、build、lint 脚本,外加 git 读取。绝不执行来自智能体文本的任何东西
- 唯一的网络访问是收据里可见的 localhost 接口探测。没有遥测,不回传任何数据
- 门禁不可能无限循环。它尊重钩子自身的递归标志,弹回上限 3 次,同样的失败连续出现两次立即收手。每条拒绝消息只有两行,弹回的上下文开销很低
nuhuh init写入的钩子会锁定到安装它的那个版本,门禁不会在停止时去拉取可变的代码。升级到新版是一次明确的操作,先nuhuh uninit再npx nuhuh@<版本> initnuhuh init会先备份你的配置文件,nuhuh uninit无论钩子锁在哪个版本都能干净地还原
环境要求
Node 20 或更高,macOS、Linux 和 Windows 都可以(三个平台都在 CI 里跑)。
Claude Code 会话从 ~/.claude/projects 读取,Codex rollout
从 ~/.codex 读取。新鲜的测试和构建运行使用项目自己的 package.json 脚本,
pnpm、yarn 和 bun 通过锁文件识别。除了 npm 项目,nuhuh 还能识别
Go 模块(go test ./...、go build ./...、go vet ./...)、
Cargo crate(cargo test、cargo build)、pytest 配置(pytest)
以及项目自带的 gradle wrapper。显式的 package.json 脚本始终优先,
并且只会运行标准工具链命令,绝不会用全局安装的替代品。
License
MIT
