feat(plan): plan_version 1.9 is the current target, the file first on every line [skip-docs]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 16:42:28 +02:00
commit 64e6771c8b
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
14 changed files with 219 additions and 34 deletions

View file

@ -32,7 +32,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
|---------|-------------|--------------------|
| `/trekbrief` | Brief — one question round sized by class (`ceremony: small\|bounded\|architectural`, heaviest in doubt, said aloud, only moved up) produces a task brief with explicit research plan; ends with the operator explicitly approving `## Intent` + `## Goal` («høres bra ut» is not approval), logged in the target repo's hash-chained approval log and committed (the brief too when `artifact_store: git`) (`--approve <dir>` does only the approval, then writes the trekbrief record); optionally orchestrates the pipeline | session |
| `/trekresearch` | Research — deep local + external research, produces structured research brief. Opt-in `--engine {swarm\|deep-research}` delegates the external phase to Claude Code's built-in `/deep-research` workflow (swarm default). The external agents preload the `researching-sources` skill and are told it by name at spawn | session |
| `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session |
| `/trekplan` | Plan — brief-reviewer, explore, plan, review. Requires `--brief` or `--project`; halts unless the brief's intent is approved (hash-bound, stale on edit). Auto-discovers `architecture/overview.md` if present. Plan 1.9: the file first on every line — every `Files:` entry and `Test first:` `File:` line opens with its backticked path(s). From 1.8: `Test first:` per step + one `End-to-end:` step; a design part for `ceremony: architectural` (absent = architectural); critic loop of max 3 rounds, recorded in the plan, and `plan-gate.mjs` (0 go / 1 stop / 2 could not run) decides; open minors after it are listed for the operator to pick | session |
| `/trekexecute` | Execute — disciplined plan/session-spec executor: refuses a plan whose logged approval does not cover its current sha256 (K2); one fresh `step-implementer` helper per plan step, one controller that reads the diff (spec + quality verdicts), proof of red / floor guard / step check (K9) / `Regression:` (K10) / done line as commands, fix rounds that go up (max 5: helper → plan → operator), architecture rules (K5) + plan-file sweep (K6) after the last step, then its own review (`/simplify` → `/trekreview` → verdict gate, K7; BLOCK keeps the merge into main closed), closed stop list + decision log in the final report | session |
| `/trekreview` | Review — a gate: Claude Code's `/code-review` + `/security-review` as engine, then Voyage's brief control (which stops); writes `review.md` with a required verdict that `lib/review/verdict-gate.mjs` decides on (0 ALLOW/WARN · 1 BLOCK · 2 never green); proposes recurring finding classes as project memory, line by line (Handover 6) | session |
| `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | session |

View file

