Community编程与开发github.com

devseunggwan/merge-briefing

按需准备的合并前审批流程——三面预先探测、根据阻塞标记对每个发现进行分级、将锚点的 `Unverified` 缺口带入后续处理、六部分简报,以及结束整个流程的 `AskUserQuestion` 批准提问。

merge-briefing 是什么?

merge-briefing 是一个按需技能,用于在 Git 合并前生成一份全面的简报,确保只有经过用户明确逐 PR 批准的变更才会合并。该技能的核心原则是,简报中的所有信息都应在编写时实时探测,而非依赖 PR 打开时的旧数据。它通过探测三个表面(CI 状态、对话时间线 + 审查体 + 可合并性 + 推送的 HEAD,以及内联审查线程)来收集所有发现,并根据其阻塞标记(blocking/non-blocking)对每个发现进行分级。该技能还处理 CodeRabbit 等机器人评论、锚点的未验证缺口,并在执行合并前明确询问用户的批准。它支持 GitHub CLI(gh)和 GraphQL API,并强调证据驱动,避免在 CI 未通过或存在未解决的阻塞发现时请求批准。

兼容平台✓Claude Code✓Codex CLI✓Cursor
npx skills add https://github.com/devseunggwan/praxis/tree/HEAD/skills/merge-briefing

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

merge-briefing 是做什么的?

概述

合并是不可逆的共享状态,因此它必须经过用户对每个 PR 的明确批准。这个门槛在形式上很容易满足,但也很容易虚假满足:诚实的版本需要一个足够广泛的探测来支持“没有未解决的问题”这一论断,而实际存在阻塞发现的那个表面(内联审查线程)恰恰是 gh pr view --json comments,reviews 调用静默忽略的。

该技能是按需处理此流程的场所。合并前报告规则(ETHOS.md → 规则实践部分)负责维护这个门槛;下面的步骤 4 中的六部分列表定义了报告的格式;而下面的步骤则说明了如何从“PR 看起来搞定了”过渡到一个用户可以用一个字回答的问题。

核心原则: 简报是关于 PR 状态的主张,因此其中的每一行都在编写时进行探测——而不是从 PR 打开时回忆的旧数据。

何时使用

  • 即将询问用户批准合并时。
  • 请求了合并,但您在此轮尚未给出简报时。
  • 在之前的请求未获答复,或 CI/审查有变动后重新询问时。
  • 当某个配套、后续或重新生成的 PR 是一个用户已批准过的集群中的下一个时。

铁律

  • 一个 PR,一份简报,一个答案。 批准 PR X 仅批准 X。唯一的例外是批量合并:一轮中包含每个 PR 的独立简报部分,一个列出它们的请求,以及一个批准所列集合的答案。
  • 根据本轮探测输出来编写简报。 进入时的 CLEAN 状态不是永久的;过时的锚点会让用户发现一个本应由您捕获的冲突。
  • 绝不要根据进度信号来合并。 "계속", "ok", "진행" 是推进工作的信号,而不是对共享状态变更的同意。

流程

步骤 1:探测所有三个表面

发现存在于三个表面,没有单个命令能覆盖全部。在编写简报之前,先运行以下所有命令。

# 1 — CI
gh pr checks <N> --repo <owner>/<repo>

# 2 — 对话时间线 + 审查体 + 可合并性 + 推送的 HEAD
gh pr view <N> --repo <owner>/<repo> \
  --json mergeable,mergeStateStatus,isDraft,headRefOid,reviews,comments
# 3 — 内联审查线程(表面 #2 **不**包含此部分)
gh api graphql --paginate -f query='
query($owner:String!, $repo:String!, $pr:Int!, $endCursor:String) {
  repository(owner:$owner, name:$repo) {
    pullRequest(number:$pr) {
      reviewThreads(first:100, after:$endCursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          id isResolved isOutdated path line
          comments(first:1) { nodes { author { login } body } }
        }
      }
    }
  }
}' -F owner=<owner> -F repo=<repo> -F pr=<N> \
  --jq '.data.repository.pullRequest.reviewThreads.nodes[]
        | select(.isResolved | not)
        | {id, path, line, author: .comments.nodes[0].author.login,
           body: .comments.nodes[0].body}'

body 被包含在投影中,因为标签位于线程自身的文本内,而步骤 2 正是根据该标签进行分级的。--paginate 与 $endCursor 并非装饰——一个经过两个机器人处理过的 PR 可能超过 100 个线程,单页会静默丢弃其余部分,这会被解读为“无未解决线程”。

