Hit while posting a Round-2 disposition on PR #17277. manage_pr_review rejected the body twice with "row N reads X where the prior round said X — carry it verbatim", where the two quoted strings were character-identical apart from a leading label. The rejection is not wrong about the mismatch; it is wrong about what caused it.
The Problem
extractDispositionRows() in ai/services/github-workflow/PullRequestService.mjs:1218-1238 decides whether the table's first column is a row label (to be dropped) or part of the carried action (to be compared verbatim):
The pattern accepts an optional RA prefix, an optional #, then digits only. Measured against real labels:
# cell
verdict
consequence
1, 2, #3
stripped
passes
RA-1, RA1, RA-2., RA-10
stripped
passes
RA-1b, RA-1a, RA-1B
folded
verbatim check fails
§0, §1
folded
verbatim check fails
When the cell is folded, it is joined onto the front of the action text — and the Round-1 checkbox form already begins with the same label in bold (- [ ] **RA-1b — …**). So the label appears twice on one side of the comparison and once on the other, and a body that is verbatim is reported as reworded.
The failure is diagnostically misleading, which is the expensive part. The error tells the author to "carry it verbatim" against text they already carried verbatim. There is no way to satisfy it by doing what it asks; the only escape is discovering that the first column is the problem. I got there by reading the validator source, which is not a step the message points at.
Why the labels that fail are the natural ones
RA-1b — a sub-letter is the obvious notation when one finding has a code half and a test half, which is exactly when a reviewer splits an action.
§0 — § is the pr-review guide's own sectioning vocabulary throughout, so reaching for it in a review table is the house style, not an invention.
The comment directly above the regex records that this class was already fixed once:
"It is the row's NUMBER, not part of the carried action, and folding it into the text made every row compare as reworded against a prior round that never contained it — the verbatim check failing on the one thing that is legitimately not verbatim."
The diagnosis is exactly right and the pattern written to implement it covers only the purely-numeric form — so the fix's own comment describes the bug it still has.
The Fix
Widen the label detector to the shapes reviewers actually use — a leading §, and an optional letter suffix on the ordinal — while keeping it narrow enough that a genuine action text is never mistaken for a label. The safer framing may be to invert it: a first cell is a label when it is short and contains no sentence punctuation, rather than enumerating prefixes; a cell carrying an action is never four characters long.
Whichever shape is taken, the error message should name the first column as a candidate cause when the two strings differ only by a leading token.
SECOND SPECIMEN, 2026-08-21 — the same function loses a byte, and it blocked a live review
@neo-gpt could not post a Round-2 disposition on PR #17475:
"The managed submit gate requires every Round-1 action byte-verbatim in a pipe-delimited Round-2 table. RA-2 itself contains the literal token observed|partial; the gate splits on every pipe, deletes that byte, and then rejects the row as non-verbatim. Escaping the pipe also rejects."
Reproduced against the shipped parse:
prior : "Record the nested observed|partial contract distinct from bridge readability"
parsed : "Record the nested observed partial contract distinct from bridge readability"
escaped parse : "Record the nested observed\ partial contract distinct from bridge readability"
line.split('|') splits on every pipe, so a pipe inside a cell's content becomes a cell boundary: the byte is deleted, the halves are rejoined with a space, and the disposition verb is found one column later than it is. Escaping is worse than not escaping — the backslash survives into the compared text and the pipe still splits, so the author who does the correct Markdown thing gets a stranger mismatch.
This is the same defect as the label case, one column over. Both are extractDispositionRows() reconstructing the author's cells wrongly and then blaming the author for the result; the error text says "carry it verbatim" about text that already is. A reviewer following the instruction cannot succeed, and there is no wording that gets them out — which is why it belongs on this ticket rather than beside it.
A row-level cost worth naming: the reviewer's only escape today is to reword their own Round-1 Required Action so it contains no pipe. That silently makes the Round-1 text a function of the Round-2 parser, which is backwards — and it is the shape a durable enum (observed|partial) will keep hitting.
Acceptance Criteria
§0, RA-1a, RA-1b, RA-1B are recognised as labels and stripped, alongside the existing 1 / RA-1 / #3 / RA-2. / RA-10 forms.
A first cell that is genuinely part of an action is still compared, not silently dropped — asserted with a negative case, so the widening cannot become "strip whatever is in column one".
When the carried action differs from the prior round only by a leading token that matches the row's own label, the error names the # column as the likely cause instead of repeating "carry it verbatim" against text that already is.
A cell containing an escaped pipe (\|) survives the parse as a literal |.observed\|partial in the table compares equal to observed|partial in the prior round. Control: an ordinary row with no pipe in its content parses exactly as it does today, so the split change cannot quietly re-cell every table.
The disposition verb is located in the right column when a cell contains a pipe — the findIndex must not drift, which is what turned one defect into a mismatch two columns wide.
When the carried action differs from the prior round only by pipe-or-whitespace, the error names escaping as the likely cause rather than repeating "carry it verbatim". Same requirement as the label case above, second cause.
The Round-2 template shows a label form that passes. It currently shows | RA-1 | [exact prior text] |, which works, sitting next to a Round-1 template whose checkbox form produces labels that do not.
Out of Scope
The Round-1 → Round-2 relation checks themselves (rows.length !== expected.length, the ordering rule). Those behaved correctly here.
Reworking review-label notation. The validator should accept the notation reviewers use; the notation is not the defect.
Avoided Traps
Fixing the template instead of the detector. Documenting "use numeric labels" would leave the misleading error in place for the next reviewer who reaches for § or a sub-letter, and the guide's own vocabulary would still be teaching the failing form.
Widening to "always strip column one". That would drop a real action whose table was mis-shaped, replacing a loud false failure with a silent false pass — the wrong direction for a verbatim gate.
Related
ai/services/github-workflow/PullRequestService.mjs:1218-1238 (the detector) · .agents/skills/pr-review/assets/pr-review-round-2-template.md · .agents/skills/pr-review/assets/pr-review-template.md (the Round-1 checkbox form that produces the labels) · PR #17277 (where it surfaced) · review PRR_kwDODSospM8AAAABJwd75g
Live latest-open sweep: open queue checked 2026-08-17T08:3xZ; the only adjacent item is #17261 ("pr-review §7 promises a micro-path; the validator has no shape for it") — same file family, different defect: that one is about a missing shape, this one about a label pattern rejecting valid bodies. Not a duplicate.
tobiu referenced in commit 473dac5 - "fix(ai): the Round-2 cell parse stops losing the bytes it demands verbatim (#17284) (#17498) on Aug 22, 2026, 12:50 AM
Context
Hit while posting a Round-2 disposition on PR #17277.
manage_pr_reviewrejected the body twice with "row N reads X where the prior round said X — carry it verbatim", where the two quoted strings were character-identical apart from a leading label. The rejection is not wrong about the mismatch; it is wrong about what caused it.The Problem
extractDispositionRows()inai/services/github-workflow/PullRequestService.mjs:1218-1238decides whether the table's first column is a row label (to be dropped) or part of the carried action (to be compared verbatim):const carried = cells.slice(0, index).filter(Boolean), labelled = /^(?:RA[ _-]?)?#?\d+\.?$/i.test(carried[0] || '');The pattern accepts an optional
RAprefix, an optional#, then digits only. Measured against real labels:#cell1,2,#3RA-1,RA1,RA-2.,RA-10RA-1b,RA-1a,RA-1B§0,§1When the cell is folded, it is joined onto the front of the action text — and the Round-1 checkbox form already begins with the same label in bold (
- [ ] **RA-1b — …**). So the label appears twice on one side of the comparison and once on the other, and a body that is verbatim is reported as reworded.The failure is diagnostically misleading, which is the expensive part. The error tells the author to "carry it verbatim" against text they already carried verbatim. There is no way to satisfy it by doing what it asks; the only escape is discovering that the first column is the problem. I got there by reading the validator source, which is not a step the message points at.
Why the labels that fail are the natural ones
RA-1b— a sub-letter is the obvious notation when one finding has a code half and a test half, which is exactly when a reviewer splits an action.§0—§is the pr-review guide's own sectioning vocabulary throughout, so reaching for it in a review table is the house style, not an invention.The comment directly above the regex records that this class was already fixed once:
The diagnosis is exactly right and the pattern written to implement it covers only the purely-numeric form — so the fix's own comment describes the bug it still has.
The Fix
Widen the label detector to the shapes reviewers actually use — a leading
§, and an optional letter suffix on the ordinal — while keeping it narrow enough that a genuine action text is never mistaken for a label. The safer framing may be to invert it: a first cell is a label when it is short and contains no sentence punctuation, rather than enumerating prefixes; a cell carrying an action is never four characters long.Whichever shape is taken, the error message should name the first column as a candidate cause when the two strings differ only by a leading token.
SECOND SPECIMEN, 2026-08-21 — the same function loses a byte, and it blocked a live review
@neo-gptcould not post a Round-2 disposition on PR #17475:Reproduced against the shipped parse:
line.split('|')splits on every pipe, so a pipe inside a cell's content becomes a cell boundary: the byte is deleted, the halves are rejoined with a space, and the disposition verb is found one column later than it is. Escaping is worse than not escaping — the backslash survives into the compared text and the pipe still splits, so the author who does the correct Markdown thing gets a stranger mismatch.This is the same defect as the label case, one column over. Both are
extractDispositionRows()reconstructing the author's cells wrongly and then blaming the author for the result; the error text says "carry it verbatim" about text that already is. A reviewer following the instruction cannot succeed, and there is no wording that gets them out — which is why it belongs on this ticket rather than beside it.A row-level cost worth naming: the reviewer's only escape today is to reword their own Round-1 Required Action so it contains no pipe. That silently makes the Round-1 text a function of the Round-2 parser, which is backwards — and it is the shape a durable enum (
observed|partial) will keep hitting.Acceptance Criteria
§0,RA-1a,RA-1b,RA-1Bare recognised as labels and stripped, alongside the existing1/RA-1/#3/RA-2./RA-10forms.#column as the likely cause instead of repeating "carry it verbatim" against text that already is.\|) survives the parse as a literal|.observed\|partialin the table compares equal toobserved|partialin the prior round. Control: an ordinary row with no pipe in its content parses exactly as it does today, so the split change cannot quietly re-cell every table.findIndexmust not drift, which is what turned one defect into a mismatch two columns wide.| RA-1 | [exact prior text] |, which works, sitting next to a Round-1 template whose checkbox form produces labels that do not.Out of Scope
rows.length !== expected.length, the ordering rule). Those behaved correctly here.Avoided Traps
§or a sub-letter, and the guide's own vocabulary would still be teaching the failing form.Related
ai/services/github-workflow/PullRequestService.mjs:1218-1238(the detector) ·.agents/skills/pr-review/assets/pr-review-round-2-template.md·.agents/skills/pr-review/assets/pr-review-template.md(the Round-1 checkbox form that produces the labels) · PR #17277 (where it surfaced) · review PRR_kwDODSospM8AAAABJwd75gLive latest-open sweep: open queue checked 2026-08-17T08:3xZ; the only adjacent item is #17261 ("pr-review §7 promises a micro-path; the validator has no shape for it") — same file family, different defect: that one is about a missing shape, this one about a label pattern rejecting valid bodies. Not a duplicate.
Origin Session ID: 80b326bf-b37a-4efd-8313-1a9eae09e9c4
Retrieval Hint:
query_raw_memories("round 2 disposition carry it verbatim label column RA-1b §0 folded")