AI

隔夜代理留了 7 个 PR,4 号机只有 90 分钟:先杀哪几个?(中英切换)

早上八点五十,4 号机打开 GitHub。排序是 created。最上头是一个「legacy helpers 重命名」——45 个文件,+3510/−6。再往下还有 lockfile、功能、暗色模式顺手改、CI 红的库存接口。最底下才是:登录跳转后会话丢了,auth/session.py 里 SameSite 修了十几行。

早会九点半。90 分钟。按从上往下审的习惯,人会在那坨 cleanup 里沉下去。auth 还在队尾。到点了,两个「看起来不吓人」的被盖章,剩下的标成 later——later 堆会在下一周变成墙。

三人、七台机:1–3 开发(代理高频开 PR),4–5 审查,6 Windows 验证,7 合并/运维。这篇文章不谈怎么读 diff(那是审查地图另一篇的事),也不谈 Approve 绑哪个 SHA。它只问一件更早上的事:隔夜堆出来的东西,到底是合并队列,还是 triage 队列?

下面是我在本机搭的玩具仓库:七条代理分支、一个 triage.py、90 分钟预算模拟、隔夜节流计数器。git 提交是真的;CI 状态和标签写在 JSON 里,不是 live GitHub API;没有在 Windows 上跑过。数字都来自这一次实验。

场景:七个隔夜 PR,一个错误的默认排序

隔夜代理 PR 到早间两遍 triage

# id 打开顺序 体积 CI 一句话
101 docs-typo 1 +1/−1 · 1 文件 绿 README 拼写
102 lockfile-bump 2 +92/−3 · 2 文件 绿 只动 lock
103 big-cleanup 3 +3510/−6 · 45 文件 绿 「无行为变化」的重命名
104 feature-with-tests 4 +20/−1 · 2 文件 绿 优惠券逐行打折 + 测试
105 drive-by 5 +64/−2 · 16 文件 绿 暗色模式 + 改日志 + 升 CI 镜像,无 Issue
106 failing-ci 6 +18/−0 · 3 文件 红 库存预留,import 炸了
107 auth-session-fix 7 +17/−5 · 2 文件 绿 登录会话 SameSite 回归

按 created 排,auth 是最后一个。cleanup 是第三个——足够早,早到会吃掉你剩下的上午。

FIFO:从上往下深读,预算死在 cleanup 上

我让模拟审查员做最常见的事:打开列表,从最上头开始深读,每条按体积估时(tiny 8 / small 15 / medium 25 / large 55 / huge 90 分钟),预算 90。

跑出来的账很干:

  • docs-typo 花 8 分钟 → 还剩 82
  • lockfile 花 25 分钟 → 还剩 57
  • 轮到 big-cleanup,需要 90,只剩 57 → 卡住做部分深读,预算归零
  • feature、drive-by、failing-ci、auth-session-fix 全部 deferred

merge_ready 里只有 docs 和 lockfile。生产上正在掉登录的那 12 行级修复,早会前没人碰到。drive-by 也没被杀掉——只是「还没读到」。CI 红的也还挂着,占着列表注意力。

这不是审查能力问题。是队列语义错了:你把「隔夜产出」当成了「按时间排的合并候选」,于是体积最大、打开又早的 cleanup 天然赢得早会前的注意力。

风险优先:先 disposition,再深读

同一批 PR,换规则。Pass 1:每条 ≤1 分钟,只看 receipt、路径、体积、CI、有没有 scope creep——不打开完整 diff。输出三个 disposition:kill / fix-track / merge-track,外加 pr-risk:high|med|low。

FIFO 与风险优先同一 90 分钟对比

triage.py 这一次的排序是:

  1. auth-session-fix → merge-track · high(小、绿、有 receipt、碰 auth/)
  2. failing-ci → kill(CI 红,不花审查分钟)
  3. drive-by → kill(无 receipt、多意图、「顺手」)
  4. lockfile / docs / feature → merge-track
  5. big-cleanup → fix-track(huge,改期或拆 PR,不进早会前深读)

Pass 1 七条全部标完,还剩 83 分钟。Pass 2 只深读 merge-track:auth、lockfile、docs、feature 都做完,预算剩 24 分钟。cleanup 停在 fix-track。drive-by 和红 CI 在第一遍就从「待审」里消失。

