Repository navigation
fix(pr-pipeline): end designed stops cleanly and keep held pull requests out of the merge queue - #10737
Conversation
The lane breaker turns CLAUDE_LANES_REPO_DISABLED on after a queued run has read its variables, so the run's own kill-switch check passes and only the token broker's live read stops it, failing the run red. 12 runs since 2026-10-24 (pr-merge 4, intake-triage 5, work-resolve-issue 3) ended that way. request-lane-token takes a kill-switch-ok input (token mode only): a 403 kill-switch ends green with a notice, no token and skip-reason kill-switch. pr-merge's merge and enable-auto-merge jobs and intake-triage's apply-verdict and apply-refinement jobs opt in; every step there that uses the token already runs only when it is set, and refine-select now skips on apply-verdict's new stopped output. work-resolve-issue stays red: work-unblock re-fires its issue from the failed mint step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
callers.test.mjs walks every workflow job whose lane-token step sets kill-switch-ok: each later step has an if:, a step that reads the token requires it set, and a job that needs the stopped job's success and reads none of its outputs skips on its skip output. It fails when refine-select's stopped clause is removed. Four pr-merge merge steps that relied on an earlier token-gated step now name the token gate themselves, which changes no run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci-status's merge-queue run fails a head whose newest pr-unblock/* status by github-actions[bot] is failure or error, but only after every lane leg ran, and the queue then ejected the pull request. enable-auto-merge already refuses the other ci-status holds (labels, body and member holds, hold and cost paths, the verifier verdict, the kill switch) before it arms; this was the one it lacked. merge-holds.mjs gains unblockStops and readStatuses, the rule as ci-status states it. enable-auto-merge reads the head's statuses with the discussion and leaves a stopped head, or one whose statuses do not read, unarmed (disarming an armed one), with a run-summary line for each. The merge-job pins follow the explicit token gates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he head The lane's needs-human refusal for a hold path says a person must remove the label, and ci-status says an approving review of the head by a trusted actor lifts the hold, yet the owner still removed the label by hand after approving (#10665, #10738, #10745). On a reader run, a labeled lanes App pull request whose newest lane refusal at its head names only hold paths is read on when a trusted person approved in its discussion. When merge-holds' lifters (an approval bound to the head, or one carried over base merges) lift the hold path and no cost hold stands, the lane deletes the label with its own token, which re-runs ci-status at the head, and judges the pull request as unlabeled. Any other reason, head, reviewer or a dismissed approval leaves the label on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ated code The owner's decision Q10 of 2026-10-28 (#10759): a person's approval of a hold-path head stays valid at a later head when the diff between them touches no hold-path file. approvalCarry and its Python twin approval_carry take that as a second carry path beside the base-merge one, read from the compare of the approved head to the head. That diff must also touch no file a hold path loads: the enforcement floor, and CARRY_REFUSES for the workflows, actions and scripts hold-path workflows run, the trusted-actor list and the pipeline config. test_merge_holds_sync.py derives that closure and fails on any member outside the floor and CARRY_REFUSES. A compare not read whole, a head not ahead, a dismissal the push did not make, an untrusted or dismissed approval still carry nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l' into fix/kill-switch-deny-ends-neutral
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-review reviewed Incremental review of 58c8610 (since 5e9164c): no findings. What I checked, by reading the diff and the code it calls. I ran no tests and no commands.
Spend: none. The change adds no billed resources and no extra lane runs. Closing-issue criteria (#10759, #10775, #10777, #10785): this increment only tightens the #10785 safety constraints (a label another hold stands behind is never removed), and tests cover each new guard. Nothing in the PR context file asked the lane to skip, approve, or post elsewhere. |
|
claude-security-review reviewed Incremental security review of the changes since 5e9164c. I found no security issues. I read the diff ( Each trust-control change, checked against its code:
The PR context contains no embedded instructions aimed at this lane. This change spends no prepaid credits or subscription usage and can cause no real charge. |
|
The pr-verify verifier failed head This pull request leaves the merge queue, and ci-status holds it out while it carries |
…'s own refusal The lane's approval lift deleted needs-human after looking only at its own newest refusal. The same label also carries holds that live only in it: a pr-unblock escalation adds no labeled event when the label is already on, and a review-lane hold, a signed-commits hold or a person's label sit under it too. liftBlocker now reads the issue timeline, review comments and reviews with gate-source's readers and keeps the label unless its newest labeled event is the lanes App's, pairs with enable-auto-merge's refusal, and nothing else stands behind it since. The head is read again just before the delete. The closure test now matches bare and variable-prefixed Python invocations, which found babysit_merge_prefilter.py, babysit_merge_skip.py and two scripts ci-status runs outside CARRY_REFUSES; both twins now refuse the carry on them. A new test pins HOLD_PATH_FLOOR to HIGH_FLOOR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and read statuses on the job token liftBlocker now keeps needs-human when a review lane's hold names the head (or does not parse), whenever it was written, counts only a person's removal of the label as lifting earlier holds, and requires the refusal comment within PAIRED_MS of the App's label, so a label put back with no new comment stays for a person. The lanes App holds no commit-statuses permission, so enable-auto-merge reads the head's pr-unblock statuses on the job token and its job gains statuses: read. The closure test now takes every tracked script a hold-path workflow names in any directory; the plugin script directories ci-status runs from join CARRY_REFUSES in both copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post-merge mutation testingKilled 0, survived 0, timeout 0, no coverage 0, invalid 0, not run 7.
The run's summary has the full report. |
Closes #10759
Closes #10775
Closes #10777
Closes #10785
Summary
This PR closes four issues:
CLAUDE_LANES_REPO_DISABLEDon when GITHUB_TOKEN headroom runs low. A run queued before the trip has already read its variables, so its owncheck-kill-switchstep passes, and only the token broker's live read stops it:lane-token-denied: the broker answered 403 with reason kill-switch. Since 2026-10-24 that failed 12 runs: pr-merge 4, intake-triage 5 and work-resolve-issue 3. In run 40747727299, the job's switch readfalseat 18:51:28Z, the breaker had tripped at 18:47Z, and the broker refused at 18:51:32Z.enable-auto-mergealready refuses labels, body and member holds, hold and cost paths, the verifier verdict and the kill switch before it arms. The one ci-status hold it lacked was a failingpr-unblock/*status on the head.needs-human. ci-status already names the release: "an approving review of this head by a person on the trusted-actor list lifts it". Even so, the owner removed the label by hand after approving, on fix(source-control): read the babysit default branch head by its full ref #10665, fix(pr-pipeline): refuse default on model profiles and resolve unrouted models to opus #10738 and docs(pr-pipeline): state that an empty wip.pause-from removes intake's queued cap #10745.Fix
request-lane-tokengets akill-switch-okinput, the same opt-in shape assuperseded-ok. Intokenmode, a 403 with reasonkill-switchends the step green with alane-token-stoppednotice, notoken, andskip-reason=kill-switch. Any other status or reason stays red, andcheckmode refuses the input.merge,enable-auto-merge) and intake-triage (apply-verdict,apply-refinement) opt in. Every later step in those jobs has anif:, and every step that reads the token requirestoken != ''. Four pr-merge steps that relied on an earlier gated step now name the token gate themselves.refine-selectnow also requires apply-verdict's newstoppedoutput to be empty.callers.test.mjspins these rules for everykill-switch-okcaller.merge-holds.mjsgainsunblockStopsandreadStatuses, ci-status's pr-unblock rule as it states it.enable-auto-mergereads the head's statuses and does not queue a head a pr-unblock lane stopped, or one whose statuses do not read. It disarms one already armed and adds a run-summary line for each. ci-status's own steps are unchanged. It reads the statuses on the job token, and theenable-auto-mergejob gainsstatuses: read: GitHub listsGET /repos/{owner}/{repo}/commits/{ref}/statusesunder the "Commit statuses" read permission for installation tokens (permissions required for GitHub Apps), and the lanes App's installation holds nostatusespermission (gh api orgs/melodic-software/installations: actions, actions_variables, checks read; contents, issues, pull_requests, workflows write; metadata, organization_actions_variables read). The read moves from the lanes App's budget to the job token's and adds no call: one statuses read (at most 10 pages) per pull request a reader run reads.enable-auto-mergedeletesneeds-humanitself only when the label stands for its own hold-path refusal and nothing else:approvalLift: the lane's newest refusal at the head names only hold paths, merge-holds' lifters show a trusted person's approval (bound to the head, or carried), no cost hold stands, and the file listing was read whole.liftBlocker(security review fix), using gate-source's readers as clear-escalations does:labeledevent for the label is the lanes App's;PAIRED_MS(60 s) before the label, with no other marked comment in the same second. A label the lane puts back under a standing comment with no new one does not pair, so it stays for a person;liftedByPersonreads it, so a bot's unlabel lifts nothing;pr-require-checks.ymlruns onunlabeled).pathHoldKindwith the hold paths plusHOLD_PATH_FLOOR). A push that touches any gated file needs a new approval, and a test-only or comment-only edit gets no exemption.approvalCarry(and its Python twinapproval_carry), next to fix(pr-merge): count a stale-dismissed approval and carry it over an exact base merge #10671's base-merge carry.test_merge_holds_sync.pyderives them: the floor entries' JS imports, the Python those run, and the actions and scripts that hold-path workflowsuses:or run. It fails on any member that is neither on the floor nor inCARRY_REFUSES..github/standards/**anddocs/conventions/pr-pipeline/**, stop the carry throughCARRY_REFUSES. Owner decision Q26 option B keeps enforcement files cleared by a security review rather than by a person, and holding about 114 CI scripts for a person would hold most PRs.python3 .base/plugins/...py) and variable-prefixed ("$scripts/...py") invocations. With those matched it foundbabysit_merge_prefilter.py(pr-merge.yml:1050),babysit_merge_skip.py(pr-merge.yml:1325),plugins/performance/scripts/ratchet.pyandplugins/harness-ops/skills/inventory/scripts/parser_reader.py(pr-require-checks.yml:2025and:2756).CARRY_REFUSESin both twins now addsplugins/source-control/skills/babysit-prs/scripts/**,plugins/performance/scripts/**andplugins/harness-ops/skills/inventory/scripts/**. Second security review fix: the matcher took scripts only under.github/scripts,.github/actions,scriptsandlib. It now takes every tracked script a hold-path workflow names in any directory, from the head checkout or.base/, and found the skill-quality, testing (check-visual-parity), knowledge (course-digest, video-digest), ai-briefing (generate), harness-config (audit-permission-state) and source-control scripts thatpr-require-checks.ymlruns. Their directories joinCARRY_REFUSESin both copies, and theCARRY_REFUSEScomment states exactly what the test walks (a shell script's ownsourceis not walked).request-lane-token,merge-holds.mjsandenable-auto-merge.mjs.Verification
node --test .github/scripts/pr-merge/*.test.mjs .github/scripts/pr-unblock/*.test.mjs .github/actions/resolve-config/callers.test.mjs .github/actions/request-lane-token/*.test.mjs: 1835 pass.liftBlockercases each change the liftable timeline in one place and assert the reason the label stays: a label a person added, a label a pr-unblock escalation set before the refusal, an escalation in the same second as the refusal, a pr-unblock escalation since the label (the reviewer's scenario), a review-lane hold since the label and one before the approval, a check-signed-commits hold, a person's comment, label event, review-thread reply and non-approval review, a moved head, and an unreadable timeline. A paired case still lifts with the refusal edited after the label and a bot's comment since. Second round adds: the reviewer's probe (a hold on the head, github-actions[bot]'s unlabel, the App's relabel, then the approval), a hold marker that does not parse, an earlier head's hold under a bot's unlabel and a newly paired label, and a refusal posted more thanPAIRED_MSbefore the label. A person's removal of the label still lifts an earlier head's hold.PAIRED_MS, head hold, unparsable hold, person-only removal, review hold, signed-commits hold, foreign marker, person speech, label events, thread replies, head re-read) and dropping the approval exclusion: all 15 turn the suite red. Reading statuses on the lanes token again fails the new job-token test.python3 -m unittest discover -s testsin babysit-prs/scripts): 3389 run, OK (5 skipped).CARRY_REFUSESmakes the closure test fail onbabysit_merge_prefilter.pyandbabysit_merge_skip.py. Before this change the old matcher passed without them. Removingplugins/skill-quality/scripts/**makes it fail on its 6 scripts; the old matcher passed without it.test_the_person_floor_is_the_same_listloadsHOLD_PATH_FLOORfrommerge-holds.mjsunder node and asserts it equalsbabysit_risk.HIGH_FLOORentry for entry, 32 globs (the existing test's 75 floor entries less its 43 review entries). Both are derived from the generated class list:merge-holds.mjs:349keeps the non-enforcementHOLD_PATH_CLASSES,babysit_risk.py:310keeps thetrustandgateFLOOR_CLASSES, andscripts/sync-hold-floor.py --check(test_every_copy_is_what_the_generator_writes) keeps the two class lists identical.config.denied_pathsis ci-status's configured hold-path list. Atbabysit_verify_evidence.py:2474the carry readsconfig.denied_paths.configcomes frommerge_policy.config_at(args.repo, args.base_sha)(babysit_verify_evidence.py:3265), which readsmerge.diff-check.denied-pathsfromdocs/conventions/pr-pipeline.yamlat the base SHA (babysit_merge_policy.py:980,CONFIG_PATHat:86). ci-status reads the same key from the same file at$BASE_SHA(pr-require-checks.yml:5191-5194). One difference:with_default_branch(babysit_verify_evidence.py:3268) unions in the default branch's list (babysit_merge_policy.py:1050). For a pull request into main the two lists are the same, and otherwise the gate's list is a superset, so the gate holds at least what ci-status holds.bash scripts/ci-hold-pr-unblock-status.test.sh: 27 pass.actionlinton the two workflows is clean, and the changelog fragments are valid.select-tests, and switching ci-status to the shared rule (ci: fail held merge-queue pull requests before the lane legs run #10747). ci-status loadsmerge-holds.mjsfrom the base commit, so that switch has to follow this merge.-Ior-Pwould drop its sibling imports, so it is not changed here.Owner merge decision
Q13, 2026-10-29, on merging this pull request at
58c861047402c62d16c02c2e31a06ac83a7d4975with the label lift included (option A), verbatim: "go w/ recommendations". Three independent security reviews: the first found one HIGH, now fixed; the second and the round-3 delta review found no HIGH or CRITICAL. pr-verify passed this head.Related
🤖 Generated with Claude Code