表面 #2 也静默截断在 100 个。 gh pr view --json comments,reviews 发出的是 comments(first: 100) / reviews(first: 100),且不包含游标变量。因此,当 PR 超过任一计数时,尾部内容就会简单缺失。当其中一项达到 100 行时,在分级之前,通过分页的 REST 端点重新收集该表面:

gh api repos/<owner>/<repo>/pulls/<N>/reviews --paginate -q '.[] | {user: .user.login, state, body}'
gh api repos/<owner>/<repo>/issues/<N>/comments --paginate -q '.[] | {user: .user.login, body}'

空结果本身并不是答案。 它在同一时间里既表示“无未解决线程”,也可能表示“查询出错”——直到您看到这条确切查询在一个已知有线程的 PR 上打印出结果为止。先在一个这样的 PR 上运行一次;这是支持否定论断的阳性对照。

在 fork 克隆中,每次探测都必须使用 --repo 命名该 fork:裸 gh pr 调用会解析到父仓库(参见 worktree-merge-cleanup → 调用前检查清单)。

如果 CI 处于 pending 或 failing 状态,则在此停止——不要询问。验证行仍在变动中的简报,是让用户批准一个猜测。

在编写“已批准”之前,检查每个人类批准所覆盖的提交:reviewDecision: APPROVED 在后续推送后仍然存在。按审查者分组审阅意见,并以每个审查者最新的 APPROVED 审阅为准(早前的批准会被重新批准覆盖;同一审查者后来的 CHANGES_REQUESTED 或 DISMISSED 表示当前无批准)。将该审阅的 commit_id 与 headRefOid 比较,列出其后到当前 HEAD 之间的非合并提交(git log --no-merges <commit_id>..<head>),并在第 5 部分中列为“未经 审阅”(参见失败模式行的命令)。

合并状态是一个许可列表,而非“未明显损坏”检查。 仅在 mergeable 为 MERGEABLE 并且 mergeStateStatus 为 CLEAN 或 HAS_HOOKS 时才可询问——这两个是唯一表示“可合并且提交状态通过”的值。其他所有值都停止询问,并在简报中标明自身:

mergeStateStatus含义是否询问?
CLEAN可合并且提交状态通过是
HAS_HOOKS可合并,状态通过,存在 pre-receive hooks是
UNSTABLE可合并但提交状态未通过否——即上面失败的 CI
BLOCKED合并被阻止(保护规则、所需审查)否
BEHINDHEAD 引用已经过时否——请先变基
DIRTY无法干净地创建合并提交否——存在冲突
UNKNOWN当前无法确定状态否——请重新轮询

草稿状态不在此枚举中——它在单独的 isDraft 字段中。因此,草稿 PR 也可能报告 CLEAN。将 isDraft 添加到 --json 列表中,并将草稿视为未就绪状态,无论其合并状态如何。

CLEAN 对于自分支创建以来新落入基础分支的提交保持沉默。 统计它们(git fetch origin 然后 git rev-list --count HEAD..origin/<base>),并列出 PR 也修改的这些提交所触及的文件(git diff --name-only HEAD...origin/<base>)。对于每个共享文件,在基础 diff 中搜索此 PR 更改的符号,并运行 git merge-tree --write-tree HEAD origin/<base>——零符号命中且退出码为 0 才是“无语义重叠”的证据,在第 5 部分中引用。当合并前变基触发器触发时(3 个或更多基础提交,完成了一轮审阅),使用 gh pr update-branch <N>(一个合并提交,绝不是重写)将基础分支合并进来,这会重新启动 CI 和机器人审阅:在新的 HEAD 上等待两者完成,使用更新的 HEAD 重新修订锚点,然后针对新 HEAD 重新运行步骤 1-3 并重新构建简报,然后才询问——之前收集的发现、合并状态和批准覆盖描述的是旧的 HEAD。

步骤 2:对所有三个表面上的每个发现进行分级

未解决的线程数并不等同于阻塞数,而线程表面也并非全部内容。对步骤 1 收集的每个发现(内联线程、对话评论、审查体)进行分级,依据的是修饰语(decoration),而非标签(label):

发现的第一行是否阻塞合并?
issue (blocking): / suggestion (blocking):是——合并需等待
issue (non-blocking): / suggestion (non-blocking):否
nitpick: / note:否——按设计保持未解决状态
无标签自行分级——阅读正文

suggestion 并不比 issue 更低一档:两者都恰好携带 (blocking) / (non-blocking) 之一。因此,修饰语就是全部判断依据,标签仅说明它是什么类型的发现。仅读取标签正是导致一个明确的阻塞项被当作“只是个建议”而放过的原因。

