Skip to content
Closed
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
75 changes: 75 additions & 0 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -13945,6 +13945,32 @@
"maintainerNextSteps",
"privateSummary"
]
},
"LiveGateThresholdsResponse": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"confidence_floor": {
"type": "number",
"nullable": true
},
"scope_cap_files": {
"type": "integer",
"nullable": true
},
"scope_cap_lines": {
"type": "integer",
"nullable": true
}
},
"required": [
"repoFullName",
"confidence_floor",
"scope_cap_files",
"scope_cap_lines"
]
}
},
"parameters": {},
Expand Down Expand Up @@ -17954,6 +17980,55 @@
}
]
}
},
"/v1/repos/{owner}/{repo}/live-gate-thresholds": {
"get": {
"summary": "Live self-tuned gate thresholds for AMS probe (#6486)",
"parameters": [
{
"schema": {
"type": "string"
},
"required": true,
"name": "owner",
"in": "path"
},
{
"schema": {
"type": "string"
},
"required": true,
"name": "repo",
"in": "path"
}
],
"responses": {
"200": {
"description": "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LiveGateThresholdsResponse"
}
}
}
},
"403": {
"description": "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo"
},
"404": {
"description": "No live or shadow gate override is active for this repo"
}
},
"security": [
{
"LoopOverBearer": []
},
{
"LoopOverSessionCookie": []
}
]
}
}
},
"servers": [
Expand Down
21 changes: 20 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ import { buildMaintainerActivationPreview, recommendedAdvisoryActivationSettings
import { buildRepoOutcomeCalibration } from "../services/outcome-calibration";
import { loadGatePrecisionReport } from "../services/gate-precision";
import { computeOpsStats, isOpsEnabled, resolveOpsManifestOverride } from "../review/ops-wire";
import { deleteLiveOverride, listOverrideAudit, loadOverride, loadShadowOverride, sanitizeOverridePayload, type StorageEnv } from "../review/auto-apply";
import { authoritativeGateOverride, deleteLiveOverride, listOverrideAudit, loadOverride, loadShadowOverride, sanitizeOverridePayload, toLiveGateThresholdFields, type StorageEnv } from "../review/auto-apply";
import { handleInternalCalibration, handleInternalDecision, type OpsAgentConfig } from "../review/ops";
import { computeParityReadiness, isParityAuditEnabled } from "../review/parity-wire";
import { computePredictedGateAgreement } from "../review/predicted-gate-agreement";
Expand Down Expand Up @@ -3018,6 +3018,25 @@ export function createApp() {
});
});

// AMS probe surface for a repo's live self-tuned gate thresholds (#6486, implementing #6209's decision).
// Distinct from gate-config/effective above: a field-limited, snake_case-only payload (no shadowPending, no
// nested effective object) matching tunables_overrides' own column names 1:1, and the live row wins but a
// soaking shadow's queued value fills in when live is absent -- #6209 decided AMS should see a pending
// tightening too, not just a promoted one. Never includes applied_at/clear_at or override_audit history.
app.get("/v1/repos/:owner/:repo/live-gate-thresholds", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
const identity = await authenticateRequestIdentity(c);
// Only the shared, end-user-obtainable static `mcp` token is allowlist-scoped; every other identity
// (an authenticated session, or an operator-only api/internal static token) stays trusted, matching the
// intelligence/issue-quality/reviewability precedent routes above.
if (identity?.kind === "static" && identity.actor === "mcp" && !(await import("../auth/security")).isMcpReadRepoAllowed(c.env.MCP_READ_REPO_ALLOWLIST, fullName)) return c.json({ error: "forbidden_repo" }, 403);
const storageEnv = c.env as unknown as StorageEnv;
const [live, shadow] = await Promise.all([loadOverride(storageEnv, fullName), loadShadowOverride(storageEnv, fullName)]);
const fields = toLiveGateThresholdFields(authoritativeGateOverride(live, shadow));
if (!fields) return c.json({ error: "live_gate_thresholds_not_found", repoFullName: fullName }, 404);
return c.json({ repoFullName: fullName, ...fields });
});

