# Post Inline Review

> Use when posting inline review comments, the per-agent summary review, or interacting with GitHub review threads (resolve, un-resolve, reply) for an F Prime PR review agent.

- Skill: `nasa/post-inline-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nasa/post-inline-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nasa/post-inline-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: nasa (https://skillmd.com/u/nasa)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nasa/post-inline-review

---


# Skill: Post an inline review on a GitHub PR

Reusable procedure for any F Prime review agent that needs to post
inline review comments, the per-agent summary review, or interact with
GitHub review threads (resolve / un-resolve / reply).

This skill assumes:

- An appropriately-permissioned token is exposed as `${TOKEN}` in the
  environment (provided by whatever external trigger invoked the
  agent). Required scopes: read access to the repository, write
  access to pull-request reviews, and write access to discussions /
  review threads for the GraphQL mutations.
- The agent knows the owner, repo, PR number, head commit SHA, and
  its own short name (matches the `agent-registry.yml` entry).

---

## 1. Suggestion-block syntax

GitHub renders a fenced block whose info string is exactly
`suggestion` as a one-click "Apply suggestion" diff against the line
range the comment is anchored to.

```markdown
[Security] **must fix** Unbounded copy from ground argument.

Validate `len` against the destination buffer before copying. The
incoming `len` is ground-controlled and can exceed `sizeof(dst)`.

```suggestion
if (len > sizeof(dst)) { return Status::INVALID_LENGTH; }
memcpy(dst, src, len);
```

<!-- fprime-agent: security-review; finding-key: abc123…; v1 -->
```

The suggestion block replaces the entire line range the comment is
anchored to. For multi-line replacements, anchor the comment to the
full range (`start_line` + `line`) rather than a single line.

---

## 2. Posting one review with many inline comments

A single PR review can carry many inline comments. Prefer one review
per agent run rather than many small reviews — the GitHub UI groups
them together.

**First run** — the metadata block (see §4) goes in `body` alongside
the inline `comments[]`:

```http
POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews
Authorization: Bearer ${TOKEN}
Accept: application/vnd.github+json
Content-Type: application/json

{
  "commit_id": "<head SHA>",
  "event": "COMMENT",
  "body": "<per-agent hidden metadata block — see §4>",
  "comments": [
    {
      "path": "Svc/CmdDispatcher/CmdDispatcher.cpp",
      "line": 142,
      "side": "RIGHT",
      "body": "[Security] **must fix** … \n\n<!-- fprime-agent: security-review; finding-key: abc; v1 -->"
    },
    {
      "path": "Svc/CmdDispatcher/CmdDispatcher.cpp",
      "start_line": 200,
      "line": 207,
      "start_side": "RIGHT",
      "side": "RIGHT",
      "body": "[Security] **suggestion** … \n\n```suggestion\n…\n```\n\n<!-- fprime-agent: security-review; finding-key: def; v1 -->"
    }
  ]
}
```

**Re-run** — posted **only if there are new inline comments**; it
has an **empty `body`** (no metadata), and the metadata review is
updated in place separately per §4:

```json
{
  "commit_id": "<head SHA>",
  "event": "COMMENT",
  "body": "",
  "comments": [ ... ]
}
```

`event: COMMENT` is correct for all **reviewer** agents — never
`APPROVE` and never `REQUEST_CHANGES` (the merge-readiness verdict
is the aggregator's job, not the individual reviewer's).

The **aggregator** (`review-summary`) uses `APPROVE` or
`REQUEST_CHANGES` based on its CI safety and merge readiness
verdicts — see review-contract.md §10.

`commit_id` MUST be the head SHA the agent analyzed; this is what
binds the comments to specific line positions. It is **not** a
record of the last reviewed head — the `reviewed_head` line in the
metadata body is (review contract §2), because a body edit leaves
`commit_id` untouched.

---

## 3. Replying to an existing review thread

Replies post to the in-line comment that started the thread (the
`in_reply_to` field).

```http
POST /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies
Authorization: Bearer ${TOKEN}
Content-Type: application/json

