ktg-plugin-marketplace/plugins/voyage/commands/trekrevise.md
Kjell Tore Guttormsen 4fbc52bbb4 feat(voyage): add commands/trekrevise.md — 7th pipeline command + settings.json scope — v4.2 Step 6 [skip-docs]
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.
2026-05-09 15:09:01 +02:00

18 KiB

name description argument-hint model allowed-tools
trekrevise 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). --project <dir> [--from-file <path>] [--target {brief|plan|review|auto}] [--reason <text>] [--profile <name>] [--gates <mode>] opus 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 <!-- voyage:anchor ... --> 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 "<text>".

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:

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 <dir> valued Required. Path to the trekplan project folder containing the target artifact.
--from-file <path> 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 <text> valued Optional. Required when revision is non-additive (see Phase 4).
--profile <name> valued Optional. Model profile (economy, balanced, premium, custom). Default: balanced.
--gates <mode> valued Optional. Autonomy mode (open, closed, adaptive). Default: adaptive.

Resolution:

  1. If --project is missing, print usage and stop:
    Error: --project <dir> is required.
    Usage: /trekrevise --project <dir> [--from-file <path>] [--target {brief|plan|review|auto}] [--reason <text>]
    
  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 <path> 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 <path> 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:

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:

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):

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:

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:

import { computeAnnotationDigest } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/annotation-digest.mjs';
const annotation_digest = computeAnnotationDigest(anchors);

Determine the new revision counter:

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 ## <required-section> 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 "<short explanation>" 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 = <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:

const new_body = stripAnchors(annotated_body);

Build the new frontmatter object by merging the existing fields with the revision audit fields:

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:

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:

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):

{"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 <name> where <name> 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. <!-- voyage:anchor id="ANN-NNNN" target="..." line="N" [snippet="..."] [intent="fix|change|question|block"] --> 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.