Skip to content

fix(pr-pipeline): count the whole-review cap from timeline events a lane cannot edit - #8602

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/8581-timeline-review-cap
Oct 20, 2026
Merged

kyle-sexton merged 2 commits into
mainfrom
fix/8581-timeline-review-cap

Conversation

@kyle-sexton

Copy link
Copy Markdown
Member

Closes #8581

Summary

pr-merge caps automatic whole reviews at 2 per pull request (#8580) by counting record lines in its reasons comment, which the lanes App identity writes. Count the cap from the pull request timeline too, so editing or deleting that comment cannot reset it.

Fix

  • babysit_whole_review.py:210-262: the timeline query reads the newest 100 ConvertToDraftEvent entries (was 1); _conversions parses them; new lane_toggles(node_id, identity) counts the lane identity's conversions and returns None when the timeline cannot be read; _converter (recovery) uses the same list.
  • babysit_merge.py:3750-3761: the comment's record count still runs first (no API cost); only when it leaves room does the gate read the timeline, refusing with toggle-cap:<n> at 2 or more lane conversions and toggle-cap:unread when the read fails (fails closed). The read happens before limits(), so a capped PR does not spend the sweep's one toggle. Record lines still drive the once-per-head check and the recovery window.
  • Any conversion to draft by the lane identity counts toward the cap, even one outside a toggle; that errs toward stopping sooner.
  • README and changelog fragments updated.

Verification

  • 5 new tests (failed first, 17 errors before the code), including the stay-loud case: record lines removed, 2 lane conversions in the timeline, new head with a clean incremental review → no third toggle (toggle-cap:2).
  • babysit-prs suites 2198/2198 under python3 -m unittest discover; ruff check and format pass.
  • API: one GraphQL query, only for a PR that has passed every other whole-review check.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xk7Lz7d2twxvA3LfRxnLkT

kyle-sexton and others added 2 commits October 20, 2026 06:47
…ane cannot edit

Closes #8581

The cap of two whole reviews per lane pull request was counted only from
record lines in the reasons comment, which the lane token can edit. The
gate now also counts the lane identity's ConvertToDraftEvent entries on
the timeline, read only once the comment's own count leaves room, and
refuses with toggle-cap:unread when the timeline cannot be read. The
record lines still carry the once-per-head check and the recovery window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xk7Lz7d2twxvA3LfRxnLkT
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T11:07:39.857720Z 54d2cdf PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 54d2cdf4ac14799e7077a1e06c3f55afce2aa7f2: 0 finding(s), 0 as inline comments.

Security review of PR #8602 at head 54d2cdf: no security findings.

The PR changes the whole-review cap in babysit_merge.py and babysit_whole_review.py. The cap used to be counted only from record lines in the reasons comment, which the lane token can edit. It is now also counted from the lane identity's conversions to draft on the pull request timeline, which the lane token cannot delete. This tightens a control, so I checked how it can fail.

  • Fails closed when unread: lane_toggles returns None on a GraphQL error, a malformed response, or an empty node_id. The caller then skips with toggle-cap:unread, so no whole review is requested.
  • Empty identity: an empty identity matches only conversions with no actor. That can only raise the count and cause a skip, never a toggle.
  • Identity matching: _login casefolds and strips [bot] on both sides, so an actor cannot match under a different spelling of the identity.
  • Truncation: the query takes the newest 100 conversions, and the cap is two. A truncated list cannot reduce the count below the cap.
  • No new input paths: node_id and identity come from the existing pr and inputs. The _converter refactor keeps its behaviour, including None on an empty list. I found no new injection surface.
  • Other files: the change fragments, the convention doc and the tests have no security relevance.
  • Instruction surfaces: the diff deletes or softens no standing instruction. I did not need the instruction exception register for this review.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed 54d2cdf4ac14799e7077a1e06c3f55afce2aa7f2: 0 finding(s), 0 as inline comments.

No findings on PR #8602 at head 54d2cdf. I could not run the test suite: the sandbox needed approval for python3 -m unittest. The PR's "2198/2198 pass" is the author's claim and I did not confirm it.

What I read: the full diff, the PR context and closing issue #8581, _whole_review_record in babysit_merge.py, and recover in babysit_whole_review.py (the only other caller of the shared timeline query).

Acceptance criteria for #8581

  • Edited or deleted comment does not reset the cap: met. lane_toggles counts the lane identity's ConvertToDraftEvent entries on the timeline, and that count gates the toggle after the comment's record-line check.
  • Stay-loud test: met. test_deleted_records_do_not_reset_the_timeline_cap strips the record lines, mocks 2 lane conversions and expects toggle-cap:2 with no toggle. It mocks lane_toggles, but the new LaneToggles tests cover the counting itself.

Checks that held up

  • Actor comparison goes through _login, which casefolds and strips [bot], so a GraphQL lanes actor matches the lanes[bot] identity.
  • A missing node_id, a failed query or a malformed payload gives None, which refuses with toggle-cap:unread, so the gate fails closed.
  • The timeline read happens before limits(), so a capped PR does not use the sweep's single toggle.
  • _converter is behaviour-preserving for recovery: it still takes nodes[-1] of an oldest-first list, and an empty list still gives None.
  • Both changelog fragment directories (pr-pipeline, source-control) match entries in scripts/fragment-plugins.txt.

Two behaviours are intentional and the PR states them. Any lane conversion to draft counts toward the cap, which errs toward stopping sooner. The timeline query is capped at 100 entries.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit d4d6cb7 Oct 20, 2026
29 checks passed
@kyle-sexton
kyle-sexton deleted the fix/8581-timeline-review-cap branch October 20, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(pr-pipeline): count the whole-review cap from timeline events a lane cannot edit

1 participant