mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #273] [MERGED] fix(cursor-review): carry re-raise lineage through the body-only sentinel #275
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#275
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/273
Author: @mattmillerai
Created: 9/8/2026
Status: ✅ Merged
Merged: 9/8/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-12534-repeat-lineage📝 Commits (2)
8fa6c16fix(cursor-review): carry re-raise lineage through the body-only sentineld79bc06fix(cursor-review): tighten the demoted re-raise lineage (BE-12534)📊 Changes
5 files changed (+956 additions, -26 deletions)
View changed files
📝
.github/cursor-review/README.md(+6 -0)📝
.github/cursor-review/build-ledger.py(+271 -12)📝
.github/cursor-review/post-review.py(+139 -10)📝
.github/cursor-review/tests/test_build_ledger.py(+431 -3)📝
.github/cursor-review/tests/test_post_review.py(+109 -1)📄 Description
ELI-5
When the reviewer raises a finding it already raised before, it links back to the old conversation ("re-raise of …, round 2"). That link is what stops the reviewer from re-litigating the same point forever — only two such re-raises are allowed per round, and only findings the author actually answered cost one of those two slots.
But some findings can't be attached to a line of the diff, so they get written into the review body instead. Those have no conversation of their own, so the link was deliberately scrubbed out of them — and it was scrubbed without being saved anywhere. The next round then saw a brand-new, never-answered finding and let it be raised again for free. One such hop broke the budget for that finding forever after.
This keeps the link. It travels as a hidden field rather than as visible text, and the next round looks it up against the PR's real comments to find the original conversation, check whether the author actually answered it, and quote what they said. If they did, raising it again costs a slot, exactly like it always should have.
What changed
The policy is unchanged — only an ANSWERED finding costs a repeat slot. What changed is which finding that is read off: the lineage ancestor of a body-only entry, using the ancestor's real answer state, instead of the thread-less copy that is unanswered by construction.
Writer —
.github/cursor-review/post-review.pynormalize_commentsstill stores the rendered↩︎ re-raise of …trailer initem["repeat_of"], soenforce_repeat_cap's count andstrip_repeat_line's reconstruction are byte-for-byte untouched. It now also stores the raw URL asitem["repeat_url"].render_body_only_sentinelemits one optional key,repeat_of, following the exactlost_to_fallbackdiscipline: emitted only for an item that carries it, so a payload with no re-raise in it renders byte-identically to before. The ancestor's round is deliberately not carried — the reader derives it from the resolved ancestor's own review, which is truthful by construction and available whenever the URL resolves at all, so arepeat_roundfield would be payload nothing reads inside a body under a hard size cap that can drop a real finding to make room for it.fullmatch, notmatch:$also matches just before a trailing newline, and JSON round-trips a line break losslessly — it would arrive on the ledger's ownre_raise_of:line at column 0 of the prompt, which is the forged-fence shape the field flattener already exists for.strip_repeat_linestill clears the trailer from the body. The URL never travels in prose. The prose trailer still shows the round, andcoerce_repeat_roundstill guards that render.Reader —
.github/cursor-review/build-ledger.py_resolve_lineageparses the trailing comment id, looks it up in the PR's own fetched comments, and requires a ROOT comment belonging to one of our consolidated reviews. That is the integrity check; a hallucinated id, a reply, a human's review comment and a foreign host all resolve to nothing.round_by_review, which covers every consolidated review and is built before theMAX_ROUNDSage filter — so the ancestor is still found after its own round has aged out of the ledger. That is the spike's scenario.html_url, never the relayed string, and an ancestor without one yields no lineage at all.REPEAT_URL_REshape-checks the owner/repo/PR-number half without comparing it to anything, and only the trailing id is read — so a URL naming another repo does resolve against a genuine root of ours; refusing to relay it is what keeps that from mattering.repeat_of/repeat_round/repeat_answered_count, plus a boundedrepeat_answerexcerpt of the ancestor's most recent answering reply, and rendersre_raise_of: <url> (round N; ancestor_answers=k)with are_raise_answer:line beneath it. Its ownthread.answered_countstays 0 anddiscussion_urlstays""— it still has no thread of its own.ancestor_answers=, not a secondanswers_from_author_or_maintainer=: the entry's ownthread:line already uses that token with a contradictory value, and the judge's first REPEAT POLICY bullet keys on it to demand arepeat_ofequal to the entry's owndiscussion_url, which a demoted entry has none of.k >= 1the closing parenthetical and both steering blocks say a further re-raise must carryrepeat_ofand costs a slot; withk == 0the existing "needs no repeat_of" wording is kept verbatim. The same rule covers a[post-failed]entry, which has the identical lineage problem.repeat_unresolved): the entry says the lineage is UNVERIFIED and withholds the cap exemption, without asserting the converse. Dismissing a review or deleting the ancestor comment are ordinary maintainer actions that otherwise rebuilt the entry as a plain cap-exempt finding and silently reopened the chain; asserting the opposite would instead let a forged unresolvable URL spend a repeat slot.unanswered_count/unanchorable_count/post_failed_countare unchanged, and the entry is not added tounanswered.Verification against the artifact the ticket names
The ticket cites
Comfy-Org/github-workflows#230(a 4-round chain) as where the judge'srepeat_ofbehaviour was observed. It is public, so I exercised the reader against its real payloads rather than only against fixtures — read-onlygh apireads, no writes:_resolve_lineageresolves both real trailers to their real ancestors:discussion_r3859833104→ round 1,ancestor_answers=1, anddiscussion_r3859833108→ round 1,ancestor_answers=1. Under the old behaviour a demoted hop of either would have been rebuilt as cap-free. Both now also return the ancestor's real reply text for there_raise_answer:excerpt, so that field is populated from production payloads and not only from fixtures.None, an id not on the PR →None, another host →None. A foreign repo slug with a real id does resolve — as documented — and renders our own permalink, withattacker/evilnever reaching the block.build_ledgerover #230 runs clean (17 entries, 2 rounds of 4, both existing truncation notes intact, 28.6 KB rendered) and 0 entries carry lineage — every finding there anchored, so the anchored path is provably untouched on production data.html_urlpresence was checked rather than assumed: 81/81 review comments on #230 and 8/8 on this PR carry a non-empty one, so dropping the relayed-string fallback removes a path unreachable in practice.Judgment calls
int():str.isdigit()is true for charactersint()rejects, sorepeat_round: "²"raised an unhandledValueErrorstraight out ofnormalize_commentsand would have taken down the entire review post. Confirmed reproducible on the base branch before the change;build-ledger.py's_body_only_linealready documents "degrade rather than raise" as the rule for exactly this parser.coerce_repeat_roundstill owns that guard for the prose trailer even though the sentinel no longer carries the round. Pinned by a test.html_url, on the reasoning that a resolved id means "the same link". That reasoning was wrong — the shape gate never compares the owner/repo half — and the fallback would have rendered a model-chosen foreign link that the steering then tells the judge to emit verbatim asrepeat_of. It is gone; no permalink now means no lineage.re_raise_of:/re_raise_answer:render lines are gated onnot anchoredeven though only_body_only_entrieswrites the keys, so "anchored entries are unchanged" is a property of the renderer itself rather than an invariant a reader has to confirm elsewhere._body_only_textas well as_defang_fences, and is capped atMAX_LINEAGE_ANSWER_CHARS(300, tighter thanMAX_BODY_CHARS) because every demoted re-raise pays for it out ofMAX_LEDGER_BYTES.html_urlfallback is the only user-facing denial in the diff, so it was tested empirically against every real payload available (see above: 89/89 comments carry a non-emptyhtml_url) rather than argued. The UNVERIFIED parenthetical is not a dead-end — it names the working path ("judge the finding on its merits"). Nothing else here adds a "not supported"/"unavailable"/"STOP" string, a throw/deny dead-end, or a test asserting one; the change strictly adds a capability.Residual
#230was checked and held; anything in that comment that the ticket body did not quote is unexercised.#230's re-raises all anchored, so its entries exercise the resolver against real data but not the demotion that triggers the bug. Nothing available to this run has a body-only re-raise on it; the end-to-end path is pinned by tests that render through the real writer and read through the real reader, which is the strongest available substitute but is not a production observation.discussion_url:andthread:are equally exposed) and whose fix reshapes every entry the panel and judge read. Deferred to a follow-up with a concrete approach; there_raise_answer:line this PR adds is flattened, so it cannot itself forge one.repeat_ofis still bounded by shape on the writing side, not against the ledger it was shown. The reader's ROOT check excludes arbitrary links, so the residual is a valid-but-wrong ancestor draining a repeat slot. Closing it needs the ledger's entry URLs plumbed intopost-review.py, which is a data-flow change; deferred to a follow-up. This one predates the PR —enforce_repeat_caphas always counted judge-supplied re-raise URLs unvalidated.Provenance
python3 -m unittest discover -s .github/cursor-review/tests: 444 passed, 0 failed (422 on base, +22). Also green, unchanged by this diff: agents-md-integrity 46, public-repo-hygiene 204, workflow-pins 316, refresh-reviewers 48, groom 374. Lints:check_workflow_pins.pyOK (11 workflows, 0 exempt);check_agents_md.py --root .passed with 2 pre-existing warnings (AGENTS.md 155 lines, no CODEOWNERS — both present on base);shellcheck -x bump-callers.shclean. Live read-only checks against#230and this PR as described above.enforce_repeat_capandreview-output-mcp.pyare untouched (confirmed by diff), and a sentinel with no re-raise items is byte-identical to before (pinned by test). One deviation from the first revision of this PR, made in response to the review panel: the sentinel no longer carriesrepeat_round, since nothing read it back and it cost bytes under a hard cap. Two panel findings were deferred rather than fixed (prose-line forging; writer-siderepeat_ofvalidation) — both predate this change and are recorded under Residual.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
environmentinput on the three token-minting jobs #276