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
10 changes: 10 additions & 0 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,16 @@ gate:
# value.
contentLaneDeliverable: off

# Backtest-regression gate (#8105). Governs what a REGRESSED verdict from the
# pre-merge backtest (threshold #8138 / logic #8139) does: advisory (default)
# renders the comparison in the review comment but never blocks; block
# escalates it into a backtest_regression hard blocker; off silences the
# backtest advisory entirely. Flip to block only once the persisted track
# record (scripts/backtest-track-record.ts, #8140) supports it.
# off | advisory | block. Default: advisory. DB-backed (dashboard-settable
# too); this overrides the stored value.
backtestRegression: advisory

# Gate-check dry-run. When true, the posted check conclusion remains the real
# non-enforcing verdict while comments/check text may also show the would-be
# stricter verdict for AI-review blocker mode. It does not disable downstream
Expand Down
7 changes: 4 additions & 3 deletions apps/loopover-ui/content/docs/backtest-calibration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ threshold or rewrites detection logic gets scored against the real recorded hist
targets, the same raw inputs — instead of being eyeballed.

<Callout variant="note">
Everything on this page is advisory-only today. A backtest verdict — even a regression — never
blocks a merge. Whether a REGRESSED verdict should ever gate merges is a tracked, deliberately
separate decision that waits for real production track-record data.
Everything on this page is advisory-only by default. A REGRESSED verdict blocks a merge only if a
repo explicitly opts in via `backtestRegressionGateMode: block` (`.loopover.yml
gate.backtestRegression`) — the shipped default is `advisory`, and the flip is meant to wait for
real production track-record data.
</Callout>

## The corpus
Expand Down
62 changes: 37 additions & 25 deletions apps/loopover-ui/content/docs/tuning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ forward and wins whenever both are set for the same field.
`advisory` renders the assessment in the review comment without blocking;
`block` additionally lets a confidence-floor-passing "unaddressed" verdict
become a blocker.
- `gate.backtestRegression` — governs what a REGRESSED verdict from the
[pre-merge backtest](/docs/backtest-calibration) does. Default `advisory`:
the comparison renders in the review comment but never blocks. `block`
escalates a REGRESSED verdict into a hard blocker — flip it only once the
persisted track record supports gating. `off` silences the backtest advisory
entirely.
- `gate.contentLaneDeliverable` — only meaningful for a repo with a registry
content-lane spec configured (`contentLane`); a no-op otherwise. Fully
deterministic (a text/path match, no AI call): when the PR's primary linked
Expand Down Expand Up @@ -381,36 +387,42 @@ testExpectations:
linkedIssuePolicy: preferred

# Gate policy — checkMode is set explicitly (not the legacy, ambiguous "enabled" alias)

gate:
checkMode: visible
pack: gittensor
duplicates: block
linkedIssue: advisory
readiness:
mode: advisory
minScore: 70
slop:
mode: block
minScore: 60
aiAdvisory: true
mergeReadiness: advisory
manifestPolicy: block
aiReview:
mode: advisory
byok: true
provider: anthropic
model: claude-3-5-sonnet-latest
checkMode: visible
pack: gittensor
duplicates: block
linkedIssue: advisory
readiness:
mode: advisory
minScore: 70
slop:
mode: block
minScore: 60
aiAdvisory: true
mergeReadiness: advisory
manifestPolicy: block
aiReview:
mode: advisory
byok: true
provider: anthropic
model: claude-3-5-sonnet-latest

# Generic settings overrides -- commentMode/checkRunMode/checkRunDetailLevel/badgeEnabled are

# config-as-code only (no DB column or dashboard toggle); this file is their sole source.

settings:
commentMode: detected_contributors_only
checkRunMode: enabled
checkRunDetailLevel: standard
badgeEnabled: true
# Optional path holds. Omitted or [] means no path guardrails.
# hardGuardrailGlobs:
# - "src/selfhost/**"`}
commentMode: detected_contributors_only
checkRunMode: enabled
checkRunDetailLevel: standard
badgeEnabled: true

# Optional path holds. Omitted or [] means no path guardrails.

# hardGuardrailGlobs:

# - "src/selfhost/\*\*"`}

/>

