打开一个 PR
做好一个 PR 在技术之前首先是个调研问题。评审者在看到第一屏内容时就判断作者是否了解这个仓库——标题的语法、正文的结构、提交的主题都传递了信号——所以工作就是:学会这个仓库的维护者怎么写,然后那样写。
先选择深度
快速通道 适用于“直接开PR”、“直接推送”、“把这个合进去”——以及任何改动小到研究成本超过它能节省的评审时间的场景。定向、编写、推送、移交监控权。跳过风格探查及其卡片、声音校准和散文审查。不要询问是否需要快速通道:措辞已经表明了意图,确认只会带来用户想要避免的延迟。
ccx vcs lane --json
ccx vcs diff
ccx vcs ship --no-watch --pr-title-file "$TITLE" --pr-body-file "$BODY"
正文依然要说明改了什么以及为什么改(当仓库有模板时使用模板),标题仍然要与最近几个合并 PR 的语法匹配——只需一次 gh pr list --state merged --limit 5 --json title 读取,而非全面探查。快速通道去掉了研究,但从不省略正文:没有正文的 PR 是所有通道都不会推送的。
完整通道 是本文件其余部分的内容,它在你不属于的仓库、需要对维护者进行说服的更改、或者你第一次在任何地方打开 PR 时值得付出成本。在外部仓库上,即使是在快速通道中,合规性和礼貌性检查也是不可选的——一个跳过它们的客人在浪费维护者的往返时间,而不是自己的。
定向
这里的路径很重要:三个命令,其命名输出被下游所有内容引用。
ccx vcs lane --json # lane, branch, trunk, dirty, github.mine, downstack PRs and which lack bodies
gh repo view --json isPrivate,owner,viewerPermission,pullRequestTemplates,nameWithOwner,defaultBranchRef
ccx vcs guidelines # CONTRIBUTING, PR template, code of conduct, issue config — prints the card path
然后执行 ccx vcs diff 获取变更本身。
ccx vcs lane 报告通道——它不选择通道,你也不选择。ccx vcs ship 在推送时选择 jj、git 或 gt,并且用户无法控制的仓库会静默地退回到 gt 通道,所以在推送报告通道之前不要写任何假设通道的内容。github.mine 是所有权判断;其背后的测试以及 mine: false 下游的所有内容都位于 reference/oss.md 中。读取 ccx vcs guidelines 写入的卡片,然后区分硬性要求和偏好——reference/guidelines.md 中有识别线索,以及对于没有发布任何内容的仓库的 gh api 回退方案。
学习项目风格
项目风格存在于磁盘上的一个卡片中,每个仓库探查一次并缓存:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-cache.sh" status <owner/name> # fresh | stale:<reason> | missing
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-cache.sh" path <owner/name> # prints the cache dir
fresh 不需要探查——读取卡片继续。stale 或 missing → 生成探查器,每个仓库每次运行一次:
Agent(subagent_type: "open-pr:pr-style-scout",
prompt: "repo: <owner/name>\ncache: <dir>\nviewer: <gh login>")
探查器对最近合并的 PR 进行采样,过滤掉机器人、AI 编写的 PR 以及——在外部仓库上——查看者自己的 PR,然后将卡片写入缓存目录。它返回一个路径、一个简短摘要和一个置信度——读取它写入的卡片,而不是它的叙述。
卡片携带其样本大小和每个维度的置信度,因为保持样本人为性的过滤器也会缩小样本量。标题语法和正文结构是惯例——它们在任何样本量下都成立。评审-回复语气和散文语气依赖于更少的信号:在 confidence: low 时,它们是一种参考,而非规则,并且 PR 模板和指南具有更大权重。从三个样本中读取的惯例描述了那三个样本。缓存布局、卡片模式和丢失状态位于 reference/style-cache.md 中。
校准声音
当用户有本地 wlm 配置文件时,借用其声音中能适应格式变化的部分:
wlm profile list
wlm -p <profile> stylecard show # -p comes before the subcommand and is not optional
wlm 卡片是为长篇博客散文构建的:勿用列表和单词选择完全转移,语气以一半强度转移,长度和开场白则完全不转移。完整的转移表和无配置文件回退方案在 reference/voice.md 中。当仓库的卡片和声音卡片不一致时,仓库的卡片获胜——你是他们格式的客人。
你不控制的仓库
github.mine: false 会改变语气,并在编写之前添加两个步骤(reference/oss.md):
- 礼貌性语气——对变更直接,对决定尊重:用数字支持主张,维护者保留属于他们的决策权。
- 标准合规性——对照指南卡片和检查清单检查
ccx vcs diff:范围、风格、测试、文档、提交规范性、与仓库合并 PR 标准相比的差异大小。每个不符合项要么修复,要么在正文中用一个句子说明偏差。 - 先处理 Issue——当指南卡片将 issue 标记为硬性要求时,在打开之前搜索:
gh issue list --repo <slug> --search "<keywords>" --state all。没有匹配项 → 起草 issue,然后将草稿通过AskUserQuestion呈现给用户:按起草内容打开/先编辑/链接现有 issue/故意跳过要求——并根据用户的选择执行。打开 issue 会在别人的项目下以用户的名义发布;起草不会。
编写
标题、正文和提交主题按其陈述的置信度遵循风格卡片;PR 模板(如果存在)是正文的骨架。在通常用形容词来分级变更的地方使用数字:评审者验证“冷启动快 40 毫秒”,而忽略“快得多”。
将正文和标题写入文件,永远不要作为 shell 参数——包含反引号、$ 或代码栅栏的正文无法在命令行上完整传递,标题也会因为更少的原因被拒绝(参见推送):
BODY=$(mktemp)
TITLE=$(mktemp)
正文是散文,因此在推送之前需要经过散文审查:slop-cop check "$BODY" --lang=markdown,对标记进行分类而不是全部接受。
提交消息也受到同样处理:卡片记录了此仓库的提交主题语法、长度以及其自身合并提交携带的尾部标记——匹配那些尾部标记,不要添加其他。在一个历史记录中没有 Co-Authored-By: Claude 或 Generated with Claude Code 的仓库上,一个宣布工具链的尾部标记是让一个谨慎的 PR 看起来像自动化的那一行,维护者会基于此进行过滤;在历史记录中确实携带这些标记的仓库上,保留它们才是惯例。
每次会话一个栈
来自同一会话的相关工作放在一个链式栈中,而不是 trunk 上的独立同级——即使差异明显不重叠。“文件不重叠”不是拆分的理由:同级会抹去链式栈所陈述的评审顺序,并失去中间栈的机制——一次重设基数会将修改带到其上的每个分支,而同级的每个分支单独 rebase。按依赖顺序链式排列,消费的放在它所消费的之上;仍在处理中的通道放在顶部,这样它下面已完成的分支永远不会在其变动中被重新 rebase。机制在 reference/stack.md 中。
PR 是功能级别的工作单元。后续工作、修复和评审回复是开放 PR 上的提交,永远不是栈子级。使用 ccx vcs ship 推送它们,添加 --amend 以合并到其提交中,或者当只有栈的顶部更改时使用 --tip-only。仅当父级必须先合并才能进行其他工作,或者评审需要拆分时才使用栈。在一致的检查点推送,而不是每次编辑都推送,因为每次推送都会取代正在进行的构建,并重新运行 CI 和评审机器人。仍然要在一小时内推送,因为 origin 是备份。
推送
这个标志组合是关键的:
ccx vcs ship --no-watch --pr-title-file "$TITLE" --pr-body-file "$BODY"
将标题写入一个单行文件,与正文文件放在一起,并传递 --pr-title-file,永远不要使用内联的 --pr-title。工作区隔离的代理的 Bash 沙箱会拒绝任何将引号与单词 git 或 shell 名称配对的参数,例如 GitHub's ... git,而文件中的标题永远不会到达命令行。推送从该标题获取提交主题,从 PR 正文获取提交正文,因此 -m 仅在主题与标题不同时才需要。
--no-watch 是因为推送的内置监视会阻塞直到 CI 结束——正好在你等待的时候,但在监视权即将移交的时候是错误的。推送在所有通道中拥有 PR 创建和更新:对于一个手工编写的 gh pr create 旁边,会生成一个 gt 通道无法与其自身栈协调的 PR。
对于已经打开的 PR,推送会发出警告并保留其当前的标题和正文,因此修订不会携带这两个标志,而需要更改文本的 PR 会使用 --replace-pr-text。
在 gt 通道上,一次提交可以创建多个 PR,并且每个尚未有正文的 PR 都会获得一个正文。--pr-title-file 和 --pr-body-file 可以重复使用,并通过分支进行限定(--pr-body-file <branch>=<path>;裸值应用于顶部);ccx vcs lane --json 告诉你哪些下游 PR 没有正文。
拒绝是信息,而不是重试:nothing to commit 意味着之前的推送已经提交了此内容;推送本身会重播 gt 的 needs_restack。当推送仍然因为分支偏离其父级或 trunk 移动而拒绝时,运行 ccx vcs stack submit(或当另一个通道拥有栈时,将拒绝信息原样传递给栈的所有者),然后重新运行推送。
推送报告分支、PR 编号和 URL 以及头部 sha。移交需要这四个信息。
移交监控权
在生成时,将 ${CLAUDE_PLUGIN_ROOT} 解析为调用者中的绝对路径,并用准备好运行的命令填充 poll:。此技能文本中的变量会被替换;子代理不会收到它。将 poll: 后的句子逐字传递,以便监视器即使在其配置省略代理定义时也能启动脚本。
Agent(subagent_type: "open-pr:pr-watcher", run_in_background: true,
name: "pr-watch-<number>",
prompt: "pr: <number>\nurl: <url>\nrepo: <owner/name>\nhead: <sha>\n"
"branch: <branch>\nlane: <gt|jj|git>\ncache: <dir>\nownership: <mine|foreign>\n"
"poll: bash \"<abs plugin root>/scripts/pr-poll.sh\" <owner/name> <number> <cache>/pr/<number>.json\n"
"Run PR_POLL_WINDOW=40 <poll: command> alone as foreground Bash with timeout: 60000, rerun it at once on DONE window-elapsed, and keep calling until a final verdict: never arm a Monitor or background the call, since neither wakes a subagent, never wrap it in a loop, and never substitute a hand-rolled gh pr checks/bk loop, which cannot see a queue landing. On DONE ready-to-merge, SendMessage main before anything else.")
生成后,验证轮询脚本是否写入了其状态文件:
bash "${CLAUDE_PLUGIN_ROOT}/scripts/pr-watch-armed.sh" "<cache>/pr/<number>.json"
如果退出码为 1,则向监视器 SendMessage 同样的 poll: 行,并再次运行检查。如果再次失败,向用户报告监视已解除。代理生成本身并不能证明脚本正在运行。
每个 PR 一个监视器——同一 PR 上的第二个监视器会覆盖第一个。除 evicted 外的每个报告都会结束监视;通过 SendMessage 按名称恢复它进行下一轮。evicted: <reason> <detail> 消息可能在运行中到达,监视器会继续监视,直到调用者通过标签或在 Graphite 的 UI 中重新入队。在重新入队之前推送任何修复;调用者决定何时重新入队。
在准备合并报告之后:open-pr.on-ready
ready-to-merge 报告意味着 PR 是绿色的、可合并且已批准:所有检查都已通过,头部上的每个 GitHub Actions 检查套件已完成,mergeable 为 true,队列状态已干净读取(从未是 QUEUE unknown),并且 PR 在其当前头部上既未入队也未驱逐。没有评审者的最新评审是 CHANGES_REQUESTED,并且 REST mergeable_state 不是 blocked(缺少必需批准的状态)。ready-to-merge downstack #<n> 报告是一个堆叠的 PR,正在等待下游 PR #(包括当 Graphite 的可合并性检查是遗留 Graphite 栈上唯一未完成的检查时)。PR 本身已准备好,但它只有与其下游栈一起合并:在问题中命名 # 并入队整个栈,而不是仅此 PR。
读取用户的 CLAUDE.md 以查找以下确切行之一:
open-pr.on-ready: offer-open(未设置行时的默认值):在同一回合中,通过AskUserQuestion询问用户是否要打开 PR 页面。仅在回答为是时运行gh pr view <number> --web。open-pr.on-ready: offer-merge:在同一回合中,通过AskUserQuestion询问“立即合并 #?”。仅在回答为是时添加仓库的队列标签。
永远不要仅基于报告就添加标签或打开。在 long-running 技能下,将报告转发给落地台;它拥有决定权,无论设置如何。
在向已经报告 ready-to-merge 的 PR 添加队列标签后,通过名称恢复该监视器,以便它监视队列通过检查到合并或失败:
SendMessage(to: "pr-watch-<number>", message: "Queue label <label> added. Resume the queue watch on the same state file.")
其报告作为通知在任务中间到达——继续工作直到收到。当它报告判断时...