和 FIFO 比:auth 从「队尾没人碰」变成「第一个深读」;cleanup 从「吃光上午」变成「标签改期」;列表噪声(drive-by、红 CI)不再假装自己是合并候选。

信号长什么样(决定论,可抄)

我没有用模型给 PR「打分感觉」。triage.py 用的是写死的信号,方便明天改阈值而不是改哲学:

  • 路径风险:auth/、session、billing、.github/workflows、lockfile → 抬 risk
  • 体积带:add+del 与文件数 → tiny…huge,映射到估时
  • CI:failure → 直接 kill(打回代理,不审红的)
  • Receipt:正文是否有 ## Intent / Receipt(Issue、Scope、Risk、Tests、Out of scope)
  • Scope creep:无 Issue、while I was here / quick wins / and also 等多意图措辞 → kill
  • 打开顺序:只作并列时的 tie-break,不当主排序键

auth 那条 +17/−5、两文件、有 receipt、路径命中 auth/session.py → high + merge-track。cleanup +3510、45 文件 → huge + fix-track。drive-by 无 receipt、正文自己写「No issue linked」「also」→ kill。你可以把阈值换成自己的目录约定;不要把「谁先打开」换回来当默认。

标签即状态,而不是评论区口头禅

triage 标签状态机

Pass 1 的产物应该是机器能过滤的状态,不是群里一句「这个先放放」:

  • triage:pending → triage:kill | triage:fix | triage:merge
  • 可选 pr-risk:high|med|low

4/5 号机早会前的工作区过滤:is:pr is:open label:triage:merge sort:updated-desc,再人工看 risk。7 号机合并时也可以要求「无 pending」。kit 里有 gh pr edit 命令模板;本次实验只把标签写进 pr_state.json,没有打到远程仓库。

没有标签时,disposition 只存在于审查员短期记忆里——换班、请假、代理又推了一个,队列语义就丢了。

隔夜节流:审查预算的上游闸门

就算早上 triage 很狠,隔夜仍开 7 个,Pass 1 也要 7 分钟起,Pass 2 更深。更稳的是让 1–3 号机的代理在 gh pr create 前先过闸:

max_overnight_prs=3 节流

配置 max_overnight_prs: 3(和次日约 90 分钟预算对齐)。throttle.py --demo 结果:前三个 ALLOW,第四、第五 REFUSE(exit 2)。

取舍要说清楚:光有节流不够。 这次 demo 里前三个恰好是 docs、lockfile、cleanup——auth 反而被拒。真实系统里节流必须和「谁配占隔夜名额」绑在一起:有 receipt、有 Issue、高风险热修优先占槽;drive-by 根本不该进计数器。也可以留 triage:hotfix bypass,但默认仍应先占白天队列,避免「绕过节流」变成新的 FIFO。

AGENTS.md 里应写成硬规则:18:00–08:00 开 PR 前跑 throttle;没有 Intent/Receipt 的 PR 直接不要开;一条 PR 一个意图。

机制:为什么 FIFO 在代理时代更毒

人工时代隔夜 PR 少,「从上往下」浪费的是偶尔一个大清理。代理时代 1–3 号机整晚都能开 PR,体积分布变歪:文档和小修很多,但总有一个「code health」大清理被代理写得很勤快,又往往比热修更早 push(热修要复现、要日志)。GitHub 默认列表强化了打开时间。审查员的习惯又是「先把上面的清掉」。三件事叠在一起,高风险小 diff 稳定地沉底。

另一层:合并队列暗示「绿了就该合」;triage 队列暗示「先决定配不配占用今天的注意力」。代理很会把 CI 弄绿(本次 failing-ci 是故意红的反例),不会帮你决定注意力。绿 × 早 × 大 = FIFO 最优欺骗。

