ci: merge freezes as owned, expiring GitHub issues

Merged
TE
19d ago
+606-0

Merge freezes become GitHub issues that anyone can see, that have an owner, and that expire on their own, instead of FREEZE: lines in ~/agent-mailbox/WINDOW on cmux-lawrence.

That file broke in three ways. gh-merge-green read it over ssh, so a host without ssh to cmux-lawrence (Huge Blue) couldn't merge into feat-cmux-next at all. Every token in it had lapsed by 2026-10-10, but the tools kept refusing on it. And nobody but its readers could see it was stale.

How a freeze works now

Open an issue and have an admin or maintainer add the merge-freeze label. The title is the reason, and the body says:

paths: cmux-tui/crates/acpmux/ cmux-tui/spec/
expires: 2026-10-20T18:00Z
owner: @someone          (optional; the author by default)
branch: feat-cmux-next   (optional)
  • Only a maintainer turns a freeze on: the label must come from an admin or maintainer, after the body's last edit. Editing the body afterwards, to widen paths: say, turns it off until a maintainer labels it again.
  • A freeze lasts at most 72 hours from when its issue opened. A missing or unparseable paths: or expires: blocks nothing and is reported as malformed, so a typo can't freeze the repository.
  • The owner lets a pull request through by adding freeze-ok to it. The checker reads who added the label from the timeline, so a label from anyone else doesn't count.
  • scripts/ci/merge_freeze.py check decides, using only the GitHub API, so it works the same from any host. It exits 0 for clear, 1 for frozen, and 2 when GitHub can't be read, which counts as frozen.
  • .github/workflows/merge-freeze.yml shows a merge freeze check on pull requests into main. pull_request_target reads the workflow from the base branch, so feat-cmux-next pull requests get the check once the same file lands there, in a one-file follow-up after this merges. Every hour it closes lapsed freezes with a comment, so the open issues are exactly the live freezes. It's pull_request_target, and it checks out only main's checker, never the pull request.
  • docs/ci/merge-freeze.md is the how-to.

Should freezes exist at all?

Mostly no. Per-PR risk is now the job of the risk tiers (manaflow-ai/cmux#21691), and the batch queue validates stacks. What's left is coordination: a release cut, or one lane reworking an area others would collide with. That justifies a small, visible, expiring freeze, not a standing one.

Not in this pull request: switching gh-merge-green

gh-merge-green still reads the WINDOW file. Replacing that block with merge_freeze.py check, and dropping the RELEASE mail it sends to cmux-lawrence, is a separate change. It needs explicit approval, because it edits the merge gate's freeze logic. ~/Projects/briefs/huge-blue-merge-ready.sh has its own hard-coded WINDOW path list and gets the same change. The old file on cmux-lawrence is left untouched.

Testing

  • python3 tests/test_merge_freeze.py: 13 tests pass, including that labels from triage, write, outside users or nobody don't count, and that an edit after the label turns the freeze off. They cover prefix matching including siblings, expiry, the 72-hour cap, malformed freezes, owner-only freeze-ok (including a later removal), the owner: and branch: lines, closed or unlabeled issues, and an unreadable list returning 2. The test is wired into ci-guards.yml and registered in tests/test-execution.toml.
  • Live, read-only against manaflow-ai/cmux: list shows no open freezes, check --pr 21690 exits 0, and expire --dry-run closes nothing.
  • bash scripts/ci/guards-local.sh: 108 pass and 3 fail. On main without this change, 105 pass and 5 fail, and no failure is new.
  • Not run: the workflow itself, which runs from main after merge.

Review: SHIP with nits at 5ff3070b765 (cmux-review subagent), and 2ef1e133f7c fixed those nits. coord-2's independent review at 2ef1e133f7c said fix first: the test registry entry was missing, and anyone could label a freeze. 21a1da5c5d7 fixes both. Left as-is: one freeze-ok can't satisfy two freezes with different owners on the same PR, and approval isn't reset by a later push.
Runtime test: at 21a1da5c5d7, merge_freeze.py list and check --pr 21706 against manaflow-ai/cmux (exit 0), plus the new helpers live: repo_role (admin for teamleaderleo, read for an outsider), last_edited on issue #21611, and last_labeler on #21334's events.

Changelog

none

Proof

Not UI: merge tooling and docs.

🤖 Generated with Claude Code