/review 设计
Status: 设计定稿(v14,含三种 Runner 架构 + ocr 路径按 rule group 拆多 job 并发 + simplify 作为并行 group);v15 + cursor 移到 Tier 1 通过
ReviewWithMixed(native/review-bugbotslash + simplify 并行 fan-out);§2.5 多 job 并发机制已实现(internal/agent/aggregate_sink.go+review_with_ocr.go::delegateReviewMultiJob,详见 §2.5.7 实现索引) Scope:/reviewslash 命令的架构、分流、数据流、生命周期与边界 读者: 参与 command / agent / bridge / chatsession 任一层,或想理解 review 设计意图的工程师 Related docs:
- feat/F-review.md — v9 原始设计稿(native/delegate/不支持 pattern、flag 取舍、多 reviewer 并行风险评估)
- feat/F-review-ocr-fusion.md — ocr 委托模式融合的可行性论证、落地计划、风险与待验证项
- flow/three-layer-sync.md — ChatSession → AgentSession → Conversation 三层(
/review跑在这之上)
1. 设计目标
让用户在任意 IM chat 里发 /review,对当前 workspace 的代码变更做一次 code review,findings 回到同一 chat,主 agent 能据此 fix。review 在隔离子进程里跑,不污染主 chat 上下文;支持多 reviewer 并行(--agent),findings 汇聚到同一 chat。
1.1 核心不变量
| # | 不变量 | 违反后果 |
|---|---|---|
| 1 | /review 全异步:Handle 立即返回,goroutine 跑 review |
dispatch worker / readpump 阻塞,chat 卡死 |
| 2 | review 走 RunOnce 隔离子进程,独立 context 窗口 | 主 chat 上下文被 review 推理污染(可烧数万 token) |
| 3 | review 子进程 ctx 派生自 chat session ctx,/close 自动取消 |
orphan review 子进程残留 |
| 4 | findings 双路分发:注入 AS(当 user turn)+ 发 channel(直接可见) | 主 agent 看不到 findings,或用户要等下游回复 |
| 5 | --agent 不切 AS(不同 reviewer,同一 chat) |
切 AS 副作用太大,破坏"多 reviewer 汇聚"工作流 |
| 6 | fix 对话式:不自动 apply,用户说"fix critical findings"(沿用 output schema 的 severity 词汇 critical/high/medium/low)主 agent 用原生 Edit | 违反 v1 "纯 review"边界 |
| 7 | 三层分流:native / delegate-ocr / delegate-prompt,delegate 档 ocr 缺失自动降级 | 无 ocr 环境 review 退化为不可用,或 agent 漏文件 |
| 8 | ocr 始终是被调用的外部工具(类 git),不进 agent 注册表 | 把狭义 agent 当 bridge,扭曲 bridge 语义 |
| 9 | 大 changeset 按 ocr rule group 拆多 job,每 job 独立 RunOnce / 独立 context,不累积 | 单 context 塞全量 diff 爆窗口,或同进程多轮累积爆 |
| 10 | 多 job 自动并发(sem 上限),merge 后一次返回 | 顺序跑 N job 慢;无上限并发爆 token / 撞 API rate |
| 11 | 多 job 时,上层只看到一个 review lifecycle(单 Ready / 单 Result) | chat channel 看到 N 个 ready / N 个 result,StatusBar 翻转混乱 |
| 12 | per-job 内的 ToolStart 必须等配对 ToolEnd 才转发,Start/End 在外层 wire 上连续 | 半截 tool 调用外发,chat 渲染混乱 |
| 13 | 跨 job 的 EventAgentTaskCreate/Update 按 task ID 去重 merge,同一 ID 多个 job 的最新版本只发一份 | chat checklist 看到 N 份重复 task 条目,数量 = N × tasks |
1.2 边界
- 是:对"当前分支 vs 默认分支"(PR 模式)的代码变更做 review,产出 findings 文本回 chat,主 agent 可对话式 fix。
- 不是:不开
--fix/--post(留 v2 的/review-fix//review-comment);不限定文件 / base(v1 除--agent外零限定符);不当 CI gating;不自动改代码。
2. 整体架构:三种 Review 方法
review 有三种 runner 实现。Agent.Review 接口(Starter.Review)由各 bridge 实现;bridge 决定调哪个 agent 包 runner。基础是 F-review.md §13 的三 pattern + v11 加的 simplify 并行 group。
/review [--agent <name>]
│ 解析 --agent,选定 runner;runner ≠ 当前 AS 时,findings 仍回当前 AS
▼
Starter.Review(bridge 各自的实现)
│
├─ Native bridge(claudecode / codex)
│ → 桥自己调内置命令(`claude -p code-review` / `codex review --base`)
│ 〔各家最优形态,不画蛇添足;不走 agent 包 runner〕
│
├─ Mixed bridge(cursor)
│ → agent.ReviewWithMixed(slashCommands=["/review-bugbot"])
│ ┌─ pre := precomputeReviewWithBuiltin(workspace)
│ ├─ groups := [nativeReviewGroup("/review-bugbot"), simplifyGroup(reviewable)]
│ └─ delegateReviewMultiJob → 两个 goroutine 并行
│ (cursor.RunOnce spawn cursor-agent -p "/review-bugbot" 走 Bugbot;
│ cursor.RunOnce spawn cursor-agent -p "<simplifyPrompt>" 走 simplify lens)
│ → eventAggregator 3-phase + mergeRunResults 合并
│ 〔Bugbot 不含 simplify 4 axes,nightme 补;详见 §2.1.1〕
│
├─ Delegate bridge(dsh / pi / acp / opencode)
│ → 桥检测 OcrAvailable()(SRP:路由选择放桥层,不放 agent 包内)
│ ├─ YES → agent.ReviewWithOcr
│ │ ├─ pre := precomputeReviewWithOcr(workspace)
│ │ │ ocr delegate preview → reviewable + excluded + mergeBase
│ │ │ ocr delegate rule → ocrGroups (N per-pattern)
│ │ │ git → base + merge-base + 3 diffs
│ │ ├─ groups = pre.ocrGroups + simplifyGroup(pre.reviewable)
│ │ └─ delegateReviewMultiJob(sem cap 4,eventAggregator,mergeRunResults)
│ └─ NO → agent.ReviewWithPrompt
│ ├─ pre := precomputeReviewWithBuiltin(workspace)
│ │ git → base + merge-base + 3 diffs
│ │ Go → 4 个 git 来源(inlined)+ 合成 1 个 builtin group
│ ├─ groups = pre.ocrGroups(builtin) + simplifyGroup(pre.reviewable)
│ └─ delegateReviewMultiJob(同上)
│
└─ pty / bash → ErrReviewNotSupported → 友好提示("不是 coding agent")
两条 Runner 路径都 fan-out 出 ≥2 个 reviewGroup(ocr/builtin 主 group + simplify),走同一个 delegateReviewMultiJob。详见 §2.6。
两个 precompute 函数,产物 shape 一致
precomputeReviewWithOcr 和 precomputeReviewWithBuiltin 两条路径共用一个 reviewContext shape,fan-out machinery 不知道也不关心是哪条路径产出的:
ReviewWithOcr ReviewWithPrompt
↓ ↓
precomputeReviewWithOcr precomputeReviewWithBuiltin
├─ git: detectDefaultBranch ├─ git: detectDefaultBranch
├─ git: merge-base ├─ git: merge-base
├─ ocr delegate preview ├─ inlined: 4 个 git 来源 → reviewable + untracked
│ → reviewable (ocr FileFilter) │ → reviewable (Go isReviewablePath)
│ → excluded (with reasons) │ → untracked (新文件,无 diff)
├─ ocr delegate rule ├─ synthesize 1 个 builtin group
│ → ocrGroups = [{patternBuiltin, BuiltinPrompt}]
│ → ocrGroups (N groups, per-pattern)
│ → ocrRules (markdown)
└─ git: 3 diffs └─ git: 3 diffs
↓ ↓
reviewContext (same shape) reviewContext (same shape)
↓ ↓
groups = pre.ocrGroups + simplifyGroup(pre.reviewable)
↓
delegateReviewMultiJob → fan-out
字段对照:
| 字段 | ocr 路径 | Go 路径 |
|---|---|---|
reviewable |
ocr FileFilter(精度高) | Go 4 个 git 来源(committed/staged/unstaged)+ isReviewablePath(启发式) |
untracked |
ocr 提供的未 tracked 文件列表 | Go 端 git ls-files --others --exclude-standard(独立于 reviewable,因为没有 diff 可渲染) |
excluded |
ocr 提供的排除原因列表 | nil(Go 端不跟踪 excluded) |
ocrGroups |
N 个(每 pattern 一组,Rule 是 ocr rule doc) | 1 个 builtin group(Pattern=patternBuiltin, Rule=BuiltinPrompt) |
ocrRules |
N 个 group 的 markdown 拼好 | ""(只有一个 group) |
| merge-base / 3 diffs | 都有(同) | 都有(同) |
ReviewWithOcr 调 precomputeReviewWithOcr,ReviewWithPrompt 调 precomputeReviewWithBuiltin。两个 Runner 函数返回值都是 RunResult,行为形状一致。Bridge dispatcher 仍按 OcrAvailable() 选择调用哪个 Runner —— 这是路由决策点(不属于任何 Runner 的内部职责)。
2.1 Tier 1 — native review(codex / claude / cursor)
有内置 review 子命令的 bridge 直接调内置命令(codex 跑 codex review --base <ref>、claude 跑 claude -p code-review、cursor 跑 cursor-agent -p "/review-bugbot")。理由:这三家的内置 review 是各自最优形态(codex 的 severity 分组、claude 的多 agent + confidence 评分、cursor Bugbot 的规则匹配 + 深度控制),通用 prompt 抢不过。符合 F-review.md §13"有原生就用原生"原则。零改动。
2.1.1 cursor 的 native review 入口(cursor-agent + slash command)
cursor 跟 codex / claude 不一样:它没有 cursor-agent review CLI subcommand,但 cursor-agent 在 -p print 模式下会 dispatch 内置 slash command(类似 claude -p code-review)。cursor 把 review 能力放在 skill 系统里,而不是 CLI subcommand 里。
cursor review skill 三件套(均在 ~/.cursor/skills-cursor/,cursor-agent 自动加载,无需 --plugin-dir):
| Skill | 用途 | 可在 -p 模式 dispatch? |
|---|---|---|
review |
AskQuestion 菜单,让用户选 bugbot / security(disable-model-invocation: true,纯菜单) |
❌ 跑出来只是 menu,不可用 |
review-bugbot |
Bugbot subagent——通用代码变更 review | ✅ 实证 |
review-security |
Security Review subagent——安全专项 review | ✅ 实证 |
实证(2026-09-01,本机 cursor-agent 2026.08.11):
$ cursor-agent -p "/review-bugbot" --output-format text --trust --yolo
Bugbot could not complete the review: it could not compute a branch-changes
diff in `/private/tmp` (this workspace is not a git repo).
# dispatch 成功,只是 /tmp 不是 git repo
$ cursor-agent -p "/review" --output-format text --trust --yolo
Which review should I run?
1. **Bugbot** (`/review-bugbot`) — code-change review
2. **Security Review** (`/review-security`) — security-focused review
# menu skill 也 dispatch 了,只是 menu 不可用
$ cursor-agent -p "/review-bugbot" --output-format text --trust --yolo
There was no diff to review on this branch.
# 在真实 git repo 里跑出正常响应
为什么 cursor 之前归 Tier 2/3,现在移到"mixed"档:早期调研(2026-09-01 之前)漏了 -p 模式 dispatch 的实证,误以为 cursor CLI 没有 native review,放在 Tier 2/3 走 ReviewWithOcr / ReviewWithPrompt 多 job fan-out。2026-09-01 在 fix-review-on-cursor 分支上实证 cursor-agent -p "/review-bugbot" 真能 dispatch Bugbot,把 cursor 移到 Tier 1。
但 cursor 比 codex / claudecode 多一个 simplify goroutine,所以叫 "mixed"(native + simplify),不是纯 native 单调用。原因:Bugbot 不覆盖 reuse / simplification / efficiency / altitude 这 4 个 simplify axes——nightme 用自己的 simplifyPrompt(比 Cursor IDE 的 /simplify prompt 更详细)补这块,跟 Bugbot 并行跑,merge 后产出完整 review。详见 §2.1.1 后面"为什么 cursor 多一个 simplify goroutine"。
实现入口: agent.ReviewWithMixed(internal/agent/review.go)——通用 helper,接受一个 slash command 列表,内部拼出 groups = [nativeReviewGroup(slash1), simplifyGroup(reviewable)],走 delegateReviewMultiJob + eventAggregator + mergeRunResults 现有 machinery。cursor bridge 的 Review()`` 是一行 wiring:return agent.ReviewWithMixed(ctx, s, cfg, []string{"/review-bugbot"}, opts...)`。
为什么不桥接 cursor IDE 的 /simplify:
实证: /simplify、/simplify-bugbot、/review-simplify 三个变体在 cursor-agent -p 模式下全部空输出,不 dispatch。~/.cursor/skills-cursor/(23 个内置 skill)和 ~/.cursor/skills/(9 个用户装 skill)无 simplify 相关。Cursor.app 安装包 + 二进制 + user extensions + plugins/local + statsig-cache.json 全 grep simplify:只在 telemetry key 和无关扩展里出现,没有任何 slash command 定义。
真相:/simplify 在 Cursor IDE chat 菜单里能看到,但 cursor IDE 包内 + 二进制 + 用户扩展全部零结果——它实际是 Cursor 云端"model slash command"(cli-config.json 里有 "modelSlashCommands": true 标志),只下发到 IDE chat UI,cursor-agent CLI 完全不可达。
nightme-owned simplifyPrompt(internal/agent/review.go)比 Cursor 云端的 /simplify prompt 更结构化、更详细(4 axes + reporting discipline + severity rubric),所以 nightme 不需要桥接。
groups 形态:
{Pattern: patternNativeReview, Rule: "/review-bugbot"} // cursor.RunOnce spawn --p "/review-bugbot",Bugbot dispatch
{Pattern: patternSimplify, Rule: simplifyPrompt} // cursor.RunOnce spawn --p "<simplify prompt text>",模型做 simplify lens
assembleGroupPrompt 对 patternNativeReview 返回 g.Rule 原文(不包 diff/rule),让 cursor.RunOnce 拿 /review-bugbot 直接 dispatch。simplify goroutine 走正常的 diff / file 包装。
Review depth(Quick / Deep):这是 Cursor IDE 设置里的选项(Settings → Agents → Agent Review),不暴露给 cursor-agent CLI(--help 里没有 --review-depth flag)。bridge 跑出来的是 IDE 设置的默认深度——用户调 IDE 设置即可,bridge 层不动。
前置条件:
- cursor-agent 二进制装好(
Detect()已 check) - workspace 必须是 git repo(Bugbot 自己算 diff,workspace 模式
--base不存在,不像 codex 有--uncommitted兜底) - cursor-agent ≥ 2026.08(
review-bugbotskill 在此之前可能没内置)
2.2 Tier 2 — ocr 委托模式(delegate + ocr 已装)
delegate 档 + ocr 已安装。用 alibaba open-code-review 的委托模式:ocr 只做确定性工程(文件选择 + 规则匹配),LLM-free;host agent(我们的 dsh / pi / …)用自己 LLM 跑 review。
ocr 两个子命令职责分明(分组依据来自 rule,不是 preview):
ocr delegate preview→ 扁平文件清单 + merge_base(commit hash,可解析)+ 排除原因(不分组)ocr delegate rule→ groups(按 rule content 分组,如**/*.go一组、**/*.ts一组,每组带该组专属规则)
按 rule 的 groups 拆 jobs:每组 = 一组共享同一规则的文件 + 该组 per-file diff + 该组 rule。同组文件规则上下文一致,放一个 job review 最优。
收益:ocr 的精确文件选择 + 规则降噪 + 覆盖率约束,治"漏文件 / 偷懒 / 规则噪声";LLM 走现有 agent;ocr 不配 LLM、无双配置。
边界:委托模式下定位 / 反思由 host agent 自己做,拿不到 ocr 的行级定位 / 反思精度(那是端到端 ocr review 的领域,不在 v1 默认路径)。
2.2.1 ocr 上游源 / 本地镜像 / 规则集确认
ocr = alibaba 的 open-code-review(Apache-2.0,NPM 包 @alibaba-group/open-code-review,委托模式 SKILL = skills/open-code-review-delegate/SKILL.md)。外部 CLI(类 git),不进 agent 注册表,不绑 LLM。
为了 (a) 离线审 SKILL / 看实现 / diff 版本,(b) 给 reviewer agent 提供可读的"我在跟哪个上游交互"锚点,本地维护一个浅 clone 镜像:
| 项 | 值 |
|---|---|
| 本地路径 | ~/code/geax/github.com/cnlangzi/open-code-review/(与 nightme.nightme/、seowatson/ 等外部 dep 镜像同款位置) |
| clone 方式 | git clone --depth=1 https://github.com/alibaba/open-code-review.git(浅 clone,只保留 HEAD,够查 SKILL / 命令实现 / 当前 tag) |
| 当前 pinned | v1.9.10 / 66120291271b2e605e420e9f11fbd6448f06163f(2026-08-24 确认;升级前先看 cmd/opencodereview/delegate_cmd.go 与 skills/open-code-review-delegate/SKILL.md 是否改 schema) |
| 委托入口(对应 nightme 引用) | cmd/opencodereview/delegate_cmd.go(nightme 的 internal/agent/review_with_ocr.go 注释路径直接对得上) |
ocr 子命令全清单(以本地镜像当前 HEAD 为准,完整列表见 README):
ocr config provider/ocr config model—— 配 LLM(委托模式不需要)ocr review [--from/--to/--commit/--resume]—— 端到端 review(含 LLM,nightme 不走)ocr scan [--path/--resume]—— 全文件扫描(无 git 历史,nightme 不走)ocr delegate preview—— Tier 2 第一步:扁平文件清单 + merge_base + 排除原因ocr delegate rule <files...>—— Tier 2 第二步:按 rule content 分组的 rules(触发多 job 拆分的依据)ocr session list—— 会话管理(端到端路径用)
规则集确认(2026-08-24):官方内置多语言规则集以 NPE / 线程安全 / XSS / SQL 注入 四类为锚点,未提供 simplify / simpily 类规则。internal/config/rules/rule_docs/*.md 全量 grep simplify 仅命中 kotlin.md 一处,且为英文单词用法(Use = to simplify single-expression functions,Kotlin 语法建议),非规则类别。结论:如果 nightme 想要 simplify 行为,需要 host agent 自己出 prompt,不来自 ocr —— 这是 v1 默认路径,符合 §2.2 "边界"。
2.3 Tier 3 — Go 复刻的 builtin 路径(delegate + ocr 未装)
delegate 档 + ocr 未装。ReviewWithPrompt 调 precomputeReviewWithBuiltin,Go 端复刻 ocr 的产出形状:用 4 个 git 来源(inlined 在 precomputeReviewWithBuiltin 内)+ isReviewablePath 启发式收集 reviewable / untracked,合成 1 个 patternBuiltin 的 ocrGroup(Rule = BuiltinPrompt const,跟 ocr 路径的 N 个 per-pattern ocrGroup 等价)。仍然 fan-out 出 builtin + simplify 两个 group —— 跟 Tier 2 走同一条 delegateReviewMultiJob 路径,只是 file-list 精度低一档。
workspace 为空 / precompute 全失败时,fan-out 退化为 [simplifyGroup(nil)] 单 goroutine,prompt 用 BuiltinPrompt 兜底。零外部依赖,零回归。
2.4 prompt 工程(对 Tier 2 / Tier 3 通用)
参考 ocr 的"确定性工程"思想,把原本烤进静态 prompt 里的几项挪到 Go 侧硬约束(纯工程,不依赖 LLM):
- Go 侧预算 diff —— 治 agent 偷懒 / 漏文件
- 文件清单 + 排除 ——
isReviewablePath启发式(generated / testdata / vendor 等) - reviewable vs untracked 分离 —— 改了/未改的文件走 diff 通道;新文件(untracked)走单独通道,因为
git diff <untracked>全空,把它们混进 reviewable 会让 LLM 看到文件列表却没有 diff 内容,违反 coverage mandate - 覆盖率硬约束 —— 每文件 reviewed 或 skipped-with-reason
- 输出 schema —— path / content / start_line / end_line / category(
bug|security|performance|maintainability|test|style|documentation|other)/ severity(critical|high|medium|low),结构化便于 fix 定位 - 规则匹配 —— Tier 2 用
ocr delegate rule返回的 ocrGroups(每个 pattern 一组);Tier 3 用precomputeReviewWithBuiltin合成的一个 builtinGroup(全部文件 + BuiltinPrompt)。两者形状一致,fan-out 不区分 - per-file 截断 ——
maxDiffLines = 2000兜底,超长 diff 截断并提示"read directly"
关键收敛:1–5 项是纯 Go 工程,两档都做;第 6 项规则匹配是 Tier 2 / Tier 3 的差异点(Tier 2 拿 ocr 的多 pattern groups,Tier 3 拿 Go 合成的一个 builtinGroup);第 7 项防单 job 的 prompt 膨胀。ocr 在与不在的区别收敛到"规则匹配"一项的精度(ocr FileFilter vs Go 启发式)。
BuiltinPrompt 本身不再携带 simplify 规则(原 StandardPrompt 里有这条 bullet,已删)—— simplify 作为独立并行 group 跑(SimplifyPrompt const),不再在 BuiltinPrompt 里冗余出现。severity 词汇统一为 critical/high/medium/low,跟 assembleGroupPrompt 的 output schema 一致 —— 不再有 blocker/major/minor/nit 与 critical/high/medium/low 跨 group 冲突。
2.5 多 job 并发机制(大 changeset)
Tier 2 按 ocr rule groups 拆多 job 时,核心是每 job 独立 RunOnce / 独立 context + 自动并发 + merge。
2.5.1 为什么拆 job(防大 changeset 爆)
单 RunOnce 塞全量 diff,大 PR(几十文件、几千行)会撞 host agent 的 context 窗口,review 静默降级(丢 findings / 丢文件)。按 rule group 拆:每 job 只含本组 diff,独立 context(fresh 子进程,不累积),从根上防爆。这是 ocr 端到端 smart bundling 的 code-driven 等价——用 ocr 的 rule groups 做分组边界,RunOnce 各自独立。
2.5.2 触发条件(自动)
两条 Runner 路径都永远 fan-out(v14 起,因为 simplify group 总是追加,groups 永远 ≥ 2)。具体 fan-out 数:
| 情况 | fan-out groups | goroutine 数 |
|---|---|---|
| ocr 在 + N 个 ocr group | N + 1(N 个 ocrGroup + 1 个 simplifyGroup) | N + 1 |
| ocr 在 + ocrGroup 为空(ocr 失败) | 0 + 1(只剩 simplifyGroup) | 1 |
| ocr 不在(Tier 3)+ 1 builtinGroup | 1 + 1(builtinGroup + simplifyGroup) | 2 |
| ocr 不在 + workspace 空 | 0(只有空 goroutine fallback 到 BuiltinPrompt) |
1 |
无需用户开关——delegateReviewMultiJob 自动按 groups 数 spawn goroutine,sem cap 4 限并发。
2.5.3 并发控制
- sem 上限(如 4):防 N 个 agent 子进程同时跑爆 token / 撞 API rate。超 sem 的 job 排队等空位。
- 单 job 失败不阻塞:某组 RunOnce 失败,其他组仍出结果;merge 时该组标"failed"。
- ctx 派生:所有 job 共用
ReviewWithOcr/ReviewWithPrompt的 revCtx(chat session ctx + 30min 超时)。/close→ 全部取消。
2.5.4 merge(一次返回)
各 job 输出结构化 markdown(## Coverage + ## Findings schema)。LLM 自己解析——merge 阶段不做结构化解析、不按 severity 排序、不做 path 去重、不做 coverage 聚合。原因:
- 各 agent 输出的 schema 实现细节不一致(claude 的 confidence / codex 的 severity group / ocr 的 category enum 等),结构不稳定,统一解析器易碎
- 主 agent(消费侧)对 markdown 是 LLM,自然语言理解足以接住 findings;结构化字段只是 optional hint
- 降低 merge 路径的复杂度 = 降低 surface area = 减少回归风险
merge 步骤只有两步:
- 按组标头拼接:每组
### Group: pattern X — files: A, B, C头 + 该组原文 markdown。失败组标### Group: pattern X — failed: <err>。 - 一次返回:合成的 RunResult 经
FormatReviewMessage注入 AS + 发 channel 一次。
partial failure 路径:不升级为 merge 整体错误,失败组以 inline marker 出现在合并文本里;all-failed 才返回 first error wrapped with agent name。
2.5.5 sink 契约:三相状态机 + per-job 配对
多 job 时,cmd.go 喂给 ReviewWithOcr / ReviewWithPrompt 的 outer sink(outbound.StreamRunOnceToEmitter)必须只看到一个 review lifecycle,不能感知到内部 N 个并发 job。eventAggregator 用三相状态机实现这一契约:
┌─────────────────────────────────────────────────────────────────────┐
│ Phase 1 (buffering) │
│ ───────────────── │
│ per-job 进来的所有事件进 perJob.initBuffer,**不外发** │
│ readyCount++; doneCount++(per-job terminalSeen 守卫防重复) │
│ 当 readyCount == expected: │
│ ┌── 锁内 ─────────────────────────────────────────────────────┐ │
│ │ phase = phaseStreaming │ │
│ │ snapshot 各 perJob.initBuffer,清空 │ │
│ │ 构建 synthetic outer Ready(merged metadata,Source="") │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ┌── 锁外 ─────────────────────────────────────────────────────┐ │
│ │ outer(synthetic outer Ready) │ │
│ │ replay 各 perJob 的 snapshot → handleStreaming │ │
│ │ (应用 ToolStart/End 配对、Task 合并 ID 去重等) │ │
│ └──────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ Phase 2 (streaming) │
│ ─────────────────── │
│ live 事件到达 → handleStreaming 立刻处理: │
│ - ToolStart → pendingToolStarts[ID] = ev(**不转发**) │
│ - ToolEnd → 查 pendingToolStarts[ID],有则按 Start→End 连续 │
│ 转发两条事件;无则 forward 孤儿 End │
│ - TaskCreate/Update → 按 ID dedup 后 forward ONE merged snapshot │
│ - Text / Permission → forward as-is,Source="group-N" │
│ - Result/Error → doneCount++(terminalSeen 守卫);不外发 │
│ 当 doneCount == expected: │
│ outer(synthetic outer Result,Source="") → Phase 3 │
├─────────────────────────────────────────────────────────────────────┤
│ Phase 3 (closed) │
│ ──────────────── │
│ late events 忽略(已发出的 outer lifecycle 已闭合) │
└─────────────────────────────────────────────────────────────────────┘
关键设计:
- #11 单 outer lifecycle:上层只看到一次
outer Ready和一次outer Result。N 个 per-job Ready + N 个 per-job Result 在聚合器内消化掉,只在 all-wait 条件满足时各合成一个外发。 - #12 per-job ToolStart/End 配对:每个 per-job 一个
pendingToolStarts map[string]AgentEvent。ToolStart 进缓冲不外发,等配对 ToolEnd 到达后Start → End 顺序连续转发两条事件。保证 chat 渲染层看到的 tool 调用是完整块,不出现半截 open call。 - #13 Task 跨 job 去重:aggregator 维护
tasks map[string]AgentTaskItem,每个 TaskCreate/Update 进来按 ID 写入(latest wins),forward 一份合并 snapshot。避免 chat checklist 看到 N 份重复条目。 - 进程事件实时流:Phase 2 期间 process 事件立即外发(Source="group-N" 让 chat 渲染层区分),review 期间用户能实时看到 "[group-1] tool: Read /home/repo/foo.go" 这种进度。
- 异序到达容错:job-1 先报 Result、其他还没报 Ready 的情况:
doneCount > readyCount短暂成立,但不触发 outer Result(Phase 还在 buffering);等所有 Ready 后才触发 Phase 2,然后等所有 Done 才触发 outer Result。Ready 优先于 Done 的合成外发。 - Replay 一致性:Phase 1→2 转换时,旧 initBuffer 经同一个
handleStreaming路径走完配对/合并逻辑,与 live 事件应用同一套规则,行为完全一致。
为什么 outer sink 能安全接收:outbound.StreamRunOnceToEmitter 底层是 chan-based,channel send 跨 goroutine 线程安全。aggregator 持锁区只做状态变更,outer(ev) 调用在锁外完成,避免持锁回调。
2.5.6 边界
- 超大单组:某 group diff 仍超大(如
**/*.go100 文件 5000 行)→ per-file 截断兜底(§2.4 第 7 项);极端时按目录二次拆(留后续)。 - 顺序 vs 并发:默认并发(快);若并发有风险(如 agent 不支持并发 session),可降级顺序(loop 无 sem)——但已验证 delegate bridge 的 RunOnce 并发安全(dsh sessionId 多路复用 / 其他独立子进程),默认并发。
- 跨 job 事件流交错:Phase 2 期间不同 job 的 process 事件在 outer 上可能交错(Source="group-N" 区分);同一 job 内部 ToolStart→ToolEnd 严格配对连续(per-job 缓冲保证)。
- Replay vs live 交错:Phase 1→2 转换时,replay 处理 per-job 旧事件可能与同 job 新到的 live 事件在 outer 上交错。当前实现的已知 minor race:同 job 内 replay 事件 + live 事件少数情况下顺序非严格 chronological。后果仅为 chat 时间线略不规整,无功能影响。修复方案(若需要)是在转换时加 replay-in-progress barrier。
- 未配对 ToolStart:job 在 ToolStart 后异常结束(无对应 ToolEnd),该 Start 进 buffer 后永远不被 forward——这是合理行为,chat 不会看到半截 call;该 job 的 done 仍会按 Result/Error 触发。
- orphan ToolEnd:配对 ToolStart 已先被 replay 转发过的罕见情况——orphan End 也 forward,chat 看到一条无 Start 的 End,可忽略。
2.5.7 实现索引(v14, + v15 ReviewWithMixed)
| 概念 | 实现位置 |
|---|---|
四个 Runner 入口(ReviewWithOcr / ReviewWithPrompt / ReviewWithMixed / per-bridge Review) |
internal/agent/review_with_ocr.go、internal/agent/review.go |
| ocr 路径 precompute(ocr delegate preview + rule + diffs) | internal/agent/review_with_ocr.go::precomputeReviewWithOcr |
| Go 路径 precompute(detectDefaultBranch + merge-base + 4 git 来源 file enumeration(inlined) + builtin group 合成 + 3 diffs) | internal/agent/review_with_ocr.go::precomputeReviewWithBuiltin |
OcrAvailable 导出函数(bridge dispatcher 用) |
internal/agent/review_with_ocr.go::OcrAvailable |
| Native review group(bridge 提供 slash command,prompt 原样 return,不包 diff/rule) | internal/agent/review_with_ocr.go::nativeReviewGroup + assembleGroupPrompt 的 case patternNativeReview |
| ReviewWithMixed(native slash + simplify 并行,通用 helper,不是 cursor 专属) | internal/agent/review.go::ReviewWithMixed |
| 多 job 并发编排(sem cap 4,N 个 goroutine,各自独立 ctx) | internal/agent/review_with_ocr.go::delegateReviewMultiJob |
per-group 提示词(context / file list / diff / rule / how-to / schema),按 g.Pattern 分 header(ocr / builtin / simplify / native) |
internal/agent/review_with_ocr.go::assembleGroupPrompt |
按文件过滤 diff(git diff -- <files...>) |
internal/agent/review_with_ocr.go::groupFilteredDiff |
BuiltinPrompt / SimplifyPrompt 静态 prompt 模板 |
internal/agent/review.go |
| 三相状态机 + per-job 配对缓冲(Phase 1 buffering → Phase 2 streaming → Phase 3 closed;pendingToolStarts map per-job;Task 跨 job ID 去重;异序到达容错) | internal/agent/aggregate_sink.go::eventAggregator |
| 多 job 结果合并(纯自然语言拼接 + 部分失败 inline marker,无解析/排序/去重/coverage 聚合) | internal/agent/review_with_ocr.go::mergeRunResults |
| 单元测试(聚合器 / 合并 / 并发 / 配对 / native group / mixed fan-out) | internal/agent/aggregate_sink_test.go、internal/agent/merge_results_test.go、internal/agent/fanout_test.go、internal/agent/review_mixed_test.go |
不变量与实现的对应:
- #1 全异步:
Handle立即返回,goroutine 跑 review,revCtx 派生自 chat session ctx。 - #2 / #9 RunOnce 隔离:每个 per-job 是独立
s.RunOnce(独立子进程 + 独立 context),多 job 间不共享、不累积。 - #3 ctx 派生 + /close 取消:所有 job 共用
revCtx(chat session ctx + 30min 超时)。 - #4 双路分发:
FormatReviewMessage一次产出的合并文本 → 注入 AS(user turn)+ 发 channel 一次。 - #10 自动并发 + sem + merge 一次返回:
sem chan struct{}cap 为maxConcurrentReviewJobs = 4;wg.Wait()同步;mergeRunResults一次产出最终 RunResult。 - #11 单 outer lifecycle:eventAggregator 的三相状态机在 readyCount/doneCount all-wait 时各合成一次 outer Ready / outer Result,per-job lifecycle 不外泄。
- #12 per-job ToolStart/End 配对:eventAggregator 的
perJob.pendingToolStarts按 ID 缓冲 ToolStart,等配对 ToolEnd 时按 Start→End 顺序连续转发。 - #13 Task 跨 job 去重:eventAggregator 的
tasks map[string]AgentTaskItem,latest-wins 写入,forward 合并 snapshot。
2.6 Simplify lens(nightme-owned,并行 group)
ReviewWithOcr 和 ReviewWithPrompt 都会追加一个 nightme-owned 的 simplify review group,作为独立的并行维度(独立 RunOnce)跟主 review(ocr groups 或 builtin rubric)一起跑。simplify 不是 prompt 末尾追加,而是一个完整的 reviewGroup,有自己的 Pattern sentinel (_nightme_simplify),所以它走跟 ocr/builtin groups 完全一样的 fan-out 路径。
来源
简化规则的 4 axes 借鉴自 Claude Code 的 /simplify skill(reuse / simplification / efficiency / altitude),但形态调整为 review findings 而非 refactor apply —— 它产出发现,不自动改代码。
实现
// review_with_ocr.go
const (
patternBuiltin = "_nightme_builtin" // ReviewWithPrompt 的 builtinGroup
patternSimplify = "_nightme_simplify" // 永远追加
)
func simplifyGroup(files []string) reviewGroup {
return reviewGroup{Pattern: patternSimplify, Files: files, Rule: SimplifyPrompt}
}
// ReviewWithOcr:precomputeReviewWithOcr → pre.ocrGroups + simplifyGroup → 风扇
groups := append(pre.ocrGroups, simplifyGroup(pre.reviewable))
// ReviewWithPrompt:precomputeReviewWithBuiltin → 1 builtinGroup(已含在 pre.ocrGroups) + simplifyGroup → 风扇
// (precomputeReviewWithBuiltin 合成 ocrGroups = [{patternBuiltin, BuiltinPrompt}],所以同样 append simplifyGroup)
groups := append(pre.ocrGroups, simplifyGroup(pre.reviewable))
两个 Runner 函数主体相同:都 pre := precomputeReview*(ctx, workspace); groups := append(pre.ocrGroups, simplifyGroup(pre.reviewable)); return delegateReviewMultiJob(...)。区别只在调哪个 precomputeReview*。
simplify group 的 prompt 通过 assembleGroupPrompt 渲染:switch g.Pattern 命中 patternSimplify 分支,header 是 # Simplify review lens (nightme-owned, complementary),rule 文本是 SimplifyPrompt const。
Scope
| Runner | simplify group 出现? |
|---|---|
ReviewWithNative(claudecode / codex) |
❌ 不出现(桥自己处理 prompt) |
ReviewWithMixed(cursor) |
✅ 始终追加(Bugbot 不含 simplify axes,nightme 补) |
ReviewWithOcr(ocr 已装) |
✅ 始终追加 |
ReviewWithPrompt(ocr 未装 / fallback) |
✅ 始终追加 |
ocr 检测的位置(SRP)
OcrAvailable() 是 agent 包导出的函数。delegate-tier 桥的 Starter.Review 自己做 ocr 检测,然后决定调哪个 runner:
// 4 个 delegate 桥的 Starter.Review(统一形态)
func (s *Starter) Review(ctx, cfg, opts...) (RunResult, error) {
if agent.OcrAvailable() {
return agent.ReviewWithOcr(ctx, s, cfg, opts...)
}
return agent.ReviewWithPrompt(ctx, s, cfg, opts...)
}
历史变更:cursor 在 2026-09-01 之前属于这 4 个 delegate 桥之一(
fix-review-on-cursor分支实证 cursor CLI 走-p "/review-bugbot"dispatch 走通后,移到 §2.1 Tier 1)。详见 §2.1.1。
ReviewWithOcr 内部不再做 OcrAvailable 检查或 fallback —— 单一职责:假设 ocr 可用,跑 ocr 委托模式。如果调用方在 ocr 不可用时调它,那是调用方 bug,不该偷偷 fallback。
为什么不用 WithSimplifyPrompt option
早期 v3-v7 讨论过用 functional option WithSimplifyPrompt() 控制 simplify 启用。否决原因:
- simplify 是 simplify-as-group(独立 RunOnce),不是 prompt 末尾追加的 section —— 没法用 option 控制 prompt 字段
- 始终启用 simplify 比 opt-in/opt-out 简单,且 simplify 的 findings 是低风险补充(默认 severity =
low/style) - 未来若要 disable,只需把 simplifyGroup 从 group 列表里删,改 1 个 helper 函数
3. 数据流:从 /review 到 findings
- 用户发
/review [--agent <name>]。 - dispatcher 解析 args(
--agent/-a),inline 校验未知 flag。 - 解析 inject 目标:当前 chat 的 selected AS(lookup,daemon 重启后自动 spawn)。
- 解析 review runner:
--agent覆盖则用其,否则用当前 AS 的 agent;查 agent 注册表拿 Starter。 - 启动 goroutine(chat session ctx 派生 + 30min 超时):
- 接 sink,把 review 的中间事件(思考 / 工具调用)流式进 chat(观察用)。
Starter.Review→ 三种 Runner:- Native bridge(claudecode / codex):桥自己调内置命令(
claude -p code-review/codex review --base)。 - Mixed bridge(cursor):走
ReviewWithMixed——native/review-bugbotgoroutine + nightme-owned simplify 并行 goroutine,经delegateReviewMultiJob合并。 - Delegate + ocr 在:桥 dispatch 到
ReviewWithOcr→precomputeReviewWithOcr→ ocr groups + simplifyGroup → 多 job 风扇。 - Delegate + ocr 不在:桥 dispatch 到
ReviewWithPrompt→precomputeReviewWithBuiltin→ 1 builtinGroup + simplifyGroup → 多 job 风扇。
- Native bridge(claudecode / codex):桥自己调内置命令(
FormatReviewMessage包前缀(workspace + runner 标注,让主 agent 知道"这是谁跑的 review")。- 双路分发:注入 AS 当 user turn(主 agent 能"fix critical findings")+ 发 channel(用户直接可见,不等下游回复)。
- Handle 立即返回
Consumed=true,无 inline reply。readpump 继续,dispatch worker 释放,用户可继续发消息。findings 异步到达。
4. 隔离与生命周期
- RunOnce 是隔离机制:fresh 子进程,独立 context 窗口。review 推理(可烧数万 token)不污染主 chat。
- 多 job 各自独立 context:大 changeset 按 rule group 拆的每个 job 都是独立 fresh RunOnce——不共享 context、不累积,防爆。区别于"同进程多轮 + /new reset"的弱隔离(依赖 agent reset 彻底),fresh spawn 是 OS 级强隔离,零信任。
- 超时:Review 30min(同
/gtw commit的 Agent 预算)。RunOnce 边界使超时安全:只杀 review 子进程,主 chat 不受影响。多 job 并发共用同一超时。 - ctx 派生:goroutine 用 chat session 的 ctx 当 parent。
/close→ ctx cancel → 所有 review 子进程 kill → goroutine 退出。无 orphan。 - 多 reviewer 并行:多个
/review --agent并发,各自独立 RunOnce,findings 全注入同一 AS。用户体验:多条 review 接连出现,然后一次"fix"。 - sink vs deliverable:sink 是观察用(流式中间事件),
FormatReviewMessage文本是交付物。两路独立,不冲突。
5. 设计原则
- agent-delegated —— nightme 自己不调 LLM,review 推理交给现有 agent。ocr 委托模式符合这点(ocr LLM-free,工程产出喂 agent)。
- ocr 不是 bridge —— ocr 是狭义 agent(只 review,不能 chat / Edit /
/use)。塞进 agent 注册表扭曲 bridge 语义(bridge = 通用编码 agent)。ocr 是被调用的外部工具(类 git)。 - native 优先 —— 有内置 review 的 bridge 调内置,不跑通用 prompt。尊重各家最优形态,符合 F-review.md §13。
- 优雅降级 —— delegate 档 ocr 缺失 →
ReviewWithPrompt走 Go 复刻路径(precomputeReviewWithBuiltin内联 4 个 git 来源 + 合成 builtinGroup 模拟 ocr 的产出形状),不报错不阻塞。review 在任何环境可用,只是 file-list 精度低一档(Go 启发式 vs ocr FileFilter)。 - fix 对话式 —— review 只产出 findings,不自动改代码。主 agent 用原生 Edit 工具 fix。保持 v1 纯 review。
--agent不切 AS —— runner 是一次性 spawn,findings 回当前 AS。不同 reviewer,同一 chat。- 独立 context 分 bundle(code-driven) —— 大 changeset 按 ocr rule group 拆多 job,每 job 独立 fresh RunOnce(强隔离,不累积)。这是 ocr smart bundling 的 code-driven 等价——用 ocr 的 rule groups 做分组边界,RunOnce 各自独立,不是 ocr 端到端的重 multi-agent 机器。
6. 不做的事(边界)
- ❌ 把 ocr 当 bridge / 进 agent 注册表 —— 它是被调用的外部工具。
- ❌ 动 native review(codex / claude / cursor 内置)—— 有内置就调内置。
- 例外: cursor 多跑一个 simplify 并行 goroutine,因为 Bugbot 不含 simplify 4 axes(详见 §2.1.1)
- ❌ 把 ocr 端到端(
ocr review)设为默认 —— 需配 ocr LLM、双配置,留 opt-in(--engine ocr)。 - ❌ 引入 ocr 端到端的重 multi-agent 机器(ocr 自己的定位 / 反思 / 多 bundle 子 agent)作默认 —— 那是 ocr 端到端跑、依赖 ocr 自己的 multi-agent;我们的 code-driven 拆 job 不是这个(用 ocr 的 rule groups 只取分组边界,RunOnce 各自独立 context,定位 / 反思仍由 host agent 自己做)。两者本质不同,不冲突。
- ❌ 同进程多轮 +
/newreset 池化(跨调用复用)—— 破坏 RunOnce 强隔离不变量(#2),/new是弱隔离(依赖 agent reset 彻底),且 review 非高频收益不抵。省 spawn 只在单次 /review 内(瞬态多轮),不跨调用池化。 - ❌ v1 开
--fix/--postflag —— 留 v2(/review-fix//review-comment)。 - ❌ 破坏
--agent不切 AS 的语义 —— ocr 委托模式 findings 仍注入回当前 AS,主 agent 仍能用原生 Edit 工具 fix。
7. 相关
- feat/F-review.md — v9 原始设计稿:三 pattern(native/delegate/不支持)、flag 取舍、多 reviewer 并行风险评估、
--agent不切 AS 的论证。 - feat/F-review-ocr-fusion.md — ocr 融合的可行性论证、三层分流的落地计划、风险与待验证项(双配置 / 跨平台 / prompt 膨胀等)。
- flow/three-layer-sync.md — ChatSession → AgentSession → Conversation 三层,
/review跑在这之上。 - SPEC.md §1 — 七个逻辑组件、不变式。