@ -48,7 +48,7 @@ Every artifact lives in one project directory: `.claude/projects/{YYYY-MM-DD}-{s
|---|---|---|
| `/trekbrief` | **Capture intent** — intent, goal, non-goals, success criteria, and a research plan with explicit topics. Interactive only. | `brief.md` (task brief) |
| `/trekresearch` | **Gather context** — code state, external docs, community, risk. Makes NO build decisions. | `research/NN-slug.md` (research brief) |
| `/trekplan` | **Transform intent into an executable contract** — per-step YAML manifest, regex-validated checkpoints, verifiable paths. Plan-critic is a hard gate. Auto-discovers `architecture/overview.md` as priors when an opt-in upstream architect plugin (not bundled) is installed. | `plan.md` with Manifest blocks + `plan_version: 1.7` |
| `/trekplan` | **Transform intent into an executable contract** — per-step YAML manifest, regex-validated checkpoints, verifiable paths. Plan-critic is a hard gate. Auto-discovers `architecture/overview.md` as priors when an opt-in upstream architect plugin (not bundled) is installed. | `plan.md` with Manifest blocks + `plan_version: 1.9` |
| `/trekexecute` | **Execute the contract disciplined** — fresh verification, independent manifest audit, honest reporting. Does NOT compensate for weak plans — escalates. | `progress.json` + structured report + manifest-audit status |
| `/trekreview` | **Close the loop** — independent post-hoc reviewer reads `brief.md` and the diff produced by execute, runs brief-conformance + code-correctness reviewers in parallel, dedups via Judge Agent. Severity-tagged findings (Critical/High/Medium/Low/Info) feed back into planning via Handover 6. | `review.md` (`type: trekreview`) with stable 40-char hex finding-IDs |
@ -318,7 +318,7 @@ Every plan includes:
- **Critic Verdict** — the last critic round's machine-readable verdict, written by `plan-gate.mjs --record`
Every implementation step includes:
- **Test first:** — the test the step makes pass (required from `plan_version 1.8`)
- **Test first:** — the test the step makes pass (required from `plan_version 1.8`); from `plan_version 1.9` each `Files:` entry and `Test first:` `File:` line opens with its backticked path(s), the file first on every line
- **Interface in:** / **Interface out:** / **Review focus:** — optional: what the step consumes and produces, and the untested inputs it pins to a test
- **Regression:** — optional: one runnable suite command (`npm test`, no pipe, no `&&`) run green before the checkpoint, while `Verify:` stays the step's own red-first test; the validator refuses one that can never run
- **On failure:** — what to do when verification fails; it opens with `revert`, `retry`, `skip` or `escalate`, or the validator warns
@ -895,13 +895,13 @@ Validators (zero npm deps, hand-rolled YAML subset):
|---|---|
| `lib/validators/brief-validator.mjs` | brief.md frontmatter + state machine (research_topics + status coherence) + body sections |
| `lib/validators/research-validator.mjs` | research-brief frontmatter (confidence ∈ [0,1], dimensions ≥ 1) + body sections; `--dir` mode validates a whole `research/` folder |
| `lib/validators/plan-validator.mjs` | wraps plan-schema + manifest-yaml; enforces v1.7 step heading, manifest count match, and forbidden-narrative-form denylist (`### Fase/Phase/Stage/Steg N`) — replaces the Phase 5.5 grep checks |
| `lib/validators/plan-validator.mjs` | wraps plan-schema + manifest-yaml; enforces v1.9 step heading, manifest count match, and forbidden-narrative-form denylist (`### Fase/Phase/Stage/Steg N`) — replaces the Phase 5.5 grep checks |
| `lib/validators/progress-validator.mjs` | progress.json shape (schema_version, status enum, current_step in range) + resume-readiness check |
| `lib/validators/architecture-discovery.mjs` | EXTERNAL CONTRACT — drift-WARN, never drift-FAIL. Discovers `architecture/overview.md` (owned by an opt-in upstream architect plugin, not bundled) and tolerates non-canonical filenames with warnings. |
Each module exposes a CLI: `node lib/validators/<name>.mjs --json <path>` returns structured `{valid, errors, warnings, parsed}`. Commands invoke the CLI as their schema check.
A doc-consistency test (`tests/lib/doc-consistency.test.mjs`) pins prose-vs-source invariants — the agent table in `CLAUDE.md` must match the `agents/*.md` file count, every command's frontmatter `name:` must match its filename, and `templates/plan-template.md` must declare `plan_version: 1.7`.
A doc-consistency test (`tests/lib/doc-consistency.test.mjs`) pins prose-vs-source invariants — the agent table in `CLAUDE.md` must match the `agents/*.md` file count, every command's frontmatter `name:` must match its filename, and `templates/plan-template.md` must declare `plan_version: 1.9`.
Borrowed pattern from `llm-security` (commit `97c5c9d`); extending the plugin should preserve the invariants the test pins.

View file

@ -134,7 +134,7 @@ questions, which defeats the purpose.
- Are there assertions about behavior that have no corresponding test?
- Do the verification steps cover error paths, not just happy paths?
- Are the verification commands correct and runnable?
- `plan_version: 1.8`: does every step's `Test first:` name a test that would
- `plan_version` 1.8 or later: does every step's `Test first:` name a test that would
FAIL before the step and pass after it (not a test that already passes)?
- Does any step consist of only tests — every file in its `Files:` a test —
for behaviour an earlier step delivered? Its tests pass at its start point,

View file

