Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f4b79cfc6 | |||
| dfe1986f06 | |||
| 6efcc62b68 | |||
| 113296d7de | |||
| 4504c9a8cf | |||
| d3975c441c | |||
| 56fed8f305 | |||
| 0655b57930 | |||
| bf68fe6f5f | |||
| 8cbb33e1fd | |||
| 4b5a3a24dd | |||
| c03695c97b | |||
| 9ba8b682ef | |||
| 8ea692bc60 |
51 changed files with 2320 additions and 176 deletions
|
|
@ -23,7 +23,7 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"source": "./plugins/voyage",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. /trekbrief, /trekplan, and /trekreview each end by printing a copy-paste-ready /playground document-critique invocation for the produced artifact — one paste launches an interactive annotation HTML in the browser."
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline with specialized agent swarms, external research triangulation, adversarial review, post-hoc independent review with Handover 6 feedback loop, multi-session resumption, session decomposition, and headless execution. /trekbrief, /trekplan, and /trekreview each end by building a self-contained operator-annotation HTML (scripts/annotate.mjs, modelled on claude-code-100x): pencil-toggle annotation mode, select text or click any element, pick intent (Fiks/Endre/Spørsmål), comment, Copy Prompt, paste back, Claude revises the .md."
|
||||
},
|
||||
{
|
||||
"name": "linkedin-thought-leadership",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ plugins/
|
|||
llm-security/ v6.0.0 — Security scanning, auditing, threat modeling
|
||||
ms-ai-architect/ v1.13.1 — Microsoft AI architecture (Cosmo Skyberg persona) + manual KB-refresh slash command
|
||||
okr/ v1.0.0 — OKR guidance for Norwegian public sector
|
||||
voyage/ v5.0.1 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a literal copy-paste-ready `/playground build a document-critique playground for {artifact_path}` invocation — one paste launches an interactive annotation HTML, Copy Prompt button returns the operator notes to Claude, Claude revises the .md. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 dropped the redundant standalone HTML render (`render-artifact.mjs`) and made the /playground invocation literal.
|
||||
voyage/ v5.0.3 — Brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline (six-command universal pipeline + multi-session resumption + --gates autonomy chain). /trekbrief, /trekplan, and /trekreview each end by running scripts/annotate.mjs against the just-written .md and printing the file:// link to a self-contained operator-annotation HTML modelled on claude-code-100x/build-site.js: pencil-toggle annotation mode, select text or click any element, choose intent (Fiks/Endre/Spørsmål), comment, sidebar groups by section with delete + Copy Prompt, localStorage persistence per artifact path. v5.0.0 removed the v4.2/v4.3 bespoke playground + /trekrevise + Handover 8; v5.0.1 pointed at /playground document-critique (wrong direction); v5.0.2 was operator-led but too thin; v5.0.3 matches the reference the operator pointed at from day one.
|
||||
|
||||
shared/
|
||||
playground-design-system/ v0.1 — Aksel/Digdir-aligned CSS design system + JSON schemas + self-hosted Inter/JetBrains Mono/Source Serif 4 fonts (Tier 1+2+3 wave 1+wave 2 = 20 Tier 3 components total). Consumed by ms-ai-architect, okr, llm-security, voyage, config-audit
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -77,11 +77,13 @@ Key commands: `/config-audit posture`, `/config-audit feature-gap`, `/config-aud
|
|||
|
||||
---
|
||||
|
||||
### [Voyage](plugins/voyage/) `v5.0.1`
|
||||
### [Voyage](plugins/voyage/) `v5.1.0`
|
||||
|
||||
Deep requirements gathering, research, implementation planning, self-verifying execution, independent post-hoc review, and zero-friction multi-session resumption — with specialized agent swarms, adversarial review, and failure recovery. Six-command (brief, research, plan, execute, review, continue) universal pipeline. `/trekbrief`, `/trekplan`, and `/trekreview` render their artifact to a self-contained HTML view and print the `file://` link; annotation is delegated to the official `/playground` plugin.
|
||||
Deep requirements gathering, research, implementation planning, self-verifying execution, independent post-hoc review, and zero-friction multi-session resumption — with specialized agent swarms, adversarial review, and failure recovery. Six-command (brief, research, plan, execute, review, continue) universal pipeline + adaptive-depth per-phase effort dialog. `/trekbrief`, `/trekplan`, and `/trekreview` render their artifact to a self-contained HTML view and print the `file://` link.
|
||||
|
||||
v5.0.1 finishes the annotation-UX work that v5.0.0 started: at the end of `/trekbrief`, `/trekplan`, and `/trekreview` the operator now gets a single boxed, copy-paste-ready line — `/playground build a document-critique playground for {artifact_path}` — that launches the official `claude-plugins-official` `playground` skill's `document-critique` template. One paste builds a self-contained HTML with the artifact on the left, per-line Approve/Reject/Comment cards on the right, and a Copy Prompt button at the bottom; copy the generated prompt, paste back, Claude revises the `.md` freehand. v5.0.1 also dropped the v5.0.0 stop-gap `scripts/render-artifact.mjs` (a separate read-only HTML render) and the `npm run render` alias — they were redundant with what `/playground` produces. v5.0.0 (breaking, kept) removed the v4.2/v4.3 bespoke playground SPA, `/trekrevise`, Handover 8, the supporting `lib/` modules (`anchor-parser`, `annotation-digest`, `markdown-write`, `revision-guard`), the Playwright e2e suite, and the `@playwright/test` / `@axe-core/playwright` devDeps — a browser walkthrough had found the bespoke surface borderline unusable, and it duplicated the official `/playground` plugin. Forks depending on the removed surfaces migrate to `/playground`. See `plugins/voyage/CHANGELOG.md` § v5.0.0 + § v5.0.1.
|
||||
v5.1.0 adds Phase 3.5 to `/trekbrief`: 4 tier-coupled `AskUserQuestion` calls commit an effort level (`low | standard | high`) and an optional `model` (`sonnet | opus`) per downstream phase (`research`, `plan`, `execute`, `review`). The choices land in `brief.md` as `phase_signals:` (or `phase_signals_partial: true` on force-stop). `brief_version: 2.1` activates a validator-side sequencing gate (`BRIEF_V51_MISSING_SIGNALS`) so downstream commands halt with a friendly hint when signals are missing. Composition rule per downstream command: brief signal wins per-phase, profile fills gaps. `effort == low` activates each command's existing `--quick`-equivalent code-path (`/trekexecute` low-effort = `--gates open` + sequential-only). Additive — no breaking changes; pre-2.1 briefs still validate. See `plugins/voyage/CHANGELOG.md` § v5.1.0.
|
||||
|
||||
v5.0.3 lands the annotation UX modelled on `~/repos/claude-code-100x/claude-code-100x/build-site.js`: pencil-toggle annotation mode, **select text or click any element to anchor**, choose intent (**Fiks** / **Endre** / **Spørsmål**), write a comment, save. The sidebar groups annotations by section with intent badges; Copy Prompt assembles them into a structured markdown the operator pastes back into Claude. State persists in `localStorage` per artifact path. v5.0.2 was operator-led but too thin (line-click + freeform note, no intent categories). v5.0.1 had pointed at `/playground document-critique` (Claude-leads — wrong direction). v5.0.0 (breaking, kept) removed the v4.2/v4.3 bespoke playground SPA, `/trekrevise`, Handover 8, the supporting `lib/` modules, the Playwright e2e suite, and the `@playwright/test` / `@axe-core/playwright` devDeps. v5.0.3's `scripts/annotate.mjs` is one self-contained zero-dependency Node script. **The operator drives every annotation** — Claude never pre-generates suggestions in this flow. See `plugins/voyage/CHANGELOG.md` § v5.0.0 → § v5.0.3.
|
||||
|
||||
v4.0.0 (breaking) renamed the plugin from `ultraplan-local` to **Voyage** and all commands from `/ultra*-local` to `/trek*` to remove name collision with Anthropic's `/ultraplan` and `/ultrareview` features. See `plugins/voyage/TRADEMARKS.md` and `plugins/voyage/CHANGELOG.md`.
|
||||
|
||||
|
|
@ -94,7 +96,7 @@ Six commands, one pipeline with clear division of labor:
|
|||
- **`/trekreview`** — Close the iteration loop. Independent post-hoc reviewer reads `brief.md` from scratch and evaluates the diff produced by execute. Two parallel reviewers (brief-conformance + code-correctness) plus a Judge Agent (review-coordinator) for dedup and reasonableness filtering. Severity-tagged findings (Critical/High/Medium/Low/Info) with stable 40-char hex IDs feed back into planning via Handover 6 (`/trekplan --brief review.md` → remediation plan with `source_findings:` audit trail).
|
||||
- **`/trekcontinue`** — Zero-friction multi-session resumption. In a fresh chat, type `/trekcontinue` — reads `.session-state.local.json` (Handover 7), prints a 3-line summary, and immediately begins executing the next session. Any session-end mechanism may write the state file (`/trekexecute` Phase 8/2.55/4 do so automatically; `/trekendsession` helper writes it for informal flows). Forward-compat schema (unknown top-level keys ignored) so future producers can extend additively.
|
||||
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a single boxed `/playground build a document-critique playground for {artifact_path}` line. The operator copy-pastes that into Claude, the official `playground` skill (`document-critique` template) builds an interactive HTML, the operator marks Approve/Reject/Comment + clicks Copy Prompt, the operator pastes the prompt back, Claude revises the `.md`.
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` against the just-written `.md`, printing the `file://<abs path>` link to the resulting self-contained operator-annotation HTML. The operator opens it, clicks any line to add their own note, watches a sidebar of every note (editable, deletable, persisted in browser `localStorage`), clicks "Copy Prompt" to get one structured prompt with every note, pastes back into Claude — Claude revises the `.md` from the notes. The operator drives every annotation.
|
||||
|
||||
All artifacts land in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, `review.md`, and `.session-state.local.json` (gitignored). `--project <dir>` works across `/trekresearch`, `/trekplan`, `/trekexecute`, `/trekreview`, and (optionally) `/trekcontinue`.
|
||||
|
||||
|
|
@ -120,7 +122,7 @@ Defense-in-depth security: plugin hooks block destructive commands and sensitive
|
|||
|
||||
Modes: default, brief-driven, project-scoped, research-enriched, foreground, quick, decompose, export, resume
|
||||
|
||||
23 specialized agents · 6 commands (+ 1 helper) · 5 plugin hooks · 500+ tests · Copy-paste-ready `/playground` annotation invocation · No cloud dependency
|
||||
23 specialized agents · 6 commands (+ 1 helper) · 5 plugin hooks · 500+ tests · Operator-driven HTML annotation surface · No cloud dependency
|
||||
|
||||
→ [Full documentation](plugins/voyage/README.md) · [Migration guide](plugins/voyage/MIGRATION.md)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a copy-paste-ready /playground invocation that builds an interactive document-critique HTML for the artifact.",
|
||||
"version": "5.0.1",
|
||||
"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.1.0",
|
||||
"author": {
|
||||
"name": "Kjell Tore Guttormsen"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,6 +4,227 @@ 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.1.0 — 2026-05-13 — Per-phase effort + model dialog
|
||||
|
||||
Additive. No breaking changes. Forward-compat with all v5.0.x briefs.
|
||||
|
||||
### Why
|
||||
|
||||
The voyage pipeline runs a single profile-tier setting for every task. For
|
||||
typo fixes and small bugfixes the full `brief → research → plan → execute
|
||||
→ review` ceremony is over-engineered; for risky migrations the same
|
||||
profile-tier is too thin. v5.1 hands ceremony-level back to the operator
|
||||
per phase in the same dialog that produces the brief — without removing
|
||||
the disciplined defaults that protect high-stakes work. Independent of
|
||||
v4.1's profile system: composition happens at the command level (brief
|
||||
signal wins per-phase, profile fills gaps). No `/trekflow`, no helper
|
||||
module, no per-command effort dictionary — composition is documented
|
||||
prose in each downstream command.
|
||||
|
||||
### Added
|
||||
|
||||
- **`/trekbrief` Phase 3.5** — between Phase 3 completeness exit and
|
||||
Phase 4 draft, 4 tier-coupled `AskUserQuestion` calls commit an effort
|
||||
level (`low | standard | high`) and an optional `model` (`sonnet |
|
||||
opus`) per downstream phase (`research`, `plan`, `execute`, `review`).
|
||||
Tier mapping: `low → {effort: low, model: sonnet}`, `standard →
|
||||
{effort: standard}` (model omitted; composition falls through to
|
||||
profile), `high → {effort: high, model: opus}`. Force-stop pattern
|
||||
(Phase 4f verbatim) records `phase_signals_partial: true` instead.
|
||||
`--quick` skips Phase 3.5 entirely and auto-writes
|
||||
`phase_signals_partial: true`.
|
||||
- **`brief-validator` extension** — 6 new issue codes:
|
||||
`BRIEF_INVALID_PHASE_SIGNALS`, `BRIEF_INVALID_PHASE_SIGNAL_PHASE`,
|
||||
`BRIEF_INVALID_EFFORT`, `BRIEF_INVALID_MODEL`,
|
||||
`BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE`, `BRIEF_V51_MISSING_SIGNALS` +
|
||||
exported `PHASE_SIGNAL_PHASES` + `EFFORT_LEVELS` constants. The
|
||||
`BASE_ALLOWED_MODELS` const in `lib/validators/profile-validator.mjs`
|
||||
was promoted to `export const` so the brief validator can re-use it.
|
||||
- **HANDOVER-CONTRACTS amendments** — Handover 1 gets 5 inserts:
|
||||
versioning row → `2.1`, two new schema-table rows (`phase_signals`,
|
||||
`phase_signals_partial`), v5.1 sequencing-gate validation row,
|
||||
versioning-paragraph expansion explaining the version-conditional
|
||||
gate, 6 new failure-mode bullets.
|
||||
- **Template bump** — `templates/trekbrief-template.md` → `brief_version
|
||||
2.1` with a default `phase_signals:` block (4 phases × `effort:
|
||||
standard`, model omitted) and a commented `phase_signals_partial:
|
||||
true` line showing the force-stop alternative.
|
||||
- **Composition rule (v5.1)** — new `## Composition rule (v5.1)`
|
||||
sub-section in each of `commands/{trekplan,trekresearch,trekexecute,
|
||||
trekreview}.md`. Documents `effort_for_phase = brief.phase_signals[
|
||||
<phase>]?.effort ?? 'standard'` and `model_for_phase =
|
||||
brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]`.
|
||||
Per command: `effort == low` activates that command's existing
|
||||
`--quick`-equivalent code-path (`/trekplan` skips Phase 5 agent swarm,
|
||||
`/trekresearch` inline research, `/trekreview` correctness-only,
|
||||
`/trekexecute` `--gates open` + sequential-only).
|
||||
- **Sequencing-gate surface** in 4 downstream commands — when
|
||||
`brief-validator.mjs` returns `BRIEF_V51_MISSING_SIGNALS` in `errors`,
|
||||
halt with a one-line user-readable message pointing back to
|
||||
`/trekbrief`. Enforcement is validator-only.
|
||||
- **5 new minimal command test files** under `tests/commands/` —
|
||||
`trekbrief.test.mjs` (3 cases), `trekplan.test.mjs` /
|
||||
`trekresearch.test.mjs` / `trekreview.test.mjs` (2 cases each),
|
||||
`trekexecute.test.mjs` (2 cases). Pattern D (read .md, assert prose
|
||||
patterns). Verifies sequencing-gate surface + low-effort prose.
|
||||
- **5 new doc-consistency pins** — template `brief_version 2.1` +
|
||||
`phase_signals:` block, HANDOVER schema rows, voyage CLAUDE.md +
|
||||
README.md mention `phase_signals`.
|
||||
- **2 new fixtures** — `tests/fixtures/brief-with-phase-signals.md` +
|
||||
`brief-without-phase-signals.md` (backward-compat).
|
||||
|
||||
### Changed
|
||||
|
||||
- `brief_version` bumped `2.0 → 2.1`. The bump exists because v2.1
|
||||
activates the **version-conditional sequencing gate** — the only check
|
||||
in the brief validator that triggers on `brief_version` rather than
|
||||
field-presence. The forward-compat policy still applies to the field
|
||||
itself (unknown frontmatter keys flow through).
|
||||
|
||||
### Notes
|
||||
|
||||
- Test count grows by ≥ 17 new cases minimum: 6 brief-validator + 11
|
||||
command-test minimums. Realistic delta is ~25 new cases (Step 6 adds 5
|
||||
doc-consistency pins on top). Target ≥ 533 pass at Step 10 verify.
|
||||
- `MIGRATION.md` was deliberately NOT created — v5.1 is an additive
|
||||
minor (brief_version 2.0 → 2.1, not major). v5.4 may promote
|
||||
`phase_signals` from optional to required (breaking change → 3.0).
|
||||
- High-effort behaviors for `/trekplan` / `/trekresearch` /
|
||||
`/trekreview` are deferred to v5.1.1 per brief Non-Goal ("No complete
|
||||
per-phase effort dictionary"). v5.1 locks only the low-effort floor.
|
||||
- `phase_signals_present` stats emission is also deferred to v5.1.1
|
||||
(opt-in observability per Research 03 Q5).
|
||||
|
||||
## 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
|
||||
(`<h1>`/`<p>`/`<ul>`/`<li>`/`<table>`/`<blockquote>`/`<pre>`), 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: <section>` + `Quote: «<snippet>»` +
|
||||
`Comment: <text>`. Copies to clipboard.
|
||||
- **Clear all** — wipes every annotation for the current artifact
|
||||
(after confirm).
|
||||
- **Persistence** — `localStorage` key `voyage-annotate:v2:<abs path>`.
|
||||
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
|
||||
`/playground document-critique` invocation from the producing commands'
|
||||
final report should switch to opening the `.html` that `scripts/annotate.mjs`
|
||||
now produces directly.
|
||||
|
||||
### Why
|
||||
|
||||
v5.0.0 added a read-only `scripts/render-artifact.mjs` HTML render that
|
||||
didn't afford annotation. v5.0.1 deleted that and pointed operators at
|
||||
`/playground document-critique` instead — but the `document-critique`
|
||||
template pre-generates **Claude's** suggestions and asks the operator to
|
||||
approve/reject them. The operator asked for the opposite: a surface where
|
||||
**they** select content and write **their own** notes, then ship those
|
||||
notes back to Claude. v5.0.1 still missed the actual ask.
|
||||
|
||||
v5.0.2 ships `scripts/annotate.mjs` — a small, focused, zero-dependency
|
||||
Node script that takes any artifact `.md` and writes a self-contained
|
||||
HTML next to it. The HTML renders the document with line numbers, lets
|
||||
the operator click any line to attach their own note, keeps a sidebar of
|
||||
all notes (editable + deletable, persisted in `localStorage` per artifact
|
||||
path so refresh doesn't lose work), and exposes a "Copy Prompt" button
|
||||
that gathers every note into one structured prompt. The operator copies
|
||||
that prompt and pastes it back into Claude; Claude revises the `.md`
|
||||
freehand from the notes. **One file → one HTML → click + write notes →
|
||||
copy prompt → paste back.** No Claude-generated suggestions in the loop.
|
||||
The operator drives every annotation.
|
||||
|
||||
This is the v4.2/v4.3 *concept* (operator-driven annotation) without the
|
||||
broken v4.2/v4.3 UX, without the 388 KB SPA, without `/trekrevise`,
|
||||
without anchor parsers + Handover 8 + the JSON batch round-trip. ~430
|
||||
lines of self-contained `.mjs`. Zero npm deps. Deterministic.
|
||||
|
||||
### Added
|
||||
|
||||
- **`scripts/annotate.mjs`** — operator-annotation HTML generator. Takes `<artifact.md>`, writes `<artifact>.html` (or `--out <file>`). Self-contained, design-system-aligned (light + dark + print), zero external network, deterministic. CLI: `node scripts/annotate.mjs <artifact.md> [--out <file.html>]`. Also `npm run annotate -- <artifact.md>`.
|
||||
- **`tests/scripts/annotate.test.mjs`** (10 tests) — self-contained HTML shape, no external `<link>`/`<script src>`, inline script parses, source content + path embedded, HTML escaping in title + body (XSS surface), determinism, default output path, arg parsing, and the operator-driven affordances (Click any line, Your annotations sidebar, Copy Prompt, Clear all, localStorage).
|
||||
- **`npm run annotate`** convenience script.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`commands/trekbrief.md` Step 4g, `commands/trekplan.md` Phase 10, `commands/trekreview.md` Phase 8** — each now runs `scripts/annotate.mjs` after the artifact is final and prints the resulting `file://<abs path>` link with explicit "Click any line to add YOUR OWN note" instructions. The v5.0.1 `/playground build a document-critique playground for …` line is removed from all three.
|
||||
- **`tests/lib/doc-consistency.test.mjs`** — replaced the v5.0.1 `/playground` pins with v5.0.2 pins: `scripts/annotate.mjs` exists; producing commands invoke it; producing commands no longer print the v5.0.1 `/playground document-critique` line; producing commands signal operator-driven annotation in their prose; CHANGELOG has a v5.0.2 entry.
|
||||
- **Plugin `CLAUDE.md` + `README.md` + root `CLAUDE.md` + root `README.md` + `.claude-plugin/marketplace.json`** — voyage description updated from "v5.0.1 /playground invocation" to "v5.0.2 operator-annotation HTML (`scripts/annotate.mjs`)".
|
||||
|
||||
### Notes
|
||||
|
||||
- `/playground` is unchanged — the official `claude-plugins-official` `playground` skill is great for the Claude-leads, operator-reacts flow; it just wasn't the right tool for operator-leads, Claude-reacts.
|
||||
- `npm test`: 516 tests, 514 pass, 0 fail, 2 skipped (up from 503 — 10 new `annotate.test.mjs` tests + 3 net new doc-consistency pins).
|
||||
- Version bump 5.0.1 → 5.0.2 in `.claude-plugin/plugin.json`, `package.json`, `package-lock.json`, plugin `README.md` badge.
|
||||
|
||||
## v5.0.1 — 2026-05-13 — Drop the standalone HTML render; print a literal /playground invocation
|
||||
|
||||
**No new breaking changes beyond v5.0.0.** Forks that consumed
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
|
|||
|
||||
> **v3.0.0 — architect step extracted from this plugin.** The plan command still auto-discovers `architecture/overview.md` if present, so any compatible producer (architect plugin no longer publicly distributed; the architecture/overview.md slot remains available for any compatible producer) plugs into the same slot. See [CHANGELOG.md](CHANGELOG.md) for migration history.
|
||||
|
||||
> **Trinity context (2026-05-13, informational).** Voyage is Tier 1 (per-task) of a three-tier architecture in active design under the author's private marketplace: Tier 2 `app-creator` (per-app — "what does the app need, what's the next brief?") produces briefs Voyage consumes; Tier 3 `app-factory` (per-portfolio — "which app needs me now?") aggregates state across multiple app-creator instances. Both are pre-implementation and will ship to Forgejo when ready. **Asymmetry is a hard invariant:** Voyage stays unaware of Tier 2/3. Handover 1 (brief format) is the only integration point — any compatible producer can feed Voyage, app-creator is not privileged. Brief-schema changes are therefore breaking changes for downstream consumers, formalized as a public contract in v5.4.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | Model |
|
||||
|
|
@ -16,7 +18,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
|
|||
| `/trekexecute` | Execute — disciplined plan/session-spec executor with failure recovery | opus |
|
||||
| `/trekreview` | Review — independent post-hoc review of delivered code against the brief. Produces `review.md` with severity-tagged findings (Handover 6) | opus |
|
||||
| `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | opus |
|
||||
| `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | sonnet |
|
||||
| `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | opus |
|
||||
|
||||
### /trekbrief modes
|
||||
|
||||
|
|
@ -107,26 +109,26 @@ The triage gate is deterministic — path-pattern classifier produces `{file →
|
|||
| planning-orchestrator | opus | Inline reference documentation for the planning pipeline workflow (brief-driven) |
|
||||
| research-orchestrator | opus | Inline reference documentation for the research pipeline workflow |
|
||||
| review-orchestrator | opus | Inline reference documentation for the review pipeline workflow |
|
||||
| architecture-mapper | sonnet | Codebase structure, tech stack, patterns |
|
||||
| dependency-tracer | sonnet | Import chains, data flow, side effects |
|
||||
| task-finder | sonnet | Task-relevant files, functions, reuse candidates |
|
||||
| risk-assessor | sonnet | Risks, edge cases, failure modes |
|
||||
| test-strategist | sonnet | Test patterns, coverage gaps, strategy |
|
||||
| git-historian | sonnet | Recent changes, ownership, hot files |
|
||||
| research-scout | sonnet | External docs for unfamiliar tech (conditional, planning only) |
|
||||
| convention-scanner | sonnet | Coding conventions: naming, style, error handling, test patterns |
|
||||
| brief-reviewer | sonnet | Task brief quality (5 dimensions: completeness, consistency, testability, scope clarity, research plan validity) |
|
||||
| brief-conformance-reviewer | sonnet | Brief conformance review (SC + Non-Goal traceability) |
|
||||
| code-correctness-reviewer | sonnet | Code correctness review (7 dimensions) |
|
||||
| review-coordinator | sonnet | Judge Agent — dedup + reasonableness filter + verdict |
|
||||
| plan-critic | sonnet | Adversarial plan review (9 dimensions) |
|
||||
| scope-guardian | sonnet | Scope alignment (creep + gaps) |
|
||||
| session-decomposer | sonnet | Splits plans into headless sessions with dependency graph |
|
||||
| docs-researcher | sonnet | Official documentation, RFCs, vendor docs (Tavily, MS Learn) |
|
||||
| community-researcher | sonnet | Community experience: issues, blogs, discussions |
|
||||
| security-researcher | sonnet | CVEs, audit history, supply chain risks |
|
||||
| contrarian-researcher | sonnet | Counter-evidence, overlooked alternatives |
|
||||
| gemini-bridge | sonnet | Gemini Deep Research second opinion (conditional) |
|
||||
| architecture-mapper | opus | Codebase structure, tech stack, patterns |
|
||||
| dependency-tracer | opus | Import chains, data flow, side effects |
|
||||
| task-finder | opus | Task-relevant files, functions, reuse candidates |
|
||||
| risk-assessor | opus | Risks, edge cases, failure modes |
|
||||
| test-strategist | opus | Test patterns, coverage gaps, strategy |
|
||||
| git-historian | opus | Recent changes, ownership, hot files |
|
||||
| research-scout | opus | External docs for unfamiliar tech (conditional, planning only) |
|
||||
| convention-scanner | opus | Coding conventions: naming, style, error handling, test patterns |
|
||||
| brief-reviewer | opus | Task brief quality (5 dimensions: completeness, consistency, testability, scope clarity, research plan validity) |
|
||||
| brief-conformance-reviewer | opus | Brief conformance review (SC + Non-Goal traceability) |
|
||||
| code-correctness-reviewer | opus | Code correctness review (7 dimensions) |
|
||||
| review-coordinator | opus | Judge Agent — dedup + reasonableness filter + verdict |
|
||||
| plan-critic | opus | Adversarial plan review (9 dimensions) |
|
||||
| scope-guardian | opus | Scope alignment (creep + gaps) |
|
||||
| session-decomposer | opus | Splits plans into headless sessions with dependency graph |
|
||||
| docs-researcher | opus | Official documentation, RFCs, vendor docs (Tavily, MS Learn) |
|
||||
| community-researcher | opus | Community experience: issues, blogs, discussions |
|
||||
| security-researcher | opus | CVEs, audit history, supply chain risks |
|
||||
| contrarian-researcher | opus | Counter-evidence, overlooked alternatives |
|
||||
| gemini-bridge | opus | Gemini Deep Research second opinion (conditional) |
|
||||
|
||||
## Quality infrastructure (v3.4.0)
|
||||
|
||||
|
|
@ -187,9 +189,9 @@ Three built-in model profiles plus operator-defined `<custom>.yaml`. Each profil
|
|||
|
||||
| Profile | Brief | Research | Plan | Execute | Review | Continue | Use case |
|
||||
|---------|-------|----------|------|---------|--------|----------|----------|
|
||||
| `economy` | sonnet | sonnet | sonnet | sonnet | sonnet | sonnet | Lowest cost; high-confidence small-scope tasks |
|
||||
| `balanced` (default) | sonnet | sonnet | opus | sonnet | opus | sonnet | Default — opus where reasoning depth pays off |
|
||||
| `premium` | opus | sonnet | opus | sonnet | opus | sonnet | Critical-path planning + review when budget allows |
|
||||
| `economy` | sonnet | sonnet | sonnet | sonnet | sonnet | sonnet | Lowest cost; high-confidence small-scope tasks (operator-opt-in via `--profile economy`) |
|
||||
| `balanced` | sonnet | sonnet | opus | sonnet | opus | sonnet | Mixed — opus where reasoning depth pays off (operator-opt-in via `--profile balanced`) |
|
||||
| `premium` (default) | opus | opus | opus | opus | opus | opus | Maximum quality — Opus on every phase. Default since 2026-05-13 operator request; also the hardcoded resolver default at `lib/profiles/resolver.mjs:145` |
|
||||
|
||||
### Lookup order
|
||||
|
||||
|
|
@ -220,7 +222,9 @@ Local Docker Compose stack: `examples/observability/`. Operator docs: `docs/obse
|
|||
|
||||
## Architecture
|
||||
|
||||
**Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0).
|
||||
**Brief:** 7-phase workflow: Parse mode → Create project dir → Phase 3 completeness loop (section-driven, no question cap) → Phase 3.5 per-phase effort dialog (v5.1) → Phase 4 draft/review/revise with `brief-reviewer` as stop-gate (max 3 iterations; gate = all dimensions ≥ 4 and research plan = 5) → Finalize (`brief.md` on pass, or `brief_quality: partial` on cap/force-stop) → Manual/auto opt-in → Stats. Always interactive. Auto mode runs research + plan inline in the main context (v2.4.0).
|
||||
|
||||
**Phase 3.5 (v5.1) — adaptive-depth signals:** Between Phase 3 completeness exit and Phase 4 draft, the operator commits an effort level (`low | standard | high`) and an optional `model` (`sonnet | opus`) per downstream phase (`research`, `plan`, `execute`, `review`) via 4 tier-coupled `AskUserQuestion` calls. The choices land in `brief.md` frontmatter as `phase_signals:` (a list of `{phase, effort?, model?}` entries) when committed, or `phase_signals_partial: true` when the operator force-stops. `brief_version: 2.1` activates the **sequencing gate**: validator emits `BRIEF_V51_MISSING_SIGNALS` if a 2.1-versioned brief lacks both fields. Downstream commands surface a friendly hint pointing back to `/trekbrief` — enforcement is validator-only. Composition is documented prose in each downstream command's `## Composition rule (v5.1)` section: `brief.phase_signals[phase] > profile.phase_models[phase]`. The brief signal wins per-phase when present; the profile fills gaps. `effort == low` activates each command's existing `--quick`-equivalent code-path (`/trekexecute` low-effort = `--gates open` + sequential-only). High-effort behavior is deferred to v5.1.1 per brief Non-Goal.
|
||||
|
||||
**Research:** Foreground workflow (v2.4.0): Parse mode → Interview → Parallel research swarm (5 local + 4 external + 1 bridge, spawned from main context) → Follow-ups → Triangulation → Synthesis + brief → Stats. With `--project`, writes to `{dir}/research/NN-slug.md`.
|
||||
|
||||
|
|
@ -232,27 +236,32 @@ 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 `<project-dir>` 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`.
|
||||
|
||||
**Post-command annotation invocation (v5.0.1):** the last step of `/trekbrief`, `/trekplan`, and `/trekreview` prints — verbatim — a copy-paste-ready `/playground` command pointing at the just-written artifact. Concretely: `/playground build a document-critique playground for {abs_path}/{artifact}.md`. When the operator pastes that into Claude, the official `claude-plugins-official` `playground` skill loads its `document-critique` template, reads the `.md`, generates per-line suggestions, and writes a single self-contained HTML file with the artifact nicely formatted on the left, Approve/Reject/Comment cards on the right, and a "Copy Prompt" button at the bottom. The operator marks suggestions, clicks Copy Prompt, pastes the prompt back into Claude — Claude revises the artifact freehand from the notes. **One paste → playground → mark → copy → paste back.** The v5.0.0 standalone `.html` render (`scripts/render-artifact.mjs`) was a separate read-only view that did not afford annotation; v5.0.1 dropped it because it was redundant with the `/playground` HTML (which already shows the artifact nicely) and the two HTMLs created operator confusion. The v4.2/v4.3 bespoke playground SPA + `/trekrevise` + Handover 8 — all removed in v5.0.0 — were never re-introduced. See [CHANGELOG.md](CHANGELOG.md) § v5.0.1.
|
||||
**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 `~/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:<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 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.
|
||||
|
||||
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a copy-paste-ready `/playground build a document-critique playground for {artifact_path}` command — the operator pastes it into Claude to launch the official `playground` skill (`document-critique` template), which builds an interactive HTML for browser annotation. All `.md` artifacts live in one project directory.
|
||||
**Pipeline:** `/trekbrief` produces the task brief. `/trekresearch --project <dir>` fills in `{dir}/research/`. `/trekplan --project <dir>` reads brief + research to produce `{dir}/plan.md` (and auto-discovers `{dir}/architecture/overview.md` if an opt-in upstream architect plugin produced one). `/trekexecute --project <dir>` executes and writes `{dir}/progress.json`. `/trekreview --project <dir>` produces `{dir}/review.md`. `/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` on the just-written artifact, producing `{dir}/{artifact}.html` — a self-contained operator-annotation surface — and printing the `file://` link. The operator opens it, clicks lines, writes their own notes, copies a structured prompt, pastes back, Claude revises the `.md`. All artifacts live in one project directory.
|
||||
|
||||
**Project-directory contract (v3.0.0):** trekplan owns the directory layout below. The `architecture/` subdirectory is opt-in and produced by an opt-in upstream architect plugin (not bundled) — the architect plugin is no longer publicly distributed, but the `architecture/overview.md` slot remains available for any compatible producer.
|
||||
|
||||
```
|
||||
.claude/projects/{YYYY-MM-DD}-{slug}/
|
||||
brief.md ← trekbrief writes; everyone reads
|
||||
brief.html ← trekbrief annotates (operator-annotation HTML, gitignored, re-buildable from brief.md)
|
||||
research/*.md ← trekresearch writes; plan + architect read
|
||||
architecture/ ← OPT-IN, owned by an opt-in upstream architect plugin (not bundled)
|
||||
overview.md
|
||||
gaps.md
|
||||
plan.md ← trekplan writes; trekexecute reads
|
||||
plan.html ← trekplan annotates
|
||||
progress.json ← trekexecute writes
|
||||
review.md ← trekreview writes; trekplan reads (Handover 6)
|
||||
review.html ← trekreview annotates
|
||||
```
|
||||
|
||||
When an operator runs the `/playground build a document-critique playground for <artifact>` command that the producing commands print, the `playground` skill writes the resulting HTML into its own working directory (typically next to the project dir or in `/tmp/`); voyage does not own the `.html` filename. The annotation HTML is ephemeral — re-built on each invocation from the current `.md` source.
|
||||
The `.html` files (`brief.html`, `plan.html`, `review.html`) are produced by `scripts/annotate.mjs` and live alongside their `.md` siblings in the project directory. They are re-buildable from the `.md` source at any time (deterministic, byte-identical output on re-run), so they are conventionally gitignored along with the rest of `.claude/projects/`. Operator annotations live in browser `localStorage` keyed on the absolute artifact path — they survive refresh and browser-close, but are local to the operator's machine.
|
||||
|
||||
No code-level dependency between plugins — the contract is filesystem-level only.
|
||||
|
||||
|
|
@ -260,13 +269,13 @@ No code-level dependency between plugins — the contract is filesystem-level on
|
|||
|
||||
All artifacts in one project directory (default):
|
||||
- Project root: `.claude/projects/{YYYY-MM-DD}-{slug}/`
|
||||
- `brief.md` (task brief from `/trekbrief`)
|
||||
- `brief.md` + `brief.html` (task brief from `/trekbrief`; `.html` is the operator-annotation surface from `scripts/annotate.mjs`)
|
||||
- `research/{NN}-{slug}.md` (research briefs from `/trekresearch --project`)
|
||||
- `architecture/overview.md` + `architecture/gaps.md` (opt-in, produced by an opt-in upstream architect plugin, not bundled)
|
||||
- `plan.md` (from `/trekplan --project`)
|
||||
- `plan.md` + `plan.html` (from `/trekplan --project`)
|
||||
- `sessions/session-*.md` (from `--decompose`)
|
||||
- `progress.json` (from `/trekexecute --project`)
|
||||
- `review.md` (from `/trekreview --project`)
|
||||
- `review.md` + `review.html` (from `/trekreview --project`)
|
||||
- `.session-state.local.json` (Handover 7 — gitignored via `*.local.json`; written by `/trekexecute` Phase 8/2.55/4 or `/trekendsession`; read by `/trekcontinue`)
|
||||
|
||||
Legacy paths (still work without `--project`):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# trekplan — Brief, Research, Plan, Execute, Review, Continue
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
|
@ -10,6 +10,8 @@
|
|||
|
||||
A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep implementation planning, multi-source research, autonomous execution, independent post-hoc review, and zero-friction multi-session resumption. Six commands, one pipeline:
|
||||
|
||||
> **What's new in v5.1** — `/trekbrief` Phase 3.5 commits per-phase `phase_signals` (effort + optional model for `research`/`plan`/`execute`/`review`) to `brief.md` frontmatter. `brief_version: 2.1` activates a validator-side sequencing gate (`BRIEF_V51_MISSING_SIGNALS`) so downstream commands halt with a friendly hint when signals are missing. Composition rule per downstream command: brief signal wins per-phase, profile fills gaps. `effort == low` activates the existing `--quick`-equivalent code-path in each command (`/trekexecute` low-effort = `--gates open` + sequential). Additive — no breaking changes; pre-2.1 briefs still validate.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|-------------|
|
||||
| **`/trekbrief`** | Brief — interactive interview produces a task brief with explicit research plan |
|
||||
|
|
@ -19,7 +21,7 @@ A [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugin for deep
|
|||
| **`/trekreview`** | Review — independent post-hoc review of delivered code against the brief, severity-tagged findings |
|
||||
| **`/trekcontinue`** | Continue — read `.session-state.local.json` and resume the next session in a multi-session project |
|
||||
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a single copy-paste-ready `/playground build a document-critique playground for <artifact_path>` command. Paste it into Claude, the official `playground` skill builds a browser-readable HTML with per-line Approve/Reject/Comment annotations, copy the generated prompt, paste back — Claude revises the artifact. See [Reviewing and annotating artifacts](#reviewing-and-annotating-artifacts-v501).
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by running `scripts/annotate.mjs` against the just-written artifact and printing the resulting `file://<abs path>` link. The operator opens the HTML in a browser, clicks any line of the document, writes their own note in the inline textarea, watches a sidebar of all notes (editable, deletable, persisted in browser `localStorage`), and clicks "Copy Prompt" to get one structured prompt that they paste back into Claude — Claude then revises the `.md` from the notes. **The operator drives every annotation.** See [Reviewing and annotating artifacts](#reviewing-and-annotating-artifacts-v502).
|
||||
|
||||
Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{slug}/` contains `brief.md`, `research/NN-*.md`, `plan.md`, `sessions/`, `progress.json`, and `review.md`.
|
||||
|
||||
|
|
@ -503,55 +505,81 @@ Both arguments are required. No interactive prompt — headless-safe.
|
|||
|
||||
---
|
||||
|
||||
## Reviewing and annotating artifacts (v5.0.1)
|
||||
## Reviewing and annotating artifacts (v5.0.3)
|
||||
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by printing a single
|
||||
copy-paste-ready command. After they finish, you see something like:
|
||||
`/trekbrief`, `/trekplan`, and `/trekreview` each end by running
|
||||
`scripts/annotate.mjs` against the just-written `.md` and printing the
|
||||
resulting `file://<abs path>` link. After they finish you see something
|
||||
like:
|
||||
|
||||
```
|
||||
Brief written: .claude/projects/2026-05-13-foo/brief.md
|
||||
Annotation HTML: file:///abs/path/.claude/projects/2026-05-13-foo/brief.html
|
||||
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this brief, copy and paste this into Claude:
|
||||
To review and annotate this brief, open the HTML above in a browser:
|
||||
|
||||
/playground build a document-critique playground for .claude/projects/2026-05-13-foo/brief.md
|
||||
open file:///abs/path/.claude/projects/2026-05-13-foo/brief.html
|
||||
|
||||
That builds a self-contained HTML file with the brief on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises brief.md from your notes.
|
||||
Click any line to add YOUR OWN note. The sidebar collects every note,
|
||||
the "Copy Prompt" button gathers them into one structured prompt.
|
||||
Paste that prompt back into this chat and Claude revises brief.md
|
||||
from your notes. Annotations persist in your browser if you close
|
||||
the tab and reopen the same file.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
You copy that one `/playground …` line, paste it into Claude, and the
|
||||
official `claude-plugins-official` `playground` skill takes over: it loads
|
||||
its `document-critique` template, reads the `.md`, analyses it, generates
|
||||
per-line suggestions, and writes a single self-contained HTML file that
|
||||
opens in your browser. In the browser:
|
||||
You run `open` (or click the `file://` link in your terminal), the HTML
|
||||
opens in your default browser. The annotation UX is modelled on
|
||||
`claude-code-100x/build-site.js`:
|
||||
|
||||
- **Left:** the artifact, nicely formatted, with line numbers and per-line
|
||||
highlight bars for any line a suggestion targets.
|
||||
- **Right:** suggestion cards (Approve / Reject / Comment), filterable by
|
||||
status, with a counter in the header.
|
||||
- **Bottom:** a live-updated prompt covering everything you approved or
|
||||
commented on, with a **Copy Prompt** button.
|
||||
- **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 (`voyage-annotate:v2:<abs path>`).
|
||||
Refresh the tab or close the browser and re-open — your work is there.
|
||||
|
||||
You mark what you want, click Copy Prompt, paste it back into Claude, and
|
||||
Claude revises the `.md` artifact freehand from your notes. **One paste
|
||||
→ playground → mark → copy → paste back.**
|
||||
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.1 changed from v5.0.0.** v5.0.0 had `/trekbrief`, `/trekplan`,
|
||||
> and `/trekreview` each render their `.md` to a separate read-only `.html`
|
||||
> view via `scripts/render-artifact.mjs` *and* print a vague "run the
|
||||
> `/playground` plugin" instruction. In practice the read-only `.html` was
|
||||
> redundant with what `/playground` produces (which also shows the artifact
|
||||
> nicely formatted), and the instruction wasn't copy-paste-ready — operators
|
||||
> had to guess the right invocation. v5.0.1 drops `render-artifact.mjs` and
|
||||
> its `.html` output, and makes the printed `/playground` invocation
|
||||
> literal and copy-paste-ready.
|
||||
>
|
||||
> **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 and § v5.0.1.
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -685,7 +713,7 @@ The `pre-compact-flush.mjs` hook directly fixes the documented P0 in `docs/treke
|
|||
|
||||
**Infrastructure-as-code (IaC) gets reduced value.** The exploration agents are designed for application code. Terraform, Helm, Pulumi, CDK projects will get a plan, but agents like `architecture-mapper` and `test-strategist` produce less useful output for IaC. Use trekplan for the structural plan, then supplement IaC-specific steps manually.
|
||||
|
||||
**Annotation requires the official `/playground` skill.** Voyage no longer ships an annotation UI of its own. The pasted `/playground build a document-critique playground for …` command depends on the `claude-plugins-official` `playground` skill being installed in the operator's environment. If it isn't, paste the artifact source `.md` into Claude with a "review this" prompt and revise freehand — the same end-result, just without the visual approve/reject/comment surface.
|
||||
**Annotation HTML requires a desktop browser.** `scripts/annotate.mjs` produces a single self-contained `.html` file you open with `file://` in any modern browser (Chrome / Safari / Firefox / Edge — last two versions). No CDN, no server, no npm runtime deps. State persists in `localStorage` so closing and re-opening the tab keeps your work, but it's local to one browser on one machine — not synced anywhere. If you want to annotate without a browser, paste the `.md` into Claude with "comments inline below" and write notes in chat — same end result, just without the visual surface.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Direct architecture analysis request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: cyan
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ description: |
|
|||
against the task brief — every Success Criterion must trace to delivered
|
||||
code, every Non-Goal must remain unbuilt. Emits findings with rule_keys
|
||||
from the canonical RULE_CATALOGUE. Never praises.
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: magenta
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: |
|
|||
Brief review request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: magenta
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: |
|
|||
cross-file regressions, test coverage gaps, placeholder code, security
|
||||
surface, hidden dependencies. Cites file:line for every finding. Never
|
||||
praises.
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: red
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ description: |
|
|||
finds the practical signal that helps teams make adoption decisions.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: green
|
||||
tools: ["WebSearch", "WebFetch", "mcp__tavily__tavily_search", "mcp__tavily__tavily_research"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ description: |
|
|||
but to ensure the final recommendation is genuinely considered.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: red
|
||||
tools: ["WebSearch", "WebFetch", "mcp__tavily__tavily_search", "mcp__tavily__tavily_research"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: |
|
|||
Direct convention discovery request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: yellow
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Impact analysis request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: blue
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: |
|
|||
microsoft_docs_fetch) that docs-researcher uses for higher-quality results.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: blue
|
||||
tools: ["WebSearch", "WebFetch", "Read", "mcp__tavily__tavily_search", "mcp__tavily__tavily_research", "mcp__microsoft-learn__microsoft_docs_search", "mcp__microsoft-learn__microsoft_docs_fetch"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ description: |
|
|||
Direct request for Gemini research on a complex architectural question triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: magenta
|
||||
tools: ["mcp__gemini-mcp__gemini_deep_research", "mcp__gemini-mcp__gemini_get_research_status", "mcp__gemini-mcp__gemini_get_research_result", "mcp__gemini-mcp__gemini_research_followup"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Git history analysis request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: yellow
|
||||
tools: ["Bash", "Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Plan review request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: red
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ medium: default, large: default) rather than dropping agents.
|
|||
| `research-scout` | Conditional | Conditional | Conditional | External docs (only when unfamiliar tech detected AND not covered by briefs) |
|
||||
| `convention-scanner` | No | Yes | Yes | Coding conventions, naming, style, test patterns |
|
||||
|
||||
**Convention Scanner** — use the `convention-scanner` plugin agent (model: "sonnet")
|
||||
**Convention Scanner** — use the `convention-scanner` plugin agent (model: "opus")
|
||||
for medium+ codebases only. Pass the task description as context.
|
||||
|
||||
**research-scout** — launch conditionally if the task involves technologies, APIs,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Research request for external technology triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: blue
|
||||
tools: ["WebSearch", "WebFetch", "Read"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ description: |
|
|||
applies BOUNDED operations: deduplication, severity ranking, HubSpot
|
||||
Judge filters, Cloudflare reasonableness filter, verdict computation.
|
||||
Synthesis-level inference across files is forbidden in v1.0.
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: yellow
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Risk analysis request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: yellow
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Scope verification request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: magenta
|
||||
tools: ["Read", "Glob", "Grep"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ description: |
|
|||
using CVE databases, OWASP categories, and verified audit reports.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: red
|
||||
tools: ["WebSearch", "WebFetch", "mcp__tavily__tavily_search", "mcp__tavily__tavily_research"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ description: |
|
|||
Plan decomposition request for parallel headless execution.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: green
|
||||
tools: ["Read", "Glob", "Grep", "Write"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ description: |
|
|||
Direct code discovery request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: green
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ description: |
|
|||
Test planning request triggers the agent.
|
||||
</commentary>
|
||||
</example>
|
||||
model: sonnet
|
||||
model: opus
|
||||
color: green
|
||||
tools: ["Read", "Glob", "Grep", "Bash"]
|
||||
---
|
||||
|
|
|
|||
|
|
@ -288,6 +288,118 @@ Phase 3 complete: {N} questions asked across {M} sections.
|
|||
Proceeding to draft and review.
|
||||
```
|
||||
|
||||
## Phase 3.5 — Per-phase effort dialog
|
||||
|
||||
Phase 3.5 is the v5.1 entry-point for **adaptive-depth execution**. After
|
||||
Phase 3 has gathered intent / goal / success criteria / research plan, the
|
||||
operator commits an effort level and (optional) model per downstream phase
|
||||
(`research`, `plan`, `execute`, `review`). The committed signals are written
|
||||
to brief frontmatter as a `phase_signals:` list that the four downstream
|
||||
commands read via their `## Composition rule (v5.1)` section.
|
||||
|
||||
### State requirements
|
||||
|
||||
Before entering Phase 3.5 the following must be populated:
|
||||
|
||||
- `state.intent` — the Phase 3 Intent answer (1+ paragraph)
|
||||
- `state.goal` — the Phase 3 Goal answer
|
||||
- `state.success_criteria` — at least one falsifiable SC
|
||||
- `state.research_plan.topics` — list (may be empty)
|
||||
|
||||
If any are absent: skip Phase 3.5 entirely and write `phase_signals_partial:
|
||||
true` to the draft frontmatter. Do not block.
|
||||
|
||||
### --quick mode
|
||||
|
||||
If the operator launched with `--quick`: skip Phase 3.5 entirely and
|
||||
auto-write `phase_signals_partial: true` to draft frontmatter. The brief
|
||||
will satisfy the v5.1 sequencing gate without going through the dialog.
|
||||
|
||||
### Default-derivation heuristic (LLM judgment, not algorithmic)
|
||||
|
||||
Before each phase question, propose a default tier marked `(default)`. Use
|
||||
these signals — they are weak heuristics, not rules:
|
||||
|
||||
- `research_topics_count` → high (`high`), low (`low`), absent (`low`)
|
||||
- `sc_count` (count of falsifiable SCs) → high (≥6 ⇒ `high`), low (≤2 ⇒ `low`)
|
||||
- Goal complexity: keywords like "rewrite", "migration", "refactor across",
|
||||
"new platform" ⇒ `high`; "typo", "small bugfix", "docs touch-up" ⇒ `low`
|
||||
- Otherwise: `standard`
|
||||
|
||||
Mix these into one proposed default per phase. Document the proposed tier
|
||||
in the question body so the operator sees why it was picked.
|
||||
|
||||
### The loop — 4 tier-coupled AskUserQuestion calls
|
||||
|
||||
Loop over `[research, plan, execute, review]` in order. For each phase,
|
||||
issue one `AskUserQuestion` with 3 options:
|
||||
|
||||
| Option | Maps to phase_signals entry |
|
||||
|--------|----------------------------|
|
||||
| **Low effort** | `{phase: <name>, effort: low, model: sonnet}` |
|
||||
| **Standard (default)** | `{phase: <name>, effort: standard}` *(model omitted — composition falls through to profile)* |
|
||||
| **High effort** | `{phase: <name>, effort: high, model: opus}` |
|
||||
|
||||
The proposed tier per phase (from the default-derivation heuristic) MUST be
|
||||
labelled `(default)` in the option list so the operator can one-click
|
||||
accept. Commit the chosen tier immediately to an in-memory `effort_state`
|
||||
dict — no bulk summary-before-commit. The loop is interruptible.
|
||||
|
||||
The mapping table is canonical:
|
||||
- `low → {effort: low, model: sonnet}` (force sonnet for the low-cost path)
|
||||
- `standard → {effort: standard}` (model omitted; composition rule resolves via profile)
|
||||
- `high → {effort: high, model: opus}` (force opus for the high-confidence path)
|
||||
|
||||
### Force-stop handling
|
||||
|
||||
If during any of the four `AskUserQuestion` calls the operator says "stop",
|
||||
"skip", "enough", "just write it", or similar, do NOT exit silently — apply
|
||||
the Phase 4f force-stop pattern verbatim:
|
||||
|
||||
```
|
||||
You stopped before committing per-phase signals. Remaining phases:
|
||||
- {list of phases not yet answered}
|
||||
|
||||
The brief will still be valid (v5.1 supports `phase_signals_partial: true`
|
||||
as a force-stop record). Downstream commands will fall back to the profile
|
||||
resolver for the un-committed phases.
|
||||
|
||||
Continue anyway?
|
||||
```
|
||||
|
||||
Then `AskUserQuestion`:
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| **Answer one more phase** | Return to the next un-answered phase question. |
|
||||
| **Stop now (record partial)** | Drop any in-progress `effort_state` and set `phase_signals_partial: true` in draft frontmatter. Mutually exclusive with `phase_signals`. Break Phase 3.5. |
|
||||
|
||||
This pattern matches Step 4f (line 436-458) so the force-stop UX is
|
||||
identical across both surfaces.
|
||||
|
||||
### Hand-off to Phase 4a
|
||||
|
||||
If `effort_state` is fully populated (4 commits, no force-stop): write a
|
||||
`phase_signals:` block to draft frontmatter — one entry per phase,
|
||||
preserving the canonical-mapping form above. Omit `model:` for standard
|
||||
tier (composition falls through to profile).
|
||||
|
||||
If `phase_signals_partial: true` was set: write that single line to draft
|
||||
frontmatter and skip the `phase_signals:` block (mutually exclusive per
|
||||
validator).
|
||||
|
||||
Phase 4a (Step 4a — Draft in memory) reads from `effort_state` /
|
||||
`phase_signals_partial` and incorporates the appropriate frontmatter block
|
||||
into the draft brief.
|
||||
|
||||
### Sequencing gate (downstream)
|
||||
|
||||
`brief_version: 2.1` activates the validator's sequencing gate. If the
|
||||
final brief reaches `/trekplan`, `/trekresearch`, `/trekexecute`, or
|
||||
`/trekreview` WITHOUT `phase_signals` and WITHOUT `phase_signals_partial:
|
||||
true`, the validator emits `BRIEF_V51_MISSING_SIGNALS` and the command
|
||||
halts with a friendly hint pointing back to `/trekbrief`.
|
||||
|
||||
## Phase 4 — Draft, review, and revise
|
||||
|
||||
Phase 4 runs a **draft → brief-reviewer → revise** loop. The draft is
|
||||
|
|
@ -483,34 +595,46 @@ If the validator returns errors, report them to the user and offer to
|
|||
re-enter Phase 4 with the validator's hints in scope. If only warnings,
|
||||
note them in the final report.
|
||||
|
||||
**Report — single block, printed verbatim** (substitute the bracketed
|
||||
fields, but keep the structure and the `/playground` invocation literal —
|
||||
operators copy-paste that line directly into Claude). The `/playground`
|
||||
command points at the official `claude-plugins-official` `playground`
|
||||
skill, which loads its `document-critique` template, reads the brief,
|
||||
generates per-line suggestions, and writes a single self-contained HTML
|
||||
file that opens in the browser. The HTML has the brief on the left
|
||||
(nicely formatted, line-numbered), suggestion cards on the right
|
||||
(Approve / Reject / Comment), and a "Copy Prompt" button at the bottom
|
||||
that gathers everything marked into one prompt. Paste that prompt back
|
||||
into Claude — Claude then revises `brief.md` freehand from the notes.
|
||||
**Build the operator-annotation HTML, then print the report.** After the
|
||||
brief is validated, run `scripts/annotate.mjs` to produce a self-contained
|
||||
HTML file the operator opens in their browser. The HTML renders the brief
|
||||
with line numbers, lets the operator click any line to attach their own
|
||||
note (not Claude-generated suggestions — the operator drives every
|
||||
annotation), keeps a sidebar of all notes, persists state in localStorage,
|
||||
and exposes a "Copy Prompt" button that generates a single structured
|
||||
prompt with every note. The operator copies that prompt and pastes it
|
||||
back into Claude; Claude revises `brief.md` freehand from the notes.
|
||||
|
||||
```bash
|
||||
ANNOT_HTML=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/annotate.mjs "{PROJECT_DIR}/brief.md" 2>&1)
|
||||
# stdout is the absolute path to the .html on success.
|
||||
```
|
||||
|
||||
If `annotate.mjs` exits non-zero, surface a one-line warning and continue
|
||||
— the annotation HTML is a convenience, not a gate. The report below
|
||||
still mentions the (failed) path so the operator can debug.
|
||||
|
||||
Then print this block **verbatim** (substitute `{PROJECT_DIR}` and
|
||||
`$ANNOT_HTML`):
|
||||
|
||||
```
|
||||
Brief written: {PROJECT_DIR}/brief.md
|
||||
Annotation HTML: file://{$ANNOT_HTML}
|
||||
Review iterations: {1..3}
|
||||
Final quality: {complete | partial}
|
||||
Validator: {PASS | warnings(N)}
|
||||
Research topics identified: {N}
|
||||
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this brief, copy and paste this into Claude:
|
||||
To review and annotate this brief, open the HTML above in a browser:
|
||||
|
||||
/playground build a document-critique playground for {PROJECT_DIR}/brief.md
|
||||
open file://{$ANNOT_HTML}
|
||||
|
||||
That builds a self-contained HTML file with the brief on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises brief.md from your notes.
|
||||
Click any line to add YOUR OWN note. The sidebar collects every note,
|
||||
the "Copy Prompt" button gathers them into one structured prompt.
|
||||
Paste that prompt back into this chat and Claude revises brief.md
|
||||
from your notes. Annotations persist in your browser if you close
|
||||
the tab and reopen the same file.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name: trekendsession
|
||||
description: Mark the current session as complete and write session-state pointing at the next session. Helper for informal multi-session flows.
|
||||
argument-hint: "<next-brief-path> <next-label> | --help"
|
||||
model: sonnet
|
||||
model: opus
|
||||
---
|
||||
|
||||
# Voyage End-Session Local v1.0
|
||||
|
|
|
|||
|
|
@ -1519,6 +1519,37 @@ VOYAGE_PROFILE=balanced /trekexecute --project ...
|
|||
Stats records emit `profile`, `phase_models`, and `profile_source` per
|
||||
Phase 9 record.
|
||||
|
||||
## Composition rule (v5.1)
|
||||
|
||||
Independent of the profile system. When `brief.md` carries
|
||||
`phase_signals` (brief_version ≥ 2.1), each downstream phase resolves
|
||||
effort + model as:
|
||||
|
||||
```
|
||||
effort_for_phase = brief.phase_signals[<phase>]?.effort ?? 'standard'
|
||||
model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]
|
||||
```
|
||||
|
||||
The brief signal wins per-phase when present; the profile fills any
|
||||
gaps. There is no helper module — composition is documented prose in
|
||||
each downstream command.
|
||||
|
||||
For `/trekexecute` specifically: `effort == 'low'` activates `--gates open`
|
||||
+ sequential-only execution (no worktree-isolated parallel waves — runs
|
||||
all sessions in a single foreground loop). `effort == 'standard'` (or
|
||||
absent) → no change (default execution strategy applies). High-effort
|
||||
behavior is deferred to v5.1.1 per brief Non-Goal.
|
||||
|
||||
### Sequencing gate surface
|
||||
|
||||
When `/trekexecute --project <dir>` is invoked, optionally run
|
||||
`brief-validator.mjs --soft --json` against `{dir}/brief.md`. If
|
||||
`BRIEF_V51_MISSING_SIGNALS` appears in `errors` (brief_version ≥ 2.1
|
||||
without `phase_signals` or `phase_signals_partial: true`), halt with:
|
||||
`Brief is brief_version 2.1 but does not carry phase_signals — re-run
|
||||
/trekbrief to commit them (Phase 3.5).` Enforcement is validator-only;
|
||||
commands surface, don't re-enforce.
|
||||
|
||||
## Hard rules
|
||||
|
||||
1. **No AskUserQuestion for execution decisions.** All execution decisions come
|
||||
|
|
|
|||
|
|
@ -501,7 +501,7 @@ ownership, hot files, and active branches that may affect planning."
|
|||
|
||||
### Launch for medium+ codebases (50+ files):
|
||||
|
||||
**Convention Scanner** — use the `convention-scanner` plugin agent (model: "sonnet")
|
||||
**Convention Scanner** — use the `convention-scanner` plugin agent (model: "opus")
|
||||
for medium+ codebases only.
|
||||
Provide concrete examples from the codebase, not generic advice."
|
||||
|
||||
|
|
@ -538,7 +538,7 @@ Common reasons for deep-dives:
|
|||
- A test pattern was identified but the test infrastructure needs more detail
|
||||
- A risk was flagged but the actual impact needs verification
|
||||
|
||||
For each significant gap, spawn a targeted deep-dive agent (model: "sonnet",
|
||||
For each significant gap, spawn a targeted deep-dive agent (model: "opus",
|
||||
subagent_type: "Explore") with a narrow, specific brief.
|
||||
|
||||
Launch up to 3 deep-dive agents in parallel. If no gaps exist, skip this phase
|
||||
|
|
@ -769,30 +769,43 @@ If the user asks questions or requests changes:
|
|||
- Show what changed
|
||||
- Re-present the summary
|
||||
|
||||
### Print the annotation invocation
|
||||
### Build the operator-annotation HTML and print the link
|
||||
|
||||
After the plan summary, print this block **verbatim** (substituting only
|
||||
`{plan_path}` with the absolute path). The `/playground` command must
|
||||
appear literally — operators copy-paste it directly into Claude. It
|
||||
points at the official `claude-plugins-official` `playground` skill,
|
||||
which loads its `document-critique` template, reads `plan.md`, generates
|
||||
per-line suggestions, and writes a single self-contained HTML file that
|
||||
opens in the browser. The HTML has the plan on the left (nicely
|
||||
formatted, line-numbered), suggestion cards on the right (Approve /
|
||||
Reject / Comment), and a "Copy Prompt" button at the bottom that gathers
|
||||
everything marked into one prompt. Paste that prompt back into Claude —
|
||||
Claude then revises `plan.md` freehand from the notes.
|
||||
After the plan summary, run `scripts/annotate.mjs` to produce a
|
||||
self-contained HTML the operator opens in their browser. The HTML renders
|
||||
`plan.md` with line numbers, lets the operator click any line to attach
|
||||
their own note (not Claude-generated suggestions — the operator drives
|
||||
every annotation), keeps a sidebar of all notes, persists state in
|
||||
localStorage, and exposes a "Copy Prompt" button that generates a single
|
||||
structured prompt with every note. The operator copies that prompt and
|
||||
pastes it back into Claude; Claude revises `plan.md` freehand from the
|
||||
notes.
|
||||
|
||||
```bash
|
||||
ANNOT_HTML=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/annotate.mjs "{plan_path}" 2>&1)
|
||||
# stdout is the absolute path to the .html on success.
|
||||
```
|
||||
|
||||
If `annotate.mjs` exits non-zero, surface a one-line warning and continue
|
||||
— the annotation HTML is a convenience, not a gate.
|
||||
|
||||
Then print this block **verbatim** (substituting `{plan_path}` and
|
||||
`$ANNOT_HTML`):
|
||||
|
||||
```
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this plan, copy and paste this into Claude:
|
||||
Plan written: {plan_path}
|
||||
Annotation HTML: file://{$ANNOT_HTML}
|
||||
|
||||
/playground build a document-critique playground for {plan_path}
|
||||
To review and annotate the plan, open it in a browser:
|
||||
|
||||
That builds a self-contained HTML file with the plan on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises plan.md from your notes.
|
||||
open file://{$ANNOT_HTML}
|
||||
|
||||
Click any line to add YOUR OWN note. The sidebar collects every note,
|
||||
the "Copy Prompt" button gathers them into one structured prompt.
|
||||
Paste that prompt back into this chat and Claude revises plan.md
|
||||
from your notes. Annotations persist in your browser if you close
|
||||
the tab and reopen the same file.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
```
|
||||
|
||||
|
|
@ -881,6 +894,36 @@ VOYAGE_PROFILE=balanced /trekplan --project ...
|
|||
Stats records emit `profile`, `phase_models`, `parallel_agents`, and
|
||||
`profile_source` so operators can audit which profile drove which session.
|
||||
|
||||
## Composition rule (v5.1)
|
||||
|
||||
Independent of the profile system. When `brief.md` carries
|
||||
`phase_signals` (brief_version ≥ 2.1), each downstream phase resolves
|
||||
effort + model as:
|
||||
|
||||
```
|
||||
effort_for_phase = brief.phase_signals[<phase>]?.effort ?? 'standard'
|
||||
model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]
|
||||
```
|
||||
|
||||
The brief signal wins per-phase when present; the profile fills any
|
||||
gaps. There is no helper module — composition is documented prose in
|
||||
each downstream command.
|
||||
|
||||
For `/trekplan` specifically: `effort == 'low'` activates the existing
|
||||
`--quick`-equivalent code-path (skip Phase 5 agent swarm — plan directly
|
||||
without exploration agents). `effort == 'standard'` (or absent) → no
|
||||
change. High-effort behavior is deferred to v5.1.1 per brief Non-Goal
|
||||
("No complete per-phase effort dictionary").
|
||||
|
||||
### Sequencing gate surface
|
||||
|
||||
Phase 1 already calls `brief-validator.mjs --soft`. If the validator
|
||||
returns `BRIEF_V51_MISSING_SIGNALS` in `errors` (brief_version ≥ 2.1
|
||||
without `phase_signals` or `phase_signals_partial: true`), halt with a
|
||||
one-line message: `Brief is brief_version 2.1 but does not carry phase_signals
|
||||
— re-run /trekbrief to commit them (Phase 3.5).` Enforcement is
|
||||
validator-only; this surface just makes the friendly hint readable.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Brief-driven**: Every plan decision must trace back to a section of the
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ other agents — the value of Gemini is independence.
|
|||
### Launch rules
|
||||
|
||||
- Launch ALL selected agents **in parallel** in a single message
|
||||
- Use model: "sonnet" for all sub-agents (the orchestrator runs on Opus)
|
||||
- Use model: "opus" for all sub-agents (the orchestrator runs on Opus)
|
||||
- Scale maxTurns by codebase size for local agents (same as trekplan):
|
||||
small = halved, medium/large = default
|
||||
- convention-scanner: medium+ codebases only (50+ files)
|
||||
|
|
@ -301,7 +301,7 @@ other agents — the value of Gemini is independence.
|
|||
Review all agent results. Identify knowledge gaps — areas where findings are
|
||||
thin, contradictory, or missing.
|
||||
|
||||
For each significant gap, launch a targeted follow-up agent (model: "sonnet")
|
||||
For each significant gap, launch a targeted follow-up agent (model: "opus")
|
||||
with a narrow, specific brief. Maximum 2 follow-ups.
|
||||
|
||||
If no gaps exist, skip: "Initial research sufficient — no follow-ups needed."
|
||||
|
|
@ -435,6 +435,36 @@ Stats records emit `profile`, `phase_models`, `parallel_agents`,
|
|||
`external_research_enabled`, and `profile_source` so operators can audit
|
||||
which profile drove which session.
|
||||
|
||||
## Composition rule (v5.1)
|
||||
|
||||
Independent of the profile system. When `brief.md` carries
|
||||
`phase_signals` (brief_version ≥ 2.1), each downstream phase resolves
|
||||
effort + model as:
|
||||
|
||||
```
|
||||
effort_for_phase = brief.phase_signals[<phase>]?.effort ?? 'standard'
|
||||
model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]
|
||||
```
|
||||
|
||||
The brief signal wins per-phase when present; the profile fills any
|
||||
gaps. There is no helper module — composition is documented prose in
|
||||
each downstream command.
|
||||
|
||||
For `/trekresearch` specifically: `effort == 'low'` activates the
|
||||
existing `--quick`-equivalent code-path (inline research, no agent swarm).
|
||||
`effort == 'standard'` (or absent) → no change. High-effort behavior is
|
||||
deferred to v5.1.1 per brief Non-Goal.
|
||||
|
||||
### Sequencing gate surface
|
||||
|
||||
When `/trekresearch --project <dir>` is invoked and `{dir}/brief.md`
|
||||
exists, optionally run `brief-validator.mjs --soft --json` against it.
|
||||
If `BRIEF_V51_MISSING_SIGNALS` appears in `errors` (brief_version ≥ 2.1
|
||||
without `phase_signals` or `phase_signals_partial: true`), halt with:
|
||||
`Brief is brief_version 2.1 but does not carry phase_signals — re-run
|
||||
/trekbrief to commit them (Phase 3.5).` Enforcement is validator-only;
|
||||
commands surface, don't re-enforce.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **No planning:** This command produces research briefs, not implementation plans.
|
||||
|
|
|
|||
|
|
@ -262,6 +262,20 @@ Append a stats line to `${CLAUDE_PLUGIN_DATA}/trekreview-stats.jsonl`
|
|||
If `${CLAUDE_PLUGIN_DATA}` is unset or not writable, skip stats silently.
|
||||
Never let stats failures block the main workflow.
|
||||
|
||||
**Build the operator-annotation HTML.** After stats land, run:
|
||||
|
||||
```bash
|
||||
ANNOT_HTML=$(node ${CLAUDE_PLUGIN_ROOT}/scripts/annotate.mjs "{review_path}" 2>&1)
|
||||
```
|
||||
|
||||
`stdout` is the absolute path to the `.html` on success. The HTML renders
|
||||
`review.md` with line numbers, lets the operator click any line to attach
|
||||
their own note (not Claude-generated suggestions — the operator drives
|
||||
every annotation), keeps a sidebar of all notes, persists state in
|
||||
localStorage, and exposes a "Copy Prompt" button. If `annotate.mjs`
|
||||
exits non-zero, surface a one-line warning and continue — the annotation
|
||||
HTML is a convenience, not a gate.
|
||||
|
||||
## Phase 8.5 — Validate-only mode (`--validate`)
|
||||
|
||||
When `mode == validate`:
|
||||
|
|
@ -282,6 +296,7 @@ After the write succeeds, print:
|
|||
**Brief:** {brief_path}
|
||||
**Project:** {project_dir}
|
||||
**Review:** {review_path}
|
||||
**Annotation HTML:** file://{$ANNOT_HTML}
|
||||
**Scope:** {before_sha}..{after_sha} ({reviewed_files_count} files)
|
||||
**Verdict:** {BLOCK | WARN | ALLOW}
|
||||
|
||||
|
|
@ -297,14 +312,15 @@ After the write succeeds, print:
|
|||
{up to 5 highest-severity findings}
|
||||
|
||||
────────────────────────────────────────────────────────────────────
|
||||
To review and annotate this review, copy and paste this into Claude:
|
||||
To review and annotate the review, open it in a browser:
|
||||
|
||||
/playground build a document-critique playground for {review_path}
|
||||
open file://{$ANNOT_HTML}
|
||||
|
||||
That builds a self-contained HTML file with the review on the left,
|
||||
per-line approve/reject/comment annotations on the right, and a
|
||||
"Copy Prompt" button at the bottom. Copy the generated prompt, paste
|
||||
it back here, and Claude revises review.md from your notes.
|
||||
Click any line to add YOUR OWN note. The sidebar collects every note,
|
||||
the "Copy Prompt" button gathers them into one structured prompt.
|
||||
Paste that prompt back into this chat and Claude revises review.md
|
||||
from your notes. Annotations persist in your browser if you close
|
||||
the tab and reopen the same file.
|
||||
────────────────────────────────────────────────────────────────────
|
||||
|
||||
You can also:
|
||||
|
|
@ -341,6 +357,36 @@ VOYAGE_PROFILE=premium /trekreview --project ...
|
|||
|
||||
Stats records emit `profile` and `profile_source`.
|
||||
|
||||
## Composition rule (v5.1)
|
||||
|
||||
Independent of the profile system. When `brief.md` carries
|
||||
`phase_signals` (brief_version ≥ 2.1), each downstream phase resolves
|
||||
effort + model as:
|
||||
|
||||
```
|
||||
effort_for_phase = brief.phase_signals[<phase>]?.effort ?? 'standard'
|
||||
model_for_phase = brief.phase_signals[<phase>]?.model ?? profile.phase_models[<phase>]
|
||||
```
|
||||
|
||||
The brief signal wins per-phase when present; the profile fills any
|
||||
gaps. There is no helper module — composition is documented prose in
|
||||
each downstream command.
|
||||
|
||||
For `/trekreview` specifically: `effort == 'low'` activates the existing
|
||||
`--quick`-equivalent code-path (skip the brief-conformance reviewer; run
|
||||
correctness-only). `effort == 'standard'` (or absent) → no change.
|
||||
High-effort behavior is deferred to v5.1.1 per brief Non-Goal.
|
||||
|
||||
### Sequencing gate surface
|
||||
|
||||
Phase 1 already calls `brief-validator.mjs --soft` against `{brief_path}`.
|
||||
If the validator returns `BRIEF_V51_MISSING_SIGNALS` in `errors`
|
||||
(brief_version ≥ 2.1 without `phase_signals` or `phase_signals_partial:
|
||||
true`), halt with: `Brief is brief_version 2.1 but does not carry
|
||||
phase_signals — re-run /trekbrief to commit them (Phase 3.5).`
|
||||
Enforcement is validator-only; this surface just makes the friendly hint
|
||||
readable.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Brief is the contract.** Every finding in the review traces to a
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated:
|
|||
|
||||
| Artifact | Field | Current |
|
||||
|---|---|---|
|
||||
| `brief.md` | `brief_version` (frontmatter) | `2.0` |
|
||||
| `brief.md` | `brief_version` (frontmatter) | `2.1` |
|
||||
| `research/*.md` | (implicit; tracked via `type: trekresearch-brief`) | unversioned |
|
||||
| `plan.md` | `plan_version` (frontmatter) | `1.7` |
|
||||
| `progress.json` | `schema_version` (top-level) | `"1"` |
|
||||
|
|
@ -67,6 +67,8 @@ Every validator exposes a CLI: `node lib/validators/<name>.mjs --json <path>` re
|
|||
| `interview_turns` | number | optional | ≥ 0 | |
|
||||
| `source` | string | optional | `interview \| manual` | |
|
||||
| `brief_quality` | string | optional | `complete \| partial` | Set when iteration cap is hit |
|
||||
| `phase_signals` | list | optional (v5.1+) | list of `{phase, effort?, model?}` entries | Per-phase effort + model commitment from Phase 3.5. Mutually exclusive with `phase_signals_partial`. |
|
||||
| `phase_signals_partial` | bool | optional (v5.1+) | `true` | Force-stop record from Phase 3.5. Mutually exclusive with `phase_signals`. |
|
||||
|
||||
**Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time):
|
||||
- `## Intent`
|
||||
|
|
@ -84,11 +86,12 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference
|
|||
| Type discriminator | every read | `type === "trekbrief"` |
|
||||
| Status enum | every read | `research_status ∈ allowed values` |
|
||||
| **State machine** | every read | `research_topics > 0 && research_status === "skipped"` requires `brief_quality === "partial"` |
|
||||
| **v5.1 sequencing gate** | every read | `brief_version ≥ 2.1` requires `phase_signals` (list) OR `phase_signals_partial: true` — error `BRIEF_V51_MISSING_SIGNALS` on miss. Validator-only enforcement; commands surface, don't re-enforce. |
|
||||
| Body sections | strict only | All `BRIEF_BODY_SECTIONS` present |
|
||||
|
||||
**State machine** detail: a brief that says it has research topics but skipped them must explicitly admit it (via `brief_quality: partial`). This is the most common failure mode the validator catches.
|
||||
|
||||
**Versioning:** current is `2.0`. There are no live `1.x` briefs; remove legacy paths in next major.
|
||||
**Versioning:** current is `2.1` (v5.1 — adds optional `phase_signals` + `phase_signals_partial`). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. v5.4 may promote `phase_signals` from optional to required (breaking change → `3.0`).
|
||||
|
||||
**Failure modes:**
|
||||
- `BRIEF_NOT_FOUND` → consumer halts with a usage message
|
||||
|
|
@ -97,6 +100,12 @@ Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preference
|
|||
- `BRIEF_MISSING_FIELD` → strict halt; soft-mode warning
|
||||
- `BRIEF_STATE_INCOHERENT` → strict halt; soft-mode warning (incoherence will haunt downstream agents)
|
||||
- `BRIEF_MISSING_SECTION` → strict halt; soft-mode warning
|
||||
- `BRIEF_V51_MISSING_SIGNALS` → strict halt (v5.1+ sequencing gate); soft-mode warning. Commands surface a friendly hint pointing back to `/trekbrief` (Phase 3.5).
|
||||
- `BRIEF_INVALID_PHASE_SIGNALS` → strict halt; phase_signals must be a list of `{phase, effort?, model?}` entries.
|
||||
- `BRIEF_INVALID_PHASE_SIGNAL_PHASE` → strict halt; phase ∉ `[research, plan, execute, review]`.
|
||||
- `BRIEF_INVALID_EFFORT` → strict halt; effort ∉ `[low, standard, high]`.
|
||||
- `BRIEF_INVALID_MODEL` → strict halt; model ∉ `BASE_ALLOWED_MODELS` (currently `[sonnet, opus]`).
|
||||
- `BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE` → strict halt; cannot set both `phase_signals` and `phase_signals_partial: true`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,15 @@
|
|||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { parseDocument } from '../util/frontmatter.mjs';
|
||||
import { issue, ok, fail } from '../util/result.mjs';
|
||||
import { BASE_ALLOWED_MODELS } from './profile-validator.mjs';
|
||||
|
||||
export const BRIEF_REQUIRED_FRONTMATTER = ['type', 'brief_version', 'task', 'slug', 'research_topics', 'research_status'];
|
||||
export const REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER = ['type', 'task', 'slug', 'project_dir', 'findings'];
|
||||
export const BRIEF_TYPE_VALUES = Object.freeze(['trekbrief', 'trekreview']);
|
||||
export const BRIEF_RESEARCH_STATUS_VALUES = ['pending', 'in_progress', 'complete', 'skipped'];
|
||||
export const BRIEF_BODY_SECTIONS = ['Intent', 'Goal', 'Success Criteria'];
|
||||
export const PHASE_SIGNAL_PHASES = Object.freeze(['research', 'plan', 'execute', 'review']);
|
||||
export const EFFORT_LEVELS = Object.freeze(['low', 'standard', 'high']);
|
||||
|
||||
function getRequiredFields(type) {
|
||||
return type === 'trekreview' ? REVIEW_AS_BRIEF_REQUIRED_FRONTMATTER : BRIEF_REQUIRED_FRONTMATTER;
|
||||
|
|
@ -36,6 +39,67 @@ export function validateBriefContent(text, opts = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
// v5.1 — phase_signals (additive optional field) + version-conditional sequencing gate.
|
||||
// Composition rule documented in each downstream command's "Composition rule (v5.1)" section.
|
||||
const hasSignals = 'phase_signals' in fm;
|
||||
const hasPartial = 'phase_signals_partial' in fm;
|
||||
if (hasSignals && hasPartial) {
|
||||
errors.push(issue(
|
||||
'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE',
|
||||
'phase_signals and phase_signals_partial are mutually exclusive — set exactly one',
|
||||
'Either commit per-phase signals OR record phase_signals_partial: true (force-stop).',
|
||||
));
|
||||
}
|
||||
if (hasSignals) {
|
||||
if (!Array.isArray(fm.phase_signals)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_PHASE_SIGNALS',
|
||||
'phase_signals must be a list of {phase, effort?, model?} entries',
|
||||
));
|
||||
} else {
|
||||
for (const entry of fm.phase_signals) {
|
||||
if (!entry || typeof entry !== 'object' || !('phase' in entry)) {
|
||||
errors.push(issue('BRIEF_INVALID_PHASE_SIGNALS', `phase_signals entry must include a "phase" key`));
|
||||
continue;
|
||||
}
|
||||
if (!PHASE_SIGNAL_PHASES.includes(entry.phase)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_PHASE_SIGNAL_PHASE',
|
||||
`phase_signals.phase "${entry.phase}" not in [${PHASE_SIGNAL_PHASES.join(', ')}]`,
|
||||
));
|
||||
}
|
||||
if ('effort' in entry && !EFFORT_LEVELS.includes(entry.effort)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_EFFORT',
|
||||
`phase_signals.effort "${entry.effort}" not in [${EFFORT_LEVELS.join(', ')}]`,
|
||||
));
|
||||
}
|
||||
if ('model' in entry && !BASE_ALLOWED_MODELS.includes(entry.model)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_INVALID_MODEL',
|
||||
`phase_signals.model "${entry.model}" not in [${BASE_ALLOWED_MODELS.join(', ')}]`,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sequencing gate: brief_version ≥ 2.1 requires phase_signals OR phase_signals_partial.
|
||||
if (typeof fm.brief_version === 'string') {
|
||||
const vm = fm.brief_version.match(/^(\d+)\.(\d+)$/);
|
||||
if (vm) {
|
||||
const major = Number(vm[1]);
|
||||
const minor = Number(vm[2]);
|
||||
const atLeast21 = major > 2 || (major === 2 && minor >= 1);
|
||||
if (atLeast21 && !hasSignals && !hasPartial && fm.type !== 'trekreview') {
|
||||
errors.push(issue(
|
||||
'BRIEF_V51_MISSING_SIGNALS',
|
||||
'brief_version ≥ 2.1 requires phase_signals (or phase_signals_partial: true)',
|
||||
'Re-run /trekbrief — Phase 3.5 collects per-phase effort + model signals.',
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fm.type !== undefined && !BRIEF_TYPE_VALUES.includes(fm.type)) {
|
||||
errors.push(issue(
|
||||
'BRIEF_WRONG_TYPE',
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const PROFILE_REQUIRED_PHASES = Object.freeze([
|
|||
'brief', 'research', 'plan', 'execute', 'review', 'continue',
|
||||
]);
|
||||
|
||||
const BASE_ALLOWED_MODELS = Object.freeze(['sonnet', 'opus']);
|
||||
export const BASE_ALLOWED_MODELS = Object.freeze(['sonnet', 'opus']);
|
||||
|
||||
function getAllowedModels(env = process.env) {
|
||||
if (env.VOYAGE_ALLOW_HAIKU === '1') {
|
||||
|
|
|
|||
4
plugins/voyage/package-lock.json
generated
4
plugins/voyage/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "voyage",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
{
|
||||
"name": "voyage",
|
||||
"version": "5.0.1",
|
||||
"description": "Voyage — brief, research, plan, execute, review, continue. Contract-driven Claude Code pipeline. At the end of /trekbrief, /trekplan, and /trekreview, the operator gets a copy-paste-ready /playground invocation that builds an interactive document-critique HTML for the artifact.",
|
||||
"version": "5.1.0",
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test 'tests/**/*.test.mjs'",
|
||||
"annotate": "node scripts/annotate.mjs",
|
||||
"verify": "bash verify.sh"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
921
plugins/voyage/scripts/annotate.mjs
Normal file
921
plugins/voyage/scripts/annotate.mjs
Normal file
|
|
@ -0,0 +1,921 @@
|
|||
#!/usr/bin/env node
|
||||
// scripts/annotate.mjs
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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';
|
||||
import { splitFrontmatter } from '../lib/util/frontmatter.mjs';
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function deriveTitle(mdText, fallbackName) {
|
||||
const { hasFrontmatter, frontmatter } = splitFrontmatter(mdText);
|
||||
if (hasFrontmatter) {
|
||||
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) => `<code>${c}</code>`);
|
||||
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, h) => {
|
||||
const safe = /^(https?:|mailto:|#|\.|\/)/i.test(h) ? h : '#';
|
||||
return `<a href="${safe}" target="_blank" rel="noopener">${t}</a>`;
|
||||
});
|
||||
s = s.replace(/\*\*([^*]+)\*\*/g, (_, c) => `<strong>${c}</strong>`);
|
||||
s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, (_, pre, c) => `${pre}<em>${c}</em>`);
|
||||
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 += `<p data-anchor-id="${anchor()}">${renderInline(escapeHtml(text))}</p>\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 += `<pre data-anchor-id="${anchor()}"><code${cls}>${escapeHtml(buf.join('\n'))}\n</code></pre>\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ATX heading
|
||||
const h = line.match(/^(#{1,6})\s+(.*?)\s*#*\s*$/);
|
||||
if (h) {
|
||||
flushPara();
|
||||
const lvl = h[1].length;
|
||||
html += `<h${lvl} data-anchor-id="${anchor()}">${renderInline(escapeHtml(h[2]))}</h${lvl}>\n`;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Horizontal rule
|
||||
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
||||
flushPara();
|
||||
html += '<hr>\n';
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Table
|
||||
if (/^\s*\|.*\|\s*$/.test(line) && i + 1 < lines.length &&
|
||||
/^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes('-')) {
|
||||
flushPara();
|
||||
const rows = [];
|
||||
while (i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])) { rows.push(lines[i]); i++; }
|
||||
const cells = (l) => l.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map((c) => c.trim());
|
||||
const header = cells(rows[0]);
|
||||
const body = rows.slice(2).map(cells);
|
||||
html += '<table>\n<thead><tr>';
|
||||
for (const c of header) html += `<th data-anchor-id="${anchor()}">${renderInline(escapeHtml(c))}</th>`;
|
||||
html += '</tr></thead>\n<tbody>\n';
|
||||
for (const r of body) {
|
||||
html += '<tr>';
|
||||
for (let k = 0; k < header.length; k++) html += `<td data-anchor-id="${anchor()}">${renderInline(escapeHtml(r[k] || ''))}</td>`;
|
||||
html += '</tr>\n';
|
||||
}
|
||||
html += '</tbody>\n</table>\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blockquote
|
||||
if (/^\s*>\s?/.test(line)) {
|
||||
flushPara();
|
||||
const buf = [];
|
||||
while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
|
||||
buf.push(lines[i].replace(/^\s*>\s?/, ''));
|
||||
i++;
|
||||
}
|
||||
html += `<blockquote data-anchor-id="${anchor()}">${renderInline(escapeHtml(buf.join(' ')))}</blockquote>\n`;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lists — one block, allow blank lines between items
|
||||
const listMatch = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
||||
if (listMatch) {
|
||||
flushPara();
|
||||
const items = [];
|
||||
while (i < lines.length) {
|
||||
const m = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
|
||||
if (m) {
|
||||
items.push({ indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] });
|
||||
i++;
|
||||
} else if (lines[i].trim() === '' && i + 1 < lines.length &&
|
||||
lines[i + 1].match(/^(\s*)([-*+]|\d+[.)])\s+/)) {
|
||||
i++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
html += renderList(items, anchor);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Blank
|
||||
if (line.trim() === '') {
|
||||
flushPara();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Default: paragraph accumulation
|
||||
paraBuf.push(line.trim());
|
||||
i++;
|
||||
}
|
||||
flushPara();
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderList(items, anchor) {
|
||||
let html = '';
|
||||
const stack = [];
|
||||
for (const { indent, ordered, text } of items) {
|
||||
while (stack.length && (indent < stack[stack.length - 1].indent ||
|
||||
(indent === stack[stack.length - 1].indent && ordered !== stack[stack.length - 1].ordered))) {
|
||||
const top = stack.pop();
|
||||
html += top.ordered ? '</li></ol>' : '</li></ul>';
|
||||
}
|
||||
if (!stack.length || indent > stack[stack.length - 1].indent) {
|
||||
html += ordered ? '<ol>' : '<ul>';
|
||||
stack.push({ indent, ordered });
|
||||
} else {
|
||||
html += '</li>';
|
||||
}
|
||||
html += `<li data-anchor-id="${anchor()}">${renderInline(escapeHtml(text))}`;
|
||||
}
|
||||
while (stack.length) {
|
||||
const top = stack.pop();
|
||||
html += top.ordered ? '</li></ol>' : '</li></ul>';
|
||||
}
|
||||
return html + '\n';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build full HTML document
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildHtml(artifactPath, mdText) {
|
||||
const fileName = basename(artifactPath);
|
||||
const title = deriveTitle(mdText, fileName);
|
||||
const { body } = splitFrontmatter(mdText);
|
||||
const articleHtml = renderMarkdown(body);
|
||||
return '<!DOCTYPE html>\n'
|
||||
+ '<html lang="en">\n'
|
||||
+ '<head>\n'
|
||||
+ '<meta charset="utf-8">\n'
|
||||
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
||||
+ '<title>' + escapeHtml(title) + ' — annotate</title>\n'
|
||||
+ '<style>\n' + STYLE + '\n</style>\n'
|
||||
+ '</head>\n'
|
||||
+ '<body class="ann-mode">\n'
|
||||
+ '<header class="topbar">\n'
|
||||
+ ' <div class="hdr-meta">\n'
|
||||
+ ' <h1>' + escapeHtml(title) + '</h1>\n'
|
||||
+ ' <p class="path" title="' + escapeHtml(artifactPath) + '">' + escapeHtml(fileName) + '</p>\n'
|
||||
+ ' </div>\n'
|
||||
+ ' <div class="hdr-actions">\n'
|
||||
+ ' <button class="ann-toggle" id="ann-toggle" title="Toggle annotation mode (pencil)">✎ <span id="ann-toggle-label">Annotation mode: ON</span> <span class="ann-badge" id="ann-badge">0</span></button>\n'
|
||||
+ ' <button class="ghost-btn" id="open-panel">Show annotations</button>\n'
|
||||
+ ' </div>\n'
|
||||
+ '</header>\n'
|
||||
+ '<main class="article-wrap">\n'
|
||||
+ ' <div class="article-help" id="article-help">Click any heading, paragraph, list item, table cell, or quote to add an annotation. To anchor on a specific phrase, <strong>select the text first</strong>, then click. Toggle annotation mode off (pencil button) to read normally / follow links.</div>\n'
|
||||
+ ' <article class="article" id="article">\n'
|
||||
+ articleHtml
|
||||
+ '\n </article>\n'
|
||||
+ '</main>\n'
|
||||
+ '<div class="ann-form" id="ann-form" role="dialog" aria-label="New annotation">\n'
|
||||
+ ' <div class="ann-form-section">\n'
|
||||
+ ' <div class="ann-form-section-label">Section</div>\n'
|
||||
+ ' <div class="ann-form-section-value" id="ann-form-section">—</div>\n'
|
||||
+ ' </div>\n'
|
||||
+ ' <div class="ann-form-snippet">\n'
|
||||
+ ' <div class="ann-form-section-label">Anchored to</div>\n'
|
||||
+ ' <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'
|
||||
+ ' </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'
|
||||
+ ' <button class="btn" id="ann-form-cancel">Cancel (Esc)</button>\n'
|
||||
+ ' <button class="btn primary" id="ann-form-save" disabled>Save (⌘Enter)</button>\n'
|
||||
+ ' </div>\n'
|
||||
+ '</div>\n'
|
||||
+ '<aside class="ann-panel" id="ann-panel" aria-label="Annotations panel">\n'
|
||||
+ ' <div class="ann-panel-head">\n'
|
||||
+ ' <h2>Your annotations</h2>\n'
|
||||
+ ' <button class="icon-btn" id="ann-panel-close" title="Close">✕</button>\n'
|
||||
+ ' </div>\n'
|
||||
+ ' <div class="ann-panel-body" id="ann-panel-body"></div>\n'
|
||||
+ ' <div class="ann-panel-foot">\n'
|
||||
+ ' <button class="ghost-btn" id="ann-clear-all">Clear all</button>\n'
|
||||
+ ' <button class="btn primary" id="ann-copy" disabled>Copy Prompt</button>\n'
|
||||
+ ' </div>\n'
|
||||
+ '</aside>\n'
|
||||
+ '<div class="ann-toast" id="ann-toast" role="status" aria-live="polite"></div>\n'
|
||||
+ '<div class="ann-overlay" id="ann-overlay"></div>\n'
|
||||
+ '<script>\n'
|
||||
+ 'const ARTIFACT_PATH = ' + JSON.stringify(resolve(artifactPath)) + ';\n'
|
||||
+ 'const ARTIFACT_NAME = ' + JSON.stringify(fileName) + ';\n'
|
||||
+ APP_JS
|
||||
+ '\n</script>\n'
|
||||
+ '</body>\n'
|
||||
+ '</html>\n';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stylesheet — light + dark + print. Design-system-aligned.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STYLE = `
|
||||
:root {
|
||||
--bg: #f7f7f8;
|
||||
--bg-elev: #ffffff;
|
||||
--bg-soft: #ececef;
|
||||
--border: #d6d8dc;
|
||||
--border-strong: #b3b7bd;
|
||||
--text: #1a1a1a;
|
||||
--text-dim: #555a63;
|
||||
--text-mute: #8a8f97;
|
||||
--accent: #0855a8;
|
||||
--accent-soft: #e4ecf6;
|
||||
--amber: #a86b00;
|
||||
--amber-soft: #fbeed1;
|
||||
--green: #1a7f37;
|
||||
--green-soft: #d5ecdb;
|
||||
--red: #b3262d;
|
||||
--red-soft: #f6d9da;
|
||||
--blue: #0855a8;
|
||||
--blue-soft: #e4ecf6;
|
||||
--orange: #d4790a;
|
||||
--orange-soft: #fceede;
|
||||
--purple: #6638b6;
|
||||
--purple-soft: #ebe1f9;
|
||||
--mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
|
||||
--serif: ui-serif, "Source Serif 4", Georgia, "Times New Roman", serif;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #0e1218;
|
||||
--bg-elev: #161b22;
|
||||
--bg-soft: #1c232c;
|
||||
--border: #2a323c;
|
||||
--border-strong: #3b4554;
|
||||
--text: #e5e9ef;
|
||||
--text-dim: #a5adba;
|
||||
--text-mute: #6e7681;
|
||||
--accent: #6db0ee;
|
||||
--accent-soft: rgba(109, 176, 238, 0.15);
|
||||
--amber: #d4a017;
|
||||
--amber-soft: rgba(212, 160, 23, 0.12);
|
||||
--green: #3fb950;
|
||||
--green-soft: rgba(63, 185, 80, 0.12);
|
||||
--red: #f0626a;
|
||||
--red-soft: rgba(240, 98, 106, 0.12);
|
||||
--blue: #6db0ee;
|
||||
--blue-soft: rgba(109, 176, 238, 0.15);
|
||||
--orange: #f6ad55;
|
||||
--orange-soft: rgba(246, 173, 85, 0.15);
|
||||
--purple: #d2a8ff;
|
||||
--purple-soft: rgba(210, 168, 255, 0.15);
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text);
|
||||
font-family: var(--sans); font-size: 15px; line-height: 1.6; }
|
||||
body { min-height: 100vh; }
|
||||
/* Topbar */
|
||||
.topbar { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px; padding: 12px 24px; background: var(--bg-elev); border-bottom: 1px solid var(--border); }
|
||||
.hdr-meta h1 { font-size: 16px; font-weight: 650; margin: 0; }
|
||||
.hdr-meta .path { color: var(--text-dim); font-size: 12px; font-family: var(--mono); margin: 2px 0 0; word-break: break-all; }
|
||||
.hdr-actions { display: flex; gap: 8px; align-items: center; }
|
||||
.ann-toggle { display: inline-flex; align-items: center; gap: 6px;
|
||||
background: var(--accent); color: #fff; border: 1px solid var(--accent);
|
||||
border-radius: 5px; padding: 6px 12px; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }
|
||||
.ann-toggle:hover { filter: brightness(1.05); }
|
||||
body:not(.ann-mode) .ann-toggle { background: var(--bg-soft); color: var(--text-dim); border-color: var(--border); }
|
||||
body:not(.ann-mode) .ann-toggle:hover { color: var(--text); border-color: var(--border-strong); }
|
||||
.ann-badge { background: rgba(255,255,255,0.25); color: inherit; padding: 0 6px; border-radius: 99px; font-size: 11px; font-weight: 700; }
|
||||
body:not(.ann-mode) .ann-badge { background: var(--bg); color: var(--text-dim); }
|
||||
.ghost-btn { background: transparent; color: var(--text-dim); border: 1px solid var(--border);
|
||||
border-radius: 5px; padding: 6px 12px; font-family: inherit; font-size: 13px; cursor: pointer; }
|
||||
.ghost-btn:hover { color: var(--text); border-color: var(--border-strong); }
|
||||
.icon-btn { background: transparent; border: none; color: var(--text-dim); cursor: pointer;
|
||||
font-size: 16px; padding: 4px 8px; border-radius: 4px; }
|
||||
.icon-btn:hover { color: var(--text); background: var(--bg-soft); }
|
||||
/* Article */
|
||||
.article-wrap { max-width: 820px; margin: 0 auto; padding: 24px 32px 96px; }
|
||||
.article-help { font-size: 13px; color: var(--text-dim); background: var(--accent-soft);
|
||||
border: 1px solid var(--accent); border-radius: 6px; padding: 10px 14px; margin: 0 0 24px; line-height: 1.5; }
|
||||
body:not(.ann-mode) .article-help { display: none; }
|
||||
.article-help strong { color: var(--text); }
|
||||
.article { font-size: 15px; line-height: 1.7; }
|
||||
.article h1, .article h2, .article h3, .article h4, .article h5, .article h6 {
|
||||
font-family: var(--serif); font-weight: 700; line-height: 1.25; margin: 1.8em 0 .55em; color: var(--text); }
|
||||
.article h1 { font-size: 2rem; margin-top: 0; }
|
||||
.article h2 { font-size: 1.5rem; border-bottom: 1px solid var(--border); padding-bottom: .3em; }
|
||||
.article h3 { font-size: 1.2rem; }
|
||||
.article h4 { font-size: 1.05rem; }
|
||||
.article p { margin: .9em 0; }
|
||||
.article a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
||||
.article code { font-family: var(--mono); font-size: .9em; background: var(--bg-soft);
|
||||
padding: .12em .4em; border-radius: 4px; }
|
||||
.article pre { background: #1e1e24; color: #e6e6eb; padding: 16px 18px; border-radius: 8px;
|
||||
overflow-x: auto; font-size: .88rem; line-height: 1.55; margin: 1.2em 0; }
|
||||
.article pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
|
||||
.article blockquote { margin: 1.2em 0; padding: .5em 1.2em; border-left: 4px solid var(--accent);
|
||||
background: var(--accent-soft); color: var(--text-dim); border-radius: 0 6px 6px 0; }
|
||||
.article ul, .article ol { padding-left: 1.8em; margin: .9em 0; }
|
||||
.article li { margin: .3em 0; }
|
||||
.article table { border-collapse: collapse; width: 100%; margin: 1.4em 0; font-size: .92em; }
|
||||
.article th, .article td { border: 1px solid var(--border); padding: .55em .8em; text-align: left; vertical-align: top; }
|
||||
.article th { background: var(--bg-soft); font-weight: 650; }
|
||||
.article hr { border: none; border-top: 1px solid var(--border); margin: 2.2em 0; }
|
||||
.article strong { font-weight: 700; }
|
||||
.article em { font-style: italic; }
|
||||
/* Annotation mode: highlight annotatable elements on hover, mark annotated ones */
|
||||
.article [data-anchor-id] { position: relative; transition: background .08s, outline .08s; border-radius: 3px; }
|
||||
body.ann-mode .article [data-anchor-id] { cursor: pointer; }
|
||||
body.ann-mode .article [data-anchor-id]:hover {
|
||||
outline: 2px dashed var(--accent); outline-offset: 2px; background: var(--accent-soft);
|
||||
}
|
||||
.article [data-anchor-id].annotated {
|
||||
background: var(--amber-soft);
|
||||
outline: 1px solid var(--amber); outline-offset: 1px;
|
||||
}
|
||||
.article [data-anchor-id].annotated::after {
|
||||
content: attr(data-ann-count); position: absolute; right: -22px; top: 2px;
|
||||
background: var(--amber); color: #fff; font-size: 10px; font-weight: 700;
|
||||
padding: 1px 6px; border-radius: 99px; font-family: var(--sans);
|
||||
}
|
||||
body.ann-mode .article [data-anchor-id].annotated:hover { outline-color: var(--amber); }
|
||||
.article [data-anchor-id].flash {
|
||||
animation: flash 1.6s ease-out;
|
||||
}
|
||||
@keyframes flash {
|
||||
0% { background: var(--accent-soft); outline: 2px solid var(--accent); }
|
||||
100% { background: var(--amber-soft); outline: 1px solid var(--amber); }
|
||||
}
|
||||
/* Form popover */
|
||||
.ann-form { position: fixed; z-index: 200; background: var(--bg-elev); border: 1px solid var(--border-strong);
|
||||
border-radius: 8px; padding: 14px; box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
||||
width: 380px; max-width: calc(100vw - 24px); display: none; flex-direction: column; gap: 10px;
|
||||
font-family: var(--sans); }
|
||||
.ann-form.visible { display: flex; }
|
||||
.ann-form-section-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--text-mute); font-weight: 600; margin-bottom: 3px; }
|
||||
.ann-form-section-value { font-size: 13px; color: var(--text-dim); font-style: italic; }
|
||||
.ann-form-snippet-text { margin: 0; padding: 6px 10px; border-left: 3px solid var(--accent);
|
||||
background: var(--bg); border-radius: 0 4px 4px 0; font-family: var(--mono); font-size: 12px;
|
||||
color: var(--text); max-height: 100px; overflow-y: auto; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||||
.ann-form-intents { display: flex; gap: 6px; }
|
||||
.ann-intent { flex: 1; padding: 7px 10px; border-radius: 5px; border: 1px solid var(--border);
|
||||
background: transparent; color: var(--text-dim); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.ann-intent:hover { color: var(--text); border-color: var(--border-strong); }
|
||||
.ann-intent[data-intent="fiks"].selected { background: var(--red); color: #fff; border-color: var(--red); }
|
||||
.ann-intent[data-intent="endre"].selected { background: var(--orange); color: #fff; border-color: var(--orange); }
|
||||
.ann-intent[data-intent="spørsmål"].selected { background: var(--blue); color: #fff; border-color: var(--blue); }
|
||||
.ann-form-comment { width: 100%; min-height: 80px; padding: 8px 10px;
|
||||
font-family: inherit; font-size: 13px; line-height: 1.5; color: var(--text);
|
||||
background: var(--bg); border: 1px solid var(--border); border-radius: 5px; resize: vertical; }
|
||||
.ann-form-comment:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
|
||||
.ann-form-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
||||
.btn { padding: 6px 14px; border-radius: 5px; border: 1px solid var(--border);
|
||||
background: transparent; color: var(--text-dim); font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.btn:hover { color: var(--text); border-color: var(--border-strong); }
|
||||
.btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
.btn.primary:hover:not(:disabled) { filter: brightness(1.1); color: #fff; }
|
||||
.btn.primary:disabled { background: var(--bg-soft); color: var(--text-mute); border-color: var(--border); cursor: not-allowed; filter: none; }
|
||||
/* Annotations panel (slide-in sidebar) */
|
||||
.ann-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 420px; max-width: 100vw;
|
||||
background: var(--bg-elev); border-left: 1px solid var(--border); z-index: 150;
|
||||
transform: translateX(100%); transition: transform .2s ease;
|
||||
display: flex; flex-direction: column; box-shadow: -4px 0 20px rgba(0,0,0,0.15); }
|
||||
.ann-panel.open { transform: translateX(0); }
|
||||
.ann-panel-head { display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 14px 18px; border-bottom: 1px solid var(--border); }
|
||||
.ann-panel-head h2 { font-size: 14px; font-weight: 650; margin: 0; }
|
||||
.ann-panel-body { flex: 1; overflow-y: auto; padding: 12px 14px; }
|
||||
.ann-panel-foot { display: flex; justify-content: space-between; gap: 8px;
|
||||
padding: 12px 14px; border-top: 1px solid var(--border); }
|
||||
.ann-panel-empty { color: var(--text-mute); font-size: 13px; text-align: center; padding: 32px 12px;
|
||||
font-style: italic; line-height: 1.5; }
|
||||
.ann-section { margin: 12px 0 6px; font-size: 11px; font-weight: 650; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: var(--text-mute); padding: 0 4px; }
|
||||
.ann-section:first-child { margin-top: 0; }
|
||||
.ann-item { background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
||||
padding: 10px 12px; margin-bottom: 8px; cursor: pointer; }
|
||||
.ann-item:hover { border-color: var(--border-strong); }
|
||||
.ann-item .ann-item-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; gap: 6px; }
|
||||
.ann-item-intent { font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; padding: 2px 8px; border-radius: 99px; }
|
||||
.ann-item-intent.fiks { background: var(--red-soft); color: var(--red); }
|
||||
.ann-item-intent.endre { background: var(--orange-soft); color: var(--orange); }
|
||||
.ann-item-intent.spørsmål { background: var(--blue-soft); color: var(--blue); }
|
||||
.ann-item-delete { background: transparent; border: none; color: var(--text-mute);
|
||||
cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
|
||||
.ann-item-delete:hover { color: var(--red); background: var(--red-soft); }
|
||||
.ann-item-snippet { font-family: var(--mono); font-size: 11px; color: var(--text-mute);
|
||||
margin: 0 0 6px; line-height: 1.5; padding: 4px 8px; background: var(--bg-soft);
|
||||
border-left: 2px solid var(--border-strong); border-radius: 0 4px 4px 0;
|
||||
max-height: 60px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; }
|
||||
.ann-item-comment { font-size: 13px; color: var(--text); line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||||
.ann-item-comment.empty { color: var(--text-mute); font-style: italic; }
|
||||
/* Toast */
|
||||
.ann-toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
|
||||
background: var(--text); color: var(--bg-elev); padding: 9px 16px; border-radius: 6px;
|
||||
font-size: 13px; font-weight: 500; opacity: 0; pointer-events: none;
|
||||
transition: opacity .2s, transform .2s; z-index: 300; }
|
||||
.ann-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
/* Overlay (form backdrop) */
|
||||
.ann-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.3); z-index: 100;
|
||||
opacity: 0; pointer-events: none; transition: opacity .15s; }
|
||||
.ann-overlay.visible { opacity: 1; pointer-events: auto; }
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 6px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--text-mute); }
|
||||
/* Print: hide annotation chrome, show article only */
|
||||
@media print {
|
||||
.topbar, .ann-form, .ann-panel, .ann-toast, .ann-overlay, .article-help { display: none !important; }
|
||||
.article-wrap { max-width: none; padding: 0; }
|
||||
body { background: #fff; color: #000; }
|
||||
}
|
||||
`.trim();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Embedded JS app. Uses concatenation (no template literals) to avoid
|
||||
// backtick collisions with the outer mjs string assembly.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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_ORDER = ['fiks', 'endre', 'spørsmål'];
|
||||
|
||||
let annotations = [];
|
||||
let nextId = 1;
|
||||
let mode = true;
|
||||
let currentTarget = null;
|
||||
let currentSection = null;
|
||||
let currentSnippet = null;
|
||||
let currentIntent = null;
|
||||
|
||||
// ── Storage ──
|
||||
function loadState() {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return;
|
||||
const data = JSON.parse(raw);
|
||||
if (data && Array.isArray(data.annotations)) {
|
||||
annotations = data.annotations;
|
||||
nextId = data.nextId || (annotations.reduce(function(m, a){return Math.max(m, a.id);}, 0) + 1);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function saveState() {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ annotations: annotations, nextId: nextId })); } catch (e) {}
|
||||
}
|
||||
function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
// ── DOM refs ──
|
||||
const body = document.body;
|
||||
const article = document.getElementById('article');
|
||||
const form = document.getElementById('ann-form');
|
||||
const formSection = document.getElementById('ann-form-section');
|
||||
const formSnippet = document.getElementById('ann-form-snippet');
|
||||
const formComment = document.getElementById('ann-form-comment');
|
||||
const formSave = document.getElementById('ann-form-save');
|
||||
const formCancel = document.getElementById('ann-form-cancel');
|
||||
const intents = document.querySelectorAll('.ann-intent');
|
||||
const panel = document.getElementById('ann-panel');
|
||||
const panelBody = document.getElementById('ann-panel-body');
|
||||
const panelCloseBtn = document.getElementById('ann-panel-close');
|
||||
const openPanelBtn = document.getElementById('open-panel');
|
||||
const clearAllBtn = document.getElementById('ann-clear-all');
|
||||
const copyBtn = document.getElementById('ann-copy');
|
||||
const annToggle = document.getElementById('ann-toggle');
|
||||
const annToggleLabel = document.getElementById('ann-toggle-label');
|
||||
const annBadge = document.getElementById('ann-badge');
|
||||
const toast = document.getElementById('ann-toast');
|
||||
const overlay = document.getElementById('ann-overlay');
|
||||
|
||||
// ── Section lookup ──
|
||||
function findSection(el) {
|
||||
let p = el;
|
||||
while (p) {
|
||||
if (p.previousElementSibling) {
|
||||
let s = p.previousElementSibling;
|
||||
while (s) {
|
||||
if (s.matches && (s.matches('h1') || s.matches('h2'))) return s.textContent.trim();
|
||||
s = s.previousElementSibling;
|
||||
}
|
||||
}
|
||||
p = p.parentElement;
|
||||
if (p && p.tagName === 'ARTICLE') break;
|
||||
}
|
||||
// Fallback: first h1 in article
|
||||
const firstH = article.querySelector('h1, h2');
|
||||
return firstH ? firstH.textContent.trim() : '(top)';
|
||||
}
|
||||
|
||||
// ── Snippet from selection or element text ──
|
||||
function captureSnippet(el) {
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.toString().trim().length > 0 && el.contains(sel.anchorNode)) {
|
||||
return sel.toString().trim().slice(0, 300);
|
||||
}
|
||||
return (el.textContent || '').trim().slice(0, 200);
|
||||
}
|
||||
|
||||
// ── Form open/close ──
|
||||
function openForm(evt, target) {
|
||||
currentTarget = target;
|
||||
currentSection = findSection(target);
|
||||
currentSnippet = captureSnippet(target);
|
||||
currentIntent = null;
|
||||
formSection.textContent = currentSection || '(top)';
|
||||
formSnippet.textContent = currentSnippet || '(empty)';
|
||||
formComment.value = '';
|
||||
intents.forEach(function(b) { b.classList.remove('selected'); });
|
||||
formSave.disabled = true;
|
||||
|
||||
// Position near the click (clamped to viewport)
|
||||
const fw = 380, fh = 320;
|
||||
let x = evt.clientX + 14;
|
||||
let y = evt.clientY + 14;
|
||||
if (x + fw > window.innerWidth) x = window.innerWidth - fw - 12;
|
||||
if (y + fh > window.innerHeight) y = Math.max(12, window.innerHeight - fh - 12);
|
||||
if (x < 12) x = 12;
|
||||
if (y < 12) y = 12;
|
||||
form.style.left = x + 'px';
|
||||
form.style.top = y + 'px';
|
||||
form.classList.add('visible');
|
||||
overlay.classList.add('visible');
|
||||
setTimeout(function() { formComment.focus(); }, 50);
|
||||
}
|
||||
function closeForm() {
|
||||
form.classList.remove('visible');
|
||||
overlay.classList.remove('visible');
|
||||
currentTarget = null;
|
||||
currentSection = null;
|
||||
currentSnippet = null;
|
||||
currentIntent = null;
|
||||
}
|
||||
|
||||
// ── Save ──
|
||||
function saveAnnotation() {
|
||||
if (!currentIntent || !currentTarget) return;
|
||||
const a = {
|
||||
id: nextId++,
|
||||
anchorId: currentTarget.getAttribute('data-anchor-id'),
|
||||
section: currentSection || '(top)',
|
||||
snippet: currentSnippet || '',
|
||||
intent: currentIntent,
|
||||
comment: (formComment.value || '').trim(),
|
||||
ts: new Date().toISOString(),
|
||||
};
|
||||
annotations.push(a);
|
||||
saveState();
|
||||
closeForm();
|
||||
refreshArticleAnnotations();
|
||||
renderPanel();
|
||||
updateCounts();
|
||||
showToast('Annotasjon lagret (' + annotations.length + ')');
|
||||
}
|
||||
|
||||
// ── Delete ──
|
||||
function deleteAnnotation(id) {
|
||||
annotations = annotations.filter(function(a) { return a.id !== id; });
|
||||
saveState();
|
||||
refreshArticleAnnotations();
|
||||
renderPanel();
|
||||
updateCounts();
|
||||
showToast('Annotasjon slettet');
|
||||
}
|
||||
|
||||
// ── Refresh article markers ──
|
||||
function refreshArticleAnnotations() {
|
||||
// Clear all current markers
|
||||
article.querySelectorAll('[data-anchor-id].annotated').forEach(function(el) {
|
||||
el.classList.remove('annotated');
|
||||
el.removeAttribute('data-ann-count');
|
||||
});
|
||||
// Group by anchorId
|
||||
const byAnchor = {};
|
||||
for (const a of annotations) {
|
||||
if (!a.anchorId) continue;
|
||||
if (!byAnchor[a.anchorId]) byAnchor[a.anchorId] = 0;
|
||||
byAnchor[a.anchorId]++;
|
||||
}
|
||||
for (const anchorId in byAnchor) {
|
||||
const el = article.querySelector('[data-anchor-id="' + CSS.escape(anchorId) + '"]');
|
||||
if (el) {
|
||||
el.classList.add('annotated');
|
||||
el.setAttribute('data-ann-count', byAnchor[anchorId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sidebar panel render ──
|
||||
function renderPanel() {
|
||||
if (annotations.length === 0) {
|
||||
panelBody.innerHTML = '<div class="ann-panel-empty">No annotations yet.<br><br>Click any heading, paragraph, list item, or quote in the article to add one.</div>';
|
||||
return;
|
||||
}
|
||||
// Group by section (preserve insertion order)
|
||||
const groups = [];
|
||||
const groupMap = {};
|
||||
// Sort by document order using anchorId numerical suffix
|
||||
const sorted = annotations.slice().sort(function(a, b) {
|
||||
const ai = parseInt((a.anchorId || '').replace('anch-', ''), 10) || 0;
|
||||
const bi = parseInt((b.anchorId || '').replace('anch-', ''), 10) || 0;
|
||||
if (ai !== bi) return ai - bi;
|
||||
return a.id - b.id;
|
||||
});
|
||||
for (const a of sorted) {
|
||||
if (!groupMap[a.section]) {
|
||||
groupMap[a.section] = { section: a.section, items: [] };
|
||||
groups.push(groupMap[a.section]);
|
||||
}
|
||||
groupMap[a.section].items.push(a);
|
||||
}
|
||||
let html = '';
|
||||
for (const g of groups) {
|
||||
html += '<div class="ann-section">' + escHtml(g.section) + '</div>';
|
||||
for (const a of g.items) {
|
||||
html += '<div class="ann-item" data-anchor-id="' + escHtml(a.anchorId || '') + '" data-id="' + a.id + '">'
|
||||
+ '<div class="ann-item-head">'
|
||||
+ '<span class="ann-item-intent ' + escHtml(a.intent) + '">' + escHtml(INTENT_LABELS[a.intent] || a.intent) + '</span>'
|
||||
+ '<button class="ann-item-delete" data-del="' + a.id + '" title="Delete">✕</button>'
|
||||
+ '</div>'
|
||||
+ '<blockquote class="ann-item-snippet">' + escHtml(a.snippet || '(empty)') + '</blockquote>'
|
||||
+ '<div class="ann-item-comment' + (a.comment ? '' : ' empty') + '">' + escHtml(a.comment || '(no comment)') + '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
panelBody.innerHTML = html;
|
||||
|
||||
panelBody.querySelectorAll('.ann-item-delete').forEach(function(b) {
|
||||
b.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (confirm('Delete this annotation?')) deleteAnnotation(parseInt(b.dataset.del, 10));
|
||||
});
|
||||
});
|
||||
panelBody.querySelectorAll('.ann-item').forEach(function(card) {
|
||||
card.addEventListener('click', function() {
|
||||
const anchor = card.getAttribute('data-anchor-id');
|
||||
const el = article.querySelector('[data-anchor-id="' + CSS.escape(anchor) + '"]');
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.remove('flash');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('flash');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Counts + toggle label ──
|
||||
function updateCounts() {
|
||||
annBadge.textContent = String(annotations.length);
|
||||
copyBtn.disabled = annotations.length === 0;
|
||||
}
|
||||
|
||||
function setMode(on) {
|
||||
mode = on;
|
||||
body.classList.toggle('ann-mode', on);
|
||||
annToggleLabel.textContent = on ? 'Annotation mode: ON' : 'Annotation mode: OFF';
|
||||
if (!on) closeForm();
|
||||
}
|
||||
|
||||
// ── Toast ──
|
||||
function showToast(msg) {
|
||||
toast.textContent = msg;
|
||||
toast.classList.add('visible');
|
||||
setTimeout(function() { toast.classList.remove('visible'); }, 1800);
|
||||
}
|
||||
|
||||
// ── Copy Prompt ──
|
||||
function buildPromptMarkdown() {
|
||||
if (annotations.length === 0) return '';
|
||||
const sorted = annotations.slice().sort(function(a, b) {
|
||||
const ai = parseInt((a.anchorId || '').replace('anch-', ''), 10) || 0;
|
||||
const bi = parseInt((b.anchorId || '').replace('anch-', ''), 10) || 0;
|
||||
if (ai !== bi) return ai - bi;
|
||||
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 += 'Treat the operator notes as authoritative direction.\\n\\n';
|
||||
p += '## Annotations (' + annotations.length + ' total)\\n\\n';
|
||||
let n = 0;
|
||||
for (const a of sorted) {
|
||||
n++;
|
||||
p += '### ' + n + '. [' + (INTENT_LABELS[a.intent] || a.intent) + '] Section: ' + a.section + '\\n';
|
||||
if (a.snippet) p += 'Quote: «' + a.snippet + '»\\n';
|
||||
p += 'Comment: ' + (a.comment || '(no comment)') + '\\n\\n';
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
async function copyPrompt() {
|
||||
const md = buildPromptMarkdown();
|
||||
if (!md) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(md);
|
||||
showToast('Prompt copied (' + annotations.length + ' annotation' + (annotations.length === 1 ? '' : 's') + ')');
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = md; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); showToast('Prompt copied'); } catch (e2) { alert('Copy failed: ' + e2.message); }
|
||||
ta.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wiring ──
|
||||
article.addEventListener('click', function(e) {
|
||||
if (!mode) return;
|
||||
const target = e.target.closest('[data-anchor-id]');
|
||||
if (!target) return;
|
||||
// Don't open form when clicking inside an already-open form (overlay catches outside clicks)
|
||||
if (e.target.closest('.ann-form')) return;
|
||||
// Don't open form when clicking a link the user wants to follow — but only if they didn't select text
|
||||
if (e.target.tagName === 'A' && (!window.getSelection() || window.getSelection().toString().trim().length === 0)) {
|
||||
// Allow link clicks in mode if no selection
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
openForm(e, target);
|
||||
});
|
||||
|
||||
intents.forEach(function(b) {
|
||||
b.addEventListener('click', function() {
|
||||
intents.forEach(function(x) { x.classList.remove('selected'); });
|
||||
b.classList.add('selected');
|
||||
currentIntent = b.dataset.intent;
|
||||
formSave.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
formSave.addEventListener('click', saveAnnotation);
|
||||
formCancel.addEventListener('click', closeForm);
|
||||
overlay.addEventListener('click', closeForm);
|
||||
|
||||
formComment.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !formSave.disabled) {
|
||||
saveAnnotation();
|
||||
} else if (e.key === 'Escape') {
|
||||
closeForm();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && form.classList.contains('visible')) closeForm();
|
||||
});
|
||||
|
||||
annToggle.addEventListener('click', function() { setMode(!mode); });
|
||||
|
||||
openPanelBtn.addEventListener('click', function() {
|
||||
panel.classList.toggle('open');
|
||||
});
|
||||
panelCloseBtn.addEventListener('click', function() { panel.classList.remove('open'); });
|
||||
|
||||
clearAllBtn.addEventListener('click', function() {
|
||||
if (annotations.length === 0) return;
|
||||
if (confirm('Remove all ' + annotations.length + ' annotations? This cannot be undone.')) {
|
||||
annotations = [];
|
||||
saveState();
|
||||
refreshArticleAnnotations();
|
||||
renderPanel();
|
||||
updateCounts();
|
||||
showToast('All annotations cleared');
|
||||
}
|
||||
});
|
||||
|
||||
copyBtn.addEventListener('click', copyPrompt);
|
||||
|
||||
// ── Init ──
|
||||
loadState();
|
||||
refreshArticleAnnotations();
|
||||
renderPanel();
|
||||
updateCounts();
|
||||
setMode(true);
|
||||
`.trim();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CLI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { input: null, out: null, help: false };
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--out') args.out = argv[++i];
|
||||
else if (a === '--help' || a === '-h') args.help = true;
|
||||
else if (!args.input) args.input = a;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function render(inputPath, outputPath) {
|
||||
if (!existsSync(inputPath)) {
|
||||
process.stderr.write('annotate: input not found: ' + inputPath + '\n');
|
||||
process.exit(2);
|
||||
}
|
||||
const text = readFileSync(inputPath, 'utf-8');
|
||||
const html = buildHtml(resolve(inputPath), text);
|
||||
const out = outputPath || inputPath.replace(/\.md$/, '.html');
|
||||
writeFileSync(out, html);
|
||||
return out;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help || !args.input) {
|
||||
process.stdout.write(
|
||||
'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'
|
||||
+ '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',
|
||||
);
|
||||
process.exit(args.help ? 0 : 2);
|
||||
}
|
||||
const out = render(args.input, args.out);
|
||||
process.stdout.write(out + '\n');
|
||||
}
|
||||
|
||||
export { render, buildHtml, renderMarkdown, parseArgs };
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: 2.0
|
||||
brief_version: 2.1
|
||||
created: {YYYY-MM-DD}
|
||||
task: "{one-line task description}"
|
||||
slug: {slug}
|
||||
|
|
@ -10,6 +10,20 @@ research_status: pending # pending | in_progress | complete | skipped
|
|||
auto_research: false # true if user opted into Claude-managed research
|
||||
interview_turns: {N}
|
||||
source: {interview | manual}
|
||||
# v5.1 — per-phase effort + model signal (Phase 3.5).
|
||||
# `effort` ∈ {low, standard, high}. Omit `model:` for `standard` so composition
|
||||
# falls through to profile resolver. Force-stop alternative is the commented
|
||||
# `phase_signals_partial: true` below (mutually exclusive with `phase_signals`).
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: standard
|
||||
- phase: plan
|
||||
effort: standard
|
||||
- phase: execute
|
||||
effort: standard
|
||||
- phase: review
|
||||
effort: standard
|
||||
# phase_signals_partial: true # uncomment to record force-stop instead of phase_signals
|
||||
---
|
||||
|
||||
# Task: {title}
|
||||
|
|
|
|||
42
plugins/voyage/tests/commands/trekbrief.test.mjs
Normal file
42
plugins/voyage/tests/commands/trekbrief.test.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// tests/commands/trekbrief.test.mjs
|
||||
// v5.1 — Pattern D prose-pattern regression tests for /trekbrief Phase 3.5.
|
||||
//
|
||||
// Brief SC1 + SC2: end-of-brief effort dialog covering 4 downstream phases,
|
||||
// with `phase_signals_partial` as the force-stop record.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekbrief.md');
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
test('trekbrief — Phase 3.5 heading is present', () => {
|
||||
const text = read();
|
||||
assert.match(text, /^## Phase 3\.5 — Per-phase effort dialog$/m,
|
||||
'Phase 3.5 heading missing from commands/trekbrief.md');
|
||||
});
|
||||
|
||||
test('trekbrief — Phase 3.5 references all 4 downstream phases', () => {
|
||||
const text = read();
|
||||
const startIdx = text.indexOf('## Phase 3.5');
|
||||
assert.ok(startIdx >= 0, 'Phase 3.5 not found');
|
||||
const section = text.slice(startIdx, text.indexOf('## Phase 4', startIdx));
|
||||
for (const phase of ['research', 'plan', 'execute', 'review']) {
|
||||
assert.ok(section.includes(phase),
|
||||
`Phase 3.5 missing reference to "${phase}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trekbrief — Phase 3.5 documents phase_signals_partial force-stop', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('phase_signals_partial'),
|
||||
'phase_signals_partial not mentioned in /trekbrief command prose');
|
||||
});
|
||||
34
plugins/voyage/tests/commands/trekexecute.test.mjs
Normal file
34
plugins/voyage/tests/commands/trekexecute.test.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// tests/commands/trekexecute.test.mjs
|
||||
// v5.1 — sequencing-gate surface + low-effort prose check for /trekexecute.
|
||||
// Plan Assumption 2 locks low-effort to --gates open + sequential-only.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekexecute.md');
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
test('trekexecute — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekexecute must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekexecute must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekexecute — low-effort path references --gates open + sequential', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--gates open/, 'Low-effort path must mention --gates open');
|
||||
assert.match(section, /sequential/, 'Low-effort path must mention sequential-only execution');
|
||||
});
|
||||
32
plugins/voyage/tests/commands/trekplan.test.mjs
Normal file
32
plugins/voyage/tests/commands/trekplan.test.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// tests/commands/trekplan.test.mjs
|
||||
// v5.1 — sequencing-gate surface + low-effort prose check for /trekplan.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekplan.md');
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
test('trekplan — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekplan must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekplan must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekplan — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
});
|
||||
32
plugins/voyage/tests/commands/trekresearch.test.mjs
Normal file
32
plugins/voyage/tests/commands/trekresearch.test.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// tests/commands/trekresearch.test.mjs
|
||||
// v5.1 — sequencing-gate surface + low-effort prose check for /trekresearch.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekresearch.md');
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
test('trekresearch — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekresearch must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekresearch must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekresearch — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
});
|
||||
32
plugins/voyage/tests/commands/trekreview.test.mjs
Normal file
32
plugins/voyage/tests/commands/trekreview.test.mjs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// tests/commands/trekreview.test.mjs
|
||||
// v5.1 — sequencing-gate surface + low-effort prose check for /trekreview.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(HERE, '..', '..');
|
||||
const COMMAND_FILE = join(ROOT, 'commands', 'trekreview.md');
|
||||
|
||||
function read() {
|
||||
return readFileSync(COMMAND_FILE, 'utf8');
|
||||
}
|
||||
|
||||
test('trekreview — sequencing-gate surface mentions BRIEF_V51_MISSING_SIGNALS + phase_signals', () => {
|
||||
const text = read();
|
||||
assert.ok(text.includes('BRIEF_V51_MISSING_SIGNALS'),
|
||||
'/trekreview must surface the BRIEF_V51_MISSING_SIGNALS sequencing gate');
|
||||
assert.ok(text.includes('phase_signals'),
|
||||
'/trekreview must reference phase_signals (v5.1 composition rule)');
|
||||
});
|
||||
|
||||
test('trekreview — low-effort path references --quick equivalent', () => {
|
||||
const text = read();
|
||||
const compIdx = text.indexOf('## Composition rule (v5.1)');
|
||||
assert.ok(compIdx >= 0, 'Composition rule (v5.1) section missing');
|
||||
const section = text.slice(compIdx, compIdx + 2000);
|
||||
assert.match(section, /--quick/, 'Low-effort path must mention --quick equivalent');
|
||||
});
|
||||
42
plugins/voyage/tests/fixtures/brief-with-phase-signals.md
vendored
Normal file
42
plugins/voyage/tests/fixtures/brief-with-phase-signals.md
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.1"
|
||||
created: 2026-05-13
|
||||
task: "Add per-phase effort dialog to /trekbrief"
|
||||
slug: phase-signals-example
|
||||
project_dir: .claude/projects/2026-05-13-phase-signals-example/
|
||||
research_topics: 2
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 6
|
||||
source: interview
|
||||
phase_signals:
|
||||
- phase: research
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: plan
|
||||
effort: standard
|
||||
- phase: execute
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: review
|
||||
effort: standard
|
||||
---
|
||||
|
||||
# Task: Phase-signals example
|
||||
|
||||
## Intent
|
||||
|
||||
A minimal brief that exercises the v5.1 phase_signals additive field with a
|
||||
mix of effort levels and model overrides. Used by tests/validators to confirm
|
||||
the validator accepts well-formed signals across the supported tier matrix.
|
||||
|
||||
## Goal
|
||||
|
||||
Validator returns valid: true. annotate.mjs strips phase_signals from the
|
||||
rendered HTML body (frontmatter stays in source).
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- annotate.mjs determinism: re-run produces byte-identical HTML.
|
||||
31
plugins/voyage/tests/fixtures/brief-without-phase-signals.md
vendored
Normal file
31
plugins/voyage/tests/fixtures/brief-without-phase-signals.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
type: trekbrief
|
||||
brief_version: "2.0"
|
||||
created: 2026-05-13
|
||||
task: "Backward-compat fixture for v5.0-style brief"
|
||||
slug: legacy-brief-example
|
||||
project_dir: .claude/projects/2026-05-13-legacy-brief-example/
|
||||
research_topics: 0
|
||||
research_status: complete
|
||||
auto_research: false
|
||||
interview_turns: 3
|
||||
source: interview
|
||||
---
|
||||
|
||||
# Task: Legacy brief example
|
||||
|
||||
## Intent
|
||||
|
||||
A pre-v5.1 brief that pre-dates the phase_signals field. Used by
|
||||
tests/validators to confirm backward-compatibility: the brief is accepted
|
||||
without phase_signals as long as brief_version is < 2.1.
|
||||
|
||||
## Goal
|
||||
|
||||
Validator returns valid: true. The sequencing gate
|
||||
(BRIEF_V51_MISSING_SIGNALS) does NOT fire for brief_version 2.0.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Validator passes.
|
||||
- No BRIEF_V51_MISSING_SIGNALS error in r.errors.
|
||||
|
|
@ -430,42 +430,85 @@ test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)',
|
|||
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
|
||||
});
|
||||
|
||||
test('scripts/render-artifact.mjs no longer exists (removed in v5.0.1)', () => {
|
||||
test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => {
|
||||
assert.ok(
|
||||
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
|
||||
'scripts/render-artifact.mjs should be deleted — v5.0.1 drops the redundant standalone HTML render in favour of the /playground document-critique invocation printed by the producing commands',
|
||||
'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)',
|
||||
);
|
||||
});
|
||||
|
||||
test('producing commands print a literal /playground document-critique invocation', () => {
|
||||
// The exact substring must appear in each producing command's prose so the
|
||||
// operator copy-pastes a verbatim line. Drift on this is the friction point
|
||||
// that motivated v5.0.1 — fail loudly if the prose softens back to "run the
|
||||
// /playground plugin" without the literal command.
|
||||
const REQUIRED = '/playground build a document-critique playground for';
|
||||
test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => {
|
||||
assert.ok(
|
||||
existsSync(join(ROOT, 'scripts/annotate.mjs')),
|
||||
'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML',
|
||||
);
|
||||
});
|
||||
|
||||
test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => {
|
||||
// v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't
|
||||
// afford annotation; v5.0.1 pointed at /playground document-critique (which
|
||||
// pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2
|
||||
// ships scripts/annotate.mjs — an operator-driven annotation surface where
|
||||
// the OPERATOR clicks lines and writes their own notes. Pin the wiring.
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
read(`commands/${f}`).includes(REQUIRED),
|
||||
`commands/${f} must include the literal invocation "${REQUIRED}" so the operator copy-pastes it directly (v5.0.1)`,
|
||||
read(`commands/${f}`).includes('scripts/annotate.mjs'),
|
||||
`commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('producing commands no longer reference the removed scripts/render-artifact.mjs', () => {
|
||||
test('producing commands no longer print the v5.0.1 /playground document-critique line', () => {
|
||||
// v5.0.1 told operators to copy-paste "/playground build a document-critique
|
||||
// playground for X" — but that flow pre-generates Claude's suggestions. The
|
||||
// operator asked for their own annotations, not a critique of Claude's.
|
||||
// v5.0.2 removes that line from the producing commands' final report.
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
!read(`commands/${f}`).includes('render-artifact.mjs'),
|
||||
`commands/${f} still references scripts/render-artifact.mjs — that script was removed in v5.0.1`,
|
||||
!read(`commands/${f}`).includes('/playground build a document-critique'),
|
||||
`commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('package.json no longer has an "npm run render" script (removed in v5.0.1)', () => {
|
||||
test('producing commands tell the operator the flow is THEIR own annotations', () => {
|
||||
// Pin language: every producing command's prose must mention that the
|
||||
// OPERATOR drives annotation, not Claude. Phrase variants are allowed
|
||||
// ("YOUR OWN note", "operator drives", etc.) — we look for the operator-
|
||||
// ownership signal.
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
const text = read(`commands/${f}`);
|
||||
assert.ok(
|
||||
/YOUR OWN|operator drives|your own/i.test(text),
|
||||
`commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('producing commands emit file:// link in final report (operator-UX contract, 2026-05-13)', () => {
|
||||
// Operator runs Ghostty / iTerm2 / modern Terminal.app — all support cmd+click
|
||||
// on file:// URLs. Producing commands MUST emit both forms: (a) plain file://
|
||||
// line in the report block, (b) `open file://...` copy-pasteable command.
|
||||
// Both must reference $ANNOT_HTML (absolute path from scripts/annotate.mjs).
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
const text = read(`commands/${f}`);
|
||||
assert.ok(
|
||||
/file:\/\/\{\$ANNOT_HTML\}/.test(text),
|
||||
`commands/${f} must include "file://{$ANNOT_HTML}" plain URL in the final report block`,
|
||||
);
|
||||
assert.ok(
|
||||
/open file:\/\/\{\$ANNOT_HTML\}/.test(text),
|
||||
`commands/${f} must include "open file://{$ANNOT_HTML}" copy-pasteable command in the final report block`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
|
||||
const pkg = JSON.parse(read('package.json'));
|
||||
assert.equal(
|
||||
pkg.scripts && pkg.scripts.render,
|
||||
undefined,
|
||||
'package.json scripts.render should be gone in v5.0.1',
|
||||
'package.json scripts.render should remain gone',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -479,6 +522,11 @@ test('CHANGELOG.md has v5.0.1 entry', () => {
|
|||
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v5.0.2 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry');
|
||||
});
|
||||
|
||||
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');
|
||||
|
|
@ -503,3 +551,37 @@ test('operational files no longer reference trekrevise (v5.0.0 removal)', () =>
|
|||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- v5.1 — phase_signals + brief_version 2.1 ---
|
||||
|
||||
test('v5.1 — templates/trekbrief-template.md declares brief_version: 2.1', () => {
|
||||
const t = read('templates/trekbrief-template.md');
|
||||
assert.match(t, /^brief_version: 2\.1$/m,
|
||||
'trekbrief-template.md must declare brief_version: 2.1 at top of frontmatter');
|
||||
});
|
||||
|
||||
test('v5.1 — templates/trekbrief-template.md contains phase_signals: block', () => {
|
||||
const t = read('templates/trekbrief-template.md');
|
||||
assert.match(t, /^phase_signals:$/m,
|
||||
'trekbrief-template.md must contain a phase_signals: block in frontmatter');
|
||||
});
|
||||
|
||||
test('v5.1 — HANDOVER-CONTRACTS.md schema row includes phase_signals + phase_signals_partial', () => {
|
||||
const t = read('docs/HANDOVER-CONTRACTS.md');
|
||||
assert.ok(t.includes('| `phase_signals` |'),
|
||||
'HANDOVER-CONTRACTS must add a phase_signals row to the Handover 1 schema table');
|
||||
assert.ok(t.includes('| `phase_signals_partial` |'),
|
||||
'HANDOVER-CONTRACTS must add a phase_signals_partial row to the Handover 1 schema table');
|
||||
});
|
||||
|
||||
test('v5.1 — voyage CLAUDE.md mentions phase_signals', () => {
|
||||
const t = read('CLAUDE.md');
|
||||
assert.ok(t.includes('phase_signals'),
|
||||
'voyage CLAUDE.md must document phase_signals (v5.1)');
|
||||
});
|
||||
|
||||
test('v5.1 — voyage README.md mentions phase_signals', () => {
|
||||
const t = read('README.md');
|
||||
assert.ok(t.includes('phase_signals'),
|
||||
'voyage README.md must mention phase_signals (v5.1 "What\'s new" bullet)');
|
||||
});
|
||||
|
|
|
|||
208
plugins/voyage/tests/scripts/annotate.test.mjs
Normal file
208
plugins/voyage/tests/scripts/annotate.test.mjs
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
// tests/scripts/annotate.test.mjs
|
||||
// Covers scripts/annotate.mjs — the v5.0.3 operator-annotation HTML
|
||||
// generator. UX modelled on claude-code-100x/build-site.js (pencil
|
||||
// toggle, intent buttons, form popover, selection-anchoring, localStorage
|
||||
// persistence, structured markdown export).
|
||||
//
|
||||
// What we pin:
|
||||
// • Output is a complete, self-contained HTML document.
|
||||
// • No external <link href=> or <script src=>.
|
||||
// • The embedded inline <script> parses as valid JavaScript.
|
||||
// • The artifact path is embedded (used as the localStorage key + prompt context).
|
||||
// • The markdown source is rendered to proper HTML (h1/p/li etc.), not as raw lines.
|
||||
// • HTML metacharacters in the title are escaped (XSS).
|
||||
// • Inline content from a hostile .md never appears as a live attribute.
|
||||
// • 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
|
||||
// sidebar, Copy Prompt button, Clear all, localStorage persistence.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { buildHtml, renderMarkdown, render, parseArgs } from '../../scripts/annotate.mjs';
|
||||
|
||||
const SAMPLE = `---
|
||||
type: trekplan
|
||||
plan_version: "1.7"
|
||||
task: "Operator-annotation smoke test"
|
||||
slug: annotate-smoke
|
||||
---
|
||||
|
||||
# Operator-annotation smoke test
|
||||
|
||||
This is a paragraph with **bold**, \`inline code\`, and a [link](https://example.com).
|
||||
|
||||
## Steps
|
||||
|
||||
- first item
|
||||
- second item
|
||||
|
||||
\`\`\`js
|
||||
const x = 1;
|
||||
\`\`\`
|
||||
|
||||
> a blockquote
|
||||
`;
|
||||
|
||||
test('buildHtml produces a complete self-contained HTML document', () => {
|
||||
const html = buildHtml('/abs/path/plan.md', SAMPLE);
|
||||
assert.ok(html.startsWith('<!DOCTYPE html>'), 'must start with doctype');
|
||||
assert.ok(html.includes('</html>'), 'must close html');
|
||||
assert.ok(html.includes('<style>'), 'must inline a stylesheet');
|
||||
assert.ok(html.includes('<script>'), 'must inline the app script');
|
||||
});
|
||||
|
||||
test('buildHtml has zero external network references in static HTML', () => {
|
||||
const html = buildHtml('/abs/path/plan.md', SAMPLE);
|
||||
assert.ok(!/<link[^>]+href\s*=/i.test(html), 'no external <link href> stylesheets');
|
||||
assert.ok(!/<script[^>]+src\s*=/i.test(html), 'no external <script src>');
|
||||
});
|
||||
|
||||
test('buildHtml embeds the inline <script> as parseable JavaScript', () => {
|
||||
const html = buildHtml('/abs/path/plan.md', SAMPLE);
|
||||
const m = html.match(/<script>([\s\S]*?)<\/script>/);
|
||||
assert.ok(m, 'must contain a <script> block');
|
||||
assert.doesNotThrow(() => new Function(m[1]), 'inline script must parse without SyntaxError');
|
||||
});
|
||||
|
||||
test('buildHtml embeds the artifact path (used as localStorage key + prompt context)', () => {
|
||||
const html = buildHtml('/abs/projects/2026-05-13-foo/brief.md', SAMPLE);
|
||||
assert.ok(html.includes('/abs/projects/2026-05-13-foo/brief.md'),
|
||||
'artifact path must appear in the HTML so the script can use it as the localStorage key + prompt context');
|
||||
});
|
||||
|
||||
test('buildHtml renders the markdown source to proper article HTML', () => {
|
||||
const html = buildHtml('/abs/path/plan.md', SAMPLE);
|
||||
// Headings, paragraph content, list items, code fence — all present as HTML.
|
||||
assert.ok(html.includes('<h1 data-anchor-id='), 'top-level heading rendered as <h1>');
|
||||
assert.ok(html.includes('<h2 data-anchor-id='), '## heading rendered as <h2>');
|
||||
assert.ok(html.includes('Operator-annotation smoke test'), 'h1 text preserved');
|
||||
assert.ok(html.includes('<li data-anchor-id='), 'list items rendered with anchor ids');
|
||||
assert.ok(html.includes('first item'), 'list content preserved');
|
||||
assert.ok(html.includes('<pre data-anchor-id='), 'code fence rendered with anchor');
|
||||
assert.ok(html.includes('const x = 1;'), 'code fence body preserved (escaped)');
|
||||
assert.ok(html.includes('<blockquote data-anchor-id='), 'blockquote rendered with anchor');
|
||||
});
|
||||
|
||||
test('buildHtml escapes HTML metacharacters in the title (XSS surface)', () => {
|
||||
const md = '---\ntype: trekbrief\ntask: "<script>alert(1)</script>"\n---\n\n# Foo\n';
|
||||
const html = buildHtml('/abs/path/brief.md', md);
|
||||
const titleMatch = html.match(/<title>([\s\S]*?)<\/title>/);
|
||||
assert.ok(titleMatch, 'must have a title');
|
||||
assert.ok(!titleMatch[1].includes('<script>'), 'title must not carry a raw <script> tag');
|
||||
assert.match(titleMatch[1], /<script>/, 'title must be HTML-escaped');
|
||||
});
|
||||
|
||||
test('hostile inline content cannot inject as live HTML attributes', () => {
|
||||
const md = '# Heading\n\nA paragraph with <img src=x onerror="alert(1)"> embedded.\n';
|
||||
const html = buildHtml('/abs/path/brief.md', md);
|
||||
// The article body must not carry a live onerror="..." attribute (the renderer
|
||||
// HTML-escapes everything in the body, so `<` → `<`).
|
||||
const articleMatch = html.match(/<article[^>]*>([\s\S]*?)<\/article>/);
|
||||
assert.ok(articleMatch, 'must have article body');
|
||||
assert.ok(!/onerror\s*=\s*"alert/i.test(articleMatch[1]),
|
||||
'article body must not carry a live onerror attribute');
|
||||
assert.ok(articleMatch[1].includes('<img'),
|
||||
'hostile <img> must be escaped to <img');
|
||||
});
|
||||
|
||||
test('render() is deterministic — two runs byte-identical', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'claude-annotate-'));
|
||||
try {
|
||||
const md = join(dir, 'plan.md');
|
||||
writeFileSync(md, SAMPLE);
|
||||
const a = render(md, join(dir, 'a.html'));
|
||||
const b = render(md, join(dir, 'b.html'));
|
||||
assert.ok(existsSync(a) && existsSync(b));
|
||||
assert.equal(readFileSync(a, 'utf-8'), readFileSync(b, 'utf-8'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render() defaults output to <input-basename>.html next to input', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'claude-annotate-'));
|
||||
try {
|
||||
const md = join(dir, 'review.md');
|
||||
writeFileSync(md, '# Review\n\nok\n');
|
||||
const out = render(md);
|
||||
assert.equal(out, join(dir, 'review.html'));
|
||||
assert.ok(existsSync(out));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('parseArgs handles --out, positional input, and --help', () => {
|
||||
assert.deepEqual(parseArgs(['x.md']), { input: 'x.md', out: null, help: false });
|
||||
assert.deepEqual(parseArgs(['x.md', '--out', 'y.html']), { input: 'x.md', out: 'y.html', help: false });
|
||||
assert.equal(parseArgs(['--help']).help, true);
|
||||
});
|
||||
|
||||
test('buildHtml wires the v5.0.3 operator-driven annotation affordances', () => {
|
||||
// Pin every UX-critical affordance modelled on claude-code-100x/build-site.js:
|
||||
// - Pencil-toggle button (annotation mode on/off)
|
||||
// - Form popover with three intent buttons (Fiks/Endre/Spørsmål)
|
||||
// - Annotations sidebar (Your annotations + Clear all + Copy Prompt)
|
||||
// - Selection capture (window.getSelection())
|
||||
// - Section context auto-detection (findSection)
|
||||
// - localStorage persistence (voyage-annotate:v2:...)
|
||||
// - Annotatable elements (data-anchor-id on h1-h6, p, li, td, blockquote, pre)
|
||||
const html = buildHtml('/abs/path/brief.md', SAMPLE);
|
||||
// Toggle
|
||||
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');
|
||||
// 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');
|
||||
assert.ok(html.includes('ann-form-save'), 'must have a Save button');
|
||||
// Sidebar
|
||||
assert.ok(html.includes('ann-panel'), 'must have the annotations sidebar');
|
||||
assert.ok(html.includes('Your annotations'), 'sidebar must title the list');
|
||||
assert.ok(html.includes('Clear all'), 'sidebar must offer Clear all');
|
||||
assert.ok(html.includes('Copy Prompt'), 'sidebar must offer Copy Prompt');
|
||||
// Selection + section
|
||||
assert.ok(html.includes('window.getSelection'), 'must capture selection');
|
||||
assert.ok(html.includes('findSection'), 'must auto-detect section context');
|
||||
// Persistence
|
||||
assert.ok(html.includes("'voyage-annotate:v2:'"), 'must use the v2 localStorage key prefix');
|
||||
// Anchor coverage
|
||||
const anchors = (html.match(/data-anchor-id="anch-/g) || []).length;
|
||||
assert.ok(anchors >= 5, 'must emit data-anchor-id on enough elements (got ' + anchors + ')');
|
||||
});
|
||||
|
||||
test('renderMarkdown produces headings, lists, code, table, blockquote with anchors', () => {
|
||||
const html = renderMarkdown(`# H1
|
||||
## H2
|
||||
- a
|
||||
- b
|
||||
|
||||
1. one
|
||||
2. two
|
||||
|
||||
| Col | Val |
|
||||
|-----|-----|
|
||||
| x | 1 |
|
||||
|
||||
\`\`\`
|
||||
plain code
|
||||
\`\`\`
|
||||
|
||||
> quote
|
||||
`);
|
||||
assert.match(html, /<h1 data-anchor-id="anch-0">H1<\/h1>/);
|
||||
assert.match(html, /<h2 data-anchor-id="anch-1">H2<\/h2>/);
|
||||
assert.match(html, /<ul><li data-anchor-id=/);
|
||||
assert.match(html, /<ol><li data-anchor-id=/);
|
||||
assert.match(html, /<table>[\s\S]*<th data-anchor-id=/);
|
||||
assert.match(html, /<pre data-anchor-id=/);
|
||||
assert.match(html, /<blockquote data-anchor-id=/);
|
||||
});
|
||||
|
|
@ -152,3 +152,69 @@ test('validateBrief — wrong-type error message includes accepted set', () => {
|
|||
assert.ok(/trekbrief/.test(wrongType.message));
|
||||
assert.ok(/trekreview/.test(wrongType.message));
|
||||
});
|
||||
|
||||
// --- v5.1 — phase_signals additive field + sequencing gate ---
|
||||
|
||||
const SIGNALS_BLOCK = `phase_signals:
|
||||
- phase: research
|
||||
effort: standard
|
||||
- phase: plan
|
||||
effort: high
|
||||
model: opus
|
||||
- phase: execute
|
||||
effort: low
|
||||
model: sonnet
|
||||
- phase: review
|
||||
effort: standard
|
||||
`;
|
||||
|
||||
test('validateBrief — v5.1 well-formed phase_signals accepted', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\n${SIGNALS_BLOCK}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('validateBrief — pre-v5.1 brief without phase_signals accepted (backward-compat)', () => {
|
||||
const r = validateBriefContent(GOOD_BRIEF, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.ok(!r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1+ brief missing phase_signals + partial emits BRIEF_V51_MISSING_SIGNALS', () => {
|
||||
const t = GOOD_BRIEF.replace('brief_version: "2.0"', 'brief_version: "2.1"');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_V51_MISSING_SIGNALS'));
|
||||
});
|
||||
|
||||
test('validateBrief — v5.1+ brief with phase_signals_partial: true accepted', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', 'source: interview\nphase_signals_partial: true\n');
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('validateBrief — phase_signals + phase_signals_partial both set rejected (mutually exclusive)', () => {
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\nphase_signals_partial: true\n${SIGNALS_BLOCK}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE'));
|
||||
});
|
||||
|
||||
test('validateBrief — phase_signals with unknown phase rejected', () => {
|
||||
const BAD_SIGNALS = `phase_signals:
|
||||
- phase: nonsense
|
||||
effort: standard
|
||||
`;
|
||||
const t = GOOD_BRIEF
|
||||
.replace('brief_version: "2.0"', 'brief_version: "2.1"')
|
||||
.replace('source: interview\n', `source: interview\n${BAD_SIGNALS}`);
|
||||
const r = validateBriefContent(t, { strict: true });
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'BRIEF_INVALID_PHASE_SIGNAL_PHASE'));
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue