fix(scripts): a standing request for changes is a blocker owed by the account that made it - #3268
Conversation
… account that made it `check_merge_blockers.py` modelled only the approval side of the review decision, so a pull request sitting at `CHANGES_REQUESTED` was reported as `missing-approval` owed by "a reviewer other than the pusher". That party cannot clear it. With required reviews in force a standing request for changes holds the merge until its own author approves or dismisses it, so another account's approval satisfies `required_approving_review_count` and the pull request stays `BLOCKED` -- the strands-labs#1905 presentation reached from the review-decision side rather than the last-push side. Measured on strands-labs#3205: `CHANGES_REQUESTED` stood 15h44m with its one review thread resolved, `call-test-lint` SUCCESS, and `check_thread_is_answered.py` reading `nothing-owed`. 12h51m of that was after the fix had landed. Thread resolution and review decision are separate objects, and the requester's own follow-up reply is a `COMMENTED` review, which expresses no position and supersedes nothing -- so every sweep the repository has read clean while the decision went on blocking. The new `changes-requested` outcome is reported ahead of the approval rules it is not answerable by, names the holding accounts in its detail rather than only the role, and is scoped to a ruleset that actually requires reviews. It is deliberately not a finding: it is owed by a reviewer, and a finding that fires whenever a review is in progress fires on the ordinary state. Standing is resolved by the sibling check rather than re-derived, so the rule that a `COMMENTED` review retracts nothing has one owner across both questions. Pinned by 13 cells that fail on pre-fix code, 7 of them over the sweep's verdict and 6 over the shared resolver.
yinsong1986
left a comment
There was a problem hiding this comment.
Summary
Adds a changes-requested outcome to scripts/check_merge_blockers.py, owed by the reviewer who requested changes, reported ahead of the approval rules it is not answerable by -- closing the gap where a CHANGES_REQUESTED review decision was reported as missing-approval owed by a party whose approval could not merge anything (the #1905 presentation reached from the review-decision side, measured on #3205). Standing gets one owner: current_change_requesters lives beside current_approvers in check_last_push_approval.py over a shared _latest_positions resolver, so the COMMENTED-retracts-nothing rule cannot hold on one side and lapse on the other, and the sweep binds the sibling's resolver rather than re-deriving it. I verified the counterfactual claim independently: with scripts/ reverted to main, the 13 new pins fail (13 failed / 139 passed) and with the change all 152 pass. Nothing here is a security surface, a wire format, or a public API one-way door -- these are repository maintenance scripts with an outcome vocabulary that this PR also keeps in sync with AGENTS.md.
What's good
- Every new pin fails on pre-fix code, per AGENTS.md > Review Learnings (#85) > Testing > "Pin regression tests for reviewed fixes" -- reproduced locally, not taken on trust.
CHANGES_REQUESTEDis deliberately absent from_FINDINGSand_GATING, consistent with the module's own reasoning for keepingmissing-approvalout; the third-party-approval test is the cell that shows the two questions are genuinely distinct.- Scoped to
required_approving_review_count, so the outcome cannot be invented on a branch whose ruleset does not hold the merge for a review -- same discipline as the existing pusher discount. - The relevant whole-tree graders (test-name behaviour, ASCII tool strings, host paths, docstring xrefs, review-archaeology) all pass on the head tree;
ruff checkandruff format --checkclean on the four changed Python files; changelog fragment named with the PR number per step 3. DISMISSEDwas already inPOSITION_STATESonmain, so the_latest_positionsextraction is a pure refactor of the approval side -- the 139 pre-existing tests passing unmodified confirms it.
What
scripts/check_merge_blockers.pymodelled only the approval side of the review decision. A pull request sitting atreviewDecision: CHANGES_REQUESTEDwas therefore reported asmissing-approval, owed bya reviewer other than the pusher-- and that party's approval cannot clear it.With required reviews in force, a standing request for changes holds the merge until its own author approves or dismisses it. Another account's approval satisfies
required_approving_review_countand the pull request staysBLOCKED. So this is the #1905 presentation reached from the review-decision side rather than the last-push side: a state that reads as ordinary reviewer latency, needs one specific named account, and gets a round spent on a reviewer who cannot move it.Adds a
changes-requestedoutcome, owed bythe reviewer who requested changes, reported ahead of the approval rules it is not answerable by.Why nothing caught it
Every sweep this repository has reads clean on the shape. Measured on #3205:
reviewDecisionCHANGES_REQUESTEDmergeStateStatusBLOCKEDcheck_thread_is_answered.py --pr 3205nothing-owedcheck_merge_blockers.pymissing-approval, owed by a reviewer who could not clear itcheck_pr_head_is_current.pycurrentcall-test-lint / Test and LintThread resolution and the review decision are separate objects: resolving the thread does not retract the review, so the sweep that answers "does this owe me anything" was correct to say no while the decision went on blocking. And the requester's own follow-up reply is a
COMMENTEDreview, which expresses no position and supersedes nothing -- so the one account that could clear it had already said everything it was going to say.It stood 15h44m, of which 12h51m was after the fix had landed and the thread was resolved.
Shape
current_change_requesterslives besidecurrent_approversincheck_last_push_approval.pyand both read one shared_latest_positions, so the rule that aCOMMENTEDreview retracts nothing cannot hold on one side and lapse on the other. The sweep binds the sibling's resolver rather than deriving standing itself, exactly as it already does for approvals.changes-requestedis deliberately absent from_FINDINGS, on the same reasoning that keepsmissing-approvalout: it is owed by a reviewer, and a finding that fires whenever a review is in progress fires on the ordinary state and stops meaning anything._next_action's gating language would misdescribe that.the reviewer who requested changesis not actionable on its own; the detail line names each holding account, since on a pull request with several reviewers the report is the only place that is written down.Tests
13 new cells, all 13 fail on pre-fix code (7 over the sweep's verdict, 6 over the shared resolver):
The #3205 cell is the counterfactual itself -- it asserts the primary blocker is
changes-requestedowed byREQUESTING_REVIEWER, and explicitly that it is notOTHER_REVIEWER, which was the pre-fix answer. The third-party-approval cell is the one that shows the two questions are genuinely different rather than two spellings of one: with an eligible approval present there is no approval rule left to report, so a check modelling only approvals reports nothing at all and the reader concludes the branch is ready.Gate
pytest tests/test_merge_blockers.py tests/test_last_push_approval.pyruff checkruff format --checkmypy scripts/check_merge_blockers.py scripts/check_last_push_approval.pySuccess: no issues found in 2 source filesbehind_byvsmainThe 110 failures and 178 collection errors in the grader roster are environmental in this sandbox (no
torch,mujoco,zenoh) and reproduce byte-identically on the unmerged base, which is why the claim is the delta and not the count.Docs
AGENTS.mdgainsCHANGES_REQUESTEDas the fourth reading ofreviewDecision, beside the existingnull/APPROVED/REVIEW_REQUIREDones, and the sweep's enumeration of parties gains this outcome so the file cannot drift from the tool.Advances the #1905 class. Claims no issue.
Round log