@ -215,14 +215,19 @@ Write a comprehensive implementation plan including:
criterion must be an executable command or observable condition
- **Estimated Scope** — file counts and complexity
**Plan-version header:** Include `plan_version: 1.8` and the brief's
**Plan-version header:** Include `plan_version: 1.9` and the brief's
`ceremony:` in the metadata line below the title. This signals to trekexecute
that the plan includes per-step verification manifests and enables strict
audit mode. Plans without this marker are treated as legacy v1.6 with
synthesized minimal manifests. 1.8 adds `Test first:` on every step, one
`End-to-end:` step, the optional `Interface in:` / `Interface out:` /
`Review focus:` fields and, for an architectural plan, `## Design`
(`commands/trekplan.md` Phase 8 is the full text). 1.7 plans stay valid.
(`commands/trekplan.md` Phase 8 is the full text). 1.9 adds the file first on
every line: each `Files:` entry and each `Test first:` `File:` line opens with
its backticked path(s), and everything after the first other text is
description, so every path stands before any annotation
(`` `src/a.mjs`, `src/b.mjs` (new) ``, never `` `src/a.mjs` (new), `src/b.mjs` ``).
1.8 and 1.7 plans stay valid.
### Mandatory step format — copy this exactly
@ -335,7 +340,7 @@ Internally the validator enforces (same checks as before, now in one place):
- Step count == manifest count
- Zero forbidden narrative headings (`### Fase N`, `### Phase N`, `### Stage N`,
`### Steg N`)
- `plan_version` 1.7 or 1.8 declared (warning only if older / missing)
- `plan_version` 1.7, 1.8 or 1.9 declared (warning only if older / missing)
- From 1.8: `Test first:` per step, an `End-to-end:` step, non-empty optional
fields, a valid `ceremony`, and the design part for an architectural plan

View file

