You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
check_merge_blockers.py --pr N reports a pull request that has already merged as merge-state-unknown, owed by nobody, with the remedy "Re-read the pull request:
mergeability is computed on demand and settles on a later read."
No later read ever settles it. A merged pull request is closed, so mergeable stays null permanently — the advice describes a wait with no terminating condition, and it
reads in the reassuring direction: no party owes an action is literally true, which is
exactly what a caller polling for "can I merge yet" sees while the answer is "you already
did".
Measured
GET /repos/strands-labs/robots/pulls/{n}, 2026-09-05 ~12:45 UTC, immediately after #3219 and #3230 squashed:
The third row is why this is not a one-line special case of a dead state: the genuine
transient that #2586 was written for is live at the same time, on the same fields, so mergeable is None is ambiguous between "GitHub is still computing" and "there is nothing
left to compute". Both merged rows and the open row are byte-identical in every field the
script reads.
The field that disambiguates is already in the payload
resolve_state fetches the whole REST pull request object at scripts/check_merge_blockers.py:802 and reads five keys from it — head, base, draft, mergeable, mergeable_state. The same response carries merged and state,
which answer the question outright. This is the "fetched and not read" failure AGENTS.md
names as the recurring root cause, in the one script whose stated purpose is "ask for the
verdict rather than re-deriving it".
Why the existing model misses it
The script's own docstring states the assumption verbatim, and it is the assumption that
fails:
A merge into the base invalidates it for every open pull request, so a sweep run just
after a merge is precisely when it is null.
True, and it is why merge-state-unknown exists. But the pull request whose own merge
invalidated the value is not open, and nothing downstream re-checks that. evaluate()
branches on state.mergeable with no notion of a pull request that is already merged, and PullRequestState has no field that could carry one.
This is not a regression in #2586 and the fix must not revert it: reading the null as clean
is still wrong (#1035, pusher-only-approval on a DIRTY branch). The null simply has a
third reading that #2585 did not have to separate, in the same shape as reviewDecision: null meaning two different things in AGENTS.md.
Cost, observed
A scheduled cycle polled #3230 to merge it, read merge-state-unknown once at ~12:32
while it was genuinely open and recomputing, and again at ~12:45 after it had merged. The
two reports are identical. The first was correctly treated as transient; only a direct state/merged read established that the second was terminal. The --all-open sweep is
unaffected — it filters to open pull requests — so this lands exactly on the single-PR
path a merge-and-verify cycle uses.
Proposed fix
Small, and in the seam that already exists:
PullRequestState gains merged: bool = False, populated from the key already fetched.
A terminal already-merged outcome, owed by nobody, reported ahead of every rule and
short-circuiting them: once the change is on the base, "0 of 1 approvals" is not an
unsatisfied rule, it is a question about a closed pull request.
Docstring: correct the merge-state-unknown entry so the "every open pull request"
clause no longer implies the merged case is covered.
Graded by the existing AST dataflow cell test_every_outcome_the_report_can_emit_has_an_owner, plus new cells beside the mergeable=None cluster in tests/test_merge_blockers.py pinning that a merged pull
request and an open recomputing one no longer produce the same report.
Deliberately out of scope
A pull request that is closed but not merged reads the same mergeable: null. That is
a third verdict with a different meaning again (abandoned, not landed) and a different
owner, so it is worth separating — but it is a distinct decision from this one and is not
what cost a cycle here.
AI Contribution Disclosure: This issue was generated with AI assistance using Strands Agents. The measurements reported above were taken against the live API.
The defect
check_merge_blockers.py --pr Nreports a pull request that has already merged asmerge-state-unknown, owed by nobody, with the remedy "Re-read the pull request:mergeability is computed on demand and settles on a later read."
No later read ever settles it. A merged pull request is closed, so
mergeablestaysnullpermanently — the advice describes a wait with no terminating condition, and itreads in the reassuring direction: no party owes an action is literally true, which is
exactly what a caller polling for "can I merge yet" sees while the answer is "you already
did".
Measured
GET /repos/strands-labs/robots/pulls/{n}, 2026-09-05 ~12:45 UTC, immediately after#3219 and #3230 squashed:
statemergedmergeablemergeable_stateclosedtruenullunknownmerge-state-unknown, "re-read"closedtruenullunknownmerge-state-unknown, "re-read"openfalsenullunknownmerge-state-unknown, "re-read" — correct hereThe third row is why this is not a one-line special case of a dead state: the genuine
transient that #2586 was written for is live at the same time, on the same fields, so
mergeable is Noneis ambiguous between "GitHub is still computing" and "there is nothingleft to compute". Both merged rows and the open row are byte-identical in every field the
script reads.
The field that disambiguates is already in the payload
resolve_statefetches the whole REST pull request object atscripts/check_merge_blockers.py:802and reads five keys from it —head,base,draft,mergeable,mergeable_state. The same response carriesmergedandstate,which answer the question outright. This is the "fetched and not read" failure AGENTS.md
names as the recurring root cause, in the one script whose stated purpose is "ask for the
verdict rather than re-deriving it".
Why the existing model misses it
The script's own docstring states the assumption verbatim, and it is the assumption that
fails:
True, and it is why
merge-state-unknownexists. But the pull request whose own mergeinvalidated the value is not open, and nothing downstream re-checks that.
evaluate()branches on
state.mergeablewith no notion of a pull request that is already merged, andPullRequestStatehas no field that could carry one.This is not a regression in #2586 and the fix must not revert it: reading the null as clean
is still wrong (#1035,
pusher-only-approvalon aDIRTYbranch). Thenullsimply has athird reading that #2585 did not have to separate, in the same shape as
reviewDecision: nullmeaning two different things in AGENTS.md.Cost, observed
A scheduled cycle polled #3230 to merge it, read
merge-state-unknownonce at ~12:32while it was genuinely open and recomputing, and again at ~12:45 after it had merged. The
two reports are identical. The first was correctly treated as transient; only a direct
state/mergedread established that the second was terminal. The--all-opensweep isunaffected — it filters to open pull requests — so this lands exactly on the single-PR
path a merge-and-verify cycle uses.
Proposed fix
Small, and in the seam that already exists:
PullRequestStategainsmerged: bool = False, populated from the key already fetched.already-mergedoutcome, owed by nobody, reported ahead of every rule andshort-circuiting them: once the change is on the base, "0 of 1 approvals" is not an
unsatisfied rule, it is a question about a closed pull request.
the re-read remedy or ci(actions): pin every workflow uses: to a commit SHA, and grade it #2574's merge-attempt remedy.
merge-state-unknownentry so the "every open pull request"clause no longer implies the merged case is covered.
Graded by the existing AST dataflow cell
test_every_outcome_the_report_can_emit_has_an_owner, plus new cells beside themergeable=Nonecluster intests/test_merge_blockers.pypinning that a merged pullrequest and an open recomputing one no longer produce the same report.
Deliberately out of scope
A pull request that is
closedbut not merged reads the samemergeable: null. That isa third verdict with a different meaning again (abandoned, not landed) and a different
owner, so it is worth separating — but it is a distinct decision from this one and is not
what cost a cycle here.