Skip to content

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:

  1. Spawning: The client should start the codereviewserver binary as a child process.
  2. Environment: Ensure CRS_GITHUB_TOKEN is set in the environment if required.
  3. Communication: The client sends JSON-RPC requests to the server's stdin and reads responses from its stdout.
  4. Logging: The server may write logs or errors to stderr. It is recommended that clients monitor stderr for debugging and error handling.
  5. Termination: The server will terminate when its stdin is 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 simply cd <worktree_path>
  • Git Management: The server manages these using git worktree. You can see all active worktrees with git worktree list inside 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.
    ┌─ REVIEW COMMENT ─────────────────
    
  • 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.
    ┌─ REVIEW COMMENT [OUTDATED] ──────
    
  • File Comment: Indicates a comment made on the file as a whole, rather than a specific line.
    ┌─ FILE COMMENT ───────────────────
    

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: false means "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, and adjacent_number fields identify the PR whose data is returned. Clients should use these to update their navigation state rather than parsing metadata.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:

  1. Fetch PR: Call GetPR to retrieve the pull request content
  2. Add Comments: Use AddComment to add inline comments as you review
  3. Edit/Delete: Use EditComment or DeleteComment to modify pending comments
  4. Set Feedback: Optionally use SetFeedback to add a top-level review message
  5. Submit Review: Call SubmitReview with the appropriate event type to publish the review to GitHub
  6. Sync: Use SyncPR to fetch the latest state after submission
  7. Navigate: Use GetAdjacentPR to 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: GetHunkContext fetches lines around a hunk boundary when the diff's own context isn't enough to judge a change.
  • Plugin results: GetPluginOutput polls for plugin results, which arrive asynchronously; RerunPlugins forces a re-execution.
  • AI features: ListAIFeatures says which are enabled; RunAIFeature starts one for the PR and GetAIOutput polls 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
}