NEE's Blog

BMAD 的回顾技能里有一个拒绝下判断的脚本——这个设计值得偷走

September 17, 2026

读 BMAD 仓库里的 src/bmm-skills/ship/bmad-retrospective/(回顾技能),最打动我的一条设计规则是:取证的脚本只测量,从不判断。

scripts/git_evidence.py 负责”这个 epic 实际做了什么”的取证——提交列表、每个文件的增删行数、每个故事(story)的提交归属。它的文档字符串就是一份契约:”This script only MEASURES — it never judges. The model interprets the numbers.”(本脚本只测量,不判断。数字由模型来解读。)判断留给模型,测量保持确定性和可复跑。正是这个切分让整个回顾是可审计的——数字随时可以重跑验证。

有意思的工程细节,全在”别悄悄测错东西”

1. 合并提交单独跑第二遍,绝不并入主变更统计。 合并提交对第一父节点的 diff 会复述它所合入的那些提交的变更量——直接加进按文件的累加里就是双倍计数。所以第一遍遍历全部提交、只累计非合并变更;第二遍用 -m --first-parent --min-parents=2 只测合并,输出到独立的 merge_files 键。

2. 没测到的东西要可见,而不是记零。 merge_count 和 merges_measured 是两个独立的键:不在第一父主线上的合并会被计数但不被测量,这两个数字之间的缺口就是一份刻意可见的声明。二进制文件的变更递增 binary_revisions 而不是按零行处理——不可测量 ≠ 零。

3. 范围校验读起来像一道道伤疤。 脚本拒绝 A...B(对称差——完全是另一个提交集合)、拒绝 ..b、拒绝会被 git 当作选项吞掉的前导 -、拒绝裸的单个版本号。注释的原话:”Anything else silently measures the wrong thing.”(其他任何写法都会悄悄测错对象。)

4. 真实仓库里才会咬人的小细节:

  • --no-renames:否则重命名会变成 src/{a => b} 伪路径,把一个文件的变更量拆散到多个键里;
  • core.quotePath=false 加 surrogateescape 解码:否则两个不同的非 UTF-8 路径会塌缩成同一个键,变更量被悄悄加总;
  • 故事编号按词边界匹配:故事 1-2 不能匹配到提交 11-2;
  • 剥离 GIT_* 环境变量,隔绝调用方的 git 配置干扰;
  • 连 -h 都从 argparse 里移除了——内置帮助文本会往 stdout 打印非 JSON 内容,破坏”stdout 只有 JSON”的契约。

判决规则:同一种纪律用在决策上

SKILL.md 里的裁决规则是同一套纪律:每条发现(finding)必须带出处引用——”一个 diff 里并不真实存在的模式,不是发现,删掉。”(”a pattern the diff does not actually show — is not a finding. Drop it.”)

未完成的故事会强制机器判定为 rejected;人类可以推翻,但推翻会被记录在案——未达标准的 epic “永远不会被悄悄记为通过“。连失败模式都有显式处理:传一个不存在的 epic 编号,返回的空待办列表和”全部完成”一模一样,所以 story_count: 0 被当作”很可能编号打错了”处理——停下来报告,而不是自信地对错误的 epic 做回顾。

可以带走的模式

让缺席可见(”查过且干净”与”从没查过”分开;计数了但没测量的合并单独立键)+ 拒绝解读的测量(脚本只出数字,判断在别处)。

我见过的大多数回顾工具恰好反过来:让 LLM 把一切总结掉——而总结是没法重跑来核对的。

comments powered by Disqus