Lesson 0013 · Engineering · 人和 AI 都能启动(model-invoked)· 流程课

code-review:两条轴各审各的

implement 刚把一个 Issue 建完,或者同事给你发来一个 PR——你盯着几百行 diff(两个 git 状态之间逐行对比出的改动清单),心里其实是两个独立的问题: 它写得对吗(有没有按这个仓库的规矩写代码),和它做的是那件事吗 (Issue 里要的东西是不是真做出来了)。code-review 就是把这两个问题拆成两条轴、 各派一个子代理去审、最后并排摆给你看的那个 skill。它钉在 0001 系统地图里主 build 链的末尾: implement 在 commit 之前一定会跑它一遍;你也可以随时单独启动它,审一条分支、 一个 PR、一段还没做完的改动。它只读不写——审完留下的是一份对话里的报告,不多一个文件。 学完这节课,你能说清两条轴各自吃什么原料、五个步骤每步在防什么错、报告为什么绝不合并排名, 以及行为不对时该去改 SKILL.md 的哪一段。

1. 它在整个系统里站在哪

0001 把已发布的 22 个 skill 分成配置层、编排层、纪律层。code-review 的位置一句话就能说清: 它是主 build 链的收尾审查。ask-matt 在描述主 flow 第 3 步时写得很明白: implement 逐 Issue 构建、内部驱动 tdd 走红绿循环,然后在 commit 之前 「closes out by running /code-review」——用一次两条轴的审查给这次构建收尾。 同一段还给它留了第二个入口:你想审一条分支或一个 PR 时,随时可以单独把它拿出来用。

主 build 链的收尾段:

  grill-with-docs → to-spec → to-tickets → implement
                                                │  内部驱动 /tdd,逐片构建
                                                ▼
                                           构建完成
                                                │  commit 之前
                                                ▼
                                         code-review ──→ 报告(只存在于对话里)
                                                ▲
        第二个入口:你手动 /code-review,审分支、PR、半成品改动

看依赖关系,要分两个方向数:

它和近邻 tdd 不要搞混:tdd构建中的纪律(先写测试再写实现, 在约定好的接缝上测行为),code-review构建后的审查(diff 已经成型, 拿两把尺子量一遍)。一个管「怎么写」,一个管「写完验收」。

地图:0001 · 路由原文:ask-matt/SKILL.md 主 flow 第 3 步 · 调用方:implement/SKILL.md · 相邻课:0011 implement0012 tdd

2. 调用方式与触发场景

先定性:code-reviewmodel-invoked 的——人和 AI 都能启动。 判据有两个,都查得到:它的 SKILL.md frontmatter 里没有 disable-model-invocation: true(对比 implementask-matt 这些只能人启动的 skill,frontmatter 里都带着这一行);它的 agents/openai.yaml 里也只有界面显示名「Code Review」,没有禁用隐式调用的 policy。 所以你有三种方式让它跑起来:

  1. 你直接启动:输入 /code-review,最好顺手给出固定点 (比如 /code-review main)。这是审 PR、审分支、审半成品改动的标准入口。
  2. AI 主动伸手:frontmatter 的 description 写明了触发条件—— 「Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X"」。你用自然语言说「帮我看看这个分支」「review 一下 main 以来的改动」, 它就会自己被加载。
  3. 被别的 skill 用正文调用:implement 在收尾时通过正文里的一句 「use /code-review」把它拉进来(0001 讲过,这叫 prose 调用)。

触发场景对不对,看这张表:

