docs(gate): decision 221 stated where the plan loop reads it

The verifier's look at ff46f57 found three fail-safe MINOR findings, all
text that did not yet say decision 221 or said it inaccurately:

- commands/trekplan.md Phase 9 and the plan-gate.mjs comment said only that a
  backed finding leaves the count and the stop rule; both now say it applies
  to the round's new findings, and a finding counted in an earlier round
  stays counted until a reviewer marks it resolved.
- Handover 4 said a known_outside entry "left the counted findings"; it now
  says that holds for a new finding of its round, and that a carried copy of
  the same id stays counted, so one id can sit in both findings and
  known_outside.
- The CHANGELOG Known issues bullet said the loop "then stops" and that the
  way on is the operator's choice at the stop rule. The stop rule fires only
  when the blocking findings do not get fewer, and a counting security
  finding is never accepted (PLAN_GATE_SECURITY_OPEN): the way on is a
  reviewer marking it resolved or "New mechanism". The bullet also names the
  shared id (two findings at one line and key, one mark resolves both).

No code change.

Part 2 of the E plan, decision 221.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-08 12:23:25 +01:00
commit d72d3667f6
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 12 additions and 8 deletions

View file

@ -236,7 +236,7 @@ Fix round 4.5a (decisions 57, 67 and 68): the round's 30 findings, where a Voyag
### Known issues
- **A finding counted in an earlier critic round stays counted when the threat model later puts it outside** (E, decision 221). The brief's threat model applies to a round's new findings only: a finding already counted stays counted, through rule A's carry, when an `Outside:` line approved after it was counted puts it outside, or when it moves to another plan line; the plan loop then stops, and the way on is the operator's choice at the stop rule. The gate never counts less.
- **A finding counted in an earlier critic round stays counted when the threat model later puts it outside** (E, decision 221). The brief's threat model applies to a round's new findings only: a finding already counted stays counted, through rule A's carry, when an `Outside:` line approved after it was counted puts it outside, or when it moves to another plan line (its id changes). Such a finding can also be listed in `known_outside` under its id, and two different findings at one line and key share that id, so one `previous` mark resolves both. It keeps counting in later rounds: the stop rule fires only when the blocking findings do not get fewer, and a counting security finding is never accepted as a known issue (`PLAN_GATE_SECURITY_OPEN`), so the way on is a reviewer marking it resolved or the operator's "New mechanism". The gate never counts less.
- **`tests/fixtures/bakeoff-rich/` has no reader** (cleanup R-b). It stays for V3 (decision 202); with `gold-eval` gone nothing reads `gold.json`, and its README still describes the removed bakeoff harness as current.
- **The query guard has no caller** (cleanup R-b). `lib/validators/query-privacy-gate.mjs` and its tests stay, but its callers were the removed STORM steps, so no research query passes through it; its header and `skills/researching-sources/` say so. Left for the design (187).
- **Eleven agents take the session's effort** (cleanup R-b). Nine agents pin `effort: high`; the other eleven inherit the session level, so a session started below high runs them below high. Left for the design (187).

View file

@ -1034,10 +1034,12 @@ snapshot (`not kept: no git repository`), so its next round counts fully.
A failed snapshot write does not fail the round. `stop_rule` is this round's
stop-rule decision (see "The stop rule"), and `reason` names the code of a
refusal.
`--brief` applies the brief's `## Threat Model` (decisions 164, 218): a
security finding of the two reviewers whose `threat_model_ref` equals an
`Outside:` line's text leaves the count and the stop rule and is listed in
the verdict's `known_outside` (`known_outside` in the line is how many);
`--brief` applies the brief's `## Threat Model` (decisions 164, 218) to the
round's new findings only (decision 221): a security finding of the two
reviewers whose `threat_model_ref` equals an `Outside:` line's text leaves
the count and the stop rule and is listed in the verdict's `known_outside`,
while a finding counted in an earlier round stays counted until a reviewer
marks it resolved (`known_outside` in the line is how many);
`threat_model` says whether the model was applied and, when not (no section,
the intent approval stale or not logged), why: then every security finding
counts. The gate re-reads the brief each time: a listed finding still backed

View file

