Repository navigation
fix(pr-merge): recompute an unverified-stale post-merge answer in lane_room and annotate room holds - #9602
Conversation
…e_room and annotate room holds lane_room read the run's post-merge answer as unusable once it was older than two minutes with a lane merge unverified, which the post-merge step alone nearly reaches, so every pending lane merge left the room unreadable and enable-auto-merge disarmed every armed lane pull request. On unverified-stale only, the room now reads the answer again as the gate does (head, config commit, demotion, floor, then the check), writes it back to the --postmerge-result file and uses it. A failed or unreadable check keeps the room unreadable. enable-auto-merge raises one warning annotation and one step-summary line when the room is held. Closes #9577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
claude-security-review reviewed Security review: no findingsI read the full 13-file diff and the PR context. I also read critical-findings: 0 What I checked
Manifests: the diff touches no Notes (not findings)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6fe7ac3cf3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
claude-review reviewed No blocking or advisory findings on this head (d6fbf9b). I only read the code; I ran no tests. What I checked
Author-claimed and unverified by me: the PR's test counts (2794 Python, 308 node), the Nits, not counted as findings:
|
…e; drop the room's write-back The room's write-back went to a file nothing in the job reads again, so it is removed; the gate keeps its own. On unverified-stale the room now keeps the answer when main's head, its config commit, the demotion records and the unverified merges' full and canary runs are unchanged (one run listing: no conclusion since, no merge past the dispatch deadline), and runs the full post-merge check only when one has moved. Refs: #9577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… at its quarantine expiry The enable-auto-merge job now reads the lane records the last run kept, as post-merge-dispatch does, and hands them to the room's post-merge check, so a full check answers those records without an artifact and a run read each. The post-merge report records when the earliest quarantine that excused a failing suite expires (excusesExpireAt), and the room's cheap freshness check runs the full check once that time passes, or when an answer does not carry the field. Refs: #9577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow the clock margin to skew A pull request whose disarm failed, or that report-only left armed, was listed as left unarmed. The room hold now records each outcome: left unarmed (never armed or disarmed) and report-only in the warning, and still armed after a failed disarm in an error-level line. The SINCE_CLOCK_MARGIN drops from 5 minutes to 60 seconds: computedAt is stamped before the reads it covers, so the margin covers clock skew only. Refs: #9577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m's comments The comment-hygiene check rejects issue references in code comments. The comments keep their reasoning; the issue numbers, the review thread and the run timings stay in the pull request. Refs: #9577 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
The pr-verify verifier failed head d6fbf9bcb72b2c760c5d469b7e26dbe535f57535. What it wrote, as data:
Summary: Issue #9577: the PR delivers what the issue asks for. On `unverified-stale`, `lane_room` now recomputes the post-merge answer as the gate does, and a failure found since `computedAt` still refuses. A new test shows a pending merge more than 2 minutes old giving a counted room. The scope-addition comment is also met: one warning annotation plus a step-summary line naming the reason and each pull request left unarmed. It fails for one reason. To make its cheap reuse path work, the PR cuts the shared `SINCE_CLOCK_MARGIN` from 5 minutes to 60 seconds. That constant also drives the gate's demotion re-check, so a merge-safety guard is weakened outside the issue's scope.
- PR body: its claims (tests failing on earlier heads, measured timings, a full suite run) cannot be checked from the diff, and the body itself labels the post-change wall time of the enable-auto-merge full check UNVERIFIED. I found no text trying to steer a reviewer.
- .github/scripts/pr-merge/enable-auto-merge.mjs:794: the per-pull-request `const held` inside the loop shadows the later `const held = (state) => ...` helper in the enclosing block. It works, but renaming one of them (for example `heldList`) would make it clearer.
There was a problem hiding this comment.
The pr-verify verifier failed head d6fbf9bcb72b2c760c5d469b7e26dbe535f57535. What it wrote about this file, as data:
- plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py:3884: `SINCE_CLOCK_MARGIN` drops from 5 minutes to 60 seconds. The constant is shared with the gate's demotion re-check (`_demotion_changed` / `shared_refusal`), so this narrows a guard that pr-merge's gate applies to every candidate. Issue #9577 does not ask for that. The claim that `computedAt` is always stamped before the reads it covers is asserted but not shown for the gate's own answers. Fix: leave `SINCE_CLOCK_MARGIN` at 5 minutes and add a separate constant (for example `RUNS_SINCE_CLOCK_MARGIN = timedelta(seconds=60)`) used only in `babysit_postmerge.pending_runs_unchanged`. Then update the changelog fragment in `.changes/source-control/` that says the 60-second margin applies to the demotion re-check.
- plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py:4635: the issue says to recompute on `unverified-stale` and not to ignore the age. The new `_still_current` path keeps an answer past its 2-minute reuse age without recomputing, as long as main's head, the config commit, the demotion records, the full/canary run listing and the quarantine expiry are unchanged. That goes beyond the spec and extends the gate's reuse rule for the room. It looks reasoned (the full check measured 4m20s against a 4m00s `VERIFY_TIMEOUT_MS`), but a human should confirm that the check's verdict has no other inputs (pins, lane records, timeline events) that can change without a full or canary run completing.
There was a problem hiding this comment.
The pr-verify verifier failed head d6fbf9bcb72b2c760c5d469b7e26dbe535f57535. What it wrote about this file, as data:
- plugins/source-control/skills/babysit-prs/scripts/babysit_postmerge.py:3150: `pending_runs_unchanged` lists runs only with `created>=` the earliest unverified merge's `mergedAt`. A re-run of a run created earlier is missed. That seems harmless for runs that verify the merge, but it rests on verifying runs never being created before `mergedAt`.
Closes #9577
Summary
While any lane merge is unverified,
lane_roomrefused the run's post-merge answer once it was older thanSHARED_UNVERIFIED_MAX_AGE(two minutes). The post-merge step alone takes about 1m41s, and enable-auto-merge reads the answer at about 2:01. So every pending lane merge made the room unreadable, andenable-auto-merge.mjsdisarmed every armed lane pull request, even belowmerge.max-in-flight. The only record of the hold was a console line.Fix
babysit_merge_policy.lane_roomtakes the gate'spost_mergecallable and aruns_unchangedcallable. Whenshared_refusalreturnsunverified-stale, and only then, the room handles the answer in two steps:Cheap freshness check (
_still_current). The room keeps the answer and counts from it when all five of these hold:mainSha;configSha, which with an unchanged demotion means the same floor;_demotion_changedfinds no demotion record filed or changed since;babysit_postmerge.pending_runs_unchangedpasses. It makes one actions-runs listing on the branch, covering runs created since the earliest unverified merge. Nopr-require-checks.ymlorpost-merge-detect-silent-revert.ymlrun may have completed or been re-run sincecomputedAt(lessSINCE_CLOCK_MARGIN, now 60 s and covering clock skew only, sincecomputedAtis stamped before the reads; run 40060427179's step started at 03:58:09.10Z and stamped 03:58:09.27Z), and no unverified merge may be pastDISPATCH_DEADLINE, after which a missing dispatch or pin becomes a failure. A partial listing counts as moved.excusesExpireAt, the earliest expiry (issue creation plusmerge.quarantine-days) of a quarantine that excused a failing suite, whether out of the affected set (demoting) or already failing on the baseline (expired). Once that time passes, or for an answer without the field, the room runs the full check.The time-based excuses
_judgeapplies: the missing-pin and missing-dispatch grace (DISPATCH_DEADLINE, covered by (d)), and quarantine expiry (covered by (e)). Two other timers change no verdict: the lane-record expiry only sends a record back to an API read, andlane_rerun_lookbackonly selects re-runs.A run started since that is still running stays pending, so it does not count as moved. The pr-merge run can start a full run after it writes the report, so requiring the same run set would send nearly every read to the full check.
Full check. When any of (a)-(d) has moved, the room reads the answer again as
assessdoes for a pr-merge candidate (_recompute_shared):computed_atfirst, then the head, its config commit, the demotion, the floor (PR_MERGE) and the post-merge check. It uses the fresh answer for the refusal, the rung and the in-flight count. A failure found sincecomputedAtstill refuses aspost-merge:<reason>, and a demotion record filed since holds the rung off. A check that raises or cannot be read leaves the room unreadable.Every other refusal (stale, binding, demotion changed, new merge, floor differs) behaves as before. The room writes nothing back: nothing in the job reads the file again. The gate keeps its own write-back.
pr-merge.ymlenable-auto-merge job: it now reads the lane records the last run kept, using the samelane-records.mjs readstep post-merge-dispatch uses. That step usesACTIONS_TOKEN: github.token, within the job's existingactions: read, and is limited to reader runs. The job setsBABYSIT_POSTMERGE_LANE_RECORDSon the arm step, so the room's full check answers known records without an artifact read and a run read each.VERIFY_TIMEOUT_MSis unchanged.babysit_postmerge.gate_readeris the gate'spost_merge(floor, identity).babysit_merge.assess_unattendednow uses it in place of its inline copy.babysit_verify_evidence.py lane-roomwires it andpending_runs_unchangedin (no self logins, lane merges only, as the room binds).enable-auto-merge.mjs: when the room is refused or unreadable, the run records what became of each held lanes App pull request. One::warning::annotation and$GITHUB_STEP_SUMMARYline name the reason and the pull requests left unarmed (never armed, or disarmed), and in report-only mode the ones a disarm would have turned off. One::error::annotation and summary line name any pull request still armed because its disarm failed. It posts no comment on the pull requests.The
lane_roomandlane-roomdocstrings, theenable-auto-merge.mjsheader anddocs/conventions/pr-pipeline/README.mddescribe this. There is a changelog fragment for source-control and one for pr-pipeline.Verification
test_babysit_lane_room.py, all failing onorigin/mainand on this PR's first head (6fe7ac3), checked by running the new test file against archives of each:verified, and the fake full check is never called.pending_runs_unchangedwith a faked listing, givespost-merge:failure:dispatch#5.computedAt(excusesExpireAtone minute after it, read three minutes on) runs the full check once.pending_runs_unchanged: runs still running, or finished beforecomputedAt, hold, read from one listing call. A full or canary run that completed since, a partial listing, a merge pastDISPATCH_DEADLINE, an expired or missingexcusesExpireAtcount as moved.test_babysit_postmerge_quarantine.py: the report'sexcusesExpireAtis the issue's creation plus 7 days, both for an out-of-set excuse and for a baseline failure under a live quarantine, and it is null when no quarantine issue excuses anything.workflow.test.mjs: the enable-auto-merge job reads the lane records before the arm step and setsBABYSIT_POSTMERGE_LANE_RECORDSon it.computedAtforces the full check, and one that completed 61 s before is reused. This fails on 80f859f.actionlint .github/workflows/pr-merge.ymlis clean.scripts/run-ruff.sh checkis clean.scripts/check-affected.shpassed in the pre-push hook.commits/<branch>read, (b) 1 commits-by-path read (0 when a local clone holds the head), (c) 1 issues read, (d) 1 runs listing. That is at most 4 GitHub API calls; (e) reads nothing.VERIFY_TIMEOUT_MSof 4m00s.babysit_postmerge.py --repo melodic-software/claude-code-plugins --agent-identity "melodic-automation-lanes[bot]" --lane-merges-only, read-only (no--remediate), on 2026-10-24 from 05:39:18Z to 05:43:39Z.BABYSIT_POSTMERGE_LANE_RECORDSfile, which the enable-auto-merge job does not carry either.pythonRoom's timeout and leave the room unreadable, which fails closed.Related
🤖 Generated with Claude Code