情境 该用 code-review 更该去哪
一个 Issue 刚建完,commit 前想验收一下这次改动 (这正是 implement 收尾时自动做的事)
审别人发来的 PR,或审自己「main 以来」的一串提交 (给出固定点,两条轴各审一遍)
代码还没写,想先写测试再写实现地把某个行为做出来 不该(那是构建纪律,不是审查) tdd(0012)
线上有 bug,某个行为坏了、或者偶发失败 不该(审查 diff 不治病) diagnosing-bugs(0015)
issue tracker 里堆了一堆别人报的 bug 和请求,还没分拣 不该(那是给 Issue 换状态、写成可执行单元的活) triage(0014)
觉得代码库整体形状在变差,想找重构候选 不该(它只审一个 diff,不做全库扫描) improve-codebase-architecture(0017)
不知道该走哪条流程 不该 ask-matt(0019)

触发条件原文:skills/engineering/code-review/SKILL.md 的 frontmatter · 调用面:code-review/agents/openai.yaml · 人读文档:docs/engineering/code-review.mdaihero.dev/skills-code-review

3. 核心设计:两条轴,绝不合并

整个 skill 只围绕一个设计决定:把审查拆成两条互相正交的轴。 「轴」就是一把独立的尺子,各量各的,量完不混:

为什么要拆开?SKILL.md 的「Why two axes」一节给了两个足以说服人的组合:

如果两把尺子的读数揉成一句「总体不错」,这两种情况都会被平均掉——一条轴的失败被另一条轴的通过掩盖。 所以这个 skill 把「分开报告」上升为硬性纪律,并在聚合步骤里明文禁止合并与跨轴排名(第 7 节细讲)。

一句话定性 code-review 是一次只读的、双轴的 diff 审查: 以你给的固定点为基准,Standards 和 Spec 两把尺子各量一遍,并排报告,永不合并。 它改代码、写文件、替你决定修不修—— 它只负责把「写得对吗」和「做对事吗」两个问题的答案分别摆清楚。

权威原文:code-review/SKILL.md 开篇与「Why two axes」一节 · 叙事版:docs/engineering/code-review.md 的「Two axes, never merged」

4. 第 1 步:钉死固定点,错了就地失败

审查总得有个对比基准。固定点(fixed point)就是你指定的一个 git 参照物—— 一个 commit 的 SHA、分支名、tag、mainHEAD~5 都行; diff 的另一端永远是 HEAD(你当前分支的最新提交)。 用户说了什么,什么就是固定点;用户没说,这一步必须开口问,不许猜。

钉死之后,立刻把两样东西一次性抓在手里:

git diff <fixed-point>...HEAD          # 三点:对比 merge-base
git log <fixed-point>..HEAD --oneline   # 两点:列出这段范围内的提交
git rev-parse <fixed-point>             # 先验证这个参照物真的存在

三点的写法是考点。merge-base 是你的分支和固定点所在分支的分叉点 ——三点 diff 比的是「分叉点 → 你现在的 HEAD」,所以你看到的只是这条分支自己引入的变化。 如果误用两点(..),主干上别人后来提交的代码也会被算进 diff, Standards 和 Spec 两个子代理就会去审查一堆根本不是这次工作的代码。

这一步最后一条纪律是 fail fast(快速失败):先用 git rev-parse 确认固定点解析得出来,再确认 diff 非空。参照物写错了、或者 diff 是空的, 必须在这里就报错停下——不能等到两个子代理已经并行撒出去了, 才在它们各自的环境里分别炸一次。错误发现得越早,定位成本越低,这是整个第 1 步存在的理由。

原文:code-review/SKILL.md「### 1. Pin the fixed point」

5. 第 2 步:Spec 轴的原料从哪来

Spec 轴要回答「做对事吗」,前提是找得到「当初要做的事」。 这里用上 0001 和 CONTEXT.md 的统一词汇:Issue tracker 是存放 Issue 的工具 (GitHub Issues、Linear、或本地 .scratch/ 目录下的 markdown 约定), Issue 是里面一条被跟踪的工作单元。这个 skill 按固定顺序找 spec, 找到即停:

