Found while using an agent to prepare a 9-comment review on a large PR, then correcting it after an adversarial verification pass. The agent could author review text. It could not read back what was staged, could not converge on a corrected version, and had no way to carry the review into the re-review that follows the author's next push. One claim reached the submitted review carrying text that had already been retracted, because the handoff was copy-paste.
The through-line: a review is a durable artifact with a lifecycle. The tooling models it as a single submit action.
What the workflow needs
One review object, the same shape read and written, at every stage. Whatever pr_review_submit accepts, a read tool should return: body, event, and comments with their anchors, whether the review is a pending draft, freshly submitted, or months old. Verification then becomes a diff between what was sent and what came back. Every wrong conclusion in this session traces to those two shapes not matching, which forced interpretation where comparison would have done.
One anchor vocabulary. path, line, start_line, side. position should not appear in a response at all. It was the only anchor field populated on the read side, and it is the one field that cannot be converted back to a file line, so it invites exactly the reverse-engineering that produced two false alarms here.
Declarative apply over the whole review. Hand it the final set of comments plus the body; it converges. No create-then-patch distinction visible to the caller, no change in call shape before and after submit. Idempotent, so re-running after a correction pass yields the corrected review rather than a duplicate. Deletion of comments absent from the input stays opt-in, since it is the one destructive move.
Anchor validation as a dry run. Given a path and a line range, is that anchorable against this head. Offered as a validate mode on apply, so a bad anchor is rejected before anything is created. Both false alarms in this session were an attempt to answer this question by hand from the raw diff.
Re-review as a first-class case. Each comment carries the head sha it was written against. After the author pushes, the questions are: which comments sit on lines that changed since that sha, which threads are resolved, which are outdated, which are untouched. Without this, a second pass re-reads every changed line and re-derives findings it already holds.
Threads, not just comments. Read replies, distinguish resolved from unresolved from outdated, reply into a thread, resolve one. A review is a conversation that persists across pushes; the current model is a write-once batch.
Concrete gaps behind this
These are the specific things that blocked the session. They are symptoms of the framing above, and each is independently fixable.
A pending review cannot be read back
pr_reviews maps to GET /pulls/{n}/reviews, which lists a pending review but returns body: "" for it. Its comments come only from GET /pulls/{n}/reviews/{id}/comments, and for a pending review that payload is stripped: line, start_line, side, start_side and subject_type all return null, diff_hunk is empty. Only position is populated. GET /pulls/comments/{id} returns 404 for a pending comment.
So the question "is this comment anchored where I intended" had no answer. It was settled by the user pasting a screenshot showing Comment on lines R14 to R16.
The api escape hatch cannot send a request body
GraphQL was the obvious fallback. It is unreachable. The write server's api tool builds its command at plugins/github-mcp/mcp-server-gh/lib/api.sh:75 and adds only -X <method>. No parameter maps to gh api -f / --field / --raw-field, so POST /graphql fails with A query attribute must be specified and must be a string. The same gap blocks every other POST or PATCH needing a body, which leaves the escape hatch read-only in practice whatever the method parameter says.
A fields object expanding to repeated -f key=value, plus --raw-field for values containing newlines (a GraphQL query always does), would unblock this and most of the section above.
pr_review_submit already covers the authoring half, and nothing points at it
It accepts comments[] with path, line, start_line, side, start_side, batches them into one review, and supports suggestion blocks. That is the right tool, and it went unused. The agent produced copyable comment text for a human to paste instead, which is how the stale paragraph reached the submitted review.
Nothing in the read-side tool descriptions mentions it. An agent starting from pr_view and pr_diff and working toward "help me review this" does not encounter pr_review_submit unless it already knows the name. Cross-referencing it from pr_comments, pr_reviews and pr_diff would cost nothing.
position needs a warning until it is gone
It is an index into the file's diff counted from the first @@ header, not a line number, and for a multi-line comment it does not reliably identify either end of the range. One sentence in the pr_comments and pr_reviews descriptions stating that it is diff-relative and must not be converted to a file line would have prevented both false alarms.
Where to start
The api body passthrough is the unblocker; most of the design above needs GraphQL and nothing can reach it today. The pr_review_submit cross-reference is close to free and removes the copy-paste handoff that caused the one substantive error. The position warning is a docs change. The review object and re-review support are the larger work and depend on the first item.
Found while using an agent to prepare a 9-comment review on a large PR, then correcting it after an adversarial verification pass. The agent could author review text. It could not read back what was staged, could not converge on a corrected version, and had no way to carry the review into the re-review that follows the author's next push. One claim reached the submitted review carrying text that had already been retracted, because the handoff was copy-paste.
The through-line: a review is a durable artifact with a lifecycle. The tooling models it as a single submit action.
What the workflow needs
One review object, the same shape read and written, at every stage. Whatever
pr_review_submitaccepts, a read tool should return: body, event, and comments with their anchors, whether the review is a pending draft, freshly submitted, or months old. Verification then becomes a diff between what was sent and what came back. Every wrong conclusion in this session traces to those two shapes not matching, which forced interpretation where comparison would have done.One anchor vocabulary.
path,line,start_line,side.positionshould not appear in a response at all. It was the only anchor field populated on the read side, and it is the one field that cannot be converted back to a file line, so it invites exactly the reverse-engineering that produced two false alarms here.Declarative apply over the whole review. Hand it the final set of comments plus the body; it converges. No create-then-patch distinction visible to the caller, no change in call shape before and after submit. Idempotent, so re-running after a correction pass yields the corrected review rather than a duplicate. Deletion of comments absent from the input stays opt-in, since it is the one destructive move.
Anchor validation as a dry run. Given a path and a line range, is that anchorable against this head. Offered as a validate mode on apply, so a bad anchor is rejected before anything is created. Both false alarms in this session were an attempt to answer this question by hand from the raw diff.
Re-review as a first-class case. Each comment carries the head sha it was written against. After the author pushes, the questions are: which comments sit on lines that changed since that sha, which threads are resolved, which are outdated, which are untouched. Without this, a second pass re-reads every changed line and re-derives findings it already holds.
Threads, not just comments. Read replies, distinguish resolved from unresolved from outdated, reply into a thread, resolve one. A review is a conversation that persists across pushes; the current model is a write-once batch.
Concrete gaps behind this
These are the specific things that blocked the session. They are symptoms of the framing above, and each is independently fixable.
A pending review cannot be read back
pr_reviewsmaps toGET /pulls/{n}/reviews, which lists a pending review but returnsbody: ""for it. Its comments come only fromGET /pulls/{n}/reviews/{id}/comments, and for a pending review that payload is stripped:line,start_line,side,start_sideandsubject_typeall returnnull,diff_hunkis empty. Onlypositionis populated.GET /pulls/comments/{id}returns 404 for a pending comment.So the question "is this comment anchored where I intended" had no answer. It was settled by the user pasting a screenshot showing
Comment on lines R14 to R16.The
apiescape hatch cannot send a request bodyGraphQL was the obvious fallback. It is unreachable. The write server's
apitool builds its command atplugins/github-mcp/mcp-server-gh/lib/api.sh:75and adds only-X <method>. No parameter maps togh api -f/--field/--raw-field, soPOST /graphqlfails withA query attribute must be specified and must be a string. The same gap blocks every other POST or PATCH needing a body, which leaves the escape hatch read-only in practice whatever themethodparameter says.A
fieldsobject expanding to repeated-f key=value, plus--raw-fieldfor values containing newlines (a GraphQL query always does), would unblock this and most of the section above.pr_review_submitalready covers the authoring half, and nothing points at itIt accepts
comments[]withpath,line,start_line,side,start_side, batches them into one review, and supportssuggestionblocks. That is the right tool, and it went unused. The agent produced copyable comment text for a human to paste instead, which is how the stale paragraph reached the submitted review.Nothing in the read-side tool descriptions mentions it. An agent starting from
pr_viewandpr_diffand working toward "help me review this" does not encounterpr_review_submitunless it already knows the name. Cross-referencing it frompr_comments,pr_reviewsandpr_diffwould cost nothing.positionneeds a warning until it is goneIt is an index into the file's diff counted from the first
@@header, not a line number, and for a multi-line comment it does not reliably identify either end of the range. One sentence in thepr_commentsandpr_reviewsdescriptions stating that it is diff-relative and must not be converted to a file line would have prevented both false alarms.Where to start
The
apibody passthrough is the unblocker; most of the design above needs GraphQL and nothing can reach it today. Thepr_review_submitcross-reference is close to free and removes the copy-paste handoff that caused the one substantive error. Thepositionwarning is a docs change. The review object and re-review support are the larger work and depend on the first item.