Poteto Mode 的 Shipping Playbook:以独立验证驱动绿色 PR 堆栈的自底向上落地(pstack 实战指南)
2026/9/17 22:32:53 网站建设 项目流程

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:

  1. GitHub CLI(gh)是默认
  2. 如果command -v origin成功且 Origin 能解析当前仓库,则用origin pr ...完成 view / watch / edit / merge 操作。
  3. 否则留在gh,并记录这次回退(fallback)
  4. 绝不要求 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 返回PASSPASS+NOTESFAIL,并把自己的 verdict发布到它验证的那个 PR 上
  • Safe(安全)的定义:verdict 必须来自一个没有写过该代码的 Agent。CI 绿不是 verdict,机器人给的 approved review 也不是 verdict。

这一设计对应 Poteto Mode 的Prove It WorksGuard the Context Window原则:验证必须对着真实产物而非"它能编译",同时把大批量验证工作路由给 subagent,主线程只保留结论摘要。

三、第 2 步:只落地"从底部开始的连续已验证 run"

堆栈是一串互相依赖的 PR。Shipping 的关键判断是可落地性(landability)是连通的

从最低的未合并 PR 开始向上走,停在第一个没有通过 verdict 的 PR 处。其中PASSPASS+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 记录三样东西:

  1. verdict 时的 head SHA
  2. verdict 时的 base SHA
  3. 该 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-prmergeAssessment的实现——它会检查历史 commit 上是否曾有SUCCESShadPreviousPassingCi),并区分"当前 head 的检查"与"以前通过过的检查"(policy.ts)。

五、第 4 步:只准备底部 PR

落地是严格串行的,准备阶段只允许触碰最底部的那个 PR

  1. fetch当前 trunk;
  2. 必要时把最低的已验证分支rebase 到精确的 trunk tip;
  3. push 该分支;
  4. 只把那一个 PR的 base retarget 到 trunk:origin pr edit <pr> --base <trunk>gh pr edit <pr> --base <trunk>
  5. push 之后重跑第 3 步(因为 push 改变了 head,patch-id 可能已经变化);
  6. 不要retarget、arm 或合并任何后代 PR(descendants)。

这条"一次只动一个"的纪律,对应 Poteto Mode 的Minimize Reader LoadSeparate Before Serializing Shared State原则:堆栈是共享的串行状态,任何并行修改都会让队列失去可审计性。

六、第 5 步:一次落地一个 PR

当底部 PR 满足条件时,用 squash 合并:

  • 现在可合并:origin pr merge <pr> --squashgh pr merge <pr> --squash
  • 检查还在跑、且用户要求 merge-when-ready:只 arm 那一个 PR,origin pr merge <pr> --squash --autogh 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,整个堆栈的坐标系就变了:

  1. fetch trunk;
  2. 确认合并后的 SHA 确实存在于 trunk;
  3. 把已合并的 PR 从冻结的 bottom-to-top 列表中移除
  4. 检查新的底部 PR的 base、head、checks 与 patch-id;
  5. 宿主(host)可能会自动 retarget 子 PR,但不要假设它做了
  6. 对那一个 PR 重复第 3~6 步;
  7. 独立的工作(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 --commentsorigin 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非空或stateMERGED之前,READY一律忽略。只有那时才执行第 7 步。

硬失败(hard-fail)仅限以下三种情况

  1. stateCLOSEDmergedAt为空;
  2. 某个必需检查以FAILURECANCELLED结束,且 auto-merge 已不再 pending、确实阻塞合并;
  3. mergeStateStatusUNSTABLEDIRTY且没有 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-conflictsCONFLICTING/DIRTY)→review-threads(未解决评论)→failing-checksmerge-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 步的硬失败判定正是针对其中UNSTABLEDIRTYBLOCKED的组合语义设计的。

watch-pr的常用 CLI 选项汇总(cli.ts):

选项默认值说明
--owner <owner>/--repo <repo>显式指定 GitHub 仓库
--pr <number>要观察的 PR 号
--stackfalse观察整条连接的开放堆栈
--queued-stackfalse观察冻结队列直到全部合并(与--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-onlyfalse只输出一次状态表并退出 0
--allow-draftfalse不把 draft 视为 merge gate
--prettyfalse输出人类可读文本而非 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 可以直接照做的清单:

  1. forge 解析command -v origin成功且可解析仓库 → 用origin pr ...;否则gh并记录回退;不要求gt
  2. 独立验证:每个 PR 一个 Cursor cloud subagent,对 parent vs head 实操control-ui/control-cli;verdict 发布到该 PR;PASS/PASS+NOTES通过,FAIL阻断。
  3. 记录 patch 指纹:head SHA、base SHA、git patch-id;落地前比对 patch-id,变了就重验,没变则重跑 mergeability 与 CI。
  4. 只准备底部:fetch trunk → rebase 到 trunk tip → push → retarget 唯一底部 PR 的 base → 重跑第 3 步。
  5. 逐个落地<forge> pr merge <pr> --squash;要 merge-when-ready 则加--auto(并明确你用的是哪个 forge 的--auto语义)。
  6. 不读autoMergeRequest当就绪:以活动 forge 对底部 PR 的真实报告为准,报不了就说未知。
  7. 合并后重算:确认 merged SHA 在 trunk、冻结列表去掉该 PR、检查新底部 PR 的 base/head/checks/patch-id。
  8. 观察 frontier:GitHub 用scripts/watch-pr/watch-pr --queued-stack --stack-prs <bottom>唤醒,再轮询gh pr view的终态字段;只在三种硬失败条件下判定失败;BLOCKED+pending checks 或 auto-merge armed 不是失败;在/loop动态模式下持有。
  9. 停在 ceiling:汇报 landed / next unverified PR / 验证所需工作,延长 run 是新的一轮第 1 步。

十二、适用前提与边界

  • 本 playbook 面向 GitHub 与 Origin 两个 forgewatch-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询