app.get("/v1/repos/:owner/:repo/outcome-patterns", async (c) => {
const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`;
const response = await buildRepoOutcomePatternsResponse(c.env, fullName);
Expand Down
11 changes: 11 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,17 @@ export const IssueQualityResponseSchema = z
})
.openapi("IssueQualityResponse");

/** Field-limited AMS/MCP probe payload for a repo's live gate thresholds (#6486) — snake_case column names
* matching `tunables_overrides` 1:1; deliberately excludes applied_at/clear_at and override_audit history. */
export const LiveGateThresholdsResponseSchema = z
.object({
repoFullName: z.string(),
confidence_floor: z.number().nullable(),
scope_cap_files: z.number().int().nullable(),
scope_cap_lines: z.number().int().nullable(),
})
.openapi("LiveGateThresholdsResponse");

export const BurdenForecastSchema = z
.object({
repoFullName: z.string(),
Expand Down
13 changes: 13 additions & 0 deletions src/openapi/spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
IssueQualityResponseSchema,
LabelAuditSchema,
LaneAdviceSchema,
LiveGateThresholdsResponseSchema,
LocalBranchAnalysisSchema,
LocalDiffPreflightResultSchema,
MaintainerPacketSchema,
Expand Down Expand Up @@ -148,6 +149,7 @@ export function buildOpenApiSpec() {
registry.register("ScorePreview", ScorePreviewSchema);
registry.register("IssueQualityReport", IssueQualityReportSchema);
registry.register("IssueQualityResponse", IssueQualityResponseSchema);
registry.register("LiveGateThresholdsResponse", LiveGateThresholdsResponseSchema);
registry.register("BurdenForecast", BurdenForecastSchema);
registry.register("ContributorScoringProfile", ContributorScoringProfileSchema);
registry.register("ContributorStrategy", ContributorStrategySchema);
Expand Down Expand Up @@ -419,6 +421,17 @@ export function buildOpenApiSpec() {
404: { description: "Repo is unknown or has no issue-quality coverage yet" },
},
});
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/live-gate-thresholds",
summary: "Live self-tuned gate thresholds for AMS probe (#6486)",
request: { params: z.object({ owner: z.string(), repo: z.string() }) },
responses: {
200: { description: "Field-limited live (or soaking-shadow) TunableOverride values — confidence_floor / scope_cap_files / scope_cap_lines only", content: { "application/json": { schema: LiveGateThresholdsResponseSchema } } },
403: { description: "Static mcp credential is outside MCP_READ_REPO_ALLOWLIST for this repo" },
404: { description: "No live or shadow gate override is active for this repo" },
},
});
registry.registerPath({
method: "get",
path: "/v1/repos/{owner}/{repo}/outcome-patterns",
Expand Down
27 changes: 27 additions & 0 deletions src/review/auto-apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,33 @@ export async function deleteShadowOverride(env: StorageEnv, project: string): Pr
await storage(env).prepare("DELETE FROM tunables_overrides_shadow WHERE project = ?").bind(project).run();
}

/** PURE: prefer a project's LIVE override; when absent, fall through to a soaking SHADOW override's queued
* value (#6486/#6209 — AMS probes this so a repo mid-soak still reports its pending threshold, not "none"). */
export function authoritativeGateOverride(live: TunableOverride | null, shadow: ShadowOverride | null): TunableOverride | null {
return live ?? shadow?.override ?? null;
}

/** Field-limited AMS/MCP payload for a repo's live gate thresholds (#6486). Snake_case names match the
* tunables_overrides column names AMS probes for; deliberately excludes applied_at/clear_at and the
* override_audit history — only the resolved effective values ever leave this projection. */
export type LiveGateThresholdFields = {
confidence_floor: number | null;
scope_cap_files: number | null;
scope_cap_lines: number | null;
};

/** PURE: project an authoritative TunableOverride into the exact snake_case allowlist, or null when the
* override carries neither a confidenceFloor nor a scopeCap (nothing to report). */
export function toLiveGateThresholdFields(override: TunableOverride | null): LiveGateThresholdFields | null {
if (!override) return null;
if (override.confidenceFloor === undefined && override.scopeCap === undefined) return null;
return {
confidence_floor: override.confidenceFloor ?? null,
scope_cap_files: override.scopeCap?.files ?? null,
scope_cap_lines: override.scopeCap?.lines ?? null,
};
}

/** Record one override-lifecycle event to the dedicated (target-free) audit table. Fail-safe: a write error
* never breaks the apply path, but it IS surfaced at error level (this is the operator's ONLY visibility
* into an autonomous config change — a silently-dropped log line here defeats that entirely). */
Expand Down
66 changes: 66 additions & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6787,6 +6787,72 @@ describe("api routes", () => {
const unauth = await app.request("/v1/repos/entrius/allways-ui/gate-config/effective", {}, env);
expect(unauth.status).toBe(401);
});

it("exposes field-limited snake_case live gate thresholds for the AMS probe (#6486)", async () => {
const app = createApp();
const env = createTestEnv();
// No live or shadow override yet → not-found, matching the issue-quality-route not-found convention.
const missing = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(env) }, env);
expect(missing.status).toBe(404);
await expect(missing.json()).resolves.toEqual({ error: "live_gate_thresholds_not_found", repoFullName: "entrius/allways-ui" });

// A live override surfaces as flat snake_case fields; audit rows and shadow queue detail must never leak.
const storageEnv = env as unknown as StorageEnv;
await writeLiveOverride(storageEnv, "entrius/allways-ui", { confidenceFloor: 0.91, scopeCap: { files: 8, lines: 250 } });
await writeShadowOverride(storageEnv, "entrius/allways-ui", { confidenceFloor: 0.4 }, "2099-01-01T00:00:00.000Z");
await recordOverrideAudit(storageEnv, "entrius/allways-ui", "apply", { note: "must-never-surface-on-6486" });
const live = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(env) }, env);
expect(live.status).toBe(200);
const liveBody = (await live.json()) as unknown;
expect(liveBody).toEqual({
repoFullName: "entrius/allways-ui",
confidence_floor: 0.91,
scope_cap_files: 8,
scope_cap_lines: 250,
});
expect(JSON.stringify(liveBody)).not.toMatch(/override_audit|applied_at|clear_at|must-never-surface|shadowPending|effective/i);

// A soaking shadow's queued override fills in when live is absent (#6209's decision: AMS should see a
// pending tightening too, not just a promoted one) — and a floor-only override nulls both scope_cap fields.
const shadowOnlyEnv = createTestEnv();
await writeShadowOverride(shadowOnlyEnv as unknown as StorageEnv, "entrius/allways-ui", { confidenceFloor: 0.66 }, "2099-01-01T00:00:00.000Z");
const shadowed = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(shadowOnlyEnv) }, shadowOnlyEnv);
expect(shadowed.status).toBe(200);
await expect(shadowed.json()).resolves.toEqual({
repoFullName: "entrius/allways-ui",
confidence_floor: 0.66,
scope_cap_files: null,
scope_cap_lines: null,
});

// A live override carrying only a scope cap (no confidence floor) still resolves confidence_floor to null.
const scopeCapOnlyEnv = createTestEnv();
await writeLiveOverride(scopeCapOnlyEnv as unknown as StorageEnv, "entrius/allways-ui", { scopeCap: { files: 4, lines: 120 } });
const scopeCapOnly = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: apiHeaders(scopeCapOnlyEnv) }, scopeCapOnlyEnv);
expect(scopeCapOnly.status).toBe(200);
await expect(scopeCapOnly.json()).resolves.toEqual({
repoFullName: "entrius/allways-ui",
confidence_floor: null,
scope_cap_files: 4,
scope_cap_lines: 120,
});

// A static mcp credential within the allowlist reads successfully (default MCP_READ_REPO_ALLOWLIST is "*").
const mcpAllowed = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: mcpHeaders(env) }, env);
expect(mcpAllowed.status).toBe(200);
await expect(mcpAllowed.json()).resolves.toMatchObject({ confidence_floor: 0.91 });

// The shared static mcp credential is scoped to MCP_READ_REPO_ALLOWLIST, same precedent as gate-config/effective.
const forbiddenEnv = createTestEnv({ MCP_READ_REPO_ALLOWLIST: "" });
await writeLiveOverride(forbiddenEnv as unknown as StorageEnv, "entrius/allways-ui", { confidenceFloor: 0.9 });
const forbidden = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", { headers: mcpHeaders(forbiddenEnv) }, forbiddenEnv);
expect(forbidden.status).toBe(403);
await expect(forbidden.json()).resolves.toMatchObject({ error: "forbidden_repo" });

// Unauthenticated calls are rejected before any repo lookup.
const unauth = await app.request("/v1/repos/entrius/allways-ui/live-gate-thresholds", {}, env);
expect(unauth.status).toBe(401);
});
});

async function signWebhook(body: string, secret: string | undefined): Promise<string> {
Expand Down
54 changes: 54 additions & 0 deletions test/unit/auto-apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import {
type AutoApplyContext,
applyOverrideRecommendation,
authoritativeGateOverride,
deleteLiveOverride,
deleteShadowOverride,
describeOverride,
Expand All @@ -16,14 +17,67 @@ import {
runAutoApplyRecommendations,
sanitizeOverridePayload,
SHADOW_PROMOTION_MIN_DECIDED,
type ShadowOverride,
type StorageEnv,
type StorageLike,
toLiveGateThresholdFields,
type TunableOverride,
writeLiveOverride,
writeShadowOverride,
} from "../../src/review/auto-apply";
import type { TuningRec } from "../../src/review/auto-tune";

describe("authoritativeGateOverride (#6486)", () => {
const live: TunableOverride = { confidenceFloor: 0.9 };
const shadow: ShadowOverride = { override: { confidenceFloor: 0.5 }, validatedUntil: null };

it("prefers a present live override over a present shadow override", () => {
expect(authoritativeGateOverride(live, shadow)).toBe(live);
});

it("falls through to the shadow's queued override when live is absent", () => {
expect(authoritativeGateOverride(null, shadow)).toBe(shadow.override);
});

it("returns null when neither a live nor a shadow override is active", () => {
expect(authoritativeGateOverride(null, null)).toBeNull();
});
});

describe("toLiveGateThresholdFields (#6486)", () => {
it("returns null for a null override", () => {
expect(toLiveGateThresholdFields(null)).toBeNull();
});

it("returns null for an override with neither confidenceFloor nor scopeCap set", () => {
expect(toLiveGateThresholdFields({})).toBeNull();
});

it("projects confidenceFloor-only into confidence_floor, nulling both scope_cap fields", () => {
expect(toLiveGateThresholdFields({ confidenceFloor: 0.85 })).toEqual({
confidence_floor: 0.85,
scope_cap_files: null,
scope_cap_lines: null,
});
});

it("projects scopeCap-only into scope_cap_files/scope_cap_lines, nulling confidence_floor", () => {
expect(toLiveGateThresholdFields({ scopeCap: { files: 6, lines: 180 } })).toEqual({
confidence_floor: null,
scope_cap_files: 6,
scope_cap_lines: 180,
});
});

it("projects both fields when both confidenceFloor and scopeCap are set", () => {
expect(toLiveGateThresholdFields({ confidenceFloor: 0.7, scopeCap: { files: 3, lines: 90 } })).toEqual({
confidence_floor: 0.7,
scope_cap_files: 3,
scope_cap_lines: 90,
});
});
});

describe("rowToOverride (#273 — D1 row → validated override)", () => {
it("maps a full row", () => {
expect(rowToOverride({ confidence_floor: 0.95, scope_cap_files: 5, scope_cap_lines: 200, clear_at: null })).toEqual({
Expand Down