# Handover Contracts (voyage-suite local pipeline) This document is the single source of truth for the file formats that pass between the commands of the `trekplan` pipeline. There are seven handovers. 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.2` | | `research/*.md` | (implicit; tracked via `type: trekresearch-brief`) | unversioned | | `plan.md` | `plan_version` (plan header line) | `1.8` (1.7 still valid) | | `progress.json` | `schema_version` (top-level) | `"1"` | | `review.md` | `review_version` (frontmatter) | `1.1` | | `.session-state.local.json` | `schema_version` (top-level) | `1` (number) | ## 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` | Every validator exposes a CLI: `node lib/validators/.mjs --json ` returns `{valid, errors[], warnings[], parsed}`. Errors and warnings have stable `code` fields for downstream tooling. --- ## Handover 1 — `brief.md` → research/ (PUBLIC CONTRACT) **Stability tier: PUBLIC CONTRACT.** Handover 1 is the *only* public integration boundary of the pipeline: `brief.md` is what an upstream producer hands to Voyage, and Voyage consumes it without any knowledge of who produced it. This asymmetry is a hard invariant (see `CLAUDE.md` §Trinity context) — the interactive `/trekbrief` interview is just one producer; a `manual` brief, or an external per-app / per-portfolio producer, is equally valid as long as the artifact conforms to the schema below. No producer is privileged. The consequence: changing this schema — renaming or removing a field, narrowing an enum, or promoting an optional field to required — is a **breaking change for every downstream consumer** and MUST follow the [breaking-change protocol](#breaking-change-protocol) (version bump + N-1 compatibility window). Additive *optional* fields are non-breaking by design: the validator tolerates unknown frontmatter keys silently (forward-compat), so a newer brief still validates against an older consumer. The v5.5.0 contract formalization established `brief_version` **2.1** as the public-contract baseline and, in the same coordinated release, **evolved it to `2.2`** under the breaking-change protocol — adding the required `framing` field + `## TL;DR` section gated at ≥ 2.2 (existing 2.0/2.1 briefs stay valid). See the Versioning paragraph below for the 2.2 details. **Trinity producer context (informational).** In the author's private three-tier design, Voyage is **Tier 1** (per-task). Its upstream producers are **Tier 2 `app-creator`** (per-app — "what does the app need, what's the next brief?") and **Tier 3 `app-factory`** (per-portfolio — "which app needs me now?"), both pre-implementation and bound for Forgejo when ready. None is privileged: any schema-conforming producer is equally valid, and Voyage stays unaware of Tier 2/3 — this Handover is the only coupling. **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 `. **Frontmatter schema:** | Field | Type | Required | Allowed values | Notes | |---|---|---|---|---| | `type` | string | yes | `trekbrief` | Hard-coded discriminator | | `brief_version` | string | yes | `"2.2"` (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 | | `phase_signals` | list | optional (v5.1+) | list of `{phase, effort?, model?}` entries | Per-phase effort + model commitment from Phase 3.5. Mutually exclusive with `phase_signals_partial`. | | `phase_signals_partial` | bool | optional (v5.1+) | `true` | Force-stop record from Phase 3.5. Mutually exclusive with `phase_signals`. | | `framing` | string | **required at ≥ 2.2** (v5.5) | `preserve \| refine \| replace \| new-direction` | How this brief relates to prior operator intent. Enum-checked on any version when present; missing → `BRIEF_MISSING_FRAMING` at `brief_version ≥ 2.2`. Layer 1 of the framing-alignment defense. | | `intent_approved_hash` | string | optional in the schema; **required by `/trekplan`** (veikart steg 1) | `"sha256:<64 hex>"` | Hash over the normalized `## Intent` + `## Goal`. Written only by `lib/validators/intent-approval.mjs --stamp`, which `/trekbrief` runs on the operator's explicit "Approve" (Phase 4h, or `--approve `). | | `intent_approved_at` | string | optional | ISO-8601 | When the stamp was written. Informational; the gate reads the hash. | | `ceremony` | string | optional (K1) | `small \| bounded \| architectural` | The brief's size class: sets `/trekbrief`'s question budget and review cap, and what downstream phases scale to. **Absent (or unreadable) = `architectural`**, the heaviest, so another producer's brief is never treated lighter by accident. Enum-checked when present (`BRIEF_INVALID_CEREMONY`). Read it through `briefCeremony(fm)` in `brief-validator.mjs`. | | `artifact_store` | string | optional | `git \| local` | Whether the brief and plan are committed to the target repo (`git`) or kept out of git through `.git/info/exclude` (`local`). The approval log is committed either way. **Absent = `local`**: nothing is published without the operator's yes. Enum-checked when present (`BRIEF_INVALID_ARTIFACT_STORE`). | **Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time): - `## TL;DR` — **required at `brief_version ≥ 2.2`** (v5.5); ≤ 5 content lines (soft cap → `BRIEF_TLDR_TOO_LONG` warning). Layer 3 of the framing-alignment defense. - `## 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"` | | **v5.1 sequencing gate** | every read | `brief_version ≥ 2.1` requires `phase_signals` (list) OR `phase_signals_partial: true` — error `BRIEF_V51_MISSING_SIGNALS` on miss. Validator-only enforcement; commands surface, don't re-enforce. | | **v5.5 framing gate** | every read | `framing` enum-checked on any version when present (`BRIEF_INVALID_FRAMING`); at `brief_version ≥ 2.2` a missing `framing` → `BRIEF_MISSING_FRAMING` and a missing `## TL;DR` → `BRIEF_MISSING_SECTION` (strict) / warning (soft). `trekreview` briefs are exempt. | | Body sections | strict only | All `BRIEF_BODY_SECTIONS` present (`## TL;DR` added at ≥ 2.2) | | Runnable criteria | every read | A `## Success Criteria` bullet whose first backticked span is a command the review's criteria runner never runs (the product's own binary, `jq`, `grep`, a pipe) → warning `BRIEF_SC_NOT_RUNNABLE`, never an error: a criterion a human judges stays allowed. Prose-only criteria are not flagged. `trekreview` briefs are exempt. No schema change. | **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:** the `2.1` schema axis (v5.1 — adds optional `phase_signals` + `phase_signals_partial`; current is `2.2`, see the next paragraph). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.5.0 contract formalization **established `2.1` as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change under the protocol above — explicitly *not* part of the v5.5.0 formalization. **v5.5 → `2.2` (framing enforcement).** `2.2` adds two **required-at-2.2** elements — the `framing` enum field and the `## TL;DR` body section — gated identically to the v2.1 mechanism: the new requirements fire only on `brief_version ≥ 2.2`, so every existing `2.0` / `2.1` brief still validates (forward- and backward-compatible). Because adding required elements is a breaking change for any producer that declares `2.2`, this is a controlled version bump under the breaking-change protocol: downstream producers (e.g. a Tier-2 per-app producer) that emit `brief_version: "2.2"` MUST also emit `framing` + `## TL;DR`. The framing enum is additionally enforced on *any* version when the field is present (`BRIEF_INVALID_FRAMING`). The three framing-alignment layers (framing field, `brief-reviewer` memory-alignment dimension, obligatory TL;DR) implement the `CLAUDE.md` cross-cutting invariant. Note: the *plugin* version bump + CHANGELOG entry for this schema change land at the coordinated release (matrix §S10), separate from this schema axis. **Pre-2.2 briefs receive zero framing enforcement (producer-elective defense).** The framing-alignment defense is gated at `brief_version ≥ 2.2`, so a brief declaring `2.0` or `2.1` gets **none** of it: `framing` is optional and a missing value is not an error, the `## TL;DR` section is not required, and the `brief-reviewer` memory-alignment dimension scores N/A. The backward-compatibility this buys is deliberate, but it also means the framing defense is **producer-elective**: any producer can sidestep the entire defense — deliberately or by shipping a legacy template — simply by declaring `brief_version: "2.1"`, and the pipeline raises no signal by default. This is a documented property of the contract, not a bug. Two remedies close it for a given consumer: (1) require upstream producers to emit `brief_version: "2.2"` (which forces `framing` + `## TL;DR`); or (2) pass the opt-in **`--min-brief-version `** flag to `/trekplan` or `/trekresearch` (forwarded to the validator as `--min-version`), which raises a `BRIEF_VERSION_BELOW_MINIMUM` **warning** — never a block — when a brief declares a version below the floor. The flag defaults to off, so the N-1 compatibility window for `2.0`/`2.1` briefs is preserved unless a consumer explicitly opts into the stricter floor. **Intent approval gate (veikart steg 1) — additive schema, consumer-side requirement.** The two `intent_approved_*` fields are additive and optional in the schema: `brief-validator.mjs` does not require them, `/trekresearch` does not read them, and no `brief_version` bump is involved. The requirement lives in `/trekplan` Phase 1 (`### Read the brief`), which runs `intent-approval.mjs --check` and **halts** on `BRIEF_INTENT_NOT_APPROVED` (no marker), `BRIEF_INTENT_APPROVAL_STALE` (`## Intent` or `## Goal` changed after approval) or `BRIEF_INTENT_APPROVAL_INVALID` (malformed marker, or a section missing, empty or present more than once; a `## ` line inside a code block or HTML comment is not a heading). It is version-independent, so a brief from ANY producer, of any `brief_version`, stops at `/trekplan` until approved. No producer has to emit anything new: the operator approves through `/trekbrief --approve `, a Voyage-side step open to every producer's brief, whether it is planned with `--project` (`/brief.md`) or `--brief `, so no producer is privileged. `trekreview` briefs are exempt. The exemption is read from the brief's own frontmatter, so relabelling a brief `type: trekreview` walks past the gate, the same trust class as self-stamping. **What the marker does not prove:** same user, same machine. Any session can run `--stamp`. The marker is a trace that the approval step ran against this exact text, not a signature (see the header of `lib/validators/intent-approval.mjs`). `--stamp` also emits `brief-approved` to `trekexecute-stats.jsonl` under `CLAUDE_PLUGIN_DATA`, falling back to the plugin data dir the yardstick reads, on both the manual and the auto path. **Approval log (K2) — consumer-side requirement, no schema change.** After the stamp, `/trekbrief` appends the approval to the target repo's log with `lib/util/approval-log.mjs append --kind intent --subject --sha256 ` and commits it with `lib/util/artifact-commit.mjs` (the log always; the brief too when `artifact_store: git`). The log (`.claude/voyage/approvals.jsonl`, read and written only by that CLI) is append-only and hash-chained: each record carries `seq`, `ts`, `kind` (`intent | plan | review | merge | release`), `subject`, the content's `sha256`, an optional `expires`, `prev` and its own `hash`. `/trekplan` Phase 1 runs `approval-log.mjs verify --kind intent --subject --sha256 ` after the marker check and halts on `broken` (a record edited, removed or reordered), `diverged` (the committed log is no longer a prefix of the file), `not-logged` or `mismatch`; exit 2 (could not run) halts too. A brief approved before the log existed is `not-logged`: the remedy is the same `/trekbrief --approve`, open to every producer's brief. Only an explicit "Approve" counts: `approval-log.mjs answer` refuses «høres bra ut», «gjør som du vil» and an empty answer. Trust limits are those of the marker (same user, same machine; a trace, not a signature); what the log adds is that each approval is a commit in `git log`, and that a committed approval cannot be rewritten without `diverged`. Adapted from bashebr/ai-native-sdlc@11fb8d7 (MIT), corrected so the approval is bound to content and cannot be written unseen. **Failure modes:** - `BRIEF_INTENT_NOT_APPROVED` / `BRIEF_INTENT_APPROVAL_STALE` / `BRIEF_INTENT_APPROVAL_INVALID` → `/trekplan` halts (intent-approval gate, above); remedy `/trekbrief --approve ` - approval log `broken` / `diverged` → `/trekplan` halts; restore `.claude/voyage/approvals.jsonl` from git. `not-logged` / `mismatch` → halts; remedy `/trekbrief --approve ` - `BRIEF_INVALID_CEREMONY` / `BRIEF_INVALID_ARTIFACT_STORE` → strict halt; the optional field is present with a value outside its enum - `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 - `BRIEF_V51_MISSING_SIGNALS` → strict halt (v5.1+ sequencing gate); soft-mode warning. Commands surface a friendly hint pointing back to `/trekbrief` (Phase 3.5). - `BRIEF_INVALID_PHASE_SIGNALS` → strict halt; phase_signals must be a list of `{phase, effort?, model?}` entries. - `BRIEF_INVALID_PHASE_SIGNAL_PHASE` → strict halt; phase ∉ `[research, plan, execute, review]`. - `BRIEF_INVALID_EFFORT` → strict halt; effort ∉ `[low, standard, high]`. - `BRIEF_INVALID_MODEL` → strict halt; model ∉ `BASE_ALLOWED_MODELS` (currently `[sonnet, opus, fable]`). - `BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE` → strict halt; cannot set both `phase_signals` and `phase_signals_partial: true`. - `BRIEF_SC_NOT_RUNNABLE` → warning in both modes; `/trekbrief` names each one and offers a script under `tests/` the criterion runs (`bash tests/