优先级 去哪里找 具体怎么做
1 提交信息里的 Issue 引用 在 commit message 里找 #123Closes #45、GitLab 的 !67 这类引用,然后按 docs/agents/issue-tracker.md 里写的工作流去 Issue tracker 把这条 Issue 的内容取回来
2 用户当参数传进来的路径 你启动 skill 时直接给了 spec 文件路径,就用它
3 仓库里的 PRD / spec 文件 docs/specs/.scratch/ 下找与分支名或功能名匹配的 spec 文件
4 开口问用户 以上都没有,就问 spec 在哪;如果用户说根本没有 spec,Spec 子代理整个跳过,报告里注明 "no spec available"
没有 spec 不是错误,是一种合法状态 第四级的写法很讲究:找不到 spec 时,Spec 轴跳过并明说缺席, 而不是让子代理从提交信息里「推断」出一份需求来照着审。 凭空补写的需求会让 Spec 轴的报告看起来一本正经、实际上全是编的—— 这恰好违背了「Spec 轴的每条结论都要引用 spec 原文」的纪律(第 7 节)。 顺手改的小工程本来就没有 spec,这时 code-review 退化成只有 Standards 轴的审查,完全能用。

还有一个前置条件要记牢:第一级「去 Issue tracker 取 Issue」依赖 docs/agents/issue-tracker.md 这个文件——它是 setup-matt-pocock-skills 在配置阶段写进你的仓库的。SKILL.md 开头明说:这个文件缺失就先跑 /setup-matt-pocock-skills(0002 那节课讲的就是它)。

原文:code-review/SKILL.md「### 2. Identify the spec source」 · 前置配置:0002 setup-matt-pocock-skills · spec 的生产者:0009 to-spec0010 to-tickets

6. 第 3 步:Standards 轴的两层来源与 12 条异味基线

Standards 轴要回答「做得对吗」,它的尺子由两层叠成:

  1. 仓库自己写下来的标准:任何记录「这个仓库的代码该怎么写」的文件, 比如 CODING_STANDARDS.mdCONTRIBUTING.md。这层因仓库而异。
  2. 异味基线(smell baseline):一组固定携带、开箱即用的代码异味清单, 出自 Fowler《重构》(Refactoring)第 3 章。代码异味 (code smell)是那本书提出的一类启发式信号——「看着不对劲、多半该改」, 是提醒而不是报错。哪怕仓库什么文档都没写,这层也永远在。

两层之间靠两条规则绑死,缺一条这套机制就会咬人:

6.1 12 条异味,每条都是「是什么 → 怎么修」

基线的 12 条异味逐条列在下面,按 SKILL.md 原文压缩。 每条的结构都一样:先说什么算这条异味,再给修复方向。子代理拿到 diff 后逐 hunk (hunk:diff 里一段连续的改动块)对照这张表。

异味 它是什么(在 diff 里长什么样) 修复方向
Mysterious Name
神秘命名
一个函数、变量或类型的名字,看不出它做什么、装了什么 改名;如果想不出一个诚实的名字,说明设计本身就含糊
Duplicated Code
重复代码
同一种逻辑形状,在这次改动的多个 hunk 或多个文件里重复出现 把公共形状抽出来,两处都调用它
Feature Envy
依恋情结
一个方法伸手读另一个对象的数据,比读自己的还多 把这个方法搬到它羡慕的那个数据所在的对象上
Data Clumps
数据泥团
同样几个字段或参数总是结伴出现——一个想出生的类型 把它们捆成一个类型,之后传这个类型
Primitive Obsession
基本类型偏执
用一个基本类型或字符串,顶替一个本该拥有自己类型的领域概念 给这个概念一个属于它自己的小类型
Repeated Switches
重复的 switch
对同一个类型的同一串 switch/if 级联,在改动里反复出现 用多态替换,或者抽一张两处共享的映射表
Shotgun Surgery
霰弹式手术
一个逻辑上的改动,逼着你在 diff 里散开修改很多文件 把「总是一起变的东西」收拢进同一个模块
Divergent Change
发散式变化
一个文件或模块,因为好几个互不相关的原因被修改 拆开,让每个模块只为一个原因变化
Speculative Generality
投机式泛化
为 spec 里并不存在的需求,预先加上的抽象、参数或钩子 删掉,内联回去,等真实需求出现再说
Message Chains
消息链
长长的 a.b().c().d() 导航链,调用者不该依赖这么深的走法 在第一个对象上提供一个方法,把整段走法藏在它后面
Middle Man
中间人
一个类或函数,大部分时候只是把调用继续转发给别人 砍掉它,直接调用真正的目标
Refused Bequest
拒绝继承
一个子类或接口实现者,忽略或覆盖掉了它继承来的大部分东西 放弃继承,改用组合
和 0005 的词汇是亲戚 注意 Shotgun Surgery 的修复方向(「把一起变的收进同一模块」)和 Divergent Change 的 (「每个模块只为一个原因变化」),就是 0005 codebase-design 里 locality (局部性:变更和知识集中在一处)的另一种说法;Speculative Generality 则和 「一个 adapter = 假想的接缝」同向——别为不存在的需求预建结构。 这些 skill 共用一套价值观,词汇课上学的词在这里会反复回响。

