The notabene review protocol
notabene renders a repo’s Markdown/MDX as a navigable site with Google-Docs-style review comments; this protocol is how an agent turns those comments into edits. It is file-I/O-first: you read and write plain JSON files inside the repo. No server, no port, no MCP, no API key, no account. Any agent with file access and a shell can run it.
Not set up here? If there’s no notabene.config.mjs or no .notabene/ store,
notabene isn’t configured for this repo yet — run npx -y @z29k/notabene@latest init
(writes the config, creates the store), then resume. Don’t fail; set it up.
Running the CLI. The npm package is scoped: npx -y @z29k/notabene@latest <cmd>.
If the renderer is already a local dependency, plain notabene <cmd> resolves it. Never
run npx notabene unscoped — that name is not ours. Every CLI step below is a
convenience: with file tools alone the loop still runs end to end.
Discovery — EVERYTHING comes from the config (nothing hardcoded)
Read notabene.config.mjs at the repo root to learn:
store— comments + journal folder (e.g.docs/.notabene). One file per comment:<store>/<page>/<id>.json(so branches don’t conflict on merge). Journal:<store>/journal.json. Schema version:<store>/meta.json({ "schemaVersion": <n> }, currently 3). Older stores keep one array per page (<store>/<page>.json) — both are read;notabene migrateupgrades a store to the current schema (v3, one file per comment).roots[]— the doc spaces:{ key, label, path, exclude }. A comment’spagefield is prefixed by a root’spath(e.g. rootdocs/plans→page: "docs/plans/services/x").i18n(optional,{ locales, defaultLocale, strategy }) — the doc is multi-language and a comment’spagekey is locale-encoded, mapping straight to that language’s file:strategy: "directory"→page: "docs/fr/guide/x"= filedocs/fr/guide/x.md;strategy: "suffix"→page: "docs/guide/x.fr"= filedocs/guide/x.fr.md(the default locale is unsuffixed:page: "docs/guide/x"=docs/guide/x.md). Edit that file — a comment belongs to one language; don’t touch the other language’s file or auto-translate unless asked.verify[]— project-specific checks to run after editing.review—"auto"(default) or"approve". In approve mode you don’t resolve comments yourself: you edit, mark themaddressed, and a human validates them (with a diff) at/review. See Step 5.
Assume no path, port or label. Do not require a live server or a port.
Strict rules (no exceptions)
- NEVER commit or run git operations without an explicit request (“continue”/ “go on” ≠ commit). Offer the commit at the end.
- NEVER bulk-delete the store (
rm -rf <store>): those are the user’s real comments (precious, committed). To clean a test, delete a single comment byid(edit its page file), never the folder. - Ignore
hold: true(”⏸ on hold”) andstatus≠open(addressed/resolvedalready handled): only processopenand not on hold. - MDX-safety (format
"mdx"only): when editing a.mdxfile, don’t introduce stray{or<outside code fences (MDX parses them as expression/JSX)..mdfiles (CommonMark/GFM) are lenient — no such constraint. Validated by the renderer build. - File-I/O first: read/write the
<store>/files directly with your file tools. Theastro devserver need NOT be running — the HTTP/api/commentsis only a convenience when the site is already open. Depend on neither a port nor a process.
Step 1 — Read the comments to process
List the actionable set with the CLI (any agent can shell out — no store-parsing to
reimplement, no python3):
npx -y @z29k/notabene@latest comments ls --open --json # open AND not-on-hold, machine-readable
npx -y @z29k/notabene@latest comments ls --open # …or human-readable
If the CLI isn’t available (offline, no Node, a policy against npx), read the store with
your file tools directly: each <store>/**/*.json (except journal.json/meta.json) is
one comment, or — in older v1 stores — a legacy array of comments; keep those with
status == "open" and hold != true. The loop never depends on the CLI being present.
A comment reopened after a rejection (approve mode) carries the human’s reason as
later thread replies — read them and adjust accordingly before editing. (A human
rejects from /review, or with comments reopen <id> --reply "<why>".)
A thread[].author is a plain string that may be git-style Name <email> (the browser
embeds the reviewer’s email for a unique identity) — treat the whole string as the author;
split on the trailing <…> only if you need the bare display name.
Step 2 — Locate the source page
page (= data-page) → source file, via roots[]: a page starting with
<root.path>/… maps to a file under <root.path> at the same relative path.
<root.path>/<x>→<root.path>/<x>.mdor.mdx- Index page: if
<x>.{md,mdx}doesn’t exist, it’s<x>/index.{md,mdx}(the loader stripsindexfrom the id → somedata-pagevalues omit/index). Test both.
Step 3 — Resolve the anchor
anchor.quote is the rendered text (markdown stripped: no **, links as plain
text…). To find it in the source, search tolerantly, using anchor.prefix/suffix
(disambiguating context) and anchor.section (nearest heading). scope: "page" = a
page-wide comment, no anchor.
Block comments (scope: "block", store v3) target a diagram or image, not text —
the anchor is { kind, key, label, section, index } (no quote; index disambiguates
repeated blocks with the same key). kind: "image" → find the
 whose src matches key/label and act on it; kind: "mermaid" → find the
```mermaid fence for that diagram (its source hashes to key; label = the diagram
type + first line) and edit the diagram source. anchor.section narrows the search.
Step 4 — Edit the docs (faithfully)
Apply each piece of feedback faithfully at the right spot. A comment is a user decision. If the change touches public behavior documented elsewhere, update it (see project hooks below). For what you can put in a page — Mermaid diagrams (```mermaid), GFM tables, code blocks, inter-doc links — and the MDX-safety rules, see the authoring reference: https://z29k.github.io/notabene/guide/authoring/.
Step 5 — Mark the comment + write the journal
Set the status by review mode (from the config):
auto(default):status = "resolved".approve:status = "addressed"— you propose; the human validates at/review. Do not resolve it yourself.
In both cases set resolution = { note, journalEntryId } and append a
<store>/journal.json entry: { id, date (YYYY-MM-DD), title, summary, changes[] { page, commentIds[], what, why } }. Each resolution’s journalEntryId = the journal entry’s
id.
Prefer the CLI for this step — it picks the status from review for you, preserves
every other field, and writes atomically:
# 1. journal first: --json echoes { id } so you can chain it
echo '{ "id": "j-2026-07-28", "date": "2026-07-28", "title": "…", "summary": "…",
"changes": [{ "page": "docs/guide/x", "commentIds": ["c1"], "what": "…", "why": "…" }] }' \
| npx -y @z29k/notabene@latest journal add --json
# 2. then the comments it covers (status = resolved | addressed, per the config)
npx -y @z29k/notabene@latest comments done c1 c2 --note "…" --journal j-2026-07-28
Editing the JSON by hand is still valid (journal.json: 2-space indent + trailing
newline) — just never lose a field, and never write resolved in approve mode.
Cascade (load-bearing for the review UI): if fixing a comment touched several
pages (a cross-ref, behavior documented elsewhere), emit one changes[] entry per
page actually touched, each listing that commentId. The reviewer’s diff is built by
inverting the journal — a page you don’t record there won’t be shown.
Step 6 — Verify
- ALWAYS: build the renderer — a broken doc file breaks the tool itself
(
npx -y @z29k/notabene@latest build, or the project’s renderer build). Confirm 0 remainingopennon-held comments. - Lint the inter-doc links —
npx -y @z29k/notabene@latest lint. It validates every relative.mdlink against the routes the build just emitted (with did-you-mean suggestions;--jsonfor machine reading). A broken link is a failed verification — fix it before reporting. If it exits 2, the build of step 1 didn’t run — never skip it. - Audit the store you just wrote —
npx -y @z29k/notabene@latest comments verify. It checks statuses, the comment↔journal links in both directions, the file layout and dangling pages. The one to care about: a comment whose journal entry doesn’t list it back inchanges[]makes/reviewshow the human an empty diff. Exit 1 = fix it before reporting. config.verify[]— the project’s own checks (build/lint/memory update).- Project memory — if the project keeps a memory doc (
CLAUDE.md/AGENTS.md), update it for any public-behavior change.
Steps 4–5 are the project extension point. The core loop is generic; a consumer declares its post-edit steps via
verify[]and its memory conventions. The core does not know any specific project.
Step 7 — Report (without committing)
Summarize as a table: per comment → the change made (section) + the why. Point to
/journal (and, in approve mode, to /review — the human validates each edit
against its diff there, then approves → resolved or rejects → reopened). Then ask
whether to commit, and what (doc edits only / + resolved store + journal / + project
artifacts). Wait for an explicit go-ahead.
notabene