Skip to content

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.

From a shell:

Terminal window
ws review https://github.com/acme/storefront/pull/3345
ws review billing/12 billing/14 storefront/3345

From 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.

For each PR, ws:

  1. Finds your clone of the repo: the alias’s path, else a template’s repo or ~/code/<name> whose git remote is that repo. If there’s none and you ran ws review in a terminal, it asks for the path and saves it as an alias in ~/.config/ws/config.toml. From prefix R it says what to add instead.
  2. Makes a worktree for the PR, ~/.local/share/ws/worktrees/<repo>/review-<N>, and checks the PR out in it with gh 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.
  3. Writes .ws/pr.md in 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’s info/exclude, so it never shows in git status or blocks ws done.
  4. Opens a tab in the PR Review workspace (made if missing), named billing#12 Title…, with the review layout.
  5. 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.

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:

Terminal window
ws review --update billing/12

It 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.

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 whatever origin/HEAD points at), uncommitted changes included, with +added −deleted for 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.

When the agent is done, post its review from ws. Nothing is posted until you choose how.

  1. On the review tab, right-click its title and choose Post review…, press P on it in the git panel’s Reviews tab, or run ws review --post in any of the tab’s panes.
  2. If there’s no .ws/review.md yet, ws asks the tab’s agent to write one, in the format below. When the agent has done it, choose Post review… again.
  3. 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”.
  4. Choose a approve, c comment, r request changes, e to edit .ws/review.md in your editor and see it again, or q to 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:42
An inline comment on line 42 of the new code.
## app/views/users/show.html.slim:10-14
A comment on lines 10 to 14.

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:

  • C on 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."

When an agent has written code in a workstream, read its changes and tell it what to fix, without anything going to GitHub:

  1. Press prefix c in 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.

  2. Enter on 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. a opens every changed file instead, in diffview.nvim if you have it, else in the quickfix list with the first file’s diff (:WsDiff diffs whichever file you’re in).

  3. In Neovim, :WsComment on a line, or on a visual selection (:'<,'>WsComment), asks for a comment and adds it to .ws/review.md in the repo, as ## path:line then 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.md by hand if you like; text above the first ## is an overall note.

  4. :WsSendReview in Neovim, s in the panel, or ws review --send in 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:42
    This 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.

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.

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 go
template = "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"
  • command is run in the review’s worktree. The default runs Claude Code with your /review-pr command on the PR’s URL, and tells Claude (with --append-system-prompt) to read .ws/pr.md first, so it takes the description, comments and reviews into account. /review-pr’s arguments stay just the URL, because commands like it pass them to gh. Use any agent or process:

    command = "codex 'Review {url}. Read {context} first; follow docs/review.md.'"
  • Placeholders in command and 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>]): repo is org/name; path is the clone (found automatically when left out); host is for GitHub Enterprise; gh_user is the gh account to use for it (found automatically when left out; see below).

  • default_repo: the alias or org/repo a 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.

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'"

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 review checks whether your active gh account 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 from gh auth token --user <account> as it starts. So your review agent’s own gh pr view works. 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.

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.

Terminal window
ws review --no-focus billing/12 billing/14 | while IFS=$'\t' read -r pr what pane; do
echo "$pr is in pane $pane"
done

The server side is the open_review request in the protocol.

  • GitHub only; GitLab merge requests come later.
  • Reviewing your own agents’ work compares against where the branch left origin/main (or origin/HEAD), so a branch with no remote default branch has nothing to compare with.
  • ws pr comments sends 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.md is only what gh can already read with your login.