fix(ui): English labels where Voyage shows Norwegian to every user

Operator decision 56 (2026-10-17): the product shows English.

- Annotation HTML: the intent buttons, sidebar badges and the copied
  prompt read Fix / Change / Question. The stored keys stay
  fiks / endre / spørsmål, so annotations saved earlier still load.
- /trekreview project-memory approval: "Add the line" / "Not now".
- README, CLAUDE.md, docs/architecture.md, package.json and plugin.json
  describe what the product shows; README's hollow-answer line and the
  "anslag" in the profile section are English; a Norwegian code comment
  in profile-validator.mjs translated.

The Norwegian hollow-answer list in approval-log.mjs and trekbrief.md
stays: it matches what a user types. Both new tests failed on the code
before the change. Suite 2606 (2604/0/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-17 01:00:34 +02:00
commit af5de3fcb8
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
10 changed files with 48 additions and 26 deletions

View file

@ -1,6 +1,6 @@
{
"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, pencil-toggle annotation pattern): select text or click any element, pick intent (Fiks/Endre/Spørsmål), write comment, copy structured prompt, paste back, Claude revises the .md.",
"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, pencil-toggle annotation pattern): select text or click any element, pick intent (Fix/Change/Question), write comment, copy structured prompt, paste back, Claude revises the .md.",
"version": "6.4.0",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -30,7 +30,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
| Command | Description | Orchestrator model |
|---------|-------------|--------------------|
| `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up) produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` («høres bra ut» is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session |
| `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up) produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` ("sounds good" is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session |
| `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session |
| `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session |
| `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session |

View file

@ -160,7 +160,7 @@ Interactive requirements-gathering command. Runs a **question round sized to the
**Ceremony by size.** The task is put in one of three classes, `small`, `bounded` or `architectural` (the heaviest when in doubt), said aloud, overridable by you, and only ever moved up when the task proves bigger. The class sets the question budget (a `small` task: at most 4 questions, in one round) and the review cap, and is written to the brief as `ceremony:`.
**The question round.** Facts are looked up in the repo, never asked. Every question that can be answered now is asked at once, the recommended answer first, ranked by impact × uncertainty; what the budget leaves out is on a visible Deferred list, and what can be predicted is written down as an assumption you see before approving. «Høres bra ut», «gjør som du vil» and an empty answer decide nothing.
**The question round.** Facts are looked up in the repo, never asked. Every question that can be answered now is asked at once, the recommended answer first, ranked by impact × uncertainty; what the budget leaves out is on a visible Deferred list, and what can be predicted is written down as an assumption you see before approving. A hollow answer ("sounds good", "do as you like") or an empty one decides nothing.
Then `brief-reviewer` scores the draft on six dimensions (completeness, consistency, testability, scope clarity, research-plan validity, memory alignment) and gates publication, up to 1 / 2 / 3 review iterations by class; force-stop yields a `brief_quality: partial` brief with the failing dimensions documented.
@ -705,8 +705,8 @@ annotation surface the operator pointed at:
- **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
- **Three intent buttons:** **Fix** (something is wrong — fix it),
**Change** (the wording / content), **Question** (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.
@ -930,7 +930,7 @@ Lookup order:
A brief's `phase_signals` (written by `/trekbrief`) can set the model for research, plan, execute and review; where it does, it comes before the profile.
See [`docs/profiles.md`](docs/profiles.md) for the decision tree, custom-profile authoring, and cost estimation disclaimer (the per-profile cost numbers are *anslag*, not contractual SLAs).
See [`docs/profiles.md`](docs/profiles.md) for the decision tree, custom-profile authoring, and cost estimation disclaimer (the per-profile cost numbers are estimates, not contractual SLAs).
## Observability (v4.1.0)

View file

@ -733,8 +733,8 @@ carries one line in the format **mistake · fix · how to check**:
**The operator approves every line.** For each proposal, use
`AskUserQuestion` (up to 4 proposals per call), in plain language, showing
the line and its two pieces of evidence. Options: **"Skriv linja inn"**
(recommended: the class has recurred) and **"Ikke nå"**; the operator may
the line and its two pieces of evidence. Options: **"Add the line"**
(recommended: the class has recurred) and **"Not now"**; the operator may
answer with an edited line through "Other". Never write a line the operator
did not approve, and never treat "sounds good" or "do as you like" as a yes
to a line it was not shown.

View file

@ -58,7 +58,7 @@ Doc-consistency test at `tests/lib/doc-consistency.test.mjs` pins agent-table co
**Operator-UX guarantee (since v5.0.2):** `/trekbrief`, `/trekplan`, and `/trekreview` MUST always emit (a) a plain `file://<abs path>` URL AND (b) a copy-pasteable `open file://<abs path>` command in the final report block. The file:// URL must use an ABSOLUTE path (not relative or `~/`-prefixed) so terminals with cmd+click support (Ghostty, iTerm2, modern Terminal.app) can resolve it without shell interpretation. This is a non-negotiable operator-UX contract — the doc-consistency test pins both forms in all three commands' final report blocks.
**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://<abs path>` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print) and modelled on an existing annotation surface the operator pointed at. 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:<abs path>`). 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 annotation 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.
**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://<abs path>` link. The HTML is self-contained (zero npm deps, zero external network, design-system-styled, light + dark + print) and modelled on an existing annotation surface the operator pointed at. 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 (Fix / Change / Question)**, 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:<abs path>`). 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 annotation 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:** 3-layer defense-in-depth: plugin hooks (pre-bash-executor, pre-write-executor), prompt-level denylist (holds even if a hook does not fire), pre-execution plan scan (Phase 2.4). Hard Rules 14-16 enforce verify command security, repo-boundary writes, and sensitive path protection.

View file

@ -22,8 +22,8 @@
// PROFILE_NOT_FOUND — file does not exist
//
// Allowed model values: ['sonnet', 'opus', 'fable']. Haiku is allowed only when
// VOYAGE_ALLOW_HAIKU=1 (per global CLAUDE.md modellvalg-prinsipp: Haiku skal
// ikke brukes som default; eksplisitt opt-in for spesielle bruksmønstre).
// VOYAGE_ALLOW_HAIKU=1 (the model-choice rule: Haiku is never a default, only
// an explicit opt-in for special use).
import { readFileSync, existsSync } from 'node:fs';
import { parseDocument } from '../util/frontmatter.mjs';

View file

@ -2,7 +2,7 @@
"name": "voyage",
"version": "6.4.0",
"private": true,
"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, pencil-toggle annotation pattern): 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.",
"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, pencil-toggle annotation pattern): select text or click any heading/paragraph/list-item, pick intent (Fix/Change/Question), write comment, copy structured prompt, paste back, Claude revises the .md.",
"type": "module",
"engines": {
"node": ">=18"

View file

@ -7,7 +7,7 @@
// 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
// (Fix / Change / Question), 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.
//
@ -16,7 +16,9 @@
// localStorage persistence, structured markdown export).
//
// • Operator drives every annotation. No Claude-generated suggestions.
// • Three intent categories: Fiks (fix) / Endre (change) / Spørsmål (question).
// • Three intent categories: Fix / Change / Question. The stored keys stay
// fiks / endre / spørsmål, so annotations saved before the English labels
// (operator decision 56) still load.
// • 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).
@ -256,9 +258,9 @@ function buildHtml(artifactPath, mdText) {
+ ' <blockquote class="ann-form-snippet-text" id="ann-form-snippet">…</blockquote>\n'
+ ' </div>\n'
+ ' <div class="ann-form-intents">\n'
+ ' <button class="ann-intent" data-intent="fiks" title="Something is wrong or broken — needs to be fixed">Fiks</button>\n'
+ ' <button class="ann-intent" data-intent="endre" title="Change the wording or content">Endre</button>\n'
+ ' <button class="ann-intent" data-intent="spørsmål" title="An open question or clarification request">Spørsmål</button>\n'
+ ' <button class="ann-intent" data-intent="fiks" title="Something is wrong or broken — needs to be fixed">Fix</button>\n'
+ ' <button class="ann-intent" data-intent="endre" title="Change the wording or content">Change</button>\n'
+ ' <button class="ann-intent" data-intent="spørsmål" title="An open question or clarification request">Question</button>\n'
+ ' </div>\n'
+ ' <textarea class="ann-form-comment" id="ann-form-comment" placeholder="Your comment (optional but helpful)…"></textarea>\n'
+ ' <div class="ann-form-actions">\n'
@ -527,7 +529,7 @@ body.ann-mode .article [data-anchor-id].annotated:hover { outline-color: var(--a
const APP_JS = `
const STORAGE_KEY = 'voyage-annotate:v2:' + ARTIFACT_PATH;
const INTENT_LABELS = { fiks: 'Fiks', endre: 'Endre', 'spørsmål': 'Spørsmål' };
const INTENT_LABELS = { fiks: 'Fix', endre: 'Change', 'spørsmål': 'Question' };
const INTENT_ORDER = ['fiks', 'endre', 'spørsmål'];
let annotations = [];
@ -783,8 +785,8 @@ function buildPromptMarkdown() {
return a.id - b.id;
});
let p = 'Please revise the voyage artifact at \\\`' + ARTIFACT_PATH + '\\\` with the operator annotations below.\\n';
p += 'Each annotation has an intent — **Fiks** (something is wrong / fix it), **Endre** (change wording/content),\\n';
p += 'or **Spørsmål** (operator question — clarify or answer). The quote shows what the operator anchored to.\\n';
p += 'Each annotation has an intent — **Fix** (something is wrong / fix it), **Change** (change wording/content),\\n';
p += 'or **Question** (operator question — clarify or answer). The quote shows what the operator anchored to.\\n';
p += 'Treat the operator notes as authoritative direction.\\n\\n';
p += '## Annotations (' + annotations.length + ' total)\\n\\n';
let n = 0;
@ -943,7 +945,7 @@ if (isMain(import.meta.url)) {
'Usage: annotate <artifact.md> [--out <file.html>]\n\n'
+ 'Builds a self-contained operator-annotation HTML for a voyage\n'
+ 'artifact. The operator opens the HTML, selects text or clicks any\n'
+ 'element, picks an intent (Fiks / Endre / Spørsmål), writes a\n'
+ 'element, picks an intent (Fix / Change / Question), writes a\n'
+ 'comment, and copies a structured prompt to paste back into Claude.\n'
+ 'Annotations persist in localStorage per artifact path.\n\n'
+ 'Default output: <input-basename>.html next to input.\n',

View file

@ -192,3 +192,10 @@ test('armB workflow — a missing reviewer or engine forbids ALLOW on the Workfl
assert.ok(src.includes("if (verdict === 'ALLOW' && missingReviewers > 0) verdict = 'WARN';"));
assert.ok(src.includes('enginePayloads'));
});
test('project-memory approval: the options the operator sees are English (operator decision 56)', () => {
const src = read('commands/trekreview.md');
assert.ok(src.includes('Options: **"Add the line"**'), 'the recommended option reads "Add the line"');
assert.ok(src.includes('and **"Not now"**'), 'the other option reads "Not now"');
assert.ok(!src.includes('Skriv linja inn') && !src.includes('Ikke nå'), 'no Norwegian option label left');
});

View file

@ -15,7 +15,7 @@
// • render() is deterministic — two runs produce byte-identical output.
// • Default output path is <input-basename>.html next to the input.
// • The v5.0.3 affordances are wired into the HTML: pencil-toggle, form
// popover with three intent buttons (Fiks/Endre/Spørsmål), annotations
// popover with three intent buttons (Fix/Change/Question), annotations
// sidebar, Copy Prompt button, Clear all, localStorage persistence.
import { test } from 'node:test';
@ -146,7 +146,7 @@ test('parseArgs handles --out, positional input, and --help', () => {
test('buildHtml wires the v5.0.3 operator-driven annotation affordances', () => {
// Pin every UX-critical affordance modelled on the reference annotation surface:
// - Pencil-toggle button (annotation mode on/off)
// - Form popover with three intent buttons (Fiks/Endre/Spørsmål)
// - Form popover with three intent buttons (Fix/Change/Question)
// - Annotations sidebar (Your annotations + Clear all + Copy Prompt)
// - Selection capture (window.getSelection())
// - Section context auto-detection (findSection)
@ -157,9 +157,9 @@ test('buildHtml wires the v5.0.3 operator-driven annotation affordances', () =>
assert.ok(html.includes('ann-toggle'), 'must have the pencil-toggle button');
assert.ok(html.includes('Annotation mode: ON'), 'must label the toggle state');
// Form + intents (the three CSS classes for selected state)
assert.ok(html.includes('data-intent="fiks"'), 'must have Fiks intent button');
assert.ok(html.includes('data-intent="endre"'), 'must have Endre intent button');
assert.ok(html.includes('data-intent="spørsmål"'), 'must have Spørsmål intent button');
assert.ok(html.includes('data-intent="fiks"'), 'must have the Fix intent button');
assert.ok(html.includes('data-intent="endre"'), 'must have the Change intent button');
assert.ok(html.includes('data-intent="spørsmål"'), 'must have the Question intent button');
// Form popover
assert.ok(html.includes('ann-form'), 'must have the form popover');
assert.ok(html.includes('ann-form-comment'), 'must have a comment textarea');
@ -179,6 +179,19 @@ test('buildHtml wires the v5.0.3 operator-driven annotation affordances', () =>
assert.ok(anchors >= 5, 'must emit data-anchor-id on enough elements (got ' + anchors + ')');
});
test('buildHtml shows the intents in English and keeps the stored intent keys (operator decision 56)', () => {
const html = buildHtml('/abs/path/brief.md', SAMPLE);
// What the reader sees: button text, sidebar badge labels, the copied prompt.
assert.ok(html.includes('data-intent="fiks"') && html.includes('>Fix</button>'), 'Fix button over the fiks key');
assert.ok(html.includes('data-intent="endre"') && html.includes('>Change</button>'), 'Change button over the endre key');
assert.ok(html.includes('data-intent="spørsmål"') && html.includes('>Question</button>'), 'Question button over the spørsmål key');
assert.ok(html.includes("fiks: 'Fix', endre: 'Change', 'spørsmål': 'Question'"), 'sidebar and prompt labels are English');
assert.ok(html.includes('**Fix**') && html.includes('**Change**') && html.includes('**Question**'), 'the copied prompt names the English intents');
for (const word of ['>Fiks<', '>Endre<', '>Spørsmål<', "'Fiks'", "'Endre'", "'Spørsmål'", '**Fiks**', '**Endre**', '**Spørsmål**']) {
assert.ok(!html.includes(word), `no Norwegian label left: ${word}`);
}
});
test('renderMarkdown produces headings, lists, code, table, blockquote with anchors', () => {
const html = renderMarkdown(`# H1
## H2