什么时候这套不划算

  • 隔夜几乎从不开超过 2 个 PR。 节流和 Pass 1 都是多余仪式。
  • 没有人做早间审查,只有异步评论。 那你缺的是审查职责,不是排序键——先指定 4/5 号机的值班窗。
  • 所有隔夜变更都必须当天合完(合规/热修窗口)。 那应改成「隔夜只允许 hotfix 槽」,而不是对 7 个普通 PR 做 triage 剧场。
  • 仓库没有稳定的路径约定。 路径风险信号会瞎;先定 auth/、billing/、workflow 目录,再启用自动抬 risk。
  • 用模型摘要代替 disposition。 摘要仍在诱你深读;Pass 1 要的是可执行的 kill/fix/merge,不是又一段散文。

和「怎么读大 PR」不是同一篇文章

若 Pass 2 已经决定 merge-track,大 diff 怎么切给人类读,是审查地图的问题。本文刻意停在更前面:先别把 cleanup 放进 Pass 2。顺序反了,地图再好也是在错误的 PR 上花 90 分钟。

早会前 90 分钟的两种失败,看起来不一样

第一种失败我上面写过了:FIFO 卡在 cleanup,auth 进 deferred。列表还很长,人却觉得「今天已经审得很累」。累是真的,产出不对——你把注意力花在了低风险高成本上。

第二种失败更隐蔽:审查员凭直觉跳过了 cleanup,先点开了 auth,也杀了 drive-by,但什么都没留下。标签仍是空的,群里只有一句「大的那个下午再说」。下午 2 号机代理又往 cleanup 上 force-push 了一次;5 号机换班上线,又从 created 顶上开始。直觉 triage 没有变成队列状态,等于没做。

所以 Pass 1 的「一分钟」不是为了快而快,是为了强制写出 triage:*。没有标签的跳过,只是把 FIFO 税转嫁给下一班。

代理会怎么对抗这套规则(以及你要不要让步)

代理读得到 AGENTS.md,也会「遵守」得很好看:把三个不相关改动拆成三个隔夜 PR,刚好填满 max_overnight_prs=3,每个都有像样的 Receipt。表面上节流生效了;实际上早会前仍是三个中等 PR,auth 热修还是进不来。

对策不是把上限调到 10。是让占槽规则变硬:

  • Receipt 里 Risk: high 且路径命中 auth|billing 的,可以挤掉一个已占的 pr-risk:low 隔夜槽(先评论说明替换了谁)。
  • 没有 Issue 编号的,throttle 直接拒,不管还有没有空位。
  • 连续两天被 triage:kill 的分支主题,进入冷却——当天禁止再开同名意图。

你要让步的地方是热修:生产在掉登录时,人为贴 triage:hotfix 应该能开第 4 个。不要让步的地方是「代理自己宣布这是 hotfix」。

审查预算要从日历反推,不要从代理产能正推

三人团队里,4/5 号机早会前能拿出的深度审查时间,常常被高估。扣掉站会、昨天留下的 fix-track、Windows 失败回流,净预算可能只有 60–90 分钟。代理侧却按「上下文窗口还空」继续开 PR。

max_overnight_prs 的正确输入是:

[ \left\lfloor \frac{\text{次日净审查分钟}}{\text{平均 merge-track 深读分钟}} \right\rfloor ]

而不是「模型一晚能写几份 diff」。我这次把上限写成 3,是因为 90 分钟里 Pass 1 要留 7 分钟量级、Pass 2 还要吃掉 auth+lockfile+docs+feature 大约 60 分钟。你的数字会不同;公式不该不同。

若净预算经常小于 45 分钟,隔夜上限就该是 1–2,并接受功能类 PR 只能白天开。那不是惩罚代理,是承认审查是稀缺资源。

和机器 6、7 的交接:triage 标签下游也能用

6 号机 Windows 验证不必对 triage:kill 排队;对 triage:fix 的 huge cleanup 也可以等到拆分后再跑。7 号机合并闸可以加一条软规则:头上还有 triage:pending 的 PR 不许合——逼 Pass 1 发生。这些都不是必须第一次就上的;但说明早晨的 disposition 不是审查员的私人笔记,而是整条七机流水线的输入。

本次实验没有接 Windows,也没有改 live 合并闸。写在这里只是为了标明:队列卫生的收益不止在 4/5 号机的眼睛,而在下游少做无效验证。

我怎么确认 FIFO 真的会「卡死」在 cleanup

