recordVerdict appends each round's summary to a new optional verdict field `rounds` (carried from the previous verdict; newByClass null for a round counted in full) and stores `stop_rule`; extractVerdict shape-checks both. After a fired stop rule --record exits 1 with PLAN_GATE_STOP_RULE and writes nothing (--extra-round included). The bare gate stops with PLAN_GATE_STOP_RULE and the operator line in place of the open-blocker and open-major reasons; --json and --record output carry the decision. --accept-open after a fired stop rule is allowed with rounds left, covers open blockers and majors (`under: "stop-rule"`), refuses a counting security finding (PLAN_GATE_SECURITY_OPEN); the gate then warns. Known issue fixed: REVIEW_LOG_SECTIONS drops "Adversarial Pass 2 (v5.1.1 high-effort)"; the CHANGELOG known-issue line closes and Handover 4 names the heading list, the new fields and codes. Existing tests updated for the planned rule: fixtures that ran a later round after one with no fewer blocking findings now use a falling count (plan-gate.test round cap, plan-gate-open-major lastRound, diff-mode 5/18/27 resolve two earlier majors in round 2); the diff-mode CLI pipe case now expects PLAN_GATE_STOP_RULE (round 3: 2 blocking) after its clean round 2. Part 1, step 1.2 of the E plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
646 lines
78 KiB
Markdown
646 lines
78 KiB
Markdown
# 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.9` (1.8 and 1.7 still valid) |
|
||
| `progress.json` | `schema_version` (top-level) | `"1"` |
|
||
| `review.md` | `review_version` (frontmatter) | `1.2` |
|
||
| `.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 `CHANGELOG.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/<name>.mjs --json <path>` 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 <file>`.
|
||
|
||
**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 |
|
||
| `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 <project-dir \| brief-file>`). |
|
||
| `intent_approved_at` | string | optional | ISO-8601 | When the stamp was written. Informational; the gate reads the hash. |
|
||
| `brief_reviewed_hash` / `brief_reviewed_at` | string | optional, additive (no `brief_version` bump) | `"sha256:<64 hex>"` / ISO-8601 | Hash over the brief's body and the frontmatter the reviewer judges (everything but this stamp and the intent marker), written by `lib/validators/brief-review-stamp.mjs --stamp` when `/trekbrief`'s brief-reviewer gate passes. `/trekplan` Phase 4b skips its own brief review only while the hash is current; absent (any other producer) or stale (body or reviewed frontmatter such as `framing` changed) means it reviews. A trace, not a signature. |
|
||
| `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`). |
|
||
|
||
An older brief's `phase_signals` key is ignored.
|
||
|
||
**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.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), or whose first span is not a command while a later span is one (the runner reads only the first, voyage-lekerepo trial 5) → warning `BRIEF_SC_NOT_RUNNABLE`, never an error: a criterion a human judges stays allowed. Prose-only criteria, also those that only name a flag or a path, are not flagged. `trekreview` briefs are exempt. No schema change. Indented sub-bullets belong to the criterion above them (counted, never criteria of their own); a criterion with sub-points and no command → warning `BRIEF_SC_SUBPOINTS`. A brief whose Constraints or NFRs require bash 3.2 and a criterion that runs plain `bash` → warning `BRIEF_SC_BASH_NOT_PINNED` (use `/bin/bash`). |
|
||
|
||
**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.2`, see the next paragraph. The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently. 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).
|
||
|
||
**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 on `brief_version`: 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 <ver>`** 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 <project-dir | brief-file>`, a Voyage-side step open to every producer's brief, whether it is planned with `--project` (`<dir>/brief.md`) or `--brief <file>`, 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`).
|
||
|
||
**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 <brief> --sha256 <intent hash>` and commits it with `lib/util/artifact-commit.mjs` (the log always; the brief too when `artifact_store: git`). The commit carries the caller's trailer: every call passes `--trailer "Co-Authored-By: Claude {model} <noreply@anthropic.com>"` with `{model}` filled in, and the CLI refuses (exit 1, nothing committed) a missing trailer, a multi-line one, one that does not read `Co-Authored-By: <name> <<email>>` (an unfilled `{model}` included) and one holding `Claude-Session:`; a bad `--store` stays exit 2 and is checked first. 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 <brief> --sha256 <intent_approved_hash>` 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 <project-dir | brief-file>`
|
||
- approval log `broken` / `diverged` → `/trekplan` halts; restore `.claude/voyage/approvals.jsonl` from git. `not-logged` / `mismatch` → halts; remedy `/trekbrief --approve <project-dir | brief-file>`
|
||
- `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_SC_NOT_RUNNABLE` → warning in both modes; `/trekbrief` names each one and offers a script under `tests/` the criterion runs (`bash tests/<script>.sh`).
|
||
- `BRIEF_SC_SUBPOINTS` → warning in both modes; `/trekbrief` offers one script that checks all sub-points, or one criterion per sub-point.
|
||
- `BRIEF_SC_BASH_NOT_PINNED` → warning in both modes; `/trekbrief` offers `/bin/bash tests/<script>.sh` so a bash 3.2 requirement is measured under the system bash.
|
||
|
||
---
|
||
|
||
## Handover 2 — research/*.md → plan
|
||
|
||
**Producer:** `/trekresearch` Phase 7 (synthesis + brief writer).
|
||
|
||
**Consumer:** `/trekplan` Phase 1 (project-dir auto-discovery) + `/trekplan` Phase 3 and Phase 7 (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) + `/trekplan` 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.
|
||
|
||
**Not the design part.** A plan's own design (`## Design`, plan_version 1.8 or later, architectural plans only) is part of Handover 4, written by `/trekplan` itself. It does not read or write this external slot, and this slot stays drift-WARN.
|
||
|
||
---
|
||
|
||
## Handover 4 — `plan.md` → execute
|
||
|
||
**Producer:** `/trekplan` Phase 8 (plan synthesis, then the 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:**
|
||
|
||
`plan.md` has **no required frontmatter**. The only frontmatter block,
|
||
`source_findings` (optional), appears for `type: trekreview` plans — see
|
||
Handover 6. Brief-generated plans carry no frontmatter at all.
|
||
|
||
| Field | Type | Required | Location | Allowed |
|
||
|---|---|---|---|---|
|
||
| `plan_version` | string | yes | plan header line, not frontmatter | `"1.9"` (current), `"1.8"` (valid), `"1.7"` (valid) |
|
||
| `ceremony` | string | no | plan header line, after `plan_version` | `small` \| `bounded` \| `architectural`; absent = `architectural` |
|
||
|
||
`plan_version` is emitted in the plan's "Generated by" header metadata line
|
||
(`> Generated by … — `plan_version: 1.7``), parsed by `extractPlanVersion`
|
||
(`lib/parsers/plan-schema.mjs`) from either line-start or the backtick-wrapped
|
||
prose form (relaxed in S26 so the template's prose emission parses).
|
||
|
||
**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
|
||
5. **plan_version 1.8 or later** (1.7 plans are not held to these):
|
||
- every step has `Test first:` with `File:` and `Verifies:` sub-bullets;
|
||
- at least one step has `End-to-end:` with a backticked command — the "run the thing" step;
|
||
- the optional step fields `Interface in:`, `Interface out:`, `Review focus:` are never empty when present (consumers tolerate their absence);
|
||
- `ceremony` is one of the three classes; an `architectural` plan has `## Design` with the subsections Architecture, Data Flow, Interfaces, Data Models, Error Handling, Module Map, Architecture Rules, Requirements Check, plus `## Test Strategy`; the module map (mermaid) has no cycle; every architecture rule says "checked by `<command>`" (with a Proof column naming an existing step) or "advisory — `.claude/rules/<file>.md`".
|
||
6. **`## Critic Verdict`** (written by `lib/validators/plan-gate.mjs --record`, never by hand): one fenced `json` block, schema `voyage.plan-verdict/1` — `round`, `max_rounds` (3), `extra_rounds`, `plan_sha256`, `verdict` (`BLOCK` | `REVISE` | `PASS`), `counts`, `findings` (deduplicated, each with `severity`). `plan_sha256` hashes the plan minus its review log (the `> **Plan quality:` line and the sections Critic Verdict, Revisions, Plan Quality Score; an old Adversarial Pass 2 section is plan text and hashed), with CRLF and line-end whitespace normalized. The verdict sits inside the plan because Claude Code's plan approval hands a hook the plan file's text and nothing beside it. **Optional rule-A fields** (decision 43; `DIFF_FIELDS` in `lib/review/critic-diff.mjs`): each one is optional, and absent means round-1 behaviour, so an older block gives the same exit codes. Top level: `diff_base` (the sha256 the round was counted against), `diff_note` (one line: why a round from round 2 on was counted fully), `resolved` (the ids of previous findings that left the count). Per finding: `op_line` and `op_line_end` (the cited lines in the operative text), `line_end`, `rule_keys` (the union of the keys dedup merged), `original_severity` (what a reviewer rated, set whenever the counted `severity` differs), `lowered_by` (`rule A: outside the diff since round <P>` or `rule B: <caught_by>`), `caught_by` (the test, `Regression:` command or gate a rule-B lowering names), `lowering_refused` (why a rule-B lowering was refused) and `carried_from` (the round a carried finding first appeared). `severity` is always the counted severity. `recordVerdict` called without `base`, `baseNote` and `planPaths` records as before, plus the rule-B record, and a per-finding `rule_keys` from dedup is always passed through (an optional field). **Optional stop-rule fields** (decisions 217, 220; `lib/review/stop-rule.mjs`): `rounds` (one `{round, blocking, newByClass}` per recorded round, carried from the previous verdict; `newByClass` is `null` for a round counted in full) and `stop_rule` (`{fired, reason: same-class | not-fewer | null, classes, perRound}`); a verdict without them starts its history at the next round. `accepted_open.under` is `"stop-rule"` for an acceptance made after the stop rule fired.
|
||
7. **Every plan_version, no version bump** (a plan without the field validates unchanged; voyage-lekerepo trial 2, 2026-10-08):
|
||
- **K10 — `Regression:`** (optional step field): ONE backticked command the criteria runner's allowlist runs, with no pipe and no `&&` (first backticked span, as for `Verify:`). The executor runs it green before the step's checkpoint; a step without it runs nothing new. `Verify:` stays the step's own red-first test. One that can never run is `PLAN_STEP_REGRESSION_NOT_RUNNABLE` (an error; a warning under `--soft`).
|
||
- **K11 — `On failure:`** opens with one of `revert`, `retry`, `skip`, `escalate` (case-insensitive; a leading `**`, `_`, backtick or `-` is ignored, other leading characters are not; `onFailureAction` in `lib/parsers/plan-schema.mjs`), the words `/trekexecute` reads; any other opening is the warning `PLAN_STEP_ON_FAILURE_UNKNOWN`. A missing `On failure:` is unchanged (the executor defaults to `escalate`).
|
||
- **A step of only tests** (order 10, voyage-lekerepo trial 3, 2026-10-09): when every file in a step's `Files:` is a test (a file on its `Test first:` `File:` line, or a path `lib/util/test-path.mjs` reads as a test: under `tests/`, `test/`, `__tests__/`, `spec/`, `specs/`, or named `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`, `test_*.py`, but never a document such as `docs/specs/api.md`; `stepTestsOnly` in `lib/parsers/plan-schema.mjs`), the warning is `PLAN_STEP_TESTS_ONLY`: the behaviour its tests cover exists at its start point, so it can never be proven red. A step without `Files:` is not judged. At run time `red-proof.mjs --plan <plan.md> --step <N>` puts the same reading on a `not-red-at-base` as `tests_only`: the plan's `Files:` alone decides, and the step's diff is not read (operator 2026-10-11), and `/trekexecute` sends a `tests_only: true` step straight up to the plan.
|
||
- **The `Test first:` files** (operator decision 20, 2026-10-12): a step's test files are the backticked paths that stand right after `File:` on its `Test first:` `File:` line, separated only by commas or whitespace; a path in the description after them (`` covers `parse()` in `src/parser.mjs` ``) is no test file. This holds at every plan_version (`testFirstFiles` in `lib/parsers/plan-schema.mjs`; from 1.9 the line is read by the grammar of invariant 8). Below plan_version 1.9 the run also goes on past a note in brackets (`(new)`, `*(new)*`, `(existing …)`) and the words `and`, `plus` and `+`, to a further path only when it is clearly a test (as `lib/util/test-path.mjs` and the tests-only check read it); a path that is not ends the run (operator decision 28, 2026-10-13, and operator decision 31, 2026-10-14). `red-proof.mjs --plan <plan.md> --step <N>` reads them from the plan, and the documented call passes no `--tests` (operator decision 30, 2026-10-14): it prints the list it ran (`tests`); a given `--tests` that differs is `not-run` with reason `tests-mismatch` and the plan's list in `plan_tests`; a plan or step it cannot read is `not-run` with reason `plan-unreadable`; below plan_version 1.8 only (a `File:` line there may name its test without backticks), a plan that names no test file gets `no test files given`, and then the helper runs it again with `--tests` naming the test files it wrote, and the executor with the files on the `tests:` line in the helper's report (operator decision 41, 2026-10-14).
|
||
8. **plan_version 1.9 only** (1.8 and 1.7 plans are read as before, with one warning, `PLAN_STEP_PATH_UNTICKED`, for a path their reader found outside backticks; decision 16, operator 2026-10-11) — the file first on every line:
|
||
- the entries of a step's `Files:` field are its first non-empty line and each list-item line (`-`, `*`, `+`, `1.`, `1)`); a line that is no list item continues the entry above it, when the entry so far ends with a comma, its text joins that entry with one space before the entry is read, so a list wrapped after a comma keeps its paths; otherwise it is description, never read, and so is every later wrapped line of that entry (operator decision 29, 2026-10-14); an entry with nothing on its own line (a `File:` line with nothing after it) takes its first wrapped line;
|
||
- each entry opens with its backticked path(s), separated only by commas or whitespace; everything from the first other text on is description and is never read (`` `a.test.mjs` - covers `src/parse.mjs` `` names one file), so every path stands before any annotation (`` `src/a.mjs`, `src/b.mjs` (new) ``, never `` `src/a.mjs` (new), `src/b.mjs` ``);
|
||
- each `Test first:` `File:` line is one entry under the same grammar (several test files share one line: `` File: `a.test.mjs`, `b.test.mjs` (new) ``). A line counts only when `File:` opens it (after a list marker `-`, `*`, `+`, `1.` or `1)`, or `**`); a line right after it that is no list item continues it (a wrapped line, read as above), and any list item (a `Verifies:` or `Pattern:` line too) or a blank line ends it; a step whose `Test first:` has no such line with a backticked path names no test and is `PLAN_STEP_NO_TEST_FIRST` (`File:` mid-line is description, as the executor reads it);
|
||
- an entry that does not open with a backticked span, the bare word none (written without backticks), and an empty or absent `Files:` are `PLAN_STEP_PATH_NOT_FIRST` (a 1.9 step always names a file); a leading span that is no path is `PLAN_STEP_PATH_NOT_A_PATH`: a call, an option, a URL, and (operator decision 22, 2026-10-13) a backticked placeholder (`` `none` ``, `` `TBD` ``, `` `TODO` ``, `` `TBA` ``, `` `N/A` ``, `` `NA` ``, `` `...` ``, in any case) or a bare name (no `.` or `/`) that is neither in capitals (`` `LICENSE` ``) nor a build file (`` `Makefile` ``), such as the mixed-case `` `Parser` ``; `` `./Parser` `` names such a file (`./Name`; `isPathShape` in `lib/parsers/plan-schema.mjs`). Both are errors under `--strict`, warnings under `--soft`;
|
||
- K6 (`lib/verification/plan-scope-sweep.mjs`) follows the grammar from 1.9: it reads a 1.9 plan's `Files:` and `Test first:` `File:` line by it (plan-schema `entryPaths` / `testFirstFiles`), and so does the red proof's `tests_only` reading (`lib/verification/red-proof.mjs`); below 1.9 both keep their earlier readers.
|
||
|
||
**Validation strategy:**
|
||
|
||
The strongest validation in the entire pipeline. `/trekplan` Phase 8 **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` | `plan_version` other than 1.7 / 1.8 / 1.9 | Warning only; planner should bump |
|
||
| `PLAN_NO_VERSION` | No `plan_version` found | Warning only; add the header line |
|
||
| `PLAN_NOT_FOUND` / `PLAN_READ_ERROR` | The plan file is missing or unreadable | Check the path |
|
||
| `PLAN_STEP_NO_TEST_FIRST` | 1.8: a step lacks `Test first:` with File + Verifies; 1.9: also when no line opens with `File:` and its backticked path | Add the test the step makes pass |
|
||
| `PLAN_NO_END_TO_END` | 1.8: no step carries `End-to-end:` with a command | Add the "run the thing" step |
|
||
| `PLAN_STEP_FIELD_EMPTY` | 1.8: `Interface in/out:` or `Review focus:` present but empty | Fill or remove it |
|
||
| `PLAN_VERIFICATION_NOT_RUNNABLE` | 1.8: a `## Verification` criterion has no command, or one the criteria runner's allowlist never runs | Name a test runner, or wrap the check as `bash tests/<script>.sh` |
|
||
| `PLAN_STEP_VERIFY_NOT_RUNNABLE` | 1.8: the first backticked span of a step's `Verify:` (its test command) is not one runnable test-runner call | Put the test command first; move other checks to a script under `tests/` |
|
||
| `PLAN_END_TO_END_NOT_RUNNABLE` | 1.8: an `End-to-end:` command the allowlist never runs (e.g. the product's own binary) | Run the product from `bash tests/e2e.sh` |
|
||
| `PLAN_END_TO_END_NOT_VERIFIED` | 1.8: an `End-to-end:` command does not also stand under `## Verification` | List the same command there; Phase 7 runs that section |
|
||
| `PLAN_CEREMONY_INVALID` | 1.8: `ceremony` not small / bounded / architectural | Copy the brief's value |
|
||
| `PLAN_DESIGN_MISSING` / `PLAN_DESIGN_SECTION_MISSING` | 1.8 architectural: no `## Design`, or a required subsection missing | Write it from `templates/design-template.md` |
|
||
| `PLAN_MODULE_MAP_EMPTY` / `PLAN_MODULE_CYCLE` | 1.8 architectural: no mermaid module map, or a cycle in it (all `mermaid` blocks of `### Module Map` form one map) | Draw it; break the cycle |
|
||
| `PLAN_MODULE_MAP_UNPARSED` | 1.8 architectural: a module-map line that looks like a link but gives no edge | Write it as `a --> b` |
|
||
| `PLAN_ARCH_RULE_NO_ENFORCER` / `PLAN_ARCH_RULE_NO_PROOF` | 1.8 architectural: a rule without an enforcer, or a checked rule without an existing proving step | Name the enforcer; name the step |
|
||
| `PLAN_STEP_REGRESSION_NOT_RUNNABLE` | K10, any version: a step's `Regression:` is not one command the allowlist runs (a pipe, `&&`, no backticks, an unknown runner) | One backticked runner call (`npm test`); a wider check goes in `bash tests/<script>.sh` |
|
||
| `PLAN_STEP_ON_FAILURE_UNKNOWN` | K11, any version, warning: `On failure:` does not open with revert / retry / skip / escalate | Open with the word, then the specifics |
|
||
| `PLAN_STEP_TESTS_ONLY` | Any version, warning: every file in the step's `Files:` is a test, so the step can never be proven red | Move the test into the step that builds the behaviour |
|
||
| `PLAN_STEP_PATH_NOT_FIRST` | 1.9: a `Files:` entry does not open with a backticked path (also the bare word none, and an empty or absent `Files:`: the step names no file); error, a warning under `--soft` | Open each entry with its backticked path(s), then the description: `` `src/a.mjs` - the parser `` |
|
||
| `PLAN_STEP_PATH_NOT_A_PATH` | 1.9: a `Files:` entry opens with a backticked span that is no path (a call `parse()`, an option, a URL, a backticked placeholder such as `` `TBD` `` or `` `none` ``, a mixed-case bare name such as `` `Parser` ``; operator decision 22); error, a warning under `--soft` | Put the path first; the span goes in the description; a file named `Parser` is written `./Parser` |
|
||
| `PLAN_STEP_PATH_UNTICKED` | Below 1.9, warning: the `Files:` reader found a path outside backticks; the plan is read as before (a warning under `--strict` too) | Backtick the path; from 1.9 an unticked path is refused |
|
||
|
||
**The plan gate (K3).** `node lib/validators/plan-gate.mjs <plan.md> [--json]` decides whether a plan may go on: **0** = it validates strict, its `## Critic Verdict` matches the current text, no BLOCKER is open, and every open MAJOR was accepted by the operator; **1** = stop, with `reasons` (`PLAN_GATE_INVALID_PLAN`, `PLAN_GATE_VERSION` — below plan_version 1.8, `PLAN_GATE_NO_VERDICT`, `PLAN_GATE_VERDICT_MALFORMED`, `PLAN_GATE_VERDICT_STALE`, `PLAN_GATE_OPEN_BLOCKER`, `PLAN_GATE_ROUNDS_EXHAUSTED`, `PLAN_GATE_OPEN_MAJOR` — rounds left: revise, `PLAN_GATE_OPEN_MAJOR_FINAL` — after the last round: ask the operator, `PLAN_GATE_STOP_RULE` — the stop rule fired with a blocking finding open and not accepted; it replaces the open-blocker and open-major reasons) and `warnings` (`PLAN_GATE_OPEN_MAJOR` for majors the operator accepted, `PLAN_GATE_OPEN_BLOCKER` for blockers accepted under the stop rule); **2** = could not read the plan. `--hash <plan.md>` prints the operative hash; Phase 9 takes it before the reviewers read the plan. `--record <plan.md> --reviewed <hash>` reads plan-review-dedup output on stdin (which lists the reviewers that delivered, `sources`) and writes the next round; exit 2 when a reviewer is missing or the plan changed since `--hash`; it refuses round 4 (exit 1, stop and ask) unless `--extra-round` follows the operator's yes, and refuses that flag before the cap is spent; after a verdict whose stop rule fired it refuses any new round (exit 1, `PLAN_GATE_STOP_RULE`, `--extra-round` included). The stop rule: no new round after round N ≥ 2 when one finding class has new blocking findings in rounds N−1 and N, or when the blocking findings did not get fewer; a class is the catalogue rule key, else the reviewer's dimension (the `rule_key` before the first `/`). Unrated findings count as blockers. The review log is excluded from the hash only up to its first other heading, so a step placed under it is hashed. `--accept-open <plan.md>` records the operator's "build as is" for the current round's open MAJORs (`accepted_open` in the verdict block, bound to the round and the plan hash; only after the last round; never for a BLOCKER; exit 1 when rounds are left, the verdict is stale, a blocker is open or no major is). After the stop rule fired it is allowed with rounds left and accepts the open BLOCKERs and MAJORs as known issues, except a counting security finding (exit 1, `PLAN_GATE_SECURITY_OPEN`). Operator decision 2026-10-08: open majors used to be warnings, and a weakness the critics named was built and came back from `/trekreview` as MAJOR. plan-critic and scope-guardian rate on the review catalogue's tiers. The verdict is a trace of the review, not a signature.
|
||
|
||
**Rule A and the diff (decision 43).** Round 1 is unchanged. From round 2 on, `--record` counts the round against the operative text the previous round reviewed, and both reviewers mark each previous finding by id (`previous`). The gate counts by this rule, not the session:
|
||
|
||
Rule A: from round 2 on, a new major finding that cites a plan line outside the diff since the last reviewed text counts as minor; a blocker, a security finding and a finding with no plan line count at full severity, and a previous finding stays counted until it is marked resolved.
|
||
|
||
Rule B is stated by the reviewers and recorded by the gate from round 1 (`lowered_by`, `caught_by`); it never lowers a blocker or a security finding. `--diff <plan.md>` prints what the next round reviews: `diff-base: <sha256> (round N)` and each hunk in the plan's full-file line numbers, or `diff-base: none — <reason>; review the whole plan` (exit 0; 2 = unreadable plan or bad arguments). `--record` keeps each recorded round's operative text as a snapshot, `<git dir>/voyage/plan-snapshots/<sha256>.md`, named by its own sha256. git never tracks its own directory, so a snapshot is never committed, whatever the repo's `.gitignore` or `artifact_store` says. A plan in no git repository (plan mode) keeps none (`snapshot: "not kept: no git repository"`), and its next round counts fully. A missing, oversized, misnamed or tampered snapshot, or a plan unchanged since the base, also means full counting, said in one `diff_note` line. `--record`'s JSON line adds `diff_base`, `diff_note`, `lowered` (a number) and `snapshot`. Trust limit: anything that can write the plan can also rewrite the previous verdict's `plan_sha256` and plant a matching snapshot. That was already the limit of the verdict block; rule A raises what such a forgery buys, and an unchanged base lowers nothing. The gate matches a previous finding only on overlapping text, and a new finding never takes a previous finding's id: a repeated id gets `<rule_key>#n`.
|
||
|
||
**Verification CLIs that read a plan** (0/1/2, JSON on stdout, 2 is never green): `lib/verification/arch-rules.mjs --repo <dir>` runs the target repo's own dependency-cruiser or import-linter (installs nothing; `--prove --rule <name> --plant <file>=<dest>` shows pass → fail on that rule → pass, refusing a destination that exists or resolves outside the repo); `lib/verification/plan-scope-sweep.mjs --plan <plan.md> --base <ref>` lists changed files no step names (`Files:`, `Test first:` File, manifest `expected_paths`) and no process path covers.
|
||
|
||
**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). v1.7 → v1.8 makes `Test first:` and the "run the thing" step required, adds the optional step fields and the design part, and was written validator-first with tests for both shapes (`tests/validators/plan-validator-v18.test.mjs`); 1.7 plans validate unchanged, so readers need no change (the executor already treats `plan_version ≥ 1.7` as strict). v1.8 → v1.9 gives `Files:` and the `Test first:` `File:` line a grammar, the file first on every line (invariant 8), so no reader has to tell a path from a description in free text; it was written validator-first (`tests/validators/plan-version-1-9.test.mjs`). Only a 1.9 plan is refused for it; 1.8 and 1.7 plans validate as before, with the warning `PLAN_STEP_PATH_UNTICKED`, and the 1.8 rules (invariant 5) bind 1.8 and 1.9 plans alike.
|
||
|
||
**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 (start point recorded before the step; then proof of red + Verify + Manifest audit + floor guard + control + done line + 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 the plan header's plan_version |
|
||
| `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 \| stopped` |
|
||
| `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 |
|
||
| `end_to_end` | string | optional | `verified \| not-verified` — a passing run-the-thing criterion in the plan's `## Verification`; `not-verified` turns `completed` into `partial` (wave 1, P9 (6)) |
|
||
| `run_seen` | string \| null | optional | `yes \| no — {reason}` — what Claude Code's built-in `run` showed, recorded in Phase 7; `null` = not run; additive-optional (fix round 4.5b): a file without it validates and resumes, no `schema_version` bump; Phase 8 reads it from here, so the resume and multi-session paths report it |
|
||
| `decisions` | array | optional | `[{step, decision, cost_if_wrong}]` — every call the executor made that the plan did not dictate; both strings non-empty (wave 1, P4) |
|
||
|
||
**Per-step record:**
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `status` | `completed \| in_progress \| failed \| pending \| deferred \| skipped \| blocked` | `blocked` = Step 0 sandbox sentinel |
|
||
| `attempts` | number | 1..N |
|
||
| `error` | string \| null | |
|
||
| `completed_at` | ISO string \| null | |
|
||
| `commit` | string \| null | full commit id (`git rev-parse HEAD`) after Checkpoint |
|
||
| `manifest_audit` | string | `pass \| fail \| pass-with-note \| n/a` |
|
||
| `note` | string | optional human-readable annotation |
|
||
| `start_sha` | string \| null | optional (wave 1) — git sha the step started from; every check of the step reads `git diff {start_sha}` |
|
||
| `report_path` | string \| null | optional (wave 1) — the step's report file: the helper's ran / saw / did-NOT-check rounds and the script-written `DONE step N` line |
|
||
| `red_proof` | string \| null | optional (wave 1) — `proven \| rejected \| not-run`, the exit of `lib/verification/red-proof.mjs` |
|
||
| `test_lock` | string \| null | optional (wave 1) — `locked \| released \| not-locked` (K8, `lib/util/test-lock.mjs`) |
|
||
| `fix_rounds` | number | optional (wave 1) — 0..5: 0 = passed its first control, 1–3 = fix rounds, 4 = sent up to the plan, 5 = sent up to the operator. 6 is invalid |
|
||
|
||
The wave-1 fields are **additive-optional on read**: a `progress.json` written
|
||
before them validates and resumes unchanged (no `schema_version` bump). `null`
|
||
means "not set yet". When present, `progress-validator.mjs` shape-checks them.
|
||
|
||
**Wave-2 coupling fields (order 6, additive-optional, same rule).** Each is a
|
||
contract CLI's exit said in words; `not-measured` / `not-run` are never green.
|
||
|
||
| Field | Level | Values | Source |
|
||
|---|---|---|---|
|
||
| `step_check` | step | `clean \| findings \| not-measured` | K9 `lib/verification/step-check.mjs` exit 0 / 1 / 2 (Sub-step D4) |
|
||
| `regression` | step | `passed \| failed \| unrunnable \| not-run \| absent` | K10 `lib/verification/regression-check.mjs` — the step's `Regression:` run through the criteria runner; `absent` = no field |
|
||
| `plan_approval` | run | `approved \| not-checked` | K2 `approval-log.mjs verify --kind plan` (Phase 1); `not-checked` = a session spec |
|
||
| `arch_rules` | run | `pass \| violation \| not-measured` | K5 `lib/verification/arch-rules.mjs` exit 0 / 1 / 2 (Phase 7) |
|
||
| `scope_sweep` | run | `{status: pass \| deviation \| not-measured, outside: [paths]}` | K6 `lib/verification/plan-scope-sweep.mjs` exit 0 / 1 / 2 (Phase 7) |
|
||
| `simplify` | run | `applied \| clean \| reverted \| revert-refused \| not-run` | `/simplify` in Phase 7.2; `revert-refused` = the restore was refused or declined |
|
||
| `review` | run | `{round ≥ 1, path, gate_exit: 0 \| 1 \| 2, verdict}` | K7 `lib/review/verdict-gate.mjs` on `review.md` (Phase 7.2; Phase 8 locks on it) |
|
||
|
||
`plan_verification.failed_criteria[]` carries `exit_code` (integer, or `null`
|
||
for a criterion that never ran). The criteria runner reports `exitCode`; the
|
||
executor translates the key, and the validator rejects the runner's key or a
|
||
missing `exit_code` (C15, `PROGRESS_FAILED_CRITERIA_KEY`).
|
||
|
||
**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.
|
||
|
||
**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
|
||
- `PROGRESS_FIX_ROUNDS_INVALID` → `fix_rounds` not an integer 0..5; resume halts
|
||
- `PROGRESS_START_SHA_INVALID` / `PROGRESS_REPORT_PATH_INVALID` / `PROGRESS_RED_PROOF_INVALID` / `PROGRESS_TEST_LOCK_INVALID` → a wave-1 step field of the wrong shape; resume halts
|
||
- `PROGRESS_DECISION_INVALID` → `decisions` is not an array of `{decision, cost_if_wrong}`; resume halts
|
||
- `PROGRESS_STEP_CHECK_INVALID` / `PROGRESS_REGRESSION_INVALID` / `PROGRESS_PLAN_APPROVAL_INVALID` / `PROGRESS_ARCH_RULES_INVALID` / `PROGRESS_SCOPE_SWEEP_INVALID` / `PROGRESS_SIMPLIFY_INVALID` / `PROGRESS_REVIEW_INVALID` → a wave-2 coupling field of the wrong shape; resume halts
|
||
- `PROGRESS_FAILED_CRITERIA_KEY` → a `failed_criteria` entry with the runner's `exitCode`, or without `exit_code`; resume halts. This one is a tightening, not an addition: a progress file written before it with the runner's key (the C15 defect itself) no longer resumes until the key is renamed to `exit_code`
|
||
|
||
---
|
||
|
||
## Handover 6 — `review.md` → plan
|
||
|
||
**Handover 6 closes the iteration loop.** Where Handovers 1–4 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 plan-input. Since 1.1 every BLOCKER is also written as a numbered `### Step R<n>` under `## Remediation Steps` (Files · Source finding · Test first · Verify · On failure), so a consumer can append it as a plan step without rewording it.
|
||
|
||
**Second consumer — the verdict gate (K7, 1.1):** `lib/review/verdict-gate.mjs <review.md>` reads the frontmatter `verdict` and exits 0 = ALLOW/WARN, 1 = BLOCK, 2 = could not decide. WARN opens the merge only when the verdict gate finds every counting MAJOR answered. Exit 2 is never green: a missing file, a missing or unknown verdict, two verdict lines, a trailing JSON block that is not the last fence or lacks `verdict`/`counts`/`findings`, and a file whose JSON block, `## Findings (BLOCKER)` section or finding-id list contradicts a greener verdict all land there — and so does a WARN that exists because a reviewer or engine did not run (`missing-reviewer:` / `unattributable-payload` in the JSON block's `allow_blocked_by`, which a 1.1 review must carry): a review not performed in full stops a merge (operator decision 2026-10-06). Exactly one file per call. With `--repo <dir>` (`verdict-gate.mjs --json <review.md> --repo <dir>`, R6) an ALLOW/WARN review must also be of the commit being merged in `<dir>`: its `scope_sha_end`, read only as a full 40- or 64-hex commit id (a short id or a ref name is stale), is HEAD, or an ancestor of HEAD whose later commits change only the review lessons (`.claude/rules/voyage-review-lessons.md`, inside its managed block), the `CLAUDE.md` pointer and the approval log at the git top level (`.claude/voyage/approvals.jsonl`), there only by append-only answer records: K2 `review` records added after the log's existing bytes, each with exactly the record fields, its own hash, the next `seq` and a `prev` that links (`lib/review/reviewed-commit.mjs`); otherwise exit 2 with a reason that starts `review-stale:`, which `/trekexecute`'s merge lock (Phase 7.2 step 4 and Phase 8, both with `--repo .`) maps to the decline reason `review-stale`. When the gate cannot look (the `--repo` path is missing, it is no git repository, or a git call failed), the exit is 2 with a reason that starts `review-not-measured:`, never `review-stale:`: the merge lock reports it as not measured, never green. An ALLOW/WARN review whose project folder holds a Phase 4.7 engine payload (`.review-engine-payload-*.json`) with a finding at MAJOR or BLOCKER missing from `review.md` is exit 2 as well (an engine finding returned on its own row at a lower severity is not caught), with the reason `review-not-measured: engine finding not in review` naming each missing one: finalize's put-back did not run, or `finalized: true` was written by hand. A `--repo` with no value is exit 2 as well; BLOCK stays exit 1. Whatever stops a merge or a release on a review reads this exit code, never a summary. Borrowed: the verdict goes to a file and a command gates on it (Kiro `feature-pipeline`); the executor reads the verdict but never writes it (Spec Kit).
|
||
|
||
**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. Before round 2 and later, the previous `review.md` is kept as `review-round-<run>-<n>.md` beside it (`<run>` = first 12 hex of the run's `scope_sha_start`); those archives are read by the next round and by the gate for a carried answer.
|
||
|
||
**Frontmatter schema:**
|
||
|
||
| Field | Type | Required | Allowed values | Notes |
|
||
|---|---|---|---|---|
|
||
| `type` | string | yes | `trekreview` | Hard-coded discriminator |
|
||
| `review_version` | string | yes | `"1.2"` (current); `"1.1"` still valid | 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, as a full 40- or 64-hex commit id | Defines "after" boundary. The verdict gate reads only a full 40- or 64-hex commit id (M11): a short id or a ref name can also be a branch, which resolves to wherever it points now |
|
||
| `reviewed_files_count` | number | yes | ≥ 0 | From triage gate Coverage |
|
||
| `findings` | list | yes | block-style YAML list of 40-char hex IDs; `[]` when empty | Flat array; full objects in body. A bare `findings:` is null and fails |
|
||
| `verdict` | string | yes (1.1; strict at write, warning at read) | `BLOCK \| WARN \| ALLOW` | Coordinator output; equals the trailing JSON block's `verdict` (K7) |
|
||
| `created` | date | optional | YYYY-MM-DD | |
|
||
| `scope_sha_start` | string | optional | git sha at review start | `null` if mtime fallback used |
|
||
| `round` | integer | optional (1.2) | `1`-`3` | The review round; written by `review-finalize.mjs` |
|
||
| `round_base` | string \| null | optional (1.2) | full 40- or 64-hex commit id, or `null` at round 1 | The commit the previous round reviewed; written by `review-finalize.mjs` |
|
||
| `review_policy` | string | optional (1.2) | `absent \| applied \| not-applied` | Whether a `REVIEW.md` policy block was applied; written by `review-finalize.mjs` |
|
||
| `review_policy_note` | string | optional (1.2) | one line | Why a policy was not applied, or what it did |
|
||
| `review_policy_sha256` | string | optional (1.2) | 64-hex, or `none` | sha256 of the `REVIEW.md` blob read at `scope_sha_start` |
|
||
| `finalized` | boolean | optional (1.2) | `true \| false` | `true` is set by `review-finalize.mjs` when it has written `ran` |
|
||
|
||
`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; engine findings on files outside the review's SHA range are listed there too.
|
||
|
||
Since 1.1 (ordre 5):
|
||
- `## Remediation Steps` — required at `review_version` ≥ 1.1 when there is a BLOCKER: one `### Step R<n>` per BLOCKER carrying `Source finding: <id>` (strict error `REVIEW_MISSING_REMEDIATION_STEP`, soft warning). Steps are appended and numbered, never reworded across iterations (Spec Kit `/converge`).
|
||
- `## Declined to Judge` — required at `review_version` ≥ 1.1 (strict error, soft warning; 1.0 reviews exempt): every finding set aside without a ruling, with its reason (including engine findings outside the review scope, `engine:out-of-scope`), plus `Trimmed: N`. **No finding is struck for length**: only MINOR/SUGGESTION text is trimmed, and counted (`trimmed` in the JSON block); a long BLOCKER/MAJOR keeps every character.
|
||
- Findings from Claude Code's own `/code-review` and `/security-review` enter under the catalogue keys `CODE_REVIEW_DEFECT` (MAJOR), `CODE_REVIEW_IMPROVEMENT` (MINOR: a `/code-review` finding whose category is simplification, efficiency, reuse, style or readability) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security-review`.
|
||
- Working files of one review run, in the project folder (not part of `review.md`): the engine transcriptions `.review-engine-{code-review|security-review}.json` (`/trekreview` Phase 4.7) and, beside them, the converted payloads `.review-engine-payload-{engine}.json` (the `engine-findings.mjs` output, `{reviewer, findings, out_of_scope}`). `review-finalize.mjs` reads the payloads to record which engines ran and to put back, at its own severity, an engine finding at MAJOR or BLOCKER that no finding of `review.md` carries (the standard-path comparison); the verdict gate reads the payloads too, to compare them with `review.md`: an engine finding at MAJOR or BLOCKER missing from it is exit 2 (`review-not-measured: engine finding not in review`). `review-rounds.mjs prepare` deletes the payloads at the start of each round.
|
||
|
||
Since 1.2 (every field and section optional; a malformed one present is a strict error, a soft warning):
|
||
- Trailing JSON block, additive: per finding `pre_existing` (boolean: the range did not add or change the cited line and `scope_sha_start` is bound to the fork point from main (the fork point itself, or a later commit reached through process files only); otherwise it is never true), `carried_from` (the previous round's finding id) and `instruction_ref` (the repo path of the instruction file a CLAUDE.md-key finding cites); top level `pre_existing_counts` (`{SEVERITY: n}`), `resolved` (the previous round's finding ids no longer raised) and `ran` (`{engines: [code-review|security-review], reviewers: [string]}`, written with `finalized: true`). Codes `REVIEW_PRE_EXISTING_MALFORMED`, `REVIEW_ROUND_MALFORMED`, `REVIEW_POLICY_MALFORMED`.
|
||
- `## Pre-existing` — the titles and counts of the findings marked `pre_existing`; written by `review-finalize.mjs`.
|
||
- `## Resolved since round <n>` — the previous round's findings this round no longer raises; written by `review-finalize.mjs`.
|
||
- `## Answers` — the operator's answer to each counting MAJOR, written by `/trekreview` Phase 8.8 after `## Remediation Steps` and before the trailing JSON block: a pipe table with the header exactly `| Finding | Answer | Reason | Record | Carried from |`; per row a 40-hex finding id (optionally `#n`), `accepted` or `fixed`, a reason (non-empty when `accepted`), the 64-hex hash of the K2 `review` record, and `-` or `review-round-<run12>-<n>.md#<id>`; at most one row per finding. A MAJOR answered `fixed` needs its `### Step R<n>` with `Source finding: <id>` under `## Remediation Steps`. Code `REVIEW_ANSWERS_MALFORMED`.
|
||
- `## Declined to Judge` gains three reasons: `policy:skip` and `policy:cap` (a MINOR or SUGGESTION finding the `REVIEW.md` policy skipped or capped) and `instruction-out-of-set` (a CLAUDE.md-key finding that cites a file outside the tracked instruction set).
|
||
|
||
**The brief-conflict mark (fields `engine`, `contradicts_brief`, `brief_lowered_from`).**
|
||
Each finding may carry three more fields, in its body field list and in the trailing JSON block:
|
||
`engine` (`code-review` | `security-review`, on a finding that came from an engine),
|
||
`contradicts_brief` (the brief's Non-Goal or stated choice the finding's recommended action contradicts, verbatim)
|
||
and `brief_lowered_from` (`MAJOR`, written only by the one lowering).
|
||
The mark is a field on a kept finding, never a `suppressed_reason`.
|
||
Only an engine finding is ever marked; a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue never are.
|
||
The one legal lowering: a `/code-review` MAJOR-tier finding at severity MAJOR drops to MINOR, with `brief_lowered_from: MAJOR`;
|
||
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding keep their severity and only carry the mark.
|
||
On an ALLOW or WARN review the verdict gate exits 2 on every finding `illegalBriefLowering` (`lib/review/coordinator-contract.mjs`) names:
|
||
any other `brief_lowered_from`, a mark on a reviewer-agent finding, a marked finding below its tier, a marked finding with an unknown key,
|
||
and an engine finding below its tier, marked or not (a BLOCK review exits 1 as before).
|
||
The gate cannot check an `engine` label on the prose path: there the coordinator writes the label, so a finding wrongly labelled `engine` is judged as an engine finding.
|
||
`runContract` (`lib/review/coordinator-contract.mjs`) likewise takes `engine` from the payload a finding arrived in, never from the finding's own field,
|
||
and in dedup the survivor is ranked by catalogue tier (for a catalogue key it always ties inside one triplet), so a reviewer-agent finding survives over an engine finding and carries the highest severity the group asserted; an engine's mark never lowers what a reviewer agent asserted.
|
||
A plan made from the review skips a lowered finding: it is MINOR, and `/trekplan` takes BLOCKER and MAJOR findings only.
|
||
|
||
**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}$` |
|
||
| Verdict present | strict: error · soft: warning | `verdict` ∈ `ALLOW \| WARN \| BLOCK`; an out-of-enum value is an error in both modes |
|
||
| Remediation step per BLOCKER | strict: error · soft: warning | `### Step R<n>` with `Source finding: <id>` under `## Remediation Steps` |
|
||
| Body sections | strict only | `Executive Summary`, `Coverage`, `Remediation Summary` |
|
||
| 1.2 field shapes | strict: error · soft: warning | at `review_version` ≥ 1.2, each optional field and the `## Answers` table present has its shape; a MAJOR answered `fixed` has its `### Step R<n>` |
|
||
| 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.2`. From 1.1 to 1.2 only optional fields and sections were added (the round, policy and `finalized` frontmatter fields, the `pre_existing`, `carried_from`, `instruction_ref`, `pre_existing_counts`, `resolved` and `ran` JSON fields, `## Answers`, `## Pre-existing` and `## Resolved since round <n>`), so a 1.1 review still validates, and it never opens the merge lock with an unanswered MAJOR, because the verdict gate checks the answers of a 1.1 WARN as it does a 1.2 one. 1.1 (ordre 5) made `verdict` required and added remediation steps, Declined to Judge and two engine rule keys; a third, `CODE_REVIEW_IMPROVEMENT`, came with lekerepo trial 2 — three engine rule keys. N-1 window: a `1.1` review validates unchanged for one minor version, and only with a warning after that; no reader fails on it. A `1.0` review without `verdict` still validates in soft mode (warning) and still seeds a plan; it fails strict mode and the verdict gate (exit 2), because a missing verdict is never read as ALLOW. 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
|
||
- `REVIEW_MISSING_VERDICT` → strict halt; soft-mode warning (1.0 reviews)
|
||
- `REVIEW_BAD_VERDICT` → halt; verdict outside `ALLOW | WARN | BLOCK`
|
||
- `REVIEW_MISSING_REMEDIATION_STEP` → strict halt; soft-mode warning
|
||
- `REVIEW_ANSWERS_MALFORMED` → strict halt; soft-mode warning (1.2: the `## Answers` table, or a MAJOR answered `fixed` without its R-step)
|
||
- `REVIEW_POLICY_MALFORMED` → strict halt; soft-mode warning (1.2: `review_policy`, `review_policy_sha256`, `instruction_ref`)
|
||
- `REVIEW_ROUND_MALFORMED` → strict halt; soft-mode warning (1.2: `round`, `round_base`, `finalized`, `resolved`, `ran`, `carried_from`)
|
||
- `REVIEW_PRE_EXISTING_MALFORMED` → strict halt; soft-mode warning (1.2: `pre_existing`, `pre_existing_counts`)
|
||
|
||
---
|
||
|
||
## 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
|
||
|
||
When both a repo-root and a project-dir prompt file exist, `/trekcontinue` Phase 1.5 uses the project-dir file and names the root file as ignored.
|
||
|
||
**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.
|
||
|
||
---
|
||
|
||
## Stability summary
|
||
|
||
| Handover | Validation strength | Owner | Risk |
|
||
|---|---|---|---|
|
||
| 1. brief → research | strict at write, soft at read | this plugin (**PUBLIC CONTRACT** — producer-facing) | 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 |
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
## Internal files
|
||
|
||
Files Voyage writes for its own use. They are no handover: no other stage reads
|
||
them, they carry no version promise, and they may change in any release.
|
||
|
||
**Merge evidence** (fix round 4.5b2 part B). `/trekexecute` Phase 8 runs
|
||
`lib/review/merge-evidence.mjs write` before it merges a run's branch into
|
||
main. It writes `<git common dir>/voyage/merge-evidence/<branch head sha>.json`:
|
||
inside the git directory, so it is never tracked and never published, and
|
||
shared by every worktree of the repo, so the guard finds it from main's tree.
|
||
The Bash guard reads it with `hooks/lib/merge-evidence.mjs` into a "Tested by"
|
||
and an "Undone by" line of its merge question; the guard's answer stays "ask",
|
||
and a missing, stale, foreign or unreadable file reads "no run evidence" or
|
||
"not measured". Fields: `v` (`1`), `branch`, `branch_head_sha`, `written_at`,
|
||
`end_to_end` (`{command, exit}`; `exit` is `0` when `progress.json` says
|
||
`verified`, else `null`), `verification` (`{passed, total}`), `review`
|
||
(`{verdict, sha}`) and `door` (`{type: one-way | two-way | null, text}`).
|
||
Files older than 30 days are removed when the next one is written. It is a new
|
||
internal file, not a field of `progress.json`: it is not part of Handover 5.
|