@ -653,12 +653,12 @@ Do NOT write this synthesis to disk. It is internal working context only.
> ```
> ````
>
> **plan_version 1.8 additions** (the validator enforces them; 1.7 plans stay
> **plan_version 1.8 or later additions** (the validator enforces them; 1.7 plans stay
> valid): `Test first:` with `File:` and `Verifies:` on EVERY step; exactly the
> optional fields `Interface in:`, `Interface out:`, `Review focus:` (never
> empty when present), plus `Regression:` below; at least one step with `End-to-end:` and a backticked
> command — the "run the thing" step; the header line carries
> `` `plan_version: 1.8` · `ceremony: <class>` ``; an architectural plan carries
> `` `plan_version: 1.9` · `ceremony: <class>` ``; an architectural plan carries
> `## Design` (see "Design part" below); and every check the run executes can
> run — each `## Verification` criterion, the first backticked span of each
> `Verify:` (the step's test command) and each `End-to-end:` command pass the
@ -667,6 +667,21 @@ Do NOT write this synthesis to disk. It is internal working context only.
> `PLAN_STEP_VERIFY_NOT_RUNNABLE`, `PLAN_END_TO_END_NOT_RUNNABLE`,
> `PLAN_END_TO_END_NOT_VERIFIED`).
>
> **plan_version 1.9 additions** (the validator enforces them on a 1.9 plan;
> 1.8 and 1.7 plans are read as before, with the warning
> `PLAN_STEP_PATH_UNTICKED` for a path outside backticks): the file first on
> every line. Each `Files:` entry (the field's first line and each list item)
> and each `Test first:` `File:` line opens with its backticked path(s),
> separated only by commas or spaces; everything from the first other text on
> is description and is never read. So every path stands before any annotation:
> `` `src/a.mjs`, `src/b.mjs` (new) ``, never `` `src/a.mjs` (new), `src/b.mjs` ``
> (the second path would be description); two test files share one line,
> `` File: `a.test.mjs`, `b.test.mjs` (new) ``. A 1.9 step always names a file:
> an entry that does not open with a backticked path, `none`, or an empty
> `Files:` is `PLAN_STEP_PATH_NOT_FIRST`; a leading span that is no path (a
> call, an option, a URL) is `PLAN_STEP_PATH_NOT_A_PATH`. The canonical step
> above already opens each line with its path.
>
> **Two step rules at every plan_version** (no version bump; a plan without
> the field is unchanged): the optional `Regression:` field (K10) holds ONE
> backticked command the criteria runner allows, with no pipe and no `&&` —

View file

@ -12,7 +12,7 @@ Each artifact carries an explicit version field. Schema bumps are coordinated:
|---|---|---|
| `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) |
| `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) |
@ -215,7 +215,7 @@ The validator (`lib/validators/architecture-discovery.mjs`) is intentionally min
**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.
**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.
---
@ -237,7 +237,7 @@ 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) |
| `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
@ -260,7 +260,7 @@ prose form (relaxed in S26 so the template's prose emission parses).
- 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):
5. **plan_version 1.8 or later** (1.7 plans are not held to these):
- every step has `Test first:` with `File:` and `Verifies:` sub-bullets;
- at least one step has `End-to-end:` with a backticked command — the "run the thing" step;
- the optional step fields `Interface in:`, `Interface out:`, `Review focus:` are never empty when present (consumers tolerate their absence);
@ -269,7 +269,13 @@ prose form (relaxed in S26 so the template's prose emission parses).
7. **Every plan_version, no version bump** (a plan without the field validates unchanged; voyage-lekerepo trial 2, 2026-10-08):
- **K10 — `Regression:`** (optional step field): ONE backticked command the criteria runner's allowlist runs, with no pipe and no `&&` (first backticked span, as for `Verify:`). The executor runs it green before the step's checkpoint; a step without it runs nothing new. `Verify:` stays the step's own red-first test. One that can never run is `PLAN_STEP_REGRESSION_NOT_RUNNABLE` (an error; a warning under `--soft`).
- **K11 — `On failure:`** opens with one of `revert`, `retry`, `skip`, `escalate` (case-insensitive; a leading `**`, `_`, backtick or `-` is ignored, other leading characters are not; `onFailureAction` in `lib/parsers/plan-schema.mjs`), the words `/trekexecute` reads; any other opening is the warning `PLAN_STEP_ON_FAILURE_UNKNOWN`. A missing `On failure:` is unchanged (the executor defaults to `escalate`).
- **A step of only tests** (order 10, voyage-lekerepo trial 3, 2026-10-09): when every file in a step's `Files:` is a test (its `Test first:` file, or a path `lib/util/test-path.mjs` reads as a test: under `tests/`, `test/`, `__tests__/`, `spec/`, `specs/`, or named `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`, `test_*.py`, but never a document such as `docs/specs/api.md`; `stepTestsOnly` in `lib/parsers/plan-schema.mjs`), the warning is `PLAN_STEP_TESTS_ONLY`: the behaviour its tests cover exists at its start point, so it can never be proven red. A step without `Files:` is not judged. At run time `red-proof.mjs --plan <plan.md> --step <N>` puts the same reading on a `not-red-at-base` as `tests_only`: the plan's `Files:` alone decides, and the step's diff is not read (operator 2026-10-11), and `/trekexecute` sends a `tests_only: true` step straight up to the plan.
- **A step of only tests** (order 10, voyage-lekerepo trial 3, 2026-10-09): when every file in a step's `Files:` is a test (a file on its `Test first:` `File:` line, or a path `lib/util/test-path.mjs` reads as a test: under `tests/`, `test/`, `__tests__/`, `spec/`, `specs/`, or named `*.test.*`, `*.spec.*`, `*_test.*`, `*_spec.*`, `test_*.py`, but never a document such as `docs/specs/api.md`; `stepTestsOnly` in `lib/parsers/plan-schema.mjs`), the warning is `PLAN_STEP_TESTS_ONLY`: the behaviour its tests cover exists at its start point, so it can never be proven red. A step without `Files:` is not judged. At run time `red-proof.mjs --plan <plan.md> --step <N>` puts the same reading on a `not-red-at-base` as `tests_only`: the plan's `Files:` alone decides, and the step's diff is not read (operator 2026-10-11), and `/trekexecute` sends a `tests_only: true` step straight up to the plan.
8. **plan_version 1.9 only** (1.8 and 1.7 plans are read as before, with one warning, `PLAN_STEP_PATH_UNTICKED`, for a path their reader found outside backticks; decision 16, operator 2026-10-11) — the file first on every line:
- the entries of a step's `Files:` field are its first non-empty line and each list-item line (`-`, `*`, `+`, `1.`, `1)`); any other line continues the entry above it;
- 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) ``);
- an entry that does not open with a backticked span, `none`, and an empty or absent `Files:` are `PLAN_STEP_PATH_NOT_FIRST` (a 1.9 step always names a file); a leading span that is no path (a call, an option, a URL) is `PLAN_STEP_PATH_NOT_A_PATH`. Both are errors under `--strict`, warnings under `--soft`;
- K6 (`lib/verification/plan-scope-sweep.mjs`) follows the grammar from 1.9: it reads a 1.9 plan's `Files:` and `Test first:` `File:` line by it (plan-schema `entryPaths` / `testFirstFiles`), and so does the red proof's `tests_only` reading (`lib/verification/red-proof.mjs`); below 1.9 both keep their earlier readers.
**Validation strategy:**
@ -284,7 +290,7 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
| `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_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 | Add the test the step makes pass |
@ -310,7 +316,7 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
**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).
**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.

