mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #320] [MERGED] feat(cursor-review): make panel integrity a check-run conclusion #324
Labels
No labels
cursor-review-catalog-drift
groom
groom
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/github-workflows#324
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/github-workflows/pull/320
Author: @mattmillerai
Created: 9/18/2026
Status: ✅ Merged
Merged: 10/7/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-15559-panel-integrity-check📝 Commits (10+)
4c6e90efeat(cursor-review): make panel integrity a check-run conclusion1ad01effix(cursor-review): fail Panel integrity closed when the decision jobs did not succeede4e287dfix(cursor-review): fail Panel integrity closed on a dead preflight and on absent producer results10090cbMerge remote-tracking branch 'origin/main' into matt/be-15559-panel-integrity-check2bf8e31fix(cursor-review): fail Panel integrity closed on a dead ledger too73557d7docs(cursor-review): name the Panel integrity bullets instead of counting them5d8d430Merge remote-tracking branch 'origin/main' into matt/be-15559-panel-integrity-check3d0b477docs(cursor-review): caveat that a later no-panel run supersedes a red Panel integrity4b6c9a8test(cursor-review): pinfail-fast: falseso a red leg cannot cancel the panel99be8b7Merge remote-tracking branch 'origin/main' into matt/be-15559-panel-integrity-check📊 Changes
5 files changed (+1453 additions, -5 deletions)
View changed files
📝
.github/cursor-review/README.md(+75 -1)➕
.github/cursor-review/tests/test_workflow_panel_integrity.py(+875 -0)📝
.github/workflows/cursor-review.yml(+363 -3)📝
README.md(+1 -1)📝
docs/callers/cursor-review.md(+139 -0)📄 Description
ELI-5
When the review panel runs, six AI reviewers each look at your PR and one judge writes up what they found. If one of those six gets stuck and never answers, its check still turned green — so anything reading the PR's check rollup saw a complete panel when it was really a third of a panel. This PR makes that visible: a reviewer that didn't answer turns its own check red, and one new
Panel integritycheck says whether the panel as a whole was whole. The review still posts either way — nothing new blocks a merge unless a repo chooses to require the new check.What changed
1. A leg that didn't submit is red. The
reviewjob gets a final step,Fail the leg when the cell did not submit, which reads/tmp/findings-out/findings.jsonback and exits 1 when itsstatusis notok. It sits afterUpload findings artifacton purpose: the artifact is already published,consolidateandnotify-completegate onneeds.review.result != 'skipped'(a failed matrix is not a skipped one) andpost-reviewgates onconsolidate, so the consolidated review still posts. The only thing that changes is that the gap reaches the rollup. TheRun cursor reviewcomment block is updated to say the step cap avoids a cancelled run, and that the leg is now deliberately failed when the cell did not submit.2. A panel-level
Panel integritycontext.consolidatenow exposesok_count,total(from the aggregate step),degradedandjudge_statusas job outputs. A newpanel-integrityjob —needs: [gate, round-cap, diff-size, preflight, ledger, review, consolidate, post-review](preflightandledgerjoined in the second and third review rounds,round-capwhen #345'smax_roundscap landed onmain— see Judgment calls),always()plus the same four gate conditionsconsolidateuses (nested under an upstream-failure disjunct, so a failed decision job is red rather than skipped),permissions: {},timeout-minutes: 5— emits one::error::per cause and exits 1 when any of: fewer cells submitted than ran;ungated_findingsis non-empty and greater than zero;delivered != 'true';degraded == 'true'. Afalseincremental_subsetis annotated as a::warning::and is deliberately not a cause — see Judgment calls. Otherwise it prints one::notice::Panel integrity: <ok>/<total> cells, <n> anchored finding(s), 0 unanchored.and exits 0. It gates no other job — a short panel must not also cost the PR the findings it did produce.3. Caller-facing docs. A
## Panel integritysection in the setup guide next to the Blocking gate (what red means, that it is the context an automated merge gate should read, and the two-switch required-check shape), plus a job/check-run table and a matching section in the directory README. Both surfaces scope the claim to "was the panel whole?" and state plainly that the deliberate no-panel branches stay skipped, that a required skipped check passes, and that over-cap in particular wants the Blocking gate. The rootREADME.mdcatalog row is updated too (0ca9800): it enumerated the check surface as Blocking-gate-only, so after this branch it was wrong by omission on the two things an adopter most needs — that there is now a second, always-onPanel integritycontext and that it, not a panel leg, is the stable one to require; and that the caller job goes red on ~40% of runs. The skip semantics are stated in the same breath; forgeability and same-SHA supersession are pointed at rather than restated, so the catalog cannot drift from the guide on the details.No caller change is required; the shared caller bumper propagates the workflow SHA. No
Skip-caller-bumptrailer — this is a behavioral change and every fleet caller should bump.Judgment calls
The leg check reads the artifact defensively rather than as a one-liner. That cell's own agent runs
--trustwith shell access in the same job, sofindings.jsonis untrusted bytes and itsstatuslands in a::workflow command::line. A missing or unparseable file now reads as "did not submit" and produces the proper::error::instead of a traceback, and the value is sanitized by replacing disallowed characters rather than dropping them — deleting them would let a crafted"o k"collapse into the one value that passes.Panel integrityflattens every value it interpolates for the same reason (a newline forges a second workflow command) and, sincebec0638, percent-escapes them too — the runner unescapes%0Aback into a newline, so stripping real newlines alone leftJUDGE_STATUS(an unvalidated string from the judge agent's own tool) able to forge a command without containing one. Both are covered by tests.always(), not!cancelled(), onpanel-integrity. GitHub counts a skipped required check as passing, so green-on-cancel would be a fail-open for any repo that requires this — the same reasoning the Blocking gate documents at its ownif:. A cancelled run therefore reports red; under the documentedcancel-in-progresscaller that red lands on the superseded head SHA, not the new one. Documented.The caller job now goes red on a failing leg, on a measured ~40% of runs. A reusable's caller job takes the aggregate conclusion of the jobs inside it, so this is unavoidable given (1) — and it is the point, since the rollup is the surface the whole ticket is about. Called out explicitly in the setup guide, with the instruction to require
Panel integrityrather than the caller job.A failed
Gate,Diff size checkorPreflightfailsPanel integrity, it does not skip it.Originally this skipped, on the reasoning that theThat was a fail-open, caught in review: all four gate conditions read job outputs, an output is empty when the job producing it failed, so the check skipped on exactly those runs — and GitHub counts a skipped required check as passing, which would have handed a green merge gate to any caller that took this PR's own advice and required it. The jobGatecontext is itself red so the rollup still carries the signal.if:now also runs onneeds.gate.result != 'success'and on a fresh in-scope review whosediff-sizedid not succeed, and a first stepFail if the panel decision itself did not completeturns that into red — the same fail-closed shape asBlocking gate's first two guards. Guard first, so the report step never evaluates its causes off empty outputs. A second review round found that fix had closed only two of the three decision jobs:reviewdeclaresneeds: preflight, so a failedpreflightskips the whole matrix and leavesneeds.review.result == 'skipped'— indistinguishable from the deliberate no-panel branches — and the check skipped over a run where not one cell started.preflightnow joinsneeds:and the same guard covers it. A third round found the same fail-open one dependency further over: the matrix declaresneeds: [gate, diff-size, preflight, ledger], andledgerwas the one job of the four still unguarded — a redledgerskips the matrix,reviewreportsskipped, and the check skipped into a green required check.ledgernow joinsneeds:, the jobif:and the guard as well. It reads like it cannot fail (every step in it iscontinue-on-error, precisely so it never skips the matrix) but that makes it rare, not unreachable:Ensure ledger artifact existscarries nocontinue-on-error, its 20-minute job cap sits above only 15 minutes of step caps, and a cancellation or lost runner is not a step outcome at all. The pin is now the RELATIONSHIP — a test readsreview'sneeds:and requires every entry to appear in all three places — because this hole was re-opened twice by a job being added to the matrix and not to the check. The deliberate skips are untouched: they are the branches wheregateanddiff-sizeboth succeeded and said no panel was warranted. Over-cap stays a skip on purpose and is routed to the Blocking gate, whose own second guard I verified fails closed on exactly that path.round-cap(#345, merged frommain) is the one decision job the matrix does not need directly — it sits upstream ofdiff-size, whose ownif:skips oncapped == 'true'— so it is read on both sides: a non-successround-capjoins the guard and goes red, and a successfulcappedhead is a deliberate skip like over-cap (the Blocking gate already fails closed on it). Before that, a capped head went red telling the caller to re-run a job that would just hit the cap again; the tests now evaluate the job and guardif:expressions over the capped / uncapped /max_rounds-off / failed-cap / unlabelled / over-cap cases rather than substring-matching them.Every cause is paired with its producing job's
result, not just its outputs. An output is the empty string when the job producing it failed, and every cause treated empty as a pass. Executing the pre-fixrun:block withdelivered=true(whichpost-review.pywrites the moment the POST returns, before the rest of that job runs) and a failedpost-reviewexits 0 and printsPanel integrity: 6/6 cells— a green panel signal over a red delivery job.ok_count/totalare sharper still: both empty on a deadconsolidate, and unset-vs-unset compares equal. The step now readsneeds.consolidate.resultandneeds.post-review.result(the env namesblocking-gateandnotify-completealready use) and rejects an empty count explicitly instead of comparing it.The leg check is scoped as an availability signal, not an attestation. The "did this cell submit" verdict is the
statusthe cell's own--trustagent wrote, so a prompt-injected cell can green itself and the panel count with it. That limit is now stated in the workflow comment, the caller guide and the README, along with what would fix it (recording the submission outside the cell's own writable job). It catches the stalls, crashes and step caps that are what every observed failure has been; it is not proof against an adversarial PR, and the docs no longer read as though it were.A discarded incremental block warns; it is deliberately not a cause. This started as a failing cause, written while
diff-sizedeclared no such output. Merging #321 (84db66a) both published the output and inverted what afalsemeans, without touching a line of this job. #321 made the subset check fail closed: a block that does not verify is discarded whole and the panel reviews the full reviewed diff alone. So the cells were never "prioritized onto hunks that were not in scope" as the::error::claimed — they got no prioritization hint at all, which is byte-identical input to a run that never built a block (round 1, and every "could not build one" branch), and those reporttrue. Reddening the one context callers are told to mark required, over a run whose reviewed content is indistinguishable from a normal one and where nothing was skipped and no finding suppressed, is a merge-blocking false positive — and it is the one cause that never indicated a short panel, which is the only question this check answers. It now emits a::warning::naming what actually happened and pointing at theIncremental diff discardedwarning onDiff size checkfor the counts. A non-subset block is still a real builder defect worth seeing; it is not a panel-integrity failure. The other five causes are untouched.A new sibling test file rather than edits to
test_workflow_job_isolation.py. That suite pins the credential split; this one pins the integrity signals. Same house style: stdlib only, no PyYAML, comments stripped before every assertion so a comment naming a step can never satisfy it.Capability check
The diff adds
exit 1/::error::paths, which is the shape that usually means a capability is being denied — it is not one here, and I verified that rather than asserting it. Nothing the workflow could do before is refused: the new check is advisory, gates no job, and the consolidated review still posts over a short panel. I confirmed that by reading every downstreamif:—consolidateandnotify-completekey onneeds.review.result != 'skipped',post-reviewonneeds.consolidate.result == 'success',blocking-gatedoes notneeds:the panel at all — and a repo-wide grep forneeds.reviewreturns only those three sites, so no path keys on the panel having succeeded. A test asserts no jobneeds: panel-integrity.Provenance
Authored by: agent-work loop
Verified: at head
432fa41(merge oforigin/maincfca794, #355–#363 — no textual conflict; those touch auto-approve/approve-external,post-review'sneeds:anddiff-size's newincremental_stateoutput, none of whichPanel integrityreads;incremental_subset, which it does read, is still emitted unchanged):test_workflow_panel_integrity.py.check_workflow_pins.pyOK (167uses:refs SHA-pinned);check_agents_md.py --root .passes with 2 pre-existing warnings;check-org-repo-literals.shOK.panel-integrity'sneeds:is a superset ofreview's decision jobs plusround-cap, every output it reads is declared upstream, nothingneeds:it,permissions: {}intact; theif:truth-table tests cover capped / uncapped / failed-cap / over-cap cases.55ede56: all 8 checks pass; CI on432fa41pending at time of writing; nomerge_grouprun exists for this PR.Deviations:
gate,diff-size,preflight,ledger) now fail the check rather than skip it, pinned as a relationship toreview'sneeds:; causes are paired with producing-job results, not outputs alone.%→%25escape added toflatten()so agent-writtenJUDGE_STATUScannot inject workflow commands (bec0638).incremental_subset == 'false'demoted from a failing cause to a::warning::after #321 made a discarded block mean "panel read the full diff" (76aa262) — a deviation from the plan, whose premise no longer holds.README.mdcatalog row updated (3d0b477,0ca9800), including the same-head-SHA supersession caveat.8c786ef):round-capadded as a decision job, and amax_rounds-capped head now skips the check instead of going red.exit 1, and same-head-SHA supersession (design change, filed as a follow-up).Deviation detail (carried forward)
none against the plan as specified. Three corrections after review, all fail-opens the plan did not anticipate: (1) "a failed
Gateskips this check" became fail-closed, extended in the second round topreflight, the third decision job — the matrixneeds:it, so its failure was reaching the check as an ordinaryskippedreview; (2) causes read job outputs only, so a producer that died left a clean-looking report; (3) the round-2 fix namedpreflightspecifically rather than the invariant behind it, leavingledger— the fourth job the matrixneeds:— still able to skip the check into a green required check. That one is now pinned as a relationship betweenreview'sneeds:and this job's, so the class is closed rather than the third instance of it. The docs asserting the superseded behaviour were corrected in the same commits. A fourth correction landed inbec0638, and it is a reporting-integrity hole rather than a fail-open in the gate:flatten()stripped real newlines before interpolating untrusted values into::error::lines, but the runner unescapes%0Ain a command's message, so a judge writing itsstatusaserror%0A::stop-commands::tokreaches the annotation with no newline in it and the runner supplies one.JUDGE_STATUSis the one reachable value (unvalidated agent text; the counts are python-formatted integers and the rest are job results). It cannot forge the check's conclusion, which is driven by thecausescounter and the exit code — and it is strictly narrower than the forgeability limit already documented — but it was the one annotating surface in this repo not applying the%->%25escape thatcheck_agents_md.py,check_workflow_pins.py,check_public_repo_hygiene.py,check_coderabbit_config.pyandcheck-org-repo-literals.shall do. Two findings are answered but not actioned, and both threads are left open on purpose: staging the per-legexit 1behind an input, and a fourth fail-open found during the reviews pass — a no-panel run on the same head SHA supersedes a redPanel integritywith a skip. The second is a design change, not a patch, and is filed as a follow-up; its documentation half did land, in3d0b477, because the setup guide was not merely silent on that hole — its "Re-run failed jobs" paragraph actively recommended the whole-workflow re-run that triggers it. See Residual. A fifth correction landed in0ca9800, docs-only and on a surface the earlier rounds did not touch: the PR updated the setup guide and the.github/cursor-review/directory README, but not the rootREADME.mdpublic catalog, whosecursor-review.ymlrow still described the check surface as it was before this branch. AGENTS.md carries a standing warning that this catalog has drifted once already. NoSkip-caller-bump: truetrailer on it — the trailer declares the whole PR bump-irrelevant for every fleet and this PR is behavioral, and it would be moot anyway, since the cursor-review fleet watches.github/workflows/cursor-review.yml,.github/cursor-review/**andscripts/check-pr-size, not the root README. A sixth correction landed in76aa262, and it is the first that is behavioral rather than docs-only:Panel integrityfailed onincremental_subset == 'false'with an::error::asserting the cells had been prioritized onto out-of-scope hunks. Merging #321 made that assertion false — the block is discarded whole and the panel reads the full reviewed diff — so the check was reddening a required-eligible context over a run indistinguishable from a normal one, while the setup guide and the env comment still described the cause as inert. The cause is now a non-failing::warning::with a true message, and the relationship is pinned by test, because publishing an output flipped this job's meaning without editing it and nothing else would catch the next such flip. This is a deviation from the plan as specified, which listedincremental_subsetas a failing cause; the premise it rested on no longer holds. A seventh correction is to this description rather than to the tree: What changed §2 still enumerated the new job asneeds: [gate, diff-size, review, consolidate, post-review], the five-job list from the first round. The second and third review rounds addedpreflightandledger— the fail-open fixes that are the substance of the Judgment calls entry on decision jobs — so the summary contradicted both the merged file and its own rationale, on precisely the list that whole saga is about. Corrected to the seven the file declares; no code changed.Residual
Not fixed here, and worth its own follow-up:
A no-panel run on the same head SHA supersedes a red verdict with a skip. Found while self-reviewing, after the three rounds above; thread left open at
#discussion_r4050781260. The three corrections above closed the decision-job half of the fail-open — a job dying and skipping the check. This is the other half and needs no failure at all: once run A on SHAXreports red, any later event onXwhere the gate says no panel (an unrelated label toggled,skip-cursor-reviewapplied, the trigger label removed, or the whole-workflow re-run this PR's own setup guide recommends, which setsalready_reviewed=true) makes this job'sif:false at its second disjunct. The job is skipped, a skipped job still publishes aPanel integritycheck run newer than run A's, branch protection reads the latest of a given name, and a skipped required check passes. The panel is still short and the gate is green. Not patched here, deliberately: the verdict lives only inconsolidate/post-reviewjob outputs, which do not exist on a run where the panel did not run, so making it event-independent means reading prior state from outside the run (checks: readon the previous conclusion, orpull-requests: readon the landed review) — and this job is pinned by test atpermissions: {}with "no API call, no checkout, no network" as a stated property.blocking-gateis the model and shows why it is a design decision rather than a patch: its jobif:is unconditional precisely so its verdict is event-independent, and its exemptions for those same two branches are safe only because they fall through to a live thread query. Until it lands, the guide's "requirePanel integrity" advice is a weaker gate than that section reads — worth weighing at merge, though a red that a label toggle can clear is still strictly more signal than today's silent green. The docs half is now landed (3d0b477): supersession is a named shape in the setup guide's "before you require it" list (trigger set, and "push a new commit" as the way to clear a red instead of re-triggering on the reviewed one), a matching limit sits beside the forgeability one in the README, both name BE-15604 so the caveat expires with the fix, and the "Re-run failed jobs" paragraph now points at that shape rather than silently recommending into it. The check's behaviour is unchanged; what changed is that the page no longer advises the action that hides the signal it tells you to require.No live panel run exercised this. The plan's third verification step — trigger the review label on a test PR, confirm
Panel integritygreen with all six legs submitting, then force a leg failure (e.g.timeout-minutes: 1on a branch of the reusable pinned viaworkflows_ref) and confirm that leg's context andPanel integritygo red while the consolidated review still posts — was not run. It needs a liveCURSOR_API_KEYpanel on a consumer repo, which this change cannot reach; and this repo's ownci-cursor-review.ymlpins a merged-main SHA rather than the branch, so the panel that reviews this PR runs the old code. Everything asserted above is therefore static (workflow-shape tests) plus direct execution of the tworun:blocks against fabricated inputs — it is not evidence from a real run. The first live panel after the caller bump is the real acceptance test; watch forPanel integrityred-on-cancel noise and for the ~40% caller-job-red rate landing as expected.Resolved. The sibling ticket landed as #321 and the output exists; the name and the literalincremental_subsetis unverified because it does not exist yet.falsevalue were both confirmed againstdiff-size. The branch is no longer dead code — but confirming it also showed the cause was asserting a consequence #321 had made impossible, so it was demoted from a failure to a::warning::(Judgment calls). What remains unverified is the same thing as every other cause here: no live panel run has exercised it.notify-completestill does not mention a short panel. The Slack complete DM reports judge degradation but not the cell count, so the person who triggered the review is told the review is done without being told a third of the panel never answered. Out of scope here (the ticket scoped this to check-run conclusions), but it is the same gap in the other notification channel.The measurements quoted in the code comments and docs are the investigation's, not re-measured here. The 92-run / 38-run / 52-of-552-cell figures come from the spike this change implements; I did not re-run that sweep, and I have no access to the run history to do so. They are quoted in an anonymized form (no repo named) because this repo is public. If the underlying timeout rate has moved since, the "~40% of runs" figure in the setup guide is what would need updating.
No caller-side audit was possible for the ~40% caller-job-red rate. Review raised that the per-leg
exit 1turns every consumer's caller job red on a measured ~40% of runs the moment this SHA is bumped, with no input to stage it. The setup guide's answer is to requirePanel integrityand not the caller job, but whether any consumer currently marks the caller job a required check cannot be checked from here — rosters are repo secrets. That one-time audit belongs before the fleet bump, not in this diff, and the thread is left unresolved so it is visible at merge time.The panel count is forgeable by the panel. A cell's
statusis written by its own--trustagent, so a prompt-injected cell can green its leg and the count. Now documented as a limit in all three surfaces rather than fixed: making it forgery-resistant needs the submission recorded outside the cell's own writable job, which is a change to the submission path, not to this check.Aggregate panel findingshas the samejson.JSONDecodeErrorgap.consolidate's artifact read (cursor-review.yml~line 1985) catchesJSONDecodeErrorbut notUnicodeDecodeError, on files agents write. Pre-existing and untouched here; it fails closed (the step dies,consolidategoes red, andPanel integrityis now red on that via the newCONSOLIDATE_RESULTcause), so it is visible rather than silent.Nothing here reduces the leg-failure rate. The change makes a stalled
cursor-agentvisible; it does not make it rarer. Explicitly out of scope per the plan, but the ~40% caller-job-red rate is a strong argument for tackling the timeout rate next, and it is the thing most likely to make reviewers start ignoring the red.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.