Poteto Mode 的 Shipping Playbook:以独立验证驱动绿色 PR 堆栈的自底向上落地(pstack 实战指南)
【免费下载链接】pluginsCursor plugin specification and official plugins项目地址: https://gitcode.com/GitHub_Trending/plugins125/plugins
导读
本文讲解 pstack 插件中 Poteto Mode 的Shipping playbook(pstack/skills/poteto-mode/playbooks/shipping.md),它是工程团队在 Cursor 中把一批已通过的堆栈式 PR 安全合并进主干的标准流程。它的核心信条只有一句话:绿色不代表安全(Green is not safe)——CI 全绿只是必要不充分条件,每个 PR 必须经过与写代码者相互独立的 Agent 验证,并且只能从堆栈底部开始、沿着"连续已验证"的链条逐个落地。读完本文,你将掌握:如何解析当前 forge(gh或 Origin)、如何给每个 PR 分配独立验证 Agent、如何用git patch-id防止 verdict 失效、以及如何用仓库自带的watch-pr脚本观察合并前沿(frontier)而不擅动队列。
一、Shipping 在整个流程中的位置:Babysit 的下半场
在 Poteto Mode 的二十三本 playbook 中,Shipping 与 babysit.md 是一对相邻的阶段(SKILL.md 中明确列出:"Babysit. Driving a PR or a stack to merge-ready"、"Shipping. The half after Babysit")。
- Babysit负责把堆栈"养绿":声明模式(
drive/background/threads-only/check)、解决冲突、处理 review threads、分类 CI,直到 forge 判定 frontier 达到 merge-ready。 - Shipping接手这个"看起来可以合并"的堆栈:独立验证每一个 PR、重新确认 verdict 仍然描述当前 patch、只准备并落地底部 PR、每次合并后重新计算、在缺口(ceiling)处停下。
两者的分界线非常明确:Babysit 的drive结束于 merge-ready,落地(landing)是 Shipping 的职责。任何一个 PR 状态请求如果带有"merge、land、ship、merge when ready"的意图,都会在 Babysit 中被路由到本 playbook(见 babysit.md 第 6 步)。
从源码看,二者的衔接在watch-pr脚本中也有体现:Babysit 用--status-only或轮询得到READY/WAITING就停止;而 Shipping 的观察循环则使用--queued-stack --stack-prs <bottom>作为事件唤醒,并自己轮询gh pr view的终态字段,刻意不复用 Babysit 的 queuedWAITING/merge-queue停止条件(这正是 shipping.md 第 8 步明确禁止的)。
二、第 1 步:解析 Forge,并为每个 PR 分配独立验证
Shipping 的第一步是"解析 forge",即确定用哪套 CLI 操作 PR:
- GitHub CLI(
gh)是默认。 - 如果
command -v origin成功且 Origin 能解析当前仓库,则用origin pr ...完成 view / watch / edit / merge 操作。 - 否则留在
gh,并记录这次回退(fallback)。 - 绝不要求 Graphite(
gt)——它不被视为必需依赖。
随后是验证阶段,规则非常严格:
- 一个 PR 一个 subagent,禁止批量(not batched);
- 每个验证者都是Cursor cloud agent;
- 验证者必须实际操作真实表面(real surface):按改动类型使用
cursor-team-kit插件提供的control-ui(浏览器/Electron/Web UI)或control-cli(CLI/TUI)工具,针对 parent 分支与 head 分支分别演练; - 每个 subagent 返回
PASS、PASS+NOTES或FAIL,并把自己的 verdict发布到它验证的那个 PR 上; - Safe(安全)的定义:verdict 必须来自一个没有写过该代码的 Agent。CI 绿不是 verdict,机器人给的 approved review 也不是 verdict。
这一设计对应 Poteto Mode 的Prove It Works与Guard the Context Window原则:验证必须对着真实产物而非"它能编译",同时把大批量验证工作路由给 subagent,主线程只保留结论摘要。
三、第 2 步:只落地"从底部开始的连续已验证 run"
堆栈是一串互相依赖的 PR。Shipping 的关键判断是可落地性(landability)是连通的:
从最低的未合并 PR 开始向上走,停在第一个没有通过 verdict 的 PR 处。其中
PASS与PASS+NOTES都算通过。一个位于未验证 PR 之上的已验证 PR,不可落地。
也就是说,即使第 3 个 PR 验证通过了,只要它下面的第 2 个 PR 没有通过,第 3 个也不允许合并。报告时要把这个"天花板(ceiling)"作为 PR 号明确说出,并说明是哪一环断开了链条。这是对Sequence Work into Verifiable Units原则的忠实执行:交付顺序必须让整个序列对 reviewer 自证其正确性。
四、第 3 步:Re-check——verdict 必须仍然描述当前 patch
verdict 是历史事实,而 patch 会漂移。为此 Shipping 要求为每个 PR 记录三样东西:
- verdict 时的 head SHA;
- verdict 时的 base SHA;
- 该 PR base-to-head diff 的稳定
git patch-id。
原因在于:一次 rebase 或 base retarget 会重写 SHA,可以静默地让 verdict 失效而不触碰任何检查项(checks 依然绿)。因此在落地之前必须:
- 把记录的 patch-id 与当前 base-to-head 的 patch-id 比较;
- patch 变了 → 重新验证;
- patch 没变 → 保留代码 verdict,但要在当前 head 上重跑 mergeability 与 CI。
两个常见的偷懒替代品被明确禁止:匹配的 commit message和旧 SHA 上的绿色勾都不能当作 verdict 仍然有效的证据。这从源码上对应watch-pr中mergeAssessment的实现——它会检查历史 commit 上是否曾有SUCCESS(hadPreviousPassingCi),并区分"当前 head 的检查"与"以前通过过的检查"(policy.ts)。
五、第 4 步:只准备底部 PR
落地是严格串行的,准备阶段只允许触碰最底部的那个 PR:
fetch当前 trunk;- 必要时把最低的已验证分支rebase 到精确的 trunk tip;
- push 该分支;
- 只把那一个 PR的 base retarget 到 trunk:
origin pr edit <pr> --base <trunk>或gh pr edit <pr> --base <trunk>; - push 之后重跑第 3 步(因为 push 改变了 head,patch-id 可能已经变化);
- 不要retarget、arm 或合并任何后代 PR(descendants)。
这条"一次只动一个"的纪律,对应 Poteto Mode 的Minimize Reader Load与Separate Before Serializing Shared State原则:堆栈是共享的串行状态,任何并行修改都会让队列失去可审计性。
六、第 5 步:一次落地一个 PR
当底部 PR 满足条件时,用 squash 合并:
- 现在可合并:
origin pr merge <pr> --squash或gh pr merge <pr> --squash; - 检查还在跑、且用户要求 merge-when-ready:只 arm 那一个 PR,
origin pr merge <pr> --squash --auto或gh pr merge <pr> --squash --auto。
需要注意两种--auto语义并不相同:Origin 的--auto是 Origin merge-when-ready;GitHub 的--auto是 GitHub auto-merge。合并必须等它真正完成后,才能准备下一个 PR("Wait for that PR to merge before preparing the next one")。
这里与 Babysit 有一条联动风险:当 merge-when-ready 被 arm 时,一个父 PR 没有必需检查的堆叠 PR 可能立即合并进父 PR,从而塌缩 review 粒度;lost-ref 竞态也可能让 PR 显示合并但父 ref 未更新(见 babysit.md 第 6 步)。Shipping 因此要求以 forge 的真实状态为准,而不是以某个字段为准。
七、第 6 步:不要用 GitHub 的autoMergeRequest推断堆栈就绪
GitHub 的autoMergeRequest字段最多只能证明"某个 GitHub PR 请求过 auto-merge",它不能证明:
- Origin merge-when-ready 已被 arm;
- 某个后代 PR 已被排入队列;
- 某个 patch verdict 仍然有效;
- 连续堆栈是安全的。
因此对当前底部 PR,必须确认活动 forge(active forge)自身的状态;如果活动 forge 无法报告该状态,就要明确说"状态未知",而不是用 GitHub 字段凑合。这与 policy.ts 中assessGitHubMerge的谨慎态度一脉相承:BLOCKED状态只有在 head rollup 是ERROR/FAILURE时才被判定为refused(不可合并),否则仍按"允许、依据 rollup"处理——单一的字段永远不足以拍板。
八、第 7 步:每次合并后重新计算
每合并一个 PR,整个堆栈的坐标系就变了:
- fetch trunk;
- 确认合并后的 SHA 确实存在于 trunk;
- 把已合并的 PR 从冻结的 bottom-to-top 列表中移除;
- 检查新的底部 PR的 base、head、checks 与 patch-id;
- 宿主(host)可能会自动 retarget 子 PR,但不要假设它做了;
- 对那一个 PR 重复第 3~6 步;
- 独立的工作(independent work)不在这条链上,自行单独发布。
注意"冻结列表"(frozen bottom-to-top list)一词——它来自 Babysit 的 queued 模式:在队列模式下,PR 列表一旦捕获就冻结,每次重新 arm watcher 时传入同一份列表,只在"拥有 PR 已合并、产生 sanctioned follow-up PR"这一种例外下修订(babysit.md 第 6 步)。Shipping 复用了同一套心智模型:队列快照不可随意变更。
九、第 8 步:观察 frontier,直到合并或失败——但不擅动队列
这是 Shipping 中自动化程度最高的一步,也是与watch-pr脚本结合最紧密的一步。
Origin 路径:使用origin pr view <pr> --checks --comments与origin pr checks <pr> --watch,然后重读 PR,直到它报告 merged 或 blocked。
GitHub 路径:把 scripts/watch-pr/watch-pr 当作事件唤醒(event wake):
scripts/watch-pr/watch-pr --queued-stack --stack-prs <bottom>每次被唤醒后,轮询:
gh pr view <pr> --json state,mergedAt,mergeStateStatus,statusCheckRollup,autoMergeRequest在mergedAt非空或state为MERGED之前,READY一律忽略。只有那时才执行第 7 步。
硬失败(hard-fail)仅限以下三种情况:
state为CLOSED且mergedAt为空;- 某个必需检查以
FAILURE或CANCELLED结束,且 auto-merge 已不再 pending、确实阻塞合并; mergeStateStatus为UNSTABLE或DIRTY且没有 pending 的 auto-merge。
反过来说:checks 还在跑、或 auto-merge 已 arm 时的BLOCKED,不是失败。也不要使用 Babysit 的 queuedWAITING/merge-queue停止条件(那是 Babysit 报告 frontier merge-ready 用的,不是 Shipping 观察合并用的)。
观察循环应该在/loop的动态模式下持有,每有合并或新的 ceiling 就报告;如果队列卡住,要先诊断再动手,而不是直接改动队列。
从源码理解watch-pr的判定逻辑
watch-pr是 pstack 自带的一个 GitHub 专用观察器,默认输出 JSON(--pretty输出人类可读文本),轮询时输出 NDJSON(cli.ts)。它的核心判定在 policy.ts:
classifyPr(单 PR 模式)按优先级依次检查四类 blocker:merge-conflicts(CONFLICTING/DIRTY)→review-threads(未解决评论)→failing-checks→merge-gate(draft、CHANGES_REQUESTED、closed-without-merge);没有 blocker 且 checks pending 则为waiting;否则判定ready/merged。selectTierMajorStackDecision(stack 模式)对整堆做同样的分层扫描,第一层的任何一个 blocker 都会终结整个堆栈。runQueued(--queued-stack模式)实现了一个完整的状态机:整堆扫描(whole-stack-sweep)→ frontier 轮询(frontier-poll)→ 产出QUEUE/STATUS/WAITING/ADVANCE/COMPLETE/TIMEOUT/BLOCKER等事件。其中ADVANCE表示"另一个 actor 合并了 frontier,前进到新 frontier",COMPLETE表示队列全部合并(policy.ts)。- 查询失败采用指数退避重试:
min(max(interval, 60) * 2^(failures-1), 300)秒,连续错误超过--max-query-errors(默认 5)则输出status-queryblocker(policy.ts)。
数据来源在 github.ts 中通过 GitHub GraphQL 拉取:REVIEW_THREADS_QUERY(review 线程)、PR_COMMIT_STATUS_QUERY(最近 50 个 commit 的 status rollup)、PR_CHECK_ROLLUP_QUERY(最近 1 个 commit 上的 check run / status context 分页)。可观测的字段类型定义在 types.ts,其中MergeStateStatus枚举了BEHIND / BLOCKED / CLEAN / CONFLICTING / DIRTY / DRAFT / HAS_HOOKS / UNKNOWN / UNSTABLE九种状态——第 8 步的硬失败判定正是针对其中UNSTABLE、DIRTY与BLOCKED的组合语义设计的。
watch-pr的常用 CLI 选项汇总(cli.ts):
| 选项 | 默认值 | 说明 |
|---|---|---|
--owner <owner>/--repo <repo> | 无 | 显式指定 GitHub 仓库 |
--pr <number> | 无 | 要观察的 PR 号 |
--stack | false | 观察整条连接的开放堆栈 |
--queued-stack | false | 观察冻结队列直到全部合并(与--stack互斥) |
--stack-prs <n,...> | 无 | 冻结的 bottom-to-top 队列(仅 queued 模式,且要求--queued-stack) |
--interval <seconds> | 60 | 轮询间隔 |
--sweep-interval <seconds> | 300 | 整堆扫描间隔 |
--timeout <seconds> | 0 | 截止时间,0 表示禁用 |
--max-query-errors <count> | 5 | 连续查询错误预算 |
--status-only | false | 只输出一次状态表并退出 0 |
--allow-draft | false | 不把 draft 视为 merge gate |
--pretty | false | 输出人类可读文本而非 JSON |
十、第 9 步:在 ceiling 停下,并汇报
当已验证的 run 全部合并后,Shipping 结束于一次清晰的汇报。这不是"接着合并下一个"的信号,而是**新的验证回合(new pass through step 1)**的起点:
- 报告落地了什么(每个 PR 及其验证者、verdict);
- 报告下一个未验证的 PR 是谁;
- 报告验证它需要做什么。
按照 playbook 的统一要求,最终回复必须覆盖:verified run 及其 ceiling、每个 PR 的 verdict 与产出者、你 arm 了什么以及如何确认、落地了什么、以及下一个缺口(gap)需要什么。回复风格遵循 Poteto Mode 的Writing the reply规范——短陈述句、每句话承载证据或标签(measured / inferred / guess)、绝不虚构链接(SKILL.md)。
十一、落地实战:一个最小可运行的检查清单
把上述九步压缩成 Agent 可以直接照做的清单:
- forge 解析:
command -v origin成功且可解析仓库 → 用origin pr ...;否则gh并记录回退;不要求gt。 - 独立验证:每个 PR 一个 Cursor cloud subagent,对 parent vs head 实操
control-ui/control-cli;verdict 发布到该 PR;PASS/PASS+NOTES通过,FAIL阻断。 - 记录 patch 指纹:head SHA、base SHA、
git patch-id;落地前比对 patch-id,变了就重验,没变则重跑 mergeability 与 CI。 - 只准备底部:fetch trunk → rebase 到 trunk tip → push → retarget 唯一底部 PR 的 base → 重跑第 3 步。
- 逐个落地:
<forge> pr merge <pr> --squash;要 merge-when-ready 则加--auto(并明确你用的是哪个 forge 的--auto语义)。 - 不读
autoMergeRequest当就绪:以活动 forge 对底部 PR 的真实报告为准,报不了就说未知。 - 合并后重算:确认 merged SHA 在 trunk、冻结列表去掉该 PR、检查新底部 PR 的 base/head/checks/patch-id。
- 观察 frontier:GitHub 用
scripts/watch-pr/watch-pr --queued-stack --stack-prs <bottom>唤醒,再轮询gh pr view的终态字段;只在三种硬失败条件下判定失败;BLOCKED+pending checks 或 auto-merge armed 不是失败;在/loop动态模式下持有。 - 停在 ceiling:汇报 landed / next unverified PR / 验证所需工作,延长 run 是新的一轮第 1 步。
十二、适用前提与边界
- 本 playbook 面向 GitHub 与 Origin 两个 forge;
watch-pr脚本是 GitHub 专属(babysit.md 明确指出公共 watcher 保持 GitHub-specific,不要假装它能覆盖 Origin,也不要在本 playbook 里为 Origin 另写实现)。Origin 路径完全走origin pr ...命令。 - Babysit 与 Shipping 分工不可混淆:Babysit 的
drive结束于 merge-ready;落地与 merge-when-ready 的 arm 必须显式请求,否则路由回本 playbook(babysit.md 第 6、9 步)。 - 本 playbook 属于 pstack 插件,安装方式为在 Cursor 中执行
/add-plugin pstack(见 pstack/README.md),并通过/poteto-mode触发,其完整规则与原则索引位于 SKILL.md。 - 验证子代理的模型角色配置由
/setup-pstack决定(默认 code 走grok-4.6-fast-xhigh、判断与文案走claude-fable-5-1-thinking-max),可在 pstack 中按角色覆盖(SKILL.md 的 Subagents 一节)。
总结
Shipping playbook 把"落地一批绿色 PR"从一句"合并吧"变成了一套可审计、可重放、防回归的工程协议:绿色不是安全,独立 verdict 才是;堆栈只能从底部连续落地;verdict 会过期,patch-id 是对账的锚点;观察合并前沿时,你的职责是报告而不是改动队列。配合watch-pr的终态判定与冻结队列机制,一个 Agent 可以在/loop下整夜无人值守地把一摞 PR 安全地送进主干,并在每个缺口处留下清晰、可继续的交接说明。
【免费下载链接】pluginsCursor plugin specification and official plugins项目地址: https://gitcode.com/GitHub_Trending/plugins125/plugins
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考