feat(review): the security engine chosen and Claude Security converted (E 3.1)

lib/review/security-engine.mjs (decisions 175, 196, 219):
- choose: Claude Security when available and base..head has committed
  changes; else /security-review when origin/HEAD resolves and
  origin/HEAD...HEAD is non-empty; else no engine (no-engine, no-origin,
  empty-diff), which is "not measured", never 0. A git failure or bad
  usage is exit 2, never a reason.
- convert: reads the newest CLAUDE-SECURITY-<ts>/ report whose stamp is a
  changes scan of exactly base..head; confidence high becomes engine
  findings through toReviewerPayload, the rest go to below_threshold and
  never into findings. An unverified scan, missing researcher counts,
  fewer researchers returned than dispatched, an empty diff or scope, or
  a malformed record is exit 2.
- NO_ENGINE_REVIEWER: with no engine chosen the expected reviewer
  security-engine never reports, so the verdict gate exits 2.

ENGINES gains claude-security (its own reviewer and brief_ref, the
engines' key) with a per-engine label for the written action; it is a
security engine in isSecurityFinding, RAN_ENGINES, the coordinator
contract (out-of-scope and brief-conflict rules) and the coordinator
prose; engine:below-threshold is an unverified reason fed like
engineOutOfScope. Pins of the changed coordinator prose follow.

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

View file

@ -23,6 +23,7 @@ import { issue, ok, fail } from '../util/result.mjs';
import { isMain } from '../util/is-main.mjs';
import { extractFindingsBlock } from '../review/findings-schema.mjs';
import { isKnownEntry } from '../review/threat-model.mjs';
import { ENGINES } from '../review/engine-findings.mjs';
export const REVIEW_REQUIRED_FRONTMATTER = [
'type',
@ -86,7 +87,8 @@ const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const SHA256_RE = /^[0-9a-f]{64}$/;
const SEVERITIES = ['BLOCKER', 'MAJOR', 'MINOR', 'SUGGESTION'];
const REVIEW_POLICY_VALUES = ['absent', 'applied', 'not-applied'];
const RAN_ENGINES = ['code-review', 'security-review'];
// The engines a review's `ran` may name: engine-findings.mjs ENGINES (Claude Security since E 3.1).
const RAN_ENGINES = Object.keys(ENGINES);
const ANSWERS_HEADER = '| Finding | Answer | Reason | Record | Carried from |';
const ANSWER_FINDING_RE = /^([0-9a-f]{40})(?:#\d+)?$/;
const CARRIED_FROM_RE = /^review-round-[0-9a-f]{12}-\d+\.md#[0-9a-f]{40}$/;