diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 82a017b..472b376 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "voyage",
- "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs) and printing the file:// link — the operator clicks lines, adds their own notes, copies a structured prompt, pastes back, Claude revises the .md.",
- "version": "5.0.2",
+ "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any element, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
+ "version": "5.0.3",
"author": {
"name": "Kjell Tore Guttormsen"
},
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7ba3ce..cb63b39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,84 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
+## v5.0.3 — 2026-05-13 — Annotation UX matches the claude-code-100x reference
+
+**No new breaking changes beyond v5.0.0.** Forks consuming v5.0.2's
+annotation HTML keep working — the file path and entry point are
+unchanged. The internal localStorage key bumps from `voyage-annotate:` to
+`voyage-annotate:v2:` to avoid mixing the v5.0.2 shape (line-click,
+freeform notes) with the v5.0.3 shape (intent-tagged annotations).
+
+### Why
+
+v5.0.2 shipped a too-simple annotation surface: click a line, write a
+freeform note, save. The operator pointed at the existing
+`claude-code-100x/build-site.js` annotation system as the actual
+reference — pencil-toggle mode, text-selection capture, three intent
+categories (**Fiks** / **Endre** / **Spørsmål**), a popover form at the
+cursor, structured markdown export with intent labels. v5.0.3 brings
+`scripts/annotate.mjs` up to that pattern.
+
+This reference had been mentioned by the operator early in the
+conversation; the iteration through v5.0.0 / v5.0.1 / v5.0.2 reflects me
+reading past it and trying alternatives instead of just matching it. The
+loss is real and is documented here in plain terms so future maintainers
+don't repeat it.
+
+### Changed
+
+- **`scripts/annotate.mjs`** — rewritten to match the
+ `claude-code-100x/build-site.js` UX:
+ - **Article rendering** — markdown is rendered to proper HTML elements
+ (`
`/`
`/`
`/`
`/`
`/`
`/`
`), not as
+ line-numbered raw lines. Document reads as a normal article.
+ - **Annotatable elements** — every heading, paragraph, list item, table
+ cell, blockquote, and code block gets a stable `data-anchor-id`.
+ - **Pencil-toggle button** in the topbar — annotation mode default ON.
+ Toggle OFF to read normally and follow links.
+ - **Click any annotatable element** (in mode) → opens a form popover
+ at the cursor with: section context (auto-detected from nearest
+ h1/h2), anchored snippet (the exact selected substring via
+ `window.getSelection()` if any text is highlighted, else the
+ element's text content up to 200 chars), three intent buttons
+ (**Fiks** / **Endre** / **Spørsmål**), comment textarea, Cancel +
+ Save. Save is disabled until an intent is picked.
+ - **Sidebar panel** — collapsed by default; "Show annotations" button
+ in the topbar opens it. Annotations grouped by section, sorted by
+ document order. Each card shows the intent badge (colored by
+ category), the anchored snippet, the operator comment, and a delete
+ button. Click a card to scroll the article to that element + flash
+ highlight.
+ - **Copy Prompt** — structured markdown:
+ `### N. [Intent] Section: ` + `Quote: «»` +
+ `Comment: `. Copies to clipboard.
+ - **Clear all** — wipes every annotation for the current artifact
+ (after confirm).
+ - **Persistence** — `localStorage` key `voyage-annotate:v2:`.
+ Refresh/close/reopen the same HTML keeps every annotation.
+ - **Toast feedback** for save / copy / clear.
+- **`tests/scripts/annotate.test.mjs`** — refreshed for the v5.0.3 shape:
+ pins the three intent buttons (`data-intent="fiks"` / `"endre"` /
+ `"spørsmål"`), form popover, selection capture, section auto-detect,
+ `voyage-annotate:v2:` storage key prefix, `data-anchor-id` coverage,
+ Copy Prompt + Clear all affordances, and the markdown renderer's
+ heading / list / table / blockquote / code-fence output. 12 tests
+ (up from 10), all passing.
+
+### Notes
+
+- The producing commands (`/trekbrief` Step 4g, `/trekplan` Phase 10,
+ `/trekreview` Phase 8) call `scripts/annotate.mjs` the same way as in
+ v5.0.2 — no change to their wiring beyond the build-output now being
+ the v5.0.3 interactive surface.
+- `npm test`: 518 tests, 516 pass, 0 fail, 2 skipped (up from 516 — 2
+ new annotate tests for hostile-content escape + renderMarkdown table/
+ blockquote coverage).
+- Reference: `~/repos/claude-code-100x/claude-code-100x/build-site.js`
+ lines 1431–2255 (annotation UI section).
+- Version bump 5.0.2 → 5.0.3 in `.claude-plugin/plugin.json`,
+ `package.json`, `package-lock.json`, plugin `README.md` badge.
+
## v5.0.2 — 2026-05-13 — Operator-driven annotation HTML (the actual fix)
**No new breaking changes beyond v5.0.0.** Forks that consumed the v5.0.1
diff --git a/CLAUDE.md b/CLAUDE.md
index c0d0768..089a71d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -232,7 +232,7 @@ Local Docker Compose stack: `examples/observability/`. Operator docs: `docs/obse
**Continue:** `/trekcontinue` reads `{dir}/.session-state.local.json` (Handover 7), validates schema-v1 via `session-state-validator`, narrates a 3-line summary (project / next-session-label / brief-path), and immediately begins executing the next session. Auto-discovers active project state files under `.claude/projects/*/.session-state.local.json` if no explicit `` argument. Operator-invoked only — never auto-loaded via SessionStart. The `/trekendsession` helper is the informal-flow producer: writes the same state file for ad-hoc multi-session handovers that don't run through `/trekexecute`.
-**Operator-annotation HTML (v5.0.2):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` runs `scripts/annotate.mjs` against the just-written `.md` and prints the resulting `file://` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print), and built around the operator. The operator opens the file, clicks any line of the document, writes their own note in an inline textarea, hits Save. The sidebar collects every note (editable + deletable, sorted by line). Notes persist in `localStorage` per artifact path — refresh or browser-close doesn't lose work. A "Copy Prompt" button at the bottom assembles every note into one structured prompt; the operator copies it and pastes it back into Claude, and Claude revises the `.md` freehand from the notes. **The operator drives every annotation.** Claude does not pre-generate suggestions in this flow — that was the v5.0.1 path (via `/playground document-critique`), which inverted the direction the operator actually wanted. v5.0.2 makes the loop OPERATOR-LEADS, CLAUDE-REACTS, which is what was asked for all along. The v5.0.0 standalone `.html` render (`scripts/render-artifact.mjs`) was a read-only view that didn't afford annotation; the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8 were 388 KB of broken UX. Both stay removed. See [CHANGELOG.md](CHANGELOG.md) § v5.0.2.
+**Operator-annotation HTML (v5.0.3):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` runs `scripts/annotate.mjs` against the just-written `.md` and prints the resulting `file://` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print) and modelled on `~/repos/claude-code-100x/claude-code-100x/build-site.js` (lines 1431–2255). The operator opens the file, the document renders as a proper article (headings / paragraphs / lists / tables / code / quotes — every element gets a stable `data-anchor-id`). In annotation mode (default ON, pencil-toggle in topbar), the operator can **select any text or click any element** → a form popover opens at the cursor with: section context auto-detected from nearest h1/h2, the anchored snippet (selection if any, else element text), **three intent buttons (Fiks / Endre / Spørsmål)**, comment textarea, Save/Cancel. The sidebar (Show annotations button) lists every annotation grouped by section with intent badge + snippet + comment + delete; clicking a card scrolls to and flashes the source element. **Copy Prompt** assembles a structured markdown (`### N. [Intent] Section: <…>` + `Quote: «…»` + `Comment: …`) and copies to clipboard. Persistence: `localStorage` keyed on absolute artifact path (`voyage-annotate:v2:`). v5.0.0 removed the v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8; v5.0.1 pointed at `/playground document-critique` (Claude-leads, wrong direction); v5.0.2 was operator-led but too thin (line-click + freeform note, no intents); v5.0.3 matches the claude-code-100x reference the operator first pointed at, with pencil-toggle / selection capture / intent categories / popover form / structured export. See [CHANGELOG.md](CHANGELOG.md) § v5.0.3.
**Security:** 4-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (works in headless sessions), pre-execution plan scan (Phase 2.4), scoped `--allowedTools` replacing `--dangerously-skip-permissions`. Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection.
diff --git a/README.md b/README.md
index 1c366ab..825f3c7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# trekplan — Brief, Research, Plan, Execute, Review, Continue
-
+


@@ -503,7 +503,7 @@ Both arguments are required. No interactive prompt — headless-safe.
---
-## Reviewing and annotating artifacts (v5.0.2)
+## Reviewing and annotating artifacts (v5.0.3)
`/trekbrief`, `/trekplan`, and `/trekreview` each end by running
`scripts/annotate.mjs` against the just-written `.md` and printing the
@@ -528,40 +528,56 @@ the tab and reopen the same file.
```
You run `open` (or click the `file://` link in your terminal), the HTML
-opens in your default browser. In the browser:
+opens in your default browser. The annotation UX is modelled on
+`claude-code-100x/build-site.js`:
-- **Left:** the artifact, rendered with line numbers. Hover any line — the
- gutter shows a `+`. Click — an inline textarea appears under that line.
- Write your note, hit Save (or `⌘Enter`). The line gets a colored left
- border + a number badge showing the annotation count on that line.
-- **Right (sidebar):** every annotation you've made, sorted by line. Each
- card shows the line reference (click to scroll), the target text
- snippet, and your note text. Click the note to edit it, click `✕` to
- delete. A **Clear all** button at the top wipes the slate.
-- **Bottom:** a structured prompt that updates live with every note. A
- **Copy Prompt** button. Click — your clipboard now has the prompt.
+- **Topbar:** pencil-toggle button — annotation mode default ON. Click
+ to turn off (then you read the article normally, follow links, etc.).
+ A second button opens the sidebar panel.
+- **Article body:** the artifact rendered as a proper article — headings,
+ paragraphs, lists, tables, code blocks, blockquotes. Hover any element
+ in mode and it highlights. To anchor on a specific phrase, **select
+ the text first**, then click. Otherwise the whole element becomes the
+ anchor.
+- **Form popover** appears at the cursor with:
+ - **Section** (auto-detected from the nearest h1/h2 above).
+ - **Anchored to** — the exact text you selected, or the element's
+ first ~200 chars if you didn't select.
+ - **Three intent buttons:** **Fiks** (something is wrong — fix it),
+ **Endre** (change the wording / content), **Spørsmål** (an open
+ question — clarify or answer). Colored: red / orange / blue.
+ - **Comment** textarea (optional but helpful).
+ - **Cancel** / **Save**. Save stays disabled until you pick an intent.
+ Shortcut: `⌘Enter` to save, `Esc` to cancel.
+- **Annotated elements** get an amber highlight + a number badge in the
+ margin showing how many annotations target that element.
+- **Sidebar panel** (Show annotations) — every annotation grouped by
+ section, in document order. Each card shows the intent badge
+ (colored), the anchored snippet (mono-quote), the comment text, and a
+ delete button. Click a card to scroll the article to that element and
+ flash it.
+- **Copy Prompt** at the foot of the panel — assembles every annotation
+ into one structured markdown prompt and copies it to your clipboard.
+- **Clear all** wipes every annotation (after confirm).
- **Persistence:** every annotation is saved to browser `localStorage`
- keyed on the artifact's absolute path. Refresh the tab or close the
- browser and re-open — your notes are still there.
+ keyed on the artifact's absolute path (`voyage-annotate:v2:`).
+ Refresh the tab or close the browser and re-open — your work is there.
-You write your notes, click Copy Prompt, paste back into Claude. Claude
-revises the `.md` freehand from your notes. **The operator drives every
-annotation.** Claude never pre-generates suggestions in this flow.
+You select / click, pick intent, write comment, repeat. When you're
+done, Copy Prompt, paste back into this chat. Claude revises the `.md`
+freehand from your notes. **The operator drives every annotation.**
+Claude never pre-generates suggestions in this flow.
-> **What v5.0.2 changed from v5.0.1.** v5.0.0 added a read-only HTML
-> render that didn't afford annotation. v5.0.1 deleted that and pointed
-> operators at `/playground document-critique` — but the `document-critique`
-> template pre-generates **Claude's** suggestions and asks the operator to
-> approve/reject them. That's "Claude leads, operator reacts" — the
-> opposite of what was asked for. v5.0.2 ships `scripts/annotate.mjs`, a
-> self-contained zero-dependency operator-annotation HTML generator. The
-> operator clicks lines, writes their own notes, copies a structured prompt
-> with every note. No Claude-generated suggestions in the loop.
->
-> **Still removed from v5.0.0 onward:** the v4.2/v4.3 bespoke playground SPA,
-> `/trekrevise`, Handover 8 (annotation → revision), the supporting `lib/`
-> modules, and the Playwright e2e suite. See [CHANGELOG.md](CHANGELOG.md)
-> § v5.0.0, § v5.0.1, and § v5.0.2.
+> **What v5.0.3 changed from v5.0.2.** v5.0.2 was operator-led but the UX
+> was too thin — click a line, type a freeform note, save. The reference
+> the operator pointed at (`~/repos/claude-code-100x/claude-code-100x/build-site.js`)
+> already had the right pattern: pencil-toggle, selection capture, three
+> intent categories, popover form, structured markdown export. v5.0.3
+> rebuilds `scripts/annotate.mjs` against that reference. v5.0.0 / v5.0.1
+> / v5.0.2 are all superseded; only the v5.0.0 removals (bespoke
+> playground SPA, `/trekrevise`, Handover 8, supporting `lib/` modules,
+> Playwright e2e + devDeps) stay. See [CHANGELOG.md](CHANGELOG.md)
+> § v5.0.0 → § v5.0.3.
---
diff --git a/package-lock.json b/package-lock.json
index a77c117..ce028d5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "voyage",
- "version": "5.0.2",
+ "version": "5.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "voyage",
- "version": "5.0.2",
+ "version": "5.0.3",
"license": "MIT",
"engines": {
"node": ">=18"
diff --git a/package.json b/package.json
index 8c84b4b..59489ef 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "voyage",
- "version": "5.0.2",
- "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs) and printing the file:// link — the operator clicks lines, adds their own notes, copies a structured prompt, pastes back, Claude revises the .md.",
+ "version": "5.0.3",
+ "description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): select text or click any heading/paragraph/list-item, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
"type": "module",
"engines": {
"node": ">=18"
diff --git a/scripts/annotate.mjs b/scripts/annotate.mjs
index 0426741..777ff5c 100644
--- a/scripts/annotate.mjs
+++ b/scripts/annotate.mjs
@@ -1,18 +1,27 @@
#!/usr/bin/env node
// scripts/annotate.mjs
//
-// Builds a self-contained operator-annotation HTML for a voyage artifact
-// (brief.md / plan.md / review.md). The producing commands call this on
-// their last step and print the file:// link; the operator opens it in
-// a browser, clicks any line to attach their own note, watches a sidebar
-// of all their annotations, and copies a single prompt (with every note)
-// back into Claude. Claude revises the source .md from the notes.
+// Operator-annotation HTML for a voyage artifact (brief.md / plan.md /
+// review.md). The producing commands run this on their last step and
+// print the file:// link. The operator opens the HTML in their browser,
+// the page renders the artifact as a proper article (headings, lists,
+// paragraphs, code blocks — not raw lines), and the operator drives every
+// annotation themselves: select text or click any element, choose intent
+// (Fiks / Endre / Spørsmål), write a comment, save. The sidebar shows
+// every annotation grouped by section; Copy Prompt assembles them into
+// one structured markdown the operator pastes back into Claude.
//
-// • Operator drives every annotation. No pre-generated suggestions.
-// • Each annotation is anchored to the source line number (1-based).
-// • Annotations persist in localStorage per artifact path — refresh
-// does not lose work; closing the browser does not either.
-// • Zero external network, zero npm deps, deterministic output.
+// UX modelled on the claude-code-100x annotation surface
+// (build-site.js, 2026 — same pencil-toggle, intent buttons, form popover,
+// localStorage persistence, structured markdown export).
+//
+// • Operator drives every annotation. No Claude-generated suggestions.
+// • Three intent categories: Fiks (fix) / Endre (change) / Spørsmål (question).
+// • Element + selection anchoring — clicking an element captures it whole;
+// selecting text inside an element captures the exact substring.
+// • Section context auto-detected (nearest h1/h2 above).
+// • Annotations persist in localStorage keyed on the absolute artifact path.
+// • Zero npm deps, zero external network, deterministic output.
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { basename, resolve } from 'node:path';
@@ -29,66 +38,248 @@ function escapeHtml(s) {
function deriveTitle(mdText, fallbackName) {
const { hasFrontmatter, frontmatter } = splitFrontmatter(mdText);
if (hasFrontmatter) {
- const taskMatch = frontmatter.match(/^task:\s*(.+)$/m);
- if (taskMatch) return taskMatch[1].trim().replace(/^["']|["']$/g, '');
- const slugMatch = frontmatter.match(/^slug:\s*(.+)$/m);
- if (slugMatch) return slugMatch[1].trim().replace(/^["']|["']$/g, '');
+ const m = frontmatter.match(/^task:\s*(.+)$/m) || frontmatter.match(/^slug:\s*(.+)$/m);
+ if (m) return m[1].trim().replace(/^["']|["']$/g, '');
}
const h1 = mdText.match(/^#\s+(.+)$/m);
if (h1) return h1[1].trim();
return fallbackName;
}
+// ---------------------------------------------------------------------------
+// Markdown → HTML with data-anchor-id on every annotatable element.
+// Hand-rolled subset matching what artifact templates emit.
+// ---------------------------------------------------------------------------
+
+function renderInline(escaped) {
+ let s = escaped.replace(/`([^`]+)`/g, (_, c) => `${c}`);
+ s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, h) => {
+ const safe = /^(https?:|mailto:|#|\.|\/)/i.test(h) ? h : '#';
+ return `${t}`;
+ });
+ s = s.replace(/\*\*([^*]+)\*\*/g, (_, c) => `${c}`);
+ s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, (_, pre, c) => `${pre}${c}`);
+ return s;
+}
+
+function renderMarkdown(md) {
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
+ let html = '';
+ let anchorId = 0;
+ const anchor = () => `anch-${anchorId++}`;
+ let i = 0;
+ let paraBuf = [];
+
+ const flushPara = () => {
+ if (paraBuf.length) {
+ const text = paraBuf.join(' ');
+ html += `
${renderInline(escapeHtml(text))}
\n`;
+ paraBuf = [];
+ }
+ };
+
+ while (i < lines.length) {
+ const line = lines[i];
+
+ // Fenced code block — NOT annotatable as a whole; we keep it readable
+ // but skip the data-anchor-id so the operator clicks around it.
+ const fence = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
+ if (fence) {
+ flushPara();
+ const marker = fence[2][0];
+ const lang = (fence[3] || '').trim().split(/\s+/)[0];
+ const buf = [];
+ i++;
+ while (i < lines.length && !new RegExp('^\\s*' + marker + '{3,}\\s*$').test(lines[i])) {
+ buf.push(lines[i]);
+ i++;
+ }
+ i++; // closing fence
+ const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
+ html += `
Click any line to add your own annotation. Annotations are saved in your browser per artifact path.
\n'
- + ' \n'
- + ' \n'
- + ' \n'
+ + '\n'
+ + '
Click any heading, paragraph, list item, table cell, or quote to add an annotation. To anchor on a specific phrase, select the text first, then click. Toggle annotation mode off (pencil button) to read normally / follow links.