docs(plan): Phase 9, Handover 4 and CLAUDE.md name rule A, with door lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-16 01:34:51 +02:00
commit fc865ca07a
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
10 changed files with 181 additions and 13 deletions

View file

@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests
## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop
A new plan version, 1.9, in which every path in a step's `Files:` and on its
`Test first:` `File:` line stands first, in backticks, so the reader no longer
@ -15,6 +15,11 @@ failed on the code before it. Suite 2517 (2515 pass, 0 fail, 2 skipped),
measured by `npm test` on the third remediation's Step 2 commit
(`829da48`), the commit before this section.
The converging critic loop came after that measurement (decisions 43 and
44). From round 2 the plan gate counts a critic round by rule A, and both
reviewers state rule B. Suite 2592 (2590 pass, 0 fail, 2 skipped), measured
by `npm test` on this branch.
Nothing here asks anything of users. Older plans warn, they do not fail: a plan
below 1.9 is read as in 6.3.1, apart from the fixes under Fixed, and gets one
warning per step whose `Files:` names a path that is not backticked.
@ -36,6 +41,10 @@ warning per step whose `Files:` names a path that is not backticked.
- two-way: original_severity names what the kept finding asserted — only tests read the field.
- two-way: the workflow review fails closed when its coordinator returns nothing, and names every reviewer that did not run in allow_blocked_by — opt-in path only.
- two-way: below 1.8, when the plan names no test file, the helper and the executor give red proof the files the helper wrote (operator decision 41) — a revert leaves such a step unproven and unlocked.
- two-way: rule A in the plan gate (decision 43) — an older gate ignores the new verdict fields, and a revert counts every round fully.
- two-way: rule B and the security key in the reviewers (decision 43) — prose only; a revert restores the old rating.
- two-way: plan dedup passes the reviewers' marks and rule-B fields through, and its two fixes — a revert drops the fields and brings back the line-0 collapse and the crash.
- two-way: the post-compact hook test's timeout (decision 44) — test-side only.
### Added
@ -61,6 +70,28 @@ warning per step whose `Files:` names a path that is not backticked.
`brief_lowered_from` on any other finding, a mark on a reviewer finding, a
mark or an engine finding below its catalogue tier, or a mark with a missing
or unknown rule key.
- **The critic loop converges by rule A, counted by the plan gate**
(decision 43). Round 1 is unchanged. From round 2 on,
`plan-gate.mjs --diff` prints what changed since the text the last round
reviewed. Both reviewers get it and mark each previous finding by id. Then
`--record` counts the round: a new major that cites a plan line outside
that diff counts as minor. A blocker, a security finding and a finding
with no plan line count at full severity. A previous finding stays counted
until it is marked resolved. The gate keeps each recorded round's text in
the git directory, so a snapshot is never committed. A plan outside any git
repository keeps none, and its next round counts fully. Every new verdict
field is optional, and absent means round-1 behaviour, so an older verdict
block gives the same exit codes. Phase 9, Handover 4, the planning
orchestrator, CLAUDE.md and the README say so.
- **Rule B in both reviewers** (decision 43). plan-critic and scope-guardian
state it: a major that a named test, `Regression:` command or gate in the
plan will fail on during execution is minor, never a blocker and never a
security finding. The gate records each such lowering from round 1 on.
Plan dedup passes the reviewers' marks and rule-B fields through. The
operator answered three questions on 2026-10-15: rule B applies from round
1 and rule A from round 2; both reviewers tag a security finding with the
review catalogue's security key; a lowered finding that is re-reported as
major stays minor, and only a re-report as blocker counts as blocker.
### Fixed
@ -211,6 +242,15 @@ warning per step whose `Files:` names a path that is not backticked.
file from the plan and answers `no test files given`. The helper then runs
the same red proof again with `--tests` naming the files it wrote, and the
executor gives red proof that same list, so such a step is proven and locked.
- **The post-compact hook test survives a slow start** (decision 44). Its
hook child had a fixed 5 s timeout and was killed under full-suite load.
The timeout now comes from `tests/helpers/timing.mjs`, and a killed hook
child reads as a failure instead of exit 0.
- **Plan dedup keeps distinct file-level findings apart, and no longer
crashes on a bad field** (decision 43). Findings of one rule key on line 0
or with no line no longer collapse into one. An empty `file` or a `line`
that is not a whole number no longer crashes the finding id; the line
becomes null and the finding stands.
### Known issues

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.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 |
| `/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, rounds 2+ in diff mode (rule A, counted by the gate; rule B in the reviewers), 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 |
@ -42,6 +42,11 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
Full flag reference for each command (modes, `--gates`, `--profile`, breaking changes): see `docs/command-modes.md`.
> **The critic loop converges (decision 43).** Round 1 reviews the whole plan.
> From round 2 the reviewers get `plan-gate.mjs --diff` and mark each previous
> finding. The reviewers state rule B; the gate counts by rule A:
> Rule A: from round 2 on, a new major finding that cites a plan line outside the diff since the last reviewed text counts as minor; a blocker, a security finding and a finding with no plan line count at full severity, and a previous finding stays counted until it is marked resolved.
> **Plan step fields K10/K11 (any plan_version).** Optional `Regression:` = ONE
> allowlisted command, unpiped, run green before the checkpoint (`Verify:` stays
> the red-first test); `On failure:` opens with revert | retry | skip | escalate.

View file

@ -329,7 +329,7 @@ One step per plan is the "run the thing" step (`End-to-end:`): the product run t
Exploration uses 6–8 agents in parallel (architecture-mapper, dependency-tracer, task-finder, test-strategist, git-historian, risk-assessor, plus convention-scanner on medium+ codebases and research-scout when unfamiliar tech is detected). Adversarial review then runs `brief-reviewer`, `plan-critic` (12 dimensions, no-placeholder enforcement, manifest audit, interfaces and slices, design and architecture rules), and `scope-guardian` (creep + gap detection, ceremony match). Per-agent details in [`agents/`](agents/).
**The plan gate.** Each critic round is written into the plan by `lib/validators/plan-gate.mjs --record`; `plan-gate.mjs <plan.md>` then exits 0 (go on), 1 (stop: invalid plan, no verdict, a verdict the plan has changed since, or an open BLOCKER) or 2 (could not run). After 3 rounds with a BLOCKER open, `/trekplan` stops and asks; it never continues silently, and "execute" is offered only when the gate exits 0. Minors still open after the last round are listed numbered in the summary; the ones you pick are fixed and get one more round, the rest are carried. In Claude Code's plan mode the plan is written to Claude Code's plan file and approved through its own approval prompt (that `/trekplan` runs cleanly inside plan mode is not verified yet).
**The plan gate.** Each critic round is written into the plan by `lib/validators/plan-gate.mjs --record`; `plan-gate.mjs <plan.md>` then exits 0 (go on), 1 (stop: invalid plan, no verdict, a verdict the plan has changed since, an open BLOCKER or an unaccepted MAJOR) or 2 (could not run). From round 2 the gate counts by rule A: a new major outside the diff since the last reviewed text counts as minor, never a blocker or a security finding (Handover 4). After 3 rounds with a BLOCKER open, `/trekplan` stops and asks; it never continues silently, and "execute" is offered only when the gate exits 0. Minors still open after the last round are listed numbered in the summary; the ones you pick are fixed and get one more round, the rest are carried. In Claude Code's plan mode the plan is written to Claude Code's plan file and approved through its own approval prompt (that `/trekplan` runs cleanly inside plan mode is not verified yet).
**Checks the plan hands on** (0 / 1 / 2, and 2 = "not measured", never green): `lib/verification/arch-rules.mjs --repo <dir>` runs the target repo's own dependency-cruiser or import-linter — Voyage installs nothing — and `--prove` shows a rule pass, fail on a planted violation, and pass again; `lib/verification/plan-scope-sweep.mjs --plan <plan.md> --base <ref>` lists changed files that no step names.

View file

@ -440,9 +440,16 @@ The dedup output is piped on to `lib/validators/plan-gate.mjs --record
<plan> --reviewed <hash>` (the hash `--hash` printed before the reviewers read
the plan), which writes the round's verdict into the plan's `## Critic Verdict`.
This is a loop of at most 3 rounds: revise, then review again; after round 3
with a BLOCKER open, stop and ask the operator. `plan-gate.mjs <plan>` (K3)
exits 0 only for a valid plan whose current text carries a verdict with no
open BLOCKER (`commands/trekplan.md` Phase 9 is the full text).
with a BLOCKER open, stop and ask the operator. Round 1 reviews the whole
plan. Rounds 2+ run in diff mode: `plan-gate.mjs --diff <plan>` prints what
changed since the text the last round reviewed, both reviewers get it and
mark each previous finding by id, and the gate counts the round by rule A:
Rule A: from round 2 on, a new major finding that cites a plan line outside the diff since the last reviewed text counts as minor; a blocker, a security finding and a finding with no plan line count at full severity, and a previous finding stays counted until it is marked resolved.
`plan-gate.mjs <plan>` (K3) exits 0 only for a valid plan whose current text
carries a verdict with no open BLOCKER and no open MAJOR the operator has not
accepted (`commands/trekplan.md` Phase 9 is the full text).
After both complete:
- Address all blockers and major issues by revising the plan

