feat(review-pr): emit and validate GitHub suggestion blocks#24
Conversation
14b9447 to
1f88080
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
| } | ||
| if (close === -1) { | ||
| blocks.push({ start: i, end: lines.length - 1, closed: false }); | ||
| break; // unclosed fence swallows the remainder; nothing left to scan |
There was a problem hiding this comment.
[medium] findSuggestionBlocks stops scanning after the first unclosed block, silently dropping any closed suggestion blocks that follow it
When the body contains a closed suggestion block followed by an unclosed one (e.g. [closed][unclosed][…remaining…]), the outer loop breaks at line 261 as soon as the unclosed block is detected and sets end: lines.length - 1. This causes stripBlocks to treat everything from the unclosed opener to the end of the body as part of the unclosed block — including the content of any legitimate, properly-closed suggestion blocks that happen to come after it.
Trigger: a comment body shaped like:
```suggestion
const a = 1;
Some prose.
| break; // unclosed fence swallows the remainder; nothing left to scan | |
| const b = 2; | |
| ← no closing fence here |
The first block is valid and closed; the second is unclosed. findSuggestionBlocks records block 1 as {start:0, end:2, closed:true} and, when the inner loop finds no closer for block 2, pushes {start:4, end:N-1, closed:false} and breaks. The caller never sees block 1 as separate from block 2's range, so stripBlocks strips block 1's content as part of the unclosed range.
Impact: a valid, well-formed suggestion block after an unclosed one is silently dropped from the posted comment. The author's one-click suggestion is lost without any warning in result.issues.
Suggestion: instead of breaking out of the outer loop entirely, record the unclosed block and continue scanning from i + 1 (or from the index of the second opener when the inner loop broke on it). If "swallow to end of document" semantics are intentional, add a test for [closed][unclosed][closed] to document and assert that behaviour explicitly.
Add one-click GitHub suggestion blocks to PR reviews, plus a validator that sanitizes them before posting. GitHub rejects an entire review (HTTP 422) when any one inline suggestion anchors to an invalid line, so the validator strips malformed suggestions while keeping the prose finding, ensuring one bad suggestion cannot lose the whole review. Emission: the agent embeds a `suggestion` fenced block when a finding has an exact fix for contiguous changed lines (review-pr/agents/pr-review.yaml, refs/posting-format.md). Validation (src/validate-suggestions): parse the diff's addressable right-side lines and strip any suggestion whose anchor GitHub would reject (wrong side, out of range, invalid multi-line range, unclosed fence). Built as a standalone tsup entry and run before the gh api post. Robustness, covered by unit tests: - decode the "+++" header paths git delimits (trailing tab for names with a space, C-quoting for non-ASCII) so suggestions on those files are not dropped - require a closing fence at least as long as the opener, and treat a second opener before any closer as the first block being unclosed - fail closed when a comment has no integer line anchor - redirect the agent diff fallbacks into pr.diff so the validator reads the same diff the agent used
1f88080 to
57364f3
Compare
Resolve conflict in review-pr/agents/pr-review.yaml: keep the new suggestion-block emission instruction from this branch and adopt main's configurable inline-threshold (T) wording for the lower-confidence summary bullet (from #23).
derekmisler
left a comment
There was a problem hiding this comment.
nice work overall. the validation logic is solid and the test coverage is thorough. one non-blocking thing to consider: the node /tmp/validate-suggestions.js call in posting-format.md has no guard for when the file is absent (the staging step only emits a warning and skips the copy). if dist/validate-suggestions.js is ever missing, the node call exits non-zero and could abort the shell before the gh api post runs. an easy fix: [ -f /tmp/validate-suggestions.js ] && node /tmp/validate-suggestions.js /tmp/review_comments.json pr.diff || true. ci is green, approving!
Summary
Adds GitHub one-click suggestion blocks to PR reviews, plus a validator that sanitizes them before posting. GitHub rejects an entire review (HTTP 422) if any one inline suggestion anchors to an invalid line, so the validator strips malformed suggestions while keeping the prose finding, so one bad suggestion cannot lose the whole review.
What changed
suggestionfenced block for findings with an exact fix on contiguous changed lines (review-pr/agents/pr-review.yaml,refs/posting-format.md)src/validate-suggestionsmodule parses addressable right-side diff lines and strips suggestions GitHub would rejecttsupentry plusaction.ymlstaging; the validator runs before thegh apipost; agent diff fallbacks redirect intopr.diffRobustness (covered by unit tests)
+++header)comment.path; suggestion keptpr.diffso the validator reads the same diffValidation
validate-suggestions)tsup)biome ci,tsc --noEmitactionlintEach robustness case was additionally reproduced against the implementation and confirmed handled via an independent adversarial verification pass.