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.
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:
- If
--projectis missing, print usage and stop:Error: --project <dir> is required. Usage: /trekrevise --project <dir> [--from-file <path>] [--target {brief|plan|review|auto}] [--reason <text>] - Trim trailing slash from
{dir}. Set:project_dir = {dir}brief_path = {dir}/brief.mdplan_path = {dir}/plan.mdreview_path = {dir}/review.md
- If
{dir}does not exist:Error: project directory missing: {dir} Run /trekbrief first. - Determine the annotated input source:
- If
--from-file <path>is set, read that file. If the file is missing or unreadable, stop withError: --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.
- If
- 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---\npair). If detected:
Stop. No partial revisions.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. - Resolve
target:- If
--targetisbrief,plan, orreview: use it explicitly. - If
--targetisauto(or missing): parse the annotated input's frontmatter viaparseDocument. Read thetype:field — expectbrief,plan, orreview. 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.
- If
- 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,reasonper 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.frontmatterexisting_body = source.parsed.bodyexisting_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
*_versionfield (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.bakdeleted.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):
--profileflag (source:flag)VOYAGE_PROFILEenv-var (source:env)balanceddefault (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
*_versionbumps require an explicit reason recorded in frontmatter for audit. - Backup hygiene. Pre-existing
.local.bakblocks 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, andrevision_reasonare additive frontmatter fields. Validators that predate v4.2 ignore them. Artifacts withoutrevision:are treated asrevision: 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.