docs(review): /trekreview chooses its security engine (E 3.2)

Phase 4.7 of /trekreview runs security-engine.mjs choose first. Claude
Security runs through its own skill (scan changes --base {before_sha}
--effort high); its cost question goes to the operator, and "No", a
question nobody can answer or a missing Workflow tool records it as not
run, never a fallback to /security-review. /security-review runs only
when choose picked it. Its report is converted by security-engine.mjs
convert, never transcribed; below_threshold is declined as
engine:below-threshold. With no engine the expected reviewer
security-engine never reports, so the verdict gate exits 2 and the
summary says "security: not measured" and why, never 0. --quick keeps no
security engine. The Engine summary line, the template's ran line, the
hard rules (the engine's report folder, scan changes only) and the
allowed-tools (Workflow) follow.

Engine prose follows in commands/trekexecute.md, docs/command-modes.md,
docs/architecture.md, CLAUDE.md, README.md, SECURITY.md and Handover 6:
Claude Security recommended, proprietary so called by name and never a
manifest dependency; without it /security-review; with neither "not
measured".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-08 13:39:44 +01:00
commit d97e9b5b19
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
10 changed files with 279 additions and 49 deletions

View file

@ -34,7 +34,7 @@ Voyage — a contract-driven Claude Code pipeline: brief, research, plan, execut
| `/trekresearch` | Research — deep local + external research, produces structured research brief. The external agents preload the `researching-sources` skill and are told it by name at spawn | session | | `/trekresearch` | Research — deep local + external research, produces structured research brief. 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; every check written by `voyage:writing-criteria` (named where the plan is written and where its three reviewers start); 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 | | `/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; every check written by `voyage:writing-criteria` (named where the plan is written and where its three reviewers start); 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, the app seen with Claude Code's built-in `run` while the `End-to-end:` command stays the verdict (`run` gives no exit code), 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 | | `/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, the app seen with Claude Code's built-in `run` while the `End-to-end:` command stays the verdict (`run` gives no exit code), 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, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green); the operator answers each counting MAJOR (Phase 8.8; `--answers` runs only that, no new round); proposes recurring finding classes as project memory, line by line (Handover 6). The reviewers and coordinator preload `voyage:reviewing-adversarially` and are told it by name at spawn; each reviewer's Declined to Judge lines reach `review.md`; the operator's notes from the annotation page go through `voyage:annotating` | session | | `/trekreview` | Review — a gate: Claude Code's `/code-review` + one security engine (Claude Security, else `/security-review`; with neither, "not measured") 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, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green); the operator answers each counting MAJOR (Phase 8.8; `--answers` runs only that, no new round); proposes recurring finding classes as project memory, line by line (Handover 6). The reviewers and coordinator preload `voyage:reviewing-adversarially` and are told it by name at spawn; each reviewer's Declined to Judge lines reach `review.md`; the operator's notes from the annotation page go through `voyage:annotating` | 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 | | `/trekcontinue` | Continue — resumes the next session of a multi-session voyage project. Reads `.session-state.local.json` (Handover 7) and immediately begins executing | session |
| `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | session | | `/trekendsession` | End-session — mark the current session complete and write session-state pointing at the next session. Helper for informal multi-session flows | session |

View file

@ -343,7 +343,7 @@ A run stops only on a **closed list**: something that cannot be undone · someth
### The review at the end — a gate (P11) ### The review at the end — a gate (P11)
After the last step, and with nobody typing anything, the executor runs Cherny's order: **run the thing** (the plan's `## Verification`, its end-to-end criterion included) → **`/simplify`** (Claude Code's cleanup skill; edits outside the run's files are restored, and a cleanup that breaks the verification is reverted) → **`/trekreview`** (Claude Code's `/code-review` and `/security-review`, then Voyage's check against the brief) → **`verdict-gate.mjs`**. After the last step, and with nobody typing anything, the executor runs Cherny's order: **run the thing** (the plan's `## Verification`, its end-to-end criterion included) → **`/simplify`** (Claude Code's cleanup skill; edits outside the run's files are restored, and a cleanup that breaks the verification is reverted) → **`/trekreview`** (Claude Code's `/code-review` and a security engine — Claude Security, else `/security-review` — then Voyage's check against the brief) → **`verdict-gate.mjs`**.
| Verdict gate | Merge into main | | Verdict gate | Merge into main |
|---|---| |---|---|
@ -470,7 +470,7 @@ and a contracted **Handover 6 (review → plan)** so findings can be fed back in
the iteration loop without ad-hoc conventions. the iteration loop without ad-hoc conventions.
**The review is a gate.** In this order: Claude Code's own `/code-review` and **The review is a gate.** In this order: Claude Code's own `/code-review` and
`/security-review` find correctness and security defects (started through the a security engine find correctness and security defects (started through the
Skill tool, never with `--fix`); Voyage's control against the brief runs after Skill tool, never with `--fix`); Voyage's control against the brief runs after
them and is the part that stops. The verdict is written in `review.md` them and is the part that stops. The verdict is written in `review.md`
(`verdict: ALLOW | WARN | BLOCK`, required) and one command decides on it: (`verdict: ALLOW | WARN | BLOCK`, required) and one command decides on it:
@ -485,9 +485,23 @@ review that was not performed in full stops a merge.
No finding is struck for length: only MINOR/SUGGESTION text is trimmed, and No finding is struck for length: only MINOR/SUGGESTION text is trimmed, and
counted; what the coordinator set aside is listed under `## Declined to counted; what the coordinator set aside is listed under `## Declined to
Judge`. An engine that could not run (older Claude Code, no `origin` remote) Judge`. An engine that could not run (older Claude Code, no `origin` remote,
keeps the review from coming back `ALLOW` — and from passing the gate — and Claude Security's cost question answered "No") keeps the review from coming
the summary says why. back `ALLOW` — and from passing the gate — and the summary says why.
**The security engine.** Claude Security is the recommended one. When its
plugin is installed (`/plugin install claude-security@claude-plugins-official`;
it needs Python 3.9 or newer on `PATH` and Claude Code's Workflow tool),
`/trekreview` runs its `scan changes` over the review's range, after Claude
Security's own question about the scan's cost, which is put to you. Claude
Security has a proprietary licence, so Voyage calls it by name and it is never
a dependency in Voyage's manifest. Without it, `/trekreview` uses Claude Code's
`/security-review` (it needs an `origin` remote and reads the branch against
origin's default branch). With neither engine able to look (no Claude Security
and no `origin` remote, or a branch that is already on origin's default
branch), security is "not measured", never 0, and the verdict gate exits 2. A
Claude Security finding blocks only at confidence `high`; the rest are listed
under `## Declined to Judge` and keep the review from `ALLOW`.
**One stance for every reviewer.** Both reviewers and the coordinator preload **One stance for every reviewer.** Both reviewers and the coordinator preload
the `voyage:reviewing-adversarially` skill, and `/trekreview` names it when it the `voyage:reviewing-adversarially` skill, and `/trekreview` names it when it
@ -502,9 +516,9 @@ Declined to Judge list, and the coordinator carries those lines into
| Mode | Command | Description | | Mode | Command | Description |
|------|---------|-------------| |------|---------|-------------|
| **Default** | `/trekreview --project <dir>` | Claude Code `/code-review` + `/security-review`, then brief-conformance + code-correctness reviewers in parallel, coordinator dedup + verdict, write `{dir}/review.md`, verdict gate | | **Default** | `/trekreview --project <dir>` | Claude Code `/code-review` + the security engine (Claude Security, else `/security-review`, else "not measured"), then brief-conformance + code-correctness reviewers in parallel, coordinator dedup + verdict, write `{dir}/review.md`, verdict gate |
| **Since ref** | `/trekreview --project <dir> --since <ref>` | Override "before" SHA for the diff range. Validated via `git rev-parse --verify` | | **Since ref** | `/trekreview --project <dir> --since <ref>` | Override "before" SHA for the diff range. Validated via `git rev-parse --verify` |
| **Quick** | `/trekreview --project <dir> --quick` | `/code-review` + code-correctness reviewer only; skip brief-conformance reviewer, `/security-review` and the coordinator's reasonableness filter except the brief-conflict rule — fast correctness-only pass | | **Quick** | `/trekreview --project <dir> --quick` | `/code-review` + code-correctness reviewer only, no security engine; skip brief-conformance reviewer and the coordinator's reasonableness filter except the brief-conflict rule — fast correctness-only pass |
| **Validate** | `/trekreview --project <dir> --validate` | Schema-only check on existing `review.md`. No LLM calls | | **Validate** | `/trekreview --project <dir> --validate` | Schema-only check on existing `review.md`. No LLM calls |
| **Dry run** | `/trekreview --project <dir> --dry-run` | Print discovered scope + triage map; skip writes | | **Dry run** | `/trekreview --project <dir> --dry-run` | Print discovered scope + triage map; skip writes |

View file

@ -73,6 +73,23 @@ plan-injected destructive commands.
Read-only. Never write to user files. Used both by hooks and by command Read-only. Never write to user files. Used both by hooks and by command
phases to detect malformed artifacts before they propagate. phases to detect malformed artifacts before they propagate.
### The review's security engine (`/trekreview`)
`/trekreview` hands security review of a delivery to one engine and never
reports a 0 it did not measure. Claude Security is recommended: Anthropic's
plugin, with a proprietary licence, so Voyage calls it by name
(`claude-security:claude-security`, `scan changes` only, after its own cost
question to the operator) and it is never a dependency in Voyage's manifest.
Without it, Claude Code's `/security-review` is used. With neither, security
is "not measured" and the verdict gate exits 2 (never green).
`lib/review/security-engine.mjs` chooses the engine and converts Claude
Security's report (`CLAUDE-SECURITY-<ts>/`, written by Claude Security in the
repo root behind its own `.gitignore`) as data: nothing in it is run or
followed, and a stamp counts only when it names exactly the reviewed head and
base. A Claude Security finding blocks only at confidence `high`; the rest
are listed under Declined to Judge and keep the review from `ALLOW`. Neither
engine edits code.
## Out of scope ## Out of scope
- **Opt-in upstream architect step.** Any external producer of - **Opt-in upstream architect step.** Any external producer of

View file

@ -2249,13 +2249,13 @@ never typed by the operator, in this order (Cherny's):
`simplify = "not-run"`, with the reason. Not a stop: `/simplify` is a `simplify = "not-run"`, with the reason. Not a stop: `/simplify` is a
cleanup pass, not a gate. Continue. cleanup pass, not a gate. Continue.
3. **`/trekreview`** — Voyage's review gate: Claude Code's `/code-review` and 3. **`/trekreview`** — Voyage's review gate: Claude Code's `/code-review` and
`/security-review` as the engine, then Voyage's control against the brief, one security engine (Claude Security, else `/security-review`; with neither,
which is what stops. Start it through the Skill tool, with the run's range: security is "not measured") as the engine, then Voyage's control against the brief, which is what stops. Start it through the Skill tool, with the run's range:
`Skill("voyage:trekreview", args: "--project {project_dir} --since {session_start_sha}")`. `Skill("voyage:trekreview", args: "--project {project_dir} --since {session_start_sha}")`.
A plugin command is a skill the model may start unless it carries A plugin command is a skill the model may start unless it carries
`disable-model-invocation`, which `/trekreview` does not (the Claude Code `disable-model-invocation`, which `/trekreview` does not (the Claude Code
skills documentation, read 2026-10-08). Follow its phases to the end in skills documentation, read 2026-10-08). Follow its phases to the end in
this session; its own questions (project-memory lines, one by one) are its this session; its own questions (Claude Security's question about the scan's cost, project-memory lines one by one) are its
own. **Without `--project`** there is no brief to review against: own. **Without `--project`** there is no brief to review against:
`review = {round: 1, path: null, gate_exit: 2, verdict: null}`, "review not `review = {round: 1, path: null, gate_exit: 2, verdict: null}`, "review not
run — no project directory", and the merge stays closed (Phase 8). run — no project directory", and the merge stays closed (Phase 8).
@ -3158,7 +3158,7 @@ pins and quotes in the local practice analysis.
- **Claude Code** itself: subagents for the helper and the controller; the - **Claude Code** itself: subagents for the helper and the controller; the
`PreToolUse` guard behind the test lock (K8); `/simplify`, `/code-review` and `PreToolUse` guard behind the test lock (K8); `/simplify`, `/code-review` and
`/security-review` as the review's engine, started through the Skill tool `/security-review` as the review's engine, started through the Skill tool
(P11). (P11); Claude Security, Anthropic's plugin, as the security engine where it is installed (E).
- **The review at the end (P11, wave 2)** — Cherny: the order — run the - **The review at the end (P11, wave 2)** — Cherny: the order — run the
thing, simplify, then review. thing, simplify, then review.
Kiro (`feature-pipeline`, documentation only): the gate — a verdict in a file. Kiro (`feature-pipeline`, documentation only): the gate — a verdict in a file.

View file

@ -5,7 +5,7 @@ description: |
review.md with severity-tagged findings (BLOCKER/MAJOR/MINOR/SUGGESTION) review.md with severity-tagged findings (BLOCKER/MAJOR/MINOR/SUGGESTION)
per Handover 6 (review → plan). per Handover 6 (review → plan).
argument-hint: "--project <dir> [--since <ref>] [--quick] [--validate] [--dry-run] [--answers]" argument-hint: "--project <dir> [--since <ref>] [--quick] [--validate] [--dry-run] [--answers]"
allowed-tools: Agent, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion, Skill allowed-tools: Agent, Read, Glob, Grep, Write, Edit, Bash, AskUserQuestion, Skill, Workflow
--- ---
# Ultrareview Local v1.0 # Ultrareview Local v1.0
@ -30,9 +30,11 @@ and the coordinator applies BOUNDED operations only. Synthesis-level
inference across files is forbidden in v1.0 (Judge Agent pattern). inference across files is forbidden in v1.0 (Judge Agent pattern).
**The review is a gate (review_version 1.2, ordre 5).** In this order: **The review is a gate (review_version 1.2, ordre 5).** In this order:
Claude Code's own `/code-review` and `/security-review` are the engine Claude Code's own `/code-review` and one security engine (Claude Security
(Phase 4.7); Voyage's control against the brief runs after them and is the where it is installed, else Claude Code's `/security-review`; with neither,
part that stops (Phase 5–6); the verdict is written in `review.md` and one security is "not measured", never 0) are the engine (Phase 4.7); Voyage's
control against the brief runs after them and is the part that stops
(Phase 5–6); the verdict is written in `review.md` and one
command decides on it (`lib/review/verdict-gate.mjs`, Phase 8: 0 = ALLOW, or command decides on it (`lib/review/verdict-gate.mjs`, Phase 8: 0 = ALLOW, or
WARN with every counting MAJOR answered, 1 = BLOCK, 2 = could not decide, WARN with every counting MAJOR answered, 1 = BLOCK, 2 = could not decide,
never green). No finding is struck for never green). No finding is struck for
@ -295,12 +297,12 @@ prompt in Phase 5, and its summary line goes into the Coverage section of
built by `formatCriteriaEvidence` precisely so the orchestrator cannot narrate a built by `formatCriteriaEvidence` precisely so the orchestrator cannot narrate a
pass that never happened. pass that never happened.
## Phase 4.7 — Claude Code's review engine (`/code-review`, `/security-review`) ## Phase 4.7 — The engine: Claude Code's `/code-review` and the security engine
**Skipped in `dry-run` and `validate`.** In `quick` mode only `/code-review` **Skipped in `dry-run` and `validate`.** In `quick` mode only `/code-review`
runs. Wrap, not a new engine: Claude Code's built-in reviews find the runs: no security engine. Wrap, not a new engine: `/code-review` and the
correctness and security defects; Voyage's brief control (Phase 5) runs after security engine find the correctness and security defects; Voyage's brief
them and is what stops. control (Phase 5) runs after them and is what stops.
Verified in Claude Code's live documentation (2026-10-06): Verified in Claude Code's live documentation (2026-10-06):
- Claude may start `/code-review` itself (`code-review.md`: "Claude can start - Claude may start `/code-review` itself (`code-review.md`: "Claude can start
@ -314,21 +316,55 @@ Verified in Claude Code's live documentation (2026-10-06):
default branch" and "Needs an `origin` remote". The docs do not state its default branch" and "Needs an `origin` remote". The docs do not state its
output format or whether it runs in the background. output format or whether it runs in the background.
Run both through the **Skill tool**, one after the other: **Choose the security engine first** (not in `quick`; E 3.1, decisions 175,
196). Claude Security is `available` when the `claude-security:claude-security`
skill is in this session's skill list AND the Workflow tool is among the tools
you can call right now (look at your tools, do not assume); otherwise it is
`missing`.
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/review/security-engine.mjs choose --repo . --base {before_sha} --head {after_sha} --claude-security {available|missing}
```
It prints `{engine, reason}`: `claude-security` when Claude Security is
available and `{before_sha}..{after_sha}` has committed changes; else
`security-review` when origin's HEAD resolves and `origin/HEAD...HEAD` is
non-empty; else `null`, with `no-engine` (no `origin` remote and no Claude
Security), `no-origin` (an `origin` remote whose HEAD does not resolve) or
`empty-diff` (the engine's range holds no committed change). Exit 2 = it could
not run: print the reason in one line and record the security engine as not
run. Keep `engine` and `reason` for Phases 5 and 9.
Then run, through the **Skill tool**, one after the other:
1. `Skill("code-review", args: "high {before_sha}...{after_sha}")` — the 1. `Skill("code-review", args: "high {before_sha}...{after_sha}")` — the
review's own SHA range from Phase 3. `high` is set explicitly; note in the review's own SHA range from Phase 3. `high` is set explicitly; note in the
Executive Summary that this also becomes the level later `/code-review` Executive Summary that this also becomes the level later `/code-review`
runs in this session reuse. runs in this session reuse.
2. `Skill("security-review")` — no arguments; its base is fixed. 2. The security engine `choose` picked, and only that one:
- `claude-security` —
`Skill("claude-security:claude-security", args: "scan changes --base {before_sha} --effort high")`.
Claude Security asks one question of its own before the scan starts (the
scan's time and token cost): put it to the operator as it stands and never
answer it yourself. "No", a question nobody can answer (no operator
present), the Workflow tool missing after all, or a scan that ends without
a report: record Claude Security as not run, with the reason. **Never fall
back to `/security-review`**: the review is then not measured for
security.
- `security-review` — `Skill("security-review")`, no arguments; its base is
fixed (origin's default branch). `/security-review` runs only when
`choose` picked it.
- `null` — no security engine runs: record it as not run with `choose`'s
reason.
**Never** pass `--fix`, `--comment`, `--post` or `ultra`: a review never edits **Never** pass `--fix`, `--comment`, `--post` or `ultra`: a review never edits
code, never posts anywhere, and never uploads the repo to a cloud review. code, never posts anywhere, and never uploads the repo to a cloud review.
**Wait for the findings.** A background `/code-review` delivers its findings **Wait for the findings.** A background `/code-review` delivers its findings
later in this conversation. Do not start Phase 6 before both engines have later in this conversation, and Claude Security's scan runs as a workflow
delivered or have been recorded as not run. For each engine that delivered, until it writes its report. Do not start Phase 6 before both engines have
transcribe every finding — file, line (0 when none), the one-sentence summary, delivered or have been recorded as not run. For `/code-review` and
`/security-review`, transcribe every finding — file, line (0 when none), the one-sentence summary,
the failure scenario, the category if given (it decides the tier: the failure scenario, the category if given (it decides the tier:
`simplification`, `efficiency`, `reuse`, `style` and `readability` enter as MINOR), the `simplification`, `efficiency`, `reuse`, `style` and `readability` enter as MINOR), the
fix the engine proposes (`fix`) only when its text states one — never write one fix the engine proposes (`fix`) only when its text states one — never write one
@ -349,7 +385,7 @@ node ${CLAUDE_PLUGIN_ROOT}/lib/review/engine-findings.mjs \
``` ```
The converted payload is saved to `{project_dir}/.review-engine-payload-{engine}.json` The converted payload is saved to `{project_dir}/.review-engine-payload-{engine}.json`
(`code-review` or `security-review`), the same stdout, also written: Phase 7's finalize (`code-review`, `security-review` or `claude-security`), the same stdout, also written: Phase 7's finalize
reads it from there. Never edit that file by hand. reads it from there. Never edit that file by hand.
Exit 0 prints a reviewer payload (`claude-code-review` / Exit 0 prints a reviewer payload (`claude-code-review` /
`claude-security-review`) for Phase 6 plus `out_of_scope`: engine findings on `claude-security-review`) for Phase 6 plus `out_of_scope`: engine findings on
@ -371,12 +407,40 @@ at least the engine's own (MAJOR for `/code-review`, BLOCKER for
`/security-review`); the adapter refuses to let a transcription lower an `/security-review`); the adapter refuses to let a transcription lower an
engine finding. engine finding.
**Claude Security's findings are converted, never transcribed.** When its
scan has finished it has written `CLAUDE-SECURITY-<ts>/` in the repo root
(behind its own `.gitignore`); convert that report, after the scope file
above exists:
```bash
node ${CLAUDE_PLUGIN_ROOT}/lib/review/security-engine.mjs convert --report-dir . \
--base {before_sha} --head {after_sha} --scope "{project_dir}/.review-scope.txt" \
> "{project_dir}/.review-engine-payload-claude-security.json" \
&& cat "{project_dir}/.review-engine-payload-claude-security.json"
```
Exit 0 prints the `claude-security` payload: `findings` and `out_of_scope` as
for any engine, and `below_threshold`, the findings Claude Security marked
below confidence `high` (decision 219). Pass `below_threshold` to the
coordinator as well: each one is listed under `## Declined to Judge`
(`engine:below-threshold`), never counted, and it forbids `ALLOW`. Exit 2 =
not measured (no report stamp of a changes scan of exactly
`{before_sha}..{after_sha}` — an empty diff writes none —, a scan not
verified, researchers that did not all return, an empty diff or scope, a
malformed record): print the reason and record Claude Security as not run;
the empty file the redirect leaves is no payload. Exit 1 = the scope list is
empty: rebuild it from Phase 3 and re-run.
**An engine that did not run** (Claude Code older than v2.1.246, the Skill **An engine that did not run** (Claude Code older than v2.1.246, the Skill
call refused, no `origin` remote, an `ambiguous argument` error, or no findings call refused, no `origin` remote, an `ambiguous argument` error, Claude
ever delivered) is recorded as not run with its reason. It stays in the Security's question answered "No" or left unanswered, a report `convert`
expected reviewer set, so it forbids `ALLOW` (fail-closed); the Executive reads as not measured, `choose` picking no engine, or no findings ever
Summary's first sentence names it. An engine that ran and found nothing delivered) is recorded as not run with its reason. It stays in the expected
delivers an empty payload — that is a report, not an absence. reviewer set, so it forbids `ALLOW` (fail-closed) and the verdict gate exits
2; the Executive Summary's first sentence names it. For security that
sentence says "security: not measured" and why (`choose`'s reason, or why
the engine did not run): never 0, never "no findings". An engine that ran and
found nothing delivers an empty payload — that is a report, not an absence.
The two engine payloads are NOT shown to the Phase 5 reviewers: reviewers stay The two engine payloads are NOT shown to the Phase 5 reviewers: reviewers stay
independent, and the coordinator is the only place the outputs meet. independent, and the coordinator is the only place the outputs meet.
@ -451,7 +515,7 @@ the round input:
Finalize (Phase 7) carries a previous finding until every reviewer that ran in Finalize (Phase 7) carries a previous finding until every reviewer that ran in
its round marks it resolved, and an engine finding until its engine ran this its round marks it resolved, and an engine finding until its engine ran this
round and did not raise it again. The engines (Phase 4.7) stay on the run range round and did not raise it again. The engines (Phase 4.7) stay on the run range
(`/security-review` takes no range). (`/security-review` takes no range; Claude Security scans from `{before_sha}`).
`brief-conformance-reviewer` additionally receives `sc_evidence_block` from `brief-conformance-reviewer` additionally receives `sc_evidence_block` from
Phase 4.5, pasted verbatim — the command, exit code and first line of output for Phase 4.5, pasted verbatim — the command, exit code and first line of output for
@ -502,8 +566,12 @@ Summary will note the brief-conformance pass was skipped.
Write down the expected reviewer set BEFORE the spawn: both reviewers in Write down the expected reviewer set BEFORE the spawn: both reviewers in
default mode, `code-correctness-reviewer` alone in `quick` mode — plus, in default mode, `code-correctness-reviewer` alone in `quick` mode — plus, in
every mode, the Phase 4.7 engines that were due (`claude-code-review`, and every mode, the Phase 4.7 engines that were due: `claude-code-review`, and
`claude-security-review` outside `quick`). After the spawn, account for each outside `quick` the security engine `choose` picked (`claude-security` for
Claude Security, `claude-security-review` for `/security-review`), or
`security-engine` when it picked none or could not run. `security-engine` never reports, so the
review carries `missing-reviewer:security-engine` and the verdict gate exits 2:
security not measured, never 0. After the spawn, account for each
one by name. An engine recorded as not run is passed on as missing, never one by name. An engine recorded as not run is passed on as missing, never
left out of the set. left out of the set.
@ -535,7 +603,8 @@ forbids `ALLOW` (`lib/review/coordinator-contract.mjs`, `missing_reviewers`).
## Phase 6 — Coordinator dedup + verdict ## Phase 6 — Coordinator dedup + verdict
Launch `review-coordinator` (Agent tool) with the merged findings array Launch `review-coordinator` (Agent tool) with the merged findings array
from Phase 5 AND the Phase 4.7 engine payloads, plus the triage map, brief from Phase 5 AND the Phase 4.7 engine payloads (Claude Security's
`below_threshold` list with them), plus the triage map, brief
metadata, SHA range, the expected reviewer set (engines included), and each metadata, SHA range, the expected reviewer set (engines included), and each
not-run engine with its reason. Name `voyage:reviewing-adversarially` in the not-run engine with its reason. Name `voyage:reviewing-adversarially` in the
coordinator's prompt as in Phase 5, and pass each reviewer's coordinator's prompt as in Phase 5, and pass each reviewer's
@ -894,7 +963,7 @@ After the write succeeds, print:
**Scope:** {before_sha}..{after_sha} ({reviewed_files_count} files) **Scope:** {before_sha}..{after_sha} ({reviewed_files_count} files)
**Verdict:** {BLOCK | WARN | ALLOW} **Verdict:** {BLOCK | WARN | ALLOW}
**Gate:** verdict-gate exit {gate_exit} ({0 = ALLOW, or WARN with every counting MAJOR answered | 1 = BLOCK | 2 = could not decide, a control did not run, or a MAJOR without an answer or answered "fixed" and not yet confirmed — not green}) **Gate:** verdict-gate exit {gate_exit} ({0 = ALLOW, or WARN with every counting MAJOR answered | 1 = BLOCK | 2 = could not decide, a control did not run, or a MAJOR without an answer or answered "fixed" and not yet confirmed — not green})
**Engine:** /code-review {delivered N | not run: reason} · /security-review {delivered N | not run: reason} **Engine:** /code-review {delivered N | not run: reason} · security {Claude Security delivered N, M below threshold | /security-review delivered N | not measured: reason | none in quick mode}
### Counts ### Counts
- BLOCKER: {N} - BLOCKER: {N}
@ -966,9 +1035,12 @@ the contract for that handover (see `docs/HANDOVER-CONTRACTS.md`).
writes to anything outside `{project_dir}` — writes to anything outside `{project_dir}` —
with one exception, only after the operator approved that exact line in with one exception, only after the operator approved that exact line in
Phase 8.7: Voyage's own marked block in `.claude/rules/voyage-review-lessons.md` Phase 8.7: Voyage's own marked block in `.claude/rules/voyage-review-lessons.md`
and a one-line pointer in `CLAUDE.md`. and a one-line pointer in `CLAUDE.md`. Claude Security writes its own report
- **The engine never edits code.** `/code-review` and `/security-review` run folder `CLAUDE-SECURITY-<ts>/` in the repo root, behind its own `.gitignore`;
without `--fix`, `--comment`, `--post` or `ultra`. A review that changes `/trekreview` only reads it.
- **The engine never edits code.** `/code-review`, `/security-review` and Claude Security run
without `--fix`, `--comment`, `--post` or `ultra`; Claude Security runs
`scan changes` only, never its patch job. A review that changes
code is not a review. code is not a review.
- **No finding is struck for length.** Only MINOR/SUGGESTION text is trimmed, - **No finding is struck for length.** Only MINOR/SUGGESTION text is trimmed,
and every trim is counted; what the coordinator set aside is listed under and every trim is counted; what the coordinator set aside is listed under

View file

@ -460,12 +460,12 @@ Optional but standard sections: `## Findings (BLOCKER)`, `## Findings (MAJOR)`,
Since 1.1 (ordre 5): 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`). - `## 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. - `## 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`, and Claude Security findings below confidence `high`, `engine:below-threshold`), 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), `CODE_REVIEW_IMPROVEMENT` (MINOR: a `/code-review` finding whose category is simplification, efficiency, reuse, style or readability) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security-review`. - Findings from the engines — Claude Code's own `/code-review` and one security engine, chosen by `lib/review/security-engine.mjs choose` (Claude Security where it is installed and the range has committed changes, else `/security-review`) — enter under the catalogue keys `CODE_REVIEW_DEFECT` (MAJOR), `CODE_REVIEW_IMPROVEMENT` (MINOR: a `/code-review` finding whose category is simplification, efficiency, reuse, style or readability) and `SECURITY_REVIEW_VULNERABILITY` (BLOCKER), or a narrower key, with `reviewer` `claude-code-review` / `claude-security` (engine `claude-security`) / `claude-security-review`. A Claude Security finding counts only at confidence `high` (decision 219); the rest stand under `## Declined to Judge` as `engine:below-threshold`, an unverified reason: not counted, it forbids `ALLOW`. With no security engine (`choose` gives `no-engine`, `no-origin` or `empty-diff`, or the chosen engine did not run) the expected reviewer `security-engine` never reports: `missing-reviewer:security-engine`, verdict gate exit 2 — security not measured, never 0.
- Working files of one review run, in the project folder (not part of `review.md`): the engine transcriptions `.review-engine-{code-review|security-review}.json` (`/trekreview` Phase 4.7) and, beside them, the converted payloads `.review-engine-payload-{engine}.json` (the `engine-findings.mjs` output, `{reviewer, findings, out_of_scope}`). `review-finalize.mjs` reads the payloads to record which engines ran and to put back, at its own severity, an engine finding at MAJOR or BLOCKER that no finding of `review.md` carries (the standard-path comparison); the verdict gate reads the payloads too, to compare them with `review.md`: an engine finding at MAJOR or BLOCKER missing from it is exit 2 (`review-not-measured: engine finding not in review`). `review-rounds.mjs prepare` deletes the payloads at the start of each round. - Working files of one review run, in the project folder (not part of `review.md`): the engine transcriptions `.review-engine-{code-review|security-review}.json` (`/trekreview` Phase 4.7) and, beside them, the converted payloads `.review-engine-payload-{engine}.json` (the `engine-findings.mjs` output, `{reviewer, findings, out_of_scope}`). Claude Security's is never transcribed: `.review-engine-payload-claude-security.json` is the `security-engine.mjs convert` output from its own report, `{reviewer, findings, out_of_scope, below_threshold, report}`; `below_threshold` is never compared or put back. `review-finalize.mjs` reads the payloads to record which engines ran and to put back, at its own severity, an engine finding at MAJOR or BLOCKER that no finding of `review.md` carries (the standard-path comparison); the verdict gate reads the payloads too, to compare them with `review.md`: an engine finding at MAJOR or BLOCKER missing from it is exit 2 (`review-not-measured: engine finding not in review`). `review-rounds.mjs prepare` deletes the payloads at the start of each round.
Since 1.2 (every field and section optional; a malformed one present is a strict error, a soft warning): Since 1.2 (every field and section optional; a malformed one present is a strict error, a soft warning):
- Trailing JSON block, additive: per finding `pre_existing` (boolean: the range did not add or change the cited line and `scope_sha_start` is bound to the fork point from main (the fork point itself, or a later commit reached through process files only); otherwise it is never true), `carried_from` (the previous round's finding id) and `instruction_ref` (the repo path of the instruction file a CLAUDE.md-key finding cites); top level `pre_existing_counts` (`{SEVERITY: n}`), `resolved` (the previous round's finding ids no longer raised) and `ran` (`{engines: [code-review|security-review], reviewers: [string]}`, written with `finalized: true`). Codes `REVIEW_PRE_EXISTING_MALFORMED`, `REVIEW_ROUND_MALFORMED`, `REVIEW_POLICY_MALFORMED`. - Trailing JSON block, additive: per finding `pre_existing` (boolean: the range did not add or change the cited line and `scope_sha_start` is bound to the fork point from main (the fork point itself, or a later commit reached through process files only); otherwise it is never true), `carried_from` (the previous round's finding id) and `instruction_ref` (the repo path of the instruction file a CLAUDE.md-key finding cites); top level `pre_existing_counts` (`{SEVERITY: n}`), `resolved` (the previous round's finding ids no longer raised) and `ran` (`{engines: [code-review|security-review|claude-security], reviewers: [string]}`, written with `finalized: true`). Codes `REVIEW_PRE_EXISTING_MALFORMED`, `REVIEW_ROUND_MALFORMED`, `REVIEW_POLICY_MALFORMED`.
- `## Pre-existing` — the titles and counts of the findings marked `pre_existing`; written by `review-finalize.mjs`. - `## Pre-existing` — the titles and counts of the findings marked `pre_existing`; written by `review-finalize.mjs`.
- `## Resolved since round <n>` — the previous round's findings this round no longer raises; written by `review-finalize.mjs`. - `## Resolved since round <n>` — the previous round's findings this round no longer raises; written by `review-finalize.mjs`.
- `## Answers` — the operator's answer to each counting MAJOR, written by `/trekreview` Phase 8.8 after `## Remediation Steps` and before the trailing JSON block: a pipe table with the header exactly `| Finding | Answer | Reason | Record | Carried from |`; per row a 40-hex finding id (optionally `#n`), `accepted` or `fixed`, a reason (non-empty when `accepted`), the 64-hex hash of the K2 `review` record, and `-` or `review-round-<run12>-<n>.md#<id>`; at most one row per finding. A MAJOR answered `fixed` needs its `### Step R<n>` with `Source finding: <id>` under `## Remediation Steps`. Code `REVIEW_ANSWERS_MALFORMED`. - `## Answers` — the operator's answer to each counting MAJOR, written by `/trekreview` Phase 8.8 after `## Remediation Steps` and before the trailing JSON block: a pipe table with the header exactly `| Finding | Answer | Reason | Record | Carried from |`; per row a 40-hex finding id (optionally `#n`), `accepted` or `fixed`, a reason (non-empty when `accepted`), the 64-hex hash of the K2 `review` record, and `-` or `review-round-<run12>-<n>.md#<id>`; at most one row per finding. A MAJOR answered `fixed` needs its `### Step R<n>` with `Source finding: <id>` under `## Remediation Steps`. Code `REVIEW_ANSWERS_MALFORMED`.
@ -474,13 +474,13 @@ Since 1.2 (every field and section optional; a malformed one present is a strict
**The brief-conflict mark (fields `engine`, `contradicts_brief`, `brief_lowered_from`).** **The brief-conflict mark (fields `engine`, `contradicts_brief`, `brief_lowered_from`).**
Each finding may carry three more fields, in its body field list and in the trailing JSON block: Each finding may carry three more fields, in its body field list and in the trailing JSON block:
`engine` (`code-review` | `security-review`, on a finding that came from an engine), `engine` (`code-review` | `security-review` | `claude-security`, on a finding that came from an engine),
`contradicts_brief` (the brief's Non-Goal or stated choice the finding's recommended action contradicts, verbatim) `contradicts_brief` (the brief's Non-Goal or stated choice the finding's recommended action contradicts, verbatim)
and `brief_lowered_from` (`MAJOR`, written only by the one lowering). and `brief_lowered_from` (`MAJOR`, written only by the one lowering).
The mark is a field on a kept finding, never a `suppressed_reason`. The mark is a field on a kept finding, never a `suppressed_reason`.
Only an engine finding is ever marked; a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue never are. Only an engine finding is ever marked; a reviewer-agent finding (no `engine`) and a finding whose `rule_key` is not in the catalogue never are.
The one legal lowering: a `/code-review` MAJOR-tier finding at severity MAJOR drops to MINOR, with `brief_lowered_from: MAJOR`; The one legal lowering: a `/code-review` MAJOR-tier finding at severity MAJOR drops to MINOR, with `brief_lowered_from: MAJOR`;
a BLOCKER-tier, a MINOR-tier and any `/security-review` finding keep their severity and only carry the mark. a BLOCKER-tier, a MINOR-tier and any finding from a security engine (`/security-review`, Claude Security) keep their severity and only carry the mark.
On an ALLOW or WARN review the verdict gate exits 2 on every finding `illegalBriefLowering` (`lib/review/coordinator-contract.mjs`) names: On an ALLOW or WARN review the verdict gate exits 2 on every finding `illegalBriefLowering` (`lib/review/coordinator-contract.mjs`) names:
any other `brief_lowered_from`, a mark on a reviewer-agent finding, a marked finding below its tier, a marked finding with an unknown key, any other `brief_lowered_from`, a mark on a reviewer-agent finding, a marked finding below its tier, a marked finding with an unknown key,
and an engine finding below its tier, marked or not (a BLOCK review exits 1 as before). and an engine finding below its tier, marked or not (a BLOCK review exits 1 as before).

View file

@ -9,7 +9,7 @@ Imported from `CLAUDE.md` via pointer.
- `lib/util/{frontmatter,result,atomic-write,autonomy-gate}.mjs` — shared YAML-frontmatter parser + Result helpers + `atomicWriteJson(path, obj)` for tmp+rename writes + `--gates` flag parser (v3.4.0) - `lib/util/{frontmatter,result,atomic-write,autonomy-gate}.mjs` — shared YAML-frontmatter parser + Result helpers + `atomicWriteJson(path, obj)` for tmp+rename writes + `--gates` flag parser (v3.4.0)
- `lib/parsers/{plan-schema,manifest-yaml,arg-parser,jaccard,finding-id}.mjs` — pure parsers (no I/O), unit-tested. `manifest-yaml` extended in v3.4.0 with additive `skip_commit_check` + `memory_write` flags (forward-compat: unknown keys ignored) - `lib/parsers/{plan-schema,manifest-yaml,arg-parser,jaccard,finding-id}.mjs` — pure parsers (no I/O), unit-tested. `manifest-yaml` extended in v3.4.0 with additive `skip_commit_check` + `memory_write` flags (forward-compat: unknown keys ignored)
- `lib/review/{rule-catalogue,plan-review-dedup}.mjs` — version-pinned rule catalogue (18 keys) + Phase 9 inline dedup helpers (v3.4.0) - `lib/review/{rule-catalogue,plan-review-dedup}.mjs` — version-pinned rule catalogue (18 keys) + Phase 9 inline dedup helpers (v3.4.0)
- `lib/review/{verdict-gate,engine-findings,recurrence}.mjs` — the review gate (K7: 0 ALLOW, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green), Claude Code `/code-review` + `/security-review` findings → reviewer payload, recurring finding classes → operator-approved project-memory lines (review_version 1.2); `lib/review/fill-finding-ids.mjs` computes the coordinator's `@@ID_<n>@@` placeholders over `(file, line, rule_key)`; `lib/validators/brief-review-stamp.mjs` lets `/trekplan` skip a brief review `/trekbrief` already ran on the same text - `lib/review/{verdict-gate,engine-findings,security-engine,recurrence}.mjs` — the review gate (K7: 0 ALLOW, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green), Claude Code `/code-review` + `/security-review` findings → reviewer payload, the security engine chosen (Claude Security, else `/security-review`, else "not measured") and Claude Security's report converted (confidence `high` counts, the rest `below_threshold`), recurring finding classes → operator-approved project-memory lines (review_version 1.2); `lib/review/fill-finding-ids.mjs` computes the coordinator's `@@ID_<n>@@` placeholders over `(file, line, rule_key)`; `lib/validators/brief-review-stamp.mjs` lets `/trekplan` skip a brief review `/trekbrief` already ran on the same text
- `lib/validators/{brief,research,plan,progress,session-state}-validator.mjs` — schema validators with CLI shims (`node lib/validators/X.mjs --json <path>`) - `lib/validators/{brief,research,plan,progress,session-state}-validator.mjs` — schema validators with CLI shims (`node lib/validators/X.mjs --json <path>`)
- `lib/validators/architecture-discovery.mjs` — drift-WARN external-contract discovery for `architecture/overview.md` - `lib/validators/architecture-discovery.mjs` — drift-WARN external-contract discovery for `architecture/overview.md`
- `lib/validators/query-privacy-gate.mjs` — gates an outbound research query before it leaves the machine (no command calls it now: its callers were the removed STORM steps); the hard-block tier (secret-shaped strings) is not operator-overridable, so a query that trips it must be reformulated rather than forced through. CLI shim: `node lib/validators/query-privacy-gate.mjs "<query>"` - `lib/validators/query-privacy-gate.mjs` — gates an outbound research query before it leaves the machine (no command calls it now: its callers were the removed STORM steps); the hard-block tier (secret-shaped strings) is not operator-overridable, so a query that trips it must be reformulated rather than forced through. CLI shim: `node lib/validators/query-privacy-gate.mjs "<query>"`

View file

@ -68,10 +68,10 @@ Every mode that executes refuses a plan whose approval in the approval log does
| Flag | Behavior | | Flag | Behavior |
|------|----------| |------|----------|
| _(default)_ | Claude Code `/code-review` + `/security-review` (engine, via the Skill tool, never `--fix`), then brief-conformance + code-correctness reviewers in parallel, coordinator dedup + verdict, write `{project_dir}/review.md` with the required `verdict:`, decide with `lib/review/verdict-gate.mjs` (0 ALLOW, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green), the operator answers each counting MAJOR (Phase 8.8), propose recurring finding classes as project memory (operator approves each line) | | _(default)_ | Claude Code `/code-review` + one security engine chosen by `lib/review/security-engine.mjs` (Claude Security where it is installed, else `/security-review`; with neither, security is "not measured", never 0 — the engine, via the Skill tool, never `--fix`), then brief-conformance + code-correctness reviewers in parallel, coordinator dedup + verdict, write `{project_dir}/review.md` with the required `verdict:`, decide with `lib/review/verdict-gate.mjs` (0 ALLOW, or WARN with every counting MAJOR answered · 1 BLOCK · 2 never green), the operator answers each counting MAJOR (Phase 8.8), propose recurring finding classes as project memory (operator approves each line) |
| `--project <dir>` | **Required.** Path to trekplan project folder containing `brief.md`. Review is written to `{dir}/review.md` | | `--project <dir>` | **Required.** Path to trekplan project folder containing `brief.md`. Review is written to `{dir}/review.md` |
| `--since <ref>` | Override "before" SHA for the diff range. Validated via `git rev-parse --verify` | | `--since <ref>` | Override "before" SHA for the diff range. Validated via `git rev-parse --verify` |
| `--quick` | `/code-review` + code-correctness reviewer only; skip brief-conformance reviewer, `/security-review` and the coordinator's reasonableness filter except the brief-conflict rule — fast correctness-only pass | | `--quick` | `/code-review` + code-correctness reviewer only, no security engine; skip brief-conformance reviewer and the coordinator's reasonableness filter except the brief-conflict rule — fast correctness-only pass |
| `--answers` | Only the operator's answers to the counting MAJORs (Phase 8.8) on the existing `{dir}/review.md`, then the verdict gate. No reviewer and no write of findings: it opens no new round and counts nothing against the cap | | `--answers` | Only the operator's answers to the counting MAJORs (Phase 8.8) on the existing `{dir}/review.md`, then the verdict gate. No reviewer and no write of findings: it opens no new round and counts nothing against the cap |
| `--validate` | Schema-only check on existing `{dir}/review.md` plus the verdict gate. No LLM calls. A 1.0 review without `verdict:` fails here (it still seeds a plan in soft mode) | | `--validate` | Schema-only check on existing `{dir}/review.md` plus the verdict gate. No LLM calls. A 1.0 review without `verdict:` fails here (it still seeds a plan in soft mode) |
| `--dry-run` | Print discovered scope + triage map; skip writes | | `--dry-run` | Print discovered scope + triage map; skip writes |

View file

@ -170,7 +170,7 @@ plan-input.
"counts": { "BLOCKER": 1, "MAJOR": 1, "MINOR": 1, "SUGGESTION": 1 }, "counts": { "BLOCKER": 1, "MAJOR": 1, "MINOR": 1, "SUGGESTION": 1 },
"pre_existing_counts": { "BLOCKER": 0, "MAJOR": 0, "MINOR": 0, "SUGGESTION": 0 }, "pre_existing_counts": { "BLOCKER": 0, "MAJOR": 0, "MINOR": 0, "SUGGESTION": 0 },
"resolved": [], "resolved": [],
"ran": { "engines": ["code-review", "security-review"], "reviewers": ["brief-conformance-reviewer", "code-correctness-reviewer"] }, "ran": { "engines": ["claude-security", "code-review"], "reviewers": ["brief-conformance-reviewer", "code-correctness-reviewer"] },
"allow_blocked_by": [], "allow_blocked_by": [],
"declined_to_judge": [], "declined_to_judge": [],
"known_outside": [], "known_outside": [],

View file

@ -362,3 +362,130 @@ test('[STP] /trekexecute Phase 7.2 step 4 runs prepare --dry-run after a round-2
} }
assert.doesNotMatch(part, /one more round/i); assert.doesNotMatch(part, /one more round/i);
}); });
// ---- E 3.2: the security engine in /trekreview (decisions 175, 196, 219) -----------
const P47 = () => flat(section(CMD, '## Phase 4.7'));
test('[E32] trekreview — Phase 4.7 chooses the security engine first, with security-engine.mjs choose', () => {
const p = P47();
const at = p.indexOf('lib/review/security-engine.mjs choose --repo . --base {before_sha} --head {after_sha} --claude-security');
assert.ok(at >= 0, 'the choose call is written out');
for (const call of ['Skill("security-review")', 'Skill("claude-security:claude-security"']) {
assert.ok(p.indexOf(call) > at, `${call} comes after choose`);
}
assert.match(p, /`claude-security:claude-security` skill is in this session's skill list/, 'available = the skill is listed');
assert.match(p, /the Workflow tool is among the tools you can call/, 'available = the Workflow tool is callable');
for (const r of ['no-engine', 'no-origin', 'empty-diff']) assert.ok(p.includes(`\`${r}\``), `choose's reason ${r} is named`);
});
test('[E32] trekreview — Claude Security runs through its own skill on the review range; its cost question goes to the operator', () => {
const p = P47();
const call = p.match(/Skill\("claude-security:claude-security", args: "([^"]*)"\)/);
assert.ok(call, 'the Claude Security call is written out');
assert.equal(call[1], 'scan changes --base {before_sha} --effort high');
for (const banned of ['--fix', '--comment', '--post', 'ultra', 'patch']) {
assert.ok(!call[1].includes(banned), `the Claude Security call must not carry ${banned}`);
}
assert.match(p, /put it to the operator as it stands and never answer it yourself/);
});
test('[E32] trekreview — "No", an unanswerable question or no Workflow tool is recorded not run, never a silent fallback to /security-review', () => {
const p = P47();
assert.match(p, /"No", a question nobody can answer/);
assert.match(p, /the Workflow tool missing/);
assert.match(p, /\*\*Never fall back to `\/security-review`\*\*/);
assert.match(p, /`\/security-review` runs only when `choose` picked it/);
});
test('[E32] trekreview — Claude Security\'s report is converted by security-engine.mjs convert, never transcribed; below_threshold is declined', () => {
const p = P47();
assert.ok(p.includes('lib/review/security-engine.mjs convert --report-dir .'), 'the convert call is written out');
assert.ok(p.includes('.review-engine-payload-claude-security.json'), 'the payload file finalize reads');
assert.match(p, /converted, never transcribed/);
assert.match(p, /`below_threshold`[^.]*confidence `high`/);
assert.ok(p.includes('`engine:below-threshold`'), 'the declined reason is named');
});
test('[E32] trekreview — the not-run sentence for security says "not measured" and why, never 0', () => {
const p = P47();
assert.match(p, /"security: not measured"/);
assert.match(p, /never 0/);
});
test('[E32] trekreview — the expected security reviewer is the chosen engine; none chosen is security-engine, which the verdict gate reads as exit 2; --quick keeps none', () => {
const acc = section(CMD, '### Reviewer accounting');
assert.ok(acc.includes('`claude-security`') && acc.includes('`claude-security-review`'), 'both engine reviewer names');
assert.ok(acc.includes('`security-engine`'), 'the name expected when no engine was chosen');
assert.ok(acc.includes('`missing-reviewer:security-engine`'));
assert.match(acc, /outside `quick`/);
assert.match(P47(), /In `quick` mode only `\/code-review`\s+runs: no security engine/);
});
test('[E32] trekreview — the Phase 9 Engine line names Claude Security, /security-review and "not measured"', () => {
const line = CMD.split('\n').find((l) => l.startsWith('**Engine:**'));
assert.ok(line, 'the Engine line is there');
assert.match(line, /Claude Security delivered N/);
assert.match(line, /\/security-review delivered N/);
assert.match(line, /not measured: reason/);
});
test('[E32] trekreview — frontmatter declares the Workflow tool Claude Security runs on', () => {
const fm = CMD.slice(0, CMD.indexOf('\n---', 4));
assert.match(fm, /allowed-tools:.*\bWorkflow\b/);
});
test('[E32] trekreview — the engine never edits code (Claude Security included), and its report folder is the one write outside the project folder', () => {
const hard = section(CMD, '## Hard rules');
assert.match(hard, /`\/code-review`, `\/security-review` and Claude Security run/);
assert.match(hard, /`CLAUDE-SECURITY-<ts>\/`/);
});
test('[E32] template — the ran line names claude-security', () => {
const tpl = read('templates/trekreview-template.md');
assert.match(tpl, /"ran": \{ "engines": \["claude-security", "code-review"\]/);
});
test('[E32] coordinator — Claude Security\'s below_threshold list is an input, declined as engine:below-threshold', () => {
assert.match(COORD, /`below_threshold`[\s\S]{0,200}`engine:below-threshold`/);
});
test('[E32] docs — the engine prose names Claude Security wherever /security-review is the engine', () => {
const cases = [
['CLAUDE.md', (t) => t.split('\n').find((l) => l.startsWith('| `/trekreview` |'))],
['docs/command-modes.md', (t) => t.split('\n').find((l) => l.startsWith('| _(default)_ |') && l.includes('/code-review'))],
['docs/architecture.md', (t) => t.split('\n').find((l) => l.includes('lib/review/{verdict-gate,engine-findings'))],
['commands/trekexecute.md', (t) => section(t, '## Phase 7.2')],
['README.md', (t) => t],
['docs/HANDOVER-CONTRACTS.md', (t) => section(t, '## Handover 6')],
];
for (const [rel, pick] of cases) {
const part = pick(read(rel));
assert.ok(part, `${rel}: the engine passage is there`);
assert.match(part, /Claude Security/, `${rel} names Claude Security`);
}
const quick = read('docs/command-modes.md').split('\n').find((l) => l.startsWith('| `--quick` |') && l.includes('/code-review'));
assert.ok(quick, 'the /trekreview --quick row is there');
assert.match(quick, /no security engine/);
});
test('[E32] README and SECURITY.md — Claude Security recommended, proprietary so called and never a manifest dependency; without it /security-review; with neither "not measured"', () => {
for (const rel of ['README.md', 'SECURITY.md']) {
const t = flat(read(rel));
assert.match(t, /Claude Security is (the )?recommended/, `${rel}: recommended`);
assert.match(t, /proprietary licen[cs]e/, `${rel}: the licence named`);
assert.match(t, /never a dependency in Voyage's manifest|not a dependency in Voyage's manifest/, `${rel}: not in the manifest`);
assert.match(t, /`\/security-review`/, `${rel}: the fallback named`);
assert.match(t, /not measured/, `${rel}: neither engine is "not measured"`);
}
const manifest = read('.claude-plugin/plugin.json');
assert.ok(!/claude-security/.test(manifest), 'the manifest names no Claude Security dependency');
});
test('[E32] Handover 6 — names the engine claude-security and its payload file with below_threshold', () => {
const h6 = section(read('docs/HANDOVER-CONTRACTS.md'), '## Handover 6');
assert.ok(h6.includes('.review-engine-payload-claude-security.json'));
assert.ok(h6.includes('below_threshold'));
assert.ok(h6.includes('`engine:below-threshold`'));
assert.match(h6, /`claude-security`/);
});