审查体不是线程,无法被“解决”, 这恰恰是它容易被忽略的原因:没有任何东西让它显得是开放的。CodeRabbit 将其可操作的发现放在可折叠的审查体部分中,而不是作为行评论。因此,需要 grep 步骤 1 中收集的审查体,查找 Actionable、Nitpick、outside diff、🧹,并对找到的内容进行分级。提出缺陷的对话评论也以相同方式分级。在任何表面上发现的阻塞项,都会像内联发现一样停止询问——它们只是没有线程保持开放,因此会在简报的未解决项中列出。

在分级完成前,需核对 CodeRabbit 的计数:通过 `LINES`: ** 标记统计其发现数量,并使其与审查头部声明的总数(例如 Major comments: N)匹配。在一个对话评论中回答这些发现,格式为 Fixed — <sha> (n) / Not fixed (n) / False positive (n),其计数之和应等于该总数。

无标签的情况是常见的,而非例外:机器人使用自己的词汇。CodeRabbit 以其自身的严重性行开头(_🎯 Functional Correctness_ | _🟠 Major_ | …),而 Copilot 和 BugBot 则没有。将机器人的严重性视为您分级的输入,而非分级本身——也绝不能因为“它只是个机器人”而跳过该发现。

一个您已分级为阻塞的线程,在合并前只有一个合法的退出方式:被修复并关闭,或者被重新分级——将评论编辑为 (non-blocking) 并发布 Revised — 通知。将阻塞发现搁置到后续步骤而不重新分级,会使分支保持锁定,而所有标签却都显示为非阻塞。

清除审阅级别的 REQUEST_CHANGES 是一个独立的、稍后的步骤。 该状态属于整个审阅,其中可能包含多个发现。因此,只有当该审阅中的每个阻塞发现都被修复并关闭或重新分级后,才能清除该状态——绝不能仅凭一个发现的力量。最后一个发现的重新分级才是解锁它的关键;在此之前,审阅正确地仍在阻塞。

步骤 3:携带锚点的 Unverified 缺口

打开 PR 的验证锚点,阅读其 Unverified 列表。写为“머지 후 실전송으로 확인”的缺口,如果没有任何东西能超越 PR 的生命周期,则会随 PR 一同消亡。

  • 只罗列出那些指明了下一步行动的缺口——未说明下一步行动的缺口不予携带。
  • 在简报中询问是否要创建一个跟进 Issue。
  • 在合并前,将答案写入锚点的最终修订版中,格式为 Carried: #N 或 Carried: none — <사유>。
  • 写入最终修订版需要 gh——一个仅通过 MCP 服务器与 GitHub 交互的会话,如果该服务器当前活跃的工具列表中没有暴露评论的 update 功能,则无法更新评论体(在假设之前请确认)。在这种情况下,请将 Carried: 行放入合并提交或 PR 体中,而永远不要放在第二个锚点评论中。参见 anchor-comment-gate/spec.md → 无 gh 会话的流程。
  • 永远不要自动创建跟进 Issue:一个新 Issue 仍然需要其自身的实现方法审查。
  • 当跟进以评论形式记录时(Carried: <comment URL>),将其放在一个合并后保持开放的 Issue 上——即 PR 的 Ref: Issue(例如,稍后由发布步骤关闭的枢纽/伞状 Issue),而不是 Closes #N Issue,后者会被合并本身关闭,且无人会重新打开。对于在 PR 开放期间编写的超出范围的意见也适用此规则。
  • 当携带 Issue 正好是一个合并后的发布步骤会关闭的 Issue(由 /release 关闭的枢纽 Issue)时,发布步骤会像 Closes 目标一样掩埋该携带内容。请在合并报告中明确指出此冲突(现在发布则丢失未解决项,或在它们被检查后再发布),而不是静默地以任何一种方式解决。同时,在合并后检查 Closes #N 目标:GitHub 仅当 PR 合并到仓库的默认分支时才关闭它。因此,合并到非默认基础分支(例如默认分支是 dev,而合并到 prod)的 PR 会保持其开放状态,等待发布步骤处理。

步骤 4:编写六部分简报

六个部分,按以下顺序(此列表是标准格式;它最初源自作者的 dotfiles PR-工作流模板):

  1. 变更内容——用一句简单的话说明 PR 做了什么,以及合并(或不合并)对用户意味着什么变化。然后是范围(文件、逻辑变更),而不是重复 Issue 标题。如果第 1 部分仅由数字和内部缩写构成,您会得到“它有什么作用?”的反问,而不是批准,这将重置批准窗口并需要完整的重新简报。
  2. 已验证的内容——真实输出,并附上引用的命令
  3. 未验证的内容——跳过、推迟、阻塞、CI 待定
  4. 风险/影响范围——如果出错,谁会受影响
  5. 未解决项——步骤 2 中分级为开放的所有发现(来自所有三个表面);步骤 3 中的缺口
  6. 明确询问——同意合并? 单独成行,在文本中,并紧跟步骤 5 的 AskUserQuestion 调用(即使工具已经询问,该行仍保留,因为它是门槛评估的依据)