View file

@ -921,10 +921,34 @@ approval.
Phase 9 is a loop of at most **3 critic rounds**. Each round reviews the plan
as it stands, and a command — not this session's judgment — records the
verdict and decides whether the plan may go on. The loop ends on a round with
no BLOCKER; after round 3 with a BLOCKER still open it stops and asks the
verdict and decides whether the plan may go on. A BLOCKER or an open MAJOR
stops the plan: revise and run the next round. The loop is done on a round
that leaves neither, or when the operator accepts the open MAJORs after the
last round; after round 3 with a BLOCKER still open it stops and asks the
operator. It never goes on silently.
**From round 2.** Round 1 is unchanged: the reviewers read the whole plan.
From round 2 on, first print what changed since the text the last round
reviewed:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/validators/plan-gate.mjs --diff "{plan_path}"
```
It prints `diff-base: <sha256> (round N)` and each changed hunk with the
plan's full-file line numbers. Hand its output to both reviewers, with the
round-2 sentence their prompts gain below. When it says `diff-base: none`,
the round is a full review: the reviewers read the whole plan as in round 1,
and rule A lowers nothing. The gate, not this
session, counts the round by rule A:
Rule A: from round 2 on, a new major finding that cites a plan line outside the diff since the last reviewed text counts as minor; a blocker, a security finding and a finding with no plan line count at full severity, and a previous finding stays counted until it is marked resolved.
The reviewers state rule B, and the gate records each lowering they make
from round 1 on:
Rule B: a major that a named test, `Regression:` command or gate in the plan will fail on during execution is minor — never a blocker, never a security finding.
**One round:**
First take the hash of the plan the reviewers are about to read, and keep the
@ -947,6 +971,8 @@ End your response with the REQUIRED machine-readable `json` findings block
(schema in `agents/plan-critic.md`) so the orchestrator can pipe it to the
dedup helper. You have no Write tool — return the block inline, do not write a file.
Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)."
From round 2 the prompt adds one sentence, followed by the `--diff` output:
"Previous findings are in the plan's `## Critic Verdict`; the diff since the last reviewed text is below. Mark each previous finding by id in `previous`."
**scope-guardian** — scope alignment check.
Prompt: "Check this implementation plan against the brief.
@ -958,6 +984,8 @@ plan's Verification section. End your response with the REQUIRED machine-readabl
`json` findings block (schema in `agents/scope-guardian.md`). You have no Write
tool — return the block inline, do not write a file.
Read local files only inside the repo under exploration, plus paths this prompt gives you (the brief, the plan, and templates it names)."
From round 2 the prompt adds one sentence, followed by the `--diff` output:
"Previous findings are in the plan's `## Critic Verdict`; the diff since the last reviewed text is below. Mark each previous finding by id in `previous`."
After both complete, **extract each reviewer's trailing `json` findings block**
and pipe both into the dedup helper via **stdin** — the reviewers are read-only
@ -974,6 +1002,15 @@ JSON
`plan-gate.mjs --record` prints one JSON line: `round`, `verdict`
(`BLOCK` = a blocker is open, `REVISE` = majors only, `PASS`) and `counts`.
It also prints `diff_base` (the sha256 of the text this round was counted
against by rule A, or null), `diff_note` (one line: why a round from round 2
on was counted fully), `lowered` (how many findings count below what a
reviewer rated) and `snapshot` (`kept: …` or `not kept: <reason>`). The
snapshot is the operative text this round reviewed, kept under
`<git dir>/voyage/plan-snapshots/` and never committed; the next round's
`--diff` reads it. A plan outside any git repository (plan mode) keeps no
snapshot (`not kept: no git repository`), so its next round counts fully.
A failed snapshot write does not fail the round.
Exit 1 means the round cap is spent (see "After round 3"). Exit 2 means the
round cannot be recorded: broken input, a reviewer missing from it (an absent
review is not a clean one), or a plan that changed after `--hash` — the

