diff --git a/.github/workflows/scripts/check-review-execution.sh b/.github/workflows/scripts/check-review-execution.sh index d74c588a..f1c1889f 100755 --- a/.github/workflows/scripts/check-review-execution.sh +++ b/.github/workflows/scripts/check-review-execution.sh @@ -627,11 +627,75 @@ review_text_file="$(mktemp)" # the reader cannot see. A single-verdict transcript (the common case, and # the gha#173 wrap-up-after-verdict shape) selects exactly that one block, # unchanged. +# gha#805: the gha#710 widening has a failure mode of its own. A reviewer that +# REDRAFTS its final message -- re-running an instrument between drafts, each +# draft a complete review with its own `### Verdict` heading and +# structured-review-data block -- produced three verdict-bearing blocks, and +# the span rule concatenated all three (measured on Morrison-Lab/ai-config#2966, +# run 33594599768: three `## Review` headings, three verdicts, two +# Stopping-Point lines in one comment). The two shapes are told apart by the +# HEADING form: a complete draft carries `### Verdict` (or any `#` heading +# naming it), while the gha#710 follow-up tail writes a line-start `Verdict:` +# line (verdict-split-across-blocks.json, block 2), which $vidx matches and +# this does not. So when more than one block carries a verdict HEADING, the +# span starts at the LAST such block: the earlier drafts were superseded by +# their author, and the tail after the last draft is still kept. One heading +# (the gha#710 shape, and the common case) leaves the span rule exactly as it +# was. +# +# A heading is AUTHORED only outside a fenced code block and outside a +# blockquote. This corpus quotes the literal heading constantly -- CLAUDE.md, +# this file, every fixture, and a review OF this file -- so a later block that +# merely shows the heading shape in a fence, or blockquotes the previous +# verdict for context, must not read as a fresh draft: with the bare regex it +# did, and the span then started at the quotation and dropped the entire real +# review (gha#808 review). That is the failure gha#710 exists to prevent, and +# it is worse than the concatenation this fixes. A fence closes only on a run +# of the SAME character at least as long as the opener (CommonMark, and what +# strip-non-invoking-markup.sh implements); a first draft closed on any +# fence line, so a backtick fence containing a tilde line "closed" early and +# its heading counted (gha#808 review round 1). An unclosed fence runs to the +# end of its block, again per CommonMark: a redraft whose own code sample is +# never closed hides its own heading from this test, exactly as GitHub would +# hide it from the reader, and the block then falls back to the gha#710 span +# rule. That is malformed input rendered faithfully, not a case to special- +# case, since ignoring unclosed fences would re-admit the quoted-heading drop. +# The third literal construct, an INDENTED code block, needs no state at all: +# a heading may be indented by at most three spaces (CommonMark), so a line +# at four columns or a tab is never a heading, whether it is code or a lazy +# paragraph continuation. The heading test says {0,3} rather than [ \t]* for +# exactly that reason (gha#808 review round 3, which reproduced the drop with +# indentation instead of a fence). jq -r ' + def authored_heading: + ( split("\n") + | reduce .[] as $l ({fence: "", flen: 0, out: []}; + # Spaces only in the indentation allowance: a tab is four columns in + # CommonMark, so a tab-led fence line is indented code, not a fence + # (Copilot on gha#808). + ( [ $l | capture("^ {0,3}(?`{3,}|~{3,})(?.*)$") ] | first ) as $f + | if $f != null and .fence == "" + then .fence = ($f.run[0:1]) | .flen = ($f.run | length) + # A closer carries nothing but whitespace after its run; a run + # followed by text is fence content (CommonMark; the info-string + # form is legal only on an opener). gha#808 review round 2. + elif $f != null and ($f.run[0:1]) == .fence and ($f.run | length) >= .flen + and ($f.rest | test("^[ \\t]*$")) + then .fence = "" | .flen = 0 + elif .fence != "" or ($l | test("^[ \\t]*>")) then . + else .out += [$l] end) + | .out | join("\n") ) + # One to six hashes, then at least one space or tab: seven hashes, or + # hashes run into the word, are paragraph text in CommonMark (Copilot on + # gha#808). The awk invariant in run-fixture-tests.sh mirrors both limits. + | test("(?im)^ {0,3}#{1,6}[ \\t]+verdict\\b"); . as $blocks | [ range(0; $blocks | length) | select($blocks[.] | test("(?im)^[\\s>*_#-]*verdict\\b")) ] as $vidx - | if ($vidx | length) > 1 + | [ $vidx[] | select($blocks[.] | authored_heading) ] as $hidx + | if ($hidx | length) > 1 + then $blocks[($hidx | last):(($vidx | last) + 1)] | join("\n\n") + elif ($vidx | length) > 1 then $blocks[($vidx | first):(($vidx | last) + 1)] | join("\n\n") elif ($vidx | length) == 1 then $blocks[$vidx | first] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-redraft-after-tab-fence.json b/.github/workflows/scripts/tests/fixtures/verdict-redraft-after-tab-fence.json new file mode 100644 index 00000000..1bb4a095 --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-redraft-after-tab-fence.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe iota-pass first draft of the analysis.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe iota-pass second draft, whose tab-led backtick line is indented code and opens no fence:\n\n\t```\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.46, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-redrafted-thrice.json b/.github/workflows/scripts/tests/fixtures/verdict-redrafted-thrice.json new file mode 100644 index 00000000..e26a62fa --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-redrafted-thrice.json @@ -0,0 +1,71 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe change is sound. First draft names the alpha-pass fixture table.\n\n### Verdict\n\n**Ready for merge.**\n\n\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "Confirmed via the authoritative instrument: exit 1, five in-progress bullets, no failures. My content verdict stands unchanged." + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe change is sound. Second draft names the beta-pass fixture table.\n\n### Verdict\n\n**Ready for merge.**\n\n\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe change is sound. Third draft names the gamma-pass fixture table.\n\n### Verdict\n\n**Ready for merge.**\n\n\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "Instrument re-run after the third draft: still exit 0, the delta-pass tail is retained.\n\nVerdict: Ready for merge." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 2.34, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-then-indented-heading.json b/.github/workflows/scripts/tests/fixtures/verdict-then-indented-heading.json new file mode 100644 index 00000000..585a0752 --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-then-indented-heading.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe theta-pass analysis of the diff: sound, and the fixture discriminates.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "For reference, the heading shape as an indented code sample:\n\n ### Verdict\n\nand a paragraph continuation that is not code:\nsome prose\n ### Verdict continued on an indented line\n\nNeither is a new verdict." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.45, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-then-mismatched-fence.json b/.github/workflows/scripts/tests/fixtures/verdict-then-mismatched-fence.json new file mode 100644 index 00000000..2e3199e1 --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-then-mismatched-fence.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe zeta-pass analysis of the diff: sound, and the fixture discriminates.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "The stripper's own test table shows the shape:\n\n```\n~~~\n### Verdict\n~~~\n```\n\nThat is documentation, not a new verdict." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.43, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-then-quoted-heading.json b/.github/workflows/scripts/tests/fixtures/verdict-then-quoted-heading.json new file mode 100644 index 00000000..1b251e42 --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-then-quoted-heading.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe epsilon-pass analysis of the diff: the guard change is sound and the fixture discriminates.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "For the record, the heading shape the extractor keys on looks like this:\n\n```\n### Verdict\n```\n\nand the previous round said:\n\n> ### Verdict\n> Needs more work.\n\n### Verdicts of prior rounds\n\n####### Verdict\n\n###Verdict\n\nNeither of those is a new verdict; the one above stands." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.42, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-then-tab-inside-fence.json b/.github/workflows/scripts/tests/fixtures/verdict-then-tab-inside-fence.json new file mode 100644 index 00000000..3f3df483 --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-then-tab-inside-fence.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe kappa-pass analysis of the diff: sound, and the fixture discriminates.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "The shape, with a tab-led backtick line inside a real fence (fence content, not a closer):\n\n```\n\t```\n### Verdict\n```\n\nThat is documentation, not a new verdict." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.47, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/fixtures/verdict-then-trailing-text-closer.json b/.github/workflows/scripts/tests/fixtures/verdict-then-trailing-text-closer.json new file mode 100644 index 00000000..0b2c93cd --- /dev/null +++ b/.github/workflows/scripts/tests/fixtures/verdict-then-trailing-text-closer.json @@ -0,0 +1,38 @@ +[ + { + "type": "system", + "subtype": "init" + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "## Review\n\nThe eta-pass analysis of the diff: sound, and the fixture discriminates.\n\n### Verdict\n\n**Ready for merge.**\n" + } + ] + } + }, + { + "type": "assistant", + "message": { + "content": [ + { + "type": "text", + "text": "The shape, with a line that looks like a closer but is not:\n\n```\nfoo\n```not-a-closer\n### Verdict\n```\n\nThat is documentation, not a new verdict." + } + ] + } + }, + { + "type": "result", + "subtype": "success", + "is_error": false, + "num_turns": 9, + "duration_ms": 120000, + "total_cost_usd": 1.44, + "permission_denials_count": 0, + "permission_denials": [] + } +] diff --git a/.github/workflows/scripts/tests/run-fixture-tests.sh b/.github/workflows/scripts/tests/run-fixture-tests.sh index db3aa67b..a680c0cd 100755 --- a/.github/workflows/scripts/tests/run-fixture-tests.sh +++ b/.github/workflows/scripts/tests/run-fixture-tests.sh @@ -102,6 +102,30 @@ declare -A expected=( # must_contain needle is block A's analysis, which the pre-#710 last-block # extraction dropped. [verdict-split-across-blocks.json]=pass + # gha#805: three complete drafts, each with a `### Verdict` heading. Only + # the last is posted; the gha#710 span rule alone concatenates all three. + [verdict-redrafted-thrice.json]=pass + # gha#808 review: a later block that only QUOTES a verdict heading (in a + # fence, and in a blockquote) is not a draft; the real review before it + # must be posted, not dropped. + [verdict-then-quoted-heading.json]=pass + # gha#808 review round 1: a backtick fence containing a tilde line does not + # close on the tilde; the heading after it is still fenced. + [verdict-then-mismatched-fence.json]=pass + # gha#808 review round 2: a same-character run followed by text is fence + # content, not a closer. + [verdict-then-trailing-text-closer.json]=pass + # gha#808 review round 3: a heading quoted as INDENTED code is not a draft. + [verdict-then-indented-heading.json]=pass + # Copilot on gha#808: a tab-led backtick line is indented code, not a + # fence, so the real heading after it is authored and the last draft wins. + [verdict-redraft-after-tab-fence.json]=pass + # Copilot on gha#808: a tab-led backtick line INSIDE a real fence is + # content. A tab-admitting extractor reads it as the closer, counts the + # fenced heading as authored, and drops the real review (the must_contain + # needle); a tab-admitting awk counts two headings in the correct span. + # One fixture, both halves. + [verdict-then-tab-inside-fence.json]=pass [verdict-not-last-block.json]=pass [verdict-via-inline-comment-tool.json]=pass [verdict-via-gh-comment-heredoc.json]=pass @@ -142,6 +166,16 @@ declare -A must_contain=( # pre-#710 tail-only extraction (which drops it along with block A) and a # hypothetical first+last-only join (which keeps A but drops it). [verdict-split-across-blocks.json]='middle-pass rerun of the suite' + # Two needles in one: the third draft, and the tail AFTER it (a line-start + # Verdict: line, no heading), which must be kept -- narrowing the span end + # to the last heading block drops the tail. + [verdict-redrafted-thrice.json]='gamma-pass fixture table' + [verdict-then-quoted-heading.json]='epsilon-pass analysis' + [verdict-then-mismatched-fence.json]='zeta-pass analysis' + [verdict-then-trailing-text-closer.json]='eta-pass analysis' + [verdict-then-indented-heading.json]='theta-pass analysis' + [verdict-redraft-after-tab-fence.json]='iota-pass second draft' + [verdict-then-tab-inside-fence.json]='kappa-pass analysis' # gha#391: confirms review_text_file carries the actual posted verdict, not # just an empty/fallback string from the is_error early-fail path. [is-error-success-with-verdict.json]='Ready for merge' @@ -172,7 +206,17 @@ declare -A must_contain=( # posted verdict rather than an empty fallback from the error path. [quota-exhausted-midrun-with-verdict.json]='Ready for merge' ) +# gha#808 review: a second must-contain needle where one fixture pins two +# claims. Checked exactly like must_contain. +declare -A must_also_contain=( + [verdict-redrafted-thrice.json]='delta-pass tail is retained' +) + declare -A must_not_contain=( + # gha#805: the superseded first draft must not be posted. Its needle is + # what the pre-#805 span rule (first verdict block through last) keeps. + [verdict-redrafted-thrice.json]='alpha-pass fixture table' + [verdict-redraft-after-tab-fence.json]='iota-pass first draft' [verdict-not-last-block.json]="I've posted my findings" [verdict-via-inline-comment-tool.json]="Posted the inline finding and a summary comment ending in" [verdict-via-gh-comment-heredoc.json]='gh pr comment' @@ -263,6 +307,13 @@ declare -A expected_cost=( [genuine-finished-review.json]=0.42 [spawn-denials-only-retryable.json]=4.21 [verdict-split-across-blocks.json]=1.11 + [verdict-redrafted-thrice.json]=2.34 + [verdict-then-quoted-heading.json]=1.42 + [verdict-then-mismatched-fence.json]=1.43 + [verdict-then-trailing-text-closer.json]=1.44 + [verdict-then-indented-heading.json]=1.45 + [verdict-redraft-after-tab-fence.json]=1.46 + [verdict-then-tab-inside-fence.json]=1.47 [spawn-denials-plus-starved-calls.json]=3.9 [stub-background-agents-executed.json]=4.19 [stub-background-agents-omitted-param.json]=4.18 @@ -421,9 +472,52 @@ assert_pass() { if [[ -n "${must_contain[$fixture]:-}" ]] && ! grep -qF "${must_contain[$fixture]}" "$posted_file"; then return 1 fi + if [[ -n "${must_also_contain[$fixture]:-}" ]] && ! grep -qF "${must_also_contain[$fixture]}" "$posted_file"; then + return 1 + fi if [[ -n "${must_not_contain[$fixture]:-}" ]] && grep -qF "${must_not_contain[$fixture]}" "$posted_file"; then return 1 fi + # gha#805, as an invariant over every posted review rather than one + # fixture: a comment carries at most ONE authored verdict heading. A second + # means two complete drafts were concatenated, whichever fixture produced + # them. The gha#710 tail writes `Verdict:` without a heading, so it does + # not count; nor does a heading inside a fenced block or a blockquote, + # excluded here the same way the extractor excludes them (gha#808 review: + # the first draft of this counted them, so a correct single-draft review + # that quoted one example heading would have failed). No interval + # expression in the awk, per this repo's mawk rule. This is a shape check + # on our own extraction, not a verdict parse. + local headings + headings="$(awk ' + # A fence closes only on the same character, at least as long as the + # opener (CommonMark), mirroring the jq. No interval expressions. + # Spaces only: a tab is four columns, so a tab-led fence line is + # indented code (Copilot on gha#808). No interval expressions. + match($0, /^ ? ? ?(```+|~~~+)/) { + run = substr($0, RSTART, RLENGTH); sub(/^ +/, "", run) + ch = substr(run, 1, 1); len = length(run) + rest = substr($0, RSTART + RLENGTH) + if (fence == "") { fence = ch; flen = len; next } + if (ch == fence && len >= flen && rest ~ /^[ \t]*$/) { fence = ""; flen = 0; next } + } + fence != "" { next } + /^[ \t]*>/ { next } + # At most three leading spaces: a four-column or tab indent is indented + # code or a lazy continuation, never a heading (gha#808 review round 3). + # The trailing class is the word boundary the jq spells verdict\b, so + # a "Verdicts" heading counts in neither; at least one space after the + # hashes, and at most six of them, measured by run length rather than an + # interval expression (Copilot on gha#808). + tolower($0) ~ /^ ? ? ?#+[ \t]+verdict([^a-z0-9_]|$)/ { + hashes = $0; sub(/^ */, "", hashes); sub(/[^#].*$/, "", hashes) + if (length(hashes) <= 6) n++ + } + END { print n + 0 }' "$posted_file")" + if [[ "$headings" -gt 1 ]]; then + echo "::error::$fixture: posted review carries $headings verdict headings (gha#805)" + return 1 + fi return 0 } diff --git a/CLAUDE.md b/CLAUDE.md index 0e0700b7..51fbc7a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2001,6 +2001,78 @@ pins that ordering, and it passes with or without the fix by design -- the fixture that actually fails when the fix is removed is `quota-exhausted-midrun.json`, confirmed by mutation rather than assumed. +**A reviewer that redrafts its final message defeats the gha#710 span rule, +and the heading form is what tells the two shapes apart.** +gha#710 widened the posted text to the whole span from the first +verdict-bearing block to the last, because a review split across blocks was +being posted as its tail alone. +gha#805 is that rule's own failure: a reviewer re-ran an instrument between +drafts and produced three complete reviews in a row, each with its own +`### Verdict` heading and structured-review-data block, and the span rule +concatenated all three (Morrison-Lab/ai-config#2966, run 33594599768). +A complete draft carries a verdict *heading*; the gha#710 follow-up tail +writes a line-start `Verdict:` line, never a heading (a mid-sentence "my +verdict stands unchanged" matches neither regex, since both anchor at line +start). +So when more than one block carries an authored heading, the span starts at +the last such block and still runs to the last verdict-bearing block, which +drops the superseded drafts and keeps the tail; one heading leaves gha#710's +behaviour untouched. +**Authored means outside a fence, outside a blockquote, and outside an +indented code block.** +This corpus quotes the literal heading constantly, and a review of this very +script does too, so a later block that only shows the heading shape in a +code fence or blockquotes the previous verdict must not read as a new +draft; with the bare regex it did, and the span then started at the +quotation and dropped the entire real review, which is the failure gha#710 +exists to prevent (gha#808 review). +Both the jq detector and the bash invariant skip fenced and blockquoted +lines. +A fence closes only on the same character, at least as long as the opener, +with nothing but whitespace after it (CommonMark, as +`strip-non-invoking-markup.sh` does); +a first draft closed on any fence line, so a backtick fence holding a tilde +line leaked its heading, and a second closed on a run followed by text. +Both accept at most three leading spaces before the `#` or before a fence +run (spaces only: a tab is four columns, so a tab-led backtick line is +indented code rather than a fence), so a line at four columns or a tab is +never a heading, whether indented code or a lazy paragraph continuation; +round 3 of the same review reproduced the drop with indentation, and no +state machine is needed for it. +The awk that counts headings carries no interval expression, per this file's +mawk rule, spells the jq's word boundary as a trailing class, so a plural +"Verdicts" heading counts in neither, and mirrors the jq's one-to-six hash +limit by run length and its space-after-hashes requirement, so +`####### Verdict` and `###Verdict` count in neither. +An unclosed fence runs to the end of its block, so a redraft whose own code +sample is never closed hides its own heading and falls back to the gha#710 +span rule; that is malformed input rendered as GitHub renders it, and +ignoring unclosed fences would re-admit the quoted-heading drop. +`verdict-redrafted-thrice.json` pins the rule with a must-contain on the +third draft, a second must-contain on the tail after it, and a +must-not-contain on the first draft; +`verdict-then-quoted-heading.json` pins that a quoted heading is not a +draft, `verdict-then-mismatched-fence.json` that a tilde line does not +close a backtick fence, and `verdict-then-trailing-text-closer.json` that a +run followed by text does not close one either, and +`verdict-then-indented-heading.json` that an indented heading is not a +draft, `verdict-redraft-after-tab-fence.json` that a tab-led backtick line +opens no fence, and `verdict-then-tab-inside-fence.json` that one inside a +real fence closes none (there a tab-admitting extractor un-fences the quoted +heading and drops the real review, the same silent-drop class as the fence +and blockquote cases, and a tab-admitting awk counts two headings in the +correct span, so the one fixture pins both halves); +and `assert_pass` holds every posted review to at most one authored heading, +so a future concatenation fails on whichever fixture produces it. +Nine mutations turn a named case red: disabling the multi-heading branch, +narrowing the span end to the last heading block, dropping the +fence-and-blockquote exclusion, closing a fence on any delimiter, closing +on a run followed by text, widening the heading indent back to any +whitespace, admitting a tab into the fence indentation, lifting the +six-hash limit, and dropping the awk's trailing word-boundary class. +That count is a shape check on our own extraction, not a verdict parse: it +never reads which verdict was stated. + **`permission_denials_count` can be absent from the real execution file even though `claude-code-action` prints it to the job log, because the log line is a display value the action computes, not a field it always writes to disk.** diff --git a/changelog.d/post-last-complete-review-draft.fixed.md b/changelog.d/post-last-complete-review-draft.fixed.md new file mode 100644 index 00000000..765dbfe2 --- /dev/null +++ b/changelog.d/post-last-complete-review-draft.fixed.md @@ -0,0 +1,9 @@ +- **`claude-code-review` posts only the reviewer's last complete draft** + (#805). + A reviewer that redrafted its final message produced three complete + reviews, each with its own `### Verdict` heading, and the gha#710 span + rule concatenated all three into one comment. + When more than one block carries an authored verdict heading (outside a + code fence or blockquote, and indented at most three spaces), the posted + text now starts at the last such block; + a single heading, including gha#710's split-review shape, is unchanged.