Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
441f63a
implement(documenter): R1 single-body submission + E2 pr-context stag…
Jul 3, 2026
c2279b6
implement(coder): R8(a) versioned structured finding schema + validator
Jul 3, 2026
6a85827
implement(tester): unit tests for R8(a) finding schema/validator (tas…
Jul 3, 2026
107e956
implement(tester): unit tests for R8(a) finding schema/validator (tas…
Jul 3, 2026
8826894
Persist BRC history for slice-1 (#2548)
Jul 3, 2026
c02da14
slice-1: prettier fixes + changeset
jwbron Jul 3, 2026
b119a01
slice-1: drop BRC history artifacts from slice PR
jwbron Jul 3, 2026
b9508a6
review: address slice-1 feedback (empty body, lib distribution, comme…
jwbron Jul 6, 2026
123a10f
review: drop lib install step; scripts run via npx tsx
jwbron Jul 7, 2026
e637ece
implement(coder): R8(b) computed verdict + R8(c) Conventional-Comment…
Jul 3, 2026
8819339
implement(tester): R8(b)/R2 verdict truth-table + R8(c) rendering sna…
Jul 3, 2026
545e711
implement(tester): prettier-format task-2-4 test files (lint gate)
Jul 3, 2026
40efb53
implement(coder): prettier 2.6.2 lint fixes for slice-2 + finding-sch…
Jul 3, 2026
f618d2a
Persist BRC history for slice-2 (#2548)
Jul 3, 2026
6058d86
slice-2: changeset
jwbron Jul 3, 2026
da5b450
slice-2: drop BRC history artifacts from slice PR
jwbron Jul 3, 2026
0950bf2
review: address slice-2 feedback (verdict precedence, hold UX, empty …
jwbron Jul 6, 2026
6cfe3e3
implement(documenter): wire deterministic router into Step 3; drop re…
Jul 3, 2026
7959787
implement(documenter): fix router invocation path to lib/router.ts (r…
Jul 3, 2026
d96789d
implement(coder): R10 deterministic router + tier-scaled budget (slic…
Jul 3, 2026
3f919f3
implement(tester): R10 router unit tests — classification, lens, tier…
Jul 3, 2026
f3130ec
implement(coder): router CLI entrypoint + routing.json serialization …
Jul 3, 2026
a6984f1
Persist BRC history for slice-3 (#2548)
Jul 3, 2026
c41a812
implement(tester): cover router v2 serialization + CLI (toRoutingJson…
Jul 3, 2026
cb8d714
slice-3: replace satisfies with prettier-2-compatible check, changeset
jwbron Jul 3, 2026
29f2b6f
slice-3: drop BRC history artifacts from slice PR
jwbron Jul 3, 2026
abdf46c
review: address slice-3 feedback (consumer-owned routing config)
jwbron Jul 6, 2026
06cbb2e
review: last-match-wins tier precedence; move ROUTING format doc to R…
jwbron Jul 7, 2026
06ac3ee
implement(documenter): E1/E3/E5/E6/E7/R3b reliability prompt edits (s…
Jul 3, 2026
5bb941d
Persist BRC history for slice-4 (#2548)
Jul 3, 2026
a21b4ce
slice-4: changeset
jwbron Jul 3, 2026
fdf05d2
slice-4: drop BRC history artifacts from slice PR
jwbron Jul 3, 2026
798016b
review: address slice-4 feedback (steering text, touched-lines scoping)
jwbron Jul 6, 2026
238a355
implement(documenter): R9 bounded-investigation instructions for revi…
Jul 3, 2026
e5d1fd2
implement(coder): R9 per-finding investigation tool-call cap (slice-5)
Jul 3, 2026
8074db2
implement(tester): R9 per-finding tool-call cap tests (task-5-3)
Jul 3, 2026
a01c756
implement(tester): cover check() refusal previews (per-finding + run-…
Jul 3, 2026
1340a42
slice-5: prettier fix + changeset
jwbron Jul 3, 2026
7599eea
review: address slice-5 feedback (wire the investigation cap to its c…
jwbron Jul 6, 2026
3f01827
review: investigation-cap call sites run via npx tsx
jwbron Jul 7, 2026
4e62371
implement(documenter): slice-6 roster framework — always-on reviewers…
Jul 3, 2026
8d21077
Persist BRC history for slice-6 (#2548)
Jul 3, 2026
481198d
slice-6: changeset
jwbron Jul 3, 2026
33de326
slice-6: drop BRC history artifacts from slice PR
jwbron Jul 3, 2026
46228e0
review: rework slice-6 roster to opt-in (no new default cost)
jwbron Jul 6, 2026
8f8cb7c
review: genericize orchestrator (uniform findings contract, roster de…
jwbron Jul 7, 2026
6ae06ac
Merge remote-tracking branch 'origin/main' into egg/pipeline-dcdad92d…
jwbron Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/review-roster-framework.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"review": minor
---

Restructure the reviewer roster: five always-on reviewers (holistic, completeness, test-adequacy, first-principles, conventions) with explicit trust and advisory constraints and named mandates; launch-default model and effort assignments per role (Opus 4.8 workhorse, xhigh for the security lens and claim validation, Fable 5 for first-principles); and the kept gates (pattern-triage, claim-validator plus refuter panel, deterministic dedup, verdict bookends, thread-reconciler) wired to the new roster.
35 changes: 35 additions & 0 deletions workflows/review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,22 @@ rule per line:

```
# <pattern> [lens=<lens>,…] [tier=trivial|low|medium|high] [direction-dependent]
# enable <reviewer>[,<reviewer>…]
services/**/migrations/** tier=high lens=data-migrations
**/*.graphql lens=api-federation-compat
pkg/auth/** tier=high direction-dependent lens=security-auth
services/**/testdata/** tier=trivial
docs/** tier=trivial
enable holistic,test-adequacy
```

- `lens=` names the specialist lenses to spawn when the pattern is touched; when
several rules match a path their lenses are unioned (lenses are additive).
- `enable` lines turn on the opt-in whole-change reviewers (`holistic`,
`completeness`, `test-adequacy`, `first-principles`, `conventions`). Neither
lenses nor opt-in reviewers run anywhere by default: a repo opts into each
explicitly, and the policy is that a reviewer earns its line here through the
eval suite.
- `tier=` assigns the path a risk tier. When several rules match, the **last
matching rule in file order wins** (gitignore/CODEOWNERS-style): write the broad
rule first and its exceptions after it, as with `services/**` and
Expand All @@ -114,6 +121,34 @@ and are skipped; routing degrades to fewer lenses, never to a crashed review.
stays the model-facing prose about file *contents*; team ownership stays in
`.github/REVIEWERS`, unchanged.

### Models and effort per role

Each sub-agent pins its model in its own definition inside `review.md` (with a
launch-default `effort:` annotation; the gh-aw Claude engine exposes no per-agent
effort field yet). The orchestrator prompt deliberately says nothing about
sub-agent models — this table is the human-facing summary:

| Role | Model | Effort | Why |
| --- | --- | --- | --- |
| orchestrator | `claude-opus-4-8` | high | Owns every GitHub/safe-output decision |
| `pattern-triage` | `claude-sonnet-4-6` | medium | Cheap first-pass triage |
| `thread-reconciler` | `claude-opus-4-8` | medium | Reconciliation |
| `correctness-reviewer` | `claude-opus-4-8` | high | Whole-change reviewer |
| `skill-auditor` | `claude-opus-4-8` | high | Whole-change reviewer |
| `holistic` | `claude-opus-4-8` | high | Opt-in whole-change reviewer (`enable` in `ROUTING`) |
| `completeness` | `claude-opus-4-8` | high | Opt-in whole-change reviewer (`enable` in `ROUTING`) |
| `test-adequacy` | `claude-opus-4-8` | high | Opt-in whole-change reviewer (`enable` in `ROUTING`) |
| `conventions` | `claude-opus-4-8` | medium | Opt-in advisory targeted check (`enable` in `ROUTING`) |
| `first-principles` | `claude-fable-5` | high | Opt-in advisory-only; reviews the change's justification |
| `claim-validator` | `claude-opus-4-8` | xhigh | Adversarial claim validation |
| specialist lenses | `claude-opus-4-8` | high | Opt-in via `lens=` in `ROUTING`; the security & auth lens is xhigh |

Only the orchestrator and the default roster (`pattern-triage`,
`correctness-reviewer`, `skill-auditor`, `thread-reconciler`, `claim-validator`)
run by default; every other row is opt-in via `ROUTING` and earns its line through
the eval suite. Per-role Fable-5 / Sonnet experiment arms are eval-suite
measurements to run after the suite exists.

### Required secrets / variables

- `ANTHROPIC_API_KEY` — used by the `claude` engine.
Expand Down
6 changes: 5 additions & 1 deletion workflows/review/lib/finding-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ export const KNOWN_LENSES = [
"deploy-infra-config",
"money-payments",
"content-i18n",
// Always-on / whole-change reviewers and triage.
// Whole-change reviewers (the default correctness pass plus the opt-in
// reviewers a repo can `enable` in its ROUTING file) and triage.
"correctness",
"conventions",
"pattern-triage",
"first-principles",
"holistic",
"completeness",
"test-adequacy",
] as const;

export type Lens = typeof KNOWN_LENSES[number];
Expand Down
54 changes: 54 additions & 0 deletions workflows/review/lib/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,3 +755,57 @@ describe("parseRoutingConfig", () => {
expect(config.warnings.join("\n")).toContain("no lens= or tier=");
});
});

describe("parseRoutingConfig: enable directives", () => {
it("collects enabled reviewers in canonical order, deduped", () => {
const config = parseRoutingConfig(
["enable test-adequacy", "enable holistic,test-adequacy"].join(
"\n",
),
);
expect(config.enabledReviewers).toEqual(["holistic", "test-adequacy"]);
expect(config.warnings).toEqual([]);
});

it("warns on an unknown reviewer and keeps the known ones", () => {
const config = parseRoutingConfig("enable holistic,skynet");
expect(config.enabledReviewers).toEqual(["holistic"]);
expect(config.warnings.join("\n")).toContain(
'unknown reviewer "skynet"',
);
});

it("warns on a bare enable line", () => {
const config = parseRoutingConfig("enable");
expect(config.enabledReviewers).toEqual([]);
expect(config.warnings.join("\n")).toContain("names no reviewer");
});

it("defaults to no enabled reviewers", () => {
expect(
parseRoutingConfig("docs/** tier=trivial").enabledReviewers,
).toEqual([]);
});
});

describe("runCli: enabled reviewers", () => {
it("surfaces enable directives in routing.json", () => {
const {fs} = fakeFs({
["/tmp/gh-aw/review/files.json"]: JSON.stringify([
{path: "a.ts", status: "modified"},
]),
[ROUTING_CONFIG_PATH]: "enable holistic,first-principles",
});
const json = runCli(fs);
expect(json.enabledReviewers).toEqual(["holistic", "first-principles"]);
});

it("emits no enabled reviewers without a ROUTING config", () => {
const {fs} = fakeFs({
["/tmp/gh-aw/review/files.json"]: JSON.stringify([
{path: "a.ts", status: "modified"},
]),
});
expect(runCli(fs).enabledReviewers).toEqual([]);
});
});
52 changes: 41 additions & 11 deletions workflows/review/lib/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@
import {KNOWN_LENSES} from "./finding-schema";
import type {Lens} from "./finding-schema";
import {
ENABLEABLE_REVIEWERS,
parseRoutingConfig,
RISK_TIERS,
ROUTING_CONFIG_PATH,
} from "./routing-config";
import type {
EnableableReviewer,
LensRule,
RiskRule,
RiskTier,
Expand All @@ -46,25 +48,41 @@ import type {

// Re-exported so consumers (and the tests) can treat the router as the single
// entry point for routing vocabulary and the ROUTING parser.
export {parseRoutingConfig, RISK_TIERS, ROUTING_CONFIG_PATH};
export type {LensRule, RiskRule, RiskTier, RoutingFileConfig};
export {
ENABLEABLE_REVIEWERS,
parseRoutingConfig,
RISK_TIERS,
ROUTING_CONFIG_PATH,
};
export type {
EnableableReviewer,
LensRule,
RiskRule,
RiskTier,
RoutingFileConfig,
};

/* -------------------------------------------------------------------------- */
/* Lens taxonomy */
/* -------------------------------------------------------------------------- */

/**
* The always-on / whole-change reviewers and triage. These run every review
* regardless of routing, so they are NOT part of the router's `lensesToSpawn`
* (which names only the *specialist* lenses gated by touched paths). Kept here
* as the complement of {@link SPECIALIST_LENSES} so the two lists cannot drift
* from the canonical `KNOWN_LENSES`.
* The whole-change reviewers and triage: never path-gated, so NOT part of the
* router's `lensesToSpawn` (which names only the *specialist* lenses gated by
* touched paths). Whether each runs is a separate question — the default
* roster always does, and the opt-in ones ({@link ENABLEABLE_REVIEWERS}) run
* only when the repo's ROUTING file enables them (`enabledReviewers`). Kept
* here as the complement of {@link SPECIALIST_LENSES} so the two lists cannot
* drift from the canonical `KNOWN_LENSES`.
*/
export const ALWAYS_ON_LENSES = [
"correctness",
"conventions",
"pattern-triage",
"first-principles",
"holistic",
"completeness",
"test-adequacy",
] as const;

// `satisfies readonly Lens[]` is the natural spelling, but the repo's
Expand Down Expand Up @@ -761,6 +779,11 @@ export type RoutingJson = {
perFileTier: Record<string, DisplayTier>;
runBudget: RunBudget;
pendingRiskQuestions: RiskQuestion[];
/**
* Opt-in whole-change reviewers the repo's `ROUTING` file enables
* (`enable <reviewer>` lines). Empty means the default roster only.
*/
enabledReviewers: EnableableReviewer[];
/**
* Whether the consumer `ROUTING` file was found, plus any parse warnings
* (or the missing-file warning). The orchestrator surfaces these in the
Expand All @@ -781,6 +804,7 @@ export type RoutingJson = {
export const toRoutingJson = (
result: RoutingResult,
routingConfig: RoutingJson["routingConfig"] = {present: true, warnings: []},
enabledReviewers: EnableableReviewer[] = [],
): RoutingJson => {
const owners: Record<string, string[]> = {};
for (const file of result.perFile) {
Expand All @@ -800,6 +824,7 @@ export const toRoutingJson = (
perFileTier,
runBudget: result.runBudget,
pendingRiskQuestions: result.pendingRiskQuestions,
enabledReviewers,
routingConfig,
};
};
Expand Down Expand Up @@ -878,6 +903,7 @@ export const runCli = (fs: FsLike, repoRoot = "."): RoutingJson => {
: {
lensRules: [],
riskRules: [],
enabledReviewers: [],
warnings: [
`routing config missing (${ROUTING_CONFIG_PATH}): no ` +
`specialist lenses will run; always-on reviewers only ` +
Expand All @@ -903,10 +929,14 @@ export const runCli = (fs: FsLike, repoRoot = "."): RoutingJson => {
lensRules: routingFileConfig.lensRules,
riskRules: routingFileConfig.riskRules,
});
const json = toRoutingJson(result, {
present: routingConfigPresent,
warnings: routingFileConfig.warnings,
});
const json = toRoutingJson(
result,
{
present: routingConfigPresent,
warnings: routingFileConfig.warnings,
},
routingFileConfig.enabledReviewers,
);

fs.mkdirSync(REVIEW_DIR, {recursive: true});
fs.writeFileSync(ROUTING_OUT, JSON.stringify(json, null, 2));
Expand Down
65 changes: 60 additions & 5 deletions workflows/review/lib/routing-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,29 @@ export type RiskRule = {
/** Where a consuming repo keeps its routing map, next to its review config. */
export const ROUTING_CONFIG_PATH = ".github/aw/review/ROUTING";

/**
* The opt-in whole-change reviewers a repo may `enable` in its ROUTING file.
* None run by default: each costs credits on every PR, so a repo turns one on
* only once the eval suite shows it earns its keep. (The default roster —
* correctness, skill audit, triage, reconciliation, validation — needs no
* enabling.)
*/
export const ENABLEABLE_REVIEWERS = [
"holistic",
"completeness",
"test-adequacy",
"first-principles",
"conventions",
] as const;

export type EnableableReviewer = typeof ENABLEABLE_REVIEWERS[number];

/** Parsed `.github/aw/review/ROUTING` config. */
export type RoutingFileConfig = {
lensRules: LensRule[];
riskRules: RiskRule[];
/** Opt-in whole-change reviewers this repo enables (canonical order). */
enabledReviewers: EnableableReviewer[];
/** Fixed-format parse warnings (unknown lens/tier, no-op rule). */
warnings: string[];
};
Expand All @@ -56,9 +75,10 @@ const KNOWN_LENS_SET: ReadonlySet<string> = new Set(KNOWN_LENSES);

/**
* Parse the consumer-owned routing map. Line grammar, `REVIEWERS`-style —
* blanks and `#` comments skipped, one rule per line:
* blanks and `#` comments skipped, one rule or directive per line:
*
* <pattern> [lens=<lens>[,<lens>…]] [tier=trivial|low|medium|high] [direction-dependent]
* enable <reviewer>[,<reviewer>…]
*
* `lens=` names specialist lenses to spawn when the pattern is touched (multiple
* matching rules union their lenses). `tier=` assigns a risk tier; when several
Expand All @@ -68,14 +88,17 @@ const KNOWN_LENS_SET: ReadonlySet<string> = new Set(KNOWN_LENSES);
* subtree beneath it). `direction-dependent` marks a tier
* that cannot be finalised from the path alone (tightening vs. loosening; see
* {@link RiskRule.diffDirectionDependent}) and requires `tier=`.
* `enable` turns on an opt-in whole-change reviewer
* ({@link ENABLEABLE_REVIEWERS}) for every review in this repo.
*
* Malformed fields and unknown lens names produce a warning and skip the lens or
* line rather than aborting the run: routing degrades to fewer lenses, never to
* a crashed review.
* Malformed fields and unknown lens/reviewer names produce a warning and skip
* the lens or line rather than aborting the run: routing degrades to fewer
* reviewers, never to a crashed review.
*/
export const parseRoutingConfig = (content: string): RoutingFileConfig => {
const lensRules: LensRule[] = [];
const riskRules: RiskRule[] = [];
const enabled = new Set<EnableableReviewer>();
const warnings: string[] = [];

const lines = content.split(/\r?\n/);
Expand All @@ -87,6 +110,31 @@ export const parseRoutingConfig = (content: string): RoutingFileConfig => {
const lineNo = index + 1;
const [pattern, ...fields] = line.split(/\s+/);

if (pattern === "enable") {
const names = fields.flatMap((field) => field.split(","));
if (names.length === 0) {
warnings.push(
`ROUTING line ${lineNo}: enable names no reviewer (line skipped)`,
);
continue;
}
for (const name of names) {
if (name === "") {
continue;
}
if (
(ENABLEABLE_REVIEWERS as readonly string[]).includes(name)
) {
enabled.add(name as EnableableReviewer);
} else {
warnings.push(
`ROUTING line ${lineNo}: unknown reviewer "${name}" (skipped)`,
);
}
}
continue;
}

const lenses = new Set<Lens>();
let tier: RiskTier | undefined;
let directionDependent = false;
Expand Down Expand Up @@ -154,5 +202,12 @@ export const parseRoutingConfig = (content: string): RoutingFileConfig => {
}
}

return {lensRules, riskRules, warnings};
return {
lensRules,
riskRules,
enabledReviewers: ENABLEABLE_REVIEWERS.filter((reviewer) =>
enabled.has(reviewer),
),
warnings,
};
};
Loading
Loading