{
  "body": "[Security] Fixed in <commit-sha>.\n\n<!-- fprime-agent: security-review; v1; reply-kind: resolution -->"
}
```

Replies are used for:

- `[<review_label>] Fixed in <sha>.` after a clean resolution.
- The **Improperly resolved.** reply on an un-resolved thread (see
  the improper-resolution body shape in the review contract §9).
- The **Disagreement — escalating.** reply when contributor pushback
  meets the escalation criteria (review contract §11).
- The **Concur** reply a reviewer posts on another agent's thread
  that already covers the same issue at the same site-key (review
  contract §6a / §9, `reply-kind: concurrence`).
- The **Duplicate** reply the aggregator posts on a non-canonical
  duplicate thread during its de-duplication post-pass
  (review-summary.agent.md §5h, `reply-kind: duplicate-close`),
  followed by `resolveReviewThread` on that thread:

  ```
  [Summary] **Duplicate** — consolidated into <link to canonical thread>.

  <!-- fprime-review-summary; site-key: <skey>; v2; reply-kind: duplicate-close -->
  ```

---

## 4. Per-agent hidden metadata review

On first run, the metadata block lives in the `body` of the
combined review described in §2 (which also carries the inline
`comments[]` array). There is no separate metadata-only review on
first run.

On re-run, any new inline comments go in a fresh review with an
empty `body` (see §2 re-run template); if there are none, that
review is not posted. The metadata is handled separately by
**editing the prior metadata review's body in place**:

```http
PUT /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}
Authorization: Bearer ${TOKEN}
Content-Type: application/json

