Code Review Server Protocol
This document describes the JSON-RPC API exposed by the code review server. The server communicates over stdio using the JSON-RPC 1.0 protocol, making it suitable for integration with editors like Emacs.
Transport
- Protocol: JSON-RPC 1.0
- Transport: Standard input/output (stdin/stdout)
- Encoding: JSON
All methods are exposed under the RPCHandler namespace (e.g., RPCHandler.GetPR).
Lifecycle and Process Management
Since the server communicates over stdin/stdout, the client is responsible for managing the server's lifecycle:
- Spawning: The client should start the
codereviewserverbinary as a child process. - Environment: Ensure
CRS_GITHUB_TOKENis set in the environment if required. - Communication: The client sends JSON-RPC requests to the server's
stdinand reads responses from itsstdout. - Logging: The server may write logs or errors to
stderr. It is recommended that clients monitorstderrfor debugging and error handling. - Termination: The server will terminate when its
stdinis closed or when it receives an interrupt signal (SIGINT/SIGTERM).
Methods
RPCHandler.Hello
A simple health check / test method.
Arguments (HelloArgs):
Reply (HelloReply):
| Field | Type | Description |
|-----------|--------|--------------------------------------------------|
| Count | int | Number of sections in the database |
| Content | string | Greeting message with cumulative count |
RPCHandler.GetAllReviews
Fetches all review sections from the local database, rendered as org-mode formatted text.
Arguments (GetReviewsArgs):
Reply (GetReviewsReply):
| Field | Type | Description |
|-----------|--------------|---------------------------------------------------|
| content | string | Org-mode formatted string of all review sections |
| items | []ReviewItem | Structured metadata for each PR in the list |
In content, a PR's headline carries its tags: the repository, draft or
merged, conflict when merge_conflicts is true, and the review-ease rating
when that feature has one — e.g. ** TODO Fix overflow :gadgets:conflict:hard:.
ReviewItem Object
| Field | Type | Description |
|---|---|---|
section |
string | Title of the section the PR belongs to |
section_priority |
int | Priority of the section (lower sorts first) |
status |
string | Item status (TODO, WAITING, DONE) |
tags |
string | Item tags (e.g. merged) |
title |
string | PR title |
owner |
string | Repository owner |
repo |
string | Repository name |
number |
int | Pull request number |
author |
string | PR author login |
url |
string | GitHub HTML URL |
release_status |
string | Release status from the configured release check command, if any |
review_ease |
string | How easy the PR is to review: easy, medium, or hard. Empty unless the review-ease AI feature is enabled and has rated the PR |
created_at |
Time | PR creation timestamp |
required_teams |
[]TeamReviewStatus | Teams asked to review the PR, with each one's standing. Empty until a workflow cycle has resolved them |
comment_count |
int | Conversation plus review comments on the PR, the number GitHub's pull request list shows, less the authors the review view hides. Counted from the server's comment cache, never from GitHub, so it is 0 until a workflow cycle has fetched the PR's comments |
merge_conflicts |
bool | GitHub reports that the PR's branch conflicts with its base, so it can't merge until they are resolved. Only ever true for an open or draft PR. Read from the server's cache, which each workflow cycle refreshes for the PRs it lists, so it is false until GitHub has answered for the PR's current head |
TeamReviewStatus Object
The teams a PR needs a review from, resolved by the workflow layer and served
from its cache — GetAllReviews never calls GitHub for it. The list is a
high-water mark: GitHub drops a team from a PR's requested teams as soon as one
of its members reviews, so a team that has already approved stays listed here
with approved rather than disappearing.
| Field | Type | Description |
|---|---|---|
name |
string | Team display name, or the user's own login when personal is true |
slug |
string | Team slug (empty for a personal request) |
org |
string | Organization that owns the team (empty for a personal request) |
status |
string | pending (nobody has reviewed), approved, changes_requested, or reviewed — the last meaning GitHub cleared the request but the reviewer could not be tied to the team, typically because the token cannot read the org's team membership |
mine |
bool | The current user is on this team; always true for a personal request |
personal |
bool | GitHub asked the current user directly rather than through a team |
Entries are ordered with the personal request first, then the user's own teams, then the rest alphabetically.
The PR Payload
Every method that returns a pull request's full state replies with the same shared body, described here once:
GetPR, GetAdjacentPR, SyncPR, AddComment, EditComment, DeleteComment,
SetFeedback, RemovePRComments, and SubmitReview.
Some of those methods add fields on top of it; those extras are listed with each
method below. The payload is always complete — a mutation like AddComment returns
the PR's whole refreshed state, not just the part that changed, so clients can apply
the reply wholesale instead of patching local state.
| Field | Type | Description |
|---|---|---|
okay |
bool | true if the request succeeded |
content |
string | Formatted PR response (diff, comments, metadata) |
metadata |
PRMetadata | Structured PR metadata |
diff |
string | Raw diff content |
comments |
[]CommentJSON | Structured PR active comments |
outdated_comments |
[]CommentJSON | Structured PR outdated comments |
reviews |
[]ReviewJSON | Submitted reviews |
commits |
[]CommitJSON | Commits on the PR |
feedback |
string | The locally saved review feedback draft for this PR (see SetFeedback); empty when none has been saved |
annotations |
[]PRAnnotation | Diff annotations aggregated from every plugin that has already executed successfully for this PR (see Plugins) |
images |
[]ImageJSON | Every GitHub-hosted image embedded in the description, reviews and comments, with the local file the server downloaded it to |
Slice fields are normalized before sending: a client always sees [], never null.
Image Object (ImageJSON)
Screenshots are how reviewers explain a bug, and they arrive in a body as raw
<img> tags — that is what GitHub's upload widget writes. A client cannot fetch
one itself on a private repository: the attachment URL redirects to a signed URL
that GitHub issues only to an authenticated request. The server downloads them
with its own token instead, so every client renders the same images from disk.
| Field | Type | Description |
|---|---|---|
url |
string | The image URL as it appears in the body — match on this to rewrite the tag |
path |
string | Absolute path of the downloaded file, or "" when it isn't cached yet |
content_type |
string | MIME type of the cached file; empty alongside an empty path |
size |
int64 | Size of the cached file in bytes |
cached |
bool | true when path points at a file that exists |
The download is asynchronous, so a body can be served before its images have
landed — path is empty until they do. A client that meets an empty path asks
for that one image with GetImage, which downloads it on
the spot. Images hosted anywhere other than github.com or
*.githubusercontent.com are absent from this list entirely: they need no
credentials, so clients load them directly.
PRAnnotation Object
| Field | Type | Description |
|---|---|---|
source |
string | What produced the annotation: plugin, or ai for an AI feature. A client that merges both kinds labels each by its source, so AI output never passes for a plugin's |
plugin |
string | Name of the plugin that produced the annotation; empty when source is ai |
feature |
string | ID of the AI feature that produced the annotation; omitted when source is plugin |
filename |
string | Path of the file within the repo |
line |
int | 1-based line number the annotation applies to |
severity |
string | Free-form severity, e.g. info, warning, error |
content |
string | The annotation text |
The PR payload's annotations carry plugin annotations only (source: "plugin").
AI annotations come from GetAIOutput.
PRMetadata Object
| Field | Type | Description |
|---|---|---|
number |
int | Pull request number |
title |
string | PR title |
author |
string | PR author login |
base_ref |
string | Base branch name |
head_ref |
string | Head branch name |
state |
string | PR state (open, closed, etc.) |
milestone |
string | Milestone title |
labels |
[]string | List of label names |
assignees |
[]string | List of assignee logins |
reviewers |
[]string | List of requested individual reviewers |
requested_teams |
[]string | List of requested team reviewers |
approved_by |
[]string | Logins of users who approved |
changes_requested_by |
[]string | Logins of users who requested changes |
commented_by |
[]string | Logins of users who commented |
draft |
bool | Whether the PR is a draft |
ci_status |
string | Summary of CI status |
ci_failures |
[]string | List of failed CI check names and messages |
body |
string | PR description body |
url |
string | GitHub HTML URL |
repo_path |
string | Absolute path to the local clone of the repository, when one exists under the configured RepoLocation. Empty when the repo isn't checked out locally. Distinct from worktree_path: this is the repository itself, not the PR's worktree. Clients use it to gate features that need local source, such as language-server lookups or a file browser |
worktree_path |
string | Absolute path to the local git worktree (if managed by server) |
release_status |
string | Release status from the configured release check command, if any |
review_ease |
string | How easy the PR is to review: easy, medium, or hard. Empty unless the review-ease AI feature is enabled and has rated the PR |
changed_files |
int | Number of files changed by the PR |
additions |
int | Lines added by the PR |
deletions |
int | Lines removed by the PR |
Using the Worktree
When worktree_path is provided, you can use it to quickly switch to the source code for that PR:
- Shell:
cd $(codereviewserver get-path --owner octocat --repo hello --number 42)or simplycd <worktree_path> - Git Management: The server manages these using
git worktree. You can see all active worktrees withgit worktree listinside the main repository.
Rendered Comment Format
Comments are rendered inline within the diff or at the file headers. They use a boxed format with special headers to indicate their type:
- Regular Review Comment: Indicates a comment on a specific line in the current version of the code.
- Outdated Review Comment: Indicates a comment that was made on a previous version of the code that no longer matches the current head or position.
- File Comment: Indicates a comment made on the file as a whole, rather than a specific line.
Each comment block includes the file path, timestamp, author(s), and comment ID, followed by the conversation thread.
Comment Object (CommentJSON)
| Field | Type | Description |
|---|---|---|
id |
string | Comment ID. For a local (pending) comment this is the local ID accepted by EditComment / DeleteComment |
author |
string | GitHub login of the author, or local for a pending comment not yet submitted |
body |
string | Comment body text |
path |
string | File the comment is attached to |
position |
string | Position within the diff (not a file line number). 0 means the comment is on the file as a whole |
in_reply_to |
int64 | ID of the comment this one replies to; 0 for a thread root |
created_at |
Time | Creation timestamp |
outdated |
bool | Whether the comment refers to a version of the code the current diff no longer matches |
diff_hunk |
string | The diff context the comment was made against — the only way to show an outdated comment in place |
review_id |
int64 | The review this comment was submitted with; 0 for a standalone issue comment or a local (pending) comment |
html_url |
string | Link to the comment on GitHub; empty for local comments |
thread_id |
string | GraphQL node ID of the review thread this comment belongs to (see Review thread resolution) |
resolved |
bool | Whether the thread has been marked resolved on GitHub |
resolved_by |
string | GitHub login of whoever resolved the thread; empty when unresolved |
reactions |
[]Reaction | Emoji reactions on this comment, with the logins behind each one (see Reactions) |
Review thread resolution
thread_id, resolved, and resolved_by come from GitHub's GraphQL
reviewThreads connection, which the server fetches on the client's behalf —
clients never call GitHub directly. The REST comment endpoints do not report
resolution at all, so this is the only source for it.
Like every other part of the PR payload, it is cached in SQLite (the
PRReviewThreads table) and warmed by the background workflow cycle whenever a
PR is added to a section or its head SHA changes, so a GetPR normally answers
from cache without any GraphQL call. SyncPR forces a refetch.
Two consequences for clients:
- Resolution is a property of the thread, not the comment. Every comment in
a resolved thread carries
resolved: true, including replies. Reading it off the thread root is enough. resolved: falsemeans "not resolved or not known". If the GraphQL fetch is skipped or fails, the server logs it, leaves all three fields zeroed, and still returns the PR. Render the absence as "no information", not as an explicit "unresolved" — the web client does this by only ever showing a Resolved badge and never an Unresolved one.
outdated and resolved are independent, and frequently both true: a
conversation gets resolved, then later pushes rewrite the lines it pointed at.
A client that presents them as one status, or that sums the two counts, will
double-count those threads. When the thread data is available, outdated
reflects GitHub's own judgement for the whole thread rather than a single
comment's position mapping, and it also decides whether a comment lands in
comments or in outdated_comments.
Reactions
| Field | Type | Description |
|---|---|---|
content |
string | REST-style emoji name: +1, -1, laugh, hooray, confused, heart, rocket, eyes |
emoji |
string | The emoji itself (👍), so clients don't each carry the mapping; empty for a content value the server doesn't recognise |
users |
[]string | Logins that left this reaction, up to 20 |
count |
int | GitHub's own total; count > len(users) means the login list was truncated |
viewer_reacted |
bool | Whether the account behind CRS_GITHUB_TOKEN is one of the reactors |
Reactions are carried by comments (comments, outdated_comments) and by
reviews, and they answer a question a reply count cannot: whether anyone
acknowledged a comment at all. REST reports only per-emoji totals, and who
reacted is a separate REST call per comment, so the server reads them from
GraphQL's reactionGroups in one call per PR — again, clients never call
GitHub directly.
They are cached in SQLite (the PRReactions table) separately from the comment
and review caches, because a reaction lands without changing either. The
workflow cycle warms them when a PR is added or its head SHA changes, and
refreshes them whenever it refetches a PR's comments; SyncPR forces a
refetch. A comment nobody has reacted to carries null (the field is always
present but not normalised to []), and a failed or skipped fetch leaves every
comment and review that way — so treat null-or-empty as "nothing to show"
rather than as a positive statement about anything.
Review Object
Represents a submitted review (e.g. APPROVED, CHANGES_REQUESTED).
| Field | Type | Description |
|---|---|---|
id |
int64 | Review ID |
user |
string | GitHub login of the reviewer |
body |
string | Main body text of the review |
state |
string | Review state (APPROVED, CHANGES_REQUESTED, etc.) |
submitted_at |
Time | Timestamp when the review was submitted |
html_url |
string | Link to the review on GitHub |
reactions |
[]Reaction | Emoji reactions on the review's own body (see Reactions) |
Commit Object (CommitJSON)
| Field | Type | Description |
|---|---|---|
sha |
string | Commit SHA |
message |
string | Commit message |
author |
string | Commit author |
date |
string | Commit date |
url |
string | Link to the commit on GitHub |
RPCHandler.GetPR
Fetches a pull request from GitHub and returns it as rendered content (including diff, comments, conversations). Cached data is used where available; see SyncPR for a forced refresh.
This is a read-only query as far as the caller is concerned: it never blocks on plugin execution or LLM analysis. Those are dispatched in the background, and their results arrive through GetPluginOutput (or on a later GetPR's annotations).
Arguments (GetPRstructArgs):
| Field | Type | Required | Description |
|-------------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner (e.g., "octocat") |
| Repo | string | Yes | Repository name (e.g., "hello") |
| Number | int | Yes | Pull request number |
| SkipCache | bool | No | If true, bypass the DB caches and fetch from GitHub |
Reply (GetPRReply): the PR payload, with no extra fields.
RPCHandler.GetAdjacentPR
Fetches the next or previous pull request relative to the given PR in the sorted review list (same ordering as GetAllReviews: by status, then repo, then number). Navigation wraps around — calling with Previous: false on the last PR returns the first, and calling with Previous: true on the first PR returns the last.
Arguments (GetAdjacentPRArgs):
| Field | Type | Required | Description |
|------------|--------|----------|----------------------------------------------------------|
| Owner | string | Yes | Repository owner of the current PR |
| Repo | string | Yes | Repository name of the current PR |
| Number | int | Yes | Pull request number of the current PR |
| SkipCache| bool | No | If true, bypass cached data for the adjacent PR |
| Previous | bool | No | If true, return the previous PR; if false (default), return the next PR |
Reply (GetAdjacentPRReply): the PR payload, plus:
| Field | Type | Description |
|---|---|---|
adjacent_owner |
string | Owner of the adjacent PR |
adjacent_repo |
string | Repo name of the adjacent PR |
adjacent_number |
int | PR number of the adjacent PR |
Note: The
adjacent_owner,adjacent_repo, andadjacent_numberfields identify the PR whose data is returned. Clients should use these to update their navigation state rather than parsingmetadata.url.
Example — advance to the next PR from PR #42:
{
"method": "RPCHandler.GetAdjacentPR",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42, "Previous": false}],
"id": 2
}
Response:
{
"result": {
"okay": true,
"adjacent_owner": "octocat",
"adjacent_repo": "Hello-World",
"adjacent_number": 43,
"content": "... formatted PR content ...",
"metadata": { "number": 43, "title": "Next PR", ... },
"diff": "...",
"comments": [],
"outdated_comments": [],
"reviews": [],
"commits": [],
"feedback": "",
"annotations": []
},
"error": null,
"id": 2
}
RPCHandler.SyncPR
Forces a fresh fetch of the pull request from GitHub, bypassing any cache.
Arguments (SyncPRArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
Reply (SyncPRReply): the PR payload, freshly fetched, plus:
| Field | Type | Description |
|---|---|---|
updated |
bool | true if the sync pulled in a new head SHA or new comments compared to the previously cached state |
RPCHandler.GetHunkContext
Returns extra context lines immediately before or after a hunk boundary, so a client can expand the visible context around a diff without fetching the whole file.
The lines come back as plain text, without the leading space that marks a context line in a unified diff — a client splicing them into a diff adds that itself. They are not part of the PR's canonical diff, so they must not consume a comment position: counting them would shift the position of every line below the expansion, and comments created while expanded would be stored against a position that no longer exists once the canonical diff is restored.
Arguments (GetHunkContextArgs):
| Field | Type | Required | Description |
|--------------|--------|----------|-----------------------------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Filename | string | Yes | Path of the file within the repo |
| Side | string | Yes | Which version of the file to read from: "old" or "new" |
| AnchorLine | int | Yes | 1-based line number in the file (not a diff position) to expand from. With Direction: "before", lines above it are returned; with "after", lines below it |
| Direction | string | Yes | "before" or "after" |
| Count | int | No | Number of extra lines to fetch. Defaults to 20, capped at 100 |
| OrigStart | int | Yes | Current hunk's @@ -OrigStart,OrigLength |
| OrigLength | int | Yes | Current hunk's old-side length |
| NewStart | int | Yes | Current hunk's @@ +NewStart,NewLength |
| NewLength | int | Yes | Current hunk's new-side length |
| HunkHeader | string | No | Text after the @@ in the current hunk header (e.g. the enclosing function), preserved in the rewritten header |
The four range fields and HunkHeader describe the hunk as it currently stands; the server uses them to compute the header the hunk should carry once expanded.
Reply (GetHunkContextReply):
| Field | Type | Description |
|----------------|----------|--------------------------------------------------------------------|
| lines | []string | The extra context lines |
| start_line | int | 1-based line number of the first returned line |
| end_line | int | 1-based line number of the last returned line |
| range_header | string | Updated @@ -a,b +c,d @@ header for the expanded hunk; a client replaces the existing hunk header with this |
Direction and Side are validated, and AnchorLine must be at least 1 — anything else is returned as an RPC error.
Example Request (20 lines above a hunk starting at line 42 of the head file):
{
"method": "RPCHandler.GetHunkContext",
"params": [{
"Owner": "octocat", "Repo": "Hello-World", "Number": 42,
"Filename": "server/server.go",
"Side": "new", "AnchorLine": 42, "Direction": "before", "Count": 20,
"OrigStart": 40, "OrigLength": 6, "NewStart": 42, "NewLength": 8,
"HunkHeader": "func (h *RPCHandler) GetPR("
}],
"id": 9
}
RPCHandler.GetImage
Returns the local file holding an image embedded in a PR's description, reviews or comments, downloading it first if it isn't cached yet.
This is the fallback behind the payload's images
list. Most images are already on disk by the time a client asks — the server
downloads them whenever it loads a PR's discussion — so this normally costs a
stat. A comment posted since the last fetch is the case that goes to GitHub,
and this call blocks for it, unlike the rest of the read path.
Args (GetImageArgs):
| Field | Type | Required | Description |
|-------|--------|----------|----------------------------------------------------|
| URL | string | Yes | The image URL exactly as it appears in the body |
Reply (GetImageReply):
| Field | Type | Description |
|----------------|--------|--------------------------------------------------------------------|
| okay | bool | true when the image is on disk |
| url | string | The URL that was resolved |
| path | string | Absolute path of the cached file |
| content_type | string | MIME type of the cached file |
| size | int64 | Size of the cached file in bytes |
| cached | bool | false when this call had to download the image |
| error | string | Why it could not be served, when okay is false |
A URL that isn't on github.com or *.githubusercontent.com comes back with
okay: false rather than as an RPC error — the client should load it directly,
since images elsewhere need no credentials. The server only fetches GitHub hosts
on purpose: bodies are written by whoever commented on the PR, and following
every URL in one would make the review server issue requests wherever a
commenter pointed it. Redirects are re-checked against the same allowlist,
responses that aren't images are refused, and downloads are capped at 25 MB.
Cached files live in $CRS_HOME/images, named by the SHA-256 of their URL, and
are safe to delete — they are re-downloaded on demand.
Example Request:
{
"method": "RPCHandler.GetImage",
"params": [{ "URL": "https://github.com/user-attachments/assets/9b506449-0b9b-4c14-90f7-775beb9e68af" }],
"id": 11
}
Example Reply:
{
"okay": true,
"url": "https://github.com/user-attachments/assets/9b506449-0b9b-4c14-90f7-775beb9e68af",
"path": "/home/you/.crs/images/c33e7f4874...458b.png",
"content_type": "image/png",
"size": 26186,
"cached": true,
"error": ""
}
RPCHandler.AddComment
Adds a new local (pending) comment to a pull request. The comment is stored locally until the review is submitted.
Arguments (AddCommentArgs):
| Field | Type | Required | Description |
|-------------|---------|----------|----------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Filename | string | Yes | Path to the file being commented on |
| Position | int64 | Yes | Line position in the diff |
| Body | string | Yes | Comment body text |
| ReplyToID | *int64 | No | If replying to an existing comment, the comment ID |
Reply (AddCommentReply): the PR payload, plus:
| Field | Type | Description |
|---|---|---|
id |
int64 | Local ID of the newly created comment |
RPCHandler.EditComment
Edits an existing local (pending) comment.
Arguments (EditCommentArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| ID | int64 | Yes | Local comment ID to edit |
| Body | string | Yes | New body text for the comment |
Reply (EditCommentReply): the PR payload, with no extra fields.
RPCHandler.DeleteComment
Deletes a local (pending) comment.
Arguments (DeleteCommentArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| ID | int64 | Yes | Local comment ID to delete |
Reply (DeleteCommentReply): the PR payload, with no extra fields.
RPCHandler.SetFeedback
Sets the top-level feedback/review body for a pull request.
Arguments (SetFeedbackArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Body | string | Yes | Feedback/review body text |
Reply (SetFeedbackReply): the PR payload — whose feedback field carries the body just saved — plus:
| Field | Type | Description |
|---|---|---|
id |
int64 | ID of the feedback entry |
RPCHandler.RemovePRComments
Removes all local (pending) comments for a specific pull request.
Arguments (RemovePRCommentsArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
Reply (RemovePRCommentsReply): the PR payload, with no extra fields.
RPCHandler.SubmitReview
Submits a review to GitHub. This will: 1. Fetch all local pending comments for the PR 2. Submit reply comments individually to maintain threading 3. Submit top-level comments as part of a GitHub review 4. Delete all local comments after successful submission 5. Re-evaluate the PR against every workflow that targets its repo, updating the PR's membership in those workflows' sections immediately rather than at the next workflow cycle. A PR that no longer matches a section's filters (e.g. a "needs review" section, now that the review request is gone) drops out of it; one that now matches a section it wasn't in is added. Reprocessing failures are logged, not returned — the review itself has already been submitted.
Arguments (SubmitReviewArgs):
| Field | Type | Required | Description |
|----------|--------|----------|----------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Event | string | Yes | Review event type: APPROVE, REQUEST_CHANGES, or COMMENT |
| Body | string | No | Top-level review body (optional) |
Reply (SubmitReviewReply): the PR payload, with no extra fields.
RPCHandler.MergePR
Asks GitHub to merge the given pull request. The merge method defaults to squash when MergeMethod is omitted or empty; callers may override it with any value GitHub accepts ("merge", "squash", or "rebase").
The server passes through GitHub's response verbatim — clients should rely on the merged and message fields to determine whether the merge succeeded rather than inferring success from the absence of an error. A non-error response with merged: false (e.g. due to a protected branch, failing checks, or a dirty mergeable state) is still a meaningful answer from GitHub.
Arguments (MergePRArgs):
| Field | Type | Required | Description |
|---------------|--------|----------|-----------------------------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| MergeMethod | string | No | One of "merge", "squash", or "rebase". Defaults to "squash" if empty |
Reply (MergePRReply):
| Field | Type | Description |
|-----------|--------|--------------------------------------------------------------------|
| merged | bool | true if GitHub reports the PR was successfully merged |
| sha | string | SHA of the merge commit produced by GitHub (empty if not merged) |
| message | string | Human-readable message returned by GitHub describing the outcome |
Example Request (squash merge — default):
{
"method": "RPCHandler.MergePR",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42}],
"id": 7
}
Example Request (rebase merge):
{
"method": "RPCHandler.MergePR",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42, "MergeMethod": "rebase"}],
"id": 7
}
Example Response:
{
"result": {
"merged": true,
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"message": "Pull Request successfully merged"
},
"error": null,
"id": 7
}
RPCHandler.ListPlugins
Lists all installed and configured plugins.
Arguments (ListPluginsArgs):
Reply (ListPluginsReply):
| Field | Type | Description |
|-----------|------------|------------------------------------|
| plugins | []Plugin | List of configured plugin objects |
Plugin Object
| Field | Type | Description |
|---|---|---|
Name |
string | Human-readable name of the plugin |
Command |
string | Command or path to the plugin binary |
IncludeDiff |
bool | Whether the plugin receives the PR diff |
IncludeHeaders |
bool | Whether the plugin receives the PR metadata (headers) |
IncludeComments |
bool | Whether the plugin receives the PR comments |
RPCHandler.GetPluginOutput
Retrieves the output and status of all plugins for a specific pull request.
Arguments (GetPluginOutputArgs):
| Field | Type | Required | Description |
|----------|--------|----------|--------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
Reply (GetPluginOutputReply):
| Field | Type | Description |
|----------|-----------------------------|--------------------------------------------------------|
| output | map[string]PluginOutput | Map of plugin names to their respective results/status |
PluginOutput Object
| Field | Type | Description |
|---|---|---|
result |
string | The raw captured stdout of the plugin, unchanged (kept for older clients) |
status |
string | Execution status: pending, success, error, or deferred |
body |
PluginBody | The plugin's output parsed against the response contract; non-conforming output is wrapped as a markdown body holding the raw output |
annotations |
[]Annotation | Line-level diff annotations declared by the plugin (empty for legacy output) |
PluginBody Object
| Field | Type | Description |
|---|---|---|
body_type |
string | Either markdown or html |
body_content |
string | The renderable output of the plugin |
Annotation Object
| Field | Type | Description |
|---|---|---|
filename |
string | Path of the file within the repo |
line |
int | 1-based line number the annotation applies to |
severity |
string | Free-form severity, e.g. info, warning, error |
content |
string | The annotation text |
RPCHandler.RerunPlugins
Clears cached plugin results for a pull request and runs the plugins again, bypassing the head-SHA cache check that normally makes a plugin skip a PR it has already seen.
Execution happens in the background: the reply means the rerun was accepted, not that it finished. Poll GetPluginOutput for results — a plugin that is still running reports pending.
Arguments (RerunPluginsArgs):
| Field | Type | Required | Description |
|-----------|----------|----------|------------------------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Plugins | []string | No | Names of specific plugins to rerun. Omitted or empty reruns all of them |
Reply (RerunPluginsReply):
| Field | Type | Description |
|-----------|-------------------------|-----------------------------------------------------------|
| okay | bool | true if the rerun was accepted |
| message | string | Human-readable status |
| output | map[string]PluginOutput | Plugin results as they stood when the rerun was dispatched; see GetPluginOutput |
Because dispatch is asynchronous, output will generally still show the cleared or previous state. Treat GetPluginOutput as the source of truth once the rerun has been accepted.
Example Request (rerun a single plugin):
{
"method": "RPCHandler.RerunPlugins",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42, "Plugins": ["security_check"]}],
"id": 10
}
RPCHandler.ListAIFeatures
Lists every registered AI feature, enabled or not, as the server's config currently sets it up. A client shows a way to open each enabled one that isn't applied.
Arguments (ListAIFeaturesArgs):
Reply (ListAIFeaturesReply):
| Field | Type | Description |
|------------|-------------|--------------------------------------|
| features | []AIFeature | Every registered feature |
AIFeature Object
| Field | Type | Description |
|---|---|---|
id |
string | Stable identifier, e.g. comments-addressed, feature-flags or change-diagram |
name |
string | Label to show, e.g. Comments addressed? |
description |
string | One-paragraph explanation |
enabled |
bool | Whether an [[AIFeatures]] entry switches it on |
automatic |
bool | Whether it also runs after a PR is fetched or updated (implies enabled) |
mode |
string | Configured execution mode: oneshot or agent |
modes |
[]string | Modes the feature supports, default first |
provider |
string | Provider it would run with (gemini, openrouter or command), resolved from config in this order but not checked |
applied |
bool | The server applies the feature's result to what it already serves — file-ordering orders the diff GetPR returns, review-ease fills review_ease — so there is no report to open. Opening a PR asks for each enabled one itself |
RPCHandler.RunAIFeature
Asks for an AI feature to run for a pull request and returns at once; the run happens in the background, like RerunPlugins. Poll GetAIOutput while the output's status is pending.
Unless Force is set, a stored result that already covers the PR's current inputs — the same head SHA and the same inputs digest, or just the same head SHA for a feature that reads only the code — answers without a run. Only one run per PR and feature is ever in flight.
Arguments (RunAIFeatureArgs):
| Field | Type | Required | Description |
|-----------|--------|----------|-----------------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Feature | string | Yes | Feature ID |
| Force | bool | No | Run even when the stored result covers the PR's current inputs |
Reply (RunAIFeatureReply):
| Field | Type | Description |
|-----------|-----------------|--------------------------------------------------------------------------|
| okay | bool | false when the feature can't run: unknown, or not enabled |
| outcome | string | started, up-to-date, already-running, disabled or unknown-feature |
| message | string | Human-readable status |
| output | AIFeatureOutput | The feature's output right after the dispatch (pending when a run started); null for an unknown feature |
RPCHandler.GetAIOutput
Returns stored AI feature results for a pull request. It is a pure read — it never starts a run — so clients can poll it.
Arguments (GetAIOutputArgs):
| Field | Type | Required | Description |
|-----------|--------|----------|------------------------------------------------------------------|
| Owner | string | Yes | Repository owner |
| Repo | string | Yes | Repository name |
| Number | int | Yes | Pull request number |
| Feature | string | No | One feature, enabled or not; omitted returns every enabled one. An unknown ID is an error |
Reply (GetAIOutputReply):
| Field | Type | Description |
|----------|----------------------------|------------------------------|
| output | map[string]AIFeatureOutput | Outputs keyed by feature ID |
AIFeatureOutput Object
The body and annotations follow the plugin response contract; the typed payload and the result's key sit beside them.
| Field | Type | Description |
|---|---|---|
feature |
string | Feature ID |
name |
string | Feature label |
status |
string | pending while a run is in flight (the output may still carry the previous result), not-run before the first one, otherwise the stored run's: success, error or insufficient-input |
body |
PluginBody | The result rendered as markdown; an error's body says why the run failed |
annotations |
[]PRAnnotation | Diff annotations, each with source: "ai" and feature set |
report |
object | The feature's typed report, null when it has none; each feature documents its shape (comments-addressed, feature-flags, change-diagram (the raw Mermaid source, in mermaid), file-ordering, review-ease) |
outstanding |
array | For features that track it, every item still needing attention — including ones no diff line can anchor; null otherwise |
covers_sha |
string | Head SHA the stored result was computed from |
covers_digest |
string | Inputs digest the stored result was computed from; code-only for a feature that reads only the code, whose results the head SHA alone keys |
current_sha |
string | The PR's head SHA now |
current_digest |
string | The PR's inputs digest now (code-only for such a feature) |
stale |
bool | true when a stored result no longer matches the PR's SHA or digest |
truncated |
bool | true when some input was cut to fit the model's prompt |
updated_at |
string | When the stored result was written (RFC 3339); empty when there is none |
Example Request (poll one feature):
{
"method": "RPCHandler.GetAIOutput",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42, "Feature": "comments-addressed"}],
"id": 11
}
RPCHandler.GetConfig
Returns the server's configuration file, re-read from disk so the client sees edits made outside the server. The reply also carries the workflow type, filter and AI feature registries, so a client can build its pickers from what this server actually supports instead of hard-coding the lists.
Arguments (GetConfigArgs):
Reply (GetConfigReply):
| Field | Type | Description |
|------------------|--------------------|--------------------------------------------------------------------|
| okay | bool | true if the configuration was read successfully |
| message | string | Empty on success; explains the problem when the file can't be read |
| path | string | Absolute path of the TOML file being read and written |
| config | Config | The current configuration (see below) |
| using_defaults | bool | true when no file exists at path and the built-in defaults are running |
| workflow_types | []WorkflowTypeInfo | Workflow types this server can run |
| filters | []FilterInfo | Filters a workflow may use |
| ai_features | []AIFeatureTypeInfo | AI features an [[AIFeatures]] entry may name |
If the file on disk no longer parses, okay is false and config describes the configuration still running in memory.
When using_defaults is true there is no file yet; config describes the built-in defaults (see Configuration). Any UpdateConfig call writes them out along with the change, so a client can present them as an ordinary editable configuration.
Config Object
Field names match the TOML keys. See Configuration for what each one does.
| Field | Type | Description |
|---|---|---|
Repos |
[]string | Repositories in owner/repo form |
SleepDuration |
int | Minutes between workflow syncs |
JiraDomain |
string | Jira domain used by ProjectListWorkflow |
GithubUsername |
string | Login used by the "me" filters |
RepoLocation |
string | Directory holding local clones |
AutoWorktree |
bool | Whether the server manages git worktrees |
DesktopNotifications |
bool | Global desktop notification setting |
SectionPriority |
map[string]int | Section title → priority (lower sorts first) |
SectionSorting |
map[string]string | Section title → newest_first / oldest_first |
Workflows |
[]Workflow | Configured workflows |
Plugins |
[]Plugin | Configured plugins (see Plugins) |
AI |
AISettings | The [AI] table: DefaultProvider, DefaultCommand, DefaultModel |
AIFeatures |
[]AIFeature | The [[AIFeatures]] entries the file has (see below) |
Plugins, AI and AIFeatures use the TOML key names as field names: a plugin is Name, Command, IncludeDiff, IncludeHeaders, IncludeComments, IncludeBranch, OnlyOnDemand, Provider and Model, and an AI feature entry is ID, Enabled, Mode, Automatic, Provider, Command and Model (see AI Features). AIFeatures holds only the entries in the file: a feature switched on by a legacy root-level key such as ExperimentalLLMReviewEase has none, and its ai_features entry says so instead.
Workflow Object
| Field | Type | Description |
|---|---|---|
WorkflowType |
string | One of the names in workflow_types |
Name |
string | Unique workflow name; identifies which workflow owns an item |
SectionTitle |
string | Section the workflow's PRs land in |
Repos |
[]string | Repositories for this workflow; empty inherits the root-level Repos |
Repo |
string | Single repository (SingleRepoSyncReviewRequestsWorkflow) |
Owner |
string | Repository owner, for configs using the singular Owner/Repo form |
Filters |
[]string | Filter names, argument-taking ones written as FilterByLabel:bug |
Teams |
[]string | Team slugs whose review requests should match |
JiraEpic |
string | Epic key (ProjectListWorkflow) |
PRState |
string | open, closed, or all; empty means open |
IncludeDiff |
bool | Include the full diff in the section body |
GithubUsername |
string | Per-workflow username; inherits the root-level one when empty |
DesktopNotifications |
bool/null | Per-workflow override; null inherits the global setting |
WorkflowTypeInfo Object
| Field | Type | Description |
|---|---|---|
name |
string | Value to put in a workflow's WorkflowType |
description |
string | What the workflow type does |
deprecated |
bool | Whether the type still works but shouldn't be used for new configs |
deprecated_by |
string | What to use instead (deprecated types only) |
required_fields |
[]string | Type-specific fields the workflow must set |
optional_fields |
[]string | Type-specific fields the workflow may set |
Name, WorkflowType, and SectionTitle are required by every type and are not repeated in required_fields. Clients can use these two lists to decide which fields to show for the selected type.
AIFeatureTypeInfo Object
| Field | Type | Description |
|---|---|---|
id |
string | Value to put in an entry's ID |
name |
string | Label to show |
description |
string | What the feature does |
modes |
[]string | Modes an entry's Mode may name, the feature's default first |
applied |
bool | Whether the server applies the result (diff order, review ease) rather than offering a report |
legacy_key |
string | The root-level key that switches the feature on in the file, e.g. ExperimentalLLMReviewEase; omitted when none does |
legacy |
AIFeature | The entry legacy_key stands for. It applies only while AIFeatures has no entry for the feature, which would win |
Unlike ListAIFeatures, this describes what can be configured, not how the running config resolves it: a client editing the config can work out a feature's provider from the draft it holds (see Which provider runs a feature).
FilterInfo Object
| Field | Type | Description |
|---|---|---|
name |
string | Filter name as written in Filters |
description |
string | What the filter matches |
requires_arg |
bool | Whether the filter must be written as Name:argument |
arg_label |
string | What the argument means (e.g. label, username) |
RPCHandler.UpdateConfig
Writes a partial change to the configuration file and reloads the running config. Every argument is optional: a field that is omitted (or null) keeps whatever is on disk, as do settings the server doesn't model. Sending Workflows replaces the entire list, which is how a client adds, removes, or reorders entries.
The new configuration is validated before anything is written. If it fails, the file is untouched and the reply comes back with okay: false and a populated errors list — a rejected configuration is not an RPC error, so clients can attach each message to the field that caused it.
On success the file is replaced atomically and the previous contents are kept alongside it as <path>.bak. Comments and the original key ordering in the file are not preserved.
The background workflow manager re-derives its workflows from the config at the start of every sync cycle, so a saved change takes effect on the next sync rather than immediately.
Arguments (UpdateConfigArgs): any subset of the Config fields listed under GetConfig.
| Field | Type | Required | Description |
|---|---|---|---|
Repos |
[]string | No | Replaces the root-level repository list |
SleepDuration |
int | No | Minutes between syncs |
JiraDomain |
string | No | Jira domain |
GithubUsername |
string | No | GitHub login |
RepoLocation |
string | No | Local clone directory |
AutoWorktree |
bool | No | Worktree management |
DesktopNotifications |
bool | No | Global notification setting |
SectionPriority |
map[string]int | No | Replaces the section priority map |
SectionSorting |
map[string]string | No | Replaces the section sorting map |
Workflows |
[]Workflow | No | Replaces the whole workflow list |
Plugins |
[]Plugin | No | Replaces the whole plugin list; [] removes it |
AI |
AISettings | No | Replaces the [AI] table; all-empty removes it |
AIFeatures |
[]AIFeature | No | Replaces every [[AIFeatures]] entry; [] removes them |
Problems with a plugin or an AI setting are root-level (workflow is -1), and their field says which entry they belong to: Plugins[1].Model, AI.DefaultCommand, AIFeatures[0].Mode, indexed into the lists as sent. Two plugins with one name, or two AI feature entries with one ID, are reported the same way.
Reply (UpdateConfigReply):
All fields from GetConfigReply, plus:
| Field | Type | Description |
|---|---|---|
errors |
[]ValidationError | Problems that caused the update to be rejected (empty on success) |
config always describes the configuration that is actually in effect: the newly saved one after a successful update, or the unchanged one after a rejection.
ValidationError Object
| Field | Type | Description |
|---|---|---|
workflow |
int | Index into Workflows the problem belongs to, or -1 for a root setting |
field |
string | Field name the problem is about (e.g. SectionTitle, Filters) |
message |
string | Human-readable description of the problem |
Example Request (replace the workflow list):
{
"method": "RPCHandler.UpdateConfig",
"params": [{
"Workflows": [
{
"WorkflowType": "SyncReviewRequestsWorkflow",
"Name": "Team Reviews",
"SectionTitle": "Needs My Team's Review",
"Filters": ["FilterNotDraft", "FilterByLabel:bug"],
"Teams": ["my-team"]
}
]
}],
"id": 8
}
Example Response (rejected):
{
"result": {
"okay": false,
"message": "Configuration not saved: found 2 problems",
"path": "/home/user/.config/codereviewserver.toml",
"errors": [
{"workflow": 0, "field": "SectionTitle", "message": "is required"},
{"workflow": 0, "field": "Filters", "message": "FilterByLabel requires an argument (e.g. FilterByLabel:<label>)"}
],
"config": { "...": "the unchanged configuration" },
"workflow_types": [{ "...": "as in GetConfig" }],
"filters": [{ "...": "as in GetConfig" }]
},
"error": null,
"id": 8
}
RPCHandler.GetRateLimitStatus
Returns the current GitHub API rate limit status, including remaining quota, reset time, and usage metrics.
Arguments (GetRateLimitStatusArgs):
Reply (GetRateLimitStatusReply):
| Field | Type | Description |
|----------------------|--------|---------------------------------------------------------------|
| remaining | int | Number of API requests remaining in the current window |
| limit | int | Total API request limit (typically 5000 for authenticated) |
| reset_at | string | Timestamp when the rate limit resets (formatted string) |
| total_requests | int64 | Total number of API requests made since server start |
| throttled_count | int64 | Number of requests that were throttled due to low quota |
| rate_limited_count | int64 | Number of times the server hit a 429/403 rate limit response |
Example Response:
{
"remaining": 4850,
"limit": 5000,
"reset_at": "2026-02-05 20:15:32 EST",
"total_requests": 150,
"throttled_count": 0,
"rate_limited_count": 0
}
This endpoint is useful for monitoring GitHub API usage and determining if the server is approaching rate limits. The server automatically:
- Throttles requests when remaining < 100
- Blocks requests when remaining <= 10 (emergency reserve)
- Retries on 429/403 errors with exponential backoff
RPCHandler.CheckRepoExists
Checks if a repository is stored locally in the user's home directory (~/RepoName). This is useful for determining if features like LSP (which often require local source code) should be enabled.
Arguments (CheckRepoExistsArgs):
| Field | Type | Required | Description |
|--------|--------|----------|-----------------------------------|
| Repo | string | Yes | Repository name (e.g., "hello") |
Reply (CheckRepoExistsReply):
| Field | Type | Description |
|----------|--------|--------------------------------------------------|
| Exists | bool | true if the directory exists and is a directory|
| Path | string | The full absolute path to the repository |
Workflow
A typical code review workflow using this API:
- Fetch PR: Call
GetPRto retrieve the pull request content - Add Comments: Use
AddCommentto add inline comments as you review - Edit/Delete: Use
EditCommentorDeleteCommentto modify pending comments - Set Feedback: Optionally use
SetFeedbackto add a top-level review message - Submit Review: Call
SubmitReviewwith the appropriate event type to publish the review to GitHub - Sync: Use
SyncPRto fetch the latest state after submission - Navigate: Use
GetAdjacentPRto move to the next or previous PR in the review queue without returning to the list; navigation wraps around at both ends
Alongside that loop:
- Expand context:
GetHunkContextfetches lines around a hunk boundary when the diff's own context isn't enough to judge a change. - Plugin results:
GetPluginOutputpolls for plugin results, which arrive asynchronously;RerunPluginsforces a re-execution. - AI features:
ListAIFeaturessays which are enabled;RunAIFeaturestarts one for the PR andGetAIOutputpolls for its result (see AI Features).
Steps 2–5 each return the PR payload in full, so a client can apply the reply directly rather than tracking which parts of its local state a mutation invalidated.
Error Handling
Errors are returned in the standard JSON-RPC format. Common error scenarios:
- GitHub API errors (rate limiting, authentication, network issues)
- Database errors (local comment storage)
- Invalid PR references (non-existent owner/repo/number)
Example Request/Response
Request (GetPR):
{
"method": "RPCHandler.GetPR",
"params": [{"Owner": "octocat", "Repo": "Hello-World", "Number": 42}],
"id": 1
}
Response:
{
"result": {
"okay": true,
"content": "... formatted PR content ...",
"metadata": {
"number": 42,
"title": "Example PR",
"author": "octocat",
"state": "open",
"body": "PR description...",
"repo_path": "/home/user/code/Hello-World",
"worktree_path": "/home/user/code/repo_worktrees/42_branch"
},
"diff": "--- a/file.txt\n+++ b/file.txt\n...",
"comments": [
{
"id": "12345",
"author": "octocat",
"body": "Nice catch!",
"path": "file.txt",
"position": "5",
"created_at": "2023-01-01T12:00:00Z",
"outdated": false,
"review_id": 98765,
"html_url": "https://github.com/octocat/Hello-World/pull/42#discussion_r12345",
"thread_id": "PRRT_kwDOA...",
"resolved": true,
"resolved_by": "coder1",
"reactions": [
{
"content": "+1",
"emoji": "👍",
"users": ["coder1", "coder2"],
"count": 2,
"viewer_reacted": false
}
]
}
],
"outdated_comments": [],
"reviews": [
{
"id": 98765,
"user": "coder1",
"body": "Looks good!",
"state": "APPROVED",
"submitted_at": "2023-01-01T12:05:00Z",
"html_url": "https://github.com/...",
"reactions": []
}
],
"commits": [
{
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"message": "Fix the thing",
"author": "octocat",
"date": "2023-01-01T11:00:00Z",
"url": "https://github.com/..."
}
],
"feedback": "Draft review body saved locally, not yet submitted",
"annotations": [
{
"plugin": "security_check",
"filename": "file.txt",
"line": 12,
"severity": "warning",
"content": "Unvalidated input reaches the query"
}
]
},
"error": null,
"id": 1
}