voyage-lekerepo end-to-end trial (2026-10-06), report points 2, 3, 4, 9: 11 of 13 brief criteria were NOT RUN at review with no warning in /trekbrief; a plan `git grep` criterion passed validator, gate and three critic rounds, then felled the run in Phase 7; the End-to-end field and Phase 7's text search never met; a prose Verify left the test command undefined. - criteria-runner: runnableCheck() screens a declared check without running it (same allowlist; an npm run script is not looked up at write time); /bin/bash tests/<script> allowed; the plan report carries end_to_end, verified when a passed criterion runs a step's End-to-end command (the older text form still counts). - plan-validator 1.8: PLAN_VERIFICATION_NOT_RUNNABLE, PLAN_STEP_VERIFY_NOT_RUNNABLE, PLAN_END_TO_END_NOT_RUNNABLE, PLAN_END_TO_END_NOT_VERIFIED (the gate inherits them). - brief-validator: BRIEF_SC_NOT_RUNNABLE warning; /trekbrief offers a tests/ script; brief-reviewer no longer proposes unrunnable commands. - trekexecute: Phase 7 copies end_to_end from the runner; Verify's test command is its first backticked span. Measured on the trial's own brief and plan: 11 warnings (the 11 NOT RUN), and the plan stops on V5 git grep and the unlisted End-to-end command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
556 lines
53 KiB
Markdown
556 lines
53 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.8` (1.7 still valid) |
|
||
| `progress.json` | `schema_version` (top-level) | `"1"` |
|
||
| `review.md` | `review_version` (frontmatter) | `1.1` |
|
||
| `.session-state.local.json` | `schema_version` (top-level) | `1` (number) |
|
||
|
||
## Breaking-change protocol
|
||
|
||
1. Bump the artifact's version field.
|
||
2. Update the matching validator in `lib/validators/`.
|
||
3. Add a fixture under `tests/fixtures/` covering both old and new shapes.
|
||
4. Document the change in `MIGRATION.md` with at least an N-1 compatibility window in the validator (read both shapes; warn on old, fail only after one minor version of warning).
|
||
5. Bump the plugin version in `package.json` and `.claude-plugin/plugin.json`.
|
||
|
||
## Validator → handover map
|
||
|
||
| Handover | Validator |
|
||
|---|---|
|
||
| 1. brief → research | `lib/validators/brief-validator.mjs` |
|
||
| 2. research → plan | `lib/validators/research-validator.mjs` |
|
||
| 3. architecture → plan | `lib/validators/architecture-discovery.mjs` |
|
||
| 4. plan → execute | `lib/validators/plan-validator.mjs` |
|
||
| 5. progress.json (resume) | `lib/validators/progress-validator.mjs` |
|
||
| 6. review → plan | `lib/validators/review-validator.mjs` |
|
||
| 7. session-state (multi-session resume) | `lib/validators/session-state-validator.mjs` |
|
||
|
||
Every validator exposes a CLI: `node lib/validators/<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 |
|
||
| `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. |
|
||
| `ceremony` | string | optional (K1) | `small \| bounded \| architectural` | The brief's size class: sets `/trekbrief`'s question budget and review cap, and what downstream phases scale to. **Absent (or unreadable) = `architectural`**, the heaviest, so another producer's brief is never treated lighter by accident. Enum-checked when present (`BRIEF_INVALID_CEREMONY`). Read it through `briefCeremony(fm)` in `brief-validator.mjs`. |
|
||
| `artifact_store` | string | optional | `git \| local` | Whether the brief and plan are committed to the target repo (`git`) or kept out of git through `.git/info/exclude` (`local`). The approval log is committed either way. **Absent = `local`**: nothing is published without the operator's yes. Enum-checked when present (`BRIEF_INVALID_ARTIFACT_STORE`). |
|
||
|
||
**Body invariants:** required sections (validator runs in strict mode at write-time, soft mode at read-time):
|
||
- `## TL;DR` — **required at `brief_version ≥ 2.2`** (v5.5); ≤ 5 content lines (soft cap → `BRIEF_TLDR_TOO_LONG` warning). Layer 3 of the framing-alignment defense.
|
||
- `## Intent`
|
||
- `## Goal`
|
||
- `## Success Criteria`
|
||
|
||
Optional but standard sections: `## Non-Goals`, `## Constraints`, `## Preferences`, `## Non-Functional Requirements`, `## Research Plan`.
|
||
|
||
**Validation strategy:**
|
||
|
||
| Layer | When | What |
|
||
|---|---|---|
|
||
| Frontmatter parse | every read | YAML subset; reject nested dicts |
|
||
| Required fields | every read | All `BRIEF_REQUIRED_FRONTMATTER` present |
|
||
| Type discriminator | every read | `type === "trekbrief"` |
|
||
| Status enum | every read | `research_status ∈ allowed values` |
|
||
| **State machine** | every read | `research_topics > 0 && research_status === "skipped"` requires `brief_quality === "partial"` |
|
||
| **v5.1 sequencing gate** | every read | `brief_version ≥ 2.1` requires `phase_signals` (list) OR `phase_signals_partial: true` — error `BRIEF_V51_MISSING_SIGNALS` on miss. Validator-only enforcement; commands surface, don't re-enforce. |
|
||
| **v5.5 framing gate** | every read | `framing` enum-checked on any version when present (`BRIEF_INVALID_FRAMING`); at `brief_version ≥ 2.2` a missing `framing` → `BRIEF_MISSING_FRAMING` and a missing `## TL;DR` → `BRIEF_MISSING_SECTION` (strict) / warning (soft). `trekreview` briefs are exempt. |
|
||
| Body sections | strict only | All `BRIEF_BODY_SECTIONS` present (`## TL;DR` added at ≥ 2.2) |
|
||
| Runnable criteria | every read | A `## Success Criteria` bullet whose first backticked span is a command the review's criteria runner never runs (the product's own binary, `jq`, `grep`, a pipe) → warning `BRIEF_SC_NOT_RUNNABLE`, never an error: a criterion a human judges stays allowed. Prose-only criteria are not flagged. `trekreview` briefs are exempt. No schema change. |
|
||
|
||
**State machine** detail: a brief that says it has research topics but skipped them must explicitly admit it (via `brief_quality: partial`). This is the most common failure mode the validator catches.
|
||
|
||
**Versioning:** the `2.1` schema axis (v5.1 — adds optional `phase_signals` + `phase_signals_partial`; current is `2.2`, see the next paragraph). The forward-compat policy in `brief-validator.mjs` header still applies: unknown frontmatter keys flow through silently, so a `2.1` brief still validates against pre-v5.1 consumers. The version bump exists because v2.1 activates the **version-conditional sequencing gate** (above) — the only check in the validator that triggers on `brief_version` rather than field-presence. There are no live `1.x` briefs; remove legacy paths in next major. The v5.5.0 contract formalization **established `2.1` as the public-contract baseline** (see the PUBLIC CONTRACT callout under the Handover 1 heading): per the S3 effort-axis decision, `phase_signals` stays **optional**, and the v2.1 sequencing gate (`phase_signals` **or** `phase_signals_partial`) is the stability mechanism. Promoting `phase_signals` to required would be a future breaking change under the protocol above — explicitly *not* part of the v5.5.0 formalization.
|
||
|
||
**v5.5 → `2.2` (framing enforcement).** `2.2` adds two **required-at-2.2** elements — the `framing` enum field and the `## TL;DR` body section — gated identically to the v2.1 mechanism: the new requirements fire only on `brief_version ≥ 2.2`, so every existing `2.0` / `2.1` brief still validates (forward- and backward-compatible). Because adding required elements is a breaking change for any producer that declares `2.2`, this is a controlled version bump under the breaking-change protocol: downstream producers (e.g. a Tier-2 per-app producer) that emit `brief_version: "2.2"` MUST also emit `framing` + `## TL;DR`. The framing enum is additionally enforced on *any* version when the field is present (`BRIEF_INVALID_FRAMING`). The three framing-alignment layers (framing field, `brief-reviewer` memory-alignment dimension, obligatory TL;DR) implement the `CLAUDE.md` cross-cutting invariant. Note: the *plugin* version bump + CHANGELOG entry for this schema change land at the coordinated release (matrix §S10), separate from this schema axis.
|
||
|
||
**Pre-2.2 briefs receive zero framing enforcement (producer-elective defense).** The framing-alignment defense is gated at `brief_version ≥ 2.2`, so a brief declaring `2.0` or `2.1` gets **none** of it: `framing` is optional and a missing value is not an error, the `## TL;DR` section is not required, and the `brief-reviewer` memory-alignment dimension scores N/A. The backward-compatibility this buys is deliberate, but it also means the framing defense is **producer-elective**: any producer can sidestep the entire defense — deliberately or by shipping a legacy template — simply by declaring `brief_version: "2.1"`, and the pipeline raises no signal by default. This is a documented property of the contract, not a bug. Two remedies close it for a given consumer: (1) require upstream producers to emit `brief_version: "2.2"` (which forces `framing` + `## TL;DR`); or (2) pass the opt-in **`--min-brief-version <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 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_V51_MISSING_SIGNALS` → strict halt (v5.1+ sequencing gate); soft-mode warning. Commands surface a friendly hint pointing back to `/trekbrief` (Phase 3.5).
|
||
- `BRIEF_INVALID_PHASE_SIGNALS` → strict halt; phase_signals must be a list of `{phase, effort?, model?}` entries.
|
||
- `BRIEF_INVALID_PHASE_SIGNAL_PHASE` → strict halt; phase ∉ `[research, plan, execute, review]`.
|
||
- `BRIEF_INVALID_EFFORT` → strict halt; effort ∉ `[low, standard, high]`.
|
||
- `BRIEF_INVALID_MODEL` → strict halt; model ∉ `BASE_ALLOWED_MODELS` (currently `[sonnet, opus, fable]`).
|
||
- `BRIEF_SIGNALS_MUTUALLY_EXCLUSIVE` → strict halt; cannot set both `phase_signals` and `phase_signals_partial: true`.
|
||
- `BRIEF_SC_NOT_RUNNABLE` → warning in both modes; `/trekbrief` names each one and offers a script under `tests/` the criterion runs (`bash tests/<script>.sh`).
|
||
|
||
**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) + `planning-orchestrator` (consumes findings as context).
|
||
|
||
**Path conventions:**
|
||
- Project-dir mode: `.claude/projects/{YYYY-MM-DD}-{slug}/research/{NN}-{topic-slug}.md` (sorted by filename).
|
||
- Legacy: `.claude/research/trekresearch-{date}-{slug}.md`.
|
||
|
||
**Frontmatter schema:**
|
||
|
||
| Field | Type | Required | Allowed values |
|
||
|---|---|---|---|
|
||
| `type` | string | yes | `trekresearch-brief` |
|
||
| `created` | date | yes | YYYY-MM-DD |
|
||
| `question` | string | yes | the research question |
|
||
| `confidence` | number | optional | `[0.0, 1.0]` — strongly recommended |
|
||
| `dimensions` | number | optional | ≥ 1 |
|
||
| `mcp_servers_used` | list | optional | server names |
|
||
| `local_agents_used` | list | optional | agent names |
|
||
| `external_agents_used` | list | optional | agent names |
|
||
|
||
Missing `confidence` is a warning, not an error — but downstream planning has no signal to weight findings.
|
||
|
||
**Body invariants:** required sections (strict mode):
|
||
- `## Executive Summary`
|
||
- `## Dimensions`
|
||
|
||
Optional: `## Local Context`, `## External Knowledge`, `## Triangulation`, `## Sources`, `## Recommendations`.
|
||
|
||
**Validation strategy:** schema parse + body-section check. Per-file by `validateResearch`; whole-directory by `validateResearchDir`. Anchoring back to brief topics is currently best-effort, not enforced (planned for a future minor).
|
||
|
||
**Versioning:** unversioned — research briefs are write-once read-once; no migration concern. If schema changes, change `type` discriminator or add `research_brief_version`.
|
||
|
||
**Failure modes:** all same shape as brief (`RESEARCH_*` codes). Default soft mode in plan Phase 1 — research drift does not block planning, but warnings surface in the user-visible summary.
|
||
|
||
---
|
||
|
||
## Handover 3 — architecture/ → plan (EXTERNAL CONTRACT)
|
||
|
||
**This is the only handover where the producer is in a *different plugin*.** The `architecture/overview.md` (and optional `gaps.md`) are produced by an external opt-in architect plugin (no longer publicly distributed; the filesystem slot remains available for any compatible producer). When no producer is installed, this handover is absent — and that is fine.
|
||
|
||
**Producer:** external opt-in architect plugin (no longer publicly distributed).
|
||
|
||
**Consumer:** `/trekplan` Phase 1 (architecture-discovery) + `planning-orchestrator` Phase 7 (cross-reference architecture-note as priors during synthesis).
|
||
|
||
**Path conventions:**
|
||
- Canonical: `{project_dir}/architecture/overview.md`
|
||
- Optional: `{project_dir}/architecture/gaps.md`
|
||
- Tolerated alternatives (with warning): `architecture-overview.md`, `overview.markdown`, `README.md`
|
||
|
||
**Frontmatter schema:** **unenforced.** This is the external contract — `trekplan` does not validate the format. We sniff only the first H1 heading.
|
||
|
||
**Body invariants:** **unenforced.** We never read body content beyond the first heading.
|
||
|
||
**Validation strategy:** **drift-WARN, never drift-FAIL.**
|
||
|
||
| Detection | Result |
|
||
|---|---|
|
||
| File at canonical path | `found: true`, no warnings |
|
||
| File at known alternative path | `found: true`, warning `ARCH_NON_CANONICAL_OVERVIEW` |
|
||
| Loose `*.md` files in `architecture/` not in known set | warning `ARCH_LOOSE_FILES` |
|
||
| No `architecture/` dir | `found: false`, no warnings |
|
||
|
||
The validator (`lib/validators/architecture-discovery.mjs`) is intentionally minimal. It is unit-tested to assert it does NOT read body content beyond the first heading — guarding against scope creep into the producer's territory.
|
||
|
||
**Versioning:** the external producer owns its schema. We do not version this handover from our side.
|
||
|
||
**Failure modes:** none. Discovery always succeeds (returns `found: false` if absent). The handover is additive.
|
||
|
||
**Not the design part.** A plan's own design (`## Design`, plan_version 1.8, 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:** `planning-orchestrator` Phase 5 (plan synthesis) + Phase 5.5 (schema self-check via `plan-validator --strict`).
|
||
|
||
**Consumer:** `/trekexecute` Phase 2 (plan parsing) + `--validate` mode.
|
||
|
||
**Path conventions:**
|
||
- Project-dir: `{project_dir}/plan.md`
|
||
- Legacy: `.claude/plans/trekplan-{date}-{slug}.md`
|
||
|
||
**Frontmatter schema:**
|
||
|
||
`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.8"` (current), `"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 only** (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, 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.
|
||
|
||
**Validation strategy:**
|
||
|
||
The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrator) **must** run `plan-validator --strict` before handing the plan to plan-critic. `--validate` mode of `/trekexecute` runs the same check + `progress-validator`.
|
||
|
||
| Code | Meaning | Recovery |
|
||
|---|---|---|
|
||
| `PLAN_FORBIDDEN_HEADING` | Narrative drift detected | Rewrite using literal Phase 5 template |
|
||
| `PLAN_NO_STEPS` | No `### Step N:` headings | Plan is empty; restart |
|
||
| `PLAN_STEP_NUMBERING` | Steps skip a number | Renumber sequentially |
|
||
| `PLAN_MANIFEST_COUNT_MISMATCH` | Some step lost its manifest | Add missing manifest |
|
||
| `MANIFEST_MISSING` | Specific step has no manifest YAML | Add Manifest block |
|
||
| `MANIFEST_MISSING_KEY` | Manifest is missing a required key | Add the key |
|
||
| `MANIFEST_PATTERN_INVALID` | `commit_message_pattern` does not compile | Check escaping (`\\(` not `\(` in YAML double-quoted strings) |
|
||
| `PLAN_VERSION_MISMATCH` | `plan_version` other than 1.7 / 1.8 | 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 | 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 |
|
||
|
||
**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, and no BLOCKER is open (open majors are warnings); **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`) and `warnings` (`PLAN_GATE_OPEN_MAJOR`); **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. The verdict is a trace of the review, not a signature.
|
||
|
||
**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).
|
||
|
||
**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.
|
||
|
||
**Validation strategy:** `progress-validator.mjs` runs at:
|
||
1. `/trekexecute --validate` (alongside plan-validator)
|
||
2. `/trekexecute --resume` entry (must pass `checkResumeReadiness`)
|
||
3. `pre-compact-flush` hook (drift check before compaction; never blocks)
|
||
|
||
**Drift detection:** the `pre-compact-flush` hook compares `progress.steps[N].commit` against `git log --oneline {session_start_sha}..HEAD`. If git reality has progressed past the recorded `current_step`, the hook updates progress.json atomically (`tmp + rename`, monotonic only) before allowing compaction. This guards against the documented P0 drift in `docs/trekexecute-v2-observations-from-config-audit-v4.md`.
|
||
|
||
**Versioning:** `schema_version: "1"` is current. Future bump (e.g. `"2"`) should add a backward-compat read path that downgrades unknown fields to warnings.
|
||
|
||
**Failure modes:**
|
||
- `PROGRESS_PARSE_ERROR` → JSON corruption; resume halts
|
||
- `PROGRESS_SCHEMA_MISMATCH` → unknown schema version; resume halts
|
||
- `PROGRESS_MISSING_FIELD` → required top-level field absent; resume halts
|
||
- `PROGRESS_STEP_RANGE` → `current_step` outside `[0, total_steps]`; resume halts
|
||
- `PROGRESS_ALREADY_DONE` → `status === completed`; nothing to resume
|
||
- `PROGRESS_STEP_COUNT_MISMATCH` → warning; not a blocker
|
||
- `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
|
||
|
||
---
|
||
|
||
## 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 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) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security-review`.
|
||
|
||
**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). 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 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
|
||
|
||
---
|
||
|
||
## Handover 7 — `.session-state.local.json`
|
||
|
||
**Handover 7 enables zero-friction multi-session resumption.** Where Handover 5 (`progress.json`) makes a single execute run resumable after a crash inside that session, Handover 7 makes a *multi-session* plan resumable across fresh Claude Code chats. The state file is the contract; any session-end mechanism may write it; `/trekcontinue` only reads.
|
||
|
||
**Producer:**
|
||
- `/trekexecute` Phase 8 (canonical convergence — every completed/failed/stopped/partial run that reaches the final report)
|
||
- `/trekexecute` Phase 2.55 (Check 1 — dirty-tree pre-flight stop)
|
||
- `/trekexecute` Phase 4 (entry-condition stop)
|
||
- `/trekendsession` (informal multi-session helper — Step 9 of v3.3.0)
|
||
- *Future:* `graceful-handoff` v2.2 may dual-write here as part of its session-rescue artifact (additive — extra fields tolerated, see Body invariants).
|
||
- `hooks/scripts/pre-compact-flush.mjs` *refreshes* `updated_at` on existing state files (status `in_progress` or `partial` only). Never creates the file; never changes status or owned fields.
|
||
|
||
**Consumer:** `/trekcontinue` (read-only). Reads the file, validates it, narrates a 3-line summary, then begins executing the next session by reading `next_session_brief_path`.
|
||
|
||
**Path conventions:**
|
||
- Per-project: `.claude/projects/{YYYY-MM-DD}-{slug}/.session-state.local.json` — one file per project directory.
|
||
- Gitignored at the plugin level via `*.local.json` (added in v3.3.0). State files MUST NOT be committed — they may contain absolute project paths and label strings that vary per-machine.
|
||
- For `--session N` parallel multi-session runs the parent's Phase 8 aggregate write IS the canonical state. Child session writes (inside their worktrees) are ephemeral; the worktree is cleaned up after merge so child state is intentionally discarded.
|
||
|
||
**Frontmatter schema:** N/A — file is JSON, not Markdown. Top-level keys:
|
||
|
||
| Field | Type | Required | Allowed values | Notes |
|
||
|---|---|---|---|---|
|
||
| `schema_version` | number | yes | `1` (current) | Bump on breaking changes only |
|
||
| `project` | string | yes | absolute or repo-relative path | Project directory containing brief/plan |
|
||
| `next_session_brief_path` | string | yes | path to a brief or session-spec | Validator soft-checks file existence (warning, not error) |
|
||
| `next_session_label` | string | yes | human-readable label | e.g. "Session 2b" or "Continue" |
|
||
| `status` | string | yes | `in_progress \| partial \| failed \| stopped \| completed` | Mirrors progress.json status. `completed` triggers SESSION_STATE_NOT_RESUMABLE warning (valid:true) |
|
||
| `updated_at` | string | yes | ISO-8601 timestamp | Refreshed by pre-compact-flush on resumable statuses |
|
||
|
||
**Body invariants:** N/A (JSON).
|
||
|
||
**Forward-compat — drift-WARN principle:** Unknown top-level keys are **silently tolerated**. The validator does not warn on extras. This is a load-bearing decision: it lets future writers (graceful-handoff v2.2, custom plugin extensions) add metadata fields without breaking `/trekcontinue`. Mirrors Handover 3's discovery-only, drift-WARN posture.
|
||
|
||
**Validation strategy:**
|
||
|
||
| Layer | When | What |
|
||
|---|---|---|
|
||
| JSON parse | every read | `JSON.parse` → `SESSION_STATE_PARSE_ERROR` on failure |
|
||
| Required fields | every read | All six top-level keys present → `SESSION_STATE_MISSING_FIELD` on absence |
|
||
| Schema version | every read | Numeric `1` → `SESSION_STATE_SCHEMA_MISMATCH` otherwise |
|
||
| Status enum | every read | Must be one of the five values → `SESSION_STATE_INVALID_STATUS` otherwise |
|
||
| Resumability | every read | `completed` emits `SESSION_STATE_NOT_RESUMABLE` warning but valid:true |
|
||
| Path shape | every read | `next_session_brief_path` must be non-empty string → `SESSION_STATE_INVALID_PATH` otherwise |
|
||
| Timestamp shape | every read | `updated_at` parses via `Date.parse` → `SESSION_STATE_INVALID_TIMESTAMP` otherwise |
|
||
| Unknown keys | every read | Tolerated silently (drift-WARN forward-compat) |
|
||
|
||
The validator (`lib/validators/session-state-validator.mjs`) exposes the standard CLI: `node lib/validators/session-state-validator.mjs --json <path>`. Returns `{valid, errors[], warnings[], parsed}`. Exit code 0 on valid, 1 on invalid, 2 on usage error.
|
||
|
||
**Versioning:** Current is `1` (number). Schema is **additive only** — new optional fields land without bumping schema_version (forward-compat tolerates them). A breaking change (renaming a field, narrowing the status enum) requires bumping schema_version to `2`, adding migration support in the validator, and following the breaking-change protocol above.
|
||
|
||
**Failure modes:**
|
||
- `SESSION_STATE_NOT_FOUND` → `/trekcontinue` exits with cold-start message ("no active multi-session project here; start with `/trekbrief` or `/trekplan`")
|
||
- `SESSION_STATE_PARSE_ERROR` → halt with structured error; user fixes JSON
|
||
- `SESSION_STATE_MISSING_FIELD` → halt; suggests running validator directly
|
||
- `SESSION_STATE_SCHEMA_MISMATCH` → halt; future `1` → `2` migration path will warn instead
|
||
- `SESSION_STATE_INVALID_STATUS` → halt; protects against typo'd writers
|
||
- `SESSION_STATE_NOT_RESUMABLE` → warning; `/trekcontinue` exits cleanly with "no further sessions to resume; project complete"
|
||
- Validator failures during writer Phase 8 emit a stderr warning but DO NOT block the session-end report. `progress.json` remains the authoritative record of what was attempted.
|
||
|
||
### § Lifecycle
|
||
|
||
The state file follows a producer/consumer separation that keeps responsibilities narrow and the contract observable.
|
||
|
||
**Producer/consumer arbeidsdeling:**
|
||
|
||
| Role | Owners | Phase / location |
|
||
|---|---|---|
|
||
| Producer (writes the state file) | `/trekexecute` | Phase 8 (canonical), Phase 2.55 (dirty-tree pre-flight stop), Phase 4 (entry-condition stop) |
|
||
| Producer (informal multi-session helper) | `/trekendsession` | Phase 3 — writes the same schema for ad-hoc handovers that don't run through executor |
|
||
| Refresher (touch only) | `hooks/scripts/pre-compact-flush.mjs` | Updates `updated_at` only; never creates the file; never changes `status` or any owned field; only acts when `status` is `in_progress` or `partial` |
|
||
| Consumer | `/trekcontinue` | Phase 2 — reads, validates, narrates a 3-line summary, then begins executing the next session |
|
||
|
||
**Stale-file principle (SC-5):** When `status === 'completed'`, the state file and its sibling `NEXT-SESSION-PROMPT.local.md` represent finished work and SHOULD be removed. Removal is **operator-invoked** via `/trekcontinue --cleanup --confirm <project-dir>`; the plugin does NOT auto-cleanup. Stale state is actively harmful — it can mislead a fresh `/trekcontinue` into resuming a project that's already shipped. The `--cleanup` gate refuses to act unless `validateSessionState({...}).valid === true && parsed.status === 'completed'`. There is no force flag.
|
||
|
||
**Frontmatter contract for `NEXT-SESSION-PROMPT.local.md`:** Producers MUST write a YAML frontmatter block on the prompt file with at minimum:
|
||
|
||
- `produced_by:` — string identifying the producer (e.g. `trekplan-A4-session`, `trekexecute-phase-8`, `trekendsession`)
|
||
- `produced_at:` — ISO-8601 timestamp of when the file was written
|
||
|
||
The `next-session-prompt-validator` (`lib/validators/next-session-prompt-validator.mjs`) cross-checks `produced_at` against the sibling state file's `updated_at` and emits a `NEXT_SESSION_PROMPT_INCONSISTENT` error when the prompt is older than the state — that means the prompt has not been refreshed for the current session and is stale. Files **without** any frontmatter are tolerated (warning, not error) for backwards compatibility with v3.3.x and earlier hand-rolled prompt files; this is consistent with Handover 3's drift-WARN posture.
|
||
|
||
**Idempotency:** `--cleanup --confirm` is safe to re-run. If only one of the two files (state file, prompt file) was previously deleted, the second run reports the partial state ("state file: not found, prompt file: removed") but does not auto-recover or re-create. There is no rollback. Operators choosing to re-create a project after `--cleanup` should re-run `/trekbrief` from scratch.
|
||
|
||
---
|
||
|
||
## 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.
|