guard(queue): recognize existing batches -- no duplicate batching + generated-file tolerance - #734
Merged
Merged
Conversation
FIRST real-world defect in the merge-queue advancer. Two consecutive --advance passes each opened a NEW integration batch over the SAME seven members: #727 (integrate/q-1785727927) then duplicate #728 (integrate/q-1785729091), members #717 #689 #696 #702 #712 #716 #723. Root cause: the `merge-queue-batch` LABEL does not exist in the repository. `gh pr edit --add-label merge-queue-batch` therefore failed at batch creation, and build_batch ignored the result -- #727 shipped with an empty label set. On every later pass `gh pr list --label merge-queue-batch` answered `[]` with EXIT 0 (gh reports an undefined label as an empty result, not an error), so batch discovery was blind while the members still carried `merge-queue` and were admitted and batched all over again. Fix -- discovery no longer rests on a label: * list_open_batches() unions the label filter with open PRs whose head is an `integrate/q-*` branch. That branch name is minted by build_batch and pushed before the PR exists, so it cannot silently fail to be written. * resolve_batch_members() reads the body `Members:` line, falling back to the `integrate #N into` merge-commit subjects for a pre-contract batch. * _advance resolves every open batch's members BEFORE any queue work, excludes them from the queue, and evaluates the batch first. * A batch with no resolvable members whose branch is gone from origin is a `batch_branch_missing` row (stale, left open for a human), distinct from `batch_members_unparseable`. * apply_batch_label() reads the label back, creates it once if missing, retries, and rows `batch_label_failed` rather than passing silently. Second bug, dirty tree: `verify_test_suite_count.py --check` auto-corrects and WRITES tests/CLAUDE.md, so a gate run in the scheduled task's project root left the tree dirty and worktree_is_safe refused to build a batch over the repo's own generated output. worktree_is_safe now checks the branch BEFORE touching anything, and `git restore`s only paths in the new tools/generated_paths.py registry -- and only when EVERY dirty path is registered. One unregistered edit poisons the tree and nothing is touched. Never `git stash` (shared across worktrees), never a blanket restore of all. Tests: 91 -> 114. The regression test drives a full run_pass over the exact production gh responses of that second pass and asserts zero `pr create`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect (first real-world escape from the merge-queue advancer)
Two consecutive
--advancepasses each opened a new integration batch over thesame seven members:
integrate/q-1785727927integrate/q-1785729091Root cause
The
merge-queue-batchlabel does not exist in this repository. So:build_batchcalledgh pr edit <n> --add-label merge-queue-batch, which failed,and ignored the result -- merge-queue batch q-1785727927 #727 shipped with
"labels": [].gh pr list --state open --label merge-queue-batch, whichreturns
[]at exit 0 when the label is undefined. Not an error -- an emptyanswer. Batch discovery was blind.
merge-queue, so they were re-admitted, foundfile-disjoint, and batched all over again.
Verified against live GitHub before writing the fix:
Fix mechanism -- discovery no longer rests on a label
The daemon is stateless by design: labels and branches are its state. A label is a
write that can silently fail; the integration branch name is minted by
build_batchand pushed before the PR exists, so it cannot. The branch is therefore authoritative
and the label is a convenience on top of it.
list_open_batches()unions the label filter with open PRs whose head matchesintegrate/q-<digits>, deduplicated on PR number.resolve_batch_members(batch)reads the bodyMembers:line (already written bybuild_batch, so the common path costs no git call), falling back to parsing theintegrate #N into ...merge-commit subjects on the batch branch for any batch openedbefore that contract.
_advanceresolves every open batch's member set before any queue work,records it in
summary["batched_members"], excludes those numbers from the queue, andevaluates the existing batch (merge on green / dissolve on red, unchanged logic) first.
A PR whose own head is an
integrate/q-*branch can never enter the member queue.git ls-remote --heads origin <branch>empty ->
batch_branch_missingexception row. Members are not unlabelled, the batchPR is left open for a human, and nothing is rebatched.
re-enter the queue on the next pass. Covered by a test.
apply_batch_label()now reads the label back after applying it, creates thelabel once if it is missing, retries, and rows
batch_label_failedif it still did notstick -- the silent failure that started all of this can no longer be silent.
Second bug: dirty tree from a self-inflicted generated file
tools/verify_test_suite_count.py --checkauto-corrects and writestests/CLAUDE.md(
[AUTO-CORRECT] Updated tests/CLAUDE.md and continuing). When that gate ran in thescheduled task's project root, the advancer's next pass saw a dirty tree and refused to
build a batch -- stalled over output the repo itself generates.
worktree_is_safe()now:this function would modify anything in it.
tools/generated_paths.pyregistry, and only whenevery dirty path is registered. One unregistered edit poisons the tree and nothing
is touched.
git restore -- <path>, one path at a time. Nevergit stash(thestash stack is shared across worktrees and would eat another lane's WIP), never a
blanket revert. Re-reads status afterward and still refuses if the tree did not come
clean -- e.g. an untracked file, which
git restorecannot undo.tools/generated_paths.pyis the single registry (tests/CLAUDE.md,tools/CLAUDE.md)with the three membership criteria written down;
tools/auto_merge.pyalready hard-codesthe same pair, so this is where that list should live.
Tests: 91 -> 114 (all green)
TDD: the 18 new assertions were written first and failed against
origin/main. Theheadline regression drives a full
run_passover the exact productionghresponsesof that second pass (batch-label list
[], label-less open list showing #727, sevenmerge-queuemembers) and asserts zeropr createand zero branch construction. Beforethe fix it failed with:
New coverage: branch-based discovery when the label is absent; label+branch dedupe;
member exclusion; closed batch requeues members; batch branch never requeued;
is_batch_branchboundaries; commit-subject member fallback; body-wins-over-fallback(and costs no git call); deleted branch ->
batch_branch_missing; missing label createdthen reapplied; unlabelable batch rowed; label creation never forced; generated-file
restore proceeds / is targeted / refuses non-generated dirt / refuses mixed dirt /
refuses when restore did not clean; wrong branch rejected before any restore; porcelain
parsing (rename, untracked, quoted); registry shared not duplicated; module never stashes.
Three pre-existing fakes were tightened to model reality rather than stubbing every
git/ghcall to a blanket success (pr edit --add-labelnow sticks and reads back;ls-remoteanswers for a live branch; thepr listfake tolerates a label-less call).Zero-sleep proof, the forbidden-lever scan (
--admin/--auto/force-push), and thegh/git-imported-not-duplicated check all still pass unchanged.Verification
tools/CLAUDE.mddocuments both behaviours in the existingmerge_queue.pyentry,pipe-condensed to hold the file at its 149-line cap.
Labelled
merge-queue+merge-priority: the daemon should heal itself first.Not merged.
Generated with Claude Code