Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/review-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ already-enabled gate.
| AI review BYOK | `gate.aiReview.byok` | `aiReviewByok` | bool | `false` | When `true` and a provider key is configured, the *advisory* write-up uses the maintainer's frontier model. The consensus blocker always uses the free Workers-AI pair, so BYOK never changes who can be blocked. |
| AI review provider | `gate.aiReview.provider` | `aiReviewProvider` | `anthropic` / `openai` / `null` | `null` | `null` = use the stored key's own provider. Must match the stored key's provider or BYOK is skipped (Workers-AI fallback). The key itself is only in the encrypted key store. |
| AI review model | `gate.aiReview.model` | `aiReviewModel` | string / `null` | `null` | Model override for the BYOK advisory write-up (e.g. `claude-3-5-sonnet-latest`). `null` = the key record's model, else a conservative per-provider default. |
| AI close confidence | `gate.aiReview.closeConfidence` | `aiReviewCloseConfidence` | number 0–1 (nullable) | `null` (engine uses `0.9`) | Minimum **calibrated** AI-reviewer confidence for a consensus defect / split to **block** under `aiReview.mode: block`. Below-threshold AI defects stay advisory (visible, never close). Each reviewer rates its own confidence; consensus carries the weaker reviewer's. Config-as-code only (no dashboard/DB column). |

### Guardrails and scope (focus manifest)

