Summary
Triggered after all batches are green and the merged diff is available. Eagerly reads the reviewer INDEX.md (index layer only). Evaluates each reviewer's predicate against the diff using the predicate DSL (leaf: paths, new_files_in, diff_contains, always; composites: any, all, not). Loads body files only for matching reviewers. Runs matched reviewers in parallel (3–5 cap). Aggregates findings into docs/findings.json conforming to findings schema §2. Computes in_scope per finding. Respects each reviewer's priority_floor.
Procedure
Step 1 — Guard: check for empty diff
Before loading any substrate, inspect the diff:
If the diff is empty (no changed lines), output:
No diff present — review-fanout skipped.Do not proceed further. No
docs/findings.jsonis written.If the diff is non-empty, continue to Step 2.
Step 2 — Load the reviewer index (eager)
Read .substrate/reviewers/INDEX.md in full.
Do not open any individual reviewer body files yet. The index contains the human-readable "Fires on" column as a hint, but the authoritative predicate lives in each reviewer's body frontmatter — do not evaluate predicates from the index summary alone.
Record the list of all reviewers: their ID and Path fields. This is the candidate set.
Step 3 — Evaluate predicates
For each reviewer in the candidate set, evaluate whether its predicate matches the diff. To evaluate a reviewer's predicate:
- Open the reviewer's body file (e.g.,
.substrate/reviewers/<id>.md) to read its YAML frontmatter. - Extract the
predicate:field. - Evaluate the predicate against the diff using the rules below.
- If the predicate evaluates to
true, mark the reviewer as matched. Iffalse, discard the reviewer — do not load its body further.
Predicate evaluation rules:
Leaf forms:
paths: [glob, ...]— evaluates totrueif any file path present in the diff (both added and modified files) matches any of the listed globs. Use standard glob matching (**matches any path segment,*matches within one segment).new_files_in: [glob, ...]— evaluates totrueif any newly-added file in the diff (status: added, not modified) matches any of the listed globs.diff_contains: [substring, ...]— evaluates totrueif the full diff text contains any of the listed literal substrings.always: true— evaluates totrueunconditionally, regardless of diff content.
Composite forms (recursive; operands are themselves predicates):
any: [pred, ...]— evaluates totrueif at least one operand evaluates totrue.all: [pred, ...]— evaluates totrueif all operands evaluate totrue.not: pred— evaluates totrueif the operand evaluates tofalse.
Evaluate leaf forms first, then compose. Composites may nest arbitrarily.
Step 4 — Select reviewers (3–5 cap)
Collect all matched reviewers from Step 3. If none match, output:
No reviewers matched the current diff — docs/findings.json written with an empty findings array.
Write docs/findings.json with {"findings": []} and stop.
If more than 5 reviewers match, select the top 5 by this priority order:
- Reviewers with
priority_floor: P1first (highest impact). - Then reviewers with
priority_floor: P2. - Then reviewers with no
priority_floororpriority_floor: P3. - Within each tier, preserve the order they appear in INDEX.md.
If 3 or fewer reviewers match, use all of them. Do not pad to reach 3 — the cap is an upper bound, not a minimum.
Step 5 — Run matched reviewers in parallel
For each selected reviewer, run its review against the diff in parallel. Each reviewer's body (already loaded in Step 3) declares its brief identity and checklist — use that to drive the review.
For each reviewer:
Read the reviewer's full body (the body was fetched in Step 3; do not re-fetch).
Apply the reviewer's brief identity and checklist to the diff.
Emit 0 or more findings. Each finding must have:
id:fnd-<reviewer-id>-<NNN>(three-digit sequence, e.g.fnd-security-sentinel-001)reviewer: the reviewer's ID slugpriority: one ofP1,P2,P3in_scope: computed in Step 6title: imperative-verb phrasing, ≤120 charsdescription: 1–3 paragraphs; include a reproducer if availablelocation(optional):{path, line_start, line_end}when the finding is location-boundreproducer(optional): code or steps that reproduce the findingsuggested_fix(optional): reviewer's actionable suggestion
Apply
priority_floor: if a reviewer haspriority_floor: P1, it may only emitP1findings. Ifpriority_floor: P2, it may emitP1orP2findings. Ifpriority_floor: P3or absent, it may emit any priority. Drop any finding whosepriorityis below the reviewer'spriority_floor— do not include it in the output.
Step 6 — Compute in_scope for each finding
After all reviewers emit their findings, compute the in_scope boolean for each finding that has a location.path:
- Read
docs/plan.slices.yml(the slice DAG). - For each finding with a
location.path:- Set
in_scope: trueiflocation.pathmatches any glob in any slice'stouched_pathslist. - Set
in_scope: falseotherwise.
- Set
- For findings without a
location.path(not location-bound):- Set
in_scope: false.
- Set
Use standard glob matching (same rules as predicate paths evaluation).
Step 7 — Aggregate and write findings
Collect all findings from all matched reviewers after Step 6 completes. Sort findings:
- P1 findings first, then P2, then P3.
- Within each priority tier, preserve the order reviewers appear in INDEX.md (findings from earlier-indexed reviewers first).
- Within one reviewer, preserve the order the reviewer emitted them.
Ensure all id values are unique across the aggregated list. If two reviewers happen to produce conflicting ids, append a disambiguator (e.g., -a, -b).
Write docs/findings.json:
{
"findings": [
{
"id": "fnd-<reviewer-id>-001",
"reviewer": "<reviewer-id>",
"priority": "P1",
"in_scope": true,
"location": {
"path": "src/auth/login.ts",
"line_start": 42,
"line_end": 58
},
"title": "Sanitize user input before passing to SQL query",
"description": "The login handler passes `req.body.username` directly into a template string used in a SQL query. An attacker can inject arbitrary SQL.\n\nReproducer: POST /login with username `' OR 1=1 --`.",
"reproducer": "curl -X POST /login -d 'username=' OR 1=1 --&password=x'",
"suggested_fix": "Use parameterized queries: `db.query('SELECT * FROM users WHERE username = $1', [req.body.username])`."
}
]
}
If there are no findings after applying priority floors, write {"findings": []}.
Confirm the path docs/findings.json to the user and state the finding count by priority (e.g., "2 P1, 3 P2, 0 P3").
Constraints
- Never load a reviewer body before evaluating its predicate from the index — index first, body only on match.
- Never run more than 5 reviewers in one fanout. Cap selection by the priority-tier rule in Step 4.
- Never emit findings below a reviewer's
priority_floor. Drop them silently. - An empty diff must stop immediately (Step 1) — no substrate reads, no findings written.
- A reviewer with
always: truein its predicate fires on every non-empty diff. in_scopemust be computed from glob matching against the actualtouched_pathsfromdocs/plan.slices.yml, not from the reviewer's predicate paths.- All
idvalues indocs/findings.jsonmust be unique and match the pattern^fnd-[a-z0-9]+(-[a-z0-9]+)*$. - The output file is JSON (findings are machine artifacts consumed by downstream phases). Do not write findings in Markdown or YAML.
- Do not resolve findings. Resolving is the concern of
resolve-finding. - Do not modify reviewer entries. Reviewer roster management is the concern of
consolidate.