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
6 changes: 6 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ import { getPublicStats, isPublicStatsEnabled, resolvePublicStatsManifestOverrid
import { loadPublicAccuracyTrend } from "../services/public-accuracy-trend";
import { loadCalibrationTrend } from "../services/rule-calibration-trend";
import { isSatisfactionFloorAutotuneEnabled, loadSatisfactionFloorStatus, runSatisfactionFloorLoosening } from "../services/satisfaction-floor-loosening-run";
import { loadLiveKnobStatuses } from "../services/knob-loosening-run";
import { loadPublicReuseRateTrend } from "../services/public-reuse-rate-trend";
import { loadPublicReviewVolumeTrend } from "../services/public-review-volume-trend";
import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard";
Expand Down Expand Up @@ -4832,6 +4833,11 @@ export function createApp() {
// off. Same INTERNAL_JOB_TOKEN gate via the /v1/internal/* middleware; aggregate numbers/verdicts only.
app.get("/v1/internal/calibration/satisfaction-floor", async (c) => c.json(await loadSatisfactionFloorStatus(c.env)));

// The #8161 surface generalized across EVERY live registry knob (#8176): one endpoint, one projector,
// per-knob flag state + shipped/live/override values + applied history (both split verdicts). Same
// deliberate non-flag-gating and INTERNAL_JOB_TOKEN posture as the satisfaction-floor read above.
app.get("/v1/internal/calibration/knobs", async (c) => c.json({ knobs: await loadLiveKnobStatuses(c.env) }));

app.post("/v1/internal/jobs/refresh-registry", async (c) => {
const message: JobMessage = { type: "refresh-registry", requestedBy: "api" };
await c.env.JOBS.send(message);
Expand Down
9 changes: 7 additions & 2 deletions src/queue/gate-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ export function gateCheckPolicy(
guardrailHit: boolean;
guardrailMatches?: ReturnType<typeof guardrailPathMatches> | undefined;
},
// #8176: the backtest-gated GLOBAL default-override for the AI close-confidence floor, resolved by the
// env-bearing caller (getAiReviewCloseConfidenceOverride — flag-gated + bounds-validated). It only fills
// the DEFAULT: an explicit per-repo `gate.aiReview.closeConfidence` setting always wins below.
aiReviewCloseConfidenceOverride?: number | null,
) {
// `settings` is already the EFFECTIVE config (`.loopover.yml` > DB > defaults), resolved upstream by
// resolveRepositorySettings, so the blocker modes here reflect the repo's config file directly.
Expand All @@ -81,8 +85,9 @@ export function gateCheckPolicy(
qualityGateMinScore: settings.qualityGateMinScore ?? null,
aiReviewGateMode: settings.aiReviewMode,
// Calibrated AI close-confidence floor (#7) — config-as-code via `.loopover.yml gate.aiReview.closeConfidence`,
// resolved into settings upstream. `null`/undefined ⇒ advisory.ts applies the 0.93 default.
aiReviewCloseConfidence: settings.aiReviewCloseConfidence ?? null,
// resolved into settings upstream. When the repo has no explicit setting, the #8176 backtest-gated
// global override (if any) becomes the default; `null` ⇒ advisory.ts applies the 0.93 shipped default.
aiReviewCloseConfidence: settings.aiReviewCloseConfidence ?? aiReviewCloseConfidenceOverride ?? null,
// Sub-floor AI-judgment disposition (#4603) — DB-backed (dashboard-settable) + `.loopover.yml
// gate.aiReview.lowConfidenceDisposition` override, resolved into settings upstream. `null`/undefined ⇒
// advisory.ts applies the "hold_for_review" default.
Expand Down
6 changes: 6 additions & 0 deletions src/queue/job-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { isPrReconciliationEnabled, resolvePrReconciliationManifestOverride, run
import { isActiveReviewReconciliationEnabled, resolveActiveReviewReconciliationManifestOverride, runActiveReviewReconciliation } from "../review/active-review-reconciliation";
import { isSelfTuneEnabled, runSelfTune } from "../review/selftune-wire";
import { isSatisfactionFloorAutotuneEnabled, runScheduledSatisfactionFloorLoosening } from "../services/satisfaction-floor-loosening-run";
import { GENERIC_LIVE_KNOBS, isKnobAutotuneEnabled, runScheduledKnobLoosening } from "../services/knob-loosening-run";
import { runSelfTuneBreaker } from "../review/outcomes-wire";
import { isRagEnabled } from "../review/rag-wire";
import { processSubmitDraft } from "../services/draft";
Expand Down Expand Up @@ -348,6 +349,11 @@ export async function processJob(env: Env, message: JobMessage): Promise<void> {
// flag is ON, but a stale in-flight job landing after a flag-flip must still no-op. Never throws into
// the queue (the scheduled wrapper fails safe).
if (isSatisfactionFloorAutotuneEnabled(env)) await runScheduledSatisfactionFloorLoosening(env);
// #8176: every LATER live registry knob rides the same tick through the generic runner — each knob
// is double-gated on its OWN wrangler var, so an un-flagged knob does zero work here.
for (const knob of GENERIC_LIVE_KNOBS) {
if (isKnobAutotuneEnabled(env, knob)) await runScheduledKnobLoosening(env, knob);
}
return;
case "selftune":
// Convergence (self-improve / auto-tune, flag LOOPOVER_REVIEW_SELFTUNE). Defense-in-depth: the cron only
Expand Down
12 changes: 9 additions & 3 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export { runRetentionPrune } from "./retention";
// test/unit/gate-check-policy.test.ts and test/unit/repository-settings-enforcement.test.ts's existing
// `import { gateCheckPolicy } from "../../src/queue/processors"` keeps working unchanged.
import { auditGateCheckPermissionMissing, gateCheckPolicy, recordPublishedGateCheckSummary } from "./gate-checks";
import { getAiReviewCloseConfidenceOverride } from "../services/knob-loosening-run";
export { gateCheckPolicy } from "./gate-checks";
// #4013 step 9: same shim shape for the AI-review-orchestration functions -- imported here for this file's
// own remaining internal callers, and re-exported so the many existing tests importing claimAiReviewLock,
Expand Down Expand Up @@ -1538,6 +1539,9 @@ export async function sweepRepoRegate(
// isScheduledRegateSweepJob (queue-common.ts) misclassifies it as background maintenance and it inherits the
// exact starvation this priority mechanism exists to avoid. Ordinary stale candidates keep the sweep prefix
// unchanged.
// #8176: the global close-confidence default-override, resolved once for the sweep (same value the main
// webhook path threads; an explicit per-repo setting still wins inside gateCheckPolicy).
const sweepCloseConfidenceOverride = await getAiReviewCloseConfidenceOverride(env);
for (const [index, pr] of candidates.entries()) {
const others = openPullRequests.filter(
(other) => other.number !== pr.number,
Expand All @@ -1561,7 +1565,7 @@ export async function sweepRepoRegate(
});
const gate = evaluateGateCheck(
advisory,
gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null),
gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null, undefined, undefined, sweepCloseConfidenceOverride),
);
verdicts[String(pr.number)] = gate.conclusion;
if (gate.conclusion === "failure" || gate.conclusion === "action_required")
Expand Down Expand Up @@ -10381,6 +10385,8 @@ async function maybePublishPrPublicSurface(
slopRisk,
authorHistory,
gateSizeContext,
// #8176: backtest-gated global default for the close-confidence floor (explicit per-repo wins inside).
await getAiReviewCloseConfidenceOverride(env),
);
gateEvaluation = await withReviewPipelineSpan(
"selfhost.review.gate",
Expand Down Expand Up @@ -11871,7 +11877,7 @@ async function maybeProcessResolveCommand(env: Env, deliveryId: string, payload:
if (!findingRef.ok) { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: findingRef.reason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: findingRef.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: findingRef.reason } }); return true; }
const { advisory } = await buildAuthorizedPrActionAdvisory(env, req.repoFullName, pr, settings);
await appendPublishedAiReviewFindingsForResolve(env, req.repoFullName, pr, settings.aiReviewMode, advisory);
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null));
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null, undefined, undefined, await getAiReviewCloseConfidenceOverride(env)));
const selection = selectWarningsForResolve(gate.warnings, findingRef);
if (selection.reason === "finding_not_found") { await recordAuditEvent(env, { eventType: "github_app.finding_resolved_skipped", actor: req.actor, targetKey, outcome: "completed", detail: selection.reason, metadata: { deliveryId, repoFullName: req.repoFullName, reason: selection.reason } }); await recordGithubProductUsage(env, "finding_resolved_skipped", { actor: req.actor, repoFullName: req.repoFullName, targetKey, outcome: "skipped", metadata: { reason: selection.reason } }); return true; }
const mode = resolveAgentActionMode({ globalPaused: isGlobalAgentPause(env) || (await isGlobalAgentFrozen(env)), agentPaused: settings.agentPaused, agentDryRun: settings.agentDryRun });
Expand Down Expand Up @@ -12102,7 +12108,7 @@ async function maybeProcessExplainCommand(env: Env, deliveryId: string, payload:
}
const { advisory } = await buildAuthorizedPrActionAdvisory(env, req.repoFullName, pr, settings);
await appendPublishedAiReviewFindingsForResolve(env, req.repoFullName, pr, settings.aiReviewMode, advisory);
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null));
const gate = evaluateGateCheck(advisory, gateCheckPolicy(settings, null, undefined, pr.slopRisk ?? null, undefined, undefined, await getAiReviewCloseConfidenceOverride(env)));
const selection = selectWarningsForResolve(gate.warnings, findingRef);
if (selection.reason === "finding_not_found") {
const notFound = sanitizePublicComment([AGENT_COMMAND_COMMENT_MARKER, "", "> [!NOTE]", `> **No review finding \`${findingRef.findingCode}\` on this PR**`, "> That id is not among this PR's current review findings — re-run `@loopover explain <finding-id>` with an id from the review summary.", "", "---", loopoverFooter(env)].join("\n"));
Expand Down
Loading