View file

@ -265,7 +265,7 @@ prose form (relaxed in S26 so the template's prose emission parses).
- 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.
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. **Optional rule-A fields** (decision 43; `DIFF_FIELDS` in `lib/review/critic-diff.mjs`): each one is optional, and absent means round-1 behaviour, so an older block gives the same exit codes. Top level: `diff_base` (the sha256 the round was counted against), `diff_note` (one line: why a round from round 2 on was counted fully), `resolved` (the ids of previous findings that left the count). Per finding: `op_line` and `op_line_end` (the cited lines in the operative text), `line_end`, `rule_keys` (the union of the keys dedup merged), `original_severity` (what a reviewer rated, set whenever the counted `severity` differs), `lowered_by` (`rule A: outside the diff since round <P>` or `rule B: <caught_by>`), `caught_by` (the test, `Regression:` command or gate a rule-B lowering names), `lowering_refused` (why a rule-B lowering was refused) and `carried_from` (the round a carried finding first appeared). `severity` is always the counted severity. `recordVerdict` called without `base`, `baseNote` and `planPaths` records as before, plus the rule-B record, and a per-finding `rule_keys` from dedup is always passed through (an optional field).
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`).
@ -315,6 +315,12 @@ The strongest validation in the entire pipeline. Phase 5.5 (planning-orchestrato
**The plan gate (K3).** `node lib/validators/plan-gate.mjs <plan.md> [--json]` decides whether a plan may go on: **0** = it validates strict, its `## Critic Verdict` matches the current text, no BLOCKER is open, and every open MAJOR was accepted by the operator; **1** = stop, with `reasons` (`PLAN_GATE_INVALID_PLAN`, `PLAN_GATE_VERSION` — below plan_version 1.8, `PLAN_GATE_NO_VERDICT`, `PLAN_GATE_VERDICT_MALFORMED`, `PLAN_GATE_VERDICT_STALE`, `PLAN_GATE_OPEN_BLOCKER`, `PLAN_GATE_ROUNDS_EXHAUSTED`, `PLAN_GATE_OPEN_MAJOR` — rounds left: revise, `PLAN_GATE_OPEN_MAJOR_FINAL` — after the last round: ask the operator) and `warnings` (`PLAN_GATE_OPEN_MAJOR` for majors the operator accepted); **2** = could not read the plan. `--hash <plan.md>` prints the operative hash; Phase 9 takes it before the reviewers read the plan. `--record <plan.md> --reviewed <hash>` reads plan-review-dedup output on stdin (which lists the reviewers that delivered, `sources`) and writes the next round; exit 2 when a reviewer is missing or the plan changed since `--hash`; it refuses round 4 (exit 1, stop and ask) unless `--extra-round` follows the operator's yes, and refuses that flag before the cap is spent. Unrated findings count as blockers. The review log is excluded from the hash only up to its first other heading, so a step placed under it is hashed. `--accept-open <plan.md>` records the operator's "build as is" for the current round's open MAJORs (`accepted_open` in the verdict block, bound to the round and the plan hash; only after the last round; never for a BLOCKER; exit 1 when rounds are left, the verdict is stale, a blocker is open or no major is). Operator decision 2026-10-08: open majors used to be warnings, and a weakness the critics named was built and came back from `/trekreview` as MAJOR. plan-critic and scope-guardian rate on the review catalogue's tiers. The verdict is a trace of the review, not a signature.
**Rule A and the diff (decision 43).** Round 1 is unchanged. From round 2 on, `--record` counts the round against the operative text the previous round reviewed, and both reviewers mark each previous finding by id (`previous`). The gate counts by this rule, not the session:
Rule A: from round 2 on, a new major finding that cites a plan line outside the diff since the last reviewed text counts as minor; a blocker, a security finding and a finding with no plan line count at full severity, and a previous finding stays counted until it is marked resolved.
Rule B is stated by the reviewers and recorded by the gate from round 1 (`lowered_by`, `caught_by`); it never lowers a blocker or a security finding. `--diff <plan.md>` prints what the next round reviews: `diff-base: <sha256> (round N)` and each hunk in the plan's full-file line numbers, or `diff-base: none — <reason>; review the whole plan` (exit 0; 2 = unreadable plan or bad arguments). `--record` keeps each recorded round's operative text as a snapshot, `<git dir>/voyage/plan-snapshots/<sha256>.md`, named by its own sha256. git never tracks its own directory, so a snapshot is never committed, whatever the repo's `.gitignore` or `artifact_store` says. A plan in no git repository (plan mode) keeps none (`snapshot: "not kept: no git repository"`), and its next round counts fully. A missing, oversized, misnamed or tampered snapshot, or a plan unchanged since the base, also means full counting, said in one `diff_note` line. `--record`'s JSON line adds `diff_base`, `diff_note`, `lowered` (a number) and `snapshot`. Trust limit: anything that can write the plan can also rewrite the previous verdict's `plan_sha256` and plant a matching snapshot. That was already the limit of the verdict block; rule A raises what such a forgery buys, and an unchanged base lowers nothing.
**Verification CLIs that read a plan** (0/1/2, JSON on stdout, 2 is never green): `lib/verification/arch-rules.mjs --repo <dir>` runs the target repo's own dependency-cruiser or import-linter (installs nothing; `--prove --rule <name> --plant <file>=<dest>` shows pass → fail on that rule → pass, refusing a destination that exists or resolves outside the repo); `lib/verification/plan-scope-sweep.mjs --plan <plan.md> --base <ref>` lists changed files no step names (`Files:`, `Test first:` File, manifest `expected_paths`) and no process path covers.
**Versioning:** v1.7 has been stable since v1.8.0 of the plugin (when literal-template + Phase 5.5 self-check were added to fix Opus 4.7 schema drift). v1.6 → v1.7 added the Manifest block (mandatory). v1.7 → v1.8 makes `Test first:` and the "run the thing" step required, adds the optional step fields and the design part, and was written validator-first with tests for both shapes (`tests/validators/plan-validator-v18.test.mjs`); 1.7 plans validate unchanged, so readers need no change (the executor already treats `plan_version ≥ 1.7` as strict). v1.8 → v1.9 gives `Files:` and the `Test first:` `File:` line a grammar, the file first on every line (invariant 8), so no reader has to tell a path from a description in free text; it was written validator-first (`tests/validators/plan-version-1-9.test.mjs`). Only a 1.9 plan is refused for it; 1.8 and 1.7 plans validate as before, with the warning `PLAN_STEP_PATH_UNTICKED`, and the 1.8 rules (invariant 5) bind 1.8 and 1.9 plans alike.

View file

@ -315,5 +315,10 @@ later waves depend on earlier waves completing first.*
--record` appends it after the first critic round (Phase 9) and replaces it on
each later round: one fenced json block with round, max_rounds, plan_sha256
(the plan minus its review log), verdict BLOCK | REVISE | PASS, and the
deduplicated findings. `plan-gate.mjs <plan>` exits 0 only when the plan
validates, the block matches the current plan, and no BLOCKER is open. -->
deduplicated findings. It may also carry the optional rule-A and rule-B
fields (DIFF_FIELDS in lib/review/critic-diff.mjs: diff_base, diff_note,
resolved, and per finding op_line, op_line_end, line_end, rule_keys,
original_severity, lowered_by, caught_by, lowering_refused, carried_from);
absent means round-1 behaviour. `plan-gate.mjs <plan>` exits 0 only when the
plan validates, the block matches the current plan, no BLOCKER is open, and
every open MAJOR was accepted by the operator. -->

View file

@ -21,7 +21,7 @@
|-----------|--------|-------------|----------|---------------|-----|
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1137-1161 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | lib/stats/stats-append.mjs (trekresearch.md Stats tracking) | trekresearch.md:782-808 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1234-1262 | profile, phase_models, parallel_agents, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | lib/stats/stats-append.mjs (trekplan.md Phase 12) | trekplan.md:1271-1299 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2892-2914 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |

View file

@ -289,7 +289,7 @@ test('the template\'s own example steps validate as 1.9 with none of the new pat
// lines the release check accepts. The release check is imported as a module
// namespace, so a missing parseDoors export fails an assertion, not the load.
const releaseCheck = await import('../../scripts/release-check.mjs');
const UNRELEASED = '## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests';
const UNRELEASED = '## Unreleased — plan_version 1.9, the trial-4 findings, session-clean tests, the converging critic loop';
function firstChangelogSection() {
const lines = read('CHANGELOG.md').split('\n');

View file

@ -0,0 +1,68 @@
// tests/lib/review-loop-docs.test.mjs
// Prose pins for the converging critic loop (decision 43): /trekplan Phase 9,
// Handover 4, CLAUDE.md and the agent files must state rule A and rule B in the
// exact words lib/review/critic-diff.mjs exports, name the optional verdict
// fields, and no longer say the loop ends on a round with no BLOCKER.
// Texts are compared with every run of whitespace read as one space, so a
// sentence wrapped over two lines still counts.
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 { RULE_A, RULE_B, DIFF_FIELDS } from '../../lib/review/critic-diff.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = rel => readFileSync(join(ROOT, rel), 'utf-8');
const flat = text => String(text).replace(/\s+/g, ' ');
function phase(text, n) {
const start = text.indexOf(`\n## Phase ${n} `);
assert.ok(start >= 0, `## Phase ${n} missing`);
const end = text.indexOf('\n## Phase ', start + 1);
return text.slice(start, end < 0 ? undefined : end);
}
function handover4() {
const contract = read('docs/HANDOVER-CONTRACTS.md');
const start = contract.indexOf('## Handover 4');
const end = contract.indexOf('## Handover 5');
assert.ok(start >= 0 && end > start, 'Handover 4 and Handover 5 headings found in order');
return contract.slice(start, end);
}
const P9 = flat(phase(read('commands/trekplan.md'), 9));
test('RULE_A stands verbatim in Phase 9, Handover 4, CLAUDE.md and the planning orchestrator', () => {
const rule = flat(RULE_A);
assert.ok(P9.includes(rule), 'Phase 9 of commands/trekplan.md states RULE_A');
assert.ok(flat(handover4()).includes(rule), 'Handover 4 of docs/HANDOVER-CONTRACTS.md states RULE_A');
assert.ok(flat(read('CLAUDE.md')).includes(rule), 'CLAUDE.md states RULE_A');
assert.ok(flat(read('agents/planning-orchestrator.md')).includes(rule), 'agents/planning-orchestrator.md states RULE_A');
});
test('RULE_B stands verbatim in Phase 9 and in both reviewer agent files', () => {
const rule = flat(RULE_B);
assert.ok(P9.includes(rule), 'Phase 9 of commands/trekplan.md states RULE_B');
for (const rel of ['agents/plan-critic.md', 'agents/scope-guardian.md']) {
assert.ok(flat(read(rel)).includes(rule), `${rel} states RULE_B`);
}
});
test('Handover 4 names every DIFF_FIELDS entry and says absent means round-1 behaviour', () => {
const h4 = handover4();
assert.ok(DIFF_FIELDS.length > 0, 'DIFF_FIELDS lists the optional fields');
const missing = DIFF_FIELDS.filter(f => !h4.includes(`\`${f}\``));
assert.deepEqual(missing, [], `Handover 4 does not name (backticked): ${missing.join(', ')}`);
assert.ok(flat(h4).includes('absent means round-1 behaviour'), 'Handover 4 says "absent means round-1 behaviour"');
});
test('Phase 9 names plan-gate --diff and says round 1 is unchanged', () => {
assert.ok(P9.includes('plan-gate.mjs --diff'), 'Phase 9 runs plan-gate.mjs --diff');
assert.match(P9, /round 1 is unchanged/i, 'Phase 9 says "round 1 is unchanged"');
});
test('Phase 9 no longer says the loop ends on a round with no BLOCKER', () => {
assert.ok(!P9.includes('ends on a round with no BLOCKER'), 'the stale "ends on a round with no BLOCKER" is gone');
});