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}`);
}