> ## Documentation Index
> Fetch the complete documentation index at: https://obversa.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Review a Diff

> obversa-review opens a git diff in a browser pane beside the terminal and returns the comments as structured output.

Read a git diff in a browser pane beside your terminal, attach comments to
lines, and hand them back to the script or agent that asked, as one JSON
object on stdout. Use it when a person's review has to feed a run without
anyone copying comments around. For a run's own progress page, see
[Watch a run in the browser](/docs/driving/monitor).

`obversa-review` reviews one of three diffs. Run it inside the repository,
or point it at one with `--cwd`:

```bash Terminal theme={null}
# Review the working tree (git diff).
obversa-review

# Review the staged changes (git diff --cached).
obversa-review --staged

# Review a ref range (git diff A..B).
obversa-review --range main..HEAD
```

The three are one choice: naming two on one command line, such as
`--range main..HEAD --staged`, is a usage error whatever their order. The
working tree diff is `git diff`, so untracked files don't appear. Other
flags:

* **`--cwd <dir>`** runs against another repository directory.
* **`--no-open`** doesn't place the pane. The command prints the page URL
  on stderr, and you open it yourself.
* **`--app <name>`** overrides the surface app name, which sets the frame
  markers on stdout.

## Read and comment

The page shows the diff with syntax highlighting by file type, and a file
tree with two views: **Changes** lists the changed files with file-type
icons, `+`/`−` line counts and a status letter; **All files** lists every
tracked file. Click a changed file to scroll to its diff. Collapsed
**N unmodified lines** bands sit between hunks; click one to see the code
around a hunk, or **Expand full file** on a file header to open every band
in that file. Full-file context is available in the working tree and staged
modes, not in range mode. For JavaScript files (`.js`, `.mjs`, `.cjs`),
click an identifier to jump to the line that defines it when that line is
in the diff; otherwise the tooltip names its line number.

A `+` button on every diff line writes a comment on that line, and you can
remove a comment before you return. **Return** ends the review with the
comments you wrote, or with none. **Cancel** ends it with no comments and a
`cancelled` decision.

The page is placed through `OBVERSA_SURFACE_BIN`, the absolute path a host
injects. The [cmux workspace launcher](/docs/hosts/cmux) sets it, so in cmux the
page opens in a split beside the terminal. With the variable unset, the
page opens in your default browser.

## Read the result

The command writes one framed JSON object to stdout, so a caller captures
it with a pipe, and the one-line summary goes to stderr. For the default
app name the markers are `<<<REVIEW_RESULT_V1>>>` and
`<<<END_REVIEW_RESULT_V1>>>`; the marker name is the app name in upper
case, punctuation replaced by `_`. The payload is the surface result, the
one shape every Obversa review surface returns:

```json Example result theme={null}
{
  "schemaVersion": 1,
  "app": "review",
  "surface": { "package": "@obversa/surface-diff", "version": "0.1.0" },
  "status": "completed",
  "operationId": "…",
  "createdAt": "…",
  "payload": {
    "surfaceId": "6f1c…",
    "gateId": null,
    "decision": "changes-requested",
    "annotations": [
      {
        "anchor": { "target": "src/git.mjs", "side": "new", "position": 42 },
        "body": "Reject an empty range here too.",
        "author": { "kind": "human", "id": "reviewer" },
        "createdAt": "2026-01-01T00:00:00.000Z",
        "thread": [
          { "author": { "kind": "agent", "id": "fixer" }, "body": "Done: the empty range is refused with its own message." }
        ]
      }
    ],
    "meta": {
      "mode": "worktree",
      "range": null,
      "label": "working tree",
      "fileCount": 3,
      "allFiles": ["src/git.mjs", "…"]
    }
  },
  "detail": null
}
```

* **`surface`** names the package that answered and the version that was
  resolved, on every status, so a consumer can always say which surface
  produced the frame.
* **`surfaceId`** names this review session. **`gateId`** names the
  callback gate that opened it, or `null` when you ran the command
  directly.
* **`decision`** agrees with the annotations: `changes-requested` carries
  at least one, `approved` none. A submission that says otherwise is
  refused and the page stays open, so a decision is never rewritten on the
  reviewer's behalf. `cancelled` and `timed-out` are set only by the
  session's own ending.
* **Each annotation has an `anchor`**: `target` is the repository-relative
  path as it appears in the diff, `side` is `old` for a deleted line or
  `new` for an added or unchanged line, and `position` is the line number
  on that side. An anchor is those three plain values and nothing else; one
  with an extra, hidden or computed field is refused. `body` is the comment
  text with surrounding whitespace removed, `author` the reviewer,
  `createdAt` when the comment was written.
* **A `thread`** is an ordered list of replies, each an author and a body,
  bounded like the annotation. A responder agent appends to it with
  `appendToThread(annotation, entry)` from `@obversa/surface-diff`, which
  returns a new annotation and never changes the one you pass in. A thread
  submitted with the review comes back in the framed result, so the next
  round sees the conversation so far.

## Failure

* **A conflicted diff is refused whole**, with a message naming the
  conflicted path. Git describes such a path as a combined diff, which a
  review could only show as a missing file, so the command never opens a
  page that looks clean over a conflicted tree.
* **A repository that changes during capture** is retried. The diff, the
  code around each hunk and the file list are captured twice, and the
  review is accepted only when both captures agree on every line, every
  file read and the file list. After three attempts the command stops with
  `The repository changed while the review was being captured; retry when
  it is quiet`. A change made and undone the same way during both captures
  isn't detected, and the command takes no lock on the repository.
* **`status`** is `completed` when the reviewer returns. `cancelled`,
  `timed_out` and `interrupted` carry a short `detail` and a payload with
  the same `surfaceId` and `gateId`, whose `decision` is `cancelled` or
  `timed-out` with no annotations, so a gate can still route the outcome.
  The exit code is 0 for `completed`, 1 for every other status, and 2 for
  a bad argument.

## Limits

* **500 annotations** at most per review. **4000 characters** per body.
* **An annotation must anchor to a line in the diff.** The server drops
  any other.
* **Comment bodies come back as written**, apart from the trimming and the
  length cap. The secret redaction the surface server applies to other
  responses doesn't apply to them, because a comment can quote code.

## Security

The page runs on [the surface package](/docs/packages/surface): one loopback
server, one session token, one result. The static page names nothing under
review: not the diff, not the ref, not even the highlight rules, which are
built from the tokens the diff contains. The page fetches the diff, its
label and those rules from the session API with the bearer token, so a
local process that finds the port without the token sees a shell that's
the same for every review. The address the page fetches from is a path on
the session's own origin or an absolute `http` or `https` URL; a path that
would leave the origin once parsed is refused. Nothing in a diff line can
execute: the page has no inline script, a strict Content Security Policy
allows scripts and styles from the session origin only, and every line is
rendered as text.

## Next steps

* [Obversa in cmux](/docs/hosts/cmux): the launcher that places the page in a
  split.
* [Surfaces](/docs/concepts/surfaces): the session every review page runs on.
* [Surface Diff](/docs/packages/surface-diff): the package, its options and
  `appendToThread`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.