From 4fbc52bbb4cf32d833ae90f8cd66d64f39cea709 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sat, 9 May 2026 15:09:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(voyage):=20add=20commands/trekrevise.md=20?= =?UTF-8?q?=E2=80=94=207th=20pipeline=20command=20+=20settings.json=20scop?= =?UTF-8?q?e=20=E2=80=94=20v4.2=20Step=206=20[skip-docs]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements Phase 1-8 of /trekrevise (Handover 8 producer): - Phase 1: parse mode + reject MULTI_ARTIFACT_NOT_SUPPORTED - Phase 2: read source + check stale .local.bak - Phase 3: parseAnchors + validateAnchorPlacement (no partial revisions) - Phase 4: computeAnnotationDigest + non-additive detection - Phase 5: revisionGuard orchestration (backup -> mutate -> validate -> rollback-on-fail) - Phase 6: branch on outcome (applied / rolled-back / mutator-failed) - Phase 7: optional review-gate (advisory, no auto-rollback) - Phase 8: trekrevise-stats.jsonl + report Frontmatter: name=trekrevise, model=opus, allowed-tools includes Read/Write/Edit/Bash/Grep/Glob. Reuses lib/parsers/anchor-parser, lib/parsers/annotation-digest, lib/util/markdown-write, lib/util/revision-guard, lib/validators/{brief,plan,review}. settings.json: register new top-level scope trekrevise with trekrevise-stats.jsonl tracking (mirrors trekplan/trekresearch shape). Forward-pinning to keep doc-consistency invariants green: - tests/lib/doc-consistency.test.mjs: known-scopes allowlist += trekrevise - CLAUDE.md commands table: add /trekrevise row Plan Step 13 owns the full README/CLAUDE.md/CHANGELOG content sync; this commit is the implementation milestone, not the doc milestone. Refs plan.md Step 6 + plan-critic M3. --- plugins/voyage/CLAUDE.md | 1 + plugins/voyage/commands/trekrevise.md | 508 ++++++++++++++++++ plugins/voyage/settings.json | 7 + .../voyage/tests/lib/doc-consistency.test.mjs | 2 +- 4 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 plugins/voyage/commands/trekrevise.md diff --git a/plugins/voyage/CLAUDE.md b/plugins/voyage/CLAUDE.md index 7149722..2636b8c 100644 --- a/plugins/voyage/CLAUDE.md +++ b/plugins/voyage/CLAUDE.md @@ -15,6 +15,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut | `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`. Auto-discovers `architecture/overview.md` if present | opus | | `/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 | +| `/trekrevise` | Revise — apply operator annotations from the playground back into brief/plan/review with audit trail (Handover 8). Requires `--project` | 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 | diff --git a/plugins/voyage/commands/trekrevise.md b/plugins/voyage/commands/trekrevise.md new file mode 100644 index 0000000..698d66c --- /dev/null +++ b/plugins/voyage/commands/trekrevise.md @@ -0,0 +1,508 @@ +--- +name: trekrevise +description: | + Apply operator-annotated brief/plan/review back into the source artifact + with audit trail. Reads anchored comments from pasted (or `--from-file`) + content, validates anchor placement, computes a canonical + annotation_digest, and writes the revision in-place with + rollback-on-validator-fail. Implements Handover 8 (annotation → revision). +argument-hint: "--project [--from-file ] [--target {brief|plan|review|auto}] [--reason ] [--profile ] [--gates ]" +model: opus +allowed-tools: Read, Write, Edit, Bash, Grep, Glob +--- + +# Ultrarevise Local v1.0 + +Apply operator annotations from the voyage playground back into the source +artifact (`brief.md`, `plan.md`, or `review.md`) with audit-trail in +frontmatter. Implements **Handover 8 (annotation → revision)**. + +Pipeline position: + +``` +/trekbrief → brief.md +/trekresearch → research/*.md +/trekplan → plan.md +/trekexecute → progress.json (+ commits) +/trekreview → review.md +/trekrevise → in-place revision of brief|plan|review (this command) +``` + +The annotation source is the **operator-edited markdown** that the voyage +playground exports. The playground reads `{target}.md`, lets the operator +add anchored comments via three creation gestures, and exports a markdown +blob carrying `` block-level HTML comments +inline plus the operator's revised body content. `/trekrevise` consumes +that blob, validates the anchor placement, computes a canonical +`annotation_digest`, and replaces the source artifact in-place. A +pre-revision backup (`{target}.md.local.bak`) is created and used to roll +back if the post-write validator rejects the new content. + +The revision is **additive by default**: applying the same annotation set +twice is idempotent (same digest, same body). Non-additive revisions +(structural step reorder, section removal, frontmatter `*_version` +changes) require an explicit `--reason ""`. + +See `docs/HANDOVER-CONTRACTS.md § Handover 8` for the schema contract. + +## Phase 1 — Parse mode and validate input + +Parse `$ARGUMENTS` via the shared arg-parser: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekrevise "$@" +``` + +The parser recognizes these flags (see `lib/parsers/arg-parser.mjs` +FLAG_SCHEMA `trekrevise` entry): + +| Flag | Type | Purpose | +|------|------|---------| +| `--project ` | valued | Required. Path to the trekplan project folder containing the target artifact. | +| `--from-file ` | valued | Optional. Read annotated content from a file instead of stdin/pasted prompt. | +| `--target {brief\|plan\|review\|auto}` | valued | Optional. Which artifact to revise. `auto` (default) infers from frontmatter `type:` field of the annotated input. | +| `--reason ` | valued | Optional. Required when revision is non-additive (see Phase 4). | +| `--profile ` | valued | Optional. Model profile (`economy`, `balanced`, `premium`, custom). Default: `balanced`. | +| `--gates ` | valued | Optional. Autonomy mode (`open`, `closed`, `adaptive`). Default: `adaptive`. | + +Resolution: + +1. If `--project` is missing, print usage and stop: + ``` + Error: --project is required. + Usage: /trekrevise --project [--from-file ] [--target {brief|plan|review|auto}] [--reason ] + ``` +2. Trim trailing slash from `{dir}`. Set: + - `project_dir = {dir}` + - `brief_path = {dir}/brief.md` + - `plan_path = {dir}/plan.md` + - `review_path = {dir}/review.md` +3. If `{dir}` does not exist: + ``` + Error: project directory missing: {dir} + Run /trekbrief first. + ``` +4. Determine the **annotated input source**: + - If `--from-file ` is set, read that file. If the file is missing + or unreadable, stop with `Error: --from-file path not readable: {path}`. + - Otherwise, the annotated content MUST appear in the operator's prompt + (pasted directly into the chat). Capture it from the prompt text. + - If neither source yields content, stop with: + ``` + Error: no annotated content provided. Pass --from-file or paste the + content directly into your prompt. + ``` +5. **Reject multi-artifact bundle.** Scan the annotated input for two or + more lines matching `^---$` at line-start that introduce frontmatter + blocks (i.e. more than one `---\n...\n---\n` pair). If detected: + ``` + Error: MULTI_ARTIFACT_NOT_SUPPORTED — annotated input contains more than + one artifact (frontmatter blocks detected: {N}). /trekrevise revises one + artifact per invocation. Split the bundle and re-run for each target. + ``` + Stop. No partial revisions. +6. Resolve `target`: + - If `--target` is `brief`, `plan`, or `review`: use it explicitly. + - If `--target` is `auto` (or missing): parse the annotated input's + frontmatter via `parseDocument`. Read the `type:` field — expect + `brief`, `plan`, or `review`. If absent, fall back to the artifact + whose schema validates against the input (try in order: brief, plan, + review). If none match, stop with: + ``` + Error: cannot infer --target from annotated input frontmatter. + Pass --target {brief|plan|review} explicitly. + ``` +7. Resolve the target file path: `target_path = {project_dir}/{target}.md`. + If it does not exist, stop: + ``` + Error: target artifact missing: {target_path} + Run /trek{brief|plan|review} first to produce it. + ``` + +Set: +- `mode = revise` (the only mode currently) +- `profile`, `gates`, `reason` per flags + +Report: +``` +Mode: revise +Project: {project_dir} +Target: {target} → {target_path} +Source: {--from-file path | pasted} +Profile: {profile} +``` + +## Phase 2 — Read source artifact + check rollback hygiene + +Stale-backup precheck: + +```bash +test -f "{target_path}.local.bak" +``` + +If a `.local.bak` exists from a previously aborted run, abort: + +``` +Error: stale rollback backup found at {target_path}.local.bak +Inspect it (it represents the pre-revision state of a prior aborted run), +then either restore it manually with: + cp "{target_path}.local.bak" "{target_path}" + rm "{target_path}.local.bak" +or delete it if you want to keep the current target_path: + rm "{target_path}.local.bak" +After resolving, re-run /trekrevise. +``` + +Stop. The revision is not applied. + +Read the source artifact: + +```js +import { parseDocument } from '${CLAUDE_PLUGIN_ROOT}/lib/util/frontmatter.mjs'; +const source = parseDocument(readFileSync(target_path, 'utf-8')); +``` + +If `source.valid === false`: + +``` +Error: source artifact has malformed frontmatter: {target_path} +Fix it manually before annotating. +``` + +Capture: +- `existing_frontmatter = source.parsed.frontmatter` +- `existing_body = source.parsed.body` +- `existing_revision = existing_frontmatter.revision || 0` + +## Phase 3 — Parse anchors + validate placement + +Parse anchors from the **annotated input** (not the source artifact): + +```js +import { parseAnchors, validateAnchorPlacement, stripAnchors } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/anchor-parser.mjs'; +const annotated = parseDocument(annotated_input_text); +const annotated_body = annotated.parsed.body; +const anchors_result = parseAnchors(annotated_body); +``` + +If `anchors_result.valid === false`: + +``` +Error: anchor parse failed in annotated input. +{for each error: file:line:rule_key — message} +``` + +Stop. **No partial revisions** — abort BEFORE any write. + +Run placement validation: + +```js +const placement_result = validateAnchorPlacement(annotated_body, anchors_result.parsed); +``` + +If `placement_result.valid === false`: + +``` +Error: anchor placement violations detected. +{for each error: line N: rule_key — message} +``` + +Stop. + +Capture `anchors = anchors_result.parsed` (an array of anchor objects with +`{id, target, line, snippet?, intent?}`). + +If `anchors.length === 0`, the operator submitted an annotation-free +revision. Continue — empty-anchor round-trip is a valid case (used by +SC2). The body diff will still be applied; the audit fields will record +zero anchors plus an all-zeros digest baseline. + +## Phase 4 — Compute revision diff + digest + +Compute the canonical digest from the annotation set: + +```js +import { computeAnnotationDigest } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/annotation-digest.mjs'; +const annotation_digest = computeAnnotationDigest(anchors); +``` + +Determine the new revision counter: + +```js +const new_revision = (existing_revision | 0) + 1; +``` + +**Detect non-additive revision.** A revision is non-additive when ANY of: + +- The body removes a `### Step N:` heading that previously existed. +- The body removes any `## ` heading per the target's + validator (e.g. plan: "Implementation Plan", "Verification"; brief: + "Goal", "Success Criteria"; review: "Executive Summary", "Coverage"). +- The frontmatter changes any `*_version` field (e.g. `plan_version`, + `brief_version`, `review_version`). +- A `### Step N:` heading is reordered (different N sequence than before). + +Compute these by comparing `existing_body` heading sequence with the +`annotated_body` (with anchors stripped via `stripAnchors`). The +heading-sequence comparison is sufficient — content edits inside an +existing step are always additive. + +If non-additive AND `--reason` was NOT supplied: + +``` +Error: non-additive revision detected: + - {bullet list of detected non-additive changes} + +Re-run with --reason "" to acknowledge the structural +change and record it in revision_reason for the audit trail. +``` + +Stop. The revision is not applied. + +If non-additive AND `--reason` is supplied: capture +`revision_reason = `. If additive, leave `revision_reason` +unset (omitted from frontmatter). + +## Phase 5 — Apply revisions in-place + +Strip anchors from the annotated body to produce the new artifact body: + +```js +const new_body = stripAnchors(annotated_body); +``` + +Build the new frontmatter object by merging the existing fields with the +revision audit fields: + +```js +const new_frontmatter = { + ...existing_frontmatter, + revision: new_revision, + source_annotations: anchors.map(a => ({ + id: a.id, + target_artifact: target, + target_anchor: a.target, + line: a.line, + intent: a.intent || 'change', + snippet: a.snippet || '', + timestamp: new Date().toISOString(), + })), + annotation_digest, + ...(revision_reason ? { revision_reason } : {}), +}; +``` + +Apply the revision via `revisionGuard` from `lib/util/revision-guard.mjs`, +which performs the full **backup → mutate → atomic write → validate → +rollback-on-fail** orchestration: + +```js +import { revisionGuard } from '${CLAUDE_PLUGIN_ROOT}/lib/util/revision-guard.mjs'; +import { validateBrief, validatePlan, validateReview } from '${CLAUDE_PLUGIN_ROOT}/lib/validators/...'; + +const validator = ({ + brief: validateBrief, + plan: validatePlan, + review: validateReview, +})[target]; + +const result = revisionGuard( + target_path, + ({ frontmatter, body }) => ({ + frontmatter: new_frontmatter, + body: new_body, + }), + validator, +); +``` + +`revisionGuard` returns `{outcome, validator_result, sha256_before, +sha256_after, error?}`. Outcomes: + +- `applied` — write succeeded, validator passed, `.local.bak` deleted. +- `rolled-back` — write succeeded, validator FAILED, file restored from + `.local.bak`, byte-identical to pre-revision state. +- `mutator-failed` — pre-existing backup, mutator threw, or read failed. + +## Phase 6 — Validate revision + +Branch on the `revisionGuard` outcome: + +### outcome === 'applied' + +The revision is in place. Surface validator warnings (if any) to the +operator, but do not roll back. Continue to Phase 7. + +### outcome === 'rolled-back' + +The post-write validator rejected the new content. The file is +byte-identical to its pre-revision state. + +``` +Validator REJECTED the revision. Rolled back to pre-revision state. +sha256_before === sha256_after (byte-identical): {true|false} + +Validator errors: + - {code} {file}:{line} — {message} + ... + +To inspect the proposed (rejected) revision, retry with --from-file pointing +at a fixed annotated input, or fix the annotation set in the playground and +re-export. +``` + +Stop. The audit-trail fields are NOT recorded; revision counter is +unchanged. + +### outcome === 'mutator-failed' + +``` +Error: revision could not be applied: {revisionGuard.error} + +If the error references a pre-existing backup, follow Phase 2's +remediation steps. +``` + +Stop. + +## Phase 7 — Optional review-gate (plan/review targets only) + +Only runs when `target === 'plan'` AND `{review_path}` exists, OR +`target === 'review'` AND `{plan_path}` exists. Otherwise skip to Phase 8. + +Run the existing review (validate-only mode) against the revised plan, or +re-validate the revised review: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/lib/validators/review-validator.mjs --json "{review_path}" +``` + +If the validator returns BLOCK or FAIL: + +``` +Review-gate WARN: revised {target}.md is in place, but the review at +{review_path} contains BLOCKER findings against the now-revised content. +Verdict: BLOCK +Top findings: + - {list} + +The revised file is NOT auto-rolled-back. You may: + - Re-annotate to address the BLOCKER findings, then /trekrevise again. + - Accept the gap and run /trekreview to refresh review.md. + - Manually restore via: + cp "{target_path}.local.bak" "{target_path}" + (Note: the .local.bak was deleted on Phase 6 success. To re-rollback + after Phase 7, use git: `git checkout HEAD -- "{target_path}"` if you + have not yet committed the revision.) +``` + +The revised file remains as written. Continue to Phase 8. + +If the gate passes (verdict ALLOW or WARN-only): continue silently. + +## Phase 8 — Stats + report + +Append a stats line to `${CLAUDE_PLUGIN_DATA}/trekrevise-stats.jsonl` +(create the file if it does not exist): + +```json +{"ts":"{ISO-8601}","target":"{brief|plan|review}","project_dir":"{dir}","revision":{N},"anchor_count":{N},"digest":"{16-char-hex}","validator_verdict":"{pass|fail-rolled-back}","outcome":"{applied|rolled-back|mutator-failed}","profile_used":"{profile}"} +``` + +Use inline `appendFileSync` (mirrors the jsonl-append pattern in +`hooks/scripts/post-bash-stats.mjs:50`). If `${CLAUDE_PLUGIN_DATA}` is +unset or not writable, skip stats silently. Never let stats failures block +the main workflow. + +Emit the human-readable summary: + +``` +## Ultrarevise Complete + +**Project:** {project_dir} +**Target:** {target} ({target_path}) +**Revision:** {existing_revision} → {new_revision} +**Anchors applied:** {N} +**annotation_digest:** {16-char-hex} +**Outcome:** {applied | rolled-back | mutator-failed} +{if revision_reason}: **revision_reason:** {revision_reason} + +### Audit fields written +- revision: {N} +- source_annotations: {N entries} +- annotation_digest: {hex} +{if revision_reason}: - revision_reason: "{reason}" + +{if Phase 7 surfaced findings}: +### Review-gate WARN +- Verdict: {BLOCK|WARN} +- Top findings: + - {list} + +You can: +- Inspect the revised file at {target_path} +- Run /trekreview --project {project_dir} to refresh review.md +- Run /trekplan --project {project_dir} to extend the plan +- Re-annotate via the playground and run /trekrevise again +- Roll back manually via: git checkout HEAD -- "{target_path}" +``` + +## Profile (v4.1) + +Accepts `--profile ` where `` is `economy`, `balanced`, `premium`, +or a custom profile under `voyage-profiles/`. Default: `balanced`. + +Resolution order (per `lib/profiles/resolver.mjs`): + +1. `--profile` flag (source: `flag`) +2. `VOYAGE_PROFILE` env-var (source: `env`) +3. `balanced` default (source: `default`) + +The selected profile drives `phase_models.revise`. `/trekrevise` is mostly +deterministic (parsing + validating + atomic writes), so all built-in +profiles use sonnet for any sub-agent invocation. The operator-facing +synthesis in Phase 6/7 stays in the main thread regardless of profile. + +Examples: + +``` +/trekrevise --profile balanced --project .claude/projects/2026-05-09-foo +VOYAGE_PROFILE=premium /trekrevise --project ... --target plan +``` + +Stats records emit `profile_used` (and `profile_source` when available). + +## Hard rules + +- **No partial revisions.** If anchor parse or placement validation fails + in Phase 3, abort BEFORE any write. The source artifact is never left + in a half-revised state. +- **One artifact per invocation.** Multi-artifact bundles return + `MULTI_ARTIFACT_NOT_SUPPORTED`. Split into per-artifact runs. +- **revision_reason is required for non-additive changes.** Step + reorder, section removal, and `*_version` bumps require an explicit + reason recorded in frontmatter for audit. +- **Backup hygiene.** Pre-existing `.local.bak` blocks the run. Operator + must inspect or delete it; the executor never auto-overwrites it. +- **Validator is the gate.** A revision that writes successfully but + fails post-write validation is rolled back to byte-identical + pre-revision state. Audit fields are NOT recorded on rollback. +- **Idempotent digest.** Re-applying the same annotation set yields the + same `annotation_digest`. The digest is canonical SHA-256 over a + field-sorted, id-sorted, pipe-separated, line-joined serialization + (16-char hex prefix). +- **Forward-compat fields.** `revision`, `source_annotations`, + `annotation_digest`, and `revision_reason` are additive frontmatter + fields. Validators that predate v4.2 ignore them. Artifacts without + `revision:` are treated as `revision: 0`. +- **Anchor format.** `` + block-level only; placement disipline enforced (not in list-items, not + inside fenced code blocks, not at line-start collisions with frontmatter + delimiter, manifest:, plan_version:, ### Step N:, ## required sections, + or 40-char hex finding-IDs). +- **No production code.** This command never runs production code, never + writes to anything outside `{project_dir}` and `${CLAUDE_PLUGIN_DATA}`. +- **Operator has final say.** The review-gate in Phase 7 is advisory — + it never auto-rolls-back a successful revision. The operator decides + whether to re-annotate, refresh the review, or accept the gap. diff --git a/plugins/voyage/settings.json b/plugins/voyage/settings.json index 2f9a447..4903705 100644 --- a/plugins/voyage/settings.json +++ b/plugins/voyage/settings.json @@ -27,5 +27,12 @@ "enabled": true, "statsFile": "trekresearch-stats.jsonl" } + }, + "trekrevise": { + "defaultMode": "default", + "tracking": { + "enabled": true, + "statsFile": "trekrevise-stats.jsonl" + } } } \ No newline at end of file diff --git a/plugins/voyage/tests/lib/doc-consistency.test.mjs b/plugins/voyage/tests/lib/doc-consistency.test.mjs index 4e81540..191568e 100644 --- a/plugins/voyage/tests/lib/doc-consistency.test.mjs +++ b/plugins/voyage/tests/lib/doc-consistency.test.mjs @@ -80,7 +80,7 @@ test('commands/trekexecute.md still parses v1.7 plan schema', () => { test('settings.json has only known top-level scopes after Spor 0 cleanup', () => { const cfg = JSON.parse(read('settings.json')); - const known = ['trekplan', 'trekresearch']; + const known = ['trekplan', 'trekresearch', 'trekrevise']; for (const k of Object.keys(cfg)) { assert.ok(known.includes(k), `Unknown top-level scope in settings.json: ${k}`); }