View file

@ -8,7 +8,9 @@
// plan_version 1.8 adds per-step field parsing (`parseStepFields`), the
// ceremony class the plan was made under (`small | bounded | architectural`,
// absent = architectural) and the design-part helpers (`splitSections`,
// `findModuleCycle`). The rules that use them live in plan-validator.mjs.
// `findModuleCycle`). plan_version 1.9 (the current target) adds the grammar
// of the `Files:` field and the `Test first:` `File:` line, the file first on
// every line (`entryPaths`). The rules that use them live in plan-validator.mjs.
import { ok, fail, issue } from '../util/result.mjs';
import { parseDocument } from '../util/frontmatter.mjs';
@ -135,7 +137,7 @@ export function validatePlanHeadings(text, opts = {}) {
// ---- plan_version 1.8 ------------------------------------------------------
export const PLAN_VERSION_CURRENT = '1.8';
export const PLAN_VERSION_CURRENT = '1.9';
// 1.7 plans stay valid: the 1.8 rules apply only from 1.8 on, the 1.9 `Files:`
// grammar only from 1.9 on (see "plan_version 1.9" below).
export const PLAN_VERSIONS_SUPPORTED = Object.freeze(['1.7', '1.8', '1.9']);

View file

@ -1,5 +1,5 @@
<!--
The design part of a plan (plan_version 1.8). /trekplan Phase 8 writes it as
The design part of a plan (plan_version 1.8 or later). /trekplan Phase 8 writes it as
the plan's `## Design` section, between `## Context` and
`## Implementation Plan`, ONLY when the brief's `ceremony:` is
`architectural` (an absent field means architectural: heaviest on doubt).
@ -8,7 +8,8 @@ the plan's `## Design` section, between `## Context` and
The design lives in the PLAN, never in the brief: the brief format is a public
contract (Handover 1), the plan contract is Voyage's own (Handover 4).
`plan-validator --strict` checks, for an architectural 1.8 plan: the eight
`plan-validator --strict` checks, for an architectural plan at plan_version
1.8 or later: the eight
`###` subsections below exist, the module map is a mermaid graph with no
cycle, every architecture rule names its enforcer, a checked rule names the
step that proves it, and an advisory rule names its `.claude/rules/` file.

View file

@ -18,7 +18,7 @@ source_findings:
> **Plan quality: {grade}** ({score}/100) — {APPROVE | APPROVE_WITH_NOTES | REVISE | REPLAN}
>
> Generated by trekplan v{version} on {YYYY-MM-DD} — `plan_version: 1.8` · `ceremony: {small | bounded | architectural}`
> Generated by trekplan v{version} on {YYYY-MM-DD} — `plan_version: 1.9` · `ceremony: {small | bounded | architectural}`
## Context
@ -71,7 +71,7 @@ their relationships, and the data flow between them.*
Each step targets 1–2 files and one focused change, and is a vertical slice:
a narrow but complete path through every layer it touches, verifiable on its
own. Preparatory refactoring comes first, as its own steps. Every step has
`Test first:` (plan_version 1.8 requires it), and one step runs the thing
`Test first:` (plan_version 1.8 or later requires it), and one step runs the thing
end to end (`End-to-end:`). The test for a behaviour belongs in the step that
builds it: a step of only tests for behaviour an earlier step delivered passes
at its start point and can never be proven red (the validator warns:
@ -89,6 +89,17 @@ runs green before the checkpoint, on a step that can break existing tests;
never empty. `On failure:` opens with `revert`, `retry`, `skip` or `escalate`,
then the specifics.
From plan_version 1.9 the rule is the file first on every line: each `Files:`
entry and each `Test first:` `File:` line opens with its backticked path(s),
separated only by commas or spaces, and everything after the first other text
is description that is never read. A 1.9 step always names a file (`none` or
an empty `Files:` is refused). In `Files:`,
every path stands before any annotation: `` `src/a.mjs`, `src/b.mjs` (new) ``,
never `` `src/a.mjs` (new), `src/b.mjs` `` (in 1.9 that second path is
description).
Several test files share one `File:` line the same way:
`` File: `a.test.mjs`, `b.test.mjs` *(new)* ``.
### Step 1: {description}
- **Files:** `path/to/file.ts`
@ -146,8 +157,8 @@ then the specifics.
*A project without tests still gets `Test first:` on every step: the first step
adds the test runner (or a script under `tests/`) the later steps use. A plan
without an `End-to-end:` step is invalid at plan_version 1.8 — without it the
result can only be "not verified end to end", never green.*
without an `End-to-end:` step is invalid at plan_version 1.8 or later — without
it the result can only be "not verified end to end", never green.*
### Manifest — objective completion predicate

View file

@ -139,9 +139,9 @@ test('every command frontmatter name matches its filename', () => {
}
});
test('templates/plan-template.md declares plan_version: 1.8', () => {
test('templates/plan-template.md declares plan_version: 1.9', () => {
const tpl = read('templates/plan-template.md');
assert.match(tpl, /plan_version:\s*['"]?1\.8['"]?/);
assert.match(tpl, /plan_version:\s*['"]?1\.9['"]?/);
});
test('HANDOVER-CONTRACTS.md locates plan_version in the plan header line, not frontmatter (S28)', () => {
@ -162,8 +162,8 @@ test('HANDOVER-CONTRACTS.md locates plan_version in the plan header line, not fr
// "commands/trekexecute.md still parses v1.7 plan schema" was an OR-chain
// (`includes('plan_version') || includes('Step N:') || includes('### Step ')`)
// that is ~always true and asserts almost nothing. The real v1.7 coverage lives
// in the structural pin "templates/plan-template.md declares plan_version: 1.8"
// (above; 1.7 plans stay valid) + the plan-validator / plan-schema behavior tests.
// in the structural pin "templates/plan-template.md declares plan_version: 1.9"
// (above; 1.8 and 1.7 plans stay valid) + the plan-validator / plan-schema behavior tests.
test('settings.json has only known top-level scopes after Spor 0 cleanup', () => {
const cfg = JSON.parse(read('settings.json'));

View file

@ -113,10 +113,10 @@ test('Handover 4 documents every PLAN_* code the validator and the gate emit', (
assert.deepEqual(missing, [], `Handover 4 does not document: ${missing.join(', ')}`);
});
test('Handover 4 states 1.8 as current and 1.7 as still valid', () => {
test('Handover 4 states 1.9 as current and 1.8 and 1.7 as still valid', () => {
const contract = read('docs/HANDOVER-CONTRACTS.md');
assert.match(contract, /\| `plan\.md` \| `plan_version` \(plan header line\) \| `1\.8` \(1\.7 still valid\) \|/);
assert.match(contract, /`"1\.8"` \(current\), `"1\.7"` \(valid\)/);
assert.match(contract, /\| `plan\.md` \| `plan_version` \(plan header line\) \| `1\.9` \(1\.8 and 1\.7 still valid\) \|/);
assert.match(contract, /`"1\.9"` \(current\), `"1\.8"` \(valid\), `"1\.7"` \(valid\)/);
});
test('the verification CLIs credit what they borrow and say they install nothing', () => {

View file

@ -150,5 +150,5 @@ test('extractPlanVersion — recognizes the canonical plan template (regression
// The real generated artifact: no test exercised the parser against the
// actual template, which is why defect S26 went unnoticed.
const tpl = readFileSync(new URL('../../templates/plan-template.md', import.meta.url), 'utf-8');
assert.equal(extractPlanVersion(tpl), '1.8');
assert.equal(extractPlanVersion(tpl), '1.9');
});

View file

@ -0,0 +1,141 @@
// tests/lib/plan-version-1-9-docs.test.mjs
// plan_version 1.9 is the current target, and the contract docs follow it:
// Handover 4, the plan template and CLAUDE.md name 1.9 and its rule, "the file
// first on every line"; the template, /trekplan and the planning orchestrator
// say every path stands before any annotation; and the rules introduced at 1.8,
// which still bind 1.8 and 1.9 plans, say "1.8 or later" in the sentence that
// carries each of them (never "1.9", which would release a 1.8 plan from them).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { PLAN_VERSION_CURRENT } from '../../lib/parsers/plan-schema.mjs';
import { validatePlanContent } from '../../lib/validators/plan-validator.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf-8');
const RULE = 'file first on every line';
const ANNOTATION = 'every path stands before any annotation';
const NEW_CODES = ['PLAN_STEP_PATH_NOT_FIRST', 'PLAN_STEP_PATH_NOT_A_PATH', 'PLAN_STEP_PATH_UNTICKED'];
// Sentence boundaries: a full stop or question mark before whitespace, a blank
// line, or the start of a list item. "1.8" and ".mjs" are no boundary.
const BOUNDARY = /[.?!](?=\s)|\n[ \t>]*\n|\n[ \t>]*[-*] /g;
/**
* The sentence of `text` that holds the first match of `anchor`: from the
* boundary before the match to the first boundary after it.
*/
function sentence(text, anchor, label) {
const m = anchor.exec(text);
assert.ok(m, `${label}: anchor ${anchor} not found`);
let start = 0;
let end = text.length;
for (const b of text.matchAll(BOUNDARY)) {
const bEnd = b.index + b[0].length;
if (bEnd <= m.index) start = bEnd;
else if (b.index >= m.index + m[0].length) { end = b.index; break; }
}
return text.slice(start, end);
}
function holds18OrLater(text, anchor, label) {
const s = sentence(text, anchor, label);
assert.match(s, /1\.8 or later/, `${label} must say "1.8 or later" in the sentence that carries the rule: ${JSON.stringify(s)}`);
assert.doesNotMatch(s, /1\.9/, `${label} is a 1.8 rule that still binds 1.8 plans; it must not say 1.9: ${JSON.stringify(s)}`);
}
const handover4 = () => {
const c = read('docs/HANDOVER-CONTRACTS.md');
const start = c.indexOf('## Handover 4');
assert.ok(start >= 0, '## Handover 4 missing');
return c.slice(start, c.indexOf('## Handover 5', start));
};
const claudeRow = () => {
const m = /^\| `\/trekplan` \|.*$/m.exec(read('CLAUDE.md'));
assert.ok(m, 'CLAUDE.md has no /trekplan row');
return m[0];
};
test('PLAN_VERSION_CURRENT is 1.9', () => {
assert.equal(PLAN_VERSION_CURRENT, '1.9');
});
test('Handover 4 names plan_version 1.9 and the "file first on every line" rule', () => {
const h4 = handover4();
assert.match(h4, /plan_version 1\.9/);
assert.ok(h4.includes(RULE), `Handover 4 must name the rule "${RULE}"`);
assert.match(h4, /`"1\.9"` \(current\), `"1\.8"` \(valid\), `"1\.7"` \(valid\)/);
});
test('the plan template declares plan_version 1.9 and states the rule', () => {
const t = read('templates/plan-template.md');
assert.ok(t.includes('`plan_version: 1.9` · `ceremony:'), 'the template header line names plan_version 1.9');
assert.ok(t.includes(RULE), `the template must state the rule "${RULE}"`);
});
test('CLAUDE.md /trekplan row names Plan 1.9 and the rule, and labels the Test first / End-to-end rules "From 1.8"', () => {
const row = claudeRow();
assert.match(row, /Plan 1\.9: the file first on every line/);
assert.match(row, /From 1\.8: `Test first:` per step \+ one `End-to-end:` step/);
assert.doesNotMatch(row, /Plan 1\.8:/);
});
test('the template, /trekplan and the planning orchestrator say every path stands before any annotation', () => {
for (const rel of ['templates/plan-template.md', 'commands/trekplan.md', 'agents/planning-orchestrator.md']) {
assert.ok(read(rel).includes(ANNOTATION), `${rel} must say "${ANNOTATION}"`);
}
});
test('the plan template: the Test first and End-to-end rules say "1.8 or later" in their own sentence', () => {
const t = read('templates/plan-template.md');
holds18OrLater(t, /Every step has\s+`Test first:`/, 'template Test first sentence');
holds18OrLater(t, /without an\s+`End-to-end:`\s+step is invalid/, 'template End-to-end sentence');
});
test('plan-critic: the Test first check says "1.8 or later" in its own sentence', () => {
holds18OrLater(read('agents/plan-critic.md'), /does every step's\s+`Test first:`\s+name a test/, 'plan-critic Test first check');
});
test('design template: the opening line and the --strict line say "1.8 or later"', () => {
const d = read('templates/design-template.md');
holds18OrLater(d, /The design part of a plan/, 'design template opening line');
holds18OrLater(d, /`plan-validator --strict` checks/, 'design template --strict line');
});
test('HANDOVER-CONTRACTS: the design-part line and invariant 5 say "1.8 or later"; invariant 7 reads the Test first File line', () => {
// The design-part line stands in Handover 3 (it tells the external slot from
// the plan's own design); invariants 5 and 7 stand in Handover 4.
holds18OrLater(read('docs/HANDOVER-CONTRACTS.md'), /A plan's own design/, 'HANDOVER-CONTRACTS design-part line');
const h4 = handover4();
holds18OrLater(h4, /\(1\.7 plans are not held to these\)/, 'Handover 4 invariant 5');
const bullet = sentence(h4, /\*\*A step of only tests\*\*/, 'Handover 4 invariant 7 tests-only bullet');
assert.ok(bullet.includes('a file on its `Test first:` `File:` line'),
`invariant 7 must say "a file on its \`Test first:\` \`File:\` line": ${JSON.stringify(bullet)}`);
assert.ok(!bullet.includes('its `Test first:` file,'), 'invariant 7 no longer says "its `Test first:` file"');
});
test('/trekplan: the 1.8 additions heading says "1.8 or later", and a separate 1.9 additions block follows it', () => {
const p = read('commands/trekplan.md');
const h18 = p.indexOf('**plan_version 1.8 or later additions**');
assert.ok(h18 >= 0, 'the heading reads "**plan_version 1.8 or later additions**"');
assert.ok(!p.includes('**plan_version 1.8 additions**'), 'the old 1.8-only heading is gone');
const h19 = p.indexOf('**plan_version 1.9 additions**');
assert.ok(h19 > h18, 'a separate "**plan_version 1.9 additions**" block stands after the 1.8 block');
const end = p.indexOf('\n>\n> **', h19);
const block = p.slice(h19, end < 0 ? undefined : end);
assert.ok(block.includes(ANNOTATION), `the 1.9 block says "${ANNOTATION}"`);
});
test('the template\'s own example steps validate as 1.9 with none of the new path codes', () => {
const r = validatePlanContent(read('templates/plan-template.md'));
assert.equal(r.parsed.planVersion, '1.9');
const found = [...r.errors, ...r.warnings].map((i) => i.code).filter((c) => NEW_CODES.includes(c));
assert.deepEqual(found, [], JSON.stringify([...r.errors, ...r.warnings].filter((i) => NEW_CODES.includes(i.code)), null, 2));
const soft = validatePlanContent(read('templates/plan-template.md'), { strict: false });
assert.deepEqual([...soft.errors, ...soft.warnings].map((i) => i.code).filter((c) => NEW_CODES.includes(c)), []);
});

View file

@ -35,6 +35,10 @@ function entryPaths(text) {
return schema.entryPaths(text);
}
test('1.9 is the current target: PLAN_VERSION_CURRENT === \'1.9\'', () => {
assert.equal(schema.PLAN_VERSION_CURRENT, '1.9');
});
test('1.9: a plan_version 1.9 plan is supported - no PLAN_VERSION_MISMATCH, and the builder plan validates strict', () => {
const r = validatePlanContent(buildPlan({ version: '1.9' }));
assert.equal(r.warnings.find((w) => w.code === 'PLAN_VERSION_MISMATCH'), undefined, JSON.stringify(r.warnings));