Twinned from https://github.com/cfsh/ninefold/issues/711 by tools/beads/import_github.py's reconcile pass.
Unclaimed / unlabelled — inert backlog. Hit live merging #704 at 2026-08-07T05:52:16Z on nf-dev-sg.
``` $ ./tools/pr/merge_pr.py 704 authority: human approval (aedanpope) ledger clean: … presubmit PASSED against head 21f1636 refuse: a GitHub call failed, so the merge cannot be gated safely. command failed (exit 1): gh pr merge 704 -R cfsh/ninefold --squash --delete-branch --match-head-commit 21f1636… failed to delete remote branch feat/t1-check-run: HTTP 404: Reference does not exist ```
The merge succeeded. #704 is MERGED at 75bfe77. Only the branch deletion failed, because the repo has delete_branch_on_merge: true (gh api repos/cfsh/ninefold --jq .delete_branch_on_merge) — GitHub had already deleted the ref, so --delete-branch 404s on a branch that is gone *because the merge worked*.
1. A succeeded merge is reported as refuse:. A session reading that will reasonably conclude nothing merged. The next action — re-run merge_pr.py, or investigate a merge that already happened — is wasted at best. This is the #470 F2 shape (*"hand back a failed-looking command for a merge that succeeded"*) arriving through a different door.
2. ⚠️ The merged:* stamp is skipped, so the merge reads as out-of-flow. merge() raises before stamp_merged():
```python if not result.ok: raise MergeRefused(...) # <- exception jumps past everything below stamp_merged(gh, number, label) ```
#704 merged carrying no label at all; I applied merged:approved by hand afterwards. That defeats the feature @aedanpope asked for — *"can we add a label when a PR is merged with merge_pr … so we can see if any merges happen outside of that flow"* — in the direction that makes the detector useless rather than merely noisy: a gated merge that looks ungated. --audit would have reported #704 as bypassing the gate that merged it.
The post-merge steps after stamp_merged are skipped too — git checkout main, fetch, reset --hard, and the probe gate.
#633 merged the previous day under the same repo setting and did get stamped. So --delete-branch sometimes wins and sometimes loses against GitHub's own auto-delete. That makes it intermittent, which is worse than consistent: the stamp is trustworthy *most* of the time, which is exactly when nobody re-checks it.
Re-read the PR state before believing the exit code. A non-ok result from gh pr merge means "something in that command failed", not "the merge failed" — and the two need distinguishing at the one place that still can:
```python result = gh.merge(number, squash=True, delete_branch=True, match_head=head_sha) if not result.ok: state = (gh.pr_view(number, ["state"]) or {}).get("state", "") if state != "MERGED": raise MergeRefused(...) # genuinely refused — unchanged print("WARNING: merge LANDED but gh exited non-zero — continuing. " f"{result.err.strip()}") # loud, not fatal ```
Then stamp_merged() and the post-merge sync run as they should.
⚠️ Keep it loud. The point is not to swallow the error — it is that "the merge did not happen" and "the merge happened and cleanup did not" are different facts and only one of them should stop the flow. Same distinction #659 draws for the probe gate ("the probes ran and failed" vs "the probes could not start").
Simpler alternative worth considering instead: drop --delete-branch entirely, since delete_branch_on_merge: true already does it repo-wide. That removes the race rather than tolerating it — but it makes the tool depend on a repo setting nobody reads, so the guard above is probably still wanted.
⚠️ match_head must stay. It is what pins the merge to the sha the ledger and presubmit ran against (#633 F12); this issue is about the *response* handling, not the flags.
test_merge_pr.py has test_a_failed_merge_NAMES_the_children_already_retargeted, which asserts the raise on a non-ok merge — that test encodes today's behaviour and will need to distinguish the two cases rather than be deleted.
None.
No comments.