From 6d5731493747aa22c9bb0ed763ecd9f4c59abbd0 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 9 May 2026 15:36:15 +0200 Subject: [PATCH] =?UTF-8?q?docs(voyage):=20pin=20Handover=208=20+=20templa?= =?UTF-8?q?tes=20+=20PIPELINE=5FCOMMANDS=20update=20=E2=80=94=20v4.2=20Ste?= =?UTF-8?q?p=2012?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/voyage/docs/HANDOVER-CONTRACTS.md | 93 ++++++++++ plugins/voyage/templates/plan-template.md | 16 ++ .../voyage/templates/trekbrief-template.md | 16 ++ .../voyage/templates/trekreview-template.md | 16 ++ .../voyage/tests/lib/doc-consistency.test.mjs | 169 +++++++++++++++++- 5 files changed, 308 insertions(+), 2 deletions(-) diff --git a/plugins/voyage/docs/HANDOVER-CONTRACTS.md b/plugins/voyage/docs/HANDOVER-CONTRACTS.md index ce00859..efec932 100644 --- a/plugins/voyage/docs/HANDOVER-CONTRACTS.md +++ b/plugins/voyage/docs/HANDOVER-CONTRACTS.md @@ -16,6 +16,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated: | `progress.json` | `schema_version` (top-level) | `"1"` | | `review.md` | `review_version` (frontmatter) | `1.0` | | `.session-state.local.json` | `schema_version` (top-level) | `1` (number) | +| `brief.md` / `plan.md` / `review.md` (annotated) | `revision` (frontmatter) | `0` (implicit), `1+` after `/trekrevise` | ## Breaking-change protocol @@ -36,6 +37,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated: | 5. progress.json (resume) | `lib/validators/progress-validator.mjs` | | 6. review → plan | `lib/validators/review-validator.mjs` | | 7. session-state (multi-session resume) | `lib/validators/session-state-validator.mjs` | +| 8. annotation → revision | `lib/parsers/anchor-parser.mjs` + `lib/parsers/annotation-digest.mjs` (parsing) and the existing brief / plan / review validators (forward-compat — additive fields tolerated) | Every validator exposes a CLI: `node lib/validators/.mjs --json ` returns `{valid, errors[], warnings[], parsed}`. Errors and warnings have stable `code` fields for downstream tooling. @@ -438,6 +440,96 @@ The `next-session-prompt-validator` (`lib/validators/next-session-prompt-validat --- +## Handover 8 — annotation → revision + +**Handover 8 closes the operator-feedback loop.** Where Handovers 1–4 flow forward (brief → research → plan → execute), Handover 5 makes execute resumable, Handover 6 routes review findings back into planning, and Handover 7 makes multi-session work survivable across fresh chats, **Handover 8 lets a human operator annotate an artifact (brief, plan, or review) inside the playground and feed those annotations back into a revision cycle without losing the original artifact's byte-for-byte content**. The pipeline becomes round-trippable: a single artifact can be annotated, revised, and re-rendered repeatedly, each revision recorded with a deterministic digest in frontmatter. + +**Producer:** +- The operator (manual step) — open the artifact in `playground/voyage-playground.html`, drag-select or hover-to-anchor, fill comment + intent in the modal, click "Eksporter batch" to copy the `/trekrevise` invocation to clipboard. +- `/trekrevise --project ` (Phase 3 — apply) — consumes the pasted batch, writes anchor comments back into the target artifact, increments `revision:`, appends entries to `source_annotations:`, and computes a fresh `annotation_digest`. + +**Consumer:** +- Subsequent `/trekplan --project ` if the revised brief or plan needs further re-planning. +- Subsequent `/trekexecute --project ` if the revised plan is ready for execution. +- All existing validators (brief, plan, review) — they tolerate the additive frontmatter fields without version bumps. + +**Path conventions:** +- The annotated artifact is the same canonical file in `{project_dir}/` (`brief.md`, `plan.md`, `review.md`). Revisions are *in-place* — no `plan-revN.md` shadow files. Audit trail lives in git commits + the `revision:` counter + `source_annotations:` list inside frontmatter. +- The playground itself lives at `playground/voyage-playground.html` (single self-contained file with vendored `markdown-it` + `highlight.js` under `playground/lib/`). + +**Frontmatter schema (additive — applies to brief.md, plan.md, review.md):** + +| Field | Type | Required | Allowed values | Notes | +|---|---|---|---|---| +| `revision` | number | optional | `0`, `1`, `2`, … | Absent = `0` (forward-compat). Incremented by `/trekrevise` on each apply | +| `source_annotations` | list | optional | block-style YAML list of dicts | Absent = empty. Audit trail of every annotation that has been folded into this artifact | +| `annotation_digest` | string | optional | first 16 hex chars of canonical SHA-256 over the sorted `source_annotations` array | Absent = no annotations applied yet. Deterministic — re-applying the same set yields the same digest | +| `revision_reason` | string | optional | free-form text | **Required only** when the revision is non-additive (e.g. removed scope, replaced an SC). Operators are encouraged to fill it for any revision | + +Each entry in `source_annotations` is a YAML dict: + +```yaml +source_annotations: + - id: ANN-0001 + target_artifact: plan.md + target_anchor: step-3 + intent: change # change | add | remove | clarify | risk + comment: "consider rolling back if cache_creation jumps >10%" + line: 412 +``` + +**Anchor format (block-level HTML comments inside the body):** + +```html + +``` + +Anchors are placed **only at block boundaries** — not in list items, not mid-paragraph, not at line-start collision points where a markdown parser might fold them into surrounding content. The playground enforces this discipline via `validateAnchorPlacement()` in `lib/parsers/anchor-parser.mjs`. Anchors survive markdown rendering as comments (no visible artifact) and round-trip through `parseAnchors → addAnchors → stripAnchors` byte-identically (SC2 contract). + +**Body invariants:** +- The artifact's existing required sections remain untouched. `/trekrevise` writes anchor comments only at block boundaries declared by the operator's annotations. +- After each apply: byte-identical content outside anchor blocks. Stripping all anchors with `stripAnchors()` MUST yield the artifact's original body modulo the operator's deliberate prose edits. + +**Validation strategy:** + +| Layer | When | What | +|---|---|---| +| `revision` shape | every read | Number (or absent → treat as `0`). Negative or non-integer rejected by validator | +| `source_annotations` shape | every read | Array of dicts; each dict must include `id`, `target_artifact`, `target_anchor`, `intent`. Other fields tolerated (drift-WARN) | +| `annotation_digest` shape | every read | 16-char lowercase hex; presence requires `source_annotations` non-empty | +| Anchor placement | `/trekrevise` Phase 2 (validate) | `validateAnchorPlacement()` rejects in-list / mid-paragraph / line-start anchors before write | +| Round-trip integrity | `/trekrevise` Phase 4 (post-write) | `stripAnchors()` of the just-written file MUST equal the pre-write body | + +The parsers (`lib/parsers/anchor-parser.mjs`, `lib/parsers/annotation-digest.mjs`) are pure functions — no I/O, fully unit-tested. The existing brief / plan / review validators in `lib/validators/` already tolerate the new optional fields (forward-compat — see Step 12 manifest pins). + +**Forward-compat — additive principle:** Artifacts without any of the four new fields validate as `revision: 0` with empty annotations. This means every brief / plan / review written before v4.2 remains valid without migration. Producers (operators using `/trekrevise`) opt into the schema by writing the fields; consumers tolerate their presence or absence equally. + +**Idempotence:** Re-applying the same annotation batch on the same artifact (same anchors, same intents, same comments) yields the same `annotation_digest` and no body diff outside anchor refresh. This is enforced by `computeAnnotationDigest()` canonicalizing the sorted `source_annotations` array before hashing — operators can replay a batch safely, and the test suite pins this with `tests/parsers/annotation-digest.test.mjs`. + +**Versioning:** No `*_version` bump for v4.2 — the four new fields are additive. A future schema break (e.g. removing `target_anchor` in favor of structured pointer) would bump `brief_version` / `plan_version` / `review_version` per the breaking-change protocol. + +**Failure modes:** +- `ANNOTATION_PLACEMENT_INVALID` → `/trekrevise` Phase 2 halts; operator re-anchors via playground +- `ANNOTATION_DIGEST_DRIFT` → digest computed at apply-time differs from operator's expected digest in the pasted batch; halt with mismatch report (suggests the batch was hand-edited after export) +- `ANNOTATION_ROUNDTRIP_FAIL` → post-write `stripAnchors()` does not yield the original body; rollback restores the byte-identical pre-write file from `*.local.bak` +- Parser failures from `parseAnchors()` produce `{valid: false, errors: [...]}` and `/trekrevise` halts before any write. The atomic-write pattern (tmp-file + rename) guarantees the canonical file is never partially updated. + +### § Lifecycle + +The annotation cycle has three stages — no persistent state file (unlike Handover 7), only the artifact frontmatter and git history record what happened: + +| Stage | Owner | Action | +|---|---|---| +| Annotate | Operator + playground UI | Open artifact in `playground/voyage-playground.html`, anchor + comment, click "Eksporter batch" → clipboard contains a `/trekrevise --project --apply '{...JSON...}'` command | +| Apply | `/trekrevise` | Phase 1 parse the pasted batch, Phase 2 validate placement, Phase 3 atomically write anchors + frontmatter (`revision: N+1`, append to `source_annotations`, recompute `annotation_digest`), Phase 4 round-trip integrity check, Phase 5 commit | +| Re-render | Playground (next open) | The freshly-revised artifact loads with anchors visible as inline comment markers; operator can iterate or call `/trekplan` / `/trekexecute` on the revised file | + +**Single-iteration MVP (v4.2 scope):** Each operator annotation batch produces one `revision:` increment. Multi-iteration loops (e.g. revise → re-review → revise again without operator intervention) are deferred indefinitely — the brief's SC4 wording is single-revision, and operator validation that multi-iteration is desired has not been collected (see plan Alternatives table). The single-iteration MVP keeps the audit trail unambiguous: one `revision:` bump per `/trekrevise` invocation. + +**Stale-anchor principle:** Unlike `.session-state.local.json`, anchors are durable and intentionally retained — they document *why* a revision happened. There is no `--cleanup` for anchors. Removing them is a manual operator decision, executed via the playground "Strip all anchors" affordance or hand-editing the source. `/trekrevise` does not remove anchors automatically. + +--- + ## Stability summary | Handover | Validation strength | Owner | Risk | @@ -449,5 +541,6 @@ The `next-session-prompt-validator` (`lib/validators/next-session-prompt-validat | 5. progress.json | shape + resume readiness | this plugin | medium — drift during compaction handled by pre-compact-flush hook (CC v2.1.105+) | | 6. review → plan | strict at write, soft at read | this plugin | low — additive feedback loop; consumer falls back gracefully when source_findings is absent | | 7. session-state (multi-session resume) | required-fields + status enum + drift-WARN extras | this plugin | low — readers tolerate unknown keys; writers are owned by trekexecute Phase 8 + helper command | +| 8. annotation → revision | parser-strict at write (placement + round-trip), validator-soft at read (additive fields) | this plugin | low — additive frontmatter; existing artifacts validate as `revision: 0` without migration | When extending the plugin or adding a new pipeline stage, follow the same pattern: produce an artifact with a versioned frontmatter (or `schema_version` for JSON), write a validator under `lib/validators/`, add fixtures under `tests/fixtures/`, and add an entry to this document. diff --git a/plugins/voyage/templates/plan-template.md b/plugins/voyage/templates/plan-template.md index f249ff4..40b0ff2 100644 --- a/plugins/voyage/templates/plan-template.md +++ b/plugins/voyage/templates/plan-template.md @@ -14,6 +14,22 @@ source_findings: --- --> + + # {Task Title} > **Plan quality: {grade}** ({score}/100) — {APPROVE | APPROVE_WITH_NOTES | REVISE | REPLAN} diff --git a/plugins/voyage/templates/trekbrief-template.md b/plugins/voyage/templates/trekbrief-template.md index b35d893..bc7088c 100644 --- a/plugins/voyage/templates/trekbrief-template.md +++ b/plugins/voyage/templates/trekbrief-template.md @@ -1,3 +1,19 @@ + + --- type: trekbrief brief_version: 2.0 diff --git a/plugins/voyage/templates/trekreview-template.md b/plugins/voyage/templates/trekreview-template.md index a47c7cb..3a5491f 100644 --- a/plugins/voyage/templates/trekreview-template.md +++ b/plugins/voyage/templates/trekreview-template.md @@ -1,3 +1,19 @@ + + --- type: trekreview review_version: "1.0" diff --git a/plugins/voyage/tests/lib/doc-consistency.test.mjs b/plugins/voyage/tests/lib/doc-consistency.test.mjs index 191568e..b3b961f 100644 --- a/plugins/voyage/tests/lib/doc-consistency.test.mjs +++ b/plugins/voyage/tests/lib/doc-consistency.test.mjs @@ -94,8 +94,10 @@ test('settings.json no longer carries vestigial exploration block', () => { 'agentTeam block was vestigial — should be deleted in v3.1.0 Spor 0'); }); -test('CLAUDE.md mentions all six pipeline commands', () => { - // Step 21 of v4.1 — added /trekcontinue to coverage (was 5/6 before). +test('CLAUDE.md mentions all seven pipeline commands', () => { + // v4.1 Step 21 — added /trekcontinue to coverage (was 5/6 before). + // v4.2 Step 12 — added /trekrevise (Handover 8 producer), bringing the + // canonical pipeline to seven commands. const md = read('CLAUDE.md'); for (const c of [ '/trekbrief', @@ -103,6 +105,7 @@ test('CLAUDE.md mentions all six pipeline commands', () => { '/trekplan', '/trekexecute', '/trekreview', + '/trekrevise', '/trekcontinue', ]) { assert.ok(md.includes(c), `CLAUDE.md missing reference to ${c}`); @@ -258,6 +261,7 @@ const PIPELINE_COMMANDS = [ 'trekplan.md', 'trekexecute.md', 'trekreview.md', + 'trekrevise.md', 'trekcontinue.md', ]; @@ -398,3 +402,164 @@ test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => { 'Phase 8 should explicitly enumerate FORBIDDEN headings', ); }); + +// --- v4.2 Step 12 — Handover 8 + annotation pipeline pins --- +// +// CLAUDE.md / README.md / CHANGELOG / annotation-quickstart pins are deferred +// to Step 13 (post-write of those files). Step 12 only pins HANDOVER-CONTRACTS, +// templates, scaffold-files, and the parseAnchors round-trip on the example +// fixture. + +import { existsSync, statSync } from 'node:fs'; + +test('HANDOVER-CONTRACTS.md contains Handover 8 section (annotation → revision)', () => { + const text = read('docs/HANDOVER-CONTRACTS.md'); + assert.ok( + text.includes('## Handover 8'), + 'docs/HANDOVER-CONTRACTS.md should document Handover 8 (annotation → revision) — added in v4.2', + ); +}); + +test('HANDOVER-CONTRACTS.md Handover 8 names annotation_digest and source_annotations', () => { + const text = read('docs/HANDOVER-CONTRACTS.md'); + const h8Start = text.indexOf('## Handover 8'); + assert.ok(h8Start >= 0, 'Handover 8 heading missing'); + const h8End = text.indexOf('## Stability summary', h8Start); + assert.ok(h8End > h8Start, 'Stability summary heading missing — could not bound Handover 8'); + const h8 = text.slice(h8Start, h8End); + assert.ok( + h8.includes('annotation_digest'), + 'Handover 8 section should document the annotation_digest frontmatter field', + ); + assert.ok( + h8.includes('source_annotations'), + 'Handover 8 section should document the source_annotations frontmatter field', + ); + assert.ok( + h8.includes('revision'), + 'Handover 8 section should document the revision counter field', + ); +}); + +test('templates/plan-template.md documents annotation revision fields', () => { + const tpl = read('templates/plan-template.md'); + assert.ok( + tpl.includes('revision:'), + 'plan-template.md must document optional revision counter (Handover 8)', + ); + assert.ok( + tpl.includes('source_annotations:'), + 'plan-template.md must document optional source_annotations list (Handover 8)', + ); + assert.ok( + tpl.includes('annotation_digest'), + 'plan-template.md must document optional annotation_digest field (Handover 8)', + ); +}); + +test('templates/trekbrief-template.md documents annotation revision fields', () => { + const tpl = read('templates/trekbrief-template.md'); + assert.ok( + tpl.includes('revision:'), + 'trekbrief-template.md must document optional revision counter (Handover 8)', + ); + assert.ok( + tpl.includes('source_annotations:'), + 'trekbrief-template.md must document optional source_annotations list (Handover 8)', + ); + assert.ok( + tpl.includes('annotation_digest'), + 'trekbrief-template.md must document optional annotation_digest field (Handover 8)', + ); +}); + +test('templates/trekreview-template.md documents annotation revision fields', () => { + const tpl = read('templates/trekreview-template.md'); + assert.ok( + tpl.includes('revision:'), + 'trekreview-template.md must document optional revision counter (Handover 8)', + ); + assert.ok( + tpl.includes('source_annotations:'), + 'trekreview-template.md must document optional source_annotations list (Handover 8)', + ); + assert.ok( + tpl.includes('annotation_digest'), + 'trekreview-template.md must document optional annotation_digest field (Handover 8)', + ); +}); + +test('playground/ directory exists at voyage root (Handover 8 producer surface)', () => { + const playgroundDir = join(ROOT, 'playground'); + assert.ok(existsSync(playgroundDir), 'playground/ directory missing'); + assert.ok(statSync(playgroundDir).isDirectory(), 'playground/ is not a directory'); + // Self-contained HTML must exist + assert.ok( + existsSync(join(playgroundDir, 'voyage-playground.html')), + 'playground/voyage-playground.html missing — operator-facing entry point', + ); +}); + +test('playground/ files do NOT import or reference `marked` (risk-assessor H1)', () => { + // Walk playground/ recursively. Exclude vendor/playground-design-system + // (consumed via the shared design system; not part of voyage's playground + // markdown renderer). Exclude any *MANIFEST.json files. Assert no file + // contains the standalone identifier `marked` (case-sensitive, word-boundary). + // markdown-it is the locked renderer per research-03 + alternatives table. + const playgroundDir = join(ROOT, 'playground'); + assert.ok(existsSync(playgroundDir), 'playground/ directory missing — cannot verify marked-ban'); + const offenders = []; + function walk(dir) { + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + const s = statSync(p); + if (s.isDirectory()) { + // Skip vendor design-system trees (shared infra, not voyage's renderer) + if (entry === 'playground-design-system') continue; + walk(p); + } else if (s.isFile()) { + // Skip vendor manifest JSONs + if (entry.endsWith('MANIFEST.json')) continue; + if (entry === 'VENDOR-MANIFEST.json') continue; + const txt = readFileSync(p, 'utf-8'); + if (/\bmarked\b/.test(txt)) { + offenders.push(p.slice(ROOT.length + 1)); + } + } + } + } + walk(playgroundDir); + assert.deepStrictEqual( + offenders, + [], + `playground/ files contain banned identifier "marked": ${offenders.join(', ')}. ` + + `Use markdown-it instead — see plan Alternatives table (Issue #3515 disqualifies marked).`, + ); +}); + +test('scripts/render-artifact.mjs exists (SC1/SC11 self-render gate)', () => { + assert.ok( + existsSync(join(ROOT, 'scripts/render-artifact.mjs')), + 'scripts/render-artifact.mjs missing — required by SC1 (offline render) and SC11 (pipeline-self-eat)', + ); +}); + +test('lib/util/revision-guard.mjs exists (plan-critic M4 — atomic-write rollback guard)', () => { + assert.ok( + existsSync(join(ROOT, 'lib/util/revision-guard.mjs')), + 'lib/util/revision-guard.mjs missing — required for /trekrevise rollback hygiene', + ); +}); + +test('tests/fixtures/annotation/annotation-example.md parses cleanly via parseAnchors (ESM)', async () => { + // Plan-critic m4 — fix the SC12 require/import mixup. Use ESM dynamic import, + // not require(). The parser is pure — no I/O, no side effects. + const { parseAnchors } = await import('../../lib/parsers/anchor-parser.mjs'); + const fixturePath = join(ROOT, 'tests/fixtures/annotation/annotation-example.md'); + assert.ok(existsSync(fixturePath), 'tests/fixtures/annotation/annotation-example.md missing'); + const result = parseAnchors(readFileSync(fixturePath, 'utf-8')); + assert.ok( + result.valid, + `parseAnchors failed on annotation-example.md fixture: ${JSON.stringify(result.errors || [])}`, + ); +});