Bug Hunter(Bug 猎人)
**没有失败测试的断言只是猜测。**本技能报告的所有内容都经过测试证明:在修复前观察到测试为红色,修复后为绿色;任何无法证明的内容都会被丢弃。这就是核心理念,也是其输出值得阅读的原因。
它会修改工作目录、添加测试文件,并默认执行提交。
契约
- 每个修复的 bug 都有一个回归测试,该测试在未修复的代码上被观察到为红色,在修复后变为绿色。
- 每个无法驱动为红色、或被第二个意见反驳的候选 bug 都会被丢弃——其测试也会被删除,绝不会留下一个失败的测试。
- 任何真实但超出范围或无法测试的问题都会被报告,而非修复。
- 完整的测试套件最终状态不会比开始时更差。
工作流检查清单
- [ ] 步骤 0:界定范围 — 仅限暂存的差异
- [ ] 步骤 1:分诊 — 这个提交是否值得审查?
- [ ] 步骤 2:基线 — 在接触任何内容之前运行测试套件
- [ ] 步骤 3:发现候选(使用强模型,要求可达性);如果修改的函数有测试,则在旁边运行突变子代理
- [ ] 步骤 4:证明 — 编写测试,运行它,要求匹配的红色结果
- [ ] 步骤 5:反驳 — 第二个意见尝试否定每个已证明的 bug,并检查每个幸存的测试和死代码断言
- [ ] 步骤 6:修复(预防性问题,限定范围)
- [ ] 步骤 7:验证 — 新测试通过;在步骤 9 之前运行一次完整套件
- [ ] 步骤 8:迭代 — 连续进行,最多 3 次,无需询问;永远不进行第 4 次
- [ ] 步骤 9:最后报告一次;将每个未决决策询问用户(使用交互式选择器——写下来不算问);然后提交(如果安装了 prose,则附带 prose 的追踪标记)或将其留待审查——或者在“调用者提交”模式下,以 `scripts/caller-result.sh` 的输出块结束
永远不要在 git pre-commit hook 内运行此技能。它会写入文件、添加测试并提交——所有这些操作都会与 git 的索引锁冲突。它应该在创建提交之前运行,提交在步骤 9 进行。
一个配套的 hook 会监控那些从未经过此技能的提交:每次命令执行后,它会检查 HEAD 是否移动以及新提交是否带有 Bug-hunter: 追踪标记。因此,追踪标记是唯一重要的标志——没有环境变量,无需记忆,记录存在于提交本身,git log 可以审计。
该 hook 还会对任何非跳过说明的追踪标记提出第二个问题:Bug-hunter-Tree 追踪标记是否与实际落地的树匹配?这种绑定就是 scripts/mint-trailer.sh 在针对真实的暂存树运行时产生的,就在提交之前——请参阅步骤 9 了解详情。一个 Bug-hunter: 追踪标记可以是具有完全正确措辞的真实 git 追踪标记,但仍然是读过此文件后的记忆重述,而不是实际运行任何部分产生的——这是字符串存在检查无法捕获的唯一形式,也是此绑定的目的。
一个落地时没有 Bug-hunter: 追踪标记、或其 Bug-hunter-Tree 绑定不匹配的提交,将在报告中命名,并注明它所在的仓库。报告会将你送到下面的章节,并且不提供修复提交的命令:hook 无法看到你当前所处的位置,它命名的提交通常不是你的 HEAD。
如果你是一个即将提交的 agent
如果另一个技能负责你的提交并在每次提交前运行此技能,让该技能从“提交这个”开始;但仍为“检查这个 bug”运行。如果本次轮次已经声明了一个提交主题,则原样使用该主题。仅添加追踪标记。
你创建的每个提交都经过此技能,并带有 Bug-hunter: 追踪标记。 不是只在被要求时——而是每次都如此。包含代码的提交会进行 hunt。仅包含文档、规格、测试或格式化的提交会跳过 hunt,但不会跳过此技能。“提交这个”、“提交我的更改”、“搞定并提交”,或者你自己在完成任务后决定提交,所有这些都意味着:暂存更改,运行此技能,然后提交。分诊(步骤 1)决定一个给定的提交是否值得审查,因此在不适用时运行它总是廉价的。
在你的会话中自行运行此技能。 编写更改的 agent 应运行此技能。不要将整个运行交给一个子 agent,然后让子 agent 启动自己的发现器、反驳器和突变 agent:运行需要一个保持存活、收集幸存者并恢复 agent 的编排器,而嵌套的委派会在其子任务运行时不断结束自己的轮次。按照步骤所述委派单个步骤,才是它应有的运行方式。
不要规避此技能
一个标题,而非五条规则,因为一个试图绕过检查的 agent 总能可靠地找到分别陈述的规则之间的缝隙。这里的每一行都是在某个 agent 绕过它要关闭的规则之后添加的。
1. 每次运行,都要大声说出你正在做什么。 在用户可以看到的回复中写一行:“在 N 个文件上运行 bug-hunter”,或 “跳过 bug-hunter — <原因>” 。不要写在你的推理中——要写在回复中。跳过而不说什么,看起来就像运行了但什么也没发现。一个明确说明的跳过可以用四个字推翻;一个未说明的跳过是你替用户做的决定。如果你跳过了,提交仍然要附带追踪标记——使用 commit-with-trailer.sh -F msg.txt "skipped — <原因>",如步骤 9所示。两者都要,不是二选一。在调用者提交模式下,跳过信息会写入结果脚本(当调用者提交时)。
2. 永远不要请求许可去运行它。 触发条件是代码的一个属性,而不是一个时刻:如果自上次通过且干净的检查之后,代码又有新的写入,则应该进行一次检查。 在提交之前、迭代之间、用户要求的修复之后、重构之后、已经落地的提交之后——并不是不同的情况,全都是“代码被写入了”。列出具体时刻只会让人寻找不在列表中的那个时刻。
3. 只有当你已经开始执行时,宣告才有效。 “下一步将运行它,除非你想停在这里” · “如果你满意,我会启动它” · “这些修复需要它们自己的检查——想让我运行它吗?” 每一次都以检查未运行而结束轮次,这就像直接询问一样,把决定权交还给了用户。如果你用将来时描述一次运行,那你还没有开始,而这就是 bug。
4. 迭代连续进行,并且需要报告。 最多三轮,中间无需检查,也没有临时报告——迭代之间的报告会诱使提前停止,并使未完成的运行看起来像已完成。请参阅步骤 8 和 9。
三轮是预算,不是起始报价。没有第四次迭代。 “想让我再运行一轮吗?” · “迭代 3 自身的修复从未被审查——我应该做第 4 轮吗?” 两者都在用户最可能说“不”的时刻,把预算已经做出的决定交还给用户。达到上限的运行会在根因报告(步骤 9)中结束,每个原因提供一个选择,其中第一个选择是针对该原因的新运行,从迭代 1 开始。这并不意味着规则 2 被暂停:对迭代 3 的修复进行一次检查确实是必要的,而且仍然是必要的——耗尽的是在此运行内进行检查的预算,这就是为什么它变成一次新运行,以及为什么哪个原因得到它由用户决定。也不是规则 3 的逆转:在预算内,检查是必需的,你可以不问就开始;到了上限,运行结束,剩下的就是一个决定,以选项的形式提供,而不是作为一个开放性问题。
5. 这些不是合理的理由。 每一个都有过被一个能干模型用来跳过此技能,然后提交的代码中引入了 bug:
- 更改很小 — 单行更改经常出错
- 我对这段代码很有信心 — 这个技能发现的每个 bug 的作者也都这么想
- 用户正在等待 / 我们正在快速推进 — 这正是质量被妥协的时刻,也正是此技能存在的意义
- 会话很长,这个很贵 — 一次没发现任何问题的运行是廉价的,分诊在最昂贵的部分之前就停止了大多数提交,而且长时间的运行会在后台进行(步骤 3)
- 我已经在相关的代码上运行过了 — 你没有在这段代码上运行过
- 这是技能自身的机制,不是真正的代码 — 这是真正的代码,并且此技能自身历史上最糟糕的 bug 恰恰出现在这些机制中
- 用户显然在看,如果他们介意会反对的 — 他们看的是输出,而不是输出的缺失
制造 bug 然后因为检查不方便就继续前进,正是此技能要阻止的具体行为。对这个技能做这件事不是讽刺,它就是个 bug。
谁来决定成本。 一次完整运行几分钟;一次糟糕的运行半小时。安装此技能的人已经权衡了更慢的提交与 bug 到达主分支的风险。一个悄悄决定现在检查太慢的 agent 并没有做出工程判断——它替一个不在场的人覆盖了那个决定,而且方向是对自己工作量有利的方向。如果成本真的在当前很重要,用一句话说出来,让他们选择。
谁的决定
两个简短的列表,因为两种失败都发生在它们之间的边界:对第一个列表中的问题提问会让用户习惯性略过,而为用户决定第二个列表中的问题则是悄悄地做出选择 3。
你的决定,永远不要问。 是否运行以及何时运行;一个候选是被证明、反驳还是丢弃;如何在更改范围内修复一个已证明的 bug;是否迭代(在预算内);运行测试套件;提交一个干净结束、没有未决问题的运行——除了在调用者提交模式下,提交由调用者负责。
用户的决定,永远要问。 对什么采取行动:每个根因和每个建议(现在修复 / 记录下来 / 跳过),每个中止条目的未解决项,当一个已报告未修复的内容改变了提交的含义时是否继续提交(仅报告模式),以及步骤 0 的停止——一个文件中发生分歧的暂存和未暂存编辑保留哪个。
当你确实要问时,使用宿主交互式选择器(在 Claude Code 中,使用 AskUserQuestion 工具):任何带有离散选项的问题都应通过它提出,每个决定一个问题,按严重性排序,永远不要埋在一份长篇报告末尾的散文中。纯文本是那些没有选择器的宿主的备用方案,不是默认方案。选择器也是这里唯一结构性强于建议性的东西——一个打开选择器的轮次不能结束,直到用户回答——这就是为什么下一条规则很重要。
“问过”意味着用户看到了它。 一个问题在被用户读到或通过选择器回答之前,不算是被问过——而不是一个 agent 把它记下来就算。如果此技能在一个向另一个 agent 报告的 agent 中运行,它的步骤 9 报告和问题会到达那个 agent,而问题尚未被问过:这一步移动了选择器,却没有解决这个问题。所以:
- 如果你生成的 agent 在自己的更改上运行了此技能,它的报告是你步骤 9 的输入,不是你的步骤 9 输出。该报告中的每个未决决策都要在你的下一条回复中,其他事情之前展示给用户——通过你的选择器,每个决定一个问题,或者在没有选择器的情况下,逐字使用报告编号的块。“运行中止”或“它找到了三个根因”而下面没有任何选项,这是在转述报告而不是交付它;如果你在写那个句子,那就是 bug。在另一个任务完成的同时,将这个决定保留到下一轮,就是替用户做了选择 3——其他任务要么等待,要么回复同时包含两者。
- 如果你正在运行此技能,而它向另一个 agent 报告,你没有能够触达用户的选择器。以其格式打印整个报告——而不是它的摘要——并附带移交页脚(参考文档),告诉持有它的 agent 哪些问题仍需提出。明确说明哪些决定是开放的,以及你无法询问它们。在调用者提交模式下,结果块的决策列表会替换页脚(当调用者提交时)。
一次运行在它的问题被呈现在用户面前之前不算结束,无论哪个 agent 负责将它们送达。
步骤 0 — 范围界定
被审查的更改是暂存的差异:
git diff --cached --stat
git diff --cached
对每个更改的文件,阅读整个文件,而不仅仅是差异片段——bug 通常出现在新代码与周围代码的交互中。
如果任何文件同时出现在暂存差异和未暂存差异中,则停止。 你将在测试工作目录时推理索引,而两者不一致。告诉用户哪些文件存在分歧,并请他们暂存或还原这些编辑。永远不要使用 git stash 来绕过此问题——用户有未提交的工作,而此技能即将在其上写入文件。
其他文件中的未暂存更改则没有问题。在报告中记录它们。
不要寻找规格、文档或现有测试中的 bug。对这些文件的更改仍然是值得阅读的上下文。
步骤 1 — 分诊
分诊根据差异做出决定,而不是根据提交信息。 对于大约最多 300 行更改的差异,由你自己在当前的循环中进行分诊:启动一个子 agent 的成本比阅读这么大小的差异还要高。对于更大的差异,可以委托给一个小型、便宜的模型进行一次快速通过。无论哪种方式,该通过都会获得文件列表、差异以及用户是否要求了检查点(见下文)。要求返回 REVIEW 或 SKIP 以及一行原因。不要先要求提交信息,也不要将主题前缀作为信号——此技能不强制任何提交约定(参考文档)。
在以下情况下返回 SKIP:
- 差异中没有除文档、规格、测试、锁定文件