有人会说:真人看到 45 文件会跳过。会——那已经是在做手工 triage。实验里的 FIFO 模拟的是「信任列表顺序、从上往下深读」这条默认路径;一旦你开始跳过,你就已经站在风险优先一侧,只是没有标签、没有 Pass 1 预算记账,跳过决策无法交接给下一班。

估时是规则表,不是眼动仪。把 huge 设成 90,是为了匹配「早会前整段预算」;就算你觉得 cleanup 只需 40 分钟,FIFO 仍先把它排在 auth 前面——质化结论不变:打开顺序不是风险顺序。

复现:解压 kit → python3 run_lab.py。看 results.json 里 fifo_sim_summary.stuck 语义(日志里 stuck-partial-deep-read on big-cleanup)、risk_sim_summary.merge_ready 是否含 auth-session-fix、throttle 里第四条 allowed: false。

明天就能抄的最小步骤

  1. AGENTS.md(1–3 号机):隔夜开 PR 前跑 throttle;强制 Intent/Receipt;禁止 drive-by。
  2. 建标签:triage:pending|kill|fix|merge,pr-risk:high|med|low。
  3. 早会前 10 分钟:跑 triage.py(或按同一信号手工打标签),过滤 triage:merge,high 先行。
  4. 把 max_overnight_prs 调到「次日审查分钟 / 平均深读分钟」,不要调到代理能开多少。
  5. huge cleanup 一律 fix-track:拆 PR 或预约非早会时段,禁止占用站前预算。

一个可执行的「第一周」落地顺序

不要一周内同时上节流、标签、脚本、合并闸。摩擦会让人绕回 FIFO。

第 1–2 天: 只建标签,4/5 号机早会前手工 Pass 1。成功标准:站会时列表里不再出现未打 triage:* 的隔夜 PR;auth 类小修复至少被标成 merge+high 而不是埋在底部。

第 3–4 天: 加上 AGENTS.md 的 Receipt 硬要求,开始拒收无 Receipt 的代理 PR(关闭并留言,不要默默深读)。统计 kill 理由——若 drive-by 超过一半,先去改代理提示词,而不是加审查人头。

第 5 天: 打开 max_overnight_prs,数值按当时净预算反推。观察是否出现「前三个槽被低风险占满、热修进不来」;若出现,再上 high-risk 挤槽规则。

之后: 才考虑把 triage.py 接到导出 JSON 或小机器人上,以及让 7 号机拒绝 pending。自动化是放大已经做对的习惯,不是代替习惯。

若第 1–2 天都做不到「站会前打完标签」,后面的脚本只是把 FIFO 自动化得更漂亮。先证明 disposition 发生,再谈工具。

实际跑了什么

真跑:玩具 git 仓库七条分支;triage.py 分类与排序;90 分钟 FIFO vs 风险优先预算模拟;throttle.py ALLOW×3 / REFUSE×2;标签写入 pr_state.json 并生成 gh 命令模板。

没跑:非 live GitHub/Actions;标签未打到远程;无 Windows 执行;未接七台机联调。

Kit:overnight-triage-kit.tar.gz — triage.py、throttle.py、labels_kit.py、build_repo.py、run_lab.py、AGENTS.md 片段、morning-triage.yml 示意、results。

今天最小的一步:把明天早上的 PR 列表从 created 换成「先打 triage:*,再只打开 triage:merge」。若你发现自己又在 45 文件的 cleanup 里沉了二十分钟,而 auth 还在 pending——你就再次看见了 FIFO 的税。

At 08:50, machine 4 opens GitHub sorted by created. On top: a "rename legacy helpers" cleanup — 45 files, +3510/−6. Below that: a lockfile bump, a feature, a drive-by dark-mode-plus-logging rewrite, a red CI inventory PR. At the bottom: a SameSite fix in auth/session.py, about a dozen lines, for users dropped after login redirect.

Standup is 09:30. Ninety minutes. The habit of reading from the top sinks the reviewer into the cleanup. The auth fix stays at the bottom. By standup two "harmless" PRs are rubber-stamped and the rest are later — and later becomes a wall.

Three people, seven machines: 1–3 develop (agents open PRs at high frequency), 4–5 review, 6 Windows validation, 7 merge/ops. This post is not about how to read a diff, and not about which SHA an Approve binds to. It asks an earlier question: is the overnight pile a merge queue, or a triage queue?

