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
36 changes: 19 additions & 17 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,8 +929,8 @@ export async function runAiReviewForAdvisory(
/**
* AI-assisted slop advisory (opt-in `slopAiAdvisory`). Appends at most one ADVISORY-only `ai_slop_advisory`
* finding to the advisory; NEVER touches slopRisk or the gate (only the deterministic core can block). The
* caller gates on `settings.slopAiAdvisory` and reuses the already-fetched changed files. Fail-safe: any AI
* error is swallowed so the gate still finalizes.
* caller gates on `settings.slopAiAdvisory`, confirms the contributor, and reuses the already-fetched changed
* files. Fail-safe: any AI error is swallowed so the gate still finalizes.
*/
export async function runAiSlopForAdvisory(
env: Env,
Expand All @@ -939,11 +939,12 @@ export async function runAiSlopForAdvisory(
repoFullName: string;
pr: { number: number; title: string; body?: string | null | undefined };
author: string | null;
confirmedContributor: boolean;
files: Awaited<ReturnType<typeof listPullRequestFiles>>;
deterministicBand: SlopBand;
},
): Promise<void> {
if (!args.advisory.headSha) return;
if (!args.confirmedContributor || !args.advisory.headSha) return;
try {
const result = await runGittensoryAiSlopAdvisory(env, {
repoFullName: args.repoFullName,
Expand Down Expand Up @@ -1140,9 +1141,22 @@ async function maybePublishPrPublicSurface(
scopedOverlapCount: unionScopedOverlapClusters(collisions, pr, preflight.collisions).length,
});

if (gateEnabled && author && !publicSurfaceSkipped && !official) {
official = await getCachedOfficialMinerDetection(env, author, {
targetKey: `${repoFullName}#${pr.number}`,
deliveryId: webhook.deliveryId,
});
}

// Only CONFIRMED gittensor contributors can be hard-blocked; everyone else (or an unavailable
// detection) gets a neutral, non-blocking gate. Gate-only runs still verify confirmation before
// evaluating blockers so confirmed contributors cannot bypass a required Gate check.
const confirmedContributor = official?.status === "confirmed";

// Anti-slop (#530/#532): only when opted in (slopGateMode !== "off"). Surface the deterministic slop
// findings as advisory context, and feed the score to the gate (it only blocks under slop: block + the
// threshold). Loads files lazily so disabled repos pay nothing.
// threshold). Loads files lazily so disabled repos pay nothing. The AI advisory is additionally limited
// to confirmed contributors so untrusted PR authors cannot spend the shared Workers AI budget.
let slopRisk: number | null = null;
if (settings.slopGateMode !== "off") {
const slopFiles = await listPullRequestFiles(env, repoFullName, pr.number);
Expand All @@ -1155,22 +1169,10 @@ async function maybePublishPrPublicSurface(
// AI-assisted slop advisory (#533, opt-in). Reuses the already-fetched files; appends at most one
// advisory-only finding. Deliberately does NOT update slopRisk — only the deterministic core blocks.
if (settings.slopAiAdvisory) {
await runAiSlopForAdvisory(env, { advisory, repoFullName, pr, author, files: slopFiles, deterministicBand: slop.band });
await runAiSlopForAdvisory(env, { advisory, repoFullName, pr, author, confirmedContributor, files: slopFiles, deterministicBand: slop.band });
}
}

if (gateEnabled && author && !publicSurfaceSkipped && !official) {
official = await getCachedOfficialMinerDetection(env, author, {
targetKey: `${repoFullName}#${pr.number}`,
deliveryId: webhook.deliveryId,
});
}

// Only CONFIRMED gittensor contributors can be hard-blocked; everyone else (or an unavailable
// detection) gets a neutral, non-blocking gate. Gate-only runs still verify confirmation before
// evaluating blockers so confirmed contributors cannot bypass a required Gate check.
const confirmedContributor = official?.status === "confirmed";

// AI maintainer review (opt-in via aiReviewMode). Mutates `advisory` with a consensus defect (if any)
// BEFORE the gate evaluates, and returns advisory notes for the panel. Inside the try so any AI
// failure is caught and the gate is still finalized (never left in_progress).
Expand Down
5 changes: 3 additions & 2 deletions src/services/ai-slop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// • Fail-safe on every path: AI off / no binding / over-budget / unparseable / unsafe text → no finding.
// • Opt-in: only runs when the repo set `gate.slop.aiAdvisory: true` on top of `gate.slop.mode != off`.
//
// Free Cloudflare Workers AI only (one call, metered against the shared daily neuron budget). BYOK is a
// Free Cloudflare Workers AI only (bounded and metered against the shared daily neuron budget). BYOK is a
// possible later enhancement; slop assessment does not need a frontier model. Every public string is
// forced through `toPublicSafe`; anything tripping the public/private boundary is dropped, not published.
import type { SignalFinding } from "../signals/engine";
Expand Down Expand Up @@ -70,6 +70,7 @@ export type AiSlopResult =
type SlopOpinion = { band: SlopBand; rationale: string; signals: string[] };

const SLOP_BANDS: readonly SlopBand[] = ["clean", "low", "elevated", "high"];
const SLOP_AI_MAX_MODEL_CALLS = 6;

function isSlopBand(value: unknown): value is SlopBand {
return typeof value === "string" && (SLOP_BANDS as readonly string[]).includes(value);
Expand Down Expand Up @@ -173,7 +174,7 @@ export async function runGittensoryAiSlopAdvisory(env: Env, input: AiSlopInput):

const maxTokens = clampNumber(Number(env.AI_MAX_OUTPUT_TOKENS || 256), 256, 1024);
const user = buildUserPrompt(input);
const estimatedNeurons = estimateNeurons(SLOP_SYSTEM_PROMPT.length + user.length, maxTokens, 1);
const estimatedNeurons = estimateNeurons(SLOP_SYSTEM_PROMPT.length + user.length, maxTokens, SLOP_AI_MAX_MODEL_CALLS);
const budget = clampNumber(Number(env.AI_DAILY_NEURON_BUDGET || 10000), 0, 1_000_000);
const used = await sumAiEstimatedNeuronsSince(env, utcDayStartIso());
const remainingBudget = Math.max(0, budget - used);
Expand Down
23 changes: 21 additions & 2 deletions test/unit/ai-slop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,35 @@ describe("runAiSlopForAdvisory (processor wiring)", () => {
repoFullName: "acme/widgets",
pr,
author: "alice",
confirmedContributor: true,
files,
deterministicBand: "elevated",
});
expect(adv.findings.map((f) => f.code)).toEqual([AI_SLOP_FINDING_CODE]);
});


it("no-ops for unconfirmed contributors so untrusted PRs cannot spend AI budget", async () => {
const adv = advisory();
const run = vi.fn(async () => ({ response: slopJson({ band: "high" }) }));
await runAiSlopForAdvisory(enabledEnv(run), {
advisory: adv,
repoFullName: "acme/widgets",
pr,
author: "alice",
confirmedContributor: false,
files,
deterministicBand: "elevated",
});
expect(adv.findings).toEqual([]);
expect(run).not.toHaveBeenCalled();
});

it("no-ops when the advisory has no head SHA", async () => {
const noSha = advisory();
delete (noSha as Partial<Advisory>).headSha;
const run = vi.fn();
await runAiSlopForAdvisory(enabledEnv(run), { advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "low" });
await runAiSlopForAdvisory(enabledEnv(run), { advisory: noSha, repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true, files, deterministicBand: "low" });
expect(noSha.findings).toEqual([]);
expect(run).not.toHaveBeenCalled();
});
Expand All @@ -278,6 +296,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => {
repoFullName: "acme/widgets",
pr,
author: "alice",
confirmedContributor: true,
files,
deterministicBand: "clean",
});
Expand All @@ -287,7 +306,7 @@ describe("runAiSlopForAdvisory (processor wiring)", () => {
it("is fail-safe: a thrown error (broken DB) yields no finding and never throws", async () => {
const adv = advisory();
const env = { ...enabledEnv(async () => ({ response: slopJson() })), DB: undefined } as unknown as Env;
await expect(runAiSlopForAdvisory(env, { advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", files, deterministicBand: "high" })).resolves.toBeUndefined();
await expect(runAiSlopForAdvisory(env, { advisory: adv, repoFullName: "acme/widgets", pr, author: "alice", confirmedContributor: true, files, deterministicBand: "high" })).resolves.toBeUndefined();
expect(adv.findings).toEqual([]);
});
});