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.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-09 15:09:01 +02:00
commit 4fbc52bbb4
4 changed files with 517 additions and 1 deletions

View file

@ -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 |

View file

@ -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 <dir> [--from-file <path>] [--target {brief|plan|review|auto}] [--reason <text>] [--profile <name>] [--gates <mode>]"
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 `<!-- 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:
```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 <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:
```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 `## <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:
```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 <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.

View file

@ -27,5 +27,12 @@
"enabled": true,
"statsFile": "trekresearch-stats.jsonl"
}
},
"trekrevise": {
"defaultMode": "default",
"tracking": {
"enabled": true,
"statsFile": "trekrevise-stats.jsonl"
}
}
}

View file

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