I built a toy repo on this machine: seven agent branches, a triage.py, a 90-minute budget simulation, an overnight throttle counter. Commits are real; CI status and labels live in JSON — not the live GitHub API; nothing was executed on Windows. Every number below is from this run.

Setup: seven overnight PRs, one wrong default sort

Overnight agent PRs into morning two-pass triage

# id opened size CI one-liner
101 docs-typo 1 +1/−1 · 1 file green README typo
102 lockfile-bump 2 +92/−3 · 2 files green lock only
103 big-cleanup 3 +3510/−6 · 45 files green "no behavior change" rename
104 feature-with-tests 4 +20/−1 · 2 files green line discount + tests
105 drive-by 5 +64/−2 · 16 files green dark mode + logging + CI image, no issue
106 failing-ci 6 +18/−0 · 3 files red inventory reserve, import broken
107 auth-session-fix 7 +17/−5 · 2 files green SameSite session regression

By created, auth is last. Cleanup is third — early enough to eat the morning.

FIFO: deep-read from the top, die on the cleanup

The simulated reviewer does the common thing: open the list, deep-read from the top, cost by size band (tiny 8 / small 15 / medium 25 / large 55 / huge 90 minutes), budget 90.

The ledger:

  • docs-typo 8 → 82 left
  • lockfile 25 → 57 left
  • big-cleanup needs 90, only 57 left → stuck partial deep-read, budget zero
  • feature, drive-by, failing-ci, auth-session-fix all deferred

merge_ready holds only docs and lockfile. The login-breaking fix never gets touched before standup. The drive-by is not killed — merely unread. The red CI PR still occupies attention.

This is not a reviewer-skill failure. The queue semantics are wrong: overnight output is treated as time-ordered merge candidates, so the largest early cleanup wins the pre-standup attention by construction.

Risk-first: disposition, then deep-read

Same PRs, new rule. Pass 1: ≤1 minute each — receipt, paths, size, CI, scope creep — no full diff. Emit kill / fix-track / merge-track plus pr-risk:high|med|low.

FIFO vs risk-first on the same 90 minutes

triage.py ordered this run as:

  1. auth-session-fix → merge-track · high (small, green, receipt, touches auth/)
  2. failing-ci → kill (red CI; do not spend review minutes)
  3. drive-by → kill (no receipt, multi-intent, "while I was here")
  4. lockfile / docs / feature → merge-track
  5. big-cleanup → fix-track (huge; defer or split — not a pre-standup deep read)

Pass 1 labels all seven with 83 minutes left. Pass 2 deep-reads only merge-track: auth, lockfile, docs, feature finish; 24 minutes remain. Cleanup stays on fix-track. Drive-by and red CI leave the "to review" set in pass one.

Versus FIFO: auth moves from "never reached" to "first deep read"; cleanup moves from "eats the morning" to "labeled defer"; list noise stops pretending it is a merge candidate.

Signals (deterministic, copyable)

No model "vibes." triage.py uses fixed signals you can edit tomorrow without changing the philosophy:

  • Path risk: auth/, session, billing, .github/workflows, lockfiles raise risk
  • Size band: add+del and file count → tiny…huge → estimated minutes
  • CI: failure → kill (send back; do not review red)
  • Receipt: ## Intent / Receipt with Issue, Scope, Risk, Tests, Out of scope
  • Scope creep: no Issue, "quick wins" / "and also" / "while I was here" → kill
  • Opened order: tie-break only — never the primary sort key

The auth PR at +17/−5, two files, receipt, path hit on auth/session.py → high + merge-track. Cleanup at +3510 / 45 files → huge + fix-track. Drive-by without receipt, body admitting "No issue linked" → kill. Swap path regexes for your layout; do not swap "who opened first" back in as the default.

Labels as state, not chat folklore

triage label state machine

Pass 1 must produce filterable state, not a Slack "let's park this":

  • triage:pending → triage:kill | triage:fix | triage:merge
  • optional pr-risk:high|med|low

