docs(voyage): pin Handover 8 + templates + PIPELINE_COMMANDS update — v4.2 Step 12

This commit is contained in:
Kjell Tore Guttormsen 2026-05-09 15:36:15 +02:00
commit 6d57314937
5 changed files with 308 additions and 2 deletions

View file

@ -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/<name>.mjs --json <path>` 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 14 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 <dir>` (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 <dir>` if the revised brief or plan needs further re-planning.
- Subsequent `/trekexecute --project <dir>` 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
<!-- voyage:anchor id="ANN-0001" target="plan.md#step-3" line="412" -->
```
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 <dir> --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.

View file

@ -14,6 +14,22 @@ source_findings:
---
-->
<!--
Optional annotation fields (Handover 8 — added in v4.2). Written by
`/trekrevise` when an operator annotates this plan via the playground.
All fields are additive — absence implies `revision: 0`. The plan_version
is NOT bumped; the validator tolerates these fields on every plan.md.
# revision: 0 # optional — annotation revision counter (incremented by /trekrevise)
# source_annotations: # optional — list of applied annotations from /trekrevise
# - id: ANN-0001
# target_artifact: plan.md
# target_anchor: step-3
# intent: change # change | add | remove | clarify | risk
# annotation_digest: <16-char sha256 prefix> # optional, deterministic over sorted source_annotations
# revision_reason: "..." # required only if revision is non-additive
-->
# {Task Title}
> **Plan quality: {grade}** ({score}/100) — {APPROVE | APPROVE_WITH_NOTES | REVISE | REPLAN}

View file

@ -1,3 +1,19 @@
<!--
Optional annotation fields (Handover 8 — added in v4.2). Written by
`/trekrevise` when an operator annotates this brief via the playground.
All fields are additive — absence implies `revision: 0`. The brief_version
is NOT bumped; the validator tolerates these fields on every brief.md.
# revision: 0 # optional — annotation revision counter (incremented by /trekrevise)
# source_annotations: # optional — list of applied annotations from /trekrevise
# - id: ANN-0001
# target_artifact: brief.md
# target_anchor: intent
# intent: change # change | add | remove | clarify | risk
# annotation_digest: <16-char sha256 prefix> # optional, deterministic over sorted source_annotations
# revision_reason: "..." # required only if revision is non-additive
-->
---
type: trekbrief
brief_version: 2.0

View file

@ -1,3 +1,19 @@
<!--
Optional annotation fields (Handover 8 — added in v4.2). Written by
`/trekrevise` when an operator annotates this review via the playground.
All fields are additive — absence implies `revision: 0`. The review_version
is NOT bumped; the validator tolerates these fields on every review.md.
# revision: 0 # optional — annotation revision counter (incremented by /trekrevise)
# source_annotations: # optional — list of applied annotations from /trekrevise
# - id: ANN-0001
# target_artifact: review.md
# target_anchor: finding-0123456789abcdef0123456789abcdef01234567
# intent: clarify # change | add | remove | clarify | risk
# annotation_digest: <16-char sha256 prefix> # optional, deterministic over sorted source_annotations
# revision_reason: "..." # required only if revision is non-additive
-->
---
type: trekreview
review_version: "1.0"

View file

@ -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 || [])}`,
);
});