Repository navigation
fix(pr-merge): bind a cost decision to the pull request it names - #9569
Conversation
A `**Decision recorded.** Cost:` comment on a pull request covers every head of it. One on its linked issue counts only when it names the pull request as `#<number>`, so a decision recorded on an issue for another change no longer lifts the cost hold of a pull request that links the issue later through a body edit. The author, App, edit and creation-time checks are unchanged. merge-holds.mjs costDecision, babysit cost_decision and ci-status's cost-hold lift apply the same rule, pinned by the parity test. With no closing issue, the JS reader and ci-status now read the one plain `Refs:` issue, as babysit's _linked_issue does; ci-status runs the base commit's merge-holds.mjs refsIssue for it, so its Node setup moves ahead of the cost-hold step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Align costDecision, cost_decision and ci-status's cost-hold lift with ci-workflows' apply-review-hold reader, taking the stricter reading where they differ: - #N rule: a linked-issue decision names the pull request only when `#N` follows none of a letter, digit, `_`, `#`, `&`, `/`, `.` or `-` (the union of both readers' exclusions), so `&#N` and `##N` no longer count. - The issue a plain `Refs:` line names is read to be an issue, not a pull request, in the JS and bash readers too, as Python and the hold step already read it. The read happens only when a body link targets it; a failed read decides nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.65.3 aligns the hold step's cost-decision matching with the merge-side readers (#9569). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Held with do-not-merge until the owner decides Q17 in his own words. Option (b) loosens a cost hold, and the owner did not pick (b) himself (#9436 (comment)). His written constraint on #9436 (#9436 (comment)) is 'Do not suppress cost outright once any decision exists'. AGENTS.md asks first when a change alters what agents may do unattended and the owner has not decided it. The #N alignment and the Refs-target check in this PR are tightenings and can ship separately if the owner picks another option. |
A cost decision now covers the head it decided and a later head only when every change since is a merge of the base (owner decisions Q17 c and Q18 d on #9436). The decided head is the sha of the latest bot-authored `claude-lane-hold ... criteria=cost` marker on the pull request created before the decision; for a decision on the pull request with no marker before it, the head of the last push before it, from the repository activity API. The carry test compares the base...head compare files at both heads on filename, status, previous_filename and patch; 300 or more files, a file with no patch, or any failed read carries nothing. All three readers apply it: babysit_merge_policy.cost_decision, merge-holds.mjs costDecision, and ci-status's cost-hold lift, which runs the base's merge-holds.mjs decidedHead, pushedHead and sameNetChange over the reads it makes itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Align the three cost-decision readers with ci-workflows v0.65.4's apply-review-hold carry test (ci-workflows#877, 31b44f3f): - The decided head is the newest marker github-actions[bot] posted as the first line of a comment, criteria including cost, before the decision. With none, nothing carries, even at the same head; the push-time fallback is gone. - At the decided head it carries with no compare read. Otherwise compare <base branch>...decided and <base branch>...head; a duplicate filename, a file with no patch (so a pure rename), 300 or more files, an unusable base branch name or any failed read carries nothing. - The decision judged is the newest on the pull request, else the newest on its linked issue; only it may carry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A write collaborator can edit a github-actions[bot] hold comment and move
its sha= to the current head, which would turn the carry into a cost
bypass. In all three readers the newest cost hold marker before the
decision now decides a head only when its GraphQL lastEditedAt is null,
read as the decision's edit check reads it. An edited marker, or one
whose edit state is unreadable, decides no head, and no older marker
stands in, so nothing carries.
merge-holds.mjs exports costMarker ({sha, node}) in place of
decidedHead; babysit_merge_policy has cost_marker in place of
decided_head.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hold lifted. The owner decided Q17 (c), #9436 (comment), and Q18 (d), #9436 (comment). This head (1ef5787) implements both: a cost decision carries only when the change since the decided head is base merges only, the decided head comes from an unedited github-actions[bot] cost marker, and any read failure keeps the hold. Next: the ready flow, then a trust-gate review. |
A deleted newer cost hold marker made an older one the decided head, so a head matching that older marker inherited a decision the owner made at a later head. All three readers now refuse to carry when any comment on the pull request was deleted at or after the chosen marker (deletedSince / deleted_since), checked before the same-head shortcut, and fail closed when the deletion list is unreadable or overflows. ci-status's cost-hold step also keeps the hold when the pull request's live head is no longer the event's head, so a late run cannot lift the label off a newer head. costDecision rejects a non-string base name before the compare. 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-review reviewed No findings. I read the full diff (11 files) and the PR context. I did not run any tests. Checked by reading code (no execution):
Closing issues:
Manifests: the diff changes no Not independently verified: the test counts and the "fails against old code" results in the PR description are the author's claims. I did not run them. Hold: none. The change adds no model spend, no trusted-actor or CODEOWNERS changes, and no CRITICAL security finding. |
|
claude-security-review reviewed No security findings in #9569 at head 149f321. critical-findings: 0 I read the full diff, covering the JS reader ( What I checked:
I did not run the code or the tests; this review is by reading the diff. |
…count deleted_since now refuses a boolean filteredCount, as deletedSince does, and the sync test pins that case. A bash case keeps the window-deletion branch covered: a deletion after the trusted removal but before the newest marker passes the carry and still keeps the label. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workflow header still said report-outcome puts needs-human on a Refs PR's issue. It no longer sets that hold, so the sweep lifts only one that is already there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closes #9493
Closes #9478
Summary
This PR applies the owner's cost decisions Q17 (c) and Q18 (d) on #9436. It reads them the same way ci-workflows v0.65.4's apply-review-hold step does (ci-workflows#877,
31b44f3f).**Decision recorded.** Cost:comment counts when the body links it and it sits either on PR N itself or on PR N's linked issue (the closing issue, else the one plainRefs:issue). An issue comment must also name#Nand be newer than the PR. The decision is the newest qualifying comment on the PR, else the newest on the issue.Fix
sha=in the newest comment github-actions[bot] posted on PR N before the decision whose first line is<!-- claude-lane-hold lane=<lane> sha=<40hex> criteria=... -->withcostamong the criteria (costMarker/cost_marker). That marker counts only if it was never edited: its GraphQLlastEditedAtis null, read the same way as the decision's edit check. Otherwise anyone with write access could edit the comment and move itssha=to the current head. If the marker is missing, edited, or its edit state cannot be read, nothing carries, even at the same head, and no older marker is used instead.deletedSince/deleted_since, aCOMMENT_DELETED_EVENTtimeline read), or when that read fails or lists fewer deletions than it counts. The check runs before the same-head shortcut. Without it, deleting the newest marker would make an older marker's head the decided head.<base branch>...<decided head>and<base branch>...<head>(three-dot). The decision carries only when both list the same filenames, each with the same status,previous_filenameand patch (sameNetChange/same_net_change).babysit_merge_policy.cost_decisionimplements the rule in Python.merge-holds.mjs costDecisionimplements it in JS. It readsbaseRefNamein its pull query, andcostDecisionnow takes the head SHA (enable-auto-merge.mjspassesheadRefOid).pr-require-checks.ymldoes its own reads throughread_api. It runs the base commit'smerge-holds.mjscostMarkerandsameNetChangeover them, as it already ranrefsIssue, so the JS logic exists once.#Npattern, the Refs-target checks, and fail-closed handling on every read.merge-holds.mjshas nocostMarker, so on this PR the step cannot carry anything and the hold stays.Agreement with the review lanes' hold step
hold.cjs), the cutoff before the decision, a tie between two markers going to the first listed, the same-head short-circuit, the three-dot compare against the base branch name with the same name check, the compare fields, and every no-carry case.Verification
node --test .github/scripts/pr-merge/*.test.mjs .github/scripts/pr-verify/resolve-pr.test.mjs: 368 pass.python3 -m unittest discoverinbabysit-prs/scripts/tests): 2840 tests OK.test_babysit_merge_cost.pyhas 25 tests.test_merge_holds_sync.pychecks that JS and Python give the same answer on every carry case.scripts/ci-hold-cost-decision.test.sh139 pass;scripts/ci-lanes-pr-hold.test.sh128 pass.release/2.xbase do carry;costare ignored;lastEditedAtkey or cannot be read, carries nothing, both at its own head and after a base merge, and an older unedited marker does not stand in. The JS/Python sync test covers the same cases.nice bash scripts/check-affected.sh --uncommitted: 12 passed, 4 skipped.Related
Refs:PR is merge-eligible.🤖 Generated with Claude Code