原文:code-review/SKILL.md「### 3. Identify the standards sources」 · 呼应:0005 codebase-design

7. 第 4、5 步:并行子代理与聚合红线

7.1 第 4 步:一条消息,两个子代理

两条轴各派一个子代理(sub-agent:主 agent 派出去、各自独立干活的 agent), 而且是并行的——SKILL.md 的原文是「Send a single message with two Agent tool calls」,一条消息里同时发出两个调用,两个都用 general-purpose 类型的子代理。并行的理由不是省时间,是隔离: 两个子代理各自抱着干净的上下文进场,Standards 的结论不会影响 Spec 的判断,反过来也一样—— 这正是第 3 节「绝不合并」在执行层面的落实。

每个子代理的 prompt 里必须装什么,SKILL.md 列得很细:

子代理 prompt 里必须带的东西 任务书(brief)的要点
Standards 完整的 diff 命令和提交清单;第 3 步找到的仓库标准文件清单;整份异味基线原文照贴——子代理没有别的渠道拿到它,不贴就等于没给尺子 逐文件 / hunk 报告:(a) 每处违反明文标准的地方,引用出处(哪个文件的哪条规则);(b) 每条发现的基线异味,报名字并引用 hunk。必须区分硬性违规与裁量项,声明仓库标准优先、跳过工具已强制的。400 词以内
Spec diff 命令和提交清单;spec 的路径或取回来的内容 报告三类问题:(a) spec 要求了但没做或只做了一半的需求;(b) diff 里做了但没人要求的行为——这叫范围蔓延(scope creep,悄悄超出需求边界的多余工作);(c) 看起来做了、但实现看起来是错的需求。每条结论必须引用 spec 原文那一行。400 词以内

两个细节值得记住:其一,400 词上限写在两个任务书里,是防止子代理把 「审查报告」写成「重构论文」的缰绳;其二,第 2 步如果已经确定没有 spec, Spec 子代理根本不派,只在最终报告里注明——省一次调用,也堵住编需求的路。

7.2 第 5 步:聚合的两条红线

两个子代理交回报告后,主 agent 只做整理,不做裁决:

  1. 把两份报告分别放在 ## Standards## Spec 两个标题下, 原文照摆,最多轻度清理排版。
  2. 结尾给一行总结:每条轴各有多少条 findings、本轴内部最严重的是哪条(如果有)。