Machines 4–5 filter is:pr is:open label:triage:merge before standup, then sort by risk. Machine 7 can refuse merges while triage:pending remains. The kit prints gh pr edit templates; this run only wrote labels into pr_state.json, not a remote repo.

Without labels, disposition lives in one reviewer's working memory — handoff, leave, or one more agent push, and the queue semantics evaporate.

Overnight throttle: the upstream gate on review budget

Even a ruthless morning triage still pays Pass 1 for every overnight PR. Better: agents on machines 1–3 call a gate before gh pr create:

max_overnight_prs=3 throttle

Config max_overnight_prs: 3 (aligned to ~90 minutes next day). throttle.py --demo: first three ALLOW, fourth and fifth REFUSE (exit 2).

Trade-off, stated plainly: throttle alone is not enough. In the demo the first three were docs, lockfile, cleanup — auth was refused. Real systems must couple the counter to "who earns an overnight slot": receipt + issue + high-risk hotfix first; drive-bys should never increment the counter. A triage:hotfix bypass can exist, but daytime queue should still be preferred so bypass does not become a new FIFO.

Put it in AGENTS.md as a hard rule: before 18:00–08:00 opens, run throttle; no Intent/Receipt → do not open; one intent per PR.

Mechanism: why FIFO got more poisonous with agents

In the human-only era, overnight PRs were few; reading top-down wasted attention on an occasional cleanup. With agents on machines 1–3 all night, size skew appears: many docs/small fixes, and at least one industrious "code health" cleanup that often pushes earlier than a hotfix (hotfixes need repros and logs). GitHub's default list reinforces open time. Reviewer habit is "clear the top." Together, small high-risk diffs sink stably.

Second layer: a merge queue implies "green means ship"; a triage queue implies "decide whether this deserves today's attention." Agents are good at turning CI green (failing-ci here is the deliberate counterexample); they will not allocate your attention. Green × early × large is the FIFO deception optimum.

When this does not fit

  • You almost never open more than two overnight PRs. Throttle and Pass 1 are ceremony.
  • Nobody owns a morning review window. You are missing a duty roster, not a sort key — assign machines 4–5 first.
  • Every overnight change must ship the same day (compliance / hotfix window). Allow only hotfix overnight slots; do not run triage theater on seven ordinary PRs.
  • No stable path conventions. Path-risk signals go blind; name auth/, billing/, workflows first.
  • Model summaries instead of disposition. Summaries still bait deep reads; Pass 1 needs executable kill/fix/merge, not another paragraph.

Not the same article as "how to read a large PR"

Once Pass 2 has chosen merge-track, how to carve a large diff for humans is a review-map problem. This piece stops earlier on purpose: do not put the cleanup into Pass 2. Reverse that order and the best map still spends ninety minutes on the wrong PR.

Two morning failures that look different

The first is above: FIFO sticks on the cleanup; auth is deferred. The list is still long; the reviewer feels tired. The tiredness is real; the output is wrong — attention went to low-risk, high-cost work.

The second is quieter: the reviewer skips the cleanup by instinct, opens auth, kills the drive-by, and leaves nothing behind. Labels stay empty; chat says "big one this afternoon." At 14:00 machine 2's agent force-pushes the cleanup again; machine 5 starts a shift and sorts by created from the top. Instinct triage that never becomes queue state did not happen.

Pass 1's "one minute" is not speed for its own sake — it forces a triage:* write. A skip without a label only transfers the FIFO tax to the next shift.

How agents will game the rules (and what to concede)

Agents can read AGENTS.md and "comply" beautifully: split three unrelated edits into three overnight PRs, fill max_overnight_prs=3, each with a tidy Receipt. Throttle looks green; the morning is still three medium PRs and the auth hotfix still cannot enter.

Do not raise the cap to 10. Harden who earns a slot:

  • Receipt Risk: high with paths hitting auth|billing may displace an occupied overnight pr-risk:low slot (comment which PR was displaced).
  • No issue number → throttle refuses even if a slot is free.
  • Themes triage:killed two days running enter cooldown — same intent banned for the day.

Concede hotfixes: when production is dropping sessions, a human-applied triage:hotfix may open a fourth. Do not concede "the agent declared this a hotfix."

Review budget is calendar-back, not agent-forward

