# Notabene

> notabene docs review loop: process review comments left on the docs and verify the docs. Use when the user says "address the comments", "process the doc comments", "apply the review feedback", "review/check the docs", or references the global /comments page. Reads the notabene store, edits the docs per the feedback, marks them resolved + writes the journal, then verifies (renderer build, links, project checks). Ignores comments on "hold". This skill does NOT install, configure, or launch the review server — that's `notabene-setup`. Never commits without an explicit request.

- Skill: `z29k/notabene` (Agent Skill)
- Install (CLI): `npx skillmds@latest add z29k/notabene`
- Raw SKILL.md: https://api.skillmd.com/api/skills/z29k/notabene/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: z29k (https://skillmd.com/u/z29k)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/z29k/notabene

---


<!-- Generated from docs/reference/agent-protocol.md + packages/claude-plugin/overlays/notabene.md by scripts/gen-protocol.mjs — do not edit. -->

# Docs review loop (comments + verification)

**notabene**: a navigable renderer over a repo's docs + a human↔agent review loop.
**Stateless** — the data lives in the consumer repo's git, not in the tool. What follows
is the generic, agent-agnostic protocol; **three rules override it inside this plugin**:

1. **Not set up here?** No `notabene.config.mjs` or no `.notabene/` store → **hand off to
   the `notabene-setup` skill** (or `/notabene:setup`) to install/configure and launch,
   then resume. Don't fail; delegate — ignore the generic `npx … init` fallback below.
2. **Every CLI call goes through the plugin forwarder** — `node
   "${CLAUDE_PLUGIN_ROOT}/bin/nb.mjs" <cmd> --root <repo-root>` — never `npx notabene`
   (unscoped: not our package) and never a hardcoded version. This replaces **every**
   `npx -y @z29k/notabene@latest …` shown below (`comments ls`, `build`, `lint`,
   `journal add`). The loop still never depends on the CLI: file tools are enough.
3. **Authoring palette** — for what you can put in a page (Mermaid diagrams, GFM tables,
   code blocks, inter-doc links) and the MDX-safety rules, use the **`notabene-authoring`**
   skill rather than the web page linked in Step 4.

## 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 migrate` upgrades a store to the current
  schema (v3, one file per comment).
- **`roots[]`** — the doc spaces: `{ key, label, path, exclude }`. A comment's `page`
  field is prefixed by a root's `path` (e.g. root `docs/plans` → `page:
  "docs/plans/services/x"`).
- **`i18n`** (optional, `{ locales, defaultLocale, strategy }`) — the doc is multi-language
  and a comment's `page` key is **locale-encoded**, mapping straight to that language's file:
  `strategy: "directory"` → `page: "docs/fr/guide/x"` = file `docs/fr/guide/x.md`;
  `strategy: "suffix"` → `page: "docs/guide/x.fr"` = file `docs/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 them **`addressed`**, 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 by `id`
  (edit its page file), never the folder.
- **Ignore `hold: true`** ("⏸ on hold") and `status` ≠ `open` (`addressed`/`resolved`
  already handled): only process `open` **and not on hold**.
- **Account for EVERY comment in the roster** (step 1). A pass is not "some of the
  comments" — each id ends the pass either handled (`resolved`/`addressed`) or
  **explicitly declined**, with the reason posted as a `thread` reply so the human sees
  it. Leaving one silently untouched is a **failed pass**, not a partial success: an
  untouched comment is indistinguishable from one the human wrote a minute ago, so
  nothing downstream can flag it — not `/review`, which only ever shows what you DID,
  and not `comments verify`, for which an `open` comment is perfectly legal. You are the
  only check. A long roster is a reason to work in batches, never a reason to stop early.
- **MDX-safety** (format `"mdx"` only): when editing a **`.mdx`** file, don't introduce
  stray `{` or `<` outside code fences (MDX parses them as expression/JSX). **`.md`**
  files (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. The `astro dev` server need NOT be running — the HTTP `/api/comments` is 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`):

```bash
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
```

**That list is the pass's roster. Take it ONCE, whole, and write the ids down** — into
your task list, a scratch file, whatever survives the pass. Every later step is measured
against it, and step 6 reconciles with it. Neither the CLI nor the HTTP API paginates,
truncates or caps: one call returns every eligible comment, however many there are (the
only ellipsis anywhere is the 100-character quote preview in the *human-readable*
listing — use `--json` and you get the full text). So a short roster means a short store,
never a partial read. **Count the ids and state the number before you start** — a pass
that never named its own size cannot notice it dropped half of it, which is exactly how
a real store ended up with fifteen comments handled and six untouched, in a run that
reported success.

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>.md` or `.mdx`
- **Index page**: if `<x>.{md,mdx}` doesn't exist, it's **`<x>/index.{md,mdx}`** (the
  loader strips `index` from the id → some `data-page` values 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:

```bash
# 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

1. **ALWAYS: build the renderer** — a broken doc file breaks the tool itself
   (`npx -y @z29k/notabene@latest build`, or the project's renderer build).
2. **Lint the inter-doc links** — `npx -y @z29k/notabene@latest lint`. It validates every
   relative `.md` link against the routes the build just emitted (with did-you-mean
   suggestions; `--json` for 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.
3. **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 in `changes[]` makes `/review` show the human an **empty diff**. Exit 1 = fix
   it before reporting.
4. **Reconcile the roster** — re-run `comments ls --open --json` and subtract: **no id
   from step 1's roster may still be there.** Any that is was silently dropped — go back
   and handle it, or decline it with a reply; it is a defect to fix, not a result to
   report. Do not simply check that the list is empty: a comment written *during* your
   pass is legitimately open and must be left alone, which is precisely why the
   comparison is against the roster and not against zero.
5. **`config.verify[]`** — the project's own checks (build/lint/memory update).
6. **Project memory** — if the project keeps a memory doc (`CLAUDE.md`/`AGENTS.md`),
   update it for any public-behavior change.

> Steps 5–6 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)

**Open with the arithmetic of the pass: `N eligible → N handled + N declined`, and make
the three numbers add up.** Say it even when nothing was skipped — a report that cannot
be wrong about its own coverage is what turns "I think it's done" into something the
human can check at a glance. If a comment was declined, name it and say why.

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

