AI

周五走人后代理还在合 PR:周一早上 main 已经偏了,刹车装在哪?(中英切换)

周五晚上六点,Tech Lead 在 4 号机关掉笔记本。main 上最后一笔是下午审过的折扣空值修复,支付、登录、迁移都没动。仓库规则写着「CI 全绿可合」——这是工作日的默认,也写进了 1–3 号机上代理的 AGENTS.md。

周六凌晨两点,1 号机代理修完 CDN 后面的 SameSite 问题,CI 绿,gh pr merge。早上十点,2 号机代理顺手 bump 了 eslint 并重写 lockfile。周日下午,3 号机代理给 orders.user_id 加了 CREATE INDEX CONCURRENTLY。其间还有 Stripe webhook 429 重试、workflow 缓存、一行 terraform 清理。每张 PR 都绿。没有人按按钮——规则说可以合,代理就合了。

周一早上九点,4 号机打开 GitHub:main 相对周五已经走出七到十个提交。支付路径改了,cookie 策略改了,生产迁移已经跑过,lockfile 和 CI 工作流也不再是周五那一版。要回答的问题不是「隔夜堆了哪些未合 PR」(那是 /overnight-agent-pr-morning-triage/),而是:人离开之后已经合进 main 的东西,刹车原本该装在哪?

周五走人后的合入时间线

现场:三种人、七台机、一个错误默认

固定角色不变:1–3 号机高频写代码开 PR,4–5 号机审查,6 号机 Windows 验证,7 号机合并/运维。工作日里「CI 绿 → 合」能跑,是因为 4–5 号机还在看,7 号机还在值。周五 18:00 到周一 09:00,人离开了,默认却没改。

代理不会自己发明「周末政策」。它们读到的是:测试绿、无 do-not-merge 强制检查、token 有合入权。于是周末变成了无人值守的合入窗口。今天是 2026-11-07 周六——这篇不是事后复盘模板,是正在发生的窗口。

若你的团队已经在用「隔夜开 PR、周一再分诊」的节奏,很容易以为周末安全:PR 都还开着。一旦把 auto-merge 或代理侧 gh pr merge 打开,安全假设就反了——合入发生在人看不见的小时里,周一看到的是历史,不是队列。刹车要前移到合入瞬间,而不是周一的日历。

实验室:同一个周末,五种刹车

本机用一份 JSON fixture 排了 13 张代理 PR(drive-by 清理、中等重构、auth、支付、迁移、lockfile、workflow、infra),窗口为周五 18:00 → 周一 09:00(Asia/Taipei)。另跑 40 个周末的蒙特卡洛(CI 轻微抖动;10% 周末有人临走前祝福一张中等 PR)。这些合入数字是模拟,不是线上 GitHub merge API。 玩具仓库另有一条真实 git 时间线,用来摸 Monday 的 diff。

五种策略:

策略 含义
A 无刹车 CI 绿即合
B 仅标签 人贴了 merge-freeze,无 required check;代理忽略
C 闸门脚本 窗口内拒绝,除非带 human:weekend-ok
D Actions 冻结 cron 翻转 MERGE_FREEZE + 必过 merge-window 检查(语义同 C)
E 配额+黑名单 在闸门上再限未祝福合入 ≤2,并拦截高风险路径

五种策略对比

主周末(无任何 human:weekend-ok)headline:

  • A / B: 合入 10,高风险 6,周一理解 258 分钟,revert hard(100 分)。B 与 A 数字完全相同——只贴标签等于没装刹车。
  • C / D: 合入 0,高风险 0,周一 5 分钟,revert none。
  • E: 合入 2(两张 drive-by),高风险 0,周一 14 分钟,revert easy。

