DeepSeek Harness 团队怎么在堆叠 PR 上回应评审意见
先说清楚这篇讲的是什么处境。你有一条依赖链 A ← B ← C,评审者在 B 上留了一条行内评论,指向的那个文件在 C 里也被改过。你手边正好开着 C 的 checkout,于是顺手在 C 上改掉、推上去、回一句”已修复”。
deepseek-harness 仓库里的 docs/cookbook/responding-to-pr-review-on-a-stack.md 把这种做法明确列为错的。它的第 3 条基本规则原话是:修复落在引入问题的那个 PR 上,然后沿堆叠向上流动;把修复发起在下游,会导致 B 带着未修复的代码交付,并对 B 的评审者隐藏修复。这一条不是风格偏好,它同时决定了后面所有验证动作的对象是谁。
顺带交代限定:这个仓库自述处于开发者预览阶段,README 里用大写强调会有破坏兼容性的变更,版本号是 0.1.0-rc.5。下面提到的每一条命令、每一个字段名都可能在之后的提交里变掉,照抄前请以仓库当前内容为准。另外 CONTRIBUTING.md 写明该项目目前不接受外部 pull request,所以这套流程你直接用在它身上是用不上的——它的价值在于这是一支团队把堆叠 PR 的评审纪律写成可执行文档的完整样本。
五条基本规则,逐条落到动作上
那份指南正文只有三十来行,分成”基本规则”5 条、“沿堆叠解决评审意见”7 步、“验证”4 条。先看 5 条规则里最容易被当成客套话的两条。
一个 PR 分支一个 worktree,并行修复绝不共享同一个 checkout。 这条读起来像洁癖,但它跟仓库的钩子安装机制是咬合的。docs/development.md 写明 pnpm install 会通过 scripts/install-lefthook.mjs 配置 worktree 本地的 Lefthook 钩子;对应的 Agent Note(.agents/notes/implemented/process/2026-07-27-worktree-local-lefthook.md)写得更细:安装程序要求 Git 2.26 或更高版本,好让 git config --show-scope 能报出配置值来自哪个作用域;它会把仓库格式版本 0 升到 1、启用 extensions.worktreeConfig,再把当前 worktree 的 core.hooksPath 设成指向 $GIT_DIR/dsh-hooks 的绝对路径。检测到 CI=true 或 GITHUB_ACTIONS=true 时,安装程序在探测 Git 之前就返回,什么也不改。
把这两处放在一起看:钩子路径是按 worktree 记录的绝对路径,而 docs/development.md 另有一句”移动检出目录之后要重跑这个 wrapper 来重新生成自有路径”。所以”每个 PR 一个 worktree”在这个仓库里不是纪律口号,它落在一组具体的 Git 配置上。至于新建 worktree 之后钩子是否一定就位,文档给的是”如果因为依赖走了缓存或跳过了 postinstall 而缺失,就手动跑 node scripts/install-lefthook.mjs”——它没有承诺自动,而是给了补救口。
GitHub 的 stack 对象是权威依据。 指南写的是:base 分支确定预期的依赖顺序,而 PullRequest.stack 和 stackEntry.position 才证明 GitHub 已识别这条堆叠;未经检查这两个字段,不得仅凭分支链吻合就当成官方堆叠。落地 skill .agents/skills/dsh-merging-stacked-prs/SKILL.md 里给了现成的 GraphQL 查询,取的就是 stackEntry { position } 和 stack { number baseRefName size entries(first: 100) },并且明写 size 超过返回页时要对 entries 分页。它还要求先跑 gh stack --version:官方扩展或服务端堆叠功能不可用就硬性停止,不许退回到逐个 gh pr merge 加手动改 base;跨 fork 的链同样硬停。
剩下三条我按原文压一遍:每项评审修复保留为独立 commit,后续 rebase 可以改变它的 OID,但不得通过 amend 把已评审的修复从分支历史里抹掉,只有自己尚未推送且尚未评审的工作才可以 amend;merge-forward 与 rebase 都允许,包括评审之后;改写历史的推送必须受 lease 保护,远端 head 在此期间前移就中止,禁止直接 --force。
七步处置里,真正会咬人的是第 4 步和第 6 步
第 1 步是”就事论事地审视每条评论,对照代码验证其论断”,理由原文给了:评审者指出了正确的症状,但仍可能误诊原因。
第 4 步是委派给 subagent 的部分,也是我觉得最值得抄走的一段。原文:subagent 的报告描述的是意图,不一定是实际落地的内容;请亲自在实际代码树上重新运行门禁;对于回归守卫,要证明它在未修复的代码上失败——引入回归、观察变红、再还原——两种情况都通过的守卫什么也守不住。最后还补了一句:subagent 把问题重新定性为”已处理”时,这是一个需要亲自深入的信号。
第 6 步管的是”凭什么说这条意见还处于已解决状态”:每次改写推送之后,都要重新读取未解决线程、批准状态、可合并性和检查结果;经 force-push 改写的 commit OID 或已过时的内联锚点,都不足以作为当前证据。与之配套的是第 5 步——回复要落在评审线程里,用 gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies,而不是发顶层评论,并且要说明修复内容和当前承载修复的 commit 或 head。
第 3 步给了两条传播路线。merge-forward 是把修好的父分支合进子分支、验证子分支、再继续往上,同时要按 .agents/notes/implemented/process/2026-07-26-incremental-pr-base-retargeting.md 保留每个正在处理的检查点——那篇 Agent Note 的后果一节明写:base 多次前移时,这个 PR 可以包含多个用于合并 base 的提交。另一条是原生级联 rebase:gh stack rebase 之后验证所有被改写的层,再用 gh stack push 发布;或者用 gh stack sync。
gh stack sync 是明确的例外
.agents/skills/dsh-pre-push-checks/SKILL.md 把 gh stack sync 单列出来讲:它在一次操作里完成获取、级联 rebase 和推送,所以没有地方能把本地验证塞在改写与发布之间。同步返回之后要做四件事——重新查询每个分支 head 与官方堆叠顺序、对照实时 PR base 检查每个被改写层的变更范围、对每个受影响层跑相应证据、在所有检查通过前保持所有 PR 未合并并把验证状态报告为待定。技能文末还专门写了一句:不要因为命令成功就宣称这次同步已经让堆叠可以合并。
那”对照实时 PR base 检查变更范围”具体是什么命令?就是 pnpm --silent run change-scope --base <verified-base-ref>。package.json 里这条脚本是 tsx scripts/change-scope.ts。这个脚本值得你翻一下源码,因为它的行为跟技能文里”该命令绝不猜测或获取 base”那句是对得上的:
- 参数解析里
base没有默认值,缺了就抛missing required --base <ref>;head的默认值是'HEAD'。 - 输出是带
formatVersion的 JSON,脚本里FORMAT_VERSION常量当前是1;路径分成committed、staged、unstaged、untracked四类,其中committed是相对解析出的 merge base 算的,另外三类描述当前 worktree。 resolveMergeBase用merge-base --all,结果不是恰好一个就抛base and head do not have a unique merge base; found N。仓库文档没有说明什么情况下会出现多个 merge base,源码只是把它当硬错误抛出来;你在堆叠里跑到这一步报了这个错,先照它字面意思查 base 和 head 的关系,别当成脚本坏了。- 所有 Git 调用都带
-c core.fsmonitor=false,环境里设了GIT_OPTIONAL_LOCKS=0和LANG/LC_ALL=C;未跟踪文件走ls-files --others --exclude-standard -z,输出按 NUL 分隔解析、去重再排序。
这几个点合起来的意思是:它是个只读的范围报告器,base 必须你自己从远端或堆叠状态核准之后喂给它。技能里也补了一句:合并了变化的 base 之后要重跑这份报告,重新判断合并后的范围能影响哪些行为,并只重跑被这次合并作废的检查。
落地与收尾
落地只走官方流程:gh stack merge <stack-number> --yes --merge 合并整个官方堆叠,明确要求的部分落地则合并到边界 PR 为止。skill 明写不要传 --delete-branch、不要手动改子 PR 的 base、不要发逐个 PR 的合并命令;原生合并是全有或全无,trunk 上有合并队列时 GitHub 可能把所选范围分组落地,所以只有每个 PR 各自报告 MERGED 才算完成。删分支要放到单独的最后一趟,删每个分支之前用 gh pr list --state open --base <branch> --json number --jq length 查,结果不是 0 就不许删。
顺带一提,堆叠分支的历史麻烦在这个仓库的另一处工具里也留了痕迹:docs/cookbook/maintaining-dsh-code-review.md 说明 dsh-code-review skill 的周期维护工作流会跳过”合并 commit 无法从 origin/master 到达的 PR(例如父分支被 squash 的堆叠分支)“,把它们记进 skipped-pulls.json 而不中止本次运行。两处放在一起看,堆叠链的历史一旦被非官方路径改写,受影响的不只是评审锚点。
最后一句提醒。整份指南的语气是”证据要当场重新取”,而不是”流程走完就算数”:验证一节共四条,往回收口的是最后两条——一条要求每次改写推送之后重新审计未解决线程、批准状态、可合并性和检查结果,另一条要求相关门禁在堆叠中每个受影响 PR 上都通过,而不只是顶部那个。这两条正好对着开头那个反面例子:在 C 上改完推上去,B 的门禁根本没跑过。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。