merge_pr.py cannot merge ANY PR since 13:28Z: the ruleset now sets required_approving_review_count=0, so reviewDecision is never APPROVED nf-nv5t ← Beads

closed priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/651 by tools/beads/import_github.py's reconcile pass.

Filed unclaimed from session:session-audit (#635/#638). ⚠️ This is not specific to my PR — it blocks every session's merges from 2026-08-06T13:28Z onward.

What happens

``` $ xvfb-run -a ./tools/pr/merge_pr.py 638 refuse: PR #638 reviewDecision=(none) ```

…on a PR that @aedanpope has APPROVED, is MERGEABLE/CLEAN, and has three CONVERGED Tier-0 verdicts.

Why

tools/pr/merge_pr.py:424 gates on GitHub's computed reviewDecision:

```python if decision != "APPROVED": raise MergeRefused(f"PR #{number} reviewDecision={decision or '(none)'}") ```

The repo's ruleset "pull requests required" (id 19101787) was updated at 2026-08-06T13:28:13Z and now carries:

```json {"required_approving_review_count": 0, "dismiss_stale_reviews_on_push": false, "require_last_push_approval": false, "required_review_thread_resolution": true} ```

With required_approving_review_count: 0, GitHub reports reviewDecision as null once no review request is pending. It is REVIEW_REQUIRED while a request is outstanding, and it returns to null — not APPROVED — when the request is satisfied. There is no state in which an approval yields APPROVED, so the gate can never open.

The approval itself is fine and is not dismissed:

``` $ gh api repos/cfsh/ninefold/pulls/638/reviews aedanpope [APPROVED] 2026-08-06T14:01:51Z sha=a15aef2 ```

The timeline is the proof

| time | event | |---|---| | 12:55:59Z | #603 merged by merge_pr.py ✅ | | 13:20:31Z | #615 merged ✅ | | 13:26:15Z | #600 merged ✅ | | 13:26:21Z | #626 merged — the last one that worked ✅ | | 13:28:13Z | ruleset updated ⚠️ | | 14:01:51Z | #638 approved by @aedanpope | | 14:29Z | merge_pr.py 638refuse: reviewDecision=(none) ❌ |

Every successful merge today predates the ruleset change by minutes. Nothing has merged since.

⚠️ I first suspected my own post-approval pushes had staled the approval, and that is wrong — worth recording so nobody else spends time on it. A poll digest taken while HEAD was exactly the approved sha a15aef2 already showed an empty reviewDecision (OPEN MERGEABLE/CLEAN, versus OPEN REVIEW_REQUIRED … on the tick before the approval). The gate would have refused at the approved sha too. dismiss_stale_reviews_on_push is false and the review is still listed — pushing after an approval is not the trigger.

Two fixes, and they are not alternatives

A. Config, immediate — @aedanpope only. Set required_approving_review_count: 1 on the ruleset. That restores reviewDecision: APPROVED and unblocks every session now. It also matches what the repo already assumes: one human reviewer whose approval is the go-signal, which CLAUDE.md states as the rule.

B. Code, durable. merge_pr.py should not depend on a derived field that a repo-settings change can silently empty. CLAUDE.md already teaches exactly this lesson one level up — *"reviewDecision alone misses most feedback"* — and the same reasoning applies to the merge gate: read repos/{repo}/pulls/{n}/reviews and require a non-dismissed APPROVED from aedanpope, which is the fact the rule is actually about. ⚠️ Whoever does this must keep it fail-closed: an unreadable reviews list must refuse, not merge, and a CHANGES_REQUESTED newer than the approval must still block.

⚠️ Do B even if A happens. A restores the current behaviour and leaves the merge path resting on a field the repo owner can empty from a settings page with no error anywhere — the failure is a *refusal*, which is the safe direction, but it is silent, repo-wide, and took a timeline reconstruction to attribute.

Not a workaround

merge_pr.py has no override flag, and hand-rolled gh pr merge is forbidden by CLAUDE.md precisely because the gate is what runs presubmit --all, the feedback-ledger audit and probe_gate.py. I have not bypassed it and am holding #638 rather than merging around the check.

Dependencies

None.

Comments

No comments.

Add a comment