{ "body": "<!-- fprime-agent: security-review v1 -->\n<!-- reviewed_head: <head SHA> -->\n<!-- counts: ... -->\n..." }
```

This endpoint ("Update a review for a pull request") changes only the
summary body; the review's state, `commit_id`, and attached inline
comments are unchanged, and no notification is sent. Do **not** use
`PUT .../reviews/{review_id}/dismissals` on a metadata review: GitHub
only dismisses `APPROVED` / `CHANGES_REQUESTED` reviews and returns
`422 Can not dismiss a commented pull request review` for the
`COMMENTED` reviews reviewers post. The review body contains **only**
HTML-comment metadata (reviewed head, counts, verdict, run ordinal,
since-last-run) — no visible summary table. The HTML marker is the
de-dup key; `reviewed_head` is what tells the next run (and any
external trigger) which head this metadata describes.

If the `PUT` fails with `404`/`403` (review not editable by this
token), fall back to submitting a fresh metadata-only review
(`event: COMMENT`, no `comments[]`) and let later runs take the
newest marker match.

---

## 5. Resolving a review thread (GraphQL)

Two mutations, both keyed by the **thread ID** (NOT the comment ID).
The thread ID is fetched via a GraphQL query against
`pullRequest.reviewThreads` filtered by the comment's databaseId.

### resolveReviewThread

```graphql
mutation Resolve($threadId: ID!) {
  resolveReviewThread(input: { threadId: $threadId }) {
    thread { id isResolved }
  }
}
```

Headers:

```
Authorization: Bearer ${TOKEN}
Accept: application/vnd.github+json
```

### unresolveReviewThread

```graphql
mutation Unresolve($threadId: ID!) {
  unresolveReviewThread(input: { threadId: $threadId }) {
    thread { id isResolved }
  }
}
```

Used by the improperly-resolved row of the re-review decision table
(review contract §7, phase C).

### Fetching thread state

```graphql
query Threads($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) {
        nodes {
          id
          isResolved
          resolvedBy { login }
          comments(first: 100) {
            nodes {
              id
              databaseId
              author { login }
              body
              createdAt
            }
          }
        }
      }
      # paginate via pageInfo.endCursor / hasNextPage as needed
    }
  }
}
```

The agent uses this to:

1. Index its own prior comments by `finding-key` (parsed out of the
   HTML footer).
2. Read `isResolved` and `resolvedBy.login` (drives the
   maintainer-adjudicated vs. improperly-resolved decision — a
   core maintainer's resolution is final, `re-review-state` §3a-0).
3. Read the reply chain (drives disagreement detection in review
   contract §11).

---

## 6. Failure modes and fallbacks

| Failure | Fallback |
|---|---|
| `resolveReviewThread` returns `403` or the token lacks the discussion-write scope | Post the `[<review_label>] Fixed in <sha>.` reply and proceed. The thread visibly remains open but the audit trail is preserved; the own `Fixed in` reply makes it count as resolved in the `re-review-state` §4 recomputation. |
| `unresolveReviewThread` returns `403` | Post the improperly-resolved reply anyway. The thread remains visibly resolved on GitHub but the reply + maintainer ping is visible inline. Increment `improperly resolved` regardless. |
| Inline-comment POST returns `422 Pull Request Review thread cannot be created on this line of the diff` | The line is not in the PR's diff. Re-anchor to the nearest line that is in the diff (typically the function header) and prefix the comment body with `(Anchored above the offending line; the diff does not include line N.)` |
| `PUT .../reviews/{review_id}` (body update) returns `404`/`403` | Submit a fresh metadata-only review instead (§4). Never attempt `/dismissals` on a `COMMENTED` review. |
| Token missing entirely | Fail fast. The agent emits a single line to the orchestrator: `Cannot post review: TOKEN not provided.` and exits. The orchestrator treats this as a FAILED reviewer per review-summary.agent.md §5. |

---

## 7. Rate limits, retries, and pagination

- Treat any `5xx` response as retryable with exponential backoff
  (1s, 2s, 4s, 8s, give up).
- Treat `429` and `403` with `X-RateLimit-Remaining: 0` as backoff
  per `X-RateLimit-Reset` header.
- Treat `403` without a rate-limit header as permission failure (no
  retry).
- Do NOT retry `422` errors — they indicate a malformed request and
  retrying will produce the same error.

### Secondary rate limits — abort, never retry

`TOKEN` is shared with other services, so tripping GitHub's
secondary (abuse-detection) limit for content creation disrupts
more than this review. On a `429`, or a `403` whose body mentions
"secondary rate limit":

- Stop issuing content-creation calls (POST/PATCH to comments,
  reviews, statuses) immediately.
- Report the abort to the orchestrator as
  `FAILED: secondary rate limit`; do not retry or wait it out.
- Keep write bursts small in the first place: space
  content-creation calls out rather than firing them all at once.

### Pagination discipline

- Every list endpoint returns one page (default 30 items). Always
  request `per_page=100` and loop until a short page is returned
  (REST) or `hasNextPage` is false (GraphQL). Silent truncation
  from an unpaginated call drops findings and PRs without any
  error.
- For the Search API, verify the total number of items fetched
  equals `total_count`; on mismatch, log a warning — the search
  index may be inconsistent and results may be missing.
- The Search API has its own 30 req/min limit; pause briefly
  between search pages.

---

## 8. Worked example: the full flow on one PR

1. Read PR head SHA. Bind every subsequent call to this SHA.
2. Fetch the agent's prior metadata review by HTML marker (review
   contract §6). Note its review ID, run count, `reviewed_head`
   (fallback: `commit_id`), and the `finding-key` index.
3. Run the agent's analysis on the new head. Compute the new
   `finding-key` set; scope new below-must-fix findings to the diff
   since `reviewed_head` (`re-review-state` §2a).
4. Match prior vs current per review contract §7 phase C. Build the
   action list: `post-new`, `reply-fixed`, `resolve-thread`,
   `reply-improper`, `unresolve-thread`, `reply-disagreement`,
   `post-incorrect-fix-followup`, `do-nothing`.
5. Execute the action list. Compose the per-agent hidden metadata
   block from the resulting state.
6. POST the umbrella review (inline comments + hidden metadata body)
   or, on re-run, `PUT` the updated metadata body onto the prior
   metadata review and, only if there are new inline comments, POST
   them as one fresh empty-body review.
7. Return success to the orchestrator.

---

## External references

The endpoints and mutations referenced in this skill are documented
on the GitHub developer site. If a request behaves differently from
what this skill describes, the GitHub documentation is authoritative;
open a PR to update this skill so the next agent sees the corrected
behavior.

- GitHub REST API — Pulls: Reviews:
  https://docs.github.com/en/rest/pulls/reviews
- GitHub REST API — Pulls: Comments (inline review comments):
  https://docs.github.com/en/rest/pulls/comments
- GitHub REST API — Issues: Comments (top-level PR comments via the
  shared issue-comments endpoint):
  https://docs.github.com/en/rest/issues/comments
- GitHub GraphQL — `PullRequestReviewThread` object
  (`resolveReviewThread` / `unresolveReviewThread` mutations):
  https://docs.github.com/en/graphql/reference/objects#pullrequestreviewthread
- GitHub GraphQL — Mutations index:
  https://docs.github.com/en/graphql/reference/mutations
- GitHub REST API — Rate limiting and conditional requests:
  https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting

When the API surface evolves (endpoint paths, scope requirements,
response shapes), update the relevant section of this skill in the
same PR that addresses the change so downstream agents inherit the
new behavior automatically.

