merge_pr.py outlasts the maximum tool timeout, so the post-merge UI gate is killed and main goes silently unverified nf-9rfg ← Beads

closed priority 2 task unassigned activesession:merge-reset-guard

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

Unclaimed and unlabelled — inert backlog, per CLAUDE.md. Found by nf-dev-design-tier merging #708 on nf-dev-sg.

The mitigation CLAUDE.md prescribes cannot be applied

notes/merging.md and merge_pr.probe_gate's docstring both say the same thing:

> ⏱ THIS MAKES THE MERGE TAKE MINUTES, NOT SECONDS … Pass a generous timeout (10 min) or set SKIP_PROBE_GATE=1.

10 minutes is not a generous timeout here — it is the ceiling. An agent session's Bash tool caps timeout at 600000 ms, so "pass a generous timeout" resolves to "pass the maximum, and hope". On this merge it was not enough:

``` Command timed out after 10m 0s <- the ceiling, not a chosen value $ gh pr view 708 --json mergedAt,mergeCommit {"merged":"2026-08-07T07:25:59Z","commit":"81c4cb33a39e10edb53cb9013e2660546581b6ac"} ```

The merge landed. What the kill took was the post-merge UI gate — which is the one thing that ever verifies main, and the entire reason #389 exists (*"f5 gets swept several times an hour, main got swept never"*, and #387 sat red on main for days).

The gate ALONE exceeds the ceiling on this box

Re-run standalone afterwards, on the same box, with the ordinary concurrent load (another session's reviewer was mid-review, other sessions building):

So probe_gate.py by itself ran over 10 minutes, before merge_pr.py's build, presubmit and merge are counted at all. This is not a marginal overrun that a slightly bigger number fixes — and #620 already measured that concurrent dev sessions make Linux probe timing unreliable, while design 005 measured a sweep at 22m19s under one competing sweep. The ceiling is fixed; the runtime is not.

Why this matters more than a noisy log line

⚠️ The failure is silent in the direction that skips the gate. The operator sees a red Command timed out, has to independently discover the merge succeeded, and then has to know that the killed tail was the UI gate and re-run it by hand. Every step of that is a judgement call a session can skip, and skipping it looks identical to success. SKIP_PROBE_GATE=1 at least *prints* that main is unverified; a timeout kill prints nothing about it.

⚠️ It is not the same as #631 or #711. Those are a successful merge reported as a failure via a gh call. This one reports failure *and* silently drops the gate, and it fires on a healthy path with nothing wrong.

⚠️ The recovery is degraded by #643. After the merge, merge_pr.py had repointed the local branch at main's merge commit, so re-running the gate from that worktree produced a valid sweep that refused to record:

``` REPORT ONLY — this sweep will not be recorded: this is feat/design-tier @ 81c4cb3, not origin/main — the baseline describes main ```

Correct behaviour by the gate. But it means the hand recovery for this bug cannot restore what the killed run would have done, on a worktree box where checking out main is impossible anyway (fatal: 'main' is already used by worktree at /root/nf/devs/bork).

Suggested shapes, in the order I'd try them
  1. Detach the gate from the merge invocation. The gate is already deliberately non-fatal and post-merge; it does not need to be in the same process the operator is waiting on. Spawn it detached, write its result somewhere poll.py reads, and let a WAKE: UNVERIFIED-MAIN line surface a merge whose gate never completed. That turns a silent skip into a tracked one, and it is the same move #701 made for STALE-F5.
  2. Make merge_pr.py say what it is about to do — print merge complete: <sha>, stamped merged:*; now running the UI gate (~N min, safe to kill) *before* starting the gate. Then a timeout kill leaves the operator with the merge status on screen instead of nothing, which is the whole point of the streaming decision already made in probe_gate().
  3. Only then consider whether the sweep can be shortened (#620, design 005's selection work under #682).

⚠️ Not suggested: raising the timeout. There is no number available that is reliably above a contended sweep, and picking one that is usually enough recreates the same silent skip less often, which is worse to diagnose.

Refs #389 (why the gate exists), #631 and #711 (other ways this gate gets skipped), #620 and #682 (why the sweep is slow), #643 (why the hand recovery cannot record).

Dependencies

None.

Comments

No comments.

Add a comment