Repository navigation
feat(pr-pipeline): review an oversize main-run diff in chunks - #11160
Conversation
pr-review-main splits a diff over its 300000-byte cap into at most four chunks, cut at file boundaries and a larger file's hunks, and reviews each chunk with its own run-claude-cli step. The lane's one verdict sums the chunks' findings and CRITICAL ones and binds as a single-pass verdict does. A chunk that fails, leaves no result or skips a part writes no verdict and records `verdict=none reason=chunk-failed`; a diff needing more than four chunks still stops as `too-large`. 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 I read the whole diff ( What I checked, and found sound
Spend (not a hold, not counted): a diff over the cap now makes up to 4 Finding (1): the closing issue's size stop is only partly delivered (inline comment on Embedded text: neither the PR body nor the issue contains an instruction aimed at this review. |
|
claude-security-review reviewed Security review: PR #11160, chunked main-run reviewcritical-findings: 0 I read the whole diff (13 files) and the PR context. The workflow, What I checked
Finding (IMPORTANT)
Spend, not a holdEach of the three legs now allows up to 4 sequential review runs of 22 minutes, and the job timeout goes from 30 to 100 minutes. This draws only on prepaid API credits or subscription usage, so it is no hold and not counted as a finding. |
|
The pr-verify verifier failed head What it wrote (25 lines)This pull request leaves the merge queue, and ci-status holds it out while it carries |
…e the max CRITICAL Issue #11000 says to split an oversize diff by file, keep each file whole, and stop a single file over the cap as too-large; the max CRITICAL count across chunks wins. split-diff.py no longer cuts a file at its hunks: a file over max-bytes raises TooLarge (exit 3). buildChunkedVerdict reports critical as the most any one chunk found and keeps findings as the sum. The chunk note, workflow comments, docs and changelog fragment say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ti-file over-cap stop A chunked main-run verdict's `items` listed every chunk's findings while `critical` took the most CRITICAL in any one chunk, so the listed CRITICAL items could outnumber the count with nothing to tell them apart. Each chunked item now carries its 1-based `chunk`; the gate still reads only the counts. The convention and changelog fragment now state that a multi-file diff over MAX_CHUNKS x 300000 bytes (about 1.2 MB) still stops as too-large, and point at #11000 for the dynamic chunk count that would lift it. Refs: #11000 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs: #11000
Summary
pr-review-main used to stop on a diff over its 300000-byte cap. It recorded
verdict=none reason=too-largeand left the PRevidence-unbound, so a lane-source PR with a large diff could never merge. Now it reviews that diff in chunks, up to 4 of them. This PR follows the issue #11000 body, which is the spec, except for the multi-file over-cap stop below, which stays open in #11000.split-diff.py).run-claude-clireview with the same prompt and inputs. The prompt lists that chunk's parts and says which chunk of how many it is.findingsis the sum over the chunks, andcriticalis the most CRITICAL findings any one chunk returned, as the issue says ("the max CRITICAL count across chunks wins"). It is uploaded under the same artifact name in the sameverdict.jsonshape and binds exactly as a single-pass verdict does. The gate holds oncritical > 0, and the max is above 0 exactly when the sum is, so binding is the same either way. Each listed finding of a chunked verdict names itschunk.verdict=none reason=chunk-failedon the head, binds nothing and fails.too-large, however small its hunks, with perf(pr-pipeline): record main-review stops and cut merge-job lane reads #11084's record as before. So does a diff that needs more than 4 chunks (see Remaining work in feat(pr-pipeline): review an oversize main-run diff in chunks instead of holding it for a person #11000).chunk-failedis a lane cause, like the other stops (main-review-stop:<reason>under the MEDevidence-unbound:<lane>).Remaining work in #11000
A multi-file diff over
MAX_CHUNKS× 300000 bytes (about 1.2 MB) still stops astoo-large, while #11000 asks for the size stop to fire only for a single file over the cap. Lifting that needs a dynamic chunk count (for example, a planning job that emits a chunk matrix), which this PR does not build: any fixed slot count leaves some multi-file diff over the cap. The convention (docs/conventions/pr-pipeline/README.md) and the pr-pipeline changelog fragment state this stop and point at #11000.Fix
Design: unrolled review slots. Each review leg (key, subscription, fallback) has 4
Review chunk ksteps. Chunk k runs only when the diff has at least k chunks and chunk k-1 succeeded without handing off. This is the repository's own pattern for severalrun-claude-clicalls in one job: pr-unblock'sadjudicate-feedbackjob runs 3 adjudication slots this way. Actions cannot loop auses:step, and eachrun-claude-clicall makes its own work directory withmktemp -d, so calls in the same job do not collide..github/workflows/pr-review-main.yml. The edits are identical in all three legs, andworkflow.test.mjsholds the legs equal.split-diff.pythen writes the chunks, and exit status 3 (a file over the cap, or more chunks thanMAX_CHUNKS) meansstop too-large. Each chunk is cut into Read-sized parts underdiff/chunk-<k>/. The step outputschunksand logs the chunks notice.MAX_CHUNKS: "4"is the knob.pr-review-main-prompt-<k>.md. A newCHUNK_NOTEplaceholder renders empty when there is one chunk; otherwise it says the diff is split at file boundaries, each file whole.Report chunk k's outcome). Slots after the first have aTime chunk kstep that givesstep-started-at. Each slot uploads its own sealed transcript, and slots 2 to 4 add-<k>to the transcriptjob-id.EXECUTION_FILESandCHUNKS.<lane>-stop=chunk-failedand<lane>-head. The job outputs readsteps.diff.outputs.X || steps.chunk-stop.outputs.X.chunk-failed.timeout-minutesgoes from 30 to 100 per review leg: 4 chunk steps at 22 minutes each, plus the leg's other steps. The legs run on subscription usage, so a longer run spends subscription usage, not billing.on:, everypermissions:block, token requests and the concurrency group.write-verdict.mjs.buildChunkedVerdictchecks each chunk's output as a whole review, withFINDINGS_MAXper chunk. It sumsfindingsand takes the max ofcritical. One chunk gives the single-pass verdict. Each item of a chunked verdict carries its 1-basedchunk.mainreadsDIFF_DIR/chunk-<k>/and runsreadCoveragefor each chunk against its own parts. It requires the number of chunk directories to equalCHUNKS.pr-review-main chunknotice per chunk.split-diff.py(new, stdlib only). It cuts at file boundaries and packs whole files greedily in order. A file over the cap, or more chunks than the most, writes nothing and exits 3.babysit_gh.py.MAIN_REVIEW_STOP_TITLEaddschunk-failed. Without this, that title would read asNone, which counts 1 CRITICAL carried on every later head. Check runs cannot be deleted, so that hold would be permanent.maintenance-reconcile-lost-runs/reconcile.mjs.REVIEW_STEPbecomesReview chunk 1, the step name the reconciler reads to hold a lost run. A cut step list that reaches a later chunk also reachesWrite the prompt and schema, so the predecessor check still holds the run. The verdict write still starts only after a true success, which keeps the reconciler's rule: a run is re-run once only if it was lost before its write.docs/conventions/pr-pipeline/README.mdis updated. There are fragments in.changes/pr-pipeline/and.changes/source-control/.Alternatives considered
lane × chunkmatrix. It needs a new job with its ownpermissions:block, and per-chunk outputs would collide in the matrix-output merge, which would mean rebuilding the handoff routes per chunk. Not built here; it is the remaining work in feat(pr-pipeline): review an oversize main-run diff in chunks instead of holding it for a person #11000.chunkaxis on the existing matrix. About 16 VMs per run would mostly start only to exit, each repeating the trigger gate's API reads. Rejected for the runner and API cost.run-claude-cli's isolation, credential scrub and handoff contract. Rejected.Known limits
workflow.test.mjsrequiresMAX_CHUNKSto equal the number of review steps in every leg.write-diff.pystill prints whole any move that an added line names.itemsagainstcritical.verdict.json'sitemslists every chunk's findings, so it can hold more CRITICAL items thancriticalcounts. Binding reads only the counts (findingsandcritical, matched against the check-run title), neveritems. Each chunked item names itschunk, so a reader can see which chunk's CRITICAL findings the max counts.chunk-failed, the verdict step's error names the chunk instead.Metrics
Each run makes the chunk count and the per-chunk results measurable (§9), as workflow notices:
::notice title=pr-review-main chunks::lane=<lane> pr=<n> chunks=<k> bytes=<b1>,<b2>,..., for one chunk too.::notice title=pr-review-main chunk::lane=<lane> chunk=<i>/<k> bytes=<b> findings=<f> critical=<c>for each chunk.::notice title=pr-review-main stop::reason=<too-large|chunk-failed> ...and posts the check runpr-review-main: <lane>titledverdict=none reason=<reason>.Verification
Tests came first. Each was red before the change and is green after.
workflow.test.mjs, 35 testsstatus 0with two chunks, and the runDiff case below getschunks=2instead ofstop=too-large)write-verdict.test.mjs, 11 testscritical: 3, the sum, where 2 is expected); at the chunk-label change: the aggregation test fails (chunkundefined)test_babysit_gh.py(106) withtest_a_failed_chunk_reads_as_a_stop_that_counts_no_criticalreconcile.test.mjsdeny-rules.test.mjsworkflow.test.mjs,write-verdict.test.mjsanddeny-rules.test.mjstogether: 51 pass, 0 fail on the current head, after merging main.The keep-whole change replaces the hunk-split expectations, so
split-diff.py's single-file test now expects exit 3 where it expected 0. The spec asks for that, and it is not a weakened test.The cases:
Write the diffstep, one file of 303 bytes over a 300-byteMAX_DIFF_BYTES, spread over 12 hunks of under 30 bytes each, recordsstop=too-largeand writes no part. A 612-byte one-hunk file does the same.max-chunkschunks still split.too-large. Four givechunks=4and a notice withbytes=200,200,200,200.findings=5 critical=2, so neither the last chunk nor the sum wins. The items carrychunk1, 1, 1, 2, 2, and no chunk lists more CRITICAL items thancritical. The end-to-end run, in the other order (1 then 2 CRITICAL), writesfindings=4 critical=2.verdict.jsonand leavesGITHUB_OUTPUTempty. So does a chunk count that does not match the chunk directories.Checks:
actionlintexits clean.zizmor --offlinereports no findings (34 ignored, 25 suppressed, all from the slot steps'self-repositoryignores and secret inputs).ruff checkandruff format --checkpass throughscripts/run-ruff.sh.markdownlint-cli2reports no issues on the edited docs and fragment.check-changelog-fragments.sh --checkand--check-required origin/mainboth pass.Security review. An independent
review:security-reviewer(Opus) ran on feature commit fa3f3a4 with verdict CLEAN: no token or secret leak, no account compromise, no billing;on:,permissions:, token requests and concurrency unchanged. The follow-up changes (no hunk split, max CRITICAL, chunk-labeled items, docs) touched onlysplit-diff.py, the prompt text, tests,write-verdict.mjsand docs, and theclaude-security-reviewlane passed on 5c76ba9.Related
🤖 Generated with Claude Code