code-review 精读:两条轴各开一个子代理,最后谁也不许评总冠军
code-review 把一次评审拆成两问——代码有没有守住这个仓库明文写下的规范(Standards),它有没有忠实实现源头那份 issue 或 spec(Spec)——两条轴各开一个并行子代理,以免互相污染上下文,最后并排贴出来,并明确禁止合并、重排和跨轴选冠军。本文走一遍它的真实触发路径(钉固定点 → 找 spec → 找规范 → 并行 → 聚合),讲清它为什么把标准的定义权交给仓库文档、为什么先 `git rev-parse` 验完 ref 才肯分叉,以及那两个写进文档却至今没修的坑:子代理会再次调用它自己,以及它和 Claude Code 内置的 /code-review 撞名。
本篇属于系列 读懂最优秀的开发技能是怎么写的(Matt Pocock skills 逐技能精读) · 第 19 篇
(本文读的是 mattpocock/skills 快照 5beb35b,2026-10-19。引用原文都能按文中路径回仓库核对。这是「逐个技能精读」系列的第十九篇,聚焦 code-review 一个技能。)
它怎么工作:先钉一个固定点,再分两条轴跑
skills/engineering/code-review/SKILL.md 一共 89 行,第 6 行就把要做的事拆成了两问:
- Standards: does the code conform to this repo’s documented coding standards?
- Spec: does the code faithfully implement the originating issue / spec?
触发方式两种:敲 /code-review,或者你说要评审一个分支、一个 PR、进行中的改动、任何「review since X」。前置条件也写在正文里——issue tracker 应该已经被提供给你(那是 setup-matt-pocock-skills 干的活),没有就让你先去跑它。
第一步是钉固定点。用户说的就是固定点:一个 SHA、分支名、tag、main、HEAD~5 都行;没说就问,不猜。然后一次性捕获 diff 命令 git diff <fixed-point>...HEAD(三点点,对着 merge-base 比),再用 git log <fixed-point>..HEAD --oneline 记下提交列表。真正值得抄的是紧接着这一句——它在分叉之前设了一道最便宜的闸:
Before going further, confirm the fixed point resolves (
git rev-parse <fixed-point>) and the diff is non-empty. A bad ref or empty diff should fail here, not inside two parallel sub-agents.
第二步找 spec,按四级顺序:提交信息里的 issue 引用(#123、Closes #45,通过 tracker 配置去取)→ 你作为参数传进来的路径 → docs/、specs/、.scratch/ 下按分支名或功能名匹配的 spec 文件 → 直接问你。如果问到你说确实没有,Spec 轴就跳过、报告里写 “no spec available”,绝不从代码里反推出一份需求来评。
第三步找规范来源:把这个仓库里所有记录「代码该怎么写」的文件搜出来,有 CODING_STANDARDS.md 或 CONTRIBUTING.md 就必须在列。
第四步把两个子代理同时发出去(前台),再聚合它们返回的报告。这里有个容易漏的细节:Standards 子代理的 prompt 里必须把基线原文整段贴进去,因为原文特意注明 the sub-agent has no other access to it。
第五步聚合,也是整个技能最反直觉的地方:两份报告并排贴在 ## Standards 和 ## Spec 两个标题下,不许合并、不许重排(Do not merge or rerank findings)。结尾只给一行汇总:每轴的发现条数,以及每轴内部最严重的那条;跨轴不许选冠军——Don't pick a single winner across axes: that's the reranking the separation exists to prevent.
所以产物既不是一份清单,也不是一个分数,而是一张并排的双栏体检表。
它和「一般技能」的差别:把标准的定义权交出去,把评分权拆开
一般评审技能会把一套通用的「最佳实践」写在自己身上,然后输出一份混合清单加一个总评分。code-review 两件都反过来。
第一,标准的定义权在仓库手里。技能正文说得很直白:去找这个仓库自己文档化的规范,仓库文档就是 Standards 轴的一手来源。技能自己只带一条兜底基线,并且把优先级写死:
The repo overrides. A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell.
第二,它拒绝给出单一结论。原文专门用一节 Why two axes 解释为什么不能合并,理由是两条轴会互相遮蔽:
Code that follows every standard but implements the wrong thing → Standards pass, Spec fail. Code that does exactly what the issue asked but breaks the project’s conventions → Spec pass, Standards fail.
第三,并行子代理的目的是隔离上下文,不是提速。原文给的理由是 so they don't pollute each other's context——两条轴看到的证据天然不同,混在一个上下文里就会互相说服。
第四,决定权留在人手里:spec 找不到就问人,不猜;固定点没给就问人,不猜。
第五,它明确不做「找 bug」。文档里把这一点当成职责边界:在 diff 里抓空指针、竞态、off-by-one 是 harness 内置评审的活,这两条轴管的是「写对了没」和「写的是不是那件事」。
它有意识摒弃了什么
一、摒弃了单一评分和合并排序。 这是全篇最硬的一条:两轴分开跑,最后并排贴,故意不做归一。理由不是偷懒,而是合并就等于把一条轴的失败藏进另一条轴的通过里。
二、摒弃了自带一套硬规范。 技能不定义什么是好代码,它去读仓库写的;连那条基线也承认自己是软的——Always a judgement call. Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation. 外加一条:工具已经管的事情一律不提(skip anything tooling already enforces)。
三、摒弃了「全量提审」。 它只看一个固定点到 HEAD 之间的 diff,不看整个代码库——那是 improve-codebase-architecture 的位置。
四、摒弃了未提交的改动。 三点点 diff 从 merge-base 算起,暂存区和工作区都看不见,所以文档给的操作顺序是:先 commit,再 review,之后再 amend 或补一个 fixup。
五、没摒弃、也没修的两个坑,都如实写在文档里。 其一是子代理会再次调用它自己:两个子代理的 brief 里没有禁止再委派,于是 Standards 或 Spec 的 run 里可能再次发现 /code-review、再扇出去,有报告记录到 50 个以上的 agent,官方修法至今是用户自己在 fork 上补一句「不许再调用、不许再派子代理」。其二是撞名:Claude Code 自带一个同样能打 /code-review 的技能,装法不同赢家不同;作者的处理方式不是改掉,而是把「哪个赢、怎么绕」写清楚。这两处比技能本身更有信息量:它接受了「技能作者管不了 harness,也管不了调用方」这个事实,转而把这个事实写进文档。
原文证据
两条约束基线的原文,一字不改:
- The repo overrides. A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell.
- Always a judgement call. Each smell is a labelled heuristic (“possible Feature Envy”), never a hard violation. Like any standard here, skip anything tooling already enforces.
基线本身是从 Fowler 的 Refactoring 第 3 章取来的 12 条坏味道,从 Mysterious Name 到 Refused Bequest(skills/engineering/code-review/SKILL.md:44-56),每条都写成 what it is → how to fix——一个发现天生带着一个修法,而不是只带一句抱怨。
分叉前的那道闸(第 23 行):
Before going further, confirm the fixed point resolves (
git rev-parse <fixed-point>) and the diff is non-empty. A bad ref or empty diff should fail here, not inside two parallel sub-agents.
聚合规则(第 78、80 行):
Present the two reports under
## Standardsand## Specheadings, verbatim or lightly cleaned. Do not merge or rerank findings, because the two axes are deliberately separate (see Why two axes). End with a one-line summary: total findings per axis, and the worst issue within each axis (if any). Don’t pick a single winner across axes: that’s the reranking the separation exists to prevent.
可迁移结论
- 把评判标准放回仓库,技能只带兜底并写死优先级。 「仓库覆盖基线」这一句让技能可以被任何团队直接装上而不用改;反过来,把规范写死在技能里,等于强迫所有人按你的口味写代码。
- 每个发现都强制带引用。 Standards 轴引用「哪个文件的哪条规则」或「哪条气味 + 哪段 hunk」,Spec 轴引用「哪一行 spec」。引用不产生正确性,但它让每条结论可核对——这正好对应文档提醒的那句:子代理的输出是假设,不是证据。
- 并行子代理是上下文隔离手段,不是提速手段。 想让两条互相独立、互不污染的证据链同时存在,才值得分叉。
- 分叉之前先验证输入。 坏 ref 和空 diff 要在人面前失败,而不是在两个子代理里各失败一次。
- 多轴评审要显式禁止合并结论。 写下「不合并」还不够,还要写下「也不许跨轴选冠军」,否则聚合那一步会自动完成合并。
- 判断类发现一律降级成「可能的 X」。 把气味、把启发式当成硬违规,是评审技能最常见的自我腐化方式。
code-review 的结构可以一句话概括:判据从仓库来,结论按轴分开存。 它不追求给出一个答案,它追求让「写对了没」和「写的是不是那件事」各自留下证据,并且谁也不许替谁说话。
讨论
用 GitHub 账号留言;评论保存在公开仓库chengshu-blog-discussions的 Discussions 里。也可通过 RSS 订阅后续文章。