# GitHub Status: **draft**. The adapter that exports a review to one GitHub pull request and fetches what happens there. It follows the contract in [README.md](README.md) ("integrations §N" below). The review model is in [REVIEW-FORMAT.md](../REVIEW-FORMAT.md) ("format §N"). The code is in `src/main/github/`. ## 1. Destinations | | | | --- | --- | | `system` | `github` | | `host` | `github.com`, or a GitHub Enterprise Server host | | `locator` | `//pull/`: owner and name of `[A-Za-z0-9._-]`, a decimal number | | external ids | GraphQL node ids, of `[A-Za-z0-9_=-]`, at most 128 characters | | API | GraphQL at `https://api.github.com/graphql`, or `https:///api/graphql`; REST (for the pull request's files) at `https://api.github.com`, or `https:///api/v3` | | viewer | `viewer { login id }`: `viewer` is the login, `viewer_id` the node id | | declares | `export`, `fetch`, `describe`, `checks` (integrations §3) | | URL | `https://///pull/`, with anything after it (`/files`, a fragment) ignored; attaches when opened (integrations §3.1). The canonical URL has no suffix | | refs | head `refs/pull//head` → `refs/remotes//pull/` (forks' pull requests have it too); base `refs/heads/` → `refs/remotes//` | | names | GitHub · pull request · `pr` · `#` | For exporting and for Attach, the app suggests pull requests from the clone's GitHub remotes, one whose head is the subject's head first. The user picks one (integrations §3, §3.1). ## 2. Export mapping A review becomes one pull request review. | Local | GitHub | | --- | --- | | change review, `approve` / `request-changes` / `comment` | a review with event `APPROVE` / `REQUEST_CHANGES` / `COMMENT`. On the viewer's own pull request, approve and request changes post as `COMMENT` (shape): GitHub refuses them there. | | audit | a `COMMENT` review on the pull request the user picks (shape: no verdict) | | thread on lines in the diff | a review thread on those lines, `RIGHT` for head, `LEFT` for base | | thread on lines outside the diff | a file thread, with the line range in the text (shape) | | thread on a file the pull request does not change, at a commit not in the pull request, or without an anchor | part of the review body (shape, info) | | kind and severity | a bold prefix, like "Issue · major" (info) | | suggestion on head lines in the diff | a `suggestion` block; elsewhere a plain code block (shape) | | symbol | a code span, "in `orderPlan`" (info) | | evidence, when opted in | a collapsed block: command, exit code, boundary, and the last 40 lines (at most 6000 characters) of the log, in a fence | | `generated_by` | a line "_Generated · model_" at the top of the body | | coverage | not sent: GitHub's viewed marks are private to each user | | entries, private threads | not sent (integrations §1) | "The diff" is the pull request's files at planning time (REST `/repos///pulls//files`), whose patch hunks give the lines GitHub accepts comments on. A file with no patch (binary, or too large) takes only file threads. **Bodies** are capped at 60000 characters, marker included (GitHub refuses bodies over 65536). The cap is applied before rendering, never by cutting the rendered text: quoted evidence logs are shortened first, then the user's text, each at a line boundary, so that every fence and block stays closed. The plan shows each body as it will be sent, with a `shape` row for each cut. The **marker** is an HTML comment on the last line of every body the adapter sends: `` for the review, and `` for a thread's first comment. ## 3. Sending An export takes these steps (integrations §6.4). Each is recorded in `progress` with the node id it created. 1. `addPullRequestReview` with the pull request's id, `commitOID` the plan's head, and the review body: a **pending** review, which only the viewer can see. 2. `addPullRequestReviewThread` for each thread placed as its own item, on the pending review: `path`, `side`, `line` and `startLine` for lines, or `subjectType: FILE`. 3. `submitPullRequestReview` with the event. | Step | Done when | | --- | --- | | 1 | a review by the viewer exists with the marker (state `PENDING` or later) | | 2 | a thread of that review exists whose first comment has the marker | | 3 | the review's state is not `PENDING` | The head check of integrations §6.4 compares the plan's head with the pull request's `headRefOid`. GitHub allows one pending review per user and pull request. A pending review of the viewer that carries this review's marker is step 1's result, found by the check of integrations §6.5. If the viewer has any other pending review there, the export fails with `retryable = 0`, saying so: it may be the user's own work, and is left alone. Undoing (integrations §6.4) is `deletePullRequestReview` on the pending review of step 1, which deletes its threads with it. A submitted review cannot be undone. ## 4. Fetching A fetch reads (integrations §7): | GitHub | Remote entry | | --- | --- | | the pull request | the destination's `url`, `title`, `head` (`headRefOid`), and its description: `body`, `author` and `author_id` from `author`, `base_ref` (`baseRefName`), `head_ref` (`headRefName`), `created` (`createdAt`), `updated` (`lastEditedAt`, else `createdAt`) | | its state | `state`: `merged` when `merged`, `closed` for `CLOSED`, `draft` for `OPEN` with `isDraft`, else `open`; `remote_state` is `state`, with ` draft` added when `isDraft` | | each review (`reviews`), the viewer's pending review included | `kind = 'review'`, `remote_state` its `state`, `verdict` `approve` for `APPROVED`, `request-changes` for `CHANGES_REQUESTED`, `comment` for `COMMENTED`, none for `PENDING` or `DISMISSED` | | each review thread (`reviewThreads`) | `kind = 'thread'`, `parent_external_id` its first comment's review, `resolved` from `isResolved`, `resolved_by`, `outdated` from `isOutdated`, the anchor from `path`, `diffSide`, `startLine` and `line`, `anchor_commit` from its first comment's `commit` (for a `LEFT` thread too: the head it was made on, while its lines are of the base side). Its `body` is its first comment's text; the first comment is not a row of its own | | each later comment of a thread | `kind = 'comment'`, `parent_external_id` the thread | | each conversation comment (`comments`, which belong to no review or thread) | `kind = 'comment'`, `parent_external_id` `NULL` | - Pages use GraphQL cursors, 50 threads and 100 comments per page, following every `hasNextPage` including comments within a thread. - `author` is the login, `author_id` the account's node id; a deleted account (`author` null) is `ghost` with no id. `edited_by` is `editor`'s id. - Times are converted to the store's form (format §4.3): `2026-09-23T14:02:11Z` becomes `2026-09-23T14:02:11.000Z`. - URLs are kept only on the destination's host (integrations §5.3). The `jwt` parameter is removed from private attachment links (integrations §8). - The viewer's own pending review is included, since the check of integrations §6.5 depends on it. - `body` is the pull request's markdown (`body`), not GitHub's rendered HTML; the app renders it as it renders every fetched body. ### 4.1 Checks Checks come from the head commit's `statusCheckRollup.contexts` (`commits(last: 1)`, whose `oid` MUST equal `headRefOid`, else the fetch stops with `fetch_error` and is tried again next time), 100 per page, following `hasNextPage` up to the cap of integrations §5.6. | GitHub | Check | | --- | --- | | a `CheckRun` | `name` from its `checkSuite.workflowRun.workflow.name` and `name` joined by ` / ` when it has a workflow, else `name`; `url` its `url` (the run's page on GitHub, never `detailsUrl`, which may be another host); `started` and `finished` from `startedAt` and `completedAt` | | its `status` and `conclusion` | `QUEUED`, `WAITING`, `PENDING`, `REQUESTED` → `pending`; `IN_PROGRESS` → `running`; `COMPLETED` with `SUCCESS` → `passed`, `FAILURE`, `TIMED_OUT`, `STARTUP_FAILURE`, `ACTION_REQUIRED` → `failed`, `SKIPPED` → `skipped`, `CANCELLED`, `STALE` → `cancelled`, `NEUTRAL` → `neutral`; `remote_state` is `status`, and `conclusion` after a space when there is one | | a `StatusContext` (commit status) | `name` from `context`; `state` `PENDING`, `EXPECTED` → `pending`, `SUCCESS` → `passed`, `FAILURE`, `ERROR` → `failed`; `url` from `targetUrl` only when it passes the URL rule (it is usually another host, and is then `NULL`); `started` from `createdAt` | `pos` is the order GitHub returns them in. A check whose type is neither is skipped and counted in `checks_omitted` (integrations §5.6). ## 5. Credentials The token comes from the `github.auth` setting: `gh`, which runs `gh auth token --hostname `, or `keychain`, a token the user saved in the OS keychain for that host. Both follow integrations §8. `gh` is looked up on `PATH` and in the usual install directories (apps started from the Dock get a short `PATH`), never in a repository. ## 6. Failures and rate limits | Response | Kind (integrations §6.6) | | --- | --- | | network error before a response, TLS failure | not sent | | timeout or reset on a mutation, a 5xx | maybe sent | | a 403 or 429 with `retry-after`, or `x-ratelimit-remaining: 0` (pause until `x-ratelimit-reset`), a GraphQL `RATE_LIMITED` error | not sent; pauses the host | | a GraphQL error of type `NOT_FOUND` or `FORBIDDEN`, or a 401, 403 or 404 otherwise | permanent | | a GraphQL error on a mutation, such as a line outside the diff | permanent | A GraphQL response can carry `errors` with a 200 status. The adapter treats any `errors` on a mutation as a failure of that step, and on a query as a failed fetch.