关于 guided-review
guided-review 是用于生成本地托管 HTML 页面、以审阅提交、PR 或工作树的技能。它按主题组织文件,并在同一页面嵌入差异、可编辑文本和图像比较、备注及持久化勾选项,也提供协作审阅流程脚本。
主要功能
- 01按主题安排文件阅读顺序
- 02在单个 HTML 页面嵌入差异
- 03支持可编辑文本和图像比较
- 04持久保存审阅备注与勾选状态
- 05支持 PR 堆栈和审阅备注
适用场景
- 01审阅涉及大量文件的更改
- 02无需本地检出即可检查 GitHub PR
- 03检查合并或变基冲突解决结果
- 04协调 PR 堆栈中的备注与编辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jimhigson/ guided-review --skill guided-review安装后在 Claude Code 中输入/guided-review确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignore.npmrcackNote.tsawaitNotes.tsbuild.tsbuildPage.tsbuildStack.tschangedFiles.shconflict.tsconflictFiles.tseditPlace.tsgithub.tsheld.tsmechanical.tsmechanicalChanges.tsmoveDetection.tsmoves.tspackage.jsonpnpm-lock.yamlpnpm-workspace.yamlprComments.tsremotePr.tsreply.tsresolvePr.shresolveStack.tsrestack.tsfixtures/commitStack.shconflictRepo.shlocalStack.shmonorepo.sh详细介绍
引导式审查
生成一个在本地提供服务的单页 HTML:按主题分组,列出提交/PR/工作树中每个文件的阅读顺序(不严格遵循拓扑顺序——即使需要打破依赖顺序,也要把相似的工作归在一起);每个文件附有简短说明,并内嵌该文件的差异——文本使用可编辑的 Monaco 编辑器,图片使用可平移和缩放的对比查看器——还带有一个状态会持久保存到审查专用文件中的复选框;提供服务时,你也可以阅读这个文件。此页面包含完整的审查内容:无需在其他地方打开文件即可阅读更改。
提供服务(第 8 步),而不是发布。提供服务的页面可以编辑并保存回工作树,还可以将备注保存在文件中;发布的产物则被 CSP 封锁,无法访问任何端点,包括 localhost,因此只能阅读。只有在无法打开本地端口时才发布——例如在沙箱中,或使用他人的机器时。
当更改规模较大(数十个文件),按差异或字母顺序阅读会让人理不清头绪时使用。
几乎所有工作都已脚本化。 唯一需要判断的是中间这一部分:
| 文件 | 作用 |
|---|---|
resolvePr.sh | PR 编号/分支/URL → 要比较的本地引用,并获取这些引用(包括 fork) |
remotePr.ts | PR URL(或 owner/name#n)→ 同样的结果;如果这台机器上还没有该仓库,就以裸仓库、无 blob 的方式克隆 |
resolveStack.ts | 同一目标,或使用 --stack 指定一个 gh stack(无论是否已推送)→ 它所属的整个堆栈,供页面上的堆栈栏使用 |
buildStack.ts | 堆栈中每份已撰写的评审(每个 PR 一个 groups json,可由任意数量的 Agent 生成)→ 一个带有内嵌评审切换器的页面 |
changedFiles.sh | 范围内的文件列表,格式为 STATUS<TAB>PATH;排除二进制文件,但保留栅格图像,为其提供对比查看器 |
mechanical.ts | 范围内纯机械性变更的文件(仅导入、重新格式化、旧名称已消失的重命名、原样移动的代码),用于提供预先勾选这些文件的选项 |
mechanicalChanges.ts | mechanical.ts 背后的判定逻辑;build.ts 也会运行它,以便在页面中标记这些文件 |
moveDetection.ts | moves.ts 背后的匹配逻辑;build.ts 也会运行它,以便在页面中框出移动的代码 |
moves.ts | 范围内移动的代码:在其他位置重新出现的已删除代码块(原样出现,或移动过程中仅修改了少数几行),以及每个文件中移动而非新写的内容占比 |
| (你) | 编写 groups json:分组、顺序和每个文件的备注 |
snapshotGroups.ts | 或者,对于仅基于快照的评审,以机械方式生成该 json,无需撰写 |
build.ts | 收集每份差异及每个文件完整的前后版本,剥离/截断/转义内容,捕获每个变更图像的所有版本,计算 GitHub 锚点,内嵌字体和构建后的 UI,并写入最终 HTML |
src/ | 页面本身——一个用 tsx 编写的 preact 应用:配色方案、布局、内容侧边栏、差异渲染、图像对比查看器、备注和复选框 |
buildPage.ts | 将 src/ 打包成一个脚本和一个样式表,供 build.ts 内嵌 |
serve.ts | 提供构建后的页面——通常通过这种方式交付评审;页面中的编辑器会变为可编辑状态,并将内容保存回工作树 |
test/e2e.mjs | 在无头 Chromium 中针对临时仓库,对已提供的评审进行端到端检查(pnpm test:e2e)。对 skill 的任何更改提交前都要运行 |
fixtures/commitStack.sh | 一个包含 2 个 PR × 5 个提交的临时堆栈,用于尝试逐提交评审 |
prComments.ts | 已提供的页面 → 从 GitHub 获取其 PR 的评论,并将评论写在每份评审旁边,供页面显示 |
awaitNotes.ts | 每当评审者备注写入时就通知你——可流式接收,或使用 --once 逐条接收;具体取决于宿主最适合的唤醒方式(见“保持备注通道开启”)——这样你就能在对方仍在阅读时采取行动 |
ackNote.ts | 在你还没准备好回复前,确认已看到备注——如实地将“已发送,等待 Agent”变为“Agent 正在处理” |
reply.ts | 在备注所属的线程中回复——说明你做了什么,或提出唯一需要对方回答的问题 |
stackEdit.ts | 暂存的编辑、提交或修改提交,并对其上方的 PR 重新堆叠——仅使用 git 底层操作,不切换分支 |
editPlace.ts | 确定针对备注的编辑落在哪里(磁盘,或哪个 PR 的暂存编辑),供 held.ts 和 reply.ts --edited 共用 |
held.ts | 供处理备注的 Agent 使用:在评审者自己会进行编辑的位置,完成备注要求的编辑——先用 open 打开(磁盘,或 PR 暂存编辑的临时工作树),再用 save 保存 |
restack.ts | 供受命解决页面遇到的冲突的 Agent 使用:status 查看状态,resolve(在临时工作树中设置好环境),resolved(记录修复) |
请阅读脚本自身的 --help 和文件头部注释,而不是重新推断它们的作用。不要手动拼装 HTML 或粘贴差异文本;如果页面需要更改,请修改 src/,这样下一次评审也能使用这些更改。
所有内容都基于 Node 运行,使用的是本目录自己的 package.json,而不是被审查仓库的 package.json;后者可能完全没有 Node。安装过程还会引入 monaco-editor,serve.ts 会将其提供给页面使用,没有它就无法启动。全新检出后的首次构建会安装它:
cd .claude/skills/guided-review && pnpm install --ignore-workspace
--ignore-workspace 和本地的 pnpm-workspace.yaml 都能阻止 pnpm 向上遍历,并将此技能视为它所在仓库的软件包。node_modules/ 是构建状态目录,已加入 gitignore。
1. 选择范围:提交、PR 或工作树
这里没有任何内容专属于某个仓库——在哪个仓库运行都可以,该仓库就是工作目录。用户可能会这样提出请求:“引导我审查 PR 34”、“引导我审查 #23”、提供 PR URL、提交 SHA,或者说“我的分支”和“我尚未提交的改动”。
支持四种入口——先判断用户指的是哪一种,再进行其他操作,因为这会影响后续的每一条 git 命令:
-
单个提交。 你有一个 SHA(或者用户说了“提交 <sha>”)。差异就是
git show <sha>。 -
GitHub 上的 PR,无论本地是否有该仓库。 提供 URL 就足够了,也可以说“帮我审查 ”。
remotePr.ts会解析它:如果当前检出的是该仓库,它会像resolvePr.sh一样将 PR 的 refs 拉取到本地;其他情况下(另一个仓库、主目录、/tmp),它会将仓库以裸仓库和无 blob 的方式克隆到缓存中,并从那里读取——没有工作树,不会检出任何内容,而且只会获取审查打开的文件版本。以这种方式处理一个检出后有数百 MB 的仓库,只需几百 KB;再次审查时只需拉取,而无需重新克隆。node .claude/skills/guided-review/remotePr.ts https://github.com/owner/name/pull/34
它会输出 {number, title, url, base, head, repo}。后续每条命令都要将该 repo 作为 --repo 传入,包括 serve.ts。从缓存克隆提供服务时,审查是只读的——没有检出目录可供保存编辑内容——但备注、勾选项、差异和 PR 自带的评论都能正常使用。用户提到一个你没有本地检出的 PR 时,始终使用此方式;不要让用户先克隆仓库。
- 整个 PR。 你有 PR 编号、分支名,或者用户只说了“审查这个 PR”/“审查我的分支”。一个 PR 通常包含多个提交——差异是自它与 base 分支分叉以来的全部内容,这正是 GitHub“已更改文件”选项卡展示的内容,也是审查者真正需要的内容,而不是其中某一个提交(即使 PR 目前恰好只有一个提交,也要按此模式处理,以免工作流在你完成审查前有新提交加入时悄然漏掉它们)。
- 未提交的改动。 分支还没有自己的提交(审查自己尚在进行的改动时很常见):
git log origin/main..HEAD为空,所有内容都在git status中。差异包括已跟踪文件的git diff <base>,以及每个未跟踪文件的全部内容。没有可供链接的 GitHub 页面——请参见第 3 步。 - 冲突解决。 合并、变基、挑拣提交或还原操作已暂停,冲突已解决(无论是否已暂存),或者用户指定了一个已完成的合并提交。使用
--mode conflict构建(已完成的合并提交还要加上--ref <merge>)。详见下文“冲突审查”。它会根据MERGE_HEAD/REBASE_HEAD/CHERRY_PICK_HEAD/REVERT_HEAD或合并提交的父提交,为你确定当前版本、传入版本和祖先版本。
在未提交改动之上还有新分支内容的分支,仍然属于整个 PR:使用 --mode pr --base <base> --head <branch name> 构建,并从检出目录提供服务。服务页面的右侧就是工作目录:每项未提交的编辑都会实时显示,仅存在于磁盘上的文件会出现在页面的实时章节中,而且每次提交和变基时都会重新构建审查内容。
绝不要创建用于评审的提交。 不要使用
git commit-tree、git stash create、WIP 提交或“工作树快照”将未提交的工作纳入某个范围,也绝不要将此类提交(或任何没有分支指向的 SHA)作为--head传入。基于工作目录副本构建的评审在复制时就已冻结。它不是读者的代码,因此服务端无法将其关联到检出目录:编辑器是只读的,读者的修改不会显示,也不会重新构建。对读者来说,这看起来就像评审出了问题。始终将分支名称作为--head传入(HEAD也可以:构建会记录它指向的分支)。build.ts会拒绝不属于任何分支的 head。
确定是哪一个,并将其解析为具体引用:
# whole-PR mode - takes a number, a branch, or a pr url; fetches what it needs
.claude/skills/guided-review/resolvePr.sh 34
git log -1 <sha> --format="%H%n%s%n%b" # single-commit mode
git status -sb && git log --oneline origin/main..HEAD # to detect working-tree mode
resolvePr.sh 会输出 {number, title, url, base, head}。head 来自
refs/pull/<n>/head,因此来自 fork 的 PR 会像来自 origin 分支的 PR 一样准确解析。将它的 base/head 传给所有后续步骤,并将它的 number 传给 build.ts --pr,这样每个文件的链接才能跳转到正确页面。
在整个 PR 模式下,后续每个步骤中所说的“差异”都指三点范围 <base>...<head>(合并基点差异——与 GitHub 显示的内容一致),绝不要使用单个提交的 SHA,即使你是通过 git log 找到它的。
从 SHA 开始时,值得检查是否有 PR 涵盖了它(gh pr list --search "<sha or distinctive title words>" --state all --json number,title,url):如果有,就评审 PR 而不是提交,因为评审者要审查的正是 PR。
冲突审查
解决冲突后提供审查。 当你(或你运行的 Agent)刚刚解决了合并/变基/摘取提交/还原操作中的冲突,且解决过程并非简单处理时,应在执行 --continue 或提交之前提供冲突审查。提供即可,不要强加:用一行询问,例如“解决过程涉及 4 个文件;继续变基前要做一次三方审查吗?”。“并非简单处理”是指超出以下情况:仅涉及空白字符的冲突、重新生成的锁文件,或明显意在整份采用某一方的更改。时机很重要:操作暂停期间,git 仍保留着各方的内容,审查仍有机会改变解决结果。变基会在每个冲突提交处暂停一次,每次暂停都应单独审查。
审查内容来自 git,而不是你:
.claude/skills/guided-review/changedFiles.sh conflict # paused operation
.claude/skills/guided-review/changedFiles.sh conflict <merge> # finished merge commit
它会使用 git merge-tree 重新运行 git 的合并(git 2.40+),并列出所有与 git 自身尝试结果不同的文件:首先是 git 无法合并的文件(冲突文件),然后是解决者修改过、但 git 已干净合并的文件(合并后编辑,例如重命名后修正的调用点,或解决者新建的文件,无论是否已暂存)。
git 已干净合并且无人修改的文件不会列出。它们从未由解决者作出决策。每个文件的类型会输出到 stderr,并在页面对应行上显示为标签。暂存解决结果不会丢失任何这些信息。
随后页面会在内联和并排视图之外提供三方视图,并默认以该视图打开:当前版本 | 解决结果 | 传入版本,逐行对齐。解决结果窗格可编辑,备注也写在这里。祖先版本不会有单独的窗格,但它决定了颜色,而颜色承载着审查信息:
- 解决结果中来自当前版本(青绿色)或来自传入版本(蓝色)的行;
- 不属于任何一方的解决者自行添加的行(琥珀色)——逐行审阅;
- 未纳入解决结果的各方行:如果是该方独有的更改,则标为舍弃(红色)——除非是有意舍弃,否则意味着工作丢失;如果是另一方无论如何都会删除的祖先行,则显示为淡色。
如果某一方移动了文件,系统会跟踪该文件:每一方的窗格都会显示该方对应的文件副本,无论它在该方位于何处;系统会通过 git 的重命名检测找到它。窗格标题会标出旧路径,列表行则会显示为重命名而非新增。祖先版本的副本取自保留旧路径的一方,因为编辑完成后,git 可能已无法将祖先版本的副本识别为解决结果的重命名来源。
两方视图会比较当前版本 → 解决结果。在备注中说明每项决策倾向哪一方及其原因,并标出解决者自行添加的内容。
如果手边没有实际冲突,可以运行
fixtures/conflictRepo.sh <new dir> [merge|rebase],创建一个临时仓库,并使其暂停在合并(或变基)过程中,同时预先写入解决结果。脚本的头部注释会列出解决结果对各文件所做的更改,方便你确认页面应显示的内容。
Forge 上的堆栈或仅在本地的堆栈
还要检查更改是否位于堆栈中:一连串分支,每个分支都基于它下方的分支。解决堆栈有两种方式,而且堆栈不必位于 forge 上也可以审查:
node .claude/skills/guided-review/resolveStack.ts 34 > stack.json # from a PR
node .claude/skills/guided-review/resolveStack.ts --stack > stack.json # from gh stack
-
从 PR 开始(数字、分支或 URL):沿着开放 PR 链逐个遍历,每个 PR 都基于其下方分支的头部分支。
-
--stack:读取gh stack view --json,获取此检出对应堆栈中的分支,无论是否已推送。只要仓库使用gh stack,就应使用此选项;对于尚无 PR 的堆栈层,也务必使用。提交前审查堆栈正是分层构建的主要目的之一;如果等到有 PR 才审查,就意味着审查已经晚了。只提交了部分层的堆栈也会作为一个整体解析:已有 PR 的层会保留其编号和链接。 它会按堆栈顺序(先显示主干端)打印{current, entries},并在 stderr 中说明是否找到了堆栈。每个条目都带有一个key——其 PR 编号;如果没有 PR,则使用分支名作为 slug。这个 key 用于命名堆栈目录中的文件、标识页面 URL 中的条目,以及在堆栈栏中标注条目。请从stack.json读取 key,不要猜测。 只有一个条目 → 不用管,什么都不会改变。两个或更多条目 → 页面可以承载此堆栈,形式有以下两种: -
只审查你的 PR:照常运行
build.ts --stack stack.json。页面顶部会多出一个栏:显示整条链,突出显示此 PR,并将尚未审查的同级 PR 灰显,同时附上 forge ↗ 链接。(diff 范围无需特殊处理——位于堆栈中间的 PR,其基础版本就是堆栈父级的 head,因此resolvePr.sh已经会生成与 GitHub 显示完全一致的 diff。) -
在一个页面中查看整个堆栈:创建一个堆栈目录,然后使用
buildStack.ts构建。该目录是多个 Agent 共同协作的协调点,每个 PR 对应一个文件,这样任何人都不会覆盖其他人的内容:<stackDir>/stack.json resolveStack.ts output <stackDir>/<key>.groups.json that layer's authored groups, one author each <stackDir>/<key>.instructions.md optional: a request for another agent to author the groups json above node .claude/skills/guided-review/buildStack.ts --dir <stackDir> --out review.html [--current <key|pr|branch>]将目录放在每个参与贡献的 Agent 都能从仓库的任意工作树找到的位置——约定路径为
$(git rev-parse --path-format=absolute --git-common-dir)/guided-review/stack-<trunk-most pr>。buildStack.ts会从refs/pull/<n>/head获取每个 PR,直接从检出中的分支读取仅存在于本地的层(那是唯一的一份副本),将每一层与其下方的一层进行 diff,并写出一个页面: 所有已撰写的审查都可以通过栏切换;没有 groups json 的同级 PR 会显示为灰色(如果存在 instructions 文件,则标记为等待审查)。本地堆栈完全不需要远程服务:如果没有origin,主干就是本地分支,也不会获取任何内容。 参与贡献的方式是:将你 PR 的 groups json 写入目录,然后重新运行构建——服务进程会重新读取 html,并在不重启的情况下创建新审查的存储;已打开的页面会发现新审查并自行重新加载。服务运行期间会持续跟踪堆栈。
resolveStack.ts会记录堆栈的解析方式(stack.json中的resolvedWith),服务器也会以相同方式重新解析——gh stack每 10 秒一次,PR 堆栈每分钟一次,这会请求 forge。PR 加入堆栈、离开堆栈或在堆栈中移动时,都会重写stack.json并重新构建页面;已打开的页面会重新加载(如果有未保存的编辑,则会先询问),服务器的输出会列出新加入且尚无审查的 PR——你可以主动撰写审查,或像上文所述,为负责该 PR 的 Agent 留下说明。早于resolvedWith的stack.json不会被持续跟踪;请重新解析以开始跟踪。
坦承你尚未读过哪些内容。 开始堆栈审查的 Agent 通常只研究过自己的 PR。请说明这一点,并提出补全其余审查的两种方式:现在派遣阅读 Agent,为某个同级 PR 撰写 groups json;或者编写 <pr>.instructions.md(包括范围、refs、json 的存放位置和重新构建的方法),留给负责该 PR 的 Agent——PR 作者写出的说明会比临时阅读者写得更好。绝不要默默地为你没读过的 PR 撰写浅显的审查。
同时查看所有层。 如果有两个或更多已编写的层,buildStack.ts
还会构建一个 全部 审查,并在堆栈栏末尾提供一个按钮。它将整个堆栈视为即将合入的单个更改来查看:从主干到最顶层的已编写层进行比较,因此每个文件的差异都是其累积后的完整更改,而不是某一层中的部分更改。每个文件只出现 一次,位于第一个涉及它的 PR 下方,其行会说明哪些 PR 修改了它——对于只被其中一个 PR 修改的文件,这就回答了“是谁改的?”。如果有多个人为某个文件写了备注,该行会包含所有备注,每条备注前都有各自的标签。页面显示 “PR 2/3”(三层堆栈中的第二个 PR,无论是否每个 PR 都有审查),而不是分支名称或“层”——读者需要知道更改在堆栈中的位置,而且无论堆栈是否已推送,显示都相同;分支或 PR 编号会显示在工具提示中。它有自己独立于各层的刻度和备注(它是单独的审查),因此,整体阅读堆栈和逐层阅读不会覆盖彼此的进度。
对于本地堆栈,栏中会显示分支名称,而不是 PR 编号;没有 PR 的层会显示为纯文本,而不是指向不存在页面的链接。fixtures/localStack.sh <new dir> 会构建一个三层本地堆栈供你试用。
已服务页面中的每个 PR 都可编辑;其中一个会保存到磁盘。 服务端 --repo 检出在磁盘上的分支,其头分支对应的审查(提交或工作树审查不会命名分支,因此按定义就是该审查)会像往常一样,将其整份 PR 的行以及未提交/工作状态的标记保存到磁盘上的文件中。其他 PR 的所有行——无论属于其他分支还是任何提交——都会保存到该 PR 的 暂存编辑(参见步骤 9 中的“编辑任意 PR”):由服务端作为引用保留,读者确认后可通过提交栏提交或修订。已暂存的更改以及整体阅读所有 PR 的审查仍为只读。每份审查中的备注、回复和刻度都可使用——每份审查都有各自 ID 下的 notes.json/ticks.json,因此 awaitNotes.ts/reply.ts 会指向备注所属的审查存储区。对于服务堆栈中的每个 PR,栏旁都会标明哪个分支已 检出;每个 PR 的徽标末尾都有一个 … 菜单(弹出菜单,用于对 PR 本身执行操作,而非阅读操作),其中的 将工作副本切换到此处 会在服务端检出中运行 gh stack checkout <branch> 并重新加载
(如果 git 拒绝切换,例如有未提交的更改阻碍操作,就会显示提示消息)。在其他位置进行的切换——例如通过终端或 Agent——也会被检测到:每次轮询都会报告当前检出的分支(git 正在执行变基或合并时除外,此时处于分离状态的步骤不算切换),页面也会随之重新加载;如果编辑器中有尚未保存的编辑,则会显示带有“重新加载”按钮的提示消息(checkoutSync.ts)。
2. 获取准确的文件列表和状态
.claude/skills/guided-review/changedFiles.sh worktree
.claude/skills/guided-review/changedFiles.sh commit <sha>
.claude/skills/guided-review/changedFiles.sh pr origin/<base> origin/<head>
.claude/skills/guided-review/changedFiles.sh conflict [<merge commit>]
它会输出 STATUS<TAB>PATH,PR 模式使用三点范围(与合并基准比较,和 GitHub 的视图一致——两点范围会把分叉后合入基础分支的无关提交也纳入),将未跟踪的 ?? 映射为 A,并排除没有可审查差异的二进制文件(字体、音频、归档文件)。栅格图像(png/jpg/gif/webp/avif)会保留在列表中:build.ts 会为每个图像提供对比查看器,而不是文本差异视图。
保留每个文件的状态列——它用于生成“新增/已修改/已删除/已重命名”徽标,并告知 build.ts 哪些文件需要与 /dev/null 比较。
找出移动而非修改的代码
对文件数量不止寥寥几个的每份审查都运行此命令。它开销很低(几百个文件只需不到一秒),而且审查中的其他部分都无法发现代码提取:
node .claude/skills/guided-review/moves.ts commit <sha>
node .claude/skills/guided-review/moves.ts pr <base> <head>
node .claude/skills/guided-review/moves.ts worktree
重构时,如果把一个文件拆开或将代码提取到新模块中,Git 会显示数百行删除和数百行新增。若读者不知道这些是相同的代码,就会把它们全部重新读一遍。moves.ts 会将它们配对,并将每个移动的代码块打印为 旧路径:行号 → 新路径:行号。它比较的是 token,而不是行,因此重新缩进、Prettier 重新排版调用,以及 Prettier 添加或删除尾随逗号,都不会影响移动识别。
每个代码块要么是原样移动,要么会附上改动:移动过程中发生变化的几行,旧内容在上,新内容在下。改动通常是形状发生变化的声明(bubbles: render( 变为 export const bubblesAppearance = render(),或为适应新位置而重写的调用;移动代码块中只有这些部分需要阅读。被修改的行仍可能部分属于移动内容:例如,用 this.addFloatingText(pickup, room, ["+2", "LIVES"]) 替换 4 行的 addItemToRoom({ … loadFloatingText(["+2", "LIVES"]) }),这属于一处改动,而其中的数组也随之移动。页面会对这部分进行底色标记。它还会按文件汇总每个文件移出的或移入的代码量。对于大部分代码都已移动的文件,它会列出除此之外新增的内容(导入语句除外),或者说明“除导入语句外没有其他新增内容”。在原位置修改代码(同一文件的同一代码块中既有删除行又有新增行)绝不算作移动。--json 会以结构化输出提供相同的数据。
在接下来的步骤中使用此输出:
- 阅读(步骤 5): 将对应代码块的
moves.ts输出提供给每位阅读 Agent,并告知它,移动的代码块已计入:它应阅读其中的改动和其他新增内容,而不必重新阅读代码主体。 - 分组(步骤 6): 将每个移动代码块的源位置和目标位置放入同一组,先列删除项,再列新增项,让提取过程一目了然。
- 备注(步骤 7): 说明代码已移动,并指出来源,然后列出改动。例如:“
bubblesAppearance从appearanceForItem.ts的表中提取出来,除了改为命名导出外没有其他变化。”或者:“handleItemTouchingSlidingItem.ts的主体,现在名为onTouchedBy。只更改了实心性检查:!isSolid(x)→x[itemBehaviourKey].isNonSolid(x)。”这样读者就能快速浏览代码主体,只仔细查看你指出的行。多个文件以相同方式移动时(例如将一个表拆分为每个条目一个文件),一条组说明即可涵盖它们,每个文件的备注则各用一行说明。
工作原理:删除的代码会被索引为一个 token 流,索引内容包括每个至少包含 2 个名称或字面量的 4-token 窗口,以及每条独立的、长度为 20 个或更多字符的注释或字符串。系统会查找新增代码中的每个窗口,并从每个命中位置向两侧延伸,直到 token 不再一致。优先采用最长匹配,且每个 token 只使用一次。
只有当一段代码在删除内容中只出现过一次,且至少包含 3 个名称或字面量、总长度至少为 24 个字符时,匹配才会单独计入(--min-chars、--min-words)。由于它在删除内容中是唯一的,因此不可能来自其他位置,所以仅一行也足以认定。常见代码(false,、return;)只有在由此类匹配锚定的代码块中才会计入,除非其长度足够长(120 个字符,--min-chars-common),长到不可能是巧合。同一对文件之间,按顺序排列且间隔不超过 4 行的匹配会合并为一个代码块(--max-gap)。导入语句不会参与匹配。
“在删除内容中只出现过一次”仅针对被删除的行检查,不会检查旧版本中的每个文件。在其他位置保持不变的代码属于复制,而非移动,因此不会被查找。build.ts 会自行运行相同的检测,因此无论是否运行此工具,页面都会框出移动的代码并标注其来源。运行此工具是为了撰写备注。
如果代码被移动,且其中的名称全部经过更改,则无法匹配。这是安全的,因为这类代码会像没有此步骤时一样,作为新代码进行审查。
提议预先勾选机械性变更
开始阅读前,使用 moves.ts 运行此命令:
node .claude/skills/guided-review/mechanical.ts pr <base> <head> # or commit <sha> / worktree
它会列出变更仅涉及机械操作的文件,并说明原因:
- **imports:**每一处变更行都是导入语句;
- **reformatted:**忽略空白字符和尾随逗号后,代码相同;
- **rename
old → new:**除一个名称被另一个名称替换外,标记相同,且旧名称已从分支中移除; - **moved:**代码原样移动到该位置。
只有当文件中的每一处变更都属于上述情况之一时,该文件才会列出。
如果有此类文件,请使用你的提问工具询问读者是否要从一开始就将它们标记为已读。引用数量和变更类型,例如:“258 个文件中有 54 个只变更了导入路径(另有一个文件还原样移动了代码)。要预先将它们标记为已读吗?”。如果读者同意,请在 groups json 的 meta 中设置 "preTickMechanical": true。
无论如何,页面都会在每个机械性变更文件旁标注“Mechanical”标记,悬停提示中会说明原因。开启预先勾选后,尚未勾选任何文件的审查会从这些已勾选的文件开始。它们仍在阅读顺序中,但会折叠;如果之后发生变更,也会像其他已读文件一样取消勾选。不要安排阅读 Agent 处理这些文件:给每个文件留一行说明,指出其机械性变更即可。
3. 指向代码托管平台的逐文件链接
build.ts 会根据 origin 远程仓库推导这些链接,因此通常无需操作:使用 --pr <n> 时,会将每个文件链接到该 PR 的 Files-changed 标签页;否则链接到 commit/<sha> 或 compare/<base>...<head>,并添加锚点 #diff-<sha256hex(path)>——这是 GitHub 自己的方案(路径与 git 打印的完全一致,不带 a/ 或 b/ 前缀;重命名使用新路径,删除使用旧路径)。它会打印最终确定的基础 URL。
--github <url>可覆盖推导结果;--github none则不添加链接。- 没有
origin远程仓库的仓库不会有链接,而不会导致构建失败。 - 非 GitHub 远程仓库不会有链接:各代码托管平台锚定差异的方式各不相同,而且页面本身已包含差异,因此不提供链接也胜过提供错误链接。
- 工作树模式没有可链接的目标;此时路径会显示为复制到剪贴板按钮,页面应对此加以说明。
该链接只是辅助功能——路径旁的小型“GitHub ↗”链接,方便离开页面发表评论,因为差异本身已显示在页面中。
4. 捕获差异是 build.ts 的工作
它会根据模式为每个文件运行合适的命令(git show、三点式 git diff,或对未跟踪文件使用 --no-index /dev/null),统计 +/−,并对差异为空的文件发出警告(通常是 groups json 中存在过时路径)。它还会打印文件数量、行数总计和最终文件大小——包含图片时,几 MB 很正常;如果大小激增,说明有内容应通过 --max-side-lines/--max-images 限制。
页面渲染所依据的是每个文件变更前后的完整内容(两侧分别使用 git show <rev>:<path>;在工作树模式中,“变更后”使用工作副本)——Monaco 会自行计算并折叠差异,而不是接收补丁。长度超过 --max-side-lines(默认为 2000)的文件不会嵌入;对应行会说明这一点,并指向文件树中的该文件。
图片文件会捕获多个版本,而不是变更前后两侧。 对 groups 中的每张栅格图片,build.ts 会捕获所有值得比较的引用所对应的字节内容——始终包括生产版本(最新的发布标签),然后按模式捕获:PR 的基准版本和分支版本、提交的父提交和提交版本,或工作树的head/stage/working版本;它会去除字节完全相同的版本,并将其作为每张图片的一个惰性 json 代码块嵌入(因此负载本身不会携带图片数据)。这样,页面查看器就能比较任意一对版本,而不仅限于构建审查时所针对的那一对。--max-images(默认为 100)会限制嵌入的图片数量;超出限制的行仍会显示,并标记为已省略。所有比较都在页面中进行——这里不依赖图片处理工具。
生成文件和锁文件值得直接从 groups json 中排除,也可以只保留一行说明而不读取其内容。
5. 阅读差异 — 委派给并行后台 Agent
如果涉及的文件不止少数几个,亲自通读完整差异会消耗上下文,而这些上下文更适合用于综合分析。将文件列表拆分为 2-4 个连贯的部分(例如,“核心算法/数据结构文件”与“所有使用这些内容的文件”——按照你组织阅读顺序时采用的相同逻辑拆分,而不是随意划分),并在一条消息中为每个部分分派一个后台 Agent(通用型,run_in_background: true),让它们并行运行。
不要将图像文件放进任何 Agent 的部分。 读取像素会耗费 Agent 的精力,对审阅者却毫无帮助,因为审阅者在查看器中一眼就能看到差异;页面也会自行计算每张图像像素变更百分比。为图像安排一行(放在所属分组中),最多补充一条说明,解释为什么它发生了变化,绝不要描述图像的样子。
如果你已在上下文中掌握了这些改动,则跳过此步骤——例如,你在本次会话中编写了这些改动。重新推导已知内容,得不偿失。
每条 Agent 提示词都必须完整自足(Agent 不会记得本次对话),并包含:
- 仓库路径;单提交模式下,提交 SHA 和一行提交说明;整个 PR 模式下,PR 编号/标题以及精确的
origin/<base>...origin/<head>范围;工作树模式下,说明改动尚未提交,以及如何查看差异;无论哪种模式,都要说明这是只读分析,不得进行编辑 - 足够的架构背景(引用相关的
CLAUDE.md部分),使 Agent 无需从头摸索系统 - 该部分的确切文件列表,读取命令须与步骤 1 的模式相符;并指示 Agent,如果仅凭差异片段难以理解,还要
Read其中内容最充实的 3-5 个文件的当前完整内容 - 要求汇报的内容:(a) 一段简短说明,概述该部分的整体变化;(b) 每个文件用 2-4 句话说明改了什么、为什么改,以及它依赖的其他文件(明确列出同一部分或其他 Agent 部分中的文件名);(c) 该部分建议的阅读顺序,并按具名分组整理,每组说明为何安排在该位置——每个测试文件都应放在其所测试代码所在的分组中,紧跟在该代码之后,绝不能单独设立测试分组
- 该部分的
moves.ts输出,并指示将已移动的代码块视为已读,除编辑内容外不必重复分析;对于已移动的文件,须用“从 X 移动,并新增了 Y”的格式报告,而不要重新描述其主体 - 明确指示不要压缩每个文件的说明——报告将供你综合分析使用,不会直接展示给用户,因此完整性比简洁更重要
6. 综合出一个宏观阅读顺序
等所有 Agent 都汇报后,将它们各自按代码块整理的顺序合并成一个序列。不要只把各代码块直接拼接起来——要判断每个代码块的内容实际应放在其他代码块的什么位置(例如,先介绍共用的底层原语,再介绍核心机制,然后介绍将其接入系统其余部分的集成点,最后介绍外围内容)。如果严格按依赖关系排序会把明显相关的工作拆散到文档各处,就按主题分组,而不是严格遵循依赖顺序。
测试要和它们测试的内容放在一起,绝不能单独成章。 测试可以佐证旁边的代码,说明代码的用途,以及曾导致问题的情况;而单独放在末尾的“测试”章节会让测试和代码脱节,等读者读到那里时,被测试的代码早已落在好几章之前。将每个测试文件放在其所覆盖代码所在的章节中,紧跟在该代码后面。一个测试覆盖多个章节时,将它放在直接检查最多的章节中;测试辅助工具和夹具则放在使用它们的测试旁边。
在每个分组内,按照读者实际应打开文件的顺序从头到尾排列:删除文件与新增文件的配对内容要放在一起,以便对照阅读;基础类型要放在使用它们的代码之前;每个测试都紧跟在它所测试的代码之后;仅涉及导入路径的简单重命名要批量放在一起快速浏览,而不是与实质性的逻辑改动同等对待。
7. 编写 groups json 并构建
只写一个 json 文件——这就是你的全部输出:
{
"meta": {
"title": "the one-line thesis of the change",
"headerTitle": "short name for the sticky header",
"eyebrow": "reading order",
"lede": "a paragraph on what the change is and why this order; html allowed",
"facts": ["branch <code>x</code> vs <code>origin/main</code>", "…"],
"footer": "html; eg where the ticks live, what was left out"
},
"groups": [
{
"title": "group name",
"blurb": "one sentence on why this group sits here",
"items": [{ "path": "src/…", "status": "M", "note": "2-4 sentences" }]
// blurb and note both take html too - <code> especially
}
]
}
逐个提交编写 PR
PR 是一串提交,就像一组堆叠的 PR 是一串 PR;提交记录了更改是如何编写的:每个提交都是一个步骤,都有自己的原因。如果一个 PR 中有多个值得分开阅读的提交,就按提交来编写,而不是把它们合成一大块——将 groups 替换为 commits:
{
"meta": { "title": "…" },
"commits": [
{ "ref": "<sha>", "groups": [ { "title": "…", "items": [ { "path": "…", "status": "M", "note": "…" } ] } ] },
{ "ref": "<sha>", "groups": [ … ] }
]
}
ref可以是 Git 能解析的任何内容;构建过程会读取它的短 SHA 和主题。- 每个提交的章节只阅读该提交本身:某个文件在此处的差异是
git show <sha> -- <path>,而不是 PR 的整个差异范围。同一个文件在三个提交中都有更改,就会阅读三次,每次都按该提交对文件所做的更改来阅读——这正是重点。为 那个 提交写说明:这一步做了什么以及为什么要做,而不是概述整个文件。 groups和commits只能二选一。构建过程不接受两者同时存在。"ref": "uncommitted"是唯一不对应提交的 ref:工作树中尚未提交的内容,会与HEAD比较(对于 Git 从未见过的文件,则不与任何内容比较)。将它放在最后——它代表分支自上次提交后推进到的位置。它的行对应磁盘上的文件,因此与提交的行不同,它们始终可编辑,并与磁盘内容保持同步;栏中的其他所有内容都属于历史记录。
页面随后会在堆叠栏下方显示一个提交栏:包含该 PR 的每个提交,以及 all,默认从这里开始——整个 PR 仍然是审查对象。阅读所有提交时,每个提交的章节都位于以该提交命名的标题下;如果同时启用了逐个 PR 审查,PR 是顶层标题,提交则位于各自的 PR 下。
勾选针对的是某个提交中的文件,而不只是文件本身。同时阅读所有提交时,勾选一个文件会在该文件出现的每个提交中标记它(在逐个 PR 审查中,也会跨 PR 标记),因此阅读顺序仍会随着阅读逐渐清空;只阅读一个提交时,勾选仅属于该提交。提交的行不会保存到磁盘——磁盘上不存在该文件在该提交中的状态——但它们可以编辑:编辑内容会保存在该提交中,可从提交栏将其提交或修订到该提交(参见“编辑任何 PR”);当提交被重新生成时,勾选状态会跟随该提交。注释仍按文件和行索引,并由所有修改该文件的提交共享。
fixtures/commitStack.sh <new dir> 会创建一个包含两个 PR、共五个提交的堆叠结构,供你试用。
每个更改过的文件必须恰好归入一个组——即使一个文件涉及两个主题,也只放在最适合阅读的组中;其他组的简介或注释可以指向它。build.ts 会拒绝列出重复路径的 groups json,并指出该路径出现在哪些组中。
文件数和行数会自动计算——不要将它们填入 facts。然后:
node .claude/skills/guided-review/build.ts \
--groups <scratchpad>/review.json \
--out <scratchpad>/review.html \
--mode worktree
# a commit: --mode commit --ref <sha>
# a pr: --mode pr --base <base> --head <head> --pr <n> (refs from resolvePr.sh)
# a conflict resolution: --mode conflict (--ref <merge> for a finished merge)
仅审查快照的流程完全自动化
如果需求是“审查更改过的截图基线”,而不是审查整个更改,就无需手动编写——snapshotGroups.ts 会自行生成 groups json(groups = 快照目录,按字母顺序排列,不含注释;页面会计算每张图片的变化百分比行):
node .claude/skills/guided-review/snapshotGroups.ts --mode worktree --out <scratchpad>/groups.json
# or --mode pr --base <base> --head <head>, --mode commit --ref <sha>
# --glob 'e2e/*-snapshots' bounds what counts as a baseline (that is its default)
node .claude/skills/guided-review/build.ts --groups <scratchpad>/groups.json \
--out <scratchpad>/snapshots.html --mode <same mode and refs>
然后按常规方式启动服务。这适用于“重新生成了基线——差异正确吗?”这类场景,包括在手机上阅读。
接着启动服务(第 8 步),并提供该 URL。重新构建并刷新即可反复迭代;build.ts 会原地覆盖,因此服务会持续提供最新构建,无需重启。
编写注释
每个文件的注释和分组简介都是写给此刻正在阅读这项更改的读者的——这与代码注释不同,后者必须让从未见过旧版本、并且多年后才会阅读的人也能看懂。因此,这里适用的规则恰恰相反:评审注释可以着眼于历史。“这里以前会把房间加载两次”“以前只是两个裸引用”“第 4 组接下来的修复”这些说法在评审中都恰到好处,放在源码里却都不合适。要说明改了什么以及为什么改,而不只是描述代码现在做什么——代码就在注释旁边的屏幕上,重复它只会浪费读者的注意力。
图像行通常完全不需要撰写注释:页面会在每张图下方自动添加一行 WxH — N% of pixels differ,而查看器比文字更能展示变化。只有当周围分组无法显而易见地说明更改原因时,才写注释。
8. 交付——能访问时提供服务,不能访问时发送文件
首先自行判断当前属于哪种模式,依据你对运行环境的判断——这里没有环境变量可以说明这一点,所以要根据你对会话的了解来推断:
- 评审者可以访问你打开的
127.0.0.1端口——例如普通的本地终端会话、通过 SSH 登录到评审者自己机器上的会话,或任何可以实际“打开浏览器访问 localhost:<port>”的环境。提供服务,严格按本节所述操作:这是功能最丰富的形式——可编辑的差异会保存回工作树,还有实时注释通道,你可以根据评审者写下的内容采取行动。 - 评审者无法访问——例如云端沙箱、CI 作业,或没有将端口转发到评审者浏览器的远程机器(那里的服务 URL 一开始就无法访问)。不要提供服务。切换到下方的不可访问模式:构建好的
review.html本身就是一个单文件,会读取window.__reviewServer并据此运行,因此直接将它原样交付即可。
如果确实无法确定属于哪种情况,请询问评审者,不要猜测:在无法访问的地方提供服务,会给他们一个失效链接;而本可以提供服务却改为发送文件,则会白白失去可编辑差异和实时注释。
从这里到“发布”的所有内容都属于可访问路径。在不可访问模式下跳过这些内容,直接前往“### 不可访问模式”。
# reachable mode only:
node .claude/skills/guided-review/serve.ts --html <scratchpad>/review.html --repo . --open
--repo 必须指向被评审提交的检出副本——在 pr 模式下,必须是该分支自己的工作树,而不是你碰巧所在的位置。这是保存内容和由注释驱动的编辑会写入的地方,实时文件轮询也会将打开的编辑器与其中的磁盘文件进行比较。在堆栈页面中,检出副本所对应的、包含某个评审的头部分支就是可编辑的;服务会自行判断这一点,其他分支则会安全地降级为只读并可添加注释。
审查跟随的是分支,而不是一对提交。
-
**编辑:**即使传入
HEAD或 SHA,build.ts也会将头记录为分支名称,服务端会按该名称匹配检出内容。因此,编辑在新提交、变基和修订提交后仍会保留。若头仍是裸 SHA(因为没有单个分支指向它),则会匹配包含该 SHA 的检出内容。 -
**分支移动时重新构建:**提交、变基、修订提交或合并基分支都会移动头提交或合并基点。服务端会在下次轮询时发现这一变化;如果变基或合并仍在进行,则会等待,然后在后台重新运行
build.ts。它会使用首次构建时嵌入页面的配方(#recipe:参数中将头命名为分支、作者编写的 groups json,以及被排除的文件),并保留审查的 ID,因此备注和勾选状态会沿用。重新构建的 HTML 会替换旧文件,因此重新加载或重启服务端后也会看到新内容。页面会在下次轮询时载入新构建:两侧内容、计数、移动代码框、重命名、图片和链接都与全新构建完全一致。备注、勾选状态、打开的差异、滚动位置和未保存的编辑会保留。重新构建使用的代码与构建相同,因此它得出的任何内容都不会过时。这只适用于单个 PR 或工作树审查,不适用于堆栈或逐提交阅读。 -
**文件列表:**服务端会将当前范围与阅读顺序进行比较。自构建后进入范围的文件(新文件、新近更改的文件或重命名的文件)会在最后一章“撰写此审查后发生更改”中新增一行,并附带实时差异。服务端会为每个文件打印
new in scope: <path> - not in the reading order。看到这条信息时,请在 groups json 中添加该文件并附上备注,然后重新构建。构建时已在范围内但未纳入阅读顺序的文件(锁定文件、生成的代码)会记录为leftOut,且永远不会显示。某个列出的文件若不再属于更改范围(已还原、添加后删除、重命名移出),就会从视图中消失;如果更改再次涉及该文件,它会回到原来的位置。服务端也会打印这类信息。每个列出的文件都会按当前状态显示,因此,已删除文件上的修改会显示为 D。行按路径而非位置标识,因此行消失后再出现,不会移动已打开的差异或折叠项。 -
**提交栏也会跟随分支。**跟随分支的已提供审查始终有提交栏,即使只有一个提交也一样:“全部”(审查撰写时的内容)、分支自基点以来的每个提交,然后是 暂存(
HEAD→ 索引)和 工作区(索引 → 磁盘),只有包含内容的项目才会显示。服务端会在每次轮询时列出这些项目(/state中的stops),因此git add、编辑或提交都会立即显示。某个项目显示的内容是按更改文件筛选后的阅读顺序,另外还会有一章“未纳入阅读顺序”,列出其余文件。首次阅读时,服务端会获取其两侧内容(/stop-sides),文件发生变化时也会再次获取。工作区行可编辑并保存到磁盘。暂存行只读,并会提示应在哪里编辑。提交的行会保留针对该提交所做的编辑(参见“编辑任何 PR”)。重新加载某个项目时会返回该项目。按提交逐个撰写的审查则保留自己的提交栏。 -
**读过后发生更改:**已勾选文件的差异若发生变化(磁盘上编辑、变基),该文件会取消勾选并展开,并显示“阅读后发生更改”,直到再次勾选。无论差异是否打开,行数都会随每次更改更新。
-
**重新构建之间不会跟随的内容:**磁盘上的未提交编辑会实时更新后侧内容和计数,但移动代码框和图片要等到下次重新构建才会更新;只有提交或变基会触发重新构建。提交审查或逐提交审查属于历史记录:其提交列表和行都是固定的。
-
**编辑器保存到哪里:**编辑器下方的提示会说明保存位置——磁盘、PR 保留的编辑(“编辑会保留在此提交上……”),或只读的原因(暂存更改、所有 PR 审查均为完整只读)。 它会打印一个
http://127.0.0.1:<port>/——把这个地址作为评审链接。请在后台运行,以免阻塞会话,并说明使用的端口。 通过服务提供评审页面,能实现文件(或已发布的页面)做不到的三件事: -
可编辑的差异。 每个 Monaco 编辑器中修改的一侧都会变为可写,并出现 Save 和 Revert 按钮。每次保存都会携带页面构建时所依据的 sha256;如果文件在此期间已在磁盘上发生变化,就会返回 409 错误并提示 "changed on disk — reload",绝不会静默覆盖。仅限 localhost 访问,需通过令牌验证,路径限制在仓库内。Revert 只会在编辑器中恢复到构建评审页面时的内容,并保留未保存状态——未经明确点击 Save,任何内容都不会写入磁盘,因此误点也不会造成损失。
-
持久化的备注。 按
alt-N(或使用编辑器的上下文菜单)可在当前行添加备注;通过服务提供页面时,备注会写入<store>/notes.json和 Markdown 格式的副本<store>/notes.md,因此重建后仍会保留。“Copy notes”可将备注复制出来,作为 PR 评论。 -
存为文件而非浏览器数据的阅读标记。 已读内容会写入
<store>/ticks.json——因此即使重建到其他临时目录或改用其他端口,阅读标记仍会保留;同一评审页面的两个标签页也会保持同步,而且你可以查看阅读进度(cat <store>/ticks.json),而不必开口询问。未通过服务提供页面时,阅读标记会退回使用以评审 id 为键的 localStorage。
<store> 的路径为 <html 所在目录>/<review id>/,其中包含 ticks.json、notes.json、notes.md 和 token 这四个文件。serve.ts 会在启动时打印该路径,build.ts 会打印它生成的 id。id 根据仓库以及评审的内容(提交、pr 编号或工作树所对应的分支)生成,而非根据 html 的写入位置生成,因此明天重新构建同一评审时可以接续使用原有数据;当同一范围内的两个评审需要彼此区分时,可使用 --id 覆盖该 id。
PR 上已有的评论
这里正在审查的 PR 通常在 GitHub 上也有评论——来自同事或自动审查工具。将它们带到页面中:
node .claude/skills/guided-review/prComments.ts --html <scratchpad>/review.html --repo . --watch 60
每条带入的审查都会找到对应的 PR(如果是堆叠 PR,则使用其编号;否则使用其头部分支对应的 PR),并将评论写入 <store>/github.json。页面现有的轮询会在几秒内获取这些评论。--watch 会持续读取,因此审查进行期间留下的评论也会显示出来;请在后台运行它,并同时运行 serve.ts。如果审查没有对应的 PR——例如提交、工作树,或尚未推送的堆叠层——则会跳过,页面不会显示这些内容。
页面中:
- 行内审查讨论串会显示在 diff 中对应的行上,标记为 GitHub,并显示每条评论、作者和时间,同时附有指向代码托管平台上讨论串的链接。
- 无处可放的讨论串——例如针对已被更改删除的行,或分支更新后遗留的讨论串(代码托管平台称之为 outdated)——会显示在文件行下方,并注明原因。
- 已解决的讨论串会折叠为一行,点击后可再次展开。它们不会被隐藏:回复讨论串可能会在代码托管平台上将其标记为已解决(GitHub 会对其自己的审查者讨论串这样做),如果讨论串在你回复后立即消失,就会让人觉得回复没有发出去。在这里回复的讨论串会在本次会话期间保持展开。
- 文件路径旁会显示未解决讨论串的数量,使用
--pr的颜色,并显示在本地备注数量旁边。 - PR 自身的对话和每次审查的结论会显示在阅读顺序上方的可折叠栏中。
回复会发布到 GitHub,身份是 gh 当前登录的用户——可以在讨论串中的输入框回复,也可以在 PR 栏中回复。只有在页面中主动点击才会发布;服务器会在发布时重新读取 PR,因此讨论串会立即显示回复。Agent、监视进程或打开页面都不会发布任何内容。
请向审查者说明页面中两种评论的区别:备注是写给你(Agent)的,会保留在本地;GitHub 回复会发送到 PR,供团队其他成员查看。
及时处理备注
备注是请求,不是归档内容。不要等到别人来催,也不要拖到审查结束时才集中处理——审查者正坐在那里;趁他们还在查看对应文件时处理备注,价值远胜于一小时后再处理。
先确认收到,再考虑如何处理:
node .claude/skills/guided-review/ackNote.ts --notes <store>/notes.json \
--path src/ui/useStableValue.ts --line 7
页面中的“已发送,等待 Agent 处理”会在此操作完成后立即变为“Agent 正在处理”,这是准确的状态提示,而不是无论有没有人在监听都会播放的动画。只有当你准备立即使用 reply.ts 直接回复时才跳过此操作:真正的回复已经表明你正在处理,先确认收到只会徒增一次往返。
备注到达时,有两种处理方式:
-
请求明确(“将这个移到 src/util/preact”“这里应该用 const”“从此次更改中移除此文件”)——立即进行修改,就在审查者从该行进行编辑时会修改的位置,然后在讨论串中告知对方。备注会记住审查者撰写时正在查看的内容;如果来自服务端检出的 PR,就完整读取该 PR,并将其修改写入磁盘;如果来自其他位置——另一个 PR 或任何提交——则写入该 PR 的暂存编辑,供审查者提交或修订。你的修改也遵循相同规则,因此会出现在审查者会查看的位置,并由他们决定哪些内容最终成为提交。
held.ts会判断应采用哪种方式:node .claude/skills/guided-review/held.ts open --html <review.html> --repo <checkout> \ --notes <store>/notes.json --path src/ui/useStableValue.ts --line 7它会说明该行会保存到磁盘——像往常一样编辑检出目录中的文件——或者创建一个临时工作树,其中包含该 PR 截至该提交的内容,并应用已有的所有暂存编辑。在那里使用你惯用的工具编辑,然后运行
held.ts save --dir <that worktree>,将所有更改纳入暂存编辑,效果与在页面上保存完全相同,并移除工作树。切勿自行检出分支、提交或 amend:是否提交暂存的内容由审阅者决定,并通过提交栏操作。如果已有针对该 PR 的暂存编辑基于另一个提交,open会拒绝操作——每个 PR 只能有一个位置——因此请先在讨论串中请对方提交或 amend 这些编辑。然后回复,并通过--edited列出你编辑的每个文件(每个文件使用一次)——回复末尾会附上这些文件的列表,每个文件都是指向页面中相应文件的链接,链接指向编辑实际落地的位置(磁盘上,或暂存编辑的停止处):node .claude/skills/guided-review/reply.ts --notes <store>/notes.json \ --path src/ui/useStableValue.ts --line 7 \ --html <review.html> --repo <checkout> \ --edited src/util/preact/useStableValue.ts --edited src/ui/Panel.tsx \ --text "moved to \`src/util/preact\`, import sites updated"--text使用 markdown:code、粗体、列表、链接、围栏代码块。根据需要使用——将符号名放在反引号中,用围栏代码块展示两行代码片段——并保持简短;内容会显示在差异中。使用$'...'或 heredoc 传递多行文本。页面会在几秒内更新:文件差异会从磁盘重新加载,其
+N −M计数会更新,toast 会说明发生了什么,你的消息也会显示在审阅者自己的备注框中。对于已经请求过的事项,不要重复请求;也不要等到审阅结束才处理。 -
需要讨论——在讨论串中使用
--asking提问,并且只问那个能让你继续处理的问题。审阅者会在同一个框中输入答案,答案会通过awaitNotes.ts返回给你。只有收到答案后才进行编辑。不要停下审阅,在终端里提问:对方正在看页面,不是在聊天。
通过备注到达时所用的媒介回复,并且只用那个媒介。备注来自页面,而不是聊天——使用 reply.ts 在对应讨论串中回复,不要打扰聊天对话。在讨论串和聊天中各说一遍,并不代表更周全,只会让人困惑。
用一行说明你做了什么,不要写成一段——备注框很小,而且对方正在审阅。
备注会通过三种方式送达:
- 监视
notes.json:运行awaitNotes.ts,采用你的宿主环境最适合的方式。参见下文“保持备注通道畅通”。这是最重要的方式,因为这是备注在审阅过程中送达你的唯一途径。 - 每条备注也会实时显示在服务自身的输出中:
note <path>:<line> <text>。 - 点击页眉中的“Send notes to agent”,会在
=== N review note(s) handed off ===标记之间输出所有备注,并重写<store>/notes.md。这表示审阅者在说“我完成了,交给你处理”。告知对方备注已在此处。
如果某条备注含义不明确,就针对那条备注提问,而不要停止审阅;页面在你们沟通期间仍会正常工作。
页面遇到冲突:主动提出解决
提交暂存编辑时,会重放 PR 后续的提交,而 Rebase upstack 会重放上游的 PR。如果重放时发生冲突,不会进行任何更改;页面会在冲突发生的 PR 的审阅中,将一条备注发送给你,内容针对第一个冲突文件,说明涉及哪个提交、要应用到哪里以及涉及哪些文件。在回复中主动提出解决冲突;当审阅者同意(或要求你直接处理这类事项)时:
node .claude/skills/guided-review/restack.ts resolve --html <review.html> --repo <dir> --dir <scratch>
# fix the conflict markers in the files it lists, in <scratch> - each side is labelled
node .claude/skills/guided-review/restack.ts resolved --html <review.html> --repo <dir> --dir <scratch>
resolved 会记录你针对特定父提交(一个引用,因此记录会持续保留)解决特定提交冲突的修复,并移除工作树;它不会触碰任何分支。在讨论串中告知审阅者,再次按下同一个按钮就能顺利完成。restack.ts status 会说明哪些内容已暂存、哪些已过期,以及最近一次冲突是什么。
保持备注通道畅通
没人看的便笺比没有便笺更糟:审阅者会以为自己的意见已经被听取。每写下一条便笺,监视器都必须在整个审阅过程中把你唤回来。具体如何安排,取决于宿主能做什么,而不同宿主之间差异很大。awaitNotes.ts 提供两种模式来适配这些情况:
# streams each new note as it's written, and never exits on its own
node .claude/skills/guided-review/awaitNotes.ts --notes <store>/notes.json
# waits for new notes, prints them and exits - re-run it after every one
node .claude/skills/guided-review/awaitNotes.ts --notes <store>/notes.json --once [--timeout <seconds>]
--once 多次运行之间不会丢失任何内容。已报告的内容会保存在
<store>/awaited.json 中,因此你忙碌时写下的便笺,对下一次 --once 运行来说仍是新的,并会立即返回。重新启动始终安全。审阅过程中,请勿删除或手动编辑此文件,否则下一次等待会将其中已有的所有内容都视为已读。
先判断你的宿主属于哪一类,并使用它所支持的最可靠方式。 下文提到的宿主信息来自其文档和更新日志,统计截至 2026 年 9 月。这些信息很快就会过时:这些工具每隔几周就会增加新功能。如果你的宿主现在提供了比此处所列类别更可靠的唤醒方式(例如,原本只有退出通知,现在有了流式监视;原本没有退出通知,现在有了;内置文件监视;推送通道),请改用该方式。 同样,如果所列方式在你的会话中不起作用(工具缺失或已禁用,或始终收不到唤醒),请改用下一类方式,不要盲目相信此处的列表。
1. 能在每行到达时唤醒你的流式监视。 Claude Code 的交互式 CLI,搭配其 Monitor 工具。在运行 serve.ts 后立即通过 Monitor 启动流式模式,并在每条便笺作为事件到达时处理它。Monitor 监视可能会在截止时间到达时结束。一旦结束,请立即重新启动:不会遗漏任何内容,因为重启后会从 awaited.json 中继续读取。Monitor 并非在所有情况下都可用(例如,某些提供商不支持,或已禁用非必要流量)。如果无法使用,请采用第 2 类方式。
2. 命令退出时会唤醒你的后台命令。 Claude Code(run_in_background)、Cursor(IDE Agent 和 cursor-agent)、GitHub Copilot CLI、VS Code 中 GitHub Copilot 的 Agent 模式(后台终端通知,自 VS Code 1.116 起默认启用)、opencode v2(调用 Shell 时设置 background: true),以及 Gemini CLI,但 Gemini CLI 只有在将 tools.shell.backgroundCompletionBehavior 设为 "inject" 时才适用(其默认值 silent 不会唤醒任何内容)。
- 在后台运行
--once。有便笺到达时它会返回,命令退出便会把你唤回来。处理便笺(ackNote.ts,然后进行编辑并调用reply.ts),接着立即重新启动后台--once。 - 这些唤醒并非完全可靠:有些宿主偶尔会漏掉或延迟唤醒。VS Code 的通知可能要到你下一轮才会送达,而不会在聊天空闲时启动新一轮。因此,还要在每次回复结束时检查是否仍有
--once在等待,如果没有,就重新启动。多亏了awaited.json,这样做不会有任何损失。 - 流式模式在这里没用。它永不退出,因此也不会唤醒你。
3. 仅支持阻塞命令。 OpenAI Codex(CLI、IDE 扩展和应用)、Zed 的 Agent、Goose、Cline、Windsurf/Cascade、Amp,以及使用默认设置的 Gemini CLI。在轮次之间不会有任何唤醒。你只能看到正在等待的命令所输出的内容。
- 在前台运行
--once --timeout <seconds>,并确保超时值低于主机终止命令的时限。部分限制:Gemini CLI 会在命令 无输出 300 s 后终止命令(而--once在等待时不会输出任何内容),因此在那里请使用--timeout 240或更短的时限。opencode v1 的默认时限为 2 分钟。Codex 每次最多轮询运行中的命令 5 分钟。如果不清楚时限,240 是安全的选择。 - 如果提示“没有新备注”,就再次运行。如果有备注,处理后再次运行。这样一来,你的回合会一直保持打开状态,直到评审结束,因此期间无法聊天。请一开始就说明这一点;当评审者表示已完成,或打断你开始聊天时,就停止。
- 每次回复都要把下一次等待作为最后要做的事,包括回复与备注无关的聊天。没有以等待结束的回复,会导致评审中断。
- Codex 也可以通过 Stop hook 运行等待;该 hook 会以备注为原因阻塞,并在收到备注后继续会话。只有评审者希望这样做时才设置,因为这会修改他们的 Codex 配置。
4. 回合之间不传递内容。 云端 Agent(GitHub Copilot 的编码 Agent、Codex cloud)和不会自行运行命令的 Aider。云端 Agent 无法为评审者的机器提供服务,因此请使用不可访问模式(见下文)。否则,只有评审者通过“发送备注给 Agent”和聊天消息交给你,或粘贴 notes.md,你才能收到备注。请一开始就明确说明,避免他们等待不会到来的回复。
无论属于哪种情况:
- 在独立的后台终端中运行
serve.ts,不要在等待的终端中运行,这样等待就不会阻塞它,也不会导致它被一并终止。 - 用一句话告诉评审者如何把备注传给你:实时传递、回合之间传递,或仅在手动交给你时传递。如果备注看起来没有得到回应,在聊天中输入任意内容即可让你继续处理。
- 评审结束后,停止监视进程(流式监视,或仍在等待的后台
--once进程)和服务。
这两种方式都仅支持 Monaco,且 Monaco 由 serve.ts 本身提供——它会在 /vs 提供自己的 monaco-editor 安装包,没有该安装包就拒绝启动,因此通过服务提供的评审完全支持离线使用(页面会显示 离线 — 备注在本地排队 标签,备注讨论串会提示 Agent 稍后处理,但所有功能仍正常运行)。任何静态服务器都能提供页面供阅读,但只有 serve.ts 会响应 /vs、/save 和 /notes。
不可访问模式(沙箱):原样发送文件
如果审核者无法访问已提供服务的端口,构建好的 review.html 已经是一个完全自包含的单文件(字体和 UI 运行时已内嵌,所有前后内容及图片均已嵌入),会在运行时根据
window.__reviewServer 切换行为:只有 serve.ts 会注入该变量,因此直接打开页面时会干净地处于只读模式——逐行备注撰写 UI 不会启用(没有代码槽中的 + 符号、alt-N/上下文菜单操作或视图区域),保存/还原/“发送备注给 Agent”也会保持隐藏。没有服务端时,代码行不会显示差异:Monaco 是仅由 serve.ts 提供的依赖,未提供服务的代码行会明确说明这一点,而不会降级显示。其他功能都可用:阅读顺序、分组、简介、目录侧边栏及其可拖动的分隔栏、每个文件的备注文本、状态标签、路径/GitHub 链接、图片对比查看器及其变化百分比行,以及勾选项、按文件折叠和进度条。无需删减内容,也无需再次构建——第 7 步中的 build.ts 已打包页面并写入最终 HTML,此模式交付的正是那个文件。
将 <scratchpad>/review.html 交付给审核者:
SendUserFile(display: "render")是默认选项——审核者在本地打开文件并逐项勾选,以只读方式查看(图片完整显示;代码行会提示需启动服务才能查看差异)。如果他们想给你反馈,可以将内容粘贴到聊天中。- 如果他们想要一个方便分享给团队成员的链接,则使用
Artifact(参见“发布”)——托管页面,而不是文件。
发布,仅在无法使用端口时
只有在无法打开或访问本地端口的情况下,才使用 Artifact——例如云沙箱,或审核者使用另一台机器。然后直接发布 review.html(即使没有后端服务,它也已经是只读的),并设置符合主题的 favicon 和一句话的 description;重新发布到相同路径会保留 URL。
请了解这样做会放弃哪些功能:Artifact 托管服务的 CSP 只允许同源连接,因此已发布的页面无法访问 127.0.0.1(也无法访问任何隧道——受限的是 connect-src,而不是协议;此外,Chrome 的私有网络规则也会阻止连接)。页面本身就是只读的:无法保存,备注会回退到该浏览器的 localStorage,代码行也没有差异(Monaco 仅由 serve.ts 提供)——已发布的审核仅供查看文字内容、分组和图片。
9. 修改页面本身
仅当页面需要呈现或行为有所不同的时候才阅读本节——否则请完全跳过。
**提交前先测试:**在该 skill 的目录中运行 pnpm check:type 和 pnpm test:e2e。仓库的 CLAUDE.md 说明了测试套件的运行方式以及如何添加测试。页面的大多数功能只能在浏览器中看到,而这里的大多数 Bug 都是对先前行为的回归,因此请运行全部测试。编辑 src/,确保之后的每次审核都能继承这些更改。开始前先加载 artifact-design skill。这是一个实用的参考工具,不是编辑式页面——应采用精致的字体层级和真正的小型配色方案,不要有主视觉区,也不要做装饰。
这是一个由 vite 构建的 preact 应用
src/ 是一个常规的 preact + tsx 应用,基于 preact 11,并由 buildPage.ts
打包(vite,库模式,write: false)为一个 es module 和一个样式表字符串,然后由
build.ts 将它们与字体和 payload json 一起内联到页面中。运行时不会获取任何内容,除了
monaco;它仅来自 serve.ts 自己在 /vs 处的安装——没有 cdn,也没有备用渲染器:如果
monaco 缺失,就会如实报告故障;未提供服务的页面中的代码行会提示要提供 review 服务。
- 在 skill 目录中运行
pnpm exec tsc --noEmit可对其进行类型检查;整个应用都有类型, 包括手写的 monaco api 使用部分说明(src/monacoApi.ts),因为 monaco 是以脚本全局变量 的形式加载,而不是作为打包导入加载。 - 页面在
build.ts的page()中组装:字符集、<style>、#app、 `` 标记、payload 和模块脚本。payload 中的每个<都会转义为\u003c——这是一种有效的 json 转义,解析器会自动还原,因此经过审核的文件即使引用该标记 或结束脚本标签,也无法跳出它所在的代码块。 src/page.css包含完整样式表,FONT_B64是build.ts填入的唯一 占位符。
两个结构负责在各部分之间传递状态:
App持有阅读状态——ticked、collapsed、closedGroups、openDiffs、activeId——并将同一个state对象传给页眉、 侧边栏和各组。这就是侧边栏中的勾选和行上的勾选属于同一个勾选状态的原因;不要在任何地方 再添加一份。- 对于由命令式部分持有的内容,使用
makeStore/useStore——备注 (由 monaco 视图区域和服务器轮询写入)、toast,以及内联/并排显示选项(所有 monaco 编辑器都会读取, 无论是否已挂载)。 任何从notesStore渲染的内容都会在收到备注时重新渲染,这就是备注计数会同时出现在侧边栏和行标题中的原因。
Monaco 保持命令式——它拥有自己的 dom。createDiffEditor 会创建一个
编辑器,并通过 MonacoDiff 传入的 setter 回传信息;备注区域则是将 preact 树通过 render
渲染到区域的 dom 节点中。
配色方案:VS Code 自带的默认主题
这是一个代码审查工具;它沿用了开发者每天用来阅读差异的工具配色,而不是另行设计一套。每种颜色都对应 VS Code 自带默认主题中的一个键——自 1.113 起作为默认主题发布的 2026 Dark / 2026 Light——直接读取自 microsoft/vscode 中的 extensions/theme-defaults/themes/2026-{dark,light}.json:
| token | 来源键 | 在审查页面中的用途 |
|---|---|---|
--paper | editor.background | 页面底色 |
--card | sideBar.background | 章节卡片、页眉、提示消息 |
--control-bg | checkbox.background | 中性按钮/复选框填充色 |
--rule | panel.border / sideBar.border | 分隔线 |
--ink / --ink-soft / --ink-faint | editor.foreground / descriptionForeground / disabledForeground | 文本层级 |
--accent / --accent-hover | button.background / button.hoverBackground | 链接、主要操作 |
--accent-ink | button.foreground | 实心 --accent 填充色上的文本 |
--new / --del | editorGutter.addedBackground / deletedBackground | 新增/删除标签文本、差异统计 |
--mod | notificationsWarningIcon.foreground | 修改标签文本 |
--pr / --pr-bg / --pr-edge / --pr-ink | charts.purple | 堆栈中的 PR:堆栈栏、“全部”开关、行内标签、汇总笔记标签。除此之外别无用途,因此无论在哪里出现,PR 引用都能一眼识别——请保持如此 |
--new-bg / --mod-bg / --del-bg | diffEditor.insertedTextBackground / gauge/warning tint / removedTextBackground | 标签和提示框填充色,均为半透明——VS Code 自带颜色一贯大量使用透明度,这套配色也如此 |
page.css 自带的 :root 块在每个值旁的注释中列出了完整的当前 token 集合——如果与本表不一致,请以该块为准;本表仅作概述。边框已恢复(VS Code 自带 UI 到处都在使用边框);页面自身的 monaco 差异主题(monacoLoader.ts)也会为 editor.background/diffEditor.*/editorLineNumber.foreground 引用相同的值,因此本页面中的差异看起来就像 VS Code 本身的差异视图。
没有自定义网页字体:--display 和 --body 都使用普通的系统字体栈(system-ui, -apple-system, "Segoe UI", Roboto, sans-serif),与 VS Code 自带工作台一致;它同样没有单独的展示字体——标题通过字号和字重区分,而非字体。路径和差异内容继续使用 ui-monospace。
页面目前已有的功能——编辑时请予以保留
- 固定页眉:实时进度条和已完成/总数,全部展开、全部收起、清除勾选项,以及 Diffs 选择框 (并排,这是默认模式/内联;仅在冲突审查中提供 3 路模式)。
- 从目录所在的一侧打开目录,而不是从页眉打开:固定在视口左侧的垂直 Contents 标签,仅在目录关闭时显示。通过侧边栏自身标题中的箭头关闭。工具栏最远端的切换按钮离它所控制的内容太远了——关闭后,侧边栏不会留下任何东西来表明它的存在。
- 页眉行上方的堆栈栏,仅当 shell 知道有 2 个或更多条目的堆栈时显示:按顺序列出链中的每个 PR,高亮当前审查;如果构建时生成了 all 按钮,则将其放在末尾,一次读取所有 PR(参见“堆栈:在代码托管平台上,还是仅在本地”)。它不是链中的一个步骤,位于链条箭头之外;它是按钮而不是复选框:此栏中的每个复选框都表示“已读完这个 PR 的所有文件”,而此按钮表示“显示所有 PR”——同一个控件表达两种不同含义,还不如直接用两种形状。点击任意 PR 可返回阅读该 PR。栏中的所有内容都使用
--pr绘制,这是专为 PR 引用保留的颜色。页面还会带有一个并列审查按钮,可在原地将整个页面切换到该审查——selectReview会重新解析其惰性载荷块,App 会按审查重新挂载,重新加载该审查自己的备注和勾选项,并重新开始图像扫描;如果切换时有未保存的编辑器修改,会先询问。没有审查的并列项会置灰,并带有代码托管平台 ↗ 链接;如果曾为其 Agent 留下指令文件,还会显示 awaiting review 标签。此栏位于header.top内,因此测得的--header-height——以及其下所有固定元素——已将它计入。 - 目录侧边栏,固定在阅读顺序旁:以树形结构显示每个分组和文件,每项都有自己的复选框和
done/total;文件勾选项和行勾选项是同一状态。它的三种视图——章节、fs 树、diff 大小——共用一个树形组件(参见“每种目录视图都是树形组件”)。每个文件和每个章节标题都是真正的链接,因此中键点击或“在新标签页中打开”会在独立标签页打开;普通点击会展开分组和文件本身并滚动到该处;滚动侦测会高亮正在阅读的文件,点击后会暂停一秒,避免显式跳转被滚过的行覆盖。窄于 60rem 时,它会变成默认关闭、使用后再次关闭的覆盖层。 - 两者之间的分隔线可拖动,使用
react-resizable-panels(一个 React 库,通过preact/compat引入——buildPage.ts中的resolve.alias和tsconfig.json中匹配的paths确保由同一个 Preact 同时渲染它和页面;若有两个副本,它的钩子就没有渲染器)。它的三个组件接收className,而不是class。宽度通过autoSaveId持久化,按阅读者保存,而不是按审查保存。Layout仅在停靠布局中渲染PanelGroup。覆盖布局(窄于 60rem)或目录关闭时,都没有内容需要分隔,因此渲染普通的 flex.layout;同一个 60rem 阈值同时用于标记(useContentsOverlays)和样式表。- 面板不得变成滚动容器,否则页面就不能作为一个文档整体滚动:分组的
height: auto,两个窗格均为overflow: visible,分隔条是全高的拖动条,并绘制原本属于侧边栏border-right的分隔线。 - 在两种布局间切换时,每一行都会重新挂载,因此滚动侦测器位于
rowNodes.ts中,并在行注册时观察它们,而不是只在应用挂载时观察一次。
- 每个分组默认展开,显示阅读顺序编号(这里是真实结构,不同于其他地方仅作装饰的 01/02/03 标记)、简介和文件行。其标题会固定在页面页眉下方(页眉尺寸经过测量,因此即使控件换行也能正常工作),并带有一个复选框,可勾选整个分组;部分完成时显示不确定状态,勾选后折叠分组(取消勾选则重新展开)。此复选框必须执行
stopPropagation,否则标题自身的切换操作会与它冲突。 - 每一行:按路径关联的复选框——服务运行时持久化到
<store>/ticks.json,未通过服务运行时持久化到localStorage(按审查 id 关联);两种情况下都会在首次渲染前加载,勾选整个分组时会合并为一次写入,并从轮询中重新读取,使第二个标签页保持一致——状态标签、作为剪贴板复制按钮的路径(如果传入了--github,还会显示“GitHub ↗”链接)、+N −M和箭头。勾选文件会将其折叠(取消勾选则重新展开),因此页面会随着阅读逐渐收缩,只剩基本阅读顺序;重新加载后会准确恢复到阅读进度对应的折叠状态。折叠部分包含备注,以及覆盖一个默认关闭面板的 Show diff / Hide diff 按钮——它必须是真正的按钮,这样阅读顺序才能保持易于浏览,而 diff 则是在读者到达该文件时才采取的操作。 - Diff 在首次打开时才延迟渲染(否则几十个 diff 会在页面加载时全部构建),之后即使关闭或折叠也会保持挂载,只是隐藏——销毁它们会在每次勾选文件时丢弃未保存的编辑。Diff 使用 Monaco diff 编辑器渲染,从 jsdelivr 加载:按文件扩展名进行语法高亮,并使用
hideUnchangedRegions折叠长文件中未改动的部分——这就是使用编辑器而非静态补丁的原因。编辑器为只读,其语言服务的诊断功能关闭(这里不需要修复任何内容,还能省下一个数 MB 的 worker);编辑器主题采用页面自己的配色;scrollbar.alwaysConsumeMouseWheel: false则确保指针经过编辑器时页面仍能滚动。 - 图像行会打开对比查看器,而不是 Monaco。 工具栏包含版本选择器(每个捕获的版本占一列——例如 production/main/branch 等;标签上方的单选按钮选择“from”侧,下方的单选按钮选择“to”侧;同一版本不能同时属于两侧;字节完全相同的列会置灰并显示工具提示)、⇄ 交换按钮、模式栏 from | to | diff | swipe | flick、用于将洋红色 diff 合成到当前单个显示版本上的 overlay 复选框(仅限 from/to——diff 模式本身就是 overlay,swipe 已显示两侧,因此这两种模式下会禁用该选项)、zoom diff(以 diff 的边界框为中心缩放)、fit,以及 px/% 读数(flick 模式下显示当前屏幕上的版本标签)。Flick 每 600ms 交替显示 from/to 图层,而不是同时显示两者——差异会表现为运动,对细微或小范围差异来说,比静态 overlay 更容易发现。Diff 通过页面中的 canvas 计算——洋红色透明图层覆盖两侧并集范围,越界像素计为差异——并按版本对进行记忆。
- 查看器舞台可以自由平移和缩放。 拖动指针可平移(在 swipe 模式中,从分隔线 24px 范围内开始拖动会移动分隔线;其他位置仍可平移);双指捏合可缩放;双击可重置为适配大小;ctrl/⌘-滚轮可在光标位置缩放(macOS 触控板捏合恰好以此方式触发);其他滚轮输入则通过启发式规则区分——行/页级增量或较粗的单轴像素步进会被视作鼠标滚轮并缩放,细微或双轴增量会被视作触控板并平移;400ms 的惯性窗口可避免轻扫的尾段误触发缩放。滚轮监听器在设置了
touch-action: none的视口上使用非被动模式,否则这些行为都无法调用 preventDefault。 - 每个图像行都带有一条机械生成的摘要——
WxH — N% of pixels differ (Npx)——首次绘制后通过后台扫描计算,一次处理一张图像,并在扫描完成后释放其像素数据。图像字节数据存放在每张图像各自的惰性<script type="application/json">块中,仅在对应行需要时解析——这样载荷和可执行脚本都保持精简,手机 WebView 才能渲染页面。 - 图像备注以文件为锚点。 图像没有可供备注锚定的行,因此服务运行时的图像行会提供“Add note”按钮,打开与行备注相同的讨论串 UI,并存储在第 0 行;
reply.ts --line 0可回复该备注,Markdown 副本会将其显示为L0。 - 并排或内联由页眉中的 Diffs 选择框控制,并同时应用于所有编辑器。默认是并排模式:审查者比较的正是修改前后两侧,而内联模式则要求他们在单列中逐段完成比较——构建编辑器时会读取
diffViewStore,并订阅已显示的编辑器;此设置按阅读者保存在localStorage中。要使用并排模式,必须明确设置两处: 仅设置renderSideBySide: true不够,因为在这么窄的窗格中,monaco 的useInlineViewWhenSpaceIsLimited会自行在宽度低于约 900px 时切换回内联视图。两项设置都应放在sideBySideOptions中。 - Diff 中会框出移动的代码。
build.ts对审查范围以及逐提交审查中的每个提交,使用与moves.ts相同的检测逻辑(两者都使用moveDetection.ts)。它会将每个文件中的移动代码段嵌入为payload.moves,并像sides一样按键关联。createDiffEditor.tsx中的wireMoves会为每段代码绘制边框,并用移动颜色(--moved,即--mod,也是.tw-moved已使用的颜色)着色。边框上方会显示同色标签:修改后侧显示“moved here fromx.ts:390”,修改前侧显示“moved to”,并标明代码是否原封不动。末尾会提供一个链接,指向审查中列出该文件时的对应文件行。移动过程中被编辑的行保留边框,但不着色,并显示蓝色边栏,因为这些才是需要阅读的内容。边框使用整行装饰(每行都有两侧边框,首行有顶边框,末行有底边框)。标签使用视图区域,因此并排 diff 能保持对齐(monaco 会在另一侧补齐空白);它也位于 monaco 文本层上方,与.note-zone一样,否则链接就无法点击。行的统计信息会添加“N moved in / out”;目录标签中的大小条则不计入移动行,因为移动一行只需看一眼,而非阅读。导入语句永不计入移动:拆分操作会将它们分散到每个新文件中。3 路视图不显示移动代码,因为冲突解决本身不会移动任何代码。 - 按空格键会勾选正在阅读的文件,也就是目录中高亮的文件;再次按空格键可取消勾选。正在 Monaco 编辑器、备注或任何字段中输入时,此功能不会触发。按钮获得焦点时,按空格键会勾选文件,而不是按下按钮。行的勾选框会在工具提示中说明这一点。
- HEAD 会标明当前位置。 分支已检出到的 PR 会在堆栈栏中显示 HEAD 标签,HEAD 所在的提交会在提交栏中显示 HEAD 标签。两者都会随每次轮询更新(
checkoutStore),因此新提交会立即移动标签;切换分支会重新加载页面。 - 将鼠标悬停在 PR 或提交上会显示其大小。 堆栈栏中的 PR(以及“all”)会显示其在堆栈中的位置、提交数、文件数及增删行数;提交栏中的提交会显示其在 PR 中的位置、文件数和行数;服务运行时的栏还会显示 staged、working 和 held 状态,以及已读取的文件数和行数。构建过程会将每个 PR 的
size记录在 shell 条目中(sizeOf:从其基点开始的git diff --shortstat,即 PR 的净改动量),并将每个提交的行数记录到编辑索引中;barTips.ts会负责显示这些信息。 - 每个 diff 都会说明比较的是哪两者。 每个编辑器上方会显示:
“diff
main~2 (52e9aab)→item-behaviours (a84dc34)”,如果右侧跟随磁盘内容,还会追加“+ working tree”。名称由 git 自行生成(name-rev),构建过程会为审查范围(payload.refs)以及逐提交审查中的每个提交(commits[].refs)记录这些名称。若指定的 head 是分支,就保留该分支名。没有任何引用指向的提交没有名称,因此只会显示原始 SHA。这样一来,只需一眼就能看出审查对象并非读者以为的内容,例如另一个分支、过时的提交或虚构的提交。 - 在其他位置编辑过的文件会注明位置。 行备注下方会显示“also edited in PR 1/3 commit 3/5”,列出堆栈中每个其他 PR,以及 PR 中修改了同一文件的每个其他提交。页面当前已显示的位置会省略,因此完整阅读一个 PR 时只会列出其他 PR;阅读所有 PR 和所有提交时则不会列出任何内容。堆栈只有一个 PR 时,会省略 PR 部分。PR 只有一个提交时,会省略提交部分。每个位置都是指向该处文件的链接:在原地切换审查和提交,并像重新加载一样定位到对应行(
?file=)。只有服务端提交栏知道的提交会被请求,并在下一次轮询列出该提交时打开。页面未包含对应审查的 PR 会以纯文本显示。构建过程会将编辑索引收集到 shell(shell.edits)中:包含堆栈中所有 PR 的每个提交,无论是否经过审查;byPath则将每条路径映射到修改它的提交。每一行只查询自己的路径,不会查询其他内容(alsoEditedIn.ts)。buildStack.ts会索引每一层。build.ts --mode pr会索引该 PR;使用--stack时也会索引每一层,并获取并列 PR;无法获取某个 PR 时会发出警告,而不会导致构建失败。页面任何位置显示的“PR 2/3”都表示三层堆栈中的第二个 PR,无论其是否经过审查。fixtures/commitStack.sh会在两个 PR 中都修改src/parse/block.ts,可用于测试此功能。 - 编辑任意 PR:保留的修改。 若当前检出分支的磁盘并非某一行所基于的内容——例如它属于另一个 PR 或另一个提交——则保存该行的修改会进入该 PR 的保留修改:保留为
refs/guided-review/hold/<branch>的一个提交,其父提交是该行所读取的提交(整篇阅读某个 PR 的行则以该 PR 的最后一个提交为父提交)。每次保存都会修订它(stackEdit.ts,包括全部底层操作——临时索引、commit-tree、带旧值的update-ref,因此两个并发保存中有一个会失败,而不会丢失修改)。它们会在提交栏中显示为一个 held 状态,紧跟在所基于的提交后面,并为每个文件显示一行,呈现该提交与保留修改之间的 diff;发起这些修改的行也会显示保留后的内容,因此从那里继续编辑会修改保留内容(heldSync.ts)。每个 PR 只能保留一处修改:若尝试编辑其另一个提交,操作会被拒绝并说明修改保留在哪里。将内容改回与该提交一致后,保留状态就会移除。提交栏下方的 held bar 提供 New commit(输入消息后,在该提交之后插入一个新提交)、Amend into(修订到该提交中)和 Discard(会保留为备份引用)。堆栈栏会标记带有保留修改的 PR。审查者撰写的每条消息都会记录其阅读时对应的提交(commit:一个 sha 或held:<branch>;整篇阅读某个 PR 时则不设置),因此 Agent 根据备注采取行动时,会通过held.ts将修改提交到同一位置;awaitNotes.ts会说明每条备注对应阅读的内容。 - 提交或修订会立即移动 PR;上层 PR 则保持不变。 在内存中将该 PR 后续的提交重放到新提交之上(
merge-tree --write-tree),只有全部重放成功才会发生任何更改——并在一次引用事务中完成,同时将旧 head 备份到refs/guided-review/backup/<branch>/<time>。若分支已检出到服务所用的检出环境中,会一并更新其工作树(read-tree -m -u,保留未提交的更改;如果会覆盖这些更改则拒绝操作);若分支检出在其他工作树中,则只移动其引用。重放后的提交会保留原作者、消息和日期,因此同一重放总会生成相同的提交(并能再次找到已记录的解决方案)。上层 PR 会保留旧 head,并记录到refs/guided-review/restack-from/<branch>;堆栈栏会提供 Rebase N upstack,以同样方式将它们全部重放到已移动的 PR 上——要么全部成功,要么全部不做。这不是gh stack rebase:它无法替换分支中间的提交,而且需要检出分支并停在某个位置。冲突时不会移动任何内容,而是创建一条发给 Agent 的备注(参见“页面遇到的冲突”)。移动完成后,服务会重新映射每个审查的勾选项以及已撰写分组的提交引用到新提交,并根据构建配方重新构建页面(堆栈页面也有自己的配方,kind: "stack";逐提交或堆栈审查的配方中follow: false,因此只会在此时重建,不会自行重建),然后重新加载页面。 - 关闭 Monaco 的 diff 边栏菜单(
renderGutterMenu: false):它会根据最后一个 diff 布置菜单项,因此文件被缩短——例如执行 Revert,或保留内容到达——后,菜单仍会请求已不存在的行,结果抛出“Illegal value for lineNumber”。Revert 是普通编辑,因此可以撤销。 - 每种目录视图都是树形组件(Headless Tree,
@headless-tree/core和/react),通过单一组件ContentsTree.tsx实现;每个视图只负责构建自己的节点——文件,以及带有自身标题标记的文件夹(章节、章节内的一段目录路径、文件系统目录或软件包;diff 大小视图则是平铺列表)。只有一个 Tab 停靠点;方向键移动焦点,左右键折叠和展开,Home/End 定位首尾,Enter 跳转到文件(或章节的第一个文件);点击、ctrl/cmd-点击、shift-点击、shift-方向键和 ctrl-A 可进行选择;按空格键勾选所有选中项(如果它们都已勾选则取消勾选),勾选某个选中项的复选框也会勾选整个选区。普通点击章节会跳转到其第一个文件并保持展开;点击箭头则折叠它。按住 shift 选择范围时,仅经过而未选中的文件夹不会连带选中其所有文件——范围按文件夹所代表的文件判断,因为已读文件会从其标题下方移动出去。视图会按显示顺序先提供节点,因此键盘浏览顺序与视觉浏览顺序一致。焦点和选择由库管理;行则由页面自身管理。勾选会导致行移动,使其失去焦点,因此通过树形视图勾选后会将焦点移回;页面自身的空格键操作不会影响树。在树中,ctrl/cmd-点击用于选择,因此要在新标签页中打开链接,应使用中键点击或链接的上下文菜单。图标字形(箭头)使用\uf229格式的转义序列:原始专用区字符不可见,编辑时可能会在不知不觉中将它们删掉。 - 重命名的文件会与其旧路径进行 diff。
build.ts会请求 git 提供作用范围内的重命名(--name-status -M),并从重命名文件之前所用的路径读取修改前一侧及其行数。该行标题会说明其来源(“fromold/path.ts”),信息来自payload.renamedFrom。如果仅对新路径进行 diff,重命名文件会被视为全新文件:修改前一侧为空,所有行都被视为新增。 - 单体仓库中的路径会从所属软件包读取。 任何包含
package.json的目录都是一个软件包,仓库根目录除外(在单体仓库中,根目录是工作区)。build.ts会查找每个文件所属的最近软件包,并按审查所见读取package.json,然后在载荷中记录packages(目录 → 名称)。路径出现的位置都会由PathLabel绘制软件包名称,以带色调的描边标签显示,后面接软件包内的路径。如果仓库中的每个软件包都共享同一个 npm scope,标签会省略该 scope(@shop/core显示为core),完整名称则移到标签的工具提示中。该检查会遍历仓库中的每个package.json,而不只是受审查文件所属软件包的package.json,因此任何位置存在未使用 scope 的软件包,都会导致所有标签保留 scope。这适用于行标题、章节视图中的目录标题和 diff 大小视图。树形视图会将每个软件包作为顶层节点,软件包内的树则从该软件包开始。标签文字截断时,目录标题会保留完整标签,而文件行会优先显示文件名,舍弃软件包标签。fixtures/monorepo.sh <new dir>会在工作树模式下构建一个带有未提交更改的小型单体仓库,可用于测试此功能。 - 3 路模式是不同的界面,而不是一个选项。 对于冲突审查(
payload.conflict,且每一侧都包含incoming/ancestor),createDiffEditor会在宿主容器中构建三个普通编辑器,而不是 diff 编辑器。在 2 路和 3 路模式之间切换时,会销毁当前界面并在同一组模型上构建另一界面,因此未保存的编辑和保存 sha 都能保留。每个窗格均为全高,因此页面会让三个窗格一同滚动,完全不需要同步滚动:threeWayLayout.ts会将它们的行对齐。它会用每一侧与解决结果进行匹配(lineDiff.ts,Myers 算法),将两侧都保留在解决结果中的行作为同步点,并用带斜线纹理的视图区域将两个同步点之间的每一段填充到最高窗格的高度。备注讨论串也会计入解决结果的段落,因此其他两侧也会补齐以与备注对齐(noteZonesChanged)。三个窗格都关闭折叠和自动换行,因为任一功能都会导致行与其他窗格错位。冲突审查会单独记住视图选择,并默认使用 3 路模式。 - Monaco 是依赖项,不是增强功能。 它仅从审查服务自身安装的
monaco-editor中通过/vs提供服务(因此服务模式的审查可完全离线运行);若未安装,serve.ts会拒绝启动。没有 CDN,也没有纯文本备用渲染器——加载失败时,会在 diff 位置显示错误块;未通过服务打开的页面会提示代码行需要服务支持。网络断开时,页眉会显示 offline 标签(备注讨论串会切换为“Agent 会稍后处理”)——备注始终持久化到服务端,因此离线只会改变提示信息,不会改变行为。 - 两套主题均采用 artifact-design 令牌模式:
:root、prefers-color-scheme媒体查询,以及两种data-theme覆盖设置。Monaco 会根据这两类信号重新设置主题。 - 标签页图标会以内联 data uri 的形式嵌入页面,源自
src/favicon.png——与页眉中链接到游戏的标志相同。它必须以内联方式提供,而不是通过服务提供:直接从磁盘打开构建后的review.html时没有可请求的服务;serve.ts对/favicon.ico返回 204,正是为了避免浏览器发出任何请求。 <meta charset="utf-8">是页面组装内容的第一行,build.ts也会明确以 utf-8 读取和写入——两者缺一不可,否则通过file://打开时会被猜测为 windows-1252,文中的破折号和箭头都会变成乱码。- 服务模式下,每个编辑器还会提供 Revert(将构建时的内容恢复到缓冲区,绝不会直接写入磁盘),页眉还会提供“Send notes to agent”;无服务打开时,这两项都会隐藏,因为此时没有可供写入的目标。
- 要让
.tsx可读,需要两项设置,缺少任一项都会使文件充满'>' expected:typescript 默认值的编译器选项中设置jsx: JsxEmit.Preserve,并使用携带真实路径的模型 URI——TypeScript 会根据文件扩展名判断脚本类型,因此不带扩展名的inmemory://URI 会将每个标签都解析为类型断言。两侧必须使用不同的 URI。 - 添加备注时,会在鼠标悬停行的槽内显示
+(通过onMouseMove设置装饰,并在目标类型为GUTTER_GLYPH_MARGIN时从onMouseDown打开),因为仅有键盘快捷键并不容易被发现。alt-N和上下文菜单仍然可用。 - 备注是一个讨论串,而不是字符串:
{line, messages: [{from, text}]};旧版单个text的格式会作为第一条消息读取。区域会显示对话和 Reply 输入框;Agent 的回复通过reply.ts加入。Agent 的消息使用 Markdown 格式,由snarkdown-in-react(Markdown.tsx)渲染:它会构建元素,而不是 HTML 字符串,因此其中的原始 HTML 会显示为文本。指向此页面文件的链接(?review=…&commit=…&file=…,由reply.ts --edited写入)会通过goToPlace在原地读取该文件,与“also edited in”链接的行为相同;若保留修改已提交,则回退到整篇 PR 的阅读状态。其他链接都会在新标签页打开,因此点击链接不会离开审查;只有 http(s)、mailto、#和相对链接才会被识别为链接(javascript:等内容会显示为文本);图像仅支持来自 http(s) 或data:image的内容。审查者自己的消息会严格按输入内容显示。该软件包提供的是 TypeScript 源码而非类型声明,且源码不符合此项目更严格的检查要求,因此tsconfig.json中的paths会将类型检查器指向src/snarkdownInReact.d.ts;vite 会对软件包本身进行打包,并像react-resizable-panels一样,将其react别名指向preact/compat。读取备注的所有代码都必须兼容这两种格式以及写入到一半的文件——读取备注文件时,该文件可能仍在写入。 - 备注区域有两种状态:正在撰写或回复时显示文本框,之后显示带有 Reply/Delete 的讨论串。如果没有这种状态切换,保存看起来就像什么也没做——区域被销毁后又以完全相同的内容重建。
- 服务模式下,页面会每 2 秒轮询
/state,并附上已打开编辑器的路径。磁盘上的 sha 发生变化时,页面会通过/file重新加载文件(除非编辑器有未保存修改,在这种情况下优先保留编辑器内容),更新统计信息并显示提示;收到新的 Agent 消息时也会显示提示。这就是你针对备注进行修改后,无需任何人重新加载页面,修改仍会显示出来的方式。 serve.ts对/favicon.ico返回 204:浏览器会主动发起请求,而若返回 404,就会让每个审查的控制台都显示错误,但页面本身从未需要此图标。- 写入令牌会与审查一同保存,并在重启后继续使用。它仅用于阻止浏览器中的其他页面擅自向审查者背后的端口发送请求;每次运行都重新生成令牌会悄无声息地破坏所有保持打开状态的标签页,而丢失备注的后果远比它所防范的问题严重。
do_POST会先读取请求正文,再作任何判断,包括身份验证。拒绝请求但不读取完正文,会让其中的 json 留在套接字中;同一 keep-alive 连接上的下一个请求就会从正文中途开始解析,导致出现令人费解的400 Bad request syntax,并在错误中显示你自己的备注文本。- 备注存放在 Monaco 视图区域中。区域的四个特性很容易忽略:它们绘制在编辑器文本层下方(因此备注需要
position: relative; z-index,否则按钮无法点击);必须设置suppressMouseDown: false,否则编辑器会先拦截 mousedown;monaco 会将内联display写到区域自身的 DOM 节点上,因此备注布局必须放在其内部的包装元素上;monaco 会在自己的下一帧渲染时才把该节点加入文档,而不是在添加区域时加入,因此必须跨帧重试,直到备注输入框获得焦点(折叠的未改动区域中的区域永远无法获得焦点,因为它的display: none,而未显示的元素无法获得焦点)。 区域内容还位于aria-hidden子树中,因此备注 UI 由鼠标和键盘操作,无法通过角色查询访问。 - 页面会适配
window.__reviewServer,该变量仅由serve.ts注入(注入到 `` 标记中)。它会控制整个备注撰写/协作界面,而不仅仅是 Save/Revert/交接功能:槽内的+图标、alt-N/上下文菜单操作,以及视图区域中的备注编辑器,只有存在该变量时才会接入,因此未通过服务打开的页面完全只读。请保留模板中的该标记,并确保两种分支都能正常工作——同一文件既用于可分享的独立版本,也用于可协作的服务版本。