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
28 changes: 28 additions & 0 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,24 @@ gate:
# app(s) YOU run; loopover hardcodes no vendor. List of { name, appSlug }, or omit. Default: not
# configured (byte-identical behavior for every repo that doesn't opt in). Config-as-code only — no DB
# column or dashboard toggle.
# Review settings used INSTEAD of the repo defaults when a PR touches a `hardGuardrailGlobs` path
# (#9808/#9821) — so a guarded path buys MORE SCRUTINY rather than an automatic manual-review hold.
#
# Without this, a guardrail hit changes exactly one thing: it suppresses auto-merge and queues a human. The
# review itself is identical to any other file's — same model, same effort, single pass. That turns a
# protected path into maintainer toil without the analysis actually going up, and on a busy repo it is the
# single largest source of manual reviews.
#
# Any subset may be set; each unset field falls through to the repo value, then the global env. The common
# shape is "same model, think harder" — set only `effort` (and optionally `selfConsistencyRuns`) and the
# model/provider are inherited untouched. Manual review remains the fallback: if the escalated review is
# not clean, the PR still holds.
guardrailEscalation:
effort: high
selfConsistencyRuns: 3
# provider: anthropic
# model: claude-opus-5

advisoryCheckRuns:
- name: Contributor trust
appSlug: example-security-app
Expand Down Expand Up @@ -426,6 +444,16 @@ gate:
# String or null. Default: null (the key record's model, else a conservative
# per-provider default).
model: null
# Reasoning effort for the review pass: low | medium | high | xhigh | max (#9821). Per-repo parity with
# the global CLAUDE_AI_EFFORT / CODEX_AI_EFFORT env vars, which used to be the ONLY place this could be
# set even though provider and model were already per-repo. Unset ⇒ whatever the env configures
# (medium by default). Higher effort costs more tokens and runs longer — raise it for repos where a
# missed defect is expensive, not everywhere.
effort: medium
# TOTAL evaluations per review, primary included (#9821). Per-repo parity with
# AI_REVIEW_SELF_CONSISTENCY_RUNS. Clamped to {0, 2, 3}: one run cannot measure agreement, and the
# benefit saturates by three. 0 (default) = a single pass. Each extra run is a full paid review.
selfConsistencyRuns: 0
# Minimum calibrated AI-reviewer confidence (0-1). Under `mode: block`,
# consensus and split AI-review defects still BLOCK the gate regardless of
# this floor — what varies below it is `lowConfidenceDisposition` below.
Expand Down
45 changes: 45 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -10146,6 +10146,51 @@
"appSlug"
]
}
},
"aiReviewEffort": {
"type": "string",
"nullable": true,
"enum": [
"low",
"medium",
"high",
"xhigh",
"max",
null
]
},
"aiReviewSelfConsistencyRuns": {
"type": "number",
"nullable": true
},
"guardrailEscalationProvider": {
"type": "string",
"nullable": true,
"enum": [
"anthropic",
"openai",
null
]
},
"guardrailEscalationModel": {
"type": "string",
"nullable": true
},
"guardrailEscalationEffort": {
"type": "string",
"nullable": true,
"enum": [
"low",
"medium",
"high",
"xhigh",
"max",
null
]
},
"guardrailEscalationSelfConsistencyRuns": {
"type": "number",
"nullable": true
}
},
"required": [
Expand Down
28 changes: 28 additions & 0 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,24 @@ gate:
# app(s) YOU run; loopover hardcodes no vendor. List of { name, appSlug }, or omit. Default: not
# configured (byte-identical behavior for every repo that doesn't opt in). Config-as-code only — no DB
# column or dashboard toggle.
# Review settings used INSTEAD of the repo defaults when a PR touches a `hardGuardrailGlobs` path
# (#9808/#9821) — so a guarded path buys MORE SCRUTINY rather than an automatic manual-review hold.
#
# Without this, a guardrail hit changes exactly one thing: it suppresses auto-merge and queues a human. The
# review itself is identical to any other file's — same model, same effort, single pass. That turns a
# protected path into maintainer toil without the analysis actually going up, and on a busy repo it is the
# single largest source of manual reviews.
#
# Any subset may be set; each unset field falls through to the repo value, then the global env. The common
# shape is "same model, think harder" — set only `effort` (and optionally `selfConsistencyRuns`) and the
# model/provider are inherited untouched. Manual review remains the fallback: if the escalated review is
# not clean, the PR still holds.
guardrailEscalation:
effort: high
selfConsistencyRuns: 3
# provider: anthropic
# model: claude-opus-5

advisoryCheckRuns:
- name: Contributor trust
appSlug: example-security-app
Expand Down Expand Up @@ -440,6 +458,16 @@ gate:
# String or null. Default: null (the key record's model, else a conservative
# per-provider default).
model: null
# Reasoning effort for the review pass: low | medium | high | xhigh | max (#9821). Per-repo parity with
# the global CLAUDE_AI_EFFORT / CODEX_AI_EFFORT env vars, which used to be the ONLY place this could be
# set even though provider and model were already per-repo. Unset ⇒ whatever the env configures
# (medium by default). Higher effort costs more tokens and runs longer — raise it for repos where a
# missed defect is expensive, not everywhere.
effort: medium
# TOTAL evaluations per review, primary included (#9821). Per-repo parity with
# AI_REVIEW_SELF_CONSISTENCY_RUNS. Clamped to {0, 2, 3}: one run cannot measure agreement, and the
# benefit saturates by three. 0 (default) = a single pass. Each extra run is a full paid review.
selfConsistencyRuns: 0
# Minimum calibrated AI-reviewer confidence (0-1). Under `mode: block`,
# consensus and split AI-review defects still BLOCK the gate regardless of
# this floor — what varies below it is `lowConfidenceDisposition` below.
Expand Down
6 changes: 6 additions & 0 deletions packages/loopover-contract/src/api-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,12 @@ export const RepositorySettingsSchema = z
expectedCiContexts: z.array(z.string()).readonly().nullable().optional(),
advisoryCheckRuns: z.array(z.object({ name: z.string(), appSlug: z.string() })).readonly().nullable().optional(),
ignoredCheckRuns: z.array(z.object({ name: z.string(), appSlug: z.string() })).readonly().nullable().optional(),
aiReviewEffort: z.enum(["low", "medium", "high", "xhigh", "max"]).nullable().optional(),
aiReviewSelfConsistencyRuns: z.number().nullable().optional(),
guardrailEscalationProvider: z.enum(["anthropic", "openai"]).nullable().optional(),
guardrailEscalationModel: z.string().nullable().optional(),
guardrailEscalationEffort: z.enum(["low", "medium", "high", "xhigh", "max"]).nullable().optional(),
guardrailEscalationSelfConsistencyRuns: z.number().nullable().optional(),
copycatGateMode: z.enum(["off", "warn", "label", "block"]).optional(),
copycatGateMinScore: z.number().nullable().optional(),
gateDryRun: z.boolean().optional(),
Expand Down
67 changes: 66 additions & 1 deletion packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ export type FocusManifestGateConfig = {
aiReviewByok: boolean | null;
aiReviewProvider: "anthropic" | "openai" | null;
aiReviewModel: string | null;
/** `gate.aiReview.effort` (#9821): reasoning effort for the review pass — low | medium | high | xhigh | max.
* Per-repo parity with the global CLAUDE_AI_EFFORT/CODEX_AI_EFFORT env vars, which were previously the ONLY
* way to set it: provider and model were already per-repo, effort was not. Unset ⇒ the env default. */
aiReviewEffort: "low" | "medium" | "high" | "xhigh" | "max" | null;
/** `gate.aiReview.selfConsistencyRuns` (#9821): TOTAL evaluations per review, primary included. Per-repo
* parity with AI_REVIEW_SELF_CONSISTENCY_RUNS. Clamped downstream to {0,2,3} -- one run cannot measure
* agreement and the benefit saturates by three. Unset ⇒ the env default. */
aiReviewSelfConsistencyRuns: number | null;
/** `gate.guardrailEscalation` (#9808/#9821): the review settings to use INSTEAD of the repo defaults when a
* PR touches a `hardGuardrailGlobs` path. Any subset may be given; each unset field falls through to the
* repo/global value. This is what makes a guarded path buy MORE SCRUTINY rather than a manual hold --
* today a guardrail hit reviews identically to any other file and merely stops the merge. */
guardrailEscalationProvider: "anthropic" | "openai" | null;
guardrailEscalationModel: string | null;
guardrailEscalationEffort: "low" | "medium" | "high" | "xhigh" | "max" | null;
guardrailEscalationSelfConsistencyRuns: number | null;
aiReviewAllAuthors: boolean | null;
/** `gate.aiReview.closeConfidence` (#7): minimum calibrated AI-reviewer confidence (0-1) for an AI defect to BLOCK
* under `aiReview.mode: block`. null (unset) ⇒ the gate's 0.93 default. Clamped to [0,1] at parse time. */
Expand Down Expand Up @@ -631,6 +647,12 @@ export type FocusManifestSettings = Partial<
| "aiReviewByok"
| "aiReviewProvider"
| "aiReviewModel"
| "aiReviewEffort"
| "aiReviewSelfConsistencyRuns"
| "guardrailEscalationProvider"
| "guardrailEscalationModel"
| "guardrailEscalationEffort"
| "guardrailEscalationSelfConsistencyRuns"
| "aiReviewAllAuthors"
| "aiReviewConfirmedContributorsOnly"
| "closeOwnerAuthors"
Expand Down Expand Up @@ -1342,6 +1364,12 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
aiReviewByok: null,
aiReviewProvider: null,
aiReviewModel: null,
aiReviewEffort: null,
aiReviewSelfConsistencyRuns: null,
guardrailEscalationProvider: null,
guardrailEscalationModel: null,
guardrailEscalationEffort: null,
guardrailEscalationSelfConsistencyRuns: null,
aiReviewAllAuthors: null,
aiReviewCloseConfidence: null,
aiReviewSalvageabilityMinScore: null,
Expand Down Expand Up @@ -1815,6 +1843,7 @@ const GATE_TOP_LEVEL_KEYS = new Set<string>([
"expectedCiContexts",
"advisoryCheckRuns",
"ignoredCheckRuns",
"guardrailEscalation",
"aiJudgmentBlockers",
"copycat",
]);
Expand All @@ -1837,6 +1866,10 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
}
const aiReview = record.aiReview;
const aiReviewRecord = aiReview !== null && typeof aiReview === "object" && !Array.isArray(aiReview) ? (aiReview as Record<string, JsonValue>) : undefined;
// #9821: `gate.guardrailEscalation` — same "nested mapping, each field optional" shape as gate.aiReview above.
const escalation = record.guardrailEscalation;
const escalationRecord =
escalation !== null && typeof escalation === "object" && !Array.isArray(escalation) ? (escalation as Record<string, JsonValue>) : undefined;
if (aiReview !== undefined && aiReview !== null && aiReviewRecord === undefined) {
warnings.push(`Manifest gate field "gate.aiReview" must be a mapping; ignoring it.`);
}
Expand Down Expand Up @@ -1881,6 +1914,12 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
aiReviewByok: normalizeOptionalBoolean(aiReviewRecord?.byok, "gate.aiReview.byok", warnings),
aiReviewProvider: normalizeOptionalEnum(aiReviewRecord?.provider, "gate.aiReview.provider", ["anthropic", "openai"] as const, warnings),
aiReviewModel: normalizeOptionalString(aiReviewRecord?.model, "gate.aiReview.model", warnings),
aiReviewEffort: normalizeOptionalEnum(aiReviewRecord?.effort, "gate.aiReview.effort", ["low", "medium", "high", "xhigh", "max"] as const, warnings),
aiReviewSelfConsistencyRuns: normalizeOptionalNonNegativeInt(aiReviewRecord?.selfConsistencyRuns, "gate.aiReview.selfConsistencyRuns", warnings),
guardrailEscalationProvider: normalizeOptionalEnum(escalationRecord?.provider, "gate.guardrailEscalation.provider", ["anthropic", "openai"] as const, warnings),
guardrailEscalationModel: normalizeOptionalString(escalationRecord?.model, "gate.guardrailEscalation.model", warnings),
guardrailEscalationEffort: normalizeOptionalEnum(escalationRecord?.effort, "gate.guardrailEscalation.effort", ["low", "medium", "high", "xhigh", "max"] as const, warnings),
guardrailEscalationSelfConsistencyRuns: normalizeOptionalNonNegativeInt(escalationRecord?.selfConsistencyRuns, "gate.guardrailEscalation.selfConsistencyRuns", warnings),
aiReviewAllAuthors: normalizeOptionalBoolean(aiReviewRecord?.allAuthors, "gate.aiReview.allAuthors", warnings),
aiReviewCloseConfidence: normalizeOptionalConfidence(aiReviewRecord?.closeConfidence, "gate.aiReview.closeConfidence", warnings),
aiReviewSalvageabilityMinScore: normalizeOptionalScore(aiReviewRecord?.salvageabilityMinScore, "gate.aiReview.salvageabilityMinScore", warnings),
Expand Down Expand Up @@ -1947,6 +1986,8 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
gate.aiReviewByok !== null ||
gate.aiReviewProvider !== null ||
gate.aiReviewModel !== null ||
gate.aiReviewEffort !== null ||
gate.aiReviewSelfConsistencyRuns !== null ||
gate.aiReviewAllAuthors !== null ||
gate.aiReviewCloseConfidence !== null ||
gate.aiReviewSalvageabilityMinScore !== null ||
Expand All @@ -1970,6 +2011,12 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
gate.claCheckRunAppSlug !== null ||
gate.expectedCiContexts !== null ||
gate.advisoryCheckRuns !== null ||
gate.aiReviewEffort !== null ||
gate.aiReviewSelfConsistencyRuns !== null ||
gate.guardrailEscalationProvider !== null ||
gate.guardrailEscalationModel !== null ||
gate.guardrailEscalationEffort !== null ||
gate.guardrailEscalationSelfConsistencyRuns !== null ||
gate.ignoredCheckRuns !== null ||
gate.aiJudgmentBlockersMode !== null ||
gate.copycatMode !== null ||
Expand Down Expand Up @@ -2029,13 +2076,17 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
gate.aiReviewLowConfidenceDisposition !== null ||
gate.aiReviewCombine !== null ||
gate.aiReviewOnMerge !== null ||
gate.aiReviewReviewers !== null
gate.aiReviewReviewers !== null ||
gate.aiReviewEffort !== null ||
gate.aiReviewSelfConsistencyRuns !== null
) {
const aiReview: Record<string, JsonValue> = {};
if (gate.aiReviewMode !== null) aiReview.mode = gate.aiReviewMode;
if (gate.aiReviewByok !== null) aiReview.byok = gate.aiReviewByok;
if (gate.aiReviewProvider !== null) aiReview.provider = gate.aiReviewProvider;
if (gate.aiReviewModel !== null) aiReview.model = gate.aiReviewModel;
if (gate.aiReviewEffort !== null) aiReview.effort = gate.aiReviewEffort;
if (gate.aiReviewSelfConsistencyRuns !== null) aiReview.selfConsistencyRuns = gate.aiReviewSelfConsistencyRuns;
if (gate.aiReviewAllAuthors !== null) aiReview.allAuthors = gate.aiReviewAllAuthors;
if (gate.aiReviewCloseConfidence !== null) aiReview.closeConfidence = gate.aiReviewCloseConfidence;
if (gate.aiReviewSalvageabilityMinScore !== null) aiReview.salvageabilityMinScore = gate.aiReviewSalvageabilityMinScore;
Expand All @@ -2049,6 +2100,20 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue {
}
out.aiReview = aiReview;
}
// #9821: guardrail escalation round-trips as its own nested mapping, emitted only when something is set.
if (
gate.guardrailEscalationProvider !== null ||
gate.guardrailEscalationModel !== null ||
gate.guardrailEscalationEffort !== null ||
gate.guardrailEscalationSelfConsistencyRuns !== null
) {
const escalation: Record<string, JsonValue> = {};
if (gate.guardrailEscalationProvider !== null) escalation.provider = gate.guardrailEscalationProvider;
if (gate.guardrailEscalationModel !== null) escalation.model = gate.guardrailEscalationModel;
if (gate.guardrailEscalationEffort !== null) escalation.effort = gate.guardrailEscalationEffort;
if (gate.guardrailEscalationSelfConsistencyRuns !== null) escalation.selfConsistencyRuns = gate.guardrailEscalationSelfConsistencyRuns;
out.guardrailEscalation = escalation;
}
if (gate.mergeReadiness !== null) out.mergeReadiness = gate.mergeReadiness;
if (gate.manifestPolicy !== null) out.manifestPolicy = gate.manifestPolicy;
if (gate.selfAuthoredLinkedIssue !== null) out.selfAuthoredLinkedIssue = gate.selfAuthoredLinkedIssue;
Expand Down
13 changes: 13 additions & 0 deletions packages/loopover-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,19 @@ export type RepositorySettings = {
/** Config-as-code model override for the BYOK advisory write-up (e.g. "claude-3-5-sonnet-latest").
* `null` = use the key record's model, else a conservative per-provider default. */
aiReviewModel?: string | null | undefined;
/** `gate.aiReview.effort` (#9821): reasoning effort for the review pass (low | medium | high | xhigh | max).
* Per-repo parity with the global CLAUDE_AI_EFFORT / CODEX_AI_EFFORT env vars. Unset ⇒ the env default. */
aiReviewEffort?: "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
/** `gate.aiReview.selfConsistencyRuns` (#9821): TOTAL evaluations per review, primary included. Per-repo
* parity with AI_REVIEW_SELF_CONSISTENCY_RUNS; clamped downstream to {0,2,3}. Unset ⇒ the env default. */
aiReviewSelfConsistencyRuns?: number | null | undefined;
/** `gate.guardrailEscalation.*` (#9808/#9821): review settings used INSTEAD of the repo defaults when a PR
* touches a hardGuardrailGlobs path, so a guarded path buys MORE SCRUTINY rather than a manual hold. Each
* field falls through to the repo/global value when unset. */
guardrailEscalationProvider?: "anthropic" | "openai" | null | undefined;
guardrailEscalationModel?: string | null | undefined;
guardrailEscalationEffort?: "low" | "medium" | "high" | "xhigh" | "max" | null | undefined;
guardrailEscalationSelfConsistencyRuns?: number | null | undefined;
/** Review EVERY PR's author, not only confirmed Gittensor contributors. Only meaningful when
* {@link aiReviewConfirmedContributorsOnly} is also `true` (that field opts INTO confirmed-only
* scoping in the first place — see its own doc comment for the full invariant: AI review runs for
Expand Down
Loading
Loading