notabene
Reference
note this well
Reference
Reference
The exhaustive counterpart to the Guide — tables and contracts, one page per surface:
- CLI — every
notabenecommand and flag. - Configuration keys — the full
notabene.config.mjssurface. - Frontmatter — every key a page can carry.
- The
.notabenestore contract — the versioned JSON schema agents read and write. - The agent protocol — the file-I/O-first loop any agent follows to turn comments into edits.
- Safety model — why the write API can’t hurt you.
CLI
CLI
The npm package is scoped (@z29k/notabene); the installed command is just
notabene, so npx notabene … works as-is once the package is a dependency of
your repo. Without a local install, always use the scoped name —
npx -y @z29k/notabene@latest … — because unscoped notabene is not our package.
| Command | What it does |
|---|---|
notabene doctor | Read-only state as JSON: config/store/port + detected doc folders — --json |
notabene init | Write notabene.config.mjs + create the store (no-op if present); --detect auto-detects doc folders. Also writes the agent entry point: <store>/protocol.md + a bounded block in AGENTS.md — opt out with --no-protocol / --no-agents-md. Idempotent: re-run it to refresh both |
notabene dev | Start the review server over this repo’s docs (live-reload); --detach runs it as a background daemon. With the optional pagefind dev dep, its search is full-text too |
notabene status | Is the detached server running? (pid, port, URL) — --json |
notabene stop | Stop the detached server |
notabene build | Build the site (Node standalone; docs prerendered, no write API in the artifact) |
notabene build --public | Read-only static site for public hosting — see the guide. [--site URL] [--base /sub] [--out DIR]. With the optional pagefind dev dep installed, the artifact gets static full-text search |
notabene preview | Serve the built site |
notabene lint | Validate inter-doc links against the last build’s emitted routes (did-you-mean suggestions; --json). After build --public, also catches links from public pages into private content. Exit 1 = broken links, 2 = no build yet |
notabene pdf | Export a PDF via headless Chromium (bookmark outline + page numbers); --scope doc|space:K|folder:K/P|page:K/I, --locale, --out, --chrome. Needs the optional puppeteer peer dep (or puppeteer-core + --chrome) |
notabene migrate | Convert the store to the one-file-per-comment layout (stamps schemaVersion 3) |
notabene comments ls | List comments — --open --json --page <p> (for agents/scripts) |
notabene comments done | Mark comment(s) handled: done <id…> [--note <text>] [--journal <entryId>]. The status comes from review (auto → resolved, approve → addressed) — --status overrides, --force acts on a comment that is on hold. Atomic; every other field preserved |
notabene comments reopen | Send comment(s) back to open: reopen <id…> [--reply <text>] [--author <name>] — the reason becomes a thread reply the agent reads on its next pass (the CLI side of rejecting at /review) |
notabene comments verify | Audit the store: statuses, comment↔journal links both ways, layout, duplicates, dangling pages. --json; exit 1 on errors, 2 with no store. Run it after an agent pass, or in CI |
notabene journal add | Append a JSON journal entry read from stdin (atomic; --json echoes { id } so an agent can chain it) |
notabene protocol | Print the agent protocol on stdout — --path prints where it lives, --write refreshes <store>/protocol.md |
Global flags
| Flag | Meaning |
|---|---|
--root <path> | Consumer repo root (default: cwd) |
--config <path> | Config path (default: <root>/notabene.config.mjs) |
--port <n> | Dev server port (else config port, else a free one) |
--detach | dev only: background daemon (status/stop manage it) |
--detect | init only: prefill roots[] from the doc folders found |
--no-protocol / --no-agents-md | init only: skip the <store>/protocol.md copy / the AGENTS.md block |
--host | Expose on the LAN — trusted networks only (safety) |
--public / --site / --base / --out | build only: the public site artifact |
Configuration keys
Configuration keys
notabene.config.mjs is a data-only ES module at your repo root; every key is
optional. The narrative version with examples is in the
configuration guide.
| Key | Default | Meaning |
|---|---|---|
siteName / tagline | "Docs" / "docs" | Header brand |
locale | "en" | UI language + nav sort collation |
format | "mdx" | "mdx" (.mdx strict + .md lenient) or "commonmark" (no MDX at all). init scaffolds "commonmark" |
roots[] | [{docs}] | Doc spaces: { key, label, path, exclude, description, publish }. label/description accept a per-locale map with i18n; publish: false keeps the space out of public builds |
store | "docs/.notabene" | Comments + journal folder — commit it (contract) |
home | — | Custom landing page: a repo-relative Markdown file (or per-locale map) rendered above the space cards on / |
branding | — | Identity assets: { logo, logoDark, favicon, socialImage }, repo-relative files served at /_nb/…. Unset favicon → a built-in default mark |
theme | — | Look customization: { tokens, css, assets, code } — --nb-* token overrides (validated; a typo throws), a stylesheet loaded after the renderer’s (cascade-layer-safe), a repo folder served at /_nb/assets/… for fonts/images (extension allow-list, no traversal), a Shiki theme for code ("github-light" or { light, dark } — dual palettes follow the scheme toggle), and mermaid: false to keep Mermaid’s own diagram palette |
nav | — | Outbound links: { header[], sidebar: { title, links[] }, footer: { links[], text, poweredBy } }. One item shape — { label, href, icon, iconOnly, publish }; label accepts a per-locale map, publish: false keeps a link out of public builds. Validated at load (scheme allow-list, icon names, duplicates) |
port | 3009 | astro dev port |
host | false | true/NOTABENE_HOST=1/--host exposes the write API to the LAN (safety) |
verify[] | [] | Post-edit checks the agent runs (the renderer build always runs) |
review | "auto" | "auto" = agent resolves comments; "approve" = agent proposes (addressed), you validate each at /review with a diff (review loop) |
author | git user.name | Default comment author; each browser overrides it per-device via the identity dialog |
authorEmail | git user.email | Default author email; embedded git-style (Name <email>) so identities stay unique |
editPattern | — | “Edit this page” link under every doc page: a URL with a {path} placeholder (repo-relative source path), e.g. https://github.com/o/r/edit/main/{path}. Placeholder required — validated at load |
pdf | { enabled: true, pageSize: "A4", margin: "18mm" } | PDF export — enabled toggles the Export menu + /print routes; pageSize/margin set the @page box |
i18n | — | Multi-language docs: { locales, defaultLocale, strategy: "directory"|"suffix" }. Omit for one language |
publish | — | Public build target: { site, base, exclude }. site optional — omitted = origin-agnostic artifact |
CLI/env overrides
--site/--base override publish.site/publish.base; NOTABENE_HOST=1 matches
host: true; the CLI passes the repo’s git identity as the author/authorEmail
fallback. Nothing else is configurable outside this file.
Frontmatter
Frontmatter
Optional YAML at the very top of a page. Everything has a sensible default — a repo with zero frontmatter renders fine (humanized file names, alphabetical order).
---
title: Internal network map # page <title> + breadcrumb (overrides the first H1)
description: Segments and VLANs. # public builds: meta description + OpenGraph + JSON-LD
publish: false # public builds: keep this page out entirely
lastUpdated: 2026-05-04 # page footer: overrides the git "Updated on" date
sidebar:
label: Network map # sidebar text (else title, else humanized file name)
order: 9 # position among siblings (ascending)
indexLabel: Start here # folder landing pages: rename the "Overview" entry
---
| Key | Effect |
|---|---|
title | Page <title>, breadcrumb, search result title. Falls back to the first # H1, then the file name |
description | Public builds: <meta name="description">, OpenGraph/Twitter description, JSON-LD |
publish: false | Public builds: the page is not built — no route, nav, search, llms, twin, sitemap. Dev/normal builds always show it |
lastUpdated | Overrides the git-derived date in the page footer’s Updated on. Any date YAML can parse. Useful when git history misleads (imported or generated content) |
sidebar.label | Sidebar entry text. Resolution: sidebar.label → title → humanized file name |
sidebar.order | Sort key among siblings, ascending. Unset entries keep alphabetical order, after the ordered ones. Groups and pages share one ordering |
sidebar.indexLabel | On a folder’s landing page: renames its localized Overview entry |
Folders
A folder is named and positioned by its landing page — <folder>/index.md (or
readme.md): its sidebar frontmatter applies to the whole group, and the page itself
appears as the group’s Overview entry. Labels and order flow through to breadcrumbs
and PDF covers.
Unknown keys are ignored and preserved — agents editing a page must keep the existing frontmatter intact (the review skill does).
The .notabene store contract
The .notabene store contract
The store is a versioned public contract — committed in your repo, read and written
by agents. Its shape never changes silently: <store>/meta.json carries
{ "schemaVersion": n } (currently 3), and any shape change ships a migrator
(notabene migrate).
Layout
<store>/
meta.json # { "schemaVersion": 3 }
journal.json # one array of journal entries
protocol.md # the agent protocol (written by `init`; not data)
<page>/<comment-id>.json # ONE FILE PER COMMENT → conflict-free git merges
meta.json, journal.json and protocol.md are reserved names at the top level;
everything else under <store>/ is comment data. Readers only ever parse .json, so
protocol.md is inert — it rides along so the spec is always next to the comments.
<page> is the logical page path (docs/guide/setup — with i18n it’s the raw,
locale-encoded id, so comments are per-language). Older v1 stores (one array per page)
are still read; any write migrates that page forward.
A comment
{ "id", "space", "page", "scope", // scope: "selection" | "page" | "block"
"anchor": { // selection: W3C-style text quote
"quote", "prefix", "suffix", "section" // rendered text + context + nearest heading
} | { "kind", "key", "label", // block (diagram/image): content-derived key
"section", "index" } | null,
"thread": [{ "author", "body", "ts" }], // author may be git-style "Name <email>"
"status": "open" | "addressed" | "resolved",
"hold": false, // true → the agent skips it (reviewer WIP)
"resolution": { "note", "journalEntryId" } | null,
"createdAt", "updatedAt" }
addressed is the two-phase review state: agent-proposed,
awaiting human validation at /review.
A journal entry
{ "id", "date", // YYYY-MM-DD
"title", "summary",
"changes": [{ "page", "commentIds": [], "what", "why" }] }
One changes[] record per page actually touched — the /review diff is built by
inverting the journal, so a page not recorded there won’t show in the reviewer’s diff.
Each resolved comment’s resolution.journalEntryId points back at its entry.
Rules agents must honor
- Writes are atomic (temp file + rename) — never hand-write partial JSON. The CLI
(
comments done/reopen,journal add) does this for you;comments verifyaudits the result. - Never bulk-delete the store; delete a single comment by id if asked.
- Only process
status: "open"andhold: false. - The full protocol lives in
<store>/protocol.md— written bynotabene init, committed with the store, refreshed by re-runninginit. Point any agent at it.
The agent protocol
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.
Safety model
Safety model
The comments API writes into your git — so it’s fenced in, by construction:
- Dev-only. The write path only exists under
notabene dev. Inbuild/previewmutations return403, and a public build doesn’t contain the routes at all. - Loopback by default. The server binds
127.0.0.1; the write API is not reachable from your network unless you opt in with--host/NOTABENE_HOST=1— trusted networks only. - Every write is gated beyond the bind: cross-origin requests are refused
(anti-CSRF), a non-loopback
Hostheader is refused in loopback mode (anti-DNS-rebinding), and — when you setNOTABENE_TOKEN— each write must carry a matchingx-notabene-token. Setting a token is recommended with--host. - Identity per person. On a non-loopback host, each visitor is asked to set their name (+ optional email) before browsing, so comments attribute to real people rather than the repo owner’s git default.
- The agent never commits without asking and never bulk-deletes the store — that’s part of the protocol.
- The CLI is a separate surface. The rules above fence the HTTP write API. The
store-writing commands (
comments done/reopen,journal add) are local commands you — or an agent in your terminal — run deliberately: no server, no port, no network. They write atomically, touch one comment at a time, andcomments verifyaudits the result.
The public artifact is the mirror image: no write API, no store data, no identity — nothing to gate, because nothing is built.