Document review for agent output

Hand a document to a human.
Get structured comments back.

An agent writes a spec, a schema, a config, a diagram, a diff. Marginalia renders it as a page, the reviewer clicks a block and comments, and every comment lands on disk as an append-only JSONL event the agent reads directly.

No database. No accounts. No copy-paste step between the browser and the agent.

Install it Read the source MIT · single binary · no runtime deps
Live, not a screenshot This is the output of marginalia export — one self-contained HTML file, running below. Click a shape in the diagram, a paragraph, or one selected sentence.
handover.md the real export, with a review already in progress · makes no network request

Install

One binary. It serves a page and writes a log beside your file.

Nothing to configure before the first review. Point it at a document and it prints a URL.

macOS
$ brew install --cask gruesomeparty/tap/marginalia
Go
$ go install github.com/gruesomeparty/marginalia@latest
Claude Code
$ /plugin marketplace add gruesomeparty/marginalia
$ /plugin install marginalia
Then
$ marginalia serve plan.md marginalia: serving plan.md at http://127.0.0.1:8787 marginalia: feedback → plan.md.feedback.jsonl

The loop

How a review actually goes.

Five steps, and then it goes round again. Nothing in the middle is a human retyping something into a chat window.

01Agent serves Renders the document, prints a URL. marginalia serve
02Human annotates Clicks a block or selects a sentence and writes. in the browser
03Agent reads events One JSON object per line, appended as they save. feedback.jsonl
04Agent revises Edits the file and records which note the change addressed. type: addressed
05Human confirms Re-reads. The claim is checked against the text, not taken on trust. review_done
Ships as an MCP server. Agents call tools — serve a review, read what is new, reply to a note, mark it addressed, wait for the human to finish — instead of scraping terminal output.

Anchoring

Every block is anchored three ways.

A stable id, the quote the note was written against, and a hash of the block's content. Edit the document and the comments re-anchor themselves.

When the text a note points at has itself changed, the note comes back flagged stale rather than silently pointing at the wrong paragraph. That is the whole trick, and it is the reason the loop can survive more than one round.

plan.md.feedback.jsonl
{"doc":"plan.md","block":"1.2/2","quote":"The batch cap is 500 records.","hash":"83be6e423f0d","type":"question","text":"why no backoff? the queue will hammer the upstream","author":"berkay","ts":"2026-09-18T23:53:25Z"}
{"doc":"plan.md","block":"1.2/2","type":"reply","reply_to":"e206687d85b0","text":"Fair — switching to exponential with jitter.","author":"agent"}
Append-onlyNothing is rewritten. The history of a review is the file.
Beside your documentIt never edits your file. The log is its own file.

01 · Selection

Comment on a sentence, not the paragraph.

Select text and the note anchors to that sentence. Rewrite the sentence before it and the note stays exactly where it was.

Rewrite the sentence itself and it goes stale — honestly, and in the reviewer's face rather than quietly.

Tokens are minted by the auth service and verified at the edge. Rotation happens weekly, on Sunday at 03:00 UTC. Clients cache the JWKS document for one hour.

Weekly rotation with a one-hour JWKS cache means up to an hour of 401s. Is that intended?
anchored to the sentence, by quote and surrounding context — not by an offset
After an edit elsewherestill anchored After editing the sentencestale

02 · Diagrams

Click the arrow in a diagram.

Mermaid flowcharts are drawn as real pictures whose shapes carry the same anchors as the source.

Click a node and you are commenting on that node. Click an edge and you are commenting on that edge — not on the line of Mermaid that drew it. The picture is in the demo above; this is what a note on one looks like.

queue worker store dlq retry
The dashed retry edge is the selected shape.
note on edge
This retry edge should go to the dead letter queue after N attempts, not back to the queue.
source worker -.retry.-> queue anchor 1.1/2/worker-->queue

03 · Diffs

Review a unified diff the way you'd review prose.

Anchored by file and hunk — the thing a human is most often asked to approve.

A hunk is anchored by its position in the file, not by line number, so regenerating the patch after an earlier hunk changes doesn't orphan the notes below it.

internal/gateway/limit.go2 hunks · 1 note
@@ -41,7 +41,11 @@ func (g *Gateway) allow(key string)
ttl := g.redis.PTTL(ctx, k).Val()
- if n > limit { return false }
+ if n > limit {
+ w.Header().Set("Retry-After", secs(ttl))
+ return false
+ }
return true
secs() rounds down. At 400ms left this sends Retry-After: 0 and the client retries immediately.
anchored to internal/gateway/limit.go/2 — the file's second hunk

04 · Replies

The agent answers back, and the answer is checked.

A reviewer's question gets a reply threaded under it. The agent records which note a change addressed.

That claim is checked against whatever the note is about — the block, or the one sentence. Claim you fixed it without touching the text and the page says so, in front of the person who asked.

“the counter is reset at midnight”

Midnight in which timezone? This will be wrong for half our customers twice a year.

reviewer

agent · addressed

Changed to a rolling window anchored on first request. No midnight, no timezone.

the text changed · claim holds

“retries are capped at three”

Three retries against a cold upstream is a thundering herd. Add jitter.

reviewer

agent · addressed

Added jitter to the retry schedule.

unchanged since the note · claim not supported

Also reads

Same anchoring model, different parser.

markdownunified diff.proto jsonyamltomlmermaid

Scope

What it deliberately does not do.

Worth knowing before you install it rather than after.

No auth.Anyone who can reach the URL can comment. There is no login, no invite, no permission model. You run it on your machine and hand over the link to one person.
One reviewer at a time.No presence, no cursors, no merge of two people's notes. Two people on one document write to one log with no identity and no conflict story.
Local-first, and only local.No hosted service, no account, no telemetry. Nothing leaves the machine you started it on — which also means nothing is backed up for you.
It never edits your file.Marginalia only writes its own log. Applying a change to the document is the agent's job, and the page will tell you whether it actually happened.

Try it on the next thing your agent writes.

Takes one command and a document you were going to have to read anyway.

$ brew install --cask gruesomeparty/tap/marginalia
GitHub