On a three-person team, pre-standup deep-review time on machines 4–5 is usually overestimated. After standup, yesterday's fix-track, and Windows failure bounce-backs, net budget is often 60–90 minutes. Agents keep opening while "context remains."

The right input to max_overnight_prs is next-day net review minutes divided by average merge-track deep-read minutes — not how many diffs a model can emit overnight. I set 3 because a 90-minute window needs ~7 minutes for Pass 1 and ~60 for auth+lockfile+docs+feature in Pass 2. Your numbers differ; the formula should not.

If net budget is often under 45 minutes, the overnight cap is 1–2, and feature PRs wait for daytime. That is not punishing agents; it is naming review as the scarce resource.

Handoff to machines 6 and 7

Machine 6 need not queue Windows work for triage:kill; huge triage:fix cleanups can wait until split. Machine 7 can soft-block merges while any triage:pending remains — forcing Pass 1 to exist. None of that is required on day one; it shows morning disposition is an input to the seven-machine pipeline, not a reviewer's private note.

This run did not wire Windows or a live merge gate. The point is only: queue hygiene saves more than reviewer eyesight — it saves downstream validation on work you already decided to kill or defer.

How I know FIFO really "sticks" on the cleanup

Objection: a human seeing 45 files would skip. Yes — and that skip is already handmade triage. The FIFO simulation models trusting list order and deep-reading downward; the moment you skip, you have stepped toward risk-first, but without labels or Pass 1 accounting the skip cannot hand off to the next shift.

Estimated minutes are a rule table, not an eye tracker. Setting huge=90 matches "the whole pre-standup budget"; even if you believe the cleanup is a 40-minute read, FIFO still schedules it before auth — the qualitative claim stands: open order is not risk order.

Reproduce: unpack the kit → python3 run_lab.py. Check results.json for stuck-partial-deep-read on big-cleanup, auth-session-fix inside risk_sim_summary.merge_ready, and the fourth throttle entry allowed: false.

Smallest copy-tomorrow steps

  1. AGENTS.md (machines 1–3): throttle before overnight gh pr create; require Intent/Receipt; ban drive-bys.
  2. Create labels: triage:pending|kill|fix|merge, pr-risk:high|med|low.
  3. Ten minutes before standup: run triage.py (or label by the same signals), filter triage:merge, high first.
  4. Set max_overnight_prs from next-day review minutes / average deep-read minutes, not from how many PRs agents can open.
  5. Huge cleanups are always fix-track: split or schedule off the pre-standup window.

A one-week rollout that does not bounce back to FIFO

Do not ship throttle, labels, scripts, and merge gates in the same week. Friction sends people back to FIFO.

Days 1–2: Labels only. Machines 4–5 do Pass 1 by hand before standup. Success: no overnight PR remains without triage:* at standup; auth-sized fixes are at least merge+high, not buried.

Days 3–4: AGENTS.md hard-requires Receipt; close agent PRs that lack it (with a comment — do not silently deep-read). Count kill reasons — if drive-bys are more than half, fix agent prompts before hiring more reviewers.

Day 5: Turn on max_overnight_prs from net budget. Watch for "three low-risk slots full, hotfix blocked"; only then add high-risk displacement.

After that: wire triage.py to exported JSON or a small bot, and let machine 7 refuse pending. Automation amplifies a habit that already works; it does not replace the habit.

If days 1–2 cannot finish labeling before standup, later scripts only automate FIFO more prettily. Prove disposition happens, then tool it.

What was actually run

Run: toy git repo with seven branches; triage.py classify/sort; 90-minute FIFO vs risk-first budget sim; throttle.py ALLOW×3 / REFUSE×2; labels into pr_state.json plus printed gh templates.

Not run: not live GitHub/Actions; labels not applied remotely; no Windows execution; no seven-machine integration.

Kit: overnight-triage-kit.tar.gz — triage.py, throttle.py, labels_kit.py, build_repo.py, run_lab.py, AGENTS.md snippet, illustrative morning-triage.yml, results.

Smallest step today: stop sorting tomorrow morning by created. Label triage:* first, open only triage:merge. If you catch yourself twenty minutes into a 45-file cleanup while auth is still pending, you just paid the FIFO tax again.