@ -253,7 +253,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; an old Adversarial Pass 2 section is plan text and hashed), with CRLF and line-end whitespace normalized. The verdict sits inside the plan because Claude Code's plan approval hands a hook the plan file's text and nothing beside it. **Optional rule-A fields** (decision 43; `DIFF_FIELDS` in `lib/review/critic-diff.mjs`): each one is optional, and absent means round-1 behaviour, so an older block gives the same exit codes. Top level: `diff_base` (the sha256 the round was counted against), `diff_note` (one line: why a round from round 2 on was counted fully), `resolved` (the ids of previous findings that left the count). Per finding: `op_line` and `op_line_end` (the cited lines in the operative text), `line_end`, `rule_keys` (the union of the keys dedup merged), `original_severity` (what a reviewer rated, set whenever the counted `severity` differs), `lowered_by` (`rule A: outside the diff since round <P>` or `rule B: <caught_by>`), `caught_by` (the test, `Regression:` command or gate a rule-B lowering names), `lowering_refused` (why a rule-B lowering was refused) and `carried_from` (the round a carried finding first appeared). `severity` is always the counted severity. `recordVerdict` called without `base`, `baseNote` and `planPaths` records as before, plus the rule-B record, and a per-finding `rule_keys` from dedup is always passed through (an optional field). **Optional stop-rule fields** (decisions 217, 220; `lib/review/stop-rule.mjs`): `rounds` (one `{round, blocking, newByClass}` per recorded round, carried from the previous verdict; `newByClass` is `null` for a round counted in full) and `stop_rule` (`{fired, reason: same-class | not-fewer | null, classes, perRound}`); a verdict without them starts its history at the next round. `accepted_open.under` is `"stop-rule"` for an acceptance made after the stop rule fired. **Optional threat-model fields** (E 2.4, decisions 164, 218; `lib/review/threat-model.mjs`): `known_outside` (one `{id, rule_key, rule_keys, text, line}` per security finding of plan-critic or scope-guardian whose `threat_model_ref` equals the text of an `Outside:` line of the brief's `## Threat Model`, `line` being that text; it left the counted findings and the stop rule, and stays listed in later rounds until a reviewer marks it resolved or it is reported again under its id; the threat model applies to the round's new findings only, so a finding counted in an earlier round stays counted until a reviewer marks it resolved, decision 221) and `brief_path` (the brief `--record --brief` read, stored relative to the repo's top level and read from there whatever the working directory, carried when a later round has none); a verdict without them lists no known issue.
6. **`## Critic Verdict`** (written by `lib/validators/plan-gate.mjs --record`, never by hand): one fenced `json` block, schema `voyage.plan-verdict/1` — `round`, `max_rounds` (3), `extra_rounds`, `plan_sha256`, `verdict` (`BLOCK` | `REVISE` | `PASS`), `counts`, `findings` (deduplicated, each with `severity`). `plan_sha256` hashes the plan minus its review log (the `> **Plan quality:` line and the sections Critic Verdict, Revisions, Plan Quality Score; an old Adversarial Pass 2 section is plan text and hashed), with CRLF and line-end whitespace normalized. The verdict sits inside the plan because Claude Code's plan approval hands a hook the plan file's text and nothing beside it. **Optional rule-A fields** (decision 43; `DIFF_FIELDS` in `lib/review/critic-diff.mjs`): each one is optional, and absent means round-1 behaviour, so an older block gives the same exit codes. Top level: `diff_base` (the sha256 the round was counted against), `diff_note` (one line: why a round from round 2 on was counted fully), `resolved` (the ids of previous findings that left the count). Per finding: `op_line` and `op_line_end` (the cited lines in the operative text), `line_end`, `rule_keys` (the union of the keys dedup merged), `original_severity` (what a reviewer rated, set whenever the counted `severity` differs), `lowered_by` (`rule A: outside the diff since round <P>` or `rule B: <caught_by>`), `caught_by` (the test, `Regression:` command or gate a rule-B lowering names), `lowering_refused` (why a rule-B lowering was refused) and `carried_from` (the round a carried finding first appeared). `severity` is always the counted severity. `recordVerdict` called without `base`, `baseNote` and `planPaths` records as before, plus the rule-B record, and a per-finding `rule_keys` from dedup is always passed through (an optional field). **Optional stop-rule fields** (decisions 217, 220; `lib/review/stop-rule.mjs`): `rounds` (one `{round, blocking, newByClass}` per recorded round, carried from the previous verdict; `newByClass` is `null` for a round counted in full) and `stop_rule` (`{fired, reason: same-class | not-fewer | null, classes, perRound}`); a verdict without them starts its history at the next round. `accepted_open.under` is `"stop-rule"` for an acceptance made after the stop rule fired. **Optional threat-model fields** (E 2.4, decisions 164, 218; `lib/review/threat-model.mjs`): `known_outside` (one `{id, rule_key, rule_keys, text, line}` per security finding of plan-critic or scope-guardian whose `threat_model_ref` equals the text of an `Outside:` line of the brief's `## Threat Model`, `line` being that text; as a new finding of its round it left the counted findings and the stop rule (a copy of the same id carried from an earlier round stays counted, so one id can sit in both `findings` and `known_outside`), and stays listed in later rounds until a reviewer marks it resolved or it is reported again under its id; the threat model applies to the round's new findings only, so a finding counted in an earlier round stays counted until a reviewer marks it resolved, decision 221) and `brief_path` (the brief `--record --brief` read, stored relative to the repo's top level and read from there whatever the working directory, carried when a later round has none); a verdict without them lists no known issue.
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`).

View file

@ -405,8 +405,10 @@ export function recordVerdict(text, dedup, opts = {}) {
}
const extra = prevExtra + (capped ? 1 : 0);
const diffMode = opts.base !== undefined || opts.baseNote !== undefined || opts.planPaths !== undefined;
// The threat model (E 2.4): a backed finding leaves the counted set (and so
// the stop rule) before countRound and is listed in known_outside.
// The threat model (E 2.4): a backed finding new in this round leaves the
// counted set (and so the stop rule) before countRound and is listed in
// known_outside; a copy rule A carries from an earlier round stays counted
// until a reviewer marks it resolved (decision 221).
const use = opts.threatModel;
const tm = use && use.usable === true ? use.threatModel : null;
const backed = [];