Repository navigation
fix(pr-merge): bind a cost decision to the pull request it names - #9569
kyle-sexton wants to merge 11 commits into
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. |
There was a problem hiding this comment.
The pr-verify verifier failed head ed21dfbbc006529e494a7c242db0e9fbb41382e3. What it wrote, as data:
Summary: The PR closes #9478 and #9493 but does not finish #9478. #9493 looks delivered: the JS and Python readers, plus ci-status, now count a closing-issue or Refs-issue cost decision only when it names `#N`. A decision covers the head of an unedited github-actions[bot] cost marker posted before it, or a later head whose only changes are base merges. Every failed read keeps the hold, and the JS/Python parity test covers the rule. ci-status's cost-hold lift now reads the one plain `Refs:` issue, which is #9478's first fix, and tests cover it. #9478 also asks to correct the stale comment at `.github/workflows/work-unblock.yml:38-39`, and the diff does not touch that file. Nothing touches the takeover set.
- `.github/workflows/work-unblock.yml:38-39`: #9478 asks to fix this comment, which still says report-outcome sets the Refs needs-human hold. Since #9405, report-outcome no longer sets it, and work-unblock only lifts holds that already exist. The diff leaves the file unchanged. Fix the comment here, or change `Closes #9478` to a `Refs: #9478` line that names this part as remaining work.
- `.github/scripts/pr-merge/merge-holds.mjs:costDecision`: #9493's done-when names other ways to bind a decision: newer than the last push or naming the head SHA, and an issue linked before the comment. This PR binds through the newest cost-hold marker's `sha=` plus a `#N` mention instead. The PR body ties that rule to the owner's Q17 (c) and Q18 (d) on #9436, which are outside the spec files. It still closes #9493's exploit path, so I don't treat it as blocking. A human should confirm that the base-merge carry matches what the owner intends.
- `.github/scripts/pr-merge/merge-holds.mjs:costMarker`: the decided head trusts any github-actions[bot] comment whose first line is a `claude-lane-hold` marker. Any workflow running with GITHUB_TOKEN can post as that account. That is a trust assumption worth a human look: if the lanes App can get a workflow to post a forged marker before the owner decides, it picks the decided head. The overall change is still much tighter than the old rule, which bound no head at all.
- `plugins/source-control/skills/babysit-prs/scripts/babysit_merge_policy.py:cost_marker`: `found` is annotated as a 3-tuple but holds a 4-tuple (made, sha, node, created_at). Fix the annotation so a type checker accepts it.
- `.github/workflows/pr-require-checks.yml`: the Node setup steps move ahead of the cost-hold step, and the deleted-comment read moves earlier. Two bash tests now expect the message 'covering this head' where they expected 'was deleted'. The step still fails closed, but the message for a deletion inside the window is now less specific.
|
claude-review reviewed Incremental review of the three files changed since ed21dfb. I found nothing to flag. I read the code and tests and did not run any of them.
The bool-guard line in No hold applies. The change adds no model spend and touches no trust-gate files. Closing-issue criteria are unchanged by this increment. |
|
claude-security-review reviewed No security issues found in the incremental change since ed21dfb.
No manifest files, instruction surfaces or trust-gate paths changed in this range, so there are no critical-findings: 0 |
…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>
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