obversa-review opens a git diff as a review page in a browser pane
beside the terminal. You read the diff, attach comments to lines, and
return them. The command writes the annotations to stdout as structured
output, so a script or an agent reads them without a screen.
Use
The command reviews one of three diffs. Run it inside the repository, or point it at one with--cwd.
--range main..HEAD --staged) is a usage error, whatever their order.
A diff that holds an unresolved merge conflict 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.
The page is placed through the absolute path a host injects as
OBVERSA_SURFACE_BIN — the cmux host’s workspace launcher binds it to its
obversa-surface glue for every terminal and agent in a workspace — and
with the variable unset the default browser opens the page; nothing is
looked up on PATH. The placement command receives a one-time launch URL
(single use, valid for a minute) that redirects to the page; the page URL
with its token never enters a process argument.
Other flags:
--cwd <dir>runs against another repository directory.--no-opendoes not place the pane. The command prints the page URL on stderr, and you open it yourself.--app <name>overrides the surface app name. The name sets the frame markers on stdout (see below).
git diff, so untracked files do not appear.
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. If the repository changes
between them, the command tries the pair again — three attempts in all —
and then stops with The repository changed while the review was being captured; retry when it is quiet. A change that is made and undone the
same way during both captures is not detected; the command takes no lock
on the repository.
In cmux the page opens in a split beside the terminal. Outside cmux it
opens in your default browser. How placement works.
What the page offers
- The diff, with syntax highlighting by file type.
- 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 in the repository. Click a changed file to scroll to its diff. - Collapsed N unmodified lines bands between hunks. Click one to see the unchanged code around a hunk. Expand full file on a file header opens every band in that file. Full-file context is available in the working tree and staged modes, not in range mode.
- Go to definition for JavaScript files (
.js,.mjs,.cjs). Click an identifier to jump to the line that defines it, when that line is in the diff. When the definition is not shown, the tooltip names its line number. - A
+button on every diff line. Click it to write a comment on that line. 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
cancelleddecision.
What comes back
The command writes one framed JSON object to stdout, so a caller captures it with a pipe. 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, with punctuation replaced by _.
The payload is the surface result: the one result shape every Obversa
review surface returns, whatever it shows. The surface field 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.
thread: an ordered list of replies, each an
author and a body, shaped and bounded like the annotation itself. A
responder agent appends to it with appendToThread(annotation, entry) from
@obversa/source — the call returns a new annotation and never mutates the
input — and a thread submitted with the review rides the framed result
intact, so the next round sees the conversation so far.
surfaceIdnames this review session.gateIdnames the pipeline gate that opened it, ornullwhen you ran the command directly.decisionagrees with the annotations:changes-requestedcarries at least one, andapprovedcarries none. A submission that says otherwise is refused and the page stays open, so a decision is never rewritten on the reviewer’s behalf.cancelledandtimed-outare set only by the session’s own ending, never by a submission.- Each annotation has an
anchor:targetis the repository-relative file path as it appears in the diff,sideisoldfor a deleted line ornewfor an added or unchanged line, andpositionis 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.bodyis the comment text with surrounding whitespace removed.authoris the reviewer, andcreatedAtis when the comment was written.
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, decision set to cancelled or timed-out, and
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.
Security
The page runs on the Surfacer: 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 sees a shell that is the same for every review. The address the page fetches from is a path on the session’s own origin or an absolutehttp 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. The
returned annotations are checked on the server, and one that does not
anchor to a real diff line is dropped. Comment bodies are trimmed and cut at
4000 characters, and are otherwise returned as written: the secret
redaction the runtime applies to other responses does not apply to them,
because a comment can quote code.