Expand Down
18 changes: 18 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9761,6 +9761,14 @@
"advisory",
"block"
]
},
"backtestRegressionGateMode": {
"type": "string",
"enum": [
"off",
"advisory",
"block"
]
}
},
"required": [
Expand All @@ -9782,6 +9790,7 @@
"selfAuthoredLinkedIssueGateMode",
"linkedIssueSatisfactionGateMode",
"contentLaneDeliverableGateMode",
"backtestRegressionGateMode",
"slopAiAdvisory",
"aiReviewMode",
"aiReviewByok",
Expand Down Expand Up @@ -10492,6 +10501,14 @@
"advisory",
"block"
]
},
"backtestRegressionGateMode": {
"type": "string",
"enum": [
"off",
"advisory",
"block"
]
}
},
"required": [
Expand All @@ -10513,6 +10530,7 @@
"selfAuthoredLinkedIssueGateMode",
"linkedIssueSatisfactionGateMode",
"contentLaneDeliverableGateMode",
"backtestRegressionGateMode",
"autoLabelEnabled",
"typeLabelsEnabled",
"gittensorLabel",
Expand Down
10 changes: 10 additions & 0 deletions config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@ gate:
# value.
contentLaneDeliverable: off

# Backtest-regression gate (#8105). Governs what a REGRESSED verdict from the
# pre-merge backtest (threshold #8138 / logic #8139) does: advisory (default)
# renders the comparison in the review comment but never blocks; block
# escalates it into a backtest_regression hard blocker; off silences the
# backtest advisory entirely. Flip to block only once the persisted track
# record (scripts/backtest-track-record.ts, #8140) supports it.
# off | advisory | block. Default: advisory. DB-backed (dashboard-settable
# too); this overrides the stored value.
backtestRegression: advisory

# Gate-check dry-run. When true, the posted check conclusion remains the real
# non-enforcing verdict while comments/check text may also show the would-be
# stricter verdict for AI-review blocker mode. It does not disable downstream
Expand Down
6 changes: 6 additions & 0 deletions migrations/0174_gate_backtest_regression.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- Backtest-regression gate (#8105, epic #8082): advisory by default -- byte-identical behavior for every
-- existing row (the shipped #8138/#8142 comment-only advisory). block escalates a REGRESSED pre-merge
-- backtest verdict into a backtest_regression hard blocker; off silences the backtest advisory entirely.
-- The flip to block is deliberately a config change made only once #8140's persisted track record supports
-- it -- see #8105's own do-not-gate-before-data boundary.
ALTER TABLE repository_settings ADD COLUMN backtest_regression_gate_mode TEXT NOT NULL DEFAULT 'advisory';
7 changes: 7 additions & 0 deletions packages/loopover-engine/src/advisory/gate-advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ export type GateCheckPolicy = {
* on this mode; see runContentLaneDeliverableCheckForAdvisory, src/queue/processors.ts on the host side),
* so this branch only matters once a repo has explicitly opted into `advisory`/`block`. */
contentLaneDeliverableGateMode?: GateRuleMode | undefined;
/** Backtest-regression gate (#8105): `block` turns a `backtest_regression` finding into a hard blocker.
* Default `advisory` — the finding only exists in block mode (the host resolver pushes it there), so
* this branch is defense-in-depth, mirroring content_lane_deliverable_missing's above. */
backtestRegressionGateMode?: GateRuleMode | undefined;
/** CLA / license-compatibility gate (#2564). When `block`, a `cla_consent_missing` finding — raised when
* neither configured detection method (a consent phrase in the PR body, or a named CLA-bot check-run
* conclusion) confirms consent — becomes a hard blocker. `off` (default) = no finding at all; `advisory` =
Expand Down Expand Up @@ -636,6 +640,9 @@ function isConfiguredGateBlocker(finding: AdvisoryFinding, policy: GateCheckPoli
// opts in with `block`. Fully deterministic (no AI judgment involved), so it is exempt from the
// close-precision circuit breaker on the host side.
if (code === "content_lane_deliverable_missing") return gatePolicyBlocks(policy.contentLaneDeliverableGateMode, "off");
// Backtest-regression gate (#8105): blocks only under an explicit opt-in; default advisory (the shipped
// pre-#8105 behavior). The finding itself only exists in block mode -- see the host-side resolver.
if (code === "backtest_regression") return gatePolicyBlocks(policy.backtestRegressionGateMode, "advisory");
// Lockfile-tamper-risk gate (#2563): blocks only when the maintainer opts in with `block`. Defaults to `off`
// (the finding is never even produced — see maybeAddLockfileTamperFinding's mode gate in queue/processors.ts),
// so this branch only matters once a repo has explicitly turned the scan on.
Expand Down
9 changes: 9 additions & 0 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export type FocusManifestGateConfig = {
* `linkedIssueSatisfaction` immediately above in shape, but is a purely structural check, not an AI
* opinion, so it carries none of that feature's AI-budget/confidence-floor machinery. */
contentLaneDeliverable: GateRuleMode | null;
/** `gate.backtestRegression` (#8105, epic #8082): off|advisory|block, advisory by default — governs what a
* REGRESSED pre-merge backtest verdict (#8138 threshold / #8139 logic) does. `advisory` renders the
* comparison but never blocks (the shipped pre-#8105 behavior); `block` escalates it into a
* `backtest_regression` hard blocker; `off` silences the backtest advisory entirely. DB-backed
* (dashboard-settable too); this overrides the stored value like every other `gate:` field. */
backtestRegression: GateRuleMode | null;
dryRun: boolean | null;
/** `gate.premergeContentRecheck` (#2550): for a PR touching `migrations/**`, re-verify against a live,
* freshly-fetched tip of the base branch — unioned with this PR's own new migration filenames — for a
Expand Down Expand Up @@ -1303,6 +1309,7 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = {
selfAuthoredLinkedIssue: null,
linkedIssueSatisfaction: null,
contentLaneDeliverable: null,
backtestRegression: null,
dryRun: null,
premergeContentRecheck: null,
requireFreshRebaseWindowMinutes: null,
Expand Down Expand Up @@ -1788,6 +1795,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
selfAuthoredLinkedIssue: normalizeOptionalGateMode(record.selfAuthoredLinkedIssue, "gate.selfAuthoredLinkedIssue", warnings),
linkedIssueSatisfaction: normalizeOptionalGateMode(record.linkedIssueSatisfaction, "gate.linkedIssueSatisfaction", warnings),
contentLaneDeliverable: normalizeOptionalGateMode(record.contentLaneDeliverable, "gate.contentLaneDeliverable", warnings),
backtestRegression: normalizeOptionalGateMode(record.backtestRegression, "gate.backtestRegression", warnings),
dryRun: normalizeOptionalBoolean(record.dryRun, "gate.dryRun", warnings),
premergeContentRecheck: normalizeOptionalBoolean(record.premergeContentRecheck, "gate.premergeContentRecheck", warnings),
requireFreshRebaseWindowMinutes: normalizeOptionalPositiveInteger(record.requireFreshRebaseWindow, "gate.requireFreshRebaseWindow", warnings),
Expand Down Expand Up @@ -1845,6 +1853,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu
gate.selfAuthoredLinkedIssue !== null ||
gate.linkedIssueSatisfaction !== null ||
gate.contentLaneDeliverable !== null ||
gate.backtestRegression !== null ||
gate.dryRun !== null ||
gate.premergeContentRecheck !== null ||
gate.requireFreshRebaseWindowMinutes !== null ||
Expand Down
1 change: 1 addition & 0 deletions scripts/check-docs-drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export const GATE_MODE_MANIFEST: GateModeManifestRow[] = [
{ field: "selfAuthoredLinkedIssueGateMode", aliases: ["selfAuthoredLinkedIssueGateMode", "gate.selfAuthoredLinkedIssue"], pages: ["how-reviews-work.mdx", "tuning.mdx", "github-app.mdx"] },
{ field: "linkedIssueSatisfactionGateMode", aliases: ["linkedIssueSatisfactionGateMode", "gate.linkedIssueSatisfaction"], pages: ["how-reviews-work.mdx", "tuning.mdx", "github-app.mdx"] },
{ field: "contentLaneDeliverableGateMode", aliases: ["contentLaneDeliverableGateMode", "gate.contentLaneDeliverable"], pages: ["how-reviews-work.mdx", "tuning.mdx", "github-app.mdx"] },
{ field: "backtestRegressionGateMode", aliases: ["backtestRegressionGateMode", "gate.backtestRegression"], pages: ["backtest-calibration.mdx", "tuning.mdx"] },
{ field: "moderationGateMode", aliases: ["moderationGateMode", "settings.moderationGateMode"], pages: ["how-reviews-work.mdx", "tuning.mdx", "github-app.mdx"] },
];

Expand Down
1 change: 1 addition & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,7 @@ const maintainerSettingsSchema = z
manifestPolicyGateMode: z.enum(["off", "advisory", "block"]),
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
contentLaneDeliverableGateMode: z.enum(["off", "advisory", "block"]),
backtestRegressionGateMode: z.enum(["off", "advisory", "block"]),
// #6443: mergeTrainMode/gittensorLabel/blacklistLabel/createMissingLabel removed -- no longer DB-backed,
// config-as-code only via .loopover.yml's settings: block now.
// #6446: firstTimeContributorGrace removed -- a dead, never-wired RESERVED/INERT field (#2266); deleted
Expand Down
7 changes: 7 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
selfAuthoredLinkedIssueGateMode: "advisory",
linkedIssueSatisfactionGateMode: "off",
contentLaneDeliverableGateMode: "off",
backtestRegressionGateMode: "advisory",
slopGateMinScore: null,
slopAiAdvisory: false,
aiReviewMode: "off",
Expand Down Expand Up @@ -720,6 +721,9 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
selfAuthoredLinkedIssueGateMode: "advisory",
linkedIssueSatisfactionGateMode: parseGateRuleMode(row.linkedIssueSatisfactionGateMode),
contentLaneDeliverableGateMode: parseGateRuleMode(row.contentLaneDeliverableGateMode),
// parseGateRuleMode's fallback IS this field's default ("advisory"), so an unrecognized stored value
// degrades to the safe shipped behavior rather than silently blocking (#8105).
backtestRegressionGateMode: parseGateRuleMode(row.backtestRegressionGateMode),
slopGateMinScore: normalizeQualityGateMinScore(row.slopGateMinScore),
slopAiAdvisory: row.slopAiAdvisory,
aiReviewMode: "off",
Expand Down Expand Up @@ -865,6 +869,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
selfAuthoredLinkedIssueGateMode: "advisory",
linkedIssueSatisfactionGateMode: settings.linkedIssueSatisfactionGateMode ?? "off",
contentLaneDeliverableGateMode: settings.contentLaneDeliverableGateMode ?? "off",
backtestRegressionGateMode: settings.backtestRegressionGateMode ?? "advisory",
slopGateMinScore: normalizeQualityGateMinScore(settings.slopGateMinScore),
slopAiAdvisory: settings.slopAiAdvisory ?? false,
aiReviewMode: "off",
Expand Down Expand Up @@ -948,6 +953,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
manifestPolicyGateMode: resolved.manifestPolicyGateMode,
linkedIssueSatisfactionGateMode: resolved.linkedIssueSatisfactionGateMode,
contentLaneDeliverableGateMode: resolved.contentLaneDeliverableGateMode,
backtestRegressionGateMode: resolved.backtestRegressionGateMode,
slopGateMinScore: resolved.slopGateMinScore,
slopAiAdvisory: resolved.slopAiAdvisory,
aiReviewLowConfidenceDisposition: resolved.aiReviewLowConfidenceDisposition,
Expand Down Expand Up @@ -982,6 +988,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
manifestPolicyGateMode: resolved.manifestPolicyGateMode,
linkedIssueSatisfactionGateMode: resolved.linkedIssueSatisfactionGateMode,
contentLaneDeliverableGateMode: resolved.contentLaneDeliverableGateMode,
backtestRegressionGateMode: resolved.backtestRegressionGateMode,
slopGateMinScore: resolved.slopGateMinScore,
slopAiAdvisory: resolved.slopAiAdvisory,
aiReviewLowConfidenceDisposition: resolved.aiReviewLowConfidenceDisposition,
Expand Down
2 changes: 2 additions & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const repositorySettings = sqliteTable("repository_settings", {
// Content-lane linked-issue deliverable gate (#content-lane-deliverable): off by default. Only meaningful
// for a repo with a registry content-lane spec resolved. Enforcement lands in the content-lane wire, not here.
contentLaneDeliverableGateMode: text("content_lane_deliverable_gate_mode").notNull().default("off"),
// Backtest-regression gate (#8105): advisory by default -- the shipped pre-#8105 behavior. See types.ts.
backtestRegressionGateMode: text("backtest_regression_gate_mode").notNull().default("advisory"),
slopGateMinScore: integer("slop_gate_min_score"),
slopAiAdvisory: integer("slop_ai_advisory", { mode: "boolean" }).notNull().default(false),
// AI-review low-confidence disposition (#4603): one_shot | hold_for_review | advisory_only. Governs a
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@ export const RepositorySettingsSchema = z
selfAuthoredLinkedIssueGateMode: z.enum(["off", "advisory", "block"]),
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
contentLaneDeliverableGateMode: z.enum(["off", "advisory", "block"]),
backtestRegressionGateMode: z.enum(["off", "advisory", "block"]),
slopGateMinScore: z.number().nullable().optional(),
slopAiAdvisory: z.boolean(),
aiReviewMode: z.enum(["off", "advisory", "block"]),
Expand Down Expand Up @@ -1026,6 +1027,7 @@ export const RepoSettingsPreviewSchema = z
selfAuthoredLinkedIssueGateMode: z.enum(["off", "advisory", "block"]),
linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]),
contentLaneDeliverableGateMode: z.enum(["off", "advisory", "block"]),
backtestRegressionGateMode: z.enum(["off", "advisory", "block"]),
slopGateMinScore: z.number().nullable().optional(),
autoLabelEnabled: z.boolean(),
typeLabelsEnabled: z.boolean(),
Expand Down
1 change: 1 addition & 0 deletions src/queue/gate-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function gateCheckPolicy(
selfAuthoredLinkedIssueGateMode: settings.selfAuthoredLinkedIssueGateMode,
linkedIssueSatisfactionGateMode: settings.linkedIssueSatisfactionGateMode,
contentLaneDeliverableGateMode: settings.contentLaneDeliverableGateMode,
backtestRegressionGateMode: settings.backtestRegressionGateMode,
authorMergedPrCount: authorHistory?.mergedPrCount,
authorClosedUnmergedPrCount: authorHistory?.closedUnmergedPrCount,
slopGateMinScore: settings.slopGateMinScore ?? null,
Expand Down
Loading
Loading