40 个周末平均: A/B 合入 12.35、hard revert 100%;C/D 合入 0.17(偶发祝福)、hard 0%;E 合入 2.17、hard 0%。人临走前祝福一张中等 PR(实验室里的 #505 限流)时,C 合入 1,E 合入 2——祝福是逃生口,不是默认车道。

高风险前缀在政策里写死:src/auth/、src/payments/、migrations/、lockfile、.github/workflows/、infra/。A/B 周末会把这些全带上 main;C/D/E 在无祝福时都不让高风险落地。路径名单按你们的仓改——电商可能还要加 src/pricing/,SaaS 可能加 src/billing/——重要的是名单在闸门里强制,而不是写在周报里希望大家记得。

玩具仓库:周五 SHA → 周一 SHA

在本机建了一个线性玩具仓库。周五基线 d54365f:折扣 10%、SameSite=Lax、webhook 单次尝试、无 0042 迁移。按 A 的路径合入子集后,周一 tip 5be3b99,git log 七笔,git diff --stat 覆盖 auth、payments、billing、migration、lockfile、workflow。

对迁移提交做 git revert 在线性历史上是干净的——文件删回。生产里 CREATE INDEX CONCURRENTLY 不是 git revert 能收回的:索引已经在库上。lockfile 与 workflow 的回滚也要另算发布与缓存。这就是为什么 A 的 revert 标签是 hard:不是 git 语法难,是已经作用在现实上的变更难撤。

机制:为什么「CI 绿」在周末是错的默认

合并窗口状态机

  1. CI 绿回答的是「测试过」,不是「人希望它在睡着时上 main」。 把两者绑在同一条规则上,人一离开,合入权就落到代理手里。
  2. 标签不是控制面。 merge-freeze / do-not-merge 若没有 required check 或合并队列强制执行,对持有合入 token 的代理就是注释。B ≡ A。
  3. GitHub 原生按时间 ruleset 能力有限。 实操刹车是:Actions cron 在周五 18:00(Asia/Taipei,对应 UTC cron)把仓库变量 MERGE_FREEZE=true,周一 09:00 扳回;pull_request 任务跑 merge_window_gate.py --check-env;branch protection / ruleset 要求名为 merge-window 的检查通过。这就是 D,也是我建议的默认装法。
  4. 祝福标签是显式例外。 human:weekend-ok 表示有人承担了「这个周末可以上」的责任。没有它,窗口内合入应失败。
  5. E 的配额与黑名单是松闸,不是替代。 允许少量 drive-by 减少周一 PR 堆积,但 auth/支付/迁移/lockfile/workflow/infra 仍拦。若你的代理周末只准修文档,E 够用;若你根本不想周末动 main,用 C/D,合入 0。

闸门脚本在本机对 fixture 跑通:周六无祝福 → exit 1;带 human:weekend-ok → exit 0;周一上午 → exit 0;周五 17:59 → exit 0,18:00 → exit 1;配额模式下未满额的 README 改动 → exit 0,支付路径 → exit 1。

代价,以及什么时候别装

  • 多一个必过检查:工作日白天 MERGE_FREEZE=false,检查应直接过;不要让它变成日常摩擦。
  • cron 时区算错会早冻或晚冻——政策文件里写死 Asia/Taipei,UTC cron 按 +8 换算(kit 里 Fri 10:00 UTC / Mon 01:00 UTC)。
  • 紧急热修必须留祝福路径,并约定谁有权贴 human:weekend-ok(通常是 4/5/7 号机上的人,不是代理自己贴)。
  • 不必装: 周末明文禁止一切合入且 token 已收掉合入权;或只有一台机器、人从不离开合入按钮。
  • 装了也救不了: 有人用 admin bypass / 删除检查赶工;或迁移在合入后由 CD 立刻跑到生产——那是发布刹车,要另做,见边界。
  • 不是审查租约、不是 Windows 证据拉取、不是 Issue 抢单、不是隔夜未合 PR 分诊。触发器在这里是:人离开后,绿 PR 还能否进 main。

失败现场:A 策略周一早上实际在干什么

把主周末 10 张合入按风险摊开,周一 4 号机的队列大概是这样:

  1. 先分拣(约 40 分钟)。 哪些是 drive-by(slugify、CSS 排序),哪些碰了钱和登录。没有闸门时,分拣本身就要把 auth、支付、迁移、lockfile、workflow、infra 六类高风险路径逐个点开。
  2. 再判断「能否留」(约 90–120 分钟)。 SameSite 改成 None 在 CDN 后也许正确,但周五没人看过威胁模型;Stripe 429 重试可能把幂等假设打穿;CREATE INDEX CONCURRENTLY 已经在库上——即使 git revert 删掉迁移文件,索引还在。
  3. 再决定撤哪些(剩余时间不够)。 lockfile 与 workflow 回滚要重跑发布;infra 变量删除可能已作用于下一轮 apply。258 分钟的「理解」还没包含真正的修复与沟通。

这就是「main 已经偏了」的具体含义:不是 diff 统计好看不好看,而是人离开时的产品假设被代理用「绿」合法地改写了。B 策略下过程完全一样——仓库上挂着红色的 merge-freeze 标签,代理合入日志里不会出现这几个字。

为什么代理会「忽略」标签(以及你不能靠骂它)

代理不是恶意。它们的工具循环通常是:读 AGENTS.md → 开 PR → 等检查 → gh pr merge。除非你把「读仓库标签 / 跑闸门脚本 / 看 required check」写进循环,并且失败时退出,否则标签只是 UI 装饰。我们在多仓里见过的失败模式就三种:

  • 提示写了「周末别合」,但合入命令没有前置检查 → 等价于 A。
  • 人贴了 do-not-merge,代理用的 token 不受该标签约束(标签只影响人类合并按钮或未配置的 merge queue)→ 等价于 B。
  • 检查存在,但代理遇到红检查就 --admin 或关掉保护 → 刹车被人为卸掉。

所以文章标题问「刹车装在哪」:装在合入路径上,不是装在提示词里。C 把刹车装在脚本 exit code;D 把同一语义抬成 GitHub 必过检查;E 在闸门上再加配额与路径黑名单。提示词仍然要写,但提示词是说明书,不是锁。

装 D 时会踩的三个坑

  1. 时区。 cron 是 UTC。Asia/Taipei 周五 18:00 = UTC 周五 10:00;周一 09:00 = UTC 周一 01:00。写错成「本地周五 18 点的 cron」会提前或推后整个冻结窗。政策 JSON 里写人类可读的台北时间,workflow 里写换算后的 UTC。
  2. 失败开放 vs 失败关闭。 读不到 MERGE_FREEZE 变量时,样例默认偏失败开放(当作 false),避免工作日被误伤。若你的威胁模型是「宁可变慢也别无人合入」,改成失败关闭——读不到就当作冻结。二选一写进 AGENTS.md,别靠默契。
  3. 变量写权限。 cron job 要改 repo variable,需要超出默认 GITHUB_TOKEN 的权限(fine-grained PAT 或 GitHub App)。kit 里的 schedule 步骤默认 只打印 gh variable set,避免把样例直接推到生产就改你的变量。接真之前先在空仓演练。

C 和 D 数字相同,选 D 的理由是:代理可以忘记跑脚本,不能(轻易)绕过 required check。 把闸门只留在代理侧,等于回到「自觉」。

和发布刹车的分工

合入刹车 ≠ 发布刹车。即使 C/D 放行一张带 human:weekend-ok 的迁移 PR,CD 仍可能在周六把迁移打到生产。本文的闸门管的是 git 主线;生产释放还要:环境保护规则、手动 approve、或「周末 CD 不跑 migrate」的流水线条件。两者都缺时,你会得到「main 绿且生产已偏」;只装合入闸门时,最坏是「main 周末不动,生产保持周五」。对三人七机团队,先装合入闸门,成本更低、回滚面更小。

和相邻文章的边界

  • /overnight-agent-pr-morning-triage/: 管的是周一早上还开着的 PR,90 分钟先杀谁。本文管的是周末已经合进 main 的提交与爆破半径。两篇可以同一周并存:先靠本文减少「已合入」冲击,再靠那篇清理「未合入」队列。
  • /revert-bad-pr-after-more-merges/: 合入后如何撤。本文尽量让高风险周末根本合不进去,减少周一被迫 revert 的次数。若你已经处在 A 的周一,先去看那篇的 revert closure,再回来装闸门。
  • /approve-then-force-push-which-sha/: Approve 与 HEAD 的 SHA 对齐。本文是时间窗口,不是 SHA 锚点。
  • /agent-machine-least-privilege/: 审查机不该有合入权。互补:即使开发机有合入权,窗口闸门仍应挡住无人值守合入。最小权限减少爆破面,合并窗口减少无人值守面。
  • /two-agents-claim-same-issue/: 谁开工。本文:谁合入。抢单失败浪费 tokens;周末误合浪费周一上午与可能的生产修复。

明天可抄的最小集

  1. 建标签 human:weekend-ok(和可选的信息性 merge-freeze)。记住:只有标签没有检查 = 策略 B。
  2. 把 merge_window_gate.py + merge_window_policy.json 放进 tools/;AGENTS.md 写明:CI 绿 ≠ 可合;gate exit 1 时禁止 --admin 绕过;禁止代理给自己贴 human:weekend-ok。
  3. 加上 kit 里的 .github/workflows/merge-window.yml:cron 翻转 MERGE_FREEZE,PR 上跑闸门;在 ruleset 里勾选 required check merge-window。先在空仓把「冻结开 / 冻结关 / 祝福放行」三条路径跑绿。
  4. 高风险路径写进政策;需要周末合入时由 4/5/7 号机上的人贴 human:weekend-ok,并在 PR 评论写一句原因。
  5. 若允许极少量 drive-by,开 E 的配额(默认 2);否则保持 C/D 的「窗口内默认 0」。
  6. 周五离开前花两分钟看:MERGE_FREEZE 是否会在 18:00 变为 true;有没有未合完又必须周末上的 PR——有就现在祝福,没有就走人。

诚实说明:什么跑了,什么没有

本机跑过:

  • simulate_weekend.py:A–E 主周末 + 40 周蒙特卡洛;headline 见上。合入数、高风险数、周一理解分钟、revert 分是 fixture 策略函数的输出。
  • merge_window_gate.py:日历窗口、祝福、配额、高风险黑名单 fixture;周五 17:59/18:00、周六、周一边界;--live 只打印接线说明。
  • 玩具 git:周五 d54365f → 周一 5be3b99 七笔合入;对迁移提交的 git revert 在历史上干净,并记下生产索引无法靠 git 收回。

没有跑:

  • 没有对 shuizhengqi1/pages 或任何产品仓执行真实 gh pr merge。
  • 没有在真实仓库翻转 MERGE_FREEZE 变量;workflow 样例默认 print-only。
  • 没有七机联调;代理「忽略标签」是策略假设,与我们在多仓看到的行为一致,但本实验室未接七个 runner。
  • 没有声称 GitHub 已提供完整「按墙钟时间的 ruleset」——若你的企业版以后有,仍建议保留必过检查作兜底。

套件:weekend-agent-merges-no-human-kit.tar.gz — 闸门脚本、模拟器、results.json、policy、Actions 样例、AGENTS.md 片段、标签脚本、玩具 log。

判断就一句:CI 绿只证明测试过,不证明人睡着时也要上 main。 今天最小的一步:别再问「这张 PR 绿了吗」。人离开之前问「合入权是否还开着?」——若答案是开着,而闸门检查不存在,周一的 main 就不是你周五离开时的产品。

Friday 18:00. The Tech Lead closes the laptop on machine-4. The last commit on main is an afternoon-reviewed null-guard on discounts. Payments, auth, and migrations are untouched. The repo rule says "merge when CI is green" — a weekday default, also baked into AGENTS.md on machines 1–3.

Saturday 02:00, machine-1's agent finishes a SameSite fix behind the CDN. CI green. gh pr merge. Saturday morning, machine-2 bumps eslint and regenerates the lockfile. Sunday afternoon, machine-3 lands CREATE INDEX CONCURRENTLY on orders.user_id. In between: Stripe webhook retries on 429, a workflow cache tweak, a one-line terraform cleanup. Every PR is green. No human pressed the button — the rule said merge, so the agents merged.

Monday 09:00, machine-4 opens GitHub: main has moved seven to ten commits past Friday. Payment paths changed, cookie policy changed, a production migration already ran, lockfile and CI workflow are no longer Friday's. The question is not "which open overnight PRs do I kill in 90 minutes" (/overnight-agent-pr-morning-triage/). It is: for commits already on main while humans were away, where should the brake have been?

Timeline after humans leave Friday

Scene: three people, seven machines, one wrong default

Roles stay fixed: machines 1–3 open PRs at high frequency, 4–5 review, 6 validates on Windows, 7 merges/ops. "CI green → merge" works on weekdays because 4–5 are still looking and 7 is still on shift. Friday 18:00 to Monday 09:00, humans leave; the default does not change.

Agents do not invent a "weekend policy". They see: tests green, no enforced do-not-merge check, token can merge. The weekend becomes an unattended merge window. Today is Saturday 2026-11-07 — this essay is about the window that is open right now, not a retrospective template.

Lab: one weekend, five brakes

On this box a JSON fixture schedules 13 agent PRs (drive-by cleanup, medium refactors, auth, payments, migration, lockfile, workflow, infra) across Fri 18:00 → Mon 09:00 (Asia/Taipei). A 40-weekend Monte Carlo adds light CI flake and a 10% chance a human blesses one medium PR before leaving. Merge counts are simulated — no live GitHub merge API. A separate toy git timeline is a real repo on disk for Monday's diff.

Strategy Meaning
A no brake CI-green merges
B label-only Human stuck merge-freeze; no required check; agents ignore
C gate script Refuse in-window unless human:weekend-ok
D Actions freeze Cron flips MERGE_FREEZE + required merge-window check (same semantics as C)
E quota+blocklist Gate plus ≤2 unblessed merges and high-risk path blocklist

Five-strategy comparison

Primary weekend (no human:weekend-ok) headlines:

  • A / B: merged 10, high-risk 6, Monday understand 258 min, revert hard (score 100). B matches A exactly — a label without enforcement is not a brake.
  • C / D: merged 0, high-risk 0, Monday 5 min, revert none.
  • E: merged 2 (drive-bys), high-risk 0, Monday 14 min, revert easy.

40-weekend averages: A/B merged 12.35, hard revert 100%; C/D merged 0.17 (rare bless), hard 0%; E merged 2.17, hard 0%. When a human blesses medium PR #505 before leaving, C merges 1 and E merges 2 — blessing is an escape hatch, not the default lane.

High-risk prefixes are pinned in policy: src/auth/, src/payments/, migrations/, lockfiles, .github/workflows/, infra/. A/B drag all of them onto main; C/D/E keep high-risk off without a bless.

Toy repo: Friday SHA → Monday SHA

A linear toy repo on this box. Friday baseline d54365f: 10% discount, SameSite=Lax, single-shot webhook, no 0042 migration. After an A-path subset of merges, Monday tip 5be3b99, seven commits in git log, git diff --stat covering auth, payments, billing, migration, lockfile, workflow.

git revert of the migration commit is clean on linear history — the file disappears. In production, CREATE INDEX CONCURRENTLY is not something git revert undoes: the index already exists. Lockfile and workflow rollbacks also cost a release. That is why A's revert label is hard: not because git syntax is hard, but because changes that already hit reality are hard to take back.

Mechanism: why "CI green" is the wrong weekend default

Merge-window state machine

  1. CI green answers "tests passed", not "humans want this on main while asleep". Bind them in one rule and merge rights fall to agents the moment people leave.
  2. Labels are not a control plane. merge-freeze / do-not-merge without a required check or merge queue is a comment to an agent holding a merge token. B ≡ A.
  3. Native time-based rulesets are limited. The practical brake: Actions cron sets repo variable MERGE_FREEZE=true Friday 18:00 Asia/Taipei (UTC cron in the kit), flips it off Monday 09:00; a pull_request job runs merge_window_gate.py --check-env; branch protection / ruleset requires the check named merge-window. That is D — the install I recommend by default.
  4. Bless labels are explicit exceptions. human:weekend-ok means a human owns "this may ship this weekend". Without it, in-window merges should fail.
  5. E's quota and blocklist loosen the gate; they do not replace it. A few drive-bys cut Monday PR pile-up; auth/pay/migration/lockfile/workflow/infra still block. If weekend agents may only touch docs, E is enough; if main must not move, use C/D (merged 0).

The gate script passed fixtures on this box: Saturday without bless → exit 1; with human:weekend-ok → exit 0; Monday morning → exit 0; Friday 17:59 → exit 0, 18:00 → exit 1; under-quota README in quota mode → exit 0; payments path → exit 1.

Costs and when not to bother

  • One more required check: on weekday daytime MERGE_FREEZE=false and the check should pass immediately — do not turn it into daily friction.
  • Wrong cron timezone freezes early or late — pin Asia/Taipei in policy; convert to UTC for cron (kit: Fri 10:00 UTC / Mon 01:00 UTC).
  • Hotfixes need a bless path and a clear owner for human:weekend-ok (usually a human on machines 4/5/7 — agents must not self-bless).
  • Skip when: weekends already ban all merges and merge tokens are revoked; or one machine, human never leaves the merge button.
  • Will not save you when: someone admin-bypasses / deletes the check; or CD runs migrations the moment they land — that needs a release brake, see boundaries.
  • Not a review lease, Windows evidence fetch, Issue claim, or overnight open-PR triage. The trigger here is: after humans leave, may a green PR still enter main?

Failure scene: what Monday looks like under A

Spread the primary weekend's 10 merges by risk and machine-4's Monday queue looks like this:

  1. Triage (~40 min). Which are drive-bys (slugify, CSS sort) and which touched money and login. Without a gate you must open auth, payments, migration, lockfile, workflow, and infra — six high-risk classes — just to sort.
  2. Decide what may stay (~90–120 min). SameSite=None behind a CDN may be correct, but Friday never reviewed the threat model; Stripe 429 retries may break idempotency assumptions; CREATE INDEX CONCURRENTLY is already on the database — even if git revert deletes the migration file, the index remains.
  3. Choose what to undo (time runs out). Lockfile and workflow rollbacks need another release; deleting an infra variable may already have hit the next apply. The 258 "understand" minutes do not yet include real fixes or coordination.

That is what "main has drifted" means: not an ugly diffstat, but the product assumptions at human departure rewritten legally under "green". Under B the process is identical — a red merge-freeze label hangs on the repo, and it never appears in the agent merge log.

Why agents "ignore" labels (and why scolding them fails)

Agents are not malicious. Their loop is usually: read AGENTS.md → open PR → wait for checks → gh pr merge. Unless you put "read repo labels / run the gate / respect the required check" into that loop and exit on failure, a label is UI decoration. Three failure modes we see across repos:

  • The prompt says "don't merge on weekends" but the merge command has no pre-check → equals A.
  • A human stuck do-not-merge, yet the agent's token is not constrained by that label (it only affects the human merge button or an unconfigured merge queue) → equals B.
  • The check exists, but on red the agent --admin bypasses or turns protection off → the brake is removed by hand.

So the title's question — where does the brake go — means: on the merge path, not in the prompt. C puts it on a script exit code; D lifts the same semantics into a required GitHub check; E adds quota and a path blocklist on top. Prompts still matter; they are the manual, not the lock.

Three pits when installing D

  1. Timezone. Cron is UTC. Asia/Taipei Friday 18:00 = Friday 10:00 UTC; Monday 09:00 = Monday 01:00 UTC. A cron written as "Friday 18:00 local" freezes the window early or late. Keep human-readable Taipei time in the policy JSON; put converted UTC in the workflow.
  2. Fail open vs fail closed. If MERGE_FREEZE cannot be read, the sample defaults fail-open (treat as false) to avoid weekday false freezes. If your threat model is "rather slow than unattended merge", fail closed — missing variable means frozen. Pick one in AGENTS.md; do not leave it to vibe.
  3. Variable write permission. The cron job that flips a repo variable needs more than the default GITHUB_TOKEN (fine-grained PAT or GitHub App). The kit's schedule step prints gh variable set by default so a sample push does not mutate your variables. Rehearse on an empty repo before wiring prod.

C and D share numbers; pick D because agents can forget to run a script, and cannot (easily) skip a required check. Leaving the gate only on the agent side is back to "please be good".

Split from a release brake

A merge brake ≠ a release brake. Even if C/D allow a migration PR with human:weekend-ok, CD may still apply it to production on Saturday. This gate covers the git trunk; production release still needs environment rules, manual approve, or a pipeline condition that skips migrate on weekends. Missing both yields "main green and prod already drifted"; merge-gate alone yields at worst "main quiet, prod stays Friday". For a three-person seven-machine team, install the merge gate first — cheaper, smaller rollback surface.

Boundaries with sibling posts

  • /overnight-agent-pr-morning-triage/: open PRs Monday morning, 90-minute kill order. This post is commits already on main over the weekend and their blast radius. They can coexist in one week: this post shrinks already-merged shock; that one clears the still-open queue.
  • /revert-bad-pr-after-more-merges/: how to undo after the fact. This post tries to keep high-risk weekend merges off main so Monday needs fewer reverts. If you are already in an A-style Monday, read that revert closure first, then come back to install the gate.
  • /approve-then-force-push-which-sha/: Approve vs HEAD SHA. This post is a time window, not a SHA anchor.
  • /agent-machine-least-privilege/: review machines should not hold merge rights. Complementary: even when dev machines can merge, the window gate should still block unattended merges. Least privilege shrinks blast radius; the merge window shrinks the unattended surface.
  • /two-agents-claim-same-issue/: who may start. This post: who may merge. Failed claims burn tokens; weekend mis-merges burn Monday morning and maybe a prod fix.

Copy-tomorrow minimum

  1. Create label human:weekend-ok (and optional informational merge-freeze). Remember: label without a required check = strategy B.
  2. Drop merge_window_gate.py + merge_window_policy.json into tools/; AGENTS.md: CI green ≠ mergeable; on gate exit 1 ban --admin bypass; ban agents self-applying human:weekend-ok.
  3. Add the kit's .github/workflows/merge-window.yml: cron flips MERGE_FREEZE, PR runs the gate; tick required check merge-window in the ruleset. On an empty repo, green the three paths freeze-on / freeze-off / bless-allow first.
  4. Pin high-risk paths in policy; weekend merges get a human on machines 4/5/7 adding human:weekend-ok plus a one-line reason in the PR.
  5. If a few drive-bys are acceptable, enable E's quota (default 2); otherwise keep C/D's in-window default of zero.
  6. Before leaving Friday, spend two minutes: will MERGE_FREEZE flip true at 18:00; is there an unmerged PR that must ship this weekend — bless it now, or leave.

Honesty: what ran, what did not

Ran on this box:

  • simulate_weekend.py: A–E primary weekend + 40-weekend Monte Carlo; headlines above. Merge counts, high-risk counts, Monday minutes, revert scores are outputs of fixture strategy functions.
  • merge_window_gate.py: calendar window, bless, quota, high-risk blocklist fixtures; Friday 17:59/18:00, Saturday, Monday boundaries; --live prints wiring only.
  • Toy git: Friday d54365f → Monday 5be3b99 seven merges; git revert of the migration commit clean in history, with a note that production indexes are not undone by git.

Did not run:

  • No live gh pr merge against shuizhengqi1/pages or any product repo.
  • No real flip of a MERGE_FREEZE variable; the sample workflow is print-only by default.
  • No seven-machine integration; "agents ignore labels" is a strategy assumption consistent with multi-repo behavior we see, but this lab did not attach seven runners.
  • No claim that GitHub already ships a full wall-clock ruleset — if your enterprise plan gains one later, still keep a required check as backstop.

Kit: weekend-agent-merges-no-human-kit.tar.gz — gate script, simulator, results.json, policy, Actions sample, AGENTS.md snippet, label script, toy log.

One-line judgment: CI green proves tests passed, not that main should move while humans sleep. Smallest step today: stop asking "is this PR green?". Before humans leave, ask "is merge still open?" — if yes, and no gate check exists, Monday's main is not the product you left on Friday.