Reviewing PRs
ws review takes you from “please review this” to an agent reviewing it,
in one step. Each PR gets its own worktree, a file with everything the
reviewer should read, and a tab in the PR Review workspace where your
review agent starts right away. Open a batch at once and work through them
as they finish.
This page covers opening reviews and posting them from ws, the git panel, sending reviewers’ comments to an agent, and reviewing your own agents’ work. It covers GitHub. GitLab comes next.
prefix means Ctrl-Space.
Opening a review
Section titled “Opening a review”From a shell:
ws review https://github.com/acme/storefront/pull/3345ws review billing/12 billing/14 storefront/3345From inside ws: prefix R, type one or more PRs separated by commas or
spaces (ws/12, ws/14, billing/3), and press Enter; each gets its own tab. The top
bar says fetching … while ws works in the background, then
… : review started for each tab; errors show there too. You can keep
working while it fetches.
A PR can be written these ways:
| Form | Example | Its repo |
|---|---|---|
| URL | https://github.com/acme/storefront/pull/3345 |
From the URL |
| Alias and number | billing/12, billing#12 |
The alias, from your config |
org/repo#N |
acme/storefront#3345 |
As written |
| Number | 3345, #3345 |
The repo of the current directory (for prefix R, the current tab’s), else review.default_repo |
A number with no repo to go with it is an error that lists your aliases.
What happens
Section titled “What happens”For each PR, ws:
- Finds your clone of the repo: the alias’s
path, else a template’srepoor~/code/<name>whose git remote is that repo. If there’s none and you ranws reviewin a terminal, it asks for the path and saves it as an alias in~/.config/ws/config.toml. Fromprefix Rit says what to add instead. - Makes a worktree for the PR,
~/.local/share/ws/worktrees/<repo>/review-<N>, and checks the PR out in it withgh pr checkout(forks work). Opening the same PR later reuses the worktree and pulls the PR’s new commits. Your clone’s own checkout is never touched. Git lets a branch be checked out in only one worktree, so when the PR’s branch is already checked out somewhere else (reviewing your own PR while its branch is open in your clone), the worktree gets the PR’s commit detached instead, with no branch. The review works the same; the tab just shows no PR badge. - Writes
.ws/pr.mdin the worktree: title, author, state, branches, link, size, checks and labels; the description; every conversation comment; every review with its verdict; every inline thread with its file and line, resolved or not (unresolved first); and the changed files. It’s rewritten each time the review is opened or updated..ws/is added to the repo’sinfo/exclude, so it never shows ingit statusor blocksws done. - Opens a tab in the
PR Reviewworkspace (made if missing), namedbilling#12 Title…, with the review layout. - Starts your review agent in it straight away, as a new session.
┌─ nvim: .ws/pr.md ──────────────┬─ review (claude /review-pr …) ─────┐│ # billing#12: Invoice … │ ││ ## Description │ the agent's review ││ ## Conversation (3) │ ││ ## Inline threads (2, 1 …) ├─ shell (in the worktree) ──────────┤│ ## Files changed (4) │ $ │└────────────────────────────────┴─────────────────────────────────────┘With several PRs, ws opens them one after another and switches to the first. The others’ agents run while you read; the sidebar shows which are still working and which are done.
Opening one that’s already open
Section titled “Opening one that’s already open”ws review billing/12 when its tab is open just goes to it: the agent,
worktree and .ws/pr.md are left alone.
To start over on a PR that has changed, use --update:
ws review --update billing/12It pulls the PR’s new commits, rewrites .ws/pr.md, opens a new tab with a
new agent session, and closes the old tab. Every review ws starts is a fresh
session; it never resumes an earlier review’s conversation.
After ws restart, a review tab comes back like any other, with its agent
resuming its conversation, so you don’t lose the back-and-forth you had.
The git panel
Section titled “The git panel”prefix g opens the git panel on the right side of the screen (status line
GIT). It has four tabs; Tab / Shift-Tab or 1–4 switches between
them.
Review requests lists the open PRs asking for your review, oldest first:
the repo and number, title, author, age, size (+added −deleted) and checks
(✓ passing, ✗ failing, … running). It asks every gh account you’re
logged in to on github.com and merges the results, so a work login and a
personal one both show up. A PR you already have a review tab for says
✓ open.
Reviews lists the review tabs ws has open: the review agent’s status
(reviewing, needs you, done, idle), ready to post when
.ws/review.md is waiting, and what you posted from ws (approved,
commented, changes requested).
My PRs lists your own open PRs, most recently updated first, from every
gh account: the repo and number, title (✓ tab when a workstream tab is on
its branch), the review decision (approved, changes requested,
waiting), how many review threads are unresolved, when it last changed,
and checks.
This branch is about the tab you opened the panel from: its workstream’s worktree, or the focused pane’s folder.
- Where the branch stands against its upstream:
↑2 to push,↓1 to pull,not pushed yet. - Its PR, with checks and review decision, or
no PR yet. - The files it changed since it left the default branch (
origin/main, or whateverorigin/HEADpoints at), uncommitted changes included, with+added −deletedfor each, and counts of uncommitted and untracked files.
| Keys | Tab | Action |
|---|---|---|
Enter |
Review requests | Review it: ws review <url>, in the background |
space |
Review requests | Mark it (and move down) |
R |
Review requests | Review every marked PR (or the selected one) |
Enter |
Reviews | Go to the tab |
P |
Reviews | Post the review |
u |
Reviews | Start over with a fresh review: ws review --update |
d |
Reviews | Done: close the tab and remove the worktree, after asking |
Enter |
My PRs | Go to the workstream tab on the PR’s branch |
C |
My PRs | Send its unresolved review comments to the agent in that tab |
Enter |
This branch | Review the file: its diff in the tab’s Neovim |
a |
This branch | Review every changed file (diffview.nvim if you have it) |
s |
This branch | Send your :WsComment comments to the tab’s agent |
e |
This branch | Open the file in the tab’s Neovim, without the diff |
p |
This branch | Open the branch’s PR in your browser |
c |
This branch | Create a PR: GitHub’s “Open a pull request” page for the branch, in your browser (push the branch first) |
f |
This branch | git fetch, then read the branch again |
o |
All | Open the PR in your browser |
y |
All | Copy the PR’s link |
/ |
All | Filter by repo, number, title, author or branch (files on This branch; Enter or Esc stops typing) |
r |
All | Refresh now |
j / k, g / G |
All | Move |
Esc / q |
All | Clear the filter, then close |
Pasting a PR (a URL, alias/N, several separated by commas) into the panel
reviews it, the same as prefix R. Clicking outside the panel closes it.
Review requests and your PRs come from one GitHub query per gh account,
made when the panel opens and every two minutes while it’s open, off the UI
thread, so the lists you saw last show at once while new ones load. This
branch is read from git (and gh for its PR, trying your other logins if
the active one can’t see the repo) each time the panel opens. If something
can’t be asked, the panel says why.
Posting the review
Section titled “Posting the review”When the agent is done, post its review from ws. Nothing is posted until you choose how.
- On the review tab, right-click its title and choose Post review…,
press
Pon it in the git panel’s Reviews tab, or runws review --postin any of the tab’s panes. - If there’s no
.ws/review.mdyet, ws asks the tab’s agent to write one, in the format below. When the agent has done it, choose Post review… again. - A pane opens below with the review exactly as it would be posted: the
body, each inline comment with its
file:line, and the agent’s suggested verdict. Comments on lines that aren’t in the PR’s diff can’t be inline on GitHub, so they’re moved into the body under “On lines outside this diff”. - Choose
aapprove,ccomment,rrequest changes,eto edit.ws/review.mdin your editor and see it again, orqto post nothing.
After posting, ws prints the review’s link and moves the file to
.ws/review-posted-<time>.md, so it can’t be posted twice. That file’s
header also records what you chose (posted: approve), which is what the
git panel’s Reviews tab shows.
ws review --post needs a terminal to ask you, so an agent can’t post a
review by itself. With a PR (ws review --post billing/12) it works from
anywhere; without one it uses the review worktree you’re in.
The format of .ws/review.md:
---event: comment # approve | comment | request_changes: the agent's suggestion---The overall summary, posted as the review body.
## app/models/user.rb:42An inline comment on line 42 of the new code.
## app/views/users/show.html.slim:10-14A comment on lines 10 to 14.Sending review comments to an agent
Section titled “Sending review comments to an agent”When teammates review your PR, ws pr comments hands their unresolved
comments to the agent working on it, so it can address them. Start it with:
Con the PR in the git panel’s My PRs tab (it goes to the workstream tab on the PR’s branch),- “Send review comments to the agent…” in a tab’s menu (the PR for the tab’s branch), or
ws pr comments [PR]in a pane of the agent’s tab.
ws fetches the PR’s review threads that nobody has resolved, each with its file and line (marked outdated when the code has moved since) and every comment in it, and shows you the prompt:
Address these unresolved review comments. Reply with what you changed for each.
PR: https://github.com/org/app/pull/61
1. app/models/user.rb:42 @sam: This is an N+1 inside the loop. @you: Good catch, will fix.
2. docs/setup.md (outdated: the code moved since) @ana: Say why this step is needed.Answer y and it’s typed into the tab’s agent (the focused pane if it’s an
agent, else the tab’s first agent) and sent; anything else sends nothing.
The menu and C open this in a pane below the agent, so you see it before it
goes. --print only prints the prompt, and --yes sends without asking.
It doesn’t resolve threads or reply on GitHub; that stays with you. Change
the first line with [pr_comments] prompt:
[pr_comments]prompt = "Address these unresolved review comments. Reply with what you changed for each."Reviewing your own agents’ work
Section titled “Reviewing your own agents’ work”When an agent has written code in a workstream, read its changes and tell it what to fix, without anything going to GitHub:
-
Press
prefix cin the agent’s tab. It opens the git panel’s This branch tab: the branch, its PR and checks, and every file changed since it left the default branch, uncommitted changes included. -
Enteron a file opens its diff in the tab’s Neovim (starting Neovim if the tab has none; press it again once Neovim is up): a new Neovim tab with the file on the right and the version it started from on the left.aopens every changed file instead, in diffview.nvim if you have it, else in the quickfix list with the first file’s diff (:WsDiffdiffs whichever file you’re in). -
In Neovim,
:WsCommenton a line, or on a visual selection (:'<,'>WsComment), asks for a comment and adds it to.ws/review.mdin the repo, as## path:linethen the comment, the same format as posting a PR review. Comment on the right side (the file); a comment on the left side goes on the same lines of the file. Edit.ws/review.mdby hand if you like; text above the first##is an overall note. -
:WsSendReviewin Neovim,sin the panel, orws review --sendin a pane of the tab sends them to the tab’s agent as one prompt:I reviewed your changes. Please address these comments, then say what you changed for each.1. app/models/user.rb:42This unwrap can panic.The file moves to
.ws/review-sent-<time>.md, so nothing is sent twice. The panel shows how many comments are waiting.
ws adds :WsComment, :WsSendReview and :WsDiff to the tab’s Neovim when
you open a review from the panel (it writes a small Lua file to
~/.local/share/ws/nvim/ and loads it over Neovim’s socket), so there’s
nothing to install and your Neovim config is untouched.
Finishing
Section titled “Finishing”When you’re done with a review, close its tab and remove its worktree with
ws done (from the tab), the tab menu’s “Done: close, remove worktree…”, or
d in the git panel’s Reviews tab.
The PR’s branch stays in your clone. See
Workstreams.
Configuration
Section titled “Configuration”Settings live in ~/.config/ws/config.toml, under [review]. Everything is
optional. Settings › Reviews (prefix S) edits them, repo
aliases included (a adds one, x removes one).
The review agent gets memory’s context pack
like other agents: a Codex reviewer on its command line, a Claude reviewer
with the default command through its SessionStart hook (the command
already has its own --append-system-prompt).
[review]workspace = "PR Review" # where review tabs gotemplate = "review" # the layout (see below)default_repo = "storefront" # an alias or org/repo, for bare numbers# The agent's command. Placeholders are filled in; see below.command = "claude --append-system-prompt 'Before you review, read {context}: …' '/review-pr {url}'"
[review.repos.storefront]repo = "acme/storefront"path = "~/code/storefront"
[review.repos.billing]repo = "acme/billing-ios"path = "~/code/billing-ios"-
commandis run in the review’s worktree. The default runs Claude Code with your/review-prcommand on the PR’s URL, and tells Claude (with--append-system-prompt) to read.ws/pr.mdfirst, so it takes the description, comments and reviews into account./review-pr’s arguments stay just the URL, because commands like it pass them togh. Use any agent or process:command = "codex 'Review {url}. Read {context} first; follow docs/review.md.'" -
Placeholders in
commandand in templates:{pr}(number),{url},{repo}(org/name),{alias},{title},{branch},{base},{dir}(the worktree) and{context}(the path of.ws/pr.md). Quotes,`,$and backslashes are removed from the values, so a PR title can’t break out of your command’s quoting. -
Aliases (
[review.repos.<alias>]):repoisorg/name;pathis the clone (found automatically when left out);hostis for GitHub Enterprise;gh_useris theghaccount to use for it (found automatically when left out; see below). -
default_repo: the alias ororg/repoa bare number belongs to when you’re not in a clone. -
A typo in
[review]is an error that names the file; sections ws doesn’t know yet are ignored.
The layout
Section titled “The layout”Review tabs use the review template. Without one of your own, ws uses this:
[template.review]
[[template.review.pane]]name = "nvim"cmd = "nvim --listen {nvim_socket} {context}"
[[template.review.pane]]name = "review"cmd = "{review_command}"
[[template.review.pane]]name = "shell"cmd = "$SHELL"
[template.review.layout]preset = "main-left"To change it, put a [template.review] in ~/.config/ws/templates/ (or
point review.template at another name). {review_command} is the filled-in
command. With diffview.nvim, for example:
cmd = "nvim --listen {nvim_socket} {context} +'DiffviewOpen origin/{base}...HEAD'"Several GitHub accounts
Section titled “Several GitHub accounts”If you’re logged in to more than one GitHub account with gh (a personal one
and a work one, say), your active account may not be able to see a private
org repo. GitHub then says it “could not resolve to a Repository”. ws handles
this:
-
Before fetching,
ws reviewchecks whether your activeghaccount can see the PR’s repo. If not, it tries your other logged-in accounts (gh auth status) and uses the first that can. -
The review tab’s panes act as that account too: each pane starts with that account’s token in
GH_TOKEN, read fromgh auth token --user <account>as it starts. So your review agent’s owngh pr viewworks. ws never stores the token. -
To skip the check, name the account in the repo’s alias:
[review.repos.ws]repo = "opcodezerolabs/ws"gh_user = "opcodezerodev"
If none of your accounts can see the repo, the error lists them; log in with
gh auth login.
For scripts
Section titled “For scripts”ws review prints one line per PR: its label, what happened (opened,
updated or already open) and the review agent’s pane id, separated by
tabs. It exits non-zero if any PR didn’t open, after trying the rest.
ws review --no-focus billing/12 billing/14 | while IFS=$'\t' read -r pr what pane; do echo "$pr is in pane $pane"doneThe server side is the open_review request in the protocol.
Limitations
Section titled “Limitations”- GitHub only; GitLab merge requests come later.
- Reviewing your own agents’ work compares against where the branch left
origin/main(ororigin/HEAD), so a branch with no remote default branch has nothing to compare with. ws pr commentssends inline review threads only, not the PR’s general conversation comments, and up to 100 threads of 50 comments each.- My PRs shows up to 50 PRs per account, and review threads up to 100 per PR.
- The Reviews tab shows what you posted from ws, not reviews posted on GitHub’s site. Reviews posted before this version show no state.
- ws doesn’t clone a repo for you; it asks where your clone is.
- Private data in
.ws/pr.mdis only whatghcan already read with your login.
See also
Section titled “See also”- Workstreams: worktrees and
ws done - Configuration:
config.tomland templates - Command reference