两条红线明文写在 SKILL.md 里:不合并、不重排(do not merge or rerank findings), 以及不跨轴选总赢家(don't pick a single winner across axes)。 跨轴排名正是两条轴之所以要分开的理由想要防的事——一旦说一句「总体还行」, 一条轴的失败就被另一条轴的通过平均掉了(第 3 节的两种失败组合)。 总结行里「每轴最严重一条」是允许的,因为那是轴内的比较,没有跨轴。

原文:code-review/SKILL.md「### 4. Spawn both sub-agents in parallel」与「### 5. Aggregate」

8. 跑完一次长什么样

把五步串起来看一个虚构的例子。假设你在分支 feature/invite-emails 上, 输入 /code-review main

  1. 钉固定点:git rev-parse main 通过;git diff main...HEAD 拿到 3 个文件、约 200 行的 diff;git log main..HEAD --oneline 列出 4 个提交, 其中一个提交信息里写着 Closes #45
  2. 找 spec:命中第一级——按 docs/agents/issue-tracker.md 的流程 取回 Issue #45,正文里有一条「invite links expire after 7 days」。
  3. 找标准:仓库里有 CODING_STANDARDS.md,其中一条是 「错误必须包一层领域错误再抛出」;再叠上 12 条异味基线。
  4. 派子代理:一条消息发出两个 general-purpose 子代理, Standards 的 prompt 里照贴了基线全文,Spec 的 prompt 里是 Issue #45 的内容。
  5. 聚合:两份报告并排摆出,结尾一行总结。

最终报告的形状大致是(以下为教学用虚构示例):

## Standards
- src/invite/send.ts:42 — 违反 CODING_STANDARDS.md「错误必须包一层领域错误」:
  直接把 axios 的原始错误抛给了调用者。(硬性违规)
- src/invite/send.ts:88 — 可能的 Feature Envy:buildEmail() 读了 user 对象的
  7 个字段,自己所在模块的字段一个没用。(裁量项)

## Spec
- 缺失:Issue #45 要求「invite links expire after 7 days」,
  diff 中没有任何与过期时间相关的实现。
- 范围蔓延:新增了 spec 未要求的 Slack 通知开关
  (spec 中无对应行)。

—— Standards 2 条(最重:错误未包装);Spec 2 条(最重:过期时间未实现)

注意这份报告的形状纪律:两节分开、每条都带出处(标准的文件名加规则、spec 的原文行)、 异味标着「可能的」、总结行按轴分开说、没有一句跨轴的总评。 拿到它之后怎么行动是你或下一个流程的事——这个 skill 到此为止。

9. 会留下什么、用完接什么、想微调改哪里

9.1 副作用:只读,一个字都不写

对象 会动它吗 说明
你的仓库文件 不改 全程只跑 git diff / git log / git rev-parse 这类只读命令;SKILL.md 没有规定写任何文件
Issue tracker 只读 只为取 spec 去读 Issue 内容;不写评论、不改状态、不新建 Issue
你的代码 一行不改 报告指出的问题由你或后续流程去修,它自己不动手
对话上下文 写入 唯一的产物就是对话里的那份双轴报告;两个子代理各消耗一次并行调用

9.2 用完之后,下一步去哪

  1. 报告里有 findings → 逐条修掉,然后固定点不变、指着新的 HEAD 再跑一次 /code-review,直到两轴都干净或你都认了。
  2. 它是在 implement 流程里被自动跑的 → 通过之后,commit 是 implement 的职责(它的 SKILL.md 最后一行:「Commit your work to the current branch」),不是 code-review 的。
  3. Spec 轴报了 "no spec available" → 如果这次改动本该有需求出处, 回头用 to-spec(0009)补一份 spec 再审;如果只是随手小改, 接受只有 Standards 轴的结果就好。
  4. 审的是别人的 PR → 报告贴不贴回 PR、怎么贴,由你决定; 这个 skill 不替你对外发任何东西。

9.3 行为不对时,去改哪个文件的哪一段

这个 skill 的全部行为都装在唯一一个 SKILL.md 里(没有 sibling 参考文件), 所以微调入口很好找——按下表对号改对应小节,别去改无关的地方:

症状 code-review/SKILL.md 的哪段 不要误改
报告把两条轴揉在一起、给了跨轴总排名 「### 5. Aggregate」的禁止句 + 「## Why two axes」的理由 两个子代理任务书里的 400 词上限
基线异味被当成硬性违规来报告 「### 3. Identify the standards sources」的 "Always a judgement call",以及第 4 步 Standards 任务书里对应的区分要求 12 条异味的定义本身(那是 Fowler 的原义,不是纪律问题)
仓库明文认可的做法仍被基线挑刺 同一节的 "The repo overrides." 一句 异味清单(问题不在清单,在优先级规则)
主干上别人的新提交被算进审查范围 「### 1. Pin the fixed point」的三点写法与 rev-parse 检查 子代理的 prompt 模板(它们只是照收 diff 命令)
没有 spec 时,Spec 轴开始编需求 「### 2. Identify the spec source」第 4 条(跳过并注明缺席) Spec 任务书里「引用 spec 原文」的要求(那条是对的)
报告又臭又长,像在写论文 「### 4. Spawn both sub-agents in parallel」两个任务书里的 "Under 400 words" 「引用出处」的要求(短不等于不许引用)
两个子代理互相看到了对方的结论 第 4 步「Send a single message with two Agent tool calls」的并行要求 聚合步骤(污染发生在派发时,不在聚合时)
你说「review 一下这个分支」时 AI 从不主动用它 frontmatter 里 description 的 "Use when…" 一段(触发词) plugin.json(它本来就已发布,且是 model-invoked)

10. 检索练习

先别往回翻,凭记忆答。选项的长度刻意对齐,不会从版式泄题。答错的题号回到对应小节重读后再选。

自测(立即反馈)

1. code-review 的两条轴各问什么?
2. 为什么 diff 命令用三点 <fixed-point>...HEAD
3. 按固定顺序四处都找不到 spec 时,SKILL.md 规定怎么办?
4. 仓库明文标准与异味基线冲突时,谁赢?
5. 基线异味在报告里的定性是什么?
6. 为什么两条轴要跑成两个并行子代理?
7. 聚合阶段被明文禁止的是哪件事?
8. 一次 code-review 跑完,仓库和 issue tracker 会多出什么?
额外提取练习(无选项) 合上本页,做三件事: (1) 默写主 build 链 grill-with-docs → to-spec → to-tickets → implement → code-review, 并说出 code-review 的两个启动入口(implement 在 commit 前自动跑、你手动对分支或 PR 跑); (2) 默写 spec 的四级查找顺序(提交信息里的 Issue 引用 → 用户传的路径 → 仓库里的 PRD/spec 文件 → 问用户); (3) 从 12 条异味里默写任意 6 条,每条配一句修复方向。 写完回第 1、5、6 节对答案。

11. 下一课与一手材料

本课主一手材料(请打开原文读,不要只背本页摘要):

速查页(本课同步): reference/code-review.html

导航: 上一课 0012 tdd (构建中的纪律:先写测试再写实现,活在接缝上)。 总览仍回 0001 系统地图

建议下一课(0014): 0014 triage。 code-review 是主 build 链的收尾;triage 是另一条 on-ramp 的入口—— 别人的 bug 报告和功能请求堆进 Issue tracker 时,它把原始 Issue 分拣成 agent 可以直接接手的成品 Issue,然后交给 implement 走回主链, 最后又会来到你刚学完的这道审查。

老师就在会话里。 对本课任何一处边界有疑问——比如三点 diff 在 rebase 过的分支上还准不准、 「工具已强制的跳过」包不包括 CI 里的自定义检查、Spec 轴找不到 spec 时能不能拿 ADR 顶上——直接在对话里问。回答会回到 skills/engineering/code-review/SKILL.md 的原文,不会临场编造。 做完检索练习后,回复「练习结果 / 哪里卡住 / 开 0014 或先补前面某课」,我们安排下一课。