Expand Down
13 changes: 13 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3356,6 +3356,9 @@ export function gateCheckPolicy(
qualityGateMode: settings.qualityGateMode,
qualityGateMinScore: settings.qualityGateMinScore ?? null,
aiReviewGateMode: settings.aiReviewMode,
// Calibrated AI close-confidence floor (#7) — config-as-code via `.gittensory.yml gate.aiReview.closeConfidence`,
// resolved into settings upstream. `null`/undefined ⇒ advisory.ts applies the 0.9 default.
aiReviewCloseConfidence: settings.aiReviewCloseConfidence ?? null,
readinessScore: readinessScore ?? null,
slopGateMode: settings.slopGateMode,
mergeReadinessGateMode: settings.mergeReadinessGateMode,
Expand Down Expand Up @@ -3762,6 +3765,8 @@ export async function runAiReviewForAdvisory(
detail: result.consensusDefect.detail,
action:
"Resolve the flagged defect, or override if the AI reviewers are mistaken, then re-run the gate.",
// Calibrated confidence (#8): the gate blocks this defect only when it clears aiReviewCloseConfidence.
confidence: result.consensusDefect.confidence,
});
} else if (result.split) {
// The reviewers DISAGREED — exactly one flagged a blocking defect. reviewbot's quorum: ANY reviewer
Expand All @@ -3775,6 +3780,14 @@ export async function runAiReviewForAdvisory(
"One AI reviewer independently flagged a concrete must-fix defect in this change (the other did not). Under the quorum rule, a single rejection closes the PR; see the review notes for specifics.",
action:
"Resolve the flagged defect and open a new pull request, or override if the reviewers are mistaken.",
// Calibrated confidence (#8) of the lone flagging reviewer; the gate blocks only when it clears
// aiReviewCloseConfidence. A consensus split ALWAYS carries this (combineReviews sets it whenever split is
// true), so the spread is effectively unconditional; the guard is a defensive belt-and-braces — an absent
// value would degrade to 1.0 in the threshold check (advisory.ts `?? 1`), matching today's always-block.
/* v8 ignore next 3 -- a split always carries splitConfidence; the absent arm is an unreachable guard. */
...(result.splitConfidence !== undefined
? { confidence: result.splitConfidence }
: {}),
});
} else if (result.inconclusive) {
// Fail-CLOSED (#ai-fail-closed): block-mode AI could not return a usable verdict. Hold the PR for a human
Expand Down
30 changes: 24 additions & 6 deletions src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export type GateCheckPolicy = {
/** When `block`, a dual-model AI consensus defect (`ai_consensus_defect` finding) becomes a hard
* blocker. Defaults to advisory — AI never blocks unless the maintainer opts in. */
aiReviewGateMode?: GateRuleMode | undefined;
/** Minimum calibrated confidence (0-1) for an AI-judgment defect (`ai_consensus_defect` / `ai_review_split`) to
* BLOCK under `aiReviewGateMode: block` (#7). The finding blocks only when its `confidence >= this`; below-threshold
* AI defects stay advisory (visible, never block). `null`/undefined ⇒ the 0.9 default. A finding with no
* confidence (deterministic, or a graceful-fallback AI defect) is treated as 1.0 and always clears the floor —
* matching the historical always-block behavior. */
aiReviewCloseConfidence?: number | null | undefined;
readinessScore?: number | null | undefined;
/** When `block`, the deterministic slop score becomes a hard blocker once `slopRisk >= slopGateMinScore`
* (default threshold 60, the `high` band). Defaults to off/advisory — slop never blocks unless opted in. */
Expand Down Expand Up @@ -477,7 +483,7 @@ function evaluateGateCheckCore(advisoryResult: Advisory, policy: GateCheckPolicy
// Merge-readiness composite (#551): when set, escalate every sub-gate to its mode so they roll into one
// pass/fail. When off, this is a no-op and each sub-gate keeps its own mode.
const effective = applyMergeReadinessGate(policy);
const configuredBlockers = advisoryResult.findings.filter((finding) => isConfiguredGateBlocker(finding.code, effective));
const configuredBlockers = advisoryResult.findings.filter((finding) => isConfiguredGateBlocker(finding, effective));
const qualityBlocker = buildQualityGateBlocker(effective);
const slopBlocker = buildSlopGateBlocker(effective);
const blockers = [...configuredBlockers, ...(qualityBlocker ? [qualityBlocker] : []), ...(slopBlocker ? [slopBlocker] : [])];
Expand Down Expand Up @@ -810,17 +816,29 @@ function isEvaluationBlocker(code: string): boolean {
return code === "repo_not_registered" || code === "repo_not_seen" || code === "pr_not_cached" || code === "pre_merge_check_unresolved";
}

function isConfiguredGateBlocker(code: string, policy: GateCheckPolicy): boolean {
// Default minimum calibrated confidence for an AI defect to BLOCK (#7) — used when the repo set `aiReview: block`
// without a `closeConfidence`. 0.9 = block only on a high-confidence AI defect; below that stays advisory.
const DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE = 0.9;

function isConfiguredGateBlocker(finding: AdvisoryFinding, policy: GateCheckPolicy): boolean {
const code = finding.code;
// Missing linked issue defaults to ADVISORY — issues aren't always available, so it only blocks when a
// repo explicitly opts in with linkedIssueGateMode: "block". Duplicates still default to blocking.
if (code === "missing_linked_issue") return gateMode(policy.linkedIssueGateMode ?? "advisory") === "block";
if (code === "duplicate_pr_risk") return gateMode(policy.duplicatePrGateMode ?? "block") === "block";
// A dual-model AI consensus defect blocks ONLY when the maintainer opted into aiReview: block. It is the
// most conservative AI signal (two independent models, high confidence) but still confirmed-contributor
// gated by evaluateGateCheck, and advisory by default.
// most conservative AI signal (two independent models) but still confirmed-contributor gated by
// evaluateGateCheck, and advisory by default.
// A consensus defect (both reviewers) OR a SPLIT (one reviewer flagged a blocker the other did not) both block
// when aiReviewGateMode is `block` — reviewbot's quorum: ANY reviewer rejection closes the PR. (#ai-review-split)
if (code === "ai_consensus_defect" || code === "ai_review_split") return gateMode(policy.aiReviewGateMode ?? "advisory") === "block";
// when aiReviewGateMode is `block` AND the finding's CALIBRATED confidence clears the close-confidence floor (#7):
// the historical hardcoded `confidence: 1` always blocked, so a below-floor AI defect now stays advisory (visible,
// never closes) instead of false-closing. A finding with no confidence (graceful fallback) is treated as 1.0 and
// always clears the floor — byte-identical to today's behavior. (#ai-review-split)
if (code === "ai_consensus_defect" || code === "ai_review_split") {
if (gateMode(policy.aiReviewGateMode ?? "advisory") !== "block") return false;
const confidence = finding.confidence ?? 1;
return confidence >= (policy.aiReviewCloseConfidence ?? DEFAULT_AI_REVIEW_CLOSE_CONFIDENCE);
}
// A leaked-secret finding (`secret_leak`) ALWAYS hard-blocks: a committed credential must be removed and
// rotated before merge, with no opt-in. This finding is produced ONLY by the flag-gated safety scan
// (GITTENSORY_REVIEW_SAFETY); when the flag is off the finding never exists, so this branch is unreachable and the
Expand Down
1 change: 1 addition & 0 deletions src/rules/predicted-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ export function buildPredictedGateVerdict(args: {
qualityGateMode: gate.readinessMode ?? undefined,
qualityGateMinScore: gate.readinessMinScore ?? null,
aiReviewGateMode: gate.aiReviewMode ?? undefined,
aiReviewCloseConfidence: gate.aiReviewCloseConfidence ?? null,
mergeReadinessGateMode: gate.mergeReadiness ?? undefined,
// #12: only meaningful when changed paths were supplied (the policy findings are pushed above only then);
// absent paths ⇒ no manifest finding exists, so this mode has nothing to act on (byte-identical).
Expand Down
Loading
Loading