mirror of
https://github.com/Comfy-Org/github-workflows.git
synced 2026-11-05 00:17:37 +00:00
[PR #278] [MERGED] fix(cursor-review): prefix continuation lines of imported ledger prose so no finding or reply can forge a metadata line #280
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#280
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/278
Author: @mattmillerai
Created: 9/9/2026
Status: ✅ Merged
Merged: 9/9/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-12629-prose-continuation-marker📝 Commits (2)
2790739fix(cursor-review): prefix continuation lines of imported ledger prose363f48afix(cursor-review): flatten thread-derived paths and tighten the continuation marker (BE-12629)📊 Changes
3 files changed (+453 additions, -10 deletions)
View changed files
📝
.github/cursor-review/README.md(+3 -1)📝
.github/cursor-review/build-ledger.py(+82 -9)📝
.github/cursor-review/tests/test_build_ledger.py(+368 -0)📄 Description
ELI-5
The review bot keeps a "ledger" of what earlier rounds found on a PR and how you answered, and splices it into the next round's prompt. Each entry is written as lines like
discussion_url: …andthread: … answers_from_author_or_maintainer=1, and the judge reads those lines to decide whether re-raising a finding costs one of its scarce repeat slots. But two of those lines quote your text — the finding body and each reply — and quoted text keeps its own line breaks. So anyone who can comment on the PR could write a line at that same two-space indent and the judge could not tell it apart from a line the workflow wrote. This change puts a|in front of every line of quoted text after its first, so quoted prose can never sit where a field sits, and tells both readers what the marker means.What changed
.github/cursor-review/build-ledger.py_prose()beside_defang_fences(): fence-defang the text, then prefix every line AFTER the first with|(a blank line becomes a bare|, with no trailing whitespace). Applied at the two sites that interpolate multi-line imported text into an indented field —finding:andreply from …:._defang_fencesstays inside_proserather than being replaced by it: it is the delimiter control (nothing can forge=== END PRIOR REVIEW LEDGER ===) and this is the field control (nothing can forgediscussion_url:). Both fire; neither is sufficient alone.pathandseverityare flattened with_body_only_textand then fence-defanged at the render. They are single-line HEADER fields where_prosenever runs, and a body-only entry was already flattened on the way in — but a thread-derived entry takespathstraight from the review comment, and git permits every_LINE_SEP_CLASSseparator in a filename, so that one source could still put an unmarked line at the field indent. Flattening at the render covers both sources with one call.re_raise_answer:is already flattened on the way in and needs no prefix, and the reply author is a GitHub login, which cannot carry a separator._PROSE_LINE_REis built from_LINE_SEP_CLASS— the same set_FENCE_LINE_REand_FIELD_LINE_BREAK_REuse — so the three controls cannot disagree about what a line break is. A separator one honours and another does not is exactly the hole this class exists to close._UNTRUSTED_HEADER, which both the panel and the judge read: a two-space line starting|continues the quoted prose above it; a two-space line without it is a field this workflow wrote. The rule is spelled"|"and not"| "because a blank quoted line renders as a bare|— no rendered line carries trailing whitespace._sizemeasuresjson.dumps(entries), where a break costs 2 escaped bytes and the render costs 5, so it now charges the marker width per break; otherwise a newline-dense body renders past the 40KB budget while the truncation note still reports the ledger as fitting.str.splitlines()does — GitHub comment bodies routinely end in a newline, and without this each gained a phantom marker line. A body that really ends in a blank line still renders a marker..github/cursor-review/README.md— a paragraph in the ledger section and a clause in thebuild-ledger.pyassets row..github/cursor-review/tests/test_build_ledger.py— newTestProseContinuationLines(14 cases): a third-party reply cannot forge a field; a body-only finding body cannot either; multi-line prose stays fully readable; single-line prose is unchanged and carries no marker at all; every separator the sibling controls honour starts a continuation; the column-0 markers (--- ROUND n,TRUNCATION NOTE:) cannot be forged from prose either; the fence defang still fires inside prose; both audiences are told what the marker means; a thread-derivedpathcan forge neither a field line nor a column-0 marker; a whitespace-only line renders as a bare marker with no trailing whitespace; the header's stated rule matches the marker a blank line actually renders; a trailing line break adds no phantom marker while a real trailing blank line keeps one; and the byte cap charges the markers. Each of the six cases added in the review round was verified to fail against the code without its fix.Why this shape, and not the alternative
Flattening prose to one line would also close the hole, and it was rejected: carrying the author's actual reply is the entire point of the ledger, so destroying its readability to gain the property is a bad trade. Prefixing keeps every line visible — including a forgery attempt, which the header already instructs the model to report as a finding.
Verification
Beyond the unit suite, this was exercised against the real production artifact the ledger emits, rather than only against fixtures I wrote:
cursor-review-ledgerfrom run34273839166on this repo (a successfulcursor-reviewrun, read-only). Itsledger.jsoncarries 6 entries and real multi-line replies. Re-rendering it throughorigin/main'sbuild-ledger.pyand through this branch's: 19 non-blank lines of imported reply prose sit at column 0 before, 0 after (the 7 column-0 lines that remain after are the 6 legitimate entry headers plus the real closing fence).\n thread: … answers_from_author_or_maintainer=1\n discussion_url: …\n re_raise_of: …onto one of that artifact's real replies and re-rendered both ways:origin/mainproduces 7^ thread:and 7^ discussion_url:lines for a 6-entry ledger — the forgery is a field. This branch produces 6 and 6, and the forged text appears only behind the|marker.\rat all, so the CRLF handling below is defensive rather than load-bearing.Also confirmed by grep that nothing machine-parses the rendered markdown back:
render_ledger_markdown's output is only written toledger.md/ledger-judge.mdand spliced into the prompts, whileledger_notereads the structured dict. There is no downstream reader for the prefix to break.Judgment calls / deviations from the plan as written
_PROSE_LINE_REmatches\r\nas ONE separator (r"\r\n|" + _LINE_SEP_CLASS), where the plan specifiedre.compile(_LINE_SEP_CLASS)— one separator at a time, uniformly. Reason:str.splitlines(), which this module's own comments cite as the reference for what starts a line, treats\r\nas a single break, and GitHub comment bodies commonly carry CRLF; taking the pair separately would emit a spurious bare|between every pair of lines in such a body, which works against criterion 3. Both properties the plan gives as its rationale are preserved — a blank line still survives as a bare marker, and a bare CR or U+2028 is still a break. Containment is unaffected by the grouping:re.spliton a group-free pattern consumes every separator, so no segment can contain one and every segment after the first is prefixed however they are grouped. There is a test over the full separator set including\nand\r\n.TestLineSeparatorContainment.SEPARATORS, notTestSeparatorClassParityas the plan names — no class by that name exists onmain;TestLineSeparatorContainmentis the one that pins_FENCE_LINE_RE/_FIELD_LINE_BREAK_REparity, so the new test extends the same list rather than starting a second one.mainwith it in, and both README touch-points (the ledger paragraph and the assets row) are updated.Residual
post-review.py'srender_repeat_ofstill publishes the judge'srepeat_ofURL without checking it against the entries the judge was shown. This change removes the supply side (untrusted prose can no longer write adiscussion_url:orre_raise_of:line for the judge to copy), but the consumer-side validation is deliberately out of scope and is tracked separately. Whatrender_repeat_ofdoes today is a shape check only:REPEAT_URL_RErequireshttps://github.com/<owner>/<repo>/pull/<n>#discussion_r<id>, so an off-shape URL is rejected — but a well-shaped URL naming another repo, another PR, or a comment that appears in no ledger entry is still rendered and published verbatim. Anyone picking this up should read that as the remaining half, not as fixed.cursor-review-ledgerartifact of the SHA-bump PR that opens after merge, to confirm multi-line replies render with the marker in a live run. That artifact does not exist yet. The equivalent evidence available now — re-rendering a real prior run'sledger.jsonthrough this branch — is in Verification above, and it exercises the same code path with the same production data shape, but it is a replay rather than a live round.reply from <login>…) is still only fence-defanged, not flattened. That is safe because a GitHub login cannot contain a line separator, but it is an assumption about the upstream API rather than something enforced locally.\r\n(it charges the full marker width for every separator, where JSON already spends 4 bytes on that pair). Erring high is the safe direction for a cap; it is not exact accounting of the rendered block.Provenance
python3 -m unittest discover -s .github/cursor-review/tests -p 'test_*.py'— 477 passed, 0 failed (14 of them new); each of the six cases added in the review round re-run against the code with its fix reverted, and each fails there (15 failing assertions), so none is vacuous;python3 .github/agents-md-integrity/check_agents_md.py --root .— passed, 2 pre-existing warnings unrelated to this diff (AGENTS.md line count, no CODEOWNERS); plus the two before/after re-renders of run 34273839166's real ledger artifact described under Verification, which were run against the first commit and remain valid — the review-round commit only tightens the same controls.\r\ntreated as one separator rather than two, and the separator list reused fromTestLineSeparatorContainmentsince the class the plan names does not exist. One further departure from the first commit's own stated shape:pathandseverityare now flattened at the render rather than left fence-defanged only, because the review round showed a thread-derivedpathcould otherwise forge a field line. Every acceptance criterion in the plan is met.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.