Read a pull request closely enough to stand behind it. One beat at a time, driven by you, ending in a delivered result.
Most review tooling hands you a wall of findings and leaves the work of deciding to you. This walks a change in causal order, one coherent unit at a time, and stops on each one that needs you: a flag, or a claim it could only read and not check. Clean beats land on the page and the walk moves on. When something is worth flagging, the action names what happens next: Implement applies and verifies a branch fix, Include in review queues a finding for the GitHub review, and Include in report records a finding in the durable report. Record decision stores an answer whose words complete the work. Including a PR finding never authorizes execution. A separate implementation approval may link one accepted finding to an attested child session that lands a local branch commit.
/plugin marketplace add radkode/underwrite
/plugin install underwrite@underwrite
/underwrite 42 a PR number
/underwrite my-branch a branch, diffed against main
/underwrite the working tree
Ingest. Freezes the PR's exact base and head, no-exec policy, and lifecycle-derived
audience in one transaction, fetches both revisions into a bare repository, and builds
their three-dot diff locally. Open PRs use review mode; every non-open PR, whether merged
or closed without merge, uses report mode. That avoids remote display limits and binds
every later check to full commit IDs. The same capture freezes governing instructions
from the base, separately from the untrusted head. It then reads the last twenty base
commits touching those paths and the two or three earlier PRs the squash-merge subjects
point at. That last one is not padding: prior work in the same area is reliably where the
best finding comes from, because it is the context a diff cannot show you. The frozen diff
is read back under a one-million-byte ceiling (--max-bytes); a change past that size is
refused rather than truncated.
Orient. A short reconstruction of what the change is for, plus a claim check comparing the PR description against what the diff actually does and naming which of Testing, Risk and How to revert the description leaves unanswered. This is the first of two decisions before the page opens: confirm it, or correct either half, and your correction frames the rest. This repository's pull request template asks for the three.
Plan. Every changed file is tiered into core, enabling, follow-through, and risk. The second decision: walk it in that order, reorder it, skip a tier, or move a file that got tiered wrong. Both decisions arrive as named options, not as a question you have to infer. Your answers are kept in your words beside the reconstruction and claim check: the finished page opens on them, and a live walk folds them to one line above the facts.
Walk. One beat at a time, opening with a verdict token and running on fixed lines:
BEAT 5/7 enabling .github/workflows/ci.yml:22
FLAG unpinned attw resolves latest on every CI run
WHAT adds `attw --pack . --profile esm-only` between build and eval
PROOF `npm view @arethetypeswrong/cli versions` shows every major is 0
RISK a new rule reddens a PR that changed nothing
PRIOR #2 pinned break-check to 0.6.0 citing this exact failure mode
FIX npx --yes @arethetypeswrong/cli@0.18.5
Before a flag is written, a fresh-context agent reads it against the frozen diff and blobs
alone and answers three questions: did this PR introduce it, is it real, and would a senior
engineer stop at it. A flag the PR did not introduce, or that is not real, is demoted to
UNVERIFIED with the second reading beside it, so a wrong flag costs a glance rather than a
decision. A doubt about whether it is worth stopping for stays beside a confirmed flag
instead, because that call is yours. A flag written where no second reader was available says
"Not read twice" rather than passing for a checked one.
A flag in a PR session has to land on a line the diff touches: the store refuses one on a
line the PR never put in front of you, naming the nearest touched line. That holds a flag
to the change without proving the change caused what it flags.
PROOF names a command run under the session's execution policy, or a file that was read.
inferred is a legal value. A claim with neither does not ship. The beat says which it
was: CLEAN is reserved for a command whose output would have differed were the claim
false, and a claim that was read or inferred opens UNVERIFIED, which is where your own
reading goes. A risk appears only on a flag, with a decision beside it.
A PR session can still earn CLEAN when the host runs a proof gateway. The walk picks a
command, such as one test module, and predicts its exit code. proofctl.py prove hands
it to the execution gateway, which runs it on the gateway's own copy of the frozen head
inside its sandbox and signs a receipt only if the command exits as predicted. The
store accepts proof_kind: attested only beside a verified receipt for that beat, and
writes the signed command, exit code and the last lines it printed onto the card itself,
so the walk's PROOF line cannot pass for the run. A flag can carry its failing run the
same way. A wrong prediction yields no receipt and proves nothing.
Beats stay that size because the store makes them. A slot over 25 words, a claim over 20, a fourth filled slot on a clean beat, or an eleventh quoted line is refused at write time, with the offending slot and its count named, as is a clean verdict on a read proof or a risk on a beat that is not a flag. The example above sits at nine or ten words a line, which is the target; the budget is the ceiling. Nothing already recorded is re-judged, so a session written before the budget still opens and still renders.
FIX is the smallest concrete implementation intent, review recommendation, or decision
owed. For local branch and working-tree targets, choosing Implement authorizes Underwrite
to apply that intent and run verification. It does not claim that an exact patch already
exists. A PR snapshot remains no-exec and cannot implement directly. After its finding has
been accepted and acknowledged, a separate approval may create a one-finding child with
branch audience and gateway_attested execution. The source session stays unchanged.
Finish. Local branch and working-tree targets may land requested implementations as
commits. An open PR lands accepted findings as one anchor-validated GitHub review. The
review carries the frozen full head as commit_id, so a PR update cannot silently move
delivery onto code that was never walked. A non-open PR has no open review delivery
target, so Include in report makes the accepted SQLite beat its terminal report outcome.
It does not call land or create a GitHub effect.
A linked child may land its verified output as one commit whose only parent is the frozen
PR head. It updates only its generated local branch and never pushes.
Every session ends with its page rendered to report.html in the session directory, on your
machine. Once the final render passes, Underwrite offers to publish that page as a hosted
Artifact on claude.ai, and does so only after you say yes. The page carries quoted lines of
the reviewed diff, the changed file paths, the reconstruction, every finding, and your notes
and decisions, so publishing uploads all of it. Say no for a private or client repository you
would not paste into claude.ai.
Start a PR review from a clean checkout of its base, never from the PR head. Keep that controller checkout on the base for the whole session. A controller may load repository instructions before Underwrite begins, so discovering the mistake later is not enough: stop and restart from the base checkout. The refusal names what it found and prints the command for a fresh checkout of the exact base to restart from.
Every PR head is untrusted executable input, regardless of author, fork, audience, review,
or merge state. Underwrite reads the head, PR conversation, linked issues, and command
output as data. Governing repository instructions come only from the frozen base revision.
Changed AGENTS.md, AGENTS.override.md, CLAUDE.md, CLAUDE.local.md, REVIEW.md,
contribution guidance, and ADRs are reviewed like any other change; they do not govern
their own review.
A REVIEW.md in the base is the owners' review guide: what failure looks like in that
part of the tree, which paths are not worth walking, and what they count as evidence. The
frozen copy sharpens the claim check and the walk plan. This repository keeps one at its
root.
That prose is also barred from becoming the review. A well-commented diff offers a ready
WHY for every hunk, and a slot reworded out of the docstring above the change reads as
analysis while adding nothing, which costs more than quoting would: you have to read it
closely to find that out. Underwrite quotes the comment and spends the slot on what
reading it did not tell you.
Every PR session freezes no-exec and its lifecycle-derived audience atomically with its
target. Underwrite may inspect the hash-bound diff and frozen Git objects through its
static readers, but it does not check out the head, install, build, test, lint, run repo
scripts or interpreters, or invoke Git hooks and filters. Review comments, report
inclusions, notes, navigation, and recorded decisions still work.
A proof never changes the session's no_exec policy. This machine never runs the head;
the gateway does, in a sandbox with no network or credentials, from its own quarantine of
the frozen bundle. It happens only on a host that opted in twice: a proof configuration
for the controller, and proofArguments in the gateway's own configuration. Even then
the gateway runs the profile's fixed command with at most 16 bounded arguments after it.
The receipt is evidence for one beat and authorizes nothing.
The host execution protocol is documented in
docs/host-execution-protocol.md. Its
execution_receipt.py module checks a
signed receipt contract plus caller-supplied bindings through an out-of-band verifier. It
does not provide signing keys or authenticate a host on its own.
The repository includes a standalone privileged host gateway in
gateway/. Every source PR session still remains no_exec with its
frozen review or report audience. A separate implementation authorization binds one
acknowledged source accept, the finding it accepted, the frozen target, an actor, and the
actor's approval text. It creates a one-beat child with branch audience and
gateway_attested execution instead of changing the source session.
The gateway's one-shot adapter.py accepts that child's reserved
request and publishes the exact six-file evidence handoff consumed by the linked workflow.
The child accepts execution evidence only for its reserved request. It independently checks the frozen source bundle, signed capability and receipt, output bundle, complete streams, trusted job and sandbox profile, and expected exit code. Signatures must verify under a pinned P-256 public key whose fingerprint matches that profile. A conforming receipt or caller-supplied sandbox label alone remains evidence, not authorization.
Landing materializes the verified output, creates a local SHA-1 commit with the frozen PR head as its sole parent, remeasures that commit against the signed SHA-256 output tree, and compare-and-swap updates only the generated local branch. The target repository requires Git 2.36 or newer, explicit object and ref durability, and exclusive controller access while linking or landing. Session directories and Git metadata must be private local POSIX storage that gateway jobs and untrusted concurrent writers cannot modify. The gateway must run in its own supervised process because it installs process-wide host resource limits before handling untrusted artifacts. Neither the gateway nor the linked implementation path pushes.
A walk serves itself on loopback, and the page is where you actually review. While a walk
is listening, the beat being read leads the page on its own, under a bar that stays in
view: the plan as a track with the current beat marked, the agent's own sentence about
what it is doing, and Next item. Everything already walked sits below, ordered by what is
owed rather than by what was walked: open flags expanded at the top, resolved beats folded
to one line with your call on it, clean beats to one line each that still carries its
proof. When the walk ends, that ledger is the page, and it is what the final render keeps.
Above it sits what the walk covered against what it planned, with any beat it never
reached named and the risky ones marked read carefully, because a review that stopped at
6 of 9 should not read like one that finished.
The final render stays on your machine unless you agree to publish it, as Finish says.
The keys do what the row under the beat says: Enter saves a note, or records a decision
when the note is the decision; ⌘Enter or Ctrl+Enter fires the row's primary action; n
is Next item and f goes to the first open flag.
Resolving a beat asks twice, because none of it can be walked back: the store lets accept
and drop out of an open flag only, and refuses to re-decide a beat that carries a delivery.
So Implement, Include, Record decision and Drop all arm on the first click and send on the
second, with the prompt naming which beat. Anything else answers no, including three
seconds of nothing. Save note and Next item go straight through.
Drop also asks which kind of miss it was, wrong, intentional, not worth it or pre-existing,
as a chip beside the button and in your own words, so a later session can count what the
walk got wrong.
Decisions happen there too. Review mode uses Include in review and Drop. Report mode uses Include in report and Drop. Local branch and working-tree sessions retain Implement. A policy question whose answer completes the work carries Record decision. Each has a field for putting the call in your own words, and Next item advances the walk from anywhere. Clicking is what unblocks the terminal side, which parks on the server at every flag and unverified beat rather than spinning. The terminal still takes the same answers in words, so closing the tab never strands a session.
The labels describe the delegated effect without changing the durable protocol. Implement,
Include in review, and Include in report all record the existing accept action. Record
decision uses the existing decide action.
For a PR source, that accept delivers only to its frozen review or report audience. The
separate linked implementation authorization records its own actor and approval and never
reinterprets Include in review or Include in report as execution permission.
A no-exec PR review flow looks like this:
terminal browser
-------- -------
presents beat 5 ------> beat 5 appears, FLAG, expanded
(parked on /await) [ Include in review ] [ Drop ] [ your words ]
<------ POST /act with canonical accept and stable IDs
SQLite records the call, pending delivery, and application receipt
the frozen head remains unexecuted
presents beat 6 ------> beat 6 arrives
final approval ------> one anchor-validated GitHub review is posted
records the landing ------> accepted beats show the review URL
For a non-open PR, Include in report records accept and the accepted beat is complete in
the SQLite-backed report immediately. There is no pending delivery, land call, or GitHub
effect. report.html is a regenerable view of that durable state, not its delivery receipt.
An optional linked implementation continues in a separate child:
accepted source finding + explicit implementation approval
-> one frozen one-beat child
reserved request -> supervised gateway execution
six-file evidence set -> pinned signature and artifact verification
verified output -> exact local commit, compare-and-swap branch update
The source session keeps its original audience and no_exec policy throughout. A moved PR
head requires a replacement source snapshot; it never refreshes either session in place.
The server is loopback-only and takes no path from any request: every read and write is a fixed name inside the session directory. Loopback is not authentication on its own, so it also refuses a request whose Host is not loopback, or whose Origin is not the page's own. Each session has a durable identity, so a browser retry cannot land in another session if an operating system later reuses the same loopback port.
Sessions live in ~/.claude/reviews/<owner>-<repo>-pr<N>/, outside every repo, so a review
never shows up in git status. They are resumable, because a review that gets interrupted
should not have to start over.
session.sqlite3 authoritative versioned session, beats, action queue, and receipts
session.json derived compatibility export
beats/01.json derived compatibility export of one beat
decisions.jsonl derived compatibility export of reviewer actions
ack.json derived compatibility export of the handled cursor
pr.json GitHub metadata captured with the target
pr.diff frozen local three-dot diff, hash-bound to the target
pr.bundle frozen base and head Git objects, hash-bound to the target
trusted-context.json frozen base instructions, hash-bound to the target
report.html rendered, regenerable
report.partial.html the page as it stands, rewritten by the server on every change
implementations/<link-id>/ one linked child, nested below the source session
session.sqlite3 authoritative child, attempt, evidence, and landing state
implementation-evidence/<attempt>/
request.json exact reserved gateway request
capability.dsse.json signed host capability
receipt.dsse.json signed execution receipt
output.bundle verified output transport
stdout complete captured standard output
stderr complete captured standard error
proof-evidence/<proof>/ one verified attested proof: request, both signed
envelopes, and both complete streams
The scripts are Python 3 stdlib, with no dependencies. serve.py runs the walk and
sessionctl.py is the only command-line mutation path. SQLite commits a reviewer action
with its local beat change, deduplicates retries by action ID, and stores navigation as an
absolute application receipt. A crash before commit changes nothing; a crash after commit
resumes from the stored result without moving twice. Compatibility JSON is regenerated
from SQLite and is never authoritative once the database exists. The snapshot-pr
command in sessionctl.py records the write-once target, including the head repository
and ref, plus the trusted base context and Git object bundle. It records the no-exec policy
and derives the PR audience from the captured lifecycle in the same transaction. Existing
sessions keep their frozen audience; lifecycle drift requires a supervised replacement.
diff, read-blob and context-log are the argv-safe gateways for inspecting those
frozen objects without checking out the PR; each one verifies against the frozen digest
before it returns anything.
check-pr refuses a moved base, head, route, or lifecycle before an external effect, while
check-controller requires the clean controller to remain at the frozen base. The frozen
diff, trusted context, and object bundle are checked by byte count and SHA-256.
Controller cleanliness is rechecked immediately before capture freezes the target. It is
a point-in-time precondition, not an attestation against a concurrent local writer.
When the PR moves, the old session stays as it was and a replacement starts beside it at
<session>-r2/. link-predecessor records the round, the lines the PR touches that changed
since the walked head, and every finding the reviewer settled in an earlier round. The
replacement walks only those moved lines, the page carries the earlier calls forward, and
the store refuses a flag the reviewer already dropped while its line reads the same. By the
fourth round the recommendation says to reconsider the approach rather than fix again.
history returns what earlier sessions on the same repository learned, their facts and
recommendation and the findings they left on the paths a new one touches, so the second
review of an area starts from the first.
Linked creation is deterministic for one source action and refuses a conflicting second authorization. A complete child is atomically published at its deterministic path, so a crash cannot expose a partial final session. Request reservation reuses a nonfailed attempt; a definite failed attempt gets a new attempt number and challenge. Evidence consumption and landing accept identical retries but reject changed bytes. The prepared commit plan is durable before the branch update, so a restart can observe an already-updated ref and finish the SQLite receipt. A moved target before the update blocks landing and requires a replacement source snapshot. A move detected after the local update preserves the landed commit and reports that a replacement is required, without causing another effect.
sessionctl.py stats ~/.claude/reviews is the noise ledger. For every stored session and
in total it counts beats, flags raised, flags still open, accepted, decided, dropped by
reason, unverified beats and their share, flags a second read confirmed or disputed, and
questions asked. A drop recorded before reasons existed counts as unstated. It reads each
session from a backup copy, so counting never upgrades an old session in place. Read it
before and after a change to the skill: a change that raises the dropped share is a
regression even when it finds more.
render-report.py turns a session into the page and validates it on the way through. A
flag with no fix, a clean beat with no proof, or a proof naming no command gets an
UNPROVEN chip and exit 2. During a live walk, pending and failed delivery are visible
states and do not pretend to be completion: Implementation pending or Implementation failed in branch mode, Included, review pending or Review publication failed in
review mode. A final render uses --final and rejects any chosen branch or review delivery
that has not landed. Report accepts are already terminal and require no landing. Review
mode omits that flag only for the preview before its GitHub POST. The page still renders
when validation fails, and each unproven card lists the checks it failed.
validate-anchors.py snaps review comments to lines that exist in the diff and folds
unsnappable ones into the body rather than dropping them.
Iterating on the page design means editing skills/underwrite/assets/report.css, or
passing --css to try something without a commit.
evals/cases/ holds pull requests with a known answer: misses, where a defect shipped and a
later PR fixed it, and clean PRs that nothing later had to fix. evals/run.py clones each at
its base, walks it headlessly with this checkout's skill in eval mode, and scores the session
with evals/score.py. A flag the second read demoted does not stand, so it counts neither
for a miss nor against a clean case. Nothing is posted: every case is a merged PR, which freezes to report
audience, and eval mode delivers nothing.
python3 evals/run.py # every case
python3 evals/run.py underwrite-83-legacy-guard --keep --max-budget-usd 3A case passes when every expected finding is found, by place and by the keywords of its class,
and nothing else stands. --baseline evals/baseline.json fails only on a regression: a case
that passed and now fails, or more misses or false alarms than the committed baseline. Keep a
change to SKILL.md only when it passes that, and commit a new baseline when it improves. The
evals workflow runs this on pull requests that touch the skill or the evals, but only once
an ANTHROPIC_API_KEY secret is set, because each run spends model budget; CI checks the cases
and the scorer either way. --bare keeps hooks and personal instructions
out of the run and needs ANTHROPIC_API_KEY. docs/misses.md records what each miss lacked.
MIT