Skip to content

About

Interactive PR review for Claude Code: one beat at a time, flags that resolve into commits.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

110 Commits

Folders and files

Repository files navigation

underwrite

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.

Install

/plugin marketplace add radkode/underwrite
/plugin install underwrite@underwrite

Use

/underwrite 42          a PR number
/underwrite my-branch   a branch, diffed against main
/underwrite             the working tree

How a session goes

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.

Trust and execution

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.

The page drives

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.

Session state

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

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 3

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

License

MIT

About

Interactive PR review for Claude Code: one beat at a time, flags that resolve into commits.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages