ce-babysit-pr tick
看护一个打开中的 PR 的长跑 tick 引擎。三种姿态、8 步 tick、分支保鲜规则、settle 判断、托管 stack 巡游。
ce-babysit-pr 是本插件中唯一的长跑 tick 循环。它每次 tick 跑一次,在真正停止(terminal / looks-ready / budget / blocked-external-drained)和常驻残余(needs-human / blocked-failing / stack-blocked,阻断 ready 宣告但不结束循环)之间做判断。它把反馈委托给 ce-resolve-pr-feedback,把CI委托给 ce-debug,把描述刷新委托给 ce-commit-push-pr。它自身不开启 CI 看护、不自跑轮询,也永远不说"可以合并"。
三种姿态
每次运行选择一种姿态,写成 posture:target|stack-ready|stack-land。每次托管 stack --continue-invocation 都要重新声明。
| 姿态 | 行为 | 会合并吗? |
|---|---|---|
| target | 只看指定的那一个 PR。在 looks-ready 处停止。若确认的托管 stack 还有工作要做,仅主动提议一次 stack-wide;用户拒绝则保持 target-local。 | 从不 |
| stack-ready | 一旦当前活跃层进入 静止态(零可执行积压 + 无常驻残余 + 无进行中的委托),上移到下一个打开的、非草稿的、更上层的层。下方各层留在 downstack 探针之下;最底下一层若重新打开,就把巡游拉回来。 | 从不 |
| stack-land | 巡游行为与 stack-ready 相同。选择它即是合并授权。一旦最底部的打开层看起来 ready:gh run <that-PR> --yes --squash + gh stack sync。然后继续。 | 会(最底部打开且已 settle 的层) |
选择方式:只点了一个 PR 而无 stack 措辞 → target(但若存在确认的多层托管 stack,问一次);显式 stack 意图 → stack-ready;"landing-and-merge" 意图 → stack-land。mode:pipeline 永不询问。
8 步 tick(顺序是不变量)
一次 tick = 严格按以下顺序执行。顺序就是契约。
各步骤详解
步骤 1 — 终态检查
快照中 pr_state 为 MERGED 或 CLOSED → 停止并报告。唯一例外:当本次运行刚刚完成对该 PR 的已授权 stack-land 合并时,把 MERGED 视为托管 stack 的层过渡,而不是运行级的终态停止。
步骤 2 — 抓取基线
记录快照的 head_sha;对确认的托管 stack,还要从 gh stack view --json 记录一条可恢复的基线:在目标分支之上或同一层级的、由 manager 排序的打开分支,以及每个分支的 remote-tracking OID。工作区干净 + 仍是确认的 manager 成员是前提。
步骤 3 — 先于 CI 处理反馈
若 counts.threads > 0 或 counts.comments > 0,调用 ce-resolve-pr-feedback mode:pipeline <pr-ref> 恰好一次。当 trajectory 触发器命中(invariant_rounds[]. = 2、积压上升、重复簇等)时传入 trajectory。每个 tick 仅做一轮 resolve——绝不扇出。
对每一条返回的类型化 needs-human:在 ## Needs Needs your decision 下渲染完整 payload,通过 pr-snapshot mark --residual-file <path> --disposition needs-human 持久化,保留被覆盖的线程为打开状态。
对每一条你传入但没有返回类型化残余覆盖的评论进行核对:mark --disposition dispatched(若是 fix 结果则用 --invariant-key)。
步骤 4 — 过期 SHA 取消
将抓取的 head SHA 与当前 SHA 对比。若它移动过,说明评论轮(或别人)推送了;本快照里的 CI 失败都针对的是一个过期的 SHA——不要据此行动。新的运行将在下次 tick 出现。
步骤 5 — 对当前 head 跑 CI
把所有可执行的失败检查聚合成一轮修复处理。不要按检查逐条派发。
- Flaky/infra(已知 flaky 任务、infra/超时信号):从失败检查的
details_url中抽取 run ID 与完整基础仓库(含 host),执行gh run rerun <run-id> --failed -R <host>/<owner>/<repo>。无人值守时必须传入 run ID——省略它会让gh run rerun退化为交互式 run-picker 菜单。 - 真实测试/构建失败:以失败任务及其日志尾部为种子调用
ce-debug mode:pipeline一次。CI 触发器命中时传入trajectory。status必须恰好是fixed-and-pushed|fixed-not-pushed|flaky-infra|needs-human|diagnosed-no-fix之一。 - 永远不要为了让它通过而削弱、跳过或 mock 失败的断言。
步骤 6 — 分支保鲜
消费快照所发出的、当前 branch_currency 那一条;没有条目就不要做任何 base-into-head 变更。完整路由、声明生命周期以及 BEHIND/DIRTY 机制详见 references/branch-currency.md:
BEHIND:仅由 host 主导的更新。调用PUT /repos/{owner}/{repo}/pulls/{number}/update-branch并设置expected_head_sha。HTTP 422 head mismatch = 过期声明;重新快照并核对。DIRTY:仅做基于精确 base 的本地修复。预览冲突;若属机械性(具有正向意图证据,无合理替代方案),合入精确的 base OID,标记--currency-outcome mutation-observed,正常推送。永远不要 rebase 或 force-push。unrequested_base_merge:一种缺陷检测器——有人在 CLEAN PR 上无声明就把 base 合入了,绿色 CI 重新开始跑。上报它,永远不要撤销它,也永远不要把由此造成的"检查重跑时的BLOCKED"当成新的 blocker。
步骤 7 — 托管 upstack 维护
在确认的托管 stack 上发生一次委托推送之后,维护 upstack:重新执行 gh stack view --json,从其 tracking remote 拉取目标,验证目标的本地 head + remote-tracking tip 仍等于已推送的 SHA,然后执行 gh stack rebase "<first-dependent-branch>" --upstack --no-trunk --remote <tracking-remote> + gh stack push --remote <tracking-remote>(永远不要用原始 git push --force)。若发生冲突,立即 gh stack rebase --abort 并上报一个 needs-human/stack-sync 残余——不要在另一 PR 层上决定冲突语义。
步骤 8 — 重新快照
任何变更之后,在下次 tick 开头重新快照,并传入相同的 --invocation-id、--session-started-at 与 --invocation-budget-seconds。head SHA 与 CI 全集已变;调用级的预算未变。不要在一次 tick 中途再跑一次 snapshot 来重新派生 CI——这正是造成过期 SHA 混乱的根因。
停止条件
| 类别 | 条件 | 效果 |
|---|---|---|
| 真正停止 | 终态 — MERGED / CLOSED | 看护结束 |
| 真正停止 | 看起来可以合并 — 7 项条件全部满足(见下) | 看护结束 |
| 真正停止 | 外部受阻已耗尽 — fork-PR CI 审批门,已排干 | 看护结束 |
| 真正停止 | 预算 — 活跃预算(默认 8h)或 3 天底线已达 | 看护结束 |
| 常驻残余 | needs-human | 阻塞 ready;不停止循环 |
| 常驻残余 | blocked-failing | 阻塞 ready;不停止循环 |
| 常驻残余 | stack-blocked | 阻塞 ready;不停止循环 |
"看起来可以合并" — 7 项条件必须全部满足
mergeability_certain+mergeable == "MERGEABLE"+merge_state_status == "CLEAN"base_ref_blocker == nullchecks_terminal为真(无仍在运行的任务)- 零可执行积压:
counts.threads == 0且counts.comments == 0 open_needs_human == 0(被延迟的"或未通知"线程不算 ready)branch_currency_blocker == nullquiet_seconds≥ settle 阈值(默认 300s),且"评审仍在路上"检查通过
Settle 窗口规则
- 300s 为默认。
- 不要预先放宽第一档——无论有没有评审 bot。
- 仅在被驳回的
merge-ready唤醒之后才能放宽。--settle-seconds 900单次延长至不超过1800。1800 是上限,证据不变时不得跨越它重新加注。 - Settle 窗口是冷却信号,不是"评审不会再来"的保证。
评审仍在路上检查(即判断)
Settle 窗口说 PR 不再活动;它不说没有评审在路上。看这些:
- 正文上的 👀 反应
- 顶层评论
- 针对该 head 的 check runs
- 针对该 head 的 reviews
一个已宣告(👀 / "reviewing…")但两边都没出东西的评审者,才是真正无法判定的情形。有限等待后,坦率说出你无法确认的内容。
报告
一行固定首行状态,然后是一段读者无需回翻就能据此合并的回顾:
✅ Looks merge-ready — <one-line evidence>. Your call to merge.
🟡 Cautiously looks ready — <evidence + caveats>
🎉 Merged — <one-line evidence>
🚫 Closed — <evidence>
⛔ Blocked — <evidence>
⏱️ Budget — <active|backstop, elapsed>
⏸️ Paused — <reason>
永远不要说 "safe to merge"(references/report.md)。回顾应点明反馈主题与结果、CI 修复、推送、运行时长、暂存项,以及为用户做的任何判断。
needs-human 契约
与 ce-debug 和 ce-resolve-pr-feedback 共享的类型化契约:
- Sources 用稳定 ID 列出每一项被覆盖的来源。
- decision_context 承载引用的反馈、调查过程、决策理由、选项与权衡,可空推荐。
- 当任一来源是线程时,thread_urls 非空。
- 编排器通过
pr-snapshot mark --residual-file <path> --disposition needs-human持久化;快照把完整来源集作为一份单元冻结。 - 对已覆盖来源的远端活动会让整份决策失效,并重新激活仍存活的来源。
- 人类答复通过
mark --answer-decision <id> --answer-file <path>记录;答复的状态迁移需要一次合法的状态写入才能消费。
防"狼来了"收敛
trajectory 字段是事实而非判定——看护把它们交给被委托的叶子,由叶子判断是否收敛:
| 触发器 | 含义 |
|---|---|
check_recur_max >= 2 | 同一个 check 一再复现 |
stream_alternations >= 3 | 线程与 CI 已交替 ≥3 次 |
上升的 unresolved_trend 且 new_threads_this_tick & &> 0 | 积压在上升的同时仍有新线程到达 |
heads_since_progress >= 2 | 两个 head SHA 之间未取得任何已 settle 的进展 |
当任一触发器命中时,在叶子做出变更之前把 trajectory 传给叶子;由叶子决定这是普通进展还是真正的非收敛。叶子可能返回一个 needs-human 残余,把整条流挂起(例如浮现的 CI 权衡、第三轮 invariant)。永远不要自己宣告非收敛。
自维持 vs 检点模式
看护支持两种执行模式:
- 自维持(默认):使用
pr-snapshot watch的会话内看护。每收到一次BABYSIT_WAKE跑一次 tick。唤醒原因:actionable、feedback-candidate、terminal、blocked-external、blocked-external-drained、blocked-failing、base-ref-blocked、unrequested-base-merge、downstack-actionable、stack-blocked、needs-human、settle 窗口后的merge-ready、max-runtime、stop-signal、invocation-superseded。 - 检点:跑一次 tick 并报告已暂停的监控及恢复调用。宿主在等待时无法保持会话活跃。
- 流水线(
mode:pipeline):有界同步 tick,结构化返回;无人等待。
预算算术
| 预算 | 上限 | 会重置吗? |
|---|---|---|
| 活跃看护时间 | 默认 8h(用户在入口可覆盖) | 重新加注与确认的托管 stack 层过渡必须匹配原始 ID、起始与预算——不重置,不延长 |
| 墙钟底线 | 3 个日历日 | 从不重置(兜底) |
| 死时排除 | 挂起时间(合盖等)从活跃预算中扣除 | 粗粒度检测:宽于阈值的间隔视为死时 |
重新加注会保留 last_change_at、invocation_started_at 与 invocation_budget_seconds——任一计时器都不能被重启或延长。
参考
references/tick.md— 完整的快照/watch/marks/顺序契约references/settle.md— 停止条件、settle 窗口、"评审仍在路上"判断references/branch-currency.md— BEHIND/DIRTY 机制与缺陷检测器references/stack.md— 托管 stack:姿态、发现、过渡、合入references/stack-commands.md—gh stackCLI 配方references/pipeline.md— pipeline 模式的有界停止与残余契约references/envelope.md— 完整边界、授权、诚实契约陈述references/setup.md— 步骤 1:伪造检查、PR 解析、链分类references/watch-loop.md— watch 循环调度、状态、跨宿主去重references/report.md— 报告格式scripts/pr-snapshot— Python(3,560 行);argparse 含 snapshot/mark/watch 子命令