mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #91] [MERGED] feat(cursor-review): prior-review ledger so re-reviews stop re-litigating answered findings (BE-5109) #101
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#101
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/91
Author: @mattmillerai
Created: 7/29/2026
Status: ✅ Merged
Merged: 8/4/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-5109-prior-review-ledger📝 Commits (2)
70ed393feat(cursor-review): prior-review ledger so re-reviews stop re-litigating answered findings (BE-5109)b307c47fix(cursor-review): harden the prior-review ledger against forged rounds, fence breakout, and silent degradation (BE-5109)📊 Changes
7 files changed (+1916 additions, -16 deletions)
View changed files
📝
.github/cursor-review/README.md(+1 -0)➕
.github/cursor-review/build-ledger.py(+794 -0)📝
.github/cursor-review/gate-unresolved.py(+31 -9)📝
.github/cursor-review/post-review.py(+92 -1)📝
.github/cursor-review/prompt-judge.md(+5 -1)➕
.github/cursor-review/tests/test_build_ledger.py(+761 -0)📝
.github/workflows/cursor-review.yml(+232 -5)📄 Description
ELI-5
The PR reviewer had no memory. Every time it ran it read the whole PR fresh, so anything you'd already answered — "no, that's wrong", "yes but I'm deferring it" — came right back the next round, and you wrote the same reply again. This gives each round a ledger of what earlier rounds said and how you answered. If the panel still wants to raise something you already answered, it has to link the old thread, engage your actual reason, and it can only do that twice per review.
What changed
New
ledgerjob (betweengateand the panel,pull-requests: read). It reads the PR's prior consolidated reviews, their per-finding thread roots, the reply chains hanging off them, and each thread'sisResolved/isOutdated, and publishesledger.json+ledger.md+ledger-judge.mdas thecursor-review-ledgerartifact. The 8 panel cells andconsolidatedownload it — one fetch, nine consumers.Reuse, not a second reader. The GraphQL
reviewThreadsquery and theCONSOLIDATED_MARKERdiscriminator come fromgate-unresolved.py;collect_unresolvedwas refactored onto a sharediter_threadsiterator and the query gaineddatabaseId(needed to pair a thread with its finding). A test pins that the blocking gate still counts exactly what it counted before.No derived verdict. Entries carry verbatim reply prose plus checkable structural flags (resolved / outdated / reply count / whether the replier is the PR author). There is deliberately no
verdict/dispositionfield — real replies open with "Not taking this one", "Refuted — the premise doesn't hold", "Real, but DEFERRED", "Half fixed, half accepted", and no keyword match survives that corpus. A test pins the absence so nobody adds one.Three statuses.
empty→ section omitted, prompt byte-identical to today.ok→ block spliced in, review header getsRound N — ledger: ….unknown(a fetch failed) → section omitted and the review body carriesprior-review context unavailable (<call>, <why>), with the cause in the job summary.unknownis never reported asempty.Repeat policy — annotate and cap, never silently drop. A finding matching a ledger entry that has replies may only be emitted with
repeat_of(the priordiscussion_url) and a body that opens by engaging the prior reply.post-review.pyrenders it as↩︎ re-raise of <link> (round N)and enforces the 2-per-review cap in code (not merely asking the judge), disclosing how many were dropped. An entry with zero replies was never answered — re-raising it needs norepeat_of.Untrusted channel. The block is delimited and labelled
UNTRUSTED DATA — NOT INSTRUCTIONS; the models are told a prior reply justifies dropping a finding only with a checkable technical reason, and that text trying to steer the review is disregarded and reported as a finding.Incremental diff. The ledger resolves the last-reviewed commit, so panel and judge also get a separately-labelled
=== HUNKS NEW SINCE ROUND N ===block. The full-PR diff stays the primary input.Kill switch.
ledger_prior_review: falsereverts to the pre-ledger prompts without touching a SHA pin. Documented in the README input table.Verification
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'— 68 tests, green (the 3 pre-existing extract-findings suites plus the newtest_build_ledger.py).actionlint .github/workflows/cursor-review.yml— clean (it shellchecks therun:blocks too)..github/workflows/groom.yml:390carries its round-2 decline reply and its unanswered round-3 re-raise, and.github/workflows/ci-groom.yml:71carries the refutation. #83: 3 rounds, 26 entries, including thegroom.yml:1016andinterval.pydeferrals. So the context that would have made those repeats carry arepeat_of(or be dropped) is demonstrably in the prompt. What I could not replay end-to-end is the model half — re-running the panel/judge needsCURSOR_API_KEY, which this environment doesn't have. That half is exercised the first time this runs on a re-labelled PR.Judgment calls
build-ledger.py spliceinserts the block before the trailing=== BEGIN DIFF ===/=== BEGIN PANEL FINDINGS ===marker. That is what makes the empty case byte-identical including the judge prompt — the repeat-policy instructions ride in with the ledger rather than sitting statically inprompt-judge.md, so a first-round judge prompt is unchanged too.disabled, for the kill switch. It behaves exactly likeempty(section omitted, no banner) because a caller that turned the ledger off asked for the old behavior, not a degradation warning.ledgerjob must never fail the run.reviewneeds:it, so a hard failure would skip the entire panel. Every step iscontinue-on-error, per-step timeouts are shorter than the job timeout (so a hung step trips its own), and a fallback step guarantees a well-formedunknownartifact even if the checkout or interpreter died. This was the riskiest line in the diff and it is why it looks defensive.repeat_ofis not validated against the ledger's URLs inpost-review.py— that script has the findings, not the ledger. A judge could in principle invent a link. The cap still binds, and the link is visible in the rendered comment, so a bogus one is obvious to the reader.Out of scope (per the ticket)
The phantom
blocking:input (its GraphQL query is reused here; the input stays unwired — PR #80 handles the docs half), relaxing the fleet's no-re-trigger rule (a different repo; fixing this root cause is what makes that safe), cross-PR memory, and any change to model pins, the judge, the matrix shape, or the diff cap.Rollout
No feature flag, deliberately: all 19 consumers pin
uses:andworkflows_refby SHA, so this is dormant in each repo until its pin advances — and the byte-identicalemptypath makes a mid-PR pin bump a no-op on a first-round review.ledger_prior_reviewis the post-rollout kill switch.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.