Code review
The code review plugin puts a reviewing agent’s comments in front of you before they reach a pull or merge request. You see the diff, with each proposed comment on the line it is about, and decide which ones are worth posting. Your notes improve the ones you keep and explain the ones you reject, so the agent’s next review is better than its last.
When to use it
Section titled “When to use it”- An AI reviewer on your pull requests, whose comments you want to filter before your team sees them.
- Replies to review threads, where the agent drafts answers to your colleagues’ comments.
- Suggested changes the agent wants to commit, reviewed as a diff first.
It works with any forge. The agent maps its pull or merge request into the payload, and the decision back out; the plugin itself knows nothing about GitHub or GitLab.
Install
Section titled “Install”The plugin comes with the app. Install it in Settings › Plugins, or from the command line:
pinrail plugins install code-reviewWhat you see
Section titled “What you see”- A file tree with a count of proposals per file, in the order the agent suggests reading them or by path.
- The diff, inline or side by side, with folding and a short summary of what changed in each file.
- Proposals on their lines, each with severity, markdown body, and
suggestionblocks shown as the change they would make. Accept, reject, or add a note. - Reply threads, with the whole conversation so far.
- Your own comments, from any line of the diff.
- A confirmation before hand-over when proposals are left undecided. They are not posted, and the agent is told they were left undecided.
The layout choices are saved as plugin settings, so they hold for your next review. Press ? for the keys.
To see it before any agent asks with it, send its sample: pinrail submit code-review --sample, or Send a sample in its details in Settings › Plugins.
Asking from your agent
Section titled “Asking from your agent”## Before posting review comments
Never post review comments directly. Submit them to Pinrail with the`code-review` plugin and wait for my decision:
1. Write the payload: the change, each file's unified diff (as `git diff` prints it), and one proposal per comment, anchored on `file` and `line`. Give each proposal a stable integer `id`.2. Run: `pinrail submit code-review --title "<PR title>" --data review.json --wait`3. Post only accepted proposals. Apply an accept note as a revision before posting. Never post undecided proposals.4. Consider my line `comments` and address them in your next round.5. Learn from rejection notes: don't make the same kind of comment again.6. If the command exits 5, stop and post nothing.What the agent sends
Section titled “What the agent sends”{ "change": { "ref": "!42", "title": "Dedup tickets on save", "url": "https://gitlab.example/acme/api/-/merge_requests/42", "description": "markdown", "source": "fix/tickets", "target": "main" }, "overview": { "summary": "What changed and why.", "concerns": "What this review focused on." }, "files": [ { "path": "lib/acme/tickets.ex", "status": "modified", "summary": "Dedups before insert", "rank": 1, "diff": "@@ -140,7 +140,9 @@\n …" } ], "proposals": [ { "id": 18, "kind": "comment", "severity": "major", "title": "Reversing twice is a no-op with a cost", "body": "The second `Enum.reverse/1` undoes the first.\n\n```suggestion\n|> Enum.uniq_by(& &1.id)\n```", "file": "lib/acme/tickets.ex", "line": 149, "side": "new" } ]}diffis one unified diff per file.ranksets the reading order.linecounts onside,newby default. A proposal whose line is not in the diff shows under its file; one whose file is not in the diff shows in a section of its own.```suggestionblocks in a body render as a proposed change.- A
kind: "reply"proposal carries itsthread, so the conversation renders without the forge.
What comes back
Section titled “What comes back”{ "decisions": [ { "id": 18, "action": "accept" }, { "id": 19, "action": "reject", "note": "out of scope for this change" } ], "comments": [ { "file": "lib/acme/tickets.ex", "line": 152, "side": "new", "body": "Add a test for this." } ], "undecided": [20]}| Field | What the agent does with it |
|---|---|
decisions | Posts accepted proposals, revised by their note. Drops rejected ones, and learns from the reason. |
comments | Your own line comments, for the agent to assess and address. |
undecided | Never posted. |
Reference
Section titled “Reference”The plugin’s manifest, and the schemas a payload and a decision are checked against, read from the plugin’s own files.
name | code-review |
|---|---|
version | 1.0.0 |
title | Code review |
description | A change's diff with the comments a reviewer agent proposes to post, each accepted or rejected before anything is posted. |
use_when | You reviewed a merge or pull request and want a person to approve each comment before it is posted to the forge. |
shortcuts |
|
{ "name": "code-review", "version": "1.0.0", "title": "Code review", "description": "A change's diff with the comments a reviewer agent proposes to post, each accepted or rejected before anything is posted.", "use_when": "You reviewed a merge or pull request and want a person to approve each comment before it is posted to the forge.", "summary": { "request": { "counts": [ { "items": "/proposals", "by": "severity", "values": { "blocker": { "tone": "danger" }, "major": { "tone": "warning" }, "minor": { "tone": "info" }, "nit": { "tone": "neutral" } } } ] }, "outcome": { "counts": [ { "items": "/decisions", "by": "action", "values": { "accept": { "label": "accepted", "tone": "success" }, "reject": { "label": "rejected", "tone": "danger" } }, "other": false }, { "items": "/undecided", "label": "undecided" } ] } }, "shortcuts": [ { "keys": "j", "does": "Next undecided proposal", "group": "Move" }, { "keys": "k", "does": "Previous undecided proposal", "group": "Move" }, { "keys": "J", "does": "Next proposal", "group": "Move" }, { "keys": "K", "does": "Previous proposal", "group": "Move" }, { "keys": "a", "does": "Accept the focused proposal", "group": "Decide" }, { "keys": "x", "does": "Reject the focused proposal, with a reason", "group": "Decide" }, { "keys": "c", "does": "Write a note to the agent on the focused finding", "group": "Decide" }, { "keys": "v", "does": "Inline or side-by-side diff", "group": "View" }, { "keys": "w", "does": "Wrap long lines or scroll sideways", "group": "View" }, { "keys": "o", "does": "Semantic or alphabetical file order", "group": "View" }, { "keys": "escape", "does": "Close dialogs and composers", "group": "View" } ], "settings_schema": { "type": "object", "properties": { "diff": { "type": "string", "title": "Diff", "description": "How a file's changes are laid out", "oneOf": [ { "const": "inline", "title": "Inline" }, { "const": "split", "title": "Side by side" } ], "default": "inline" }, "wrap": { "type": "boolean", "title": "Wrap long lines", "description": "Long lines carry on underneath; off, a file scrolls sideways (inline diff only)", "default": true }, "order": { "type": "string", "title": "File order", "description": "The order the agent suggests, or by path; a change without an order is by path", "oneOf": [ { "const": "semantic", "title": "Semantic" }, { "const": "path", "title": "By path" } ], "default": "semantic" }, "findings_only": { "type": "boolean", "title": "Only files with findings", "description": "Hide files the agent had nothing to say about", "default": false }, "tree_open": { "type": "boolean", "title": "File tree open", "description": "Start with the file tree beside the diff", "default": true } } }}A change under review: its diff, the reviewer agent's overview, and the comments it proposes to post. Forge-agnostic: the requester maps its merge/pull request in, and maps the decision back out.
changeobjectrequired
- refstringrequired
the change's identifier as people write it: 42, !42, #42
- titlestringrequired
- urlstring
- descriptionstring
markdown
- sourcestring
source branch
- targetstring
target branch
overviewobject
- summarystring
what the change does and why, markdown
- concernsstring
what the review focused on, markdown
filesobject[]required
- pathstringrequired
- old_pathstring | null
- statusenum
"added""deleted""modified""renamed"· default"modified" - summarystring
a short summary of what changed in this file, in markdown; one line is best
- rankinteger
semantic reading order; omit for path order
- diffstringrequired
unified diff hunks for this file, as git diff prints them
proposalsobject[]required
- idintegerrequired
the workflow's own id; never renumbered
- kindenum
"comment""reply"· default"comment" - severityenum
"blocker""major""minor""nit"· default"minor" - titlestringrequired
- bodystringrequired
markdown; ```suggestion blocks render as a proposed change
- filestring | null
- lineinteger | null
- sideenum
which side of the diff
linecounts on"new""old"· default"new" - resolvesboolean
a reply that would resolve its thread
default
false threadobject
for replies: the conversation so far, so it renders without the forge
- roundinteger
commentsobject[]required
- authorstring
- bodystringrequired
- oursboolean
default
false
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "review payload", "description": "A change under review: its diff, the reviewer agent's overview, and the comments it proposes to post. Forge-agnostic: the requester maps its merge/pull request in, and maps the decision back out.", "type": "object", "required": [ "change", "files", "proposals" ], "additionalProperties": false, "properties": { "change": { "type": "object", "required": [ "ref", "title" ], "additionalProperties": false, "properties": { "ref": { "type": "string", "description": "the change's identifier as people write it: 42, !42, #42" }, "title": { "type": "string" }, "url": { "type": "string" }, "description": { "type": "string", "description": "markdown" }, "source": { "type": "string", "description": "source branch" }, "target": { "type": "string", "description": "target branch" } } }, "overview": { "type": "object", "additionalProperties": false, "properties": { "summary": { "type": "string", "description": "what the change does and why, markdown" }, "concerns": { "type": "string", "description": "what the review focused on, markdown" } } }, "files": { "type": "array", "items": { "type": "object", "required": [ "path", "diff" ], "additionalProperties": false, "properties": { "path": { "type": "string" }, "old_path": { "type": [ "string", "null" ] }, "status": { "enum": [ "added", "deleted", "modified", "renamed" ], "default": "modified" }, "summary": { "type": "string", "description": "a short summary of what changed in this file, in markdown; one line is best" }, "rank": { "type": "integer", "description": "semantic reading order; omit for path order" }, "diff": { "type": "string", "description": "unified diff hunks for this file, as git diff prints them" } } } }, "proposals": { "type": "array", "items": { "type": "object", "required": [ "id", "title", "body" ], "additionalProperties": false, "properties": { "id": { "type": "integer", "description": "the workflow's own id; never renumbered" }, "kind": { "enum": [ "comment", "reply" ], "default": "comment" }, "severity": { "enum": [ "blocker", "major", "minor", "nit" ], "default": "minor" }, "title": { "type": "string" }, "body": { "type": "string", "description": "markdown; ```suggestion blocks render as a proposed change" }, "file": { "type": [ "string", "null" ] }, "line": { "type": [ "integer", "null" ] }, "side": { "enum": [ "new", "old" ], "default": "new", "description": "which side of the diff `line` counts on" }, "resolves": { "type": "boolean", "default": false, "description": "a reply that would resolve its thread" }, "thread": { "type": "object", "description": "for replies: the conversation so far, so it renders without the forge", "required": [ "comments" ], "additionalProperties": false, "properties": { "round": { "type": "integer" }, "comments": { "type": "array", "items": { "type": "object", "required": [ "body" ], "additionalProperties": false, "properties": { "author": { "type": "string" }, "body": { "type": "string" }, "ours": { "type": "boolean", "default": false } } } } } } } } } }}decisionsobject[]required
- idintegerrequired
- actionenumrequired
"accept""reject" - notestring
accept: revise the comment per this before posting; reject: the reason, a lesson for the agent
commentsobject[]required
the reviewer's own line comments; handed to the agent to assess, not posted verbatim
- filestringrequired
- lineintegerrequired
- sideenumrequired
"new""old" - bodystringrequired
at least 1 chars
- undecidedinteger[]required
proposal ids left without a verdict; the requester must not post them
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "review decision", "type": "object", "required": [ "decisions", "comments", "undecided" ], "additionalProperties": false, "properties": { "decisions": { "type": "array", "items": { "type": "object", "required": [ "id", "action" ], "additionalProperties": false, "properties": { "id": { "type": "integer" }, "action": { "enum": [ "accept", "reject" ] }, "note": { "type": "string", "description": "accept: revise the comment per this before posting; reject: the reason, a lesson for the agent" } } } }, "comments": { "type": "array", "description": "the reviewer's own line comments; handed to the agent to assess, not posted verbatim", "items": { "type": "object", "required": [ "file", "line", "side", "body" ], "additionalProperties": false, "properties": { "file": { "type": "string" }, "line": { "type": "integer" }, "side": { "enum": [ "new", "old" ] }, "body": { "type": "string", "minLength": 1 } } } }, "undecided": { "type": "array", "items": { "type": "integer" }, "description": "proposal ids left without a verdict; the requester must not post them" } }}- diffstringDiff
How a file's changes are laid out
"inline"Inline ·"split"Side by side · default"inline" - wrapbooleanWrap long lines
Long lines carry on underneath; off, a file scrolls sideways (inline diff only)
default
true - orderstringFile order
The order the agent suggests, or by path; a change without an order is by path
"semantic"Semantic ·"path"By path · default"semantic" - findings_onlybooleanOnly files with findings
Hide files the agent had nothing to say about
default
false - tree_openbooleanFile tree open
Start with the file tree beside the diff
default
true
{ "type": "object", "properties": { "diff": { "type": "string", "title": "Diff", "description": "How a file's changes are laid out", "oneOf": [ { "const": "inline", "title": "Inline" }, { "const": "split", "title": "Side by side" } ], "default": "inline" }, "wrap": { "type": "boolean", "title": "Wrap long lines", "description": "Long lines carry on underneath; off, a file scrolls sideways (inline diff only)", "default": true }, "order": { "type": "string", "title": "File order", "description": "The order the agent suggests, or by path; a change without an order is by path", "oneOf": [ { "const": "semantic", "title": "Semantic" }, { "const": "path", "title": "By path" } ], "default": "semantic" }, "findings_only": { "type": "boolean", "title": "Only files with findings", "description": "Hide files the agent had nothing to say about", "default": false }, "tree_open": { "type": "boolean", "title": "File tree open", "description": "Start with the file tree beside the diff", "default": true } }}