72 KiB
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.1 |
.session-state.local.json |
schema_version (top-level) |
1 (number) |
Breaking-change protocol
- Bump the artifact's version field.
- Update the matching validator in
lib/validators/. - Add a fixture under
tests/fixtures/covering both old and new shapes. - Document the change in
MIGRATION.mdwith at least an N-1 compatibility window in the validator (read both shapes; warn on old, fail only after one minor version of warning). - Bump the plugin version in
package.jsonand.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 (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 |
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 <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). |
Body invariants: required sections (validator runs in strict mode at write-time, soft mode at read-time):
## TL;DR— required atbrief_version ≥ 2.2(v5.5); ≤ 5 content lines (soft cap →BRIEF_TLDR_TOO_LONGwarning). 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), 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: 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 <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). --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 <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→/trekplanhalts (intent-approval gate, above); remedy/trekbrief --approve <project-dir | brief-file>- approval log
broken/diverged→/trekplanhalts; restore.claude/voyage/approvals.jsonlfrom 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 enumBRIEF_NOT_FOUND→ consumer halts with a usage messageFM_MISSING→ file has no frontmatter; haltBRIEF_WRONG_TYPE→ file is not a brief; haltBRIEF_MISSING_FIELD→ strict halt; soft-mode warningBRIEF_STATE_INCOHERENT→ strict halt; soft-mode warning (incoherence will haunt downstream agents)BRIEF_MISSING_SECTION→ strict halt; soft-mode warningBRIEF_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 bothphase_signalsandphase_signals_partial: true.BRIEF_SC_NOT_RUNNABLE→ warning in both modes;/trekbriefnames each one and offers a script undertests/the criterion runs (bash tests/<script>.sh).BRIEF_SC_SUBPOINTS→ warning in both modes;/trekbriefoffers one script that checks all sub-points, or one criterion per sub-point.BRIEF_SC_BASH_NOT_PINNED→ warning in both modes;/trekbriefoffers/bin/bash tests/<script>.shso a bash 3.2 requirement is measured under the system bash.
Compatibility direction of the v5.9 allowlist widening (fable): enum
widening is safe for new readers of old data, not old readers of new data.
Existing sonnet/opus briefs stay valid under the v5.9+ validator (non-breaking,
no brief_version bump — value-space extension, not a schema change). The
reverse does NOT hold: a fable-bearing brief REQUIRES a v5.9+ validator — an
older cached brief-validator (e.g. a stale v5.8 marketplace clone) rejects it
with BRIEF_INVALID_MODEL.
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):
- Top-level structure:
## Implementation Planheading present- One or more
### Step N: <title>headings, numbered 1..N contiguously ### Step N:is the literal canonical form — colon + space
- Forbidden narrative-drift heading forms (Opus 4.7 regression guard):
## Fase N(Norwegian)### Phase N### Stage N### Steg N(Norwegian variant)
- 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)
- Indented fenced YAML:
- Step count == manifest count
- plan_version 1.8 or later (1.7 plans are not held to these):
- every step has
Test first:withFile:andVerifies: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); ceremonyis one of the three classes; anarchitecturalplan has## Designwith 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".
- every step has
## Critic Verdict(written bylib/validators/plan-gate.mjs --record, never by hand): one fencedjsonblock, schemavoyage.plan-verdict/1—round,max_rounds(3),extra_rounds,plan_sha256,verdict(BLOCK|REVISE|PASS),counts,findings(deduplicated, each withseverity).plan_sha256hashes the plan minus its review log (the> **Plan quality:line and the sections Critic Verdict, Revisions, Plan Quality Score, Adversarial Pass 2), 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_FIELDSinlib/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_lineandop_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 countedseveritydiffers),lowered_by(rule A: outside the diff since round <P>orrule 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) andcarried_from(the round a carried finding first appeared).severityis always the counted severity.recordVerdictcalled withoutbase,baseNoteandplanPathsrecords as before, plus the rule-B record, and a per-findingrule_keysfrom dedup is always passed through (an optional field).- 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 forVerify:). 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 isPLAN_STEP_REGRESSION_NOT_RUNNABLE(an error; a warning under--soft). - K11 —
On failure:opens with one ofrevert,retry,skip,escalate(case-insensitive; a leading**,_, backtick or-is ignored, other leading characters are not;onFailureActioninlib/parsers/plan-schema.mjs), the words/trekexecutereads; any other opening is the warningPLAN_STEP_ON_FAILURE_UNKNOWN. A missingOn failure:is unchanged (the executor defaults toescalate). - 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 itsTest first:File:line, or a pathlib/util/test-path.mjsreads as a test: undertests/,test/,__tests__/,spec/,specs/, or named*.test.*,*.spec.*,*_test.*,*_spec.*,test_*.py, but never a document such asdocs/specs/api.md;stepTestsOnlyinlib/parsers/plan-schema.mjs), the warning isPLAN_STEP_TESTS_ONLY: the behaviour its tests cover exists at its start point, so it can never be proven red. A step withoutFiles:is not judged. At run timered-proof.mjs --plan <plan.md> --step <N>puts the same reading on anot-red-at-baseastests_only: the plan'sFiles:alone decides, and the step's diff is not read (operator 2026-10-11), and/trekexecutesends atests_only: truestep 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 afterFile:on itsTest 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 (testFirstFilesinlib/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 wordsand,plusand+, to a further path only when it is clearly a test (aslib/util/test-path.mjsand 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--teststhat differs isnot-runwith reasontests-mismatchand the plan's list inplan_tests; a plan or step it cannot read isnot-runwith reasonplan-unreadable; below plan_version 1.8 only (aFile:line there may name its test without backticks), a plan that names no test file getsno test files given, and then the helper runs it again with--testsnaming the test files it wrote, and the executor with the files on thetests:line in the helper's report (operator decision 41, 2026-10-14).
- K10 —
- 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 (aFile: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 whenFile:opens it (after a list marker-,*,+,1.or1), or**); a line right after it that is no list item continues it (a wrapped line, read as above), and any list item (aVerifies:orPattern:line too) or a blank line ends it; a step whoseTest first:has no such line with a backticked path names no test and isPLAN_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:arePLAN_STEP_PATH_NOT_FIRST(a 1.9 step always names a file); a leading span that is no path isPLAN_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;isPathShapeinlib/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'sFiles:andTest first:File:line by it (plan-schemaentryPaths/testFirstFiles), and so does the red proof'stests_onlyreading (lib/verification/red-proof.mjs); below 1.9 both keep their earlier readers.
- the entries of a step's
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) and warnings (PLAN_GATE_OPEN_MAJOR for majors the operator accepted); 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. 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). 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)) |
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 | git sha 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:
/trekexecute --validate(alongside plan-validator)/trekexecute --resumeentry (must passcheckResumeReadiness)pre-compact-flushhook (drift check before compaction; never blocks)
Drift detection: the pre-compact-flush hook compares progress.steps[N].commit against git log --oneline {session_start_sha}..HEAD. If git reality has progressed past the recorded current_step, the hook updates progress.json atomically (tmp + rename, monotonic only) before allowing compaction. This guards against the documented P0 drift in docs/trekexecute-v2-observations-from-config-audit-v4.md.
Versioning: schema_version: "1" is current. Future bump (e.g. "2") should add a backward-compat read path that downgrades unknown fields to warnings.
Failure modes:
PROGRESS_PARSE_ERROR→ JSON corruption; resume haltsPROGRESS_SCHEMA_MISMATCH→ unknown schema version; resume haltsPROGRESS_MISSING_FIELD→ required top-level field absent; resume haltsPROGRESS_STEP_RANGE→current_stepoutside[0, total_steps]; resume haltsPROGRESS_ALREADY_DONE→status === completed; nothing to resumePROGRESS_STEP_COUNT_MISMATCH→ warning; not a blockerPROGRESS_FIX_ROUNDS_INVALID→fix_roundsnot an integer 0..5; resume haltsPROGRESS_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 haltsPROGRESS_DECISION_INVALID→decisionsis not an array of{decision, cost_if_wrong}; resume haltsPROGRESS_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 haltsPROGRESS_FAILED_CRITERIA_KEY→ afailed_criteriaentry with the runner'sexitCode, or withoutexit_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 toexit_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. 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. 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.
Frontmatter schema:
| Field | Type | Required | Allowed values | Notes |
|---|---|---|---|---|
type |
string | yes | trekreview |
Hard-coded discriminator |
review_version |
string | yes | "1.0" (current) |
Bump on schema change |
task |
string | yes | one-line description | Mirrors brief task |
slug |
string | yes | URL-safe slug | Used in project_dir |
project_dir |
string | yes | .claude/projects/{date}-{slug}/ |
|
brief_path |
string | yes | path to consumed brief.md |
Audit trail back to brief |
scope_sha_end |
string | yes | git sha of HEAD at review time | Defines "after" boundary |
reviewed_files_count |
number | yes | ≥ 0 | From triage gate Coverage |
findings |
list | yes | block-style YAML list of 40-char hex IDs; [] 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 |
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 atreview_version≥ 1.1 when there is a BLOCKER: one### Step R<n>per BLOCKER carryingSource finding: <id>(strict errorREVIEW_MISSING_REMEDIATION_STEP, soft warning). Steps are appended and numbered, never reworded across iterations (Spec Kit/converge).## Declined to Judge— required atreview_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), plusTrimmed: N. No finding is struck for length: only MINOR/SUGGESTION text is trimmed, and counted (trimmedin the JSON block); a long BLOCKER/MAJOR keeps every character.- Findings from Claude Code's own
/code-reviewand/security-reviewenter under the catalogue keysCODE_REVIEW_DEFECT(MAJOR),CODE_REVIEW_IMPROVEMENT(MINOR: a/code-reviewfinding whose category is simplification, efficiency, reuse, style or readability) andSECURITY_REVIEW_VULNERABILITY(BLOCKER), or a narrower key, withreviewerclaude-code-review/claude-security-review.
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;
the workflow path (--workflow) takes engine from the payload source and writes it into the returned 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.
On the workflow path dedup ranks as Pass 1 does (both paths), every engine finding the coordinator did not return at its exact (file, line, rule_key) is put back at its own severity (operator decision 23), and a returned finding is never below the lower of its reviewer counterpart's severity and its catalogue tier (floor (b)), so the workflow path is stricter than the prose path; a coordinator that returns no verdict and findings counts as a missing reviewer there (missing-reviewer:review-coordinator in allow_blocked_by), and its findings are read at their catalogue tier or higher, so the review is at least WARN and the verdict gate exits 2 on such a WARN (a BLOCK stays exit 1).
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 |
| 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.1 (ordre 5: verdict required, remediation steps, Declined to Judge, two engine rule keys; a third, CODE_REVIEW_IMPROVEMENT, since lekerepo trial 2 — three engine rule keys). N-1 window: 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 messageREVIEW_READ_ERROR→ I/O failure; haltFM_MISSING→ file has no frontmatter; haltREVIEW_WRONG_TYPE→type !== "trekreview"; haltREVIEW_MISSING_FIELD→ strict halt; soft-mode warningREVIEW_BAD_FINDINGS_TYPE→findingsis not an array; halt (covers the YAML flow-style trap)REVIEW_BAD_FINDING_ID→ an ID is not 40-char hex; haltREVIEW_MISSING_SECTION→ strict halt; soft-mode warningREVIEW_VERSION_FORMAT→ warning only; review_version not inN.MformREVIEW_MISSING_VERDICT→ strict halt; soft-mode warning (1.0 reviews)REVIEW_BAD_VERDICT→ halt; verdict outsideALLOW | WARN | BLOCKREVIEW_MISSING_REMEDIATION_STEP→ strict halt; soft-mode warning
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:
/trekexecutePhase 8 (canonical convergence — every completed/failed/stopped/partial run that reaches the final report)/trekexecutePhase 2.55 (Check 1 — dirty-tree pre-flight stop)/trekexecutePhase 4 (entry-condition stop)/trekendsession(informal multi-session helper — Step 9 of v3.3.0)- Future:
graceful-handoffv2.2 may dual-write here as part of its session-rescue artifact (additive — extra fields tolerated, see Body invariants). hooks/scripts/pre-compact-flush.mjsrefreshesupdated_aton existing state files (statusin_progressorpartialonly). 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 Nparallel 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→/trekcontinueexits with cold-start message ("no active multi-session project here; start with/trekbriefor/trekplan")SESSION_STATE_PARSE_ERROR→ halt with structured error; user fixes JSONSESSION_STATE_MISSING_FIELD→ halt; suggests running validator directlySESSION_STATE_SCHEMA_MISMATCH→ halt; future1→2migration path will warn insteadSESSION_STATE_INVALID_STATUS→ halt; protects against typo'd writersSESSION_STATE_NOT_RESUMABLE→ warning;/trekcontinueexits 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.jsonremains the authoritative record of what was attempted.
§ Lifecycle
The state file follows a producer/consumer separation that keeps responsibilities narrow and the contract observable.
Producer/consumer arbeidsdeling:
| Role | Owners | Phase / location |
|---|---|---|
| Producer (writes the state file) | /trekexecute |
Phase 8 (canonical), Phase 2.55 (dirty-tree pre-flight stop), Phase 4 (entry-condition stop) |
| Producer (informal multi-session helper) | /trekendsession |
Phase 3 — writes the same schema for ad-hoc handovers that don't run through executor |
| Refresher (touch only) | hooks/scripts/pre-compact-flush.mjs |
Updates updated_at only; never creates the file; never changes status or any owned field; only acts when status is in_progress or partial |
| Consumer | /trekcontinue |
Phase 2 — reads, validates, narrates a 3-line summary, then begins executing the next session |
Stale-file principle (SC-5): When status === 'completed', the state file and its sibling NEXT-SESSION-PROMPT.local.md represent finished work and SHOULD be removed. Removal is operator-invoked via /trekcontinue --cleanup --confirm <project-dir>; the plugin does NOT auto-cleanup. Stale state is actively harmful — it can mislead a fresh /trekcontinue into resuming a project that's already shipped. The --cleanup gate refuses to act unless validateSessionState({...}).valid === true && parsed.status === 'completed'. There is no force flag.
Frontmatter contract for NEXT-SESSION-PROMPT.local.md: Producers MUST write a YAML frontmatter block on the prompt file with at minimum:
produced_by:— string identifying the producer (e.g.trekplan-A4-session,trekexecute-phase-8,trekendsession)produced_at:— ISO-8601 timestamp of when the file was written
The next-session-prompt-validator (lib/validators/next-session-prompt-validator.mjs) cross-checks produced_at against the sibling state file's updated_at and emits a NEXT_SESSION_PROMPT_INCONSISTENT error when the prompt is older than the state — that means the prompt has not been refreshed for the current session and is stale. Files without any frontmatter are tolerated (warning, not error) for backwards compatibility with v3.3.x and earlier hand-rolled prompt files; this is consistent with Handover 3's drift-WARN posture.
Idempotency: --cleanup --confirm is safe to re-run. If only one of the two files (state file, prompt file) was previously deleted, the second run reports the partial state ("state file: not found, prompt file: removed") but does not auto-recover or re-create. There is no rollback. Operators choosing to re-create a project after --cleanup should re-run /trekbrief from scratch.
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.