ktg-plugin-marketplace/plugins/voyage/docs/HANDOVER-CONTRACTS.md

546 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Handover Contracts (voyage-suite local pipeline)
This document is the single source of truth for the file formats that pass between the four commands of the `trekplan` pipeline. When you fork the plugin or extend a stage, the contracts below tell you what every producer must write and what every consumer is allowed to assume.
For each handover, the same headings appear in the same order: **Producer**, **Consumer**, **Path conventions**, **Frontmatter schema**, **Body invariants**, **Validation strategy**, **Versioning**, **Failure modes**.
## Versioning policy
Each artifact carries an explicit version field. Schema bumps are coordinated:
| Artifact | Field | Current |
|---|---|---|
| `brief.md` | `brief_version` (frontmatter) | `2.0` |
| `research/*.md` | (implicit; tracked via `type: trekresearch-brief`) | unversioned |
| `plan.md` | `plan_version` (frontmatter) | `1.7` |
| `progress.json` | `schema_version` (top-level) | `"1"` |
| `review.md` | `review_version` (frontmatter) | `1.0` |
| `.session-state.local.json` | `schema_version` (top-level) | `1` (number) |
| `brief.md` / `plan.md` / `review.md` (annotated) | `revision` (frontmatter) | `0` (implicit), `1+` after `/trekrevise` |
## Breaking-change protocol
1. Bump the artifact's version field.
2. Update the matching validator in `lib/validators/`.
3. Add a fixture under `tests/fixtures/` covering both old and new shapes.
4. Document the change in `MIGRATION.md` with at least an N-1 compatibility window in the validator (read both shapes; warn on old, fail only after one minor version of warning).
5. Bump the plugin version in `package.json` and `.claude-plugin/plugin.json`.
## Validator → handover map
| Handover | Validator |
|---|---|
| 1. brief → research | `lib/validators/brief-validator.mjs` |
| 2. research → plan | `lib/validators/research-validator.mjs` |
| 3. architecture → plan | `lib/validators/architecture-discovery.mjs` |
| 4. plan → execute | `lib/validators/plan-validator.mjs` |
| 5. progress.json (resume) | `lib/validators/progress-validator.mjs` |
| 6. review → plan | `lib/validators/review-validator.mjs` |
| 7. session-state (multi-session resume) | `lib/validators/session-state-validator.mjs` |
| 8. annotation → revision | `lib/parsers/anchor-parser.mjs` + `lib/parsers/annotation-digest.mjs` (parsing) and the existing brief / plan / review validators (forward-compat — additive fields tolerated) |
Every validator exposes a CLI: `node lib/validators/<name>.mjs --json <path>` returns `{valid, errors[], warnings[], parsed}`. Errors and warnings have stable `code` fields for downstream tooling.
---
## Handover 1 — `brief.md` → research/
**Producer:** `/trekbrief` Phase 4g (after `brief-reviewer` stop-gate passes or iteration cap is hit).
**Consumer:** `/trekresearch` Phase 1 (mode parse + brief validation).
**Path conventions:**
- Project-dir mode (recommended): `.claude/projects/{YYYY-MM-DD}-{slug}/brief.md`.
- Legacy / loose mode: any path passed via `--brief <file>`.
**Frontmatter schema:**
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
| `type` | string | yes | `trekbrief` | Hard-coded discriminator |
| `brief_version` | string | yes | `"2.0"` (current) | Bump on schema change |
| `created` | date | yes | YYYY-MM-DD | |
| `task` | string | yes | one-line description | |
| `slug` | string | yes | URL-safe slug | Used in project_dir |
| `project_dir` | string | yes | `.claude/projects/{date}-{slug}/` | |
| `research_topics` | number | yes | ≥ 0 | |
| `research_status` | string | yes | `pending \| in_progress \| complete \| skipped` | State machine — see below |
| `auto_research` | bool | optional | `true \| false` | |
| `interview_turns` | number | optional | ≥ 0 | |
| `source` | string | optional | `interview \| manual` | |
| `brief_quality` | string | optional | `complete \| partial` | Set when iteration cap is hit |
**Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time):
- `## Intent`
- `## Goal`
- `## Success Criteria`
Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preferences`, `## Non-Functional Requirements`, `## Research Plan`.
**Validation strategy:**
| Layer | When | What |
|---|---|---|
| Frontmatter parse | every read | YAML subset; reject nested dicts |
| Required fields | every read | All `BRIEF_REQUIRED_FRONTMATTER` present |
| Type discriminator | every read | `type === "trekbrief"` |
| Status enum | every read | `research_status ∈ allowed values` |
| **State machine** | every read | `research_topics > 0 && research_status === "skipped"` requires `brief_quality === "partial"` |
| Body sections | strict only | All `BRIEF_BODY_SECTIONS` present |
**State machine** detail: a brief that says it has research topics but skipped them must explicitly admit it (via `brief_quality: partial`). This is the most common failure mode the validator catches.
**Versioning:** current is `2.0`. There are no live `1.x` briefs; remove legacy paths in next major.
**Failure modes:**
- `BRIEF_NOT_FOUND` → consumer halts with a usage message
- `FM_MISSING` → file has no frontmatter; halt
- `BRIEF_WRONG_TYPE` → file is not a brief; halt
- `BRIEF_MISSING_FIELD` → strict halt; soft-mode warning
- `BRIEF_STATE_INCOHERENT` → strict halt; soft-mode warning (incoherence will haunt downstream agents)
- `BRIEF_MISSING_SECTION` → strict halt; soft-mode warning
---
## Handover 2 — research/*.md → plan
**Producer:** `/trekresearch` Phase 7 (synthesis + brief writer).
**Consumer:** `/trekplan` Phase 1 (project-dir auto-discovery) + `planning-orchestrator` (consumes findings as context).
**Path conventions:**
- Project-dir mode: `.claude/projects/{YYYY-MM-DD}-{slug}/research/{NN}-{topic-slug}.md` (sorted by filename).
- Legacy: `.claude/research/trekresearch-{date}-{slug}.md`.
**Frontmatter schema:**
| Field | Type | Required | Allowed values |
|---|---|---|---|
| `type` | string | yes | `trekresearch-brief` |
| `created` | date | yes | YYYY-MM-DD |
| `question` | string | yes | the research question |
| `confidence` | number | optional | `[0.0, 1.0]` — strongly recommended |
| `dimensions` | number | optional | ≥ 1 |
| `mcp_servers_used` | list | optional | server names |
| `local_agents_used` | list | optional | agent names |
| `external_agents_used` | list | optional | agent names |
Missing `confidence` is a warning, not an error — but downstream planning has no signal to weight findings.
**Body invariants:** required sections (strict mode):
- `## Executive Summary`
- `## Dimensions`
Optional: `## Local Context`, `## External Knowledge`, `## Triangulation`, `## Sources`, `## Recommendations`.
**Validation strategy:** schema parse + body-section check. Per-file by `validateResearch`; whole-directory by `validateResearchDir`. Anchoring back to brief topics is currently best-effort, not enforced (planned for a future minor).
**Versioning:** unversioned — research briefs are write-once read-once; no migration concern. If schema changes, change `type` discriminator or add `research_brief_version`.
**Failure modes:** all same shape as brief (`RESEARCH_*` codes). Default soft mode in plan Phase 1 — research drift does not block planning, but warnings surface in the user-visible summary.
---
## Handover 3 — architecture/ → plan (EXTERNAL CONTRACT)
**This is the only handover where the producer is in a *different plugin*.** The `architecture/overview.md` (and optional `gaps.md`) are produced by an external opt-in architect plugin (no longer publicly distributed; the filesystem slot remains available for any compatible producer). When no producer is installed, this handover is absent — and that is fine.
**Producer:** external opt-in architect plugin (no longer publicly distributed).
**Consumer:** `/trekplan` Phase 1 (architecture-discovery) + `planning-orchestrator` Phase 7 (cross-reference architecture-note as priors during synthesis).
**Path conventions:**
- Canonical: `{project_dir}/architecture/overview.md`
- Optional: `{project_dir}/architecture/gaps.md`
- Tolerated alternatives (with warning): `architecture-overview.md`, `overview.markdown`, `README.md`
**Frontmatter schema:** **unenforced.** This is the external contract — `trekplan` does not validate the format. We sniff only the first H1 heading.
**Body invariants:** **unenforced.** We never read body content beyond the first heading.
**Validation strategy:** **drift-WARN, never drift-FAIL.**
| Detection | Result |
|---|---|
| File at canonical path | `found: true`, no warnings |
| File at known alternative path | `found: true`, warning `ARCH_NON_CANONICAL_OVERVIEW` |
| Loose `*.md` files in `architecture/` not in known set | warning `ARCH_LOOSE_FILES` |
| No `architecture/` dir | `found: false`, no warnings |
The validator (`lib/validators/architecture-discovery.mjs`) is intentionally minimal. It is unit-tested to assert it does NOT read body content beyond the first heading — guarding against scope creep into the producer's territory.
**Versioning:** the external producer owns its schema. We do not version this handover from our side.
**Failure modes:** none. Discovery always succeeds (returns `found: false` if absent). The handover is additive.
---
## Handover 4 — `plan.md` → execute
**Producer:** `planning-orchestrator` Phase 5 (plan synthesis) + Phase 5.5 (schema self-check via `plan-validator --strict`).
**Consumer:** `/trekexecute` Phase 2 (plan parsing) + `--validate` mode.
**Path conventions:**
- Project-dir: `{project_dir}/plan.md`
- Legacy: `.claude/plans/trekplan-{date}-{slug}.md`
**Frontmatter schema:**
| Field | Type | Required | Allowed |
|---|---|---|---|
| `plan_version` | string | yes | `"1.7"` (current) |
**Body invariants (strict, v1.7):**
1. Top-level structure:
- `## Implementation Plan` heading present
- One or more `### Step N: <title>` headings, numbered 1..N contiguously
- `### Step N: ` is the literal canonical form — colon + space
2. Forbidden narrative-drift heading forms (Opus 4.7 regression guard):
- `## Fase N` (Norwegian)
- `### Phase N`
- `### Stage N`
- `### Steg N` (Norwegian variant)
3. Per-step Manifest block — **required for every step**:
- Indented fenced YAML: ` ```yaml\n manifest:\n ...\n ``` `
- Required keys: `expected_paths` (list), `min_file_count` (number), `commit_message_pattern` (string compilable to RegExp), `bash_syntax_check` (list), `forbidden_paths` (list), `must_contain` (list of `{path, pattern}` dicts or empty list)
4. Step count == manifest count
**Validation strategy:**
The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrator) **must** run `plan-validator --strict` before handing the plan to plan-critic. `--validate` mode of `/trekexecute` runs the same check + `progress-validator`.
| Code | Meaning | Recovery |
|---|---|---|
| `PLAN_FORBIDDEN_HEADING` | Narrative drift detected | Rewrite using literal Phase 5 template |
| `PLAN_NO_STEPS` | No `### Step N:` headings | Plan is empty; restart |
| `PLAN_STEP_NUMBERING` | Steps skip a number | Renumber sequentially |
| `PLAN_MANIFEST_COUNT_MISMATCH` | Some step lost its manifest | Add missing manifest |
| `MANIFEST_MISSING` | Specific step has no manifest YAML | Add Manifest block |
| `MANIFEST_MISSING_KEY` | Manifest is missing a required key | Add the key |
| `MANIFEST_PATTERN_INVALID` | `commit_message_pattern` does not compile | Check escaping (`\\(` not `\(` in YAML double-quoted strings) |
| `PLAN_VERSION_MISMATCH` | Older `plan_version` | Warning only; planner should bump |
**Versioning:** v1.7 has been stable since v1.8.0 of the plugin (when literal-template + Phase 5.5 self-check were added to fix Opus 4.7 schema drift). v1.6 → v1.7 added the Manifest block (mandatory). Before bumping to v1.8, write the new validator branch + fixtures first.
**Failure modes:** strict mode is the default for both producer and consumer. There is no soft mode here — a malformed plan is a hard failure for execute.
---
## Handover 5 — `progress.json` (resume contract)
**Producer:** `/trekexecute` per-step (after Verify + Manifest audit + Checkpoint).
**Consumer:** `/trekexecute --resume` (re-entry) + `pre-compact-flush` hook (drift detection before context compaction).
**Path conventions:**
- Project-dir: `{project_dir}/progress.json`
- Legacy: `{plan-dir}/.trekexecute-progress-{slug}.json`
**Schema (top-level):**
| Field | Type | Required | Notes |
|---|---|---|---|
| `schema_version` | string | yes | `"1"` (current) |
| `plan` | string | yes | Path to the plan being executed |
| `plan_type` | string | optional | `plan \| session-spec` |
| `plan_version` | string | yes | Mirrors plan's frontmatter |
| `started_at` | ISO string | yes | |
| `updated_at` | ISO string | yes | Bumped on every write |
| `completed_at` | ISO string | optional | Set when status flips to completed |
| `mode` | string | yes | `execute \| dry-run \| validate` |
| `total_steps` | number | yes | |
| `current_step` | number | yes | 0..total_steps |
| `status` | string | yes | `pending \| in_progress \| completed \| failed \| partial` |
| `session_start_sha` | string | optional | git sha at execute start |
| `session_end_sha` | string | optional | git sha at execute end |
| `steps` | object | yes | Map of step number → step record |
**Per-step record:**
| Field | Type | Notes |
|---|---|---|
| `status` | `completed \| in_progress \| failed \| pending \| deferred \| skipped` | |
| `attempts` | number | 1..N |
| `error` | string \| null | |
| `completed_at` | ISO string \| null | |
| `commit` | string \| null | git sha after Checkpoint |
| `manifest_audit` | string | `pass \| fail \| pass-with-note \| n/a` |
| `note` | string | optional human-readable annotation |
**Validation strategy:** `progress-validator.mjs` runs at:
1. `/trekexecute --validate` (alongside plan-validator)
2. `/trekexecute --resume` entry (must pass `checkResumeReadiness`)
3. `pre-compact-flush` hook (drift check before compaction; never blocks)
**Drift detection:** the `pre-compact-flush` hook compares `progress.steps[N].commit` against `git log --oneline {session_start_sha}..HEAD`. If git reality has progressed past the recorded `current_step`, the hook updates progress.json atomically (`tmp + rename`, monotonic only) before allowing compaction. This guards against the documented P0 drift in `docs/trekexecute-v2-observations-from-config-audit-v4.md`.
**Versioning:** `schema_version: "1"` is current. Future bump (e.g. `"2"`) should add a backward-compat read path that downgrades unknown fields to warnings.
**Failure modes:**
- `PROGRESS_PARSE_ERROR` → JSON corruption; resume halts
- `PROGRESS_SCHEMA_MISMATCH` → unknown schema version; resume halts
- `PROGRESS_MISSING_FIELD` → required top-level field absent; resume halts
- `PROGRESS_STEP_RANGE``current_step` outside `[0, total_steps]`; resume halts
- `PROGRESS_ALREADY_DONE``status === completed`; nothing to resume
- `PROGRESS_STEP_COUNT_MISMATCH` → warning; not a blocker
---
## Handover 6 — `review.md` → plan
**Handover 6 closes the iteration loop.** Where Handovers 14 flow forward (brief → research → plan → execute) and Handover 5 makes execute resumable, Handover 6 routes review findings *back* into planning so a remediation plan can be produced with full traceability via `source_findings`.
**Producer:** `/trekreview` Phase 7 (write `review.md` after coordinator dedup + verdict).
**Consumer:** `/trekplan` Phase 1 when `--brief review.md` is supplied and the consumer detects `type: trekreview` in frontmatter. The plan command branches into a remediation-plan path: BLOCKER + MAJOR findings become plan goals, the produced `plan.md` carries a `source_findings: [<id>, ...]` frontmatter list as the audit trail back to the consumed findings. MINOR + SUGGESTION are skipped for v1.0 plan-input.
**Path conventions:**
- Project-dir mode (recommended): `{project_dir}/review.md` (one per review iteration; subsequent runs overwrite atomically).
- Multiple review iterations are allowed in the same project; each overwrites the canonical path. Audit trail lives in git history.
**Frontmatter schema:**
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
| `type` | string | yes | `trekreview` | Hard-coded discriminator |
| `review_version` | string | yes | `"1.0"` (current) | Bump on schema change |
| `task` | string | yes | one-line description | Mirrors brief task |
| `slug` | string | yes | URL-safe slug | Used in project_dir |
| `project_dir` | string | yes | `.claude/projects/{date}-{slug}/` | |
| `brief_path` | string | yes | path to consumed `brief.md` | Audit trail back to brief |
| `scope_sha_end` | string | yes | git sha of HEAD at review time | Defines "after" boundary |
| `reviewed_files_count` | number | yes | ≥ 0 | From triage gate Coverage |
| `findings` | list | yes | block-style YAML list of 40-char hex IDs | Flat array; full objects in body |
| `created` | date | optional | YYYY-MM-DD | |
| `scope_sha_start` | string | optional | git sha at review start | `null` if mtime fallback used |
| `verdict` | string | optional | `BLOCK \| WARN \| ALLOW` | Coordinator output |
`findings:` is a flat array of finding-IDs (40-char hex from `lib/parsers/finding-id.mjs`). The full finding objects (severity, location, message, evidence, fix) live in the body as `### <id>` subsections under per-severity `## Findings (...)` headings — same pattern as brief-reviewer to avoid frontmatter-parser fragility on lists of dicts.
**Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time):
- `## Executive Summary`
- `## Coverage`
- `## Remediation Summary`
Optional but standard sections: `## Findings (BLOCKER)`, `## Findings (MAJOR)`, `## Findings (MINOR)`, `## Findings (SUGGESTION)`. The `## Coverage` section enumerates which files were deep-reviewed, summary-only, or skipped (with reason) — this is how the triage gate stays honest and avoids Copilot-style silent skips.
**Validation strategy:**
| Layer | When | What |
|---|---|---|
| Frontmatter parse | every read | YAML subset; reject nested dicts |
| Required fields | every read | All `REVIEW_REQUIRED_FRONTMATTER` present |
| Type discriminator | every read | `type === "trekreview"` |
| Findings shape | every read | Array of strings, each matching `^[0-9a-f]{40}$` |
| Body sections | strict only | `Executive Summary`, `Coverage`, `Remediation Summary` |
| Version format | every read | `review_version` matches `N.M`; warning otherwise |
The validator (`lib/validators/review-validator.mjs`) exposes the same CLI as the others: `node lib/validators/review-validator.mjs --json <review.md>`. Strict mode is the default; `--soft` downgrades section-missing errors to warnings. `/trekreview` Phase 8 runs `--strict`. `/trekplan` Phase 1 (when consuming `--brief review.md`) runs `--soft` so a partially-valid review can still seed a plan.
**Versioning:** current is `1.0`. There are no live `0.x` reviews. Future schema changes follow the breaking-change protocol above.
**Failure modes:**
- `REVIEW_NOT_FOUND` → consumer halts with usage message
- `REVIEW_READ_ERROR` → I/O failure; halt
- `FM_MISSING` → file has no frontmatter; halt
- `REVIEW_WRONG_TYPE``type !== "trekreview"`; halt
- `REVIEW_MISSING_FIELD` → strict halt; soft-mode warning
- `REVIEW_BAD_FINDINGS_TYPE``findings` is not an array; halt (covers the YAML flow-style trap)
- `REVIEW_BAD_FINDING_ID` → an ID is not 40-char hex; halt
- `REVIEW_MISSING_SECTION` → strict halt; soft-mode warning
- `REVIEW_VERSION_FORMAT` → warning only; review_version not in `N.M` form
---
## Handover 7 — `.session-state.local.json`
**Handover 7 enables zero-friction multi-session resumption.** Where Handover 5 (`progress.json`) makes a single execute run resumable after a crash inside that session, Handover 7 makes a *multi-session* plan resumable across fresh Claude Code chats. The state file is the contract; any session-end mechanism may write it; `/trekcontinue` only reads.
**Producer:**
- `/trekexecute` Phase 8 (canonical convergence — every completed/failed/stopped/partial run that reaches the final report)
- `/trekexecute` Phase 2.55 (Check 1 — dirty-tree pre-flight stop)
- `/trekexecute` Phase 4 (entry-condition stop)
- `/trekendsession` (informal multi-session helper — Step 9 of v3.3.0)
- *Future:* `graceful-handoff` v2.2 may dual-write here as part of its session-rescue artifact (additive — extra fields tolerated, see Body invariants).
- `hooks/scripts/pre-compact-flush.mjs` *refreshes* `updated_at` on existing state files (status `in_progress` or `partial` only). Never creates the file; never changes status or owned fields.
**Consumer:** `/trekcontinue` (read-only). Reads the file, validates it, narrates a 3-line summary, then begins executing the next session by reading `next_session_brief_path`.
**Path conventions:**
- Per-project: `.claude/projects/{YYYY-MM-DD}-{slug}/.session-state.local.json` — one file per project directory.
- Gitignored at the plugin level via `*.local.json` (added in v3.3.0). State files MUST NOT be committed — they may contain absolute project paths and label strings that vary per-machine.
- For `--session N` parallel multi-session runs the parent's Phase 8 aggregate write IS the canonical state. Child session writes (inside their worktrees) are ephemeral; the worktree is cleaned up after merge so child state is intentionally discarded.
**Frontmatter schema:** N/A — file is JSON, not Markdown. Top-level keys:
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
| `schema_version` | number | yes | `1` (current) | Bump on breaking changes only |
| `project` | string | yes | absolute or repo-relative path | Project directory containing brief/plan |
| `next_session_brief_path` | string | yes | path to a brief or session-spec | Validator soft-checks file existence (warning, not error) |
| `next_session_label` | string | yes | human-readable label | e.g. "Session 2b" or "Continue" |
| `status` | string | yes | `in_progress \| partial \| failed \| stopped \| completed` | Mirrors progress.json status. `completed` triggers SESSION_STATE_NOT_RESUMABLE warning (valid:true) |
| `updated_at` | string | yes | ISO-8601 timestamp | Refreshed by pre-compact-flush on resumable statuses |
**Body invariants:** N/A (JSON).
**Forward-compat — drift-WARN principle:** Unknown top-level keys are **silently tolerated**. The validator does not warn on extras. This is a load-bearing decision: it lets future writers (graceful-handoff v2.2, custom plugin extensions) add metadata fields without breaking `/trekcontinue`. Mirrors Handover 3's discovery-only, drift-WARN posture.
**Validation strategy:**
| Layer | When | What |
|---|---|---|
| JSON parse | every read | `JSON.parse``SESSION_STATE_PARSE_ERROR` on failure |
| Required fields | every read | All six top-level keys present → `SESSION_STATE_MISSING_FIELD` on absence |
| Schema version | every read | Numeric `1``SESSION_STATE_SCHEMA_MISMATCH` otherwise |
| Status enum | every read | Must be one of the five values → `SESSION_STATE_INVALID_STATUS` otherwise |
| Resumability | every read | `completed` emits `SESSION_STATE_NOT_RESUMABLE` warning but valid:true |
| Path shape | every read | `next_session_brief_path` must be non-empty string → `SESSION_STATE_INVALID_PATH` otherwise |
| Timestamp shape | every read | `updated_at` parses via `Date.parse``SESSION_STATE_INVALID_TIMESTAMP` otherwise |
| Unknown keys | every read | Tolerated silently (drift-WARN forward-compat) |
The validator (`lib/validators/session-state-validator.mjs`) exposes the standard CLI: `node lib/validators/session-state-validator.mjs --json <path>`. Returns `{valid, errors[], warnings[], parsed}`. Exit code 0 on valid, 1 on invalid, 2 on usage error.
**Versioning:** Current is `1` (number). Schema is **additive only** — new optional fields land without bumping schema_version (forward-compat tolerates them). A breaking change (renaming a field, narrowing the status enum) requires bumping schema_version to `2`, adding migration support in the validator, and following the breaking-change protocol above.
**Failure modes:**
- `SESSION_STATE_NOT_FOUND``/trekcontinue` exits with cold-start message ("no active multi-session project here; start with `/trekbrief` or `/trekplan`")
- `SESSION_STATE_PARSE_ERROR` → halt with structured error; user fixes JSON
- `SESSION_STATE_MISSING_FIELD` → halt; suggests running validator directly
- `SESSION_STATE_SCHEMA_MISMATCH` → halt; future `1``2` migration path will warn instead
- `SESSION_STATE_INVALID_STATUS` → halt; protects against typo'd writers
- `SESSION_STATE_NOT_RESUMABLE` → warning; `/trekcontinue` exits cleanly with "no further sessions to resume; project complete"
- Validator failures during writer Phase 8 emit a stderr warning but DO NOT block the session-end report. `progress.json` remains the authoritative record of what was attempted.
### § Lifecycle
The state file follows a producer/consumer separation that keeps responsibilities narrow and the contract observable.
**Producer/consumer arbeidsdeling:**
| Role | Owners | Phase / location |
|---|---|---|
| Producer (writes the state file) | `/trekexecute` | Phase 8 (canonical), Phase 2.55 (dirty-tree pre-flight stop), Phase 4 (entry-condition stop) |
| Producer (informal multi-session helper) | `/trekendsession` | Phase 3 — writes the same schema for ad-hoc handovers that don't run through executor |
| Refresher (touch only) | `hooks/scripts/pre-compact-flush.mjs` | Updates `updated_at` only; never creates the file; never changes `status` or any owned field; only acts when `status` is `in_progress` or `partial` |
| Consumer | `/trekcontinue` | Phase 2 — reads, validates, narrates a 3-line summary, then begins executing the next session |
**Stale-file principle (SC-5):** When `status === 'completed'`, the state file and its sibling `NEXT-SESSION-PROMPT.local.md` represent finished work and SHOULD be removed. Removal is **operator-invoked** via `/trekcontinue --cleanup --confirm <project-dir>`; the plugin does NOT auto-cleanup. Stale state is actively harmful — it can mislead a fresh `/trekcontinue` into resuming a project that's already shipped. The `--cleanup` gate refuses to act unless `validateSessionState({...}).valid === true && parsed.status === 'completed'`. There is no force flag.
**Frontmatter contract for `NEXT-SESSION-PROMPT.local.md`:** Producers MUST write a YAML frontmatter block on the prompt file with at minimum:
- `produced_by:` — string identifying the producer (e.g. `trekplan-A4-session`, `trekexecute-phase-8`, `trekendsession`)
- `produced_at:` — ISO-8601 timestamp of when the file was written
The `next-session-prompt-validator` (`lib/validators/next-session-prompt-validator.mjs`) cross-checks `produced_at` against the sibling state file's `updated_at` and emits a `NEXT_SESSION_PROMPT_INCONSISTENT` error when the prompt is older than the state — that means the prompt has not been refreshed for the current session and is stale. Files **without** any frontmatter are tolerated (warning, not error) for backwards compatibility with v3.3.x and earlier hand-rolled prompt files; this is consistent with Handover 3's drift-WARN posture.
**Idempotency:** `--cleanup --confirm` is safe to re-run. If only one of the two files (state file, prompt file) was previously deleted, the second run reports the partial state ("state file: not found, prompt file: removed") but does not auto-recover or re-create. There is no rollback. Operators choosing to re-create a project after `--cleanup` should re-run `/trekbrief` from scratch.
---
## Handover 8 — annotation → revision
**Handover 8 closes the operator-feedback loop.** Where Handovers 14 flow forward (brief → research → plan → execute), Handover 5 makes execute resumable, Handover 6 routes review findings back into planning, and Handover 7 makes multi-session work survivable across fresh chats, **Handover 8 lets a human operator annotate an artifact (brief, plan, or review) inside the playground and feed those annotations back into a revision cycle without losing the original artifact's byte-for-byte content**. The pipeline becomes round-trippable: a single artifact can be annotated, revised, and re-rendered repeatedly, each revision recorded with a deterministic digest in frontmatter.
**Producer:**
- The operator (manual step) — open the artifact in `playground/voyage-playground.html`, drag-select or hover-to-anchor, fill comment + intent in the modal, click "Eksporter batch" to copy the `/trekrevise` invocation to clipboard.
- `/trekrevise --project <dir>` (Phase 3 — apply) — consumes the pasted batch, writes anchor comments back into the target artifact, increments `revision:`, appends entries to `source_annotations:`, and computes a fresh `annotation_digest`.
**Consumer:**
- Subsequent `/trekplan --project <dir>` if the revised brief or plan needs further re-planning.
- Subsequent `/trekexecute --project <dir>` if the revised plan is ready for execution.
- All existing validators (brief, plan, review) — they tolerate the additive frontmatter fields without version bumps.
**Path conventions:**
- The annotated artifact is the same canonical file in `{project_dir}/` (`brief.md`, `plan.md`, `review.md`). Revisions are *in-place* — no `plan-revN.md` shadow files. Audit trail lives in git commits + the `revision:` counter + `source_annotations:` list inside frontmatter.
- The playground itself lives at `playground/voyage-playground.html` (single self-contained file with vendored `markdown-it` + `highlight.js` under `playground/lib/`).
**Frontmatter schema (additive — applies to brief.md, plan.md, review.md):**
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
| `revision` | number | optional | `0`, `1`, `2`, … | Absent = `0` (forward-compat). Incremented by `/trekrevise` on each apply |
| `source_annotations` | list | optional | block-style YAML list of dicts | Absent = empty. Audit trail of every annotation that has been folded into this artifact |
| `annotation_digest` | string | optional | first 16 hex chars of canonical SHA-256 over the sorted `source_annotations` array | Absent = no annotations applied yet. Deterministic — re-applying the same set yields the same digest |
| `revision_reason` | string | optional | free-form text | **Required only** when the revision is non-additive (e.g. removed scope, replaced an SC). Operators are encouraged to fill it for any revision |
Each entry in `source_annotations` is a YAML dict:
```yaml
source_annotations:
- id: ANN-0001
target_artifact: plan.md
target_anchor: step-3
intent: change # change | add | remove | clarify | risk
comment: "consider rolling back if cache_creation jumps >10%"
line: 412
```
**Anchor format (block-level HTML comments inside the body):**
```html
<!-- voyage:anchor id="ANN-0001" target="plan.md#step-3" line="412" -->
```
Anchors are placed **only at block boundaries** — not in list items, not mid-paragraph, not at line-start collision points where a markdown parser might fold them into surrounding content. The playground enforces this discipline via `validateAnchorPlacement()` in `lib/parsers/anchor-parser.mjs`. Anchors survive markdown rendering as comments (no visible artifact) and round-trip through `parseAnchors → addAnchors → stripAnchors` byte-identically (SC2 contract).
**Body invariants:**
- The artifact's existing required sections remain untouched. `/trekrevise` writes anchor comments only at block boundaries declared by the operator's annotations.
- After each apply: byte-identical content outside anchor blocks. Stripping all anchors with `stripAnchors()` MUST yield the artifact's original body modulo the operator's deliberate prose edits.
**Validation strategy:**
| Layer | When | What |
|---|---|---|
| `revision` shape | every read | Number (or absent → treat as `0`). Negative or non-integer rejected by validator |
| `source_annotations` shape | every read | Array of dicts; each dict must include `id`, `target_artifact`, `target_anchor`, `intent`. Other fields tolerated (drift-WARN) |
| `annotation_digest` shape | every read | 16-char lowercase hex; presence requires `source_annotations` non-empty |
| Anchor placement | `/trekrevise` Phase 2 (validate) | `validateAnchorPlacement()` rejects in-list / mid-paragraph / line-start anchors before write |
| Round-trip integrity | `/trekrevise` Phase 4 (post-write) | `stripAnchors()` of the just-written file MUST equal the pre-write body |
The parsers (`lib/parsers/anchor-parser.mjs`, `lib/parsers/annotation-digest.mjs`) are pure functions — no I/O, fully unit-tested. The existing brief / plan / review validators in `lib/validators/` already tolerate the new optional fields (forward-compat — see Step 12 manifest pins).
**Forward-compat — additive principle:** Artifacts without any of the four new fields validate as `revision: 0` with empty annotations. This means every brief / plan / review written before v4.2 remains valid without migration. Producers (operators using `/trekrevise`) opt into the schema by writing the fields; consumers tolerate their presence or absence equally.
**Idempotence:** Re-applying the same annotation batch on the same artifact (same anchors, same intents, same comments) yields the same `annotation_digest` and no body diff outside anchor refresh. This is enforced by `computeAnnotationDigest()` canonicalizing the sorted `source_annotations` array before hashing — operators can replay a batch safely, and the test suite pins this with `tests/parsers/annotation-digest.test.mjs`.
**Versioning:** No `*_version` bump for v4.2 — the four new fields are additive. A future schema break (e.g. removing `target_anchor` in favor of structured pointer) would bump `brief_version` / `plan_version` / `review_version` per the breaking-change protocol.
**Failure modes:**
- `ANNOTATION_PLACEMENT_INVALID``/trekrevise` Phase 2 halts; operator re-anchors via playground
- `ANNOTATION_DIGEST_DRIFT` → digest computed at apply-time differs from operator's expected digest in the pasted batch; halt with mismatch report (suggests the batch was hand-edited after export)
- `ANNOTATION_ROUNDTRIP_FAIL` → post-write `stripAnchors()` does not yield the original body; rollback restores the byte-identical pre-write file from `*.local.bak`
- Parser failures from `parseAnchors()` produce `{valid: false, errors: [...]}` and `/trekrevise` halts before any write. The atomic-write pattern (tmp-file + rename) guarantees the canonical file is never partially updated.
### § Lifecycle
The annotation cycle has three stages — no persistent state file (unlike Handover 7), only the artifact frontmatter and git history record what happened:
| Stage | Owner | Action |
|---|---|---|
| Annotate | Operator + playground UI | Open artifact in `playground/voyage-playground.html`, anchor + comment, click "Eksporter batch" → clipboard contains a `/trekrevise --project <dir> --apply '{...JSON...}'` command |
| Apply | `/trekrevise` | Phase 1 parse the pasted batch, Phase 2 validate placement, Phase 3 atomically write anchors + frontmatter (`revision: N+1`, append to `source_annotations`, recompute `annotation_digest`), Phase 4 round-trip integrity check, Phase 5 commit |
| Re-render | Playground (next open) | The freshly-revised artifact loads with anchors visible as inline comment markers; operator can iterate or call `/trekplan` / `/trekexecute` on the revised file |
**Single-iteration MVP (v4.2 scope):** Each operator annotation batch produces one `revision:` increment. Multi-iteration loops (e.g. revise → re-review → revise again without operator intervention) are deferred indefinitely — the brief's SC4 wording is single-revision, and operator validation that multi-iteration is desired has not been collected (see plan Alternatives table). The single-iteration MVP keeps the audit trail unambiguous: one `revision:` bump per `/trekrevise` invocation.
**Stale-anchor principle:** Unlike `.session-state.local.json`, anchors are durable and intentionally retained — they document *why* a revision happened. There is no `--cleanup` for anchors. Removing them is a manual operator decision, executed via the playground "Strip all anchors" affordance or hand-editing the source. `/trekrevise` does not remove anchors automatically.
---
## Stability summary
| Handover | Validation strength | Owner | Risk |
|---|---|---|---|
| 1. brief → research | strict at write, soft at read | this plugin | low |
| 2. research → plan | soft, drift-warn | this plugin | low |
| 3. architecture → plan | discovery-only, drift-WARN | **external** (opt-in architect plugin, not bundled) | low — by design we tolerate drift |
| 4. plan → execute | **strict, both ends** | this plugin | medium — Opus 4.7 narrative drift requires constant vigilance |
| 5. progress.json | shape + resume readiness | this plugin | medium — drift during compaction handled by pre-compact-flush hook (CC v2.1.105+) |
| 6. review → plan | strict at write, soft at read | this plugin | low — additive feedback loop; consumer falls back gracefully when source_findings is absent |
| 7. session-state (multi-session resume) | required-fields + status enum + drift-WARN extras | this plugin | low — readers tolerate unknown keys; writers are owned by trekexecute Phase 8 + helper command |
| 8. annotation → revision | parser-strict at write (placement + round-trip), validator-soft at read (additive fields) | this plugin | low — additive frontmatter; existing artifacts validate as `revision: 0` without migration |
When extending the plugin or adding a new pipeline stage, follow the same pattern: produce an artifact with a versioned frontmatter (or `schema_version` for JSON), write a validator under `lib/validators/`, add fixtures under `tests/fixtures/`, and add an entry to this document.