第 3 部分是面对压力的部分,也是简报存在的原因。

当一个未验证项正是合并所触发的步骤时(例如合并后部署、CI 从未运行过的资产构建、您仅在一个平台上运行过的多平台构建),它是一个风险,而不是脚注:将其放在第 4 部分中,说明如果该步骤失败会有什么后果,以及它会阻塞哪些其他内容(同一堆栈的后续部署)。对于一个价值仅为簿记工作(版本锁定、目录、文档)的 PR,第 4 部分也要用一句话说明如果不合并会发生什么——用户在进行低风险但仍会触发部署的 PR 批准之前,会问到这一点。

第 3 部分的反模式: “완료했습니다, 머지할까요?” 但没有证据;将跳过的检查降级为附带说明;在合并时才报告本应在 PR 就绪时就应呈现的内容。

通过观察来关闭第 3 部分的事项,而非阅读。 当用户要求您检查未验证项时,请逐项进行,并运行最能解决该问题的最廉价探测。

步骤 5:通过 AskUserQuestion 请求批准

调用您的工具平台提供的 AskUserQuestion 功能。提问必须逐字包含字符串 同意合并?,并且问题必须在 <ask_question> 调用体中紧紧跟在步骤 4 的 **明确询问** 部分之后。简报中必须包含该行。

报告为已批准。 当用户以肯定语气回答时——例如“네”、“예”、“yes”、“y”、“approve”、“merge”、“proceed”或包含 #approve 的消息——将批准时间与批准者记录在您的状态中。状态必须在合并后持续存在(如果合并完成),以便在重新生成摘要时,同一集群中的后续 PR 可以跳过重新询问。

报告为已拒绝。 当用户以否定语气回答时——例如“아니요”、“no”、“n”、“wait”、“stop”、“hold”、“check”或明确拒绝——停止,不要合并。如果用户要求修改,先修改,然后返回步骤 1。

批量合并:多个 PR,一个答案

要在一个询问中批准多个 PR,请在一个简报中包含每个 PR 的独立部分。每个部分格式如前所述,但在自己的部分内。然后合并的明确询问应列出所有 PR:

**明确询问**

同意合并 PR #1、#2、#3?(所有未解决项必须在询问前消解——见上文)

AskUserQuestion 的正文必须包含此列出所有 PR 的询问。

当用户批准时,按顺序为每个 PR 调用 gh pr merge,使用 --auto --merge 或 --auto --squash(根据仓库设置),如果任何合并失败,则停止并报告。在询问第一个合并之前,确保所有 PR 都已就绪(CI 通过,无阻塞项)。

步骤 6:最终合并

使用 gh pr merge。确认合并完成,并在 PR 上发布一条最终评论,总结结果,包括合并提交 SHA。

对于大小写:由于这是按需技能,步骤 5 中的 AskUserQuestion 调用是必需的:在没有先询问的情况下合并永远不是一个选项。如果用户在步骤 5 后关闭了对话,不要自动合并。等待用户的下一个输入。

检查清单

运行之前的自检:

  • 未跳过步骤 1(所有三个表面均被探测)
  • 步骤 2 根据修饰语(而非标签)对每个发现进行了分级
  • 步骤 3 处理了锚点的 Unverified 缺口
  • 步骤 4 包含全部六个部分,格式正确
  • 步骤 5 的 AskUserQuestion 包含字符串 同意合并?
  • mergeStateStatus 为 CLEAN 或 HAS_HOOKS
  • CI 通过
  • 当前没有阻塞的发现

故障模式

症状可能原因及措施
简报对合并说“是”,但合并失败步骤 1 中的探测陈旧——运行 git fetch origin 并检查 headRefOid 是否已移动;如果已移动,则重新探测
gh pr merge 失败,原因是“基础分支已过期”mergeStateStatus 为 BEHIND——运行 gh pr update-branch,等待 CI,重新探测并重新编写简报
简报对合并说“否”,但用户继续坚持用户覆盖了简报——记录此事件,而不是静默合并;遵守铁律
简报显示“批准”,但批准来自先前的提交步骤 1 未将 commit_id 与 headRefOid 比较——运行 gh pr view 并检查;重新探测
简报未提及审查线程探测命令行拼写错误——重新运行步骤 1,检查 gh api graphql 命令;确认查询格式正确
用户对简报做出回应,但简报未接收用户消息在会话之外(另一个会话或用户界面);确认您在同一个会话上下文内

参考

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相关技能