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
5 changes: 5 additions & 0 deletions migrations/0075_ai_review_all_authors.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AI review for all authors (per-repo opt-in). The AI maintainer review is confirmed-contributor-gated by
-- default (an AI-spend guard, see runAiReviewForAdvisory). `ai_review_all_authors` lets a self-host operator
-- run the review for EVERY PR's author — intended for an operator who wants real reviews on all PRs (incl. their
-- own) and pays for the AI themselves. Default 0 (off) — additive, existing repos are byte-identical.
ALTER TABLE repository_settings ADD COLUMN ai_review_all_authors INTEGER NOT NULL DEFAULT 0;
21 changes: 21 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ import { buildRepoOutcomeCalibration } from "../services/outcome-calibration";
import { loadGatePrecisionReport } from "../services/gate-precision";
import { computeOpsStats, isOpsEnabled } from "../review/ops-wire";
import { computeParityReadiness, isParityAuditEnabled } from "../review/parity-wire";
import { isRagEnabled } from "../review/rag-wire";
import { getPublicStats, isPublicStatsEnabled } from "../review/public-stats";
import { buildMaintainerQualityDashboard, isMaintainerQualityDataStale } from "../services/maintainer-quality-dashboard";
import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics";
Expand Down Expand Up @@ -619,6 +620,7 @@ const repositorySettingsSchema = z.object({
aiReviewByok: z.boolean().default(false),
aiReviewProvider: z.enum(["anthropic", "openai"]).nullable().optional(),
aiReviewModel: z.string().trim().min(1).max(120).nullable().optional(),
aiReviewAllAuthors: z.boolean().default(false),
autoLabelEnabled: z.boolean().default(true),
gittensorLabel: z.string().trim().min(1).max(50).default("gittensor"),
blacklistLabel: z.string().trim().min(1).max(50).default("slop"),
Expand Down Expand Up @@ -710,6 +712,7 @@ const repositoryAiReviewSchema = z.object({
byok: z.boolean().default(false),
provider: z.enum(["anthropic", "openai"]).nullable().optional(),
model: z.string().trim().min(1).max(120).nullable().optional(),
allAuthors: z.boolean().default(false),
});

const contributorIssueDraftGenerateSchema = z.object({
Expand Down Expand Up @@ -2246,13 +2249,15 @@ export function createApp() {
aiReviewByok: parsed.data.byok,
aiReviewProvider: parsed.data.provider,
aiReviewModel: parsed.data.model,
aiReviewAllAuthors: parsed.data.allAuthors,
});
// getRepositorySettings normalizes these to a concrete value or null (never undefined).
return c.json({
aiReviewMode: updated.aiReviewMode,
aiReviewByok: updated.aiReviewByok,
aiReviewProvider: updated.aiReviewProvider ?? null,
aiReviewModel: updated.aiReviewModel ?? null,
aiReviewAllAuthors: updated.aiReviewAllAuthors,
});
});

Expand Down Expand Up @@ -3056,6 +3061,21 @@ export function createApp() {
return c.json({ ok: true, status: "queued" }, 202);
});

// Operator-facing RAG (re)index trigger for a self-host maintainer. Bearer-gated by the `/v1/internal/*`
// middleware (INTERNAL_JOB_TOKEN). With NO body it enqueues the fan-out (re-indexes every RAG-active configured +
// registered repo); with `{ "repoFullName": "owner/repo" }` it indexes just that repo. Either way the job is
// gated downstream by convergedFeatureActive, so a repo where RAG is off is a no-op. 404 when RAG is globally off
// so the endpoint doesn't exist on a deploy that isn't running RAG. This is how an operator adds/indexes a new
// repo on demand instead of waiting for the 6-hourly cron.
app.post("/v1/internal/jobs/rag-index", async (c) => {
if (!isRagEnabled(c.env)) return c.json({ error: "not_found" }, 404);
const body = (await c.req.json().catch(() => ({}))) as { repoFullName?: unknown };
const repoFullName = typeof body?.repoFullName === "string" && body.repoFullName.trim().length > 0 ? body.repoFullName.trim() : undefined;
const message: JobMessage = { type: "rag-index-repo", requestedBy: "api", ...(repoFullName ? { repoFullName } : {}) };
await c.env.JOBS.send(message);
return c.json({ ok: true, status: "queued", scope: repoFullName ?? "all-configured-repos" }, 202);
});

app.post("/v1/internal/jobs/refresh-registry/run", async (c) => {
return c.json(await refreshRegistry(c.env));
});
Expand Down Expand Up @@ -3365,6 +3385,7 @@ export function createApp() {
aiReviewByok: parsed.data.aiReviewByok,
aiReviewProvider: parsed.data.aiReviewProvider,
aiReviewModel: parsed.data.aiReviewModel,
aiReviewAllAuthors: parsed.data.aiReviewAllAuthors,
autoLabelEnabled: parsed.data.autoLabelEnabled,
gittensorLabel: parsed.data.gittensorLabel,
blacklistLabel: parsed.data.blacklistLabel,
Expand Down
5 changes: 5 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewByok: false,
aiReviewProvider: null,
aiReviewModel: null,
aiReviewAllAuthors: false,
autoLabelEnabled: true,
gittensorLabel: "gittensor",
blacklistLabel: "slop",
Expand Down Expand Up @@ -473,6 +474,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise
aiReviewByok: row.aiReviewByok,
aiReviewProvider: normalizeAiReviewProvider(row.aiReviewProvider),
aiReviewModel: row.aiReviewModel ?? null,
aiReviewAllAuthors: row.aiReviewAllAuthors,
autoLabelEnabled: row.autoLabelEnabled,
gittensorLabel: row.gittensorLabel,
blacklistLabel: row.blacklistLabel,
Expand Down Expand Up @@ -519,6 +521,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewByok: settings.aiReviewByok ?? false,
aiReviewProvider: normalizeAiReviewProvider(settings.aiReviewProvider),
aiReviewModel: typeof settings.aiReviewModel === "string" && settings.aiReviewModel.trim() ? settings.aiReviewModel.trim() : null,
aiReviewAllAuthors: settings.aiReviewAllAuthors ?? false,
autoLabelEnabled: settings.autoLabelEnabled ?? true,
gittensorLabel: settings.gittensorLabel ?? "gittensor",
blacklistLabel: settings.blacklistLabel ?? "slop",
Expand Down Expand Up @@ -563,6 +566,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewByok: resolved.aiReviewByok,
aiReviewProvider: resolved.aiReviewProvider,
aiReviewModel: resolved.aiReviewModel,
aiReviewAllAuthors: resolved.aiReviewAllAuthors,
autoLabelEnabled: resolved.autoLabelEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
Expand Down Expand Up @@ -608,6 +612,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial<Repos
aiReviewByok: resolved.aiReviewByok,
aiReviewProvider: resolved.aiReviewProvider,
aiReviewModel: resolved.aiReviewModel,
aiReviewAllAuthors: resolved.aiReviewAllAuthors,
autoLabelEnabled: resolved.autoLabelEnabled,
gittensorLabel: resolved.gittensorLabel,
blacklistLabel: resolved.blacklistLabel,
Expand Down
1 change: 1 addition & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const repositorySettings = sqliteTable("repository_settings", {
aiReviewByok: integer("ai_review_byok", { mode: "boolean" }).notNull().default(false),
aiReviewProvider: text("ai_review_provider"),
aiReviewModel: text("ai_review_model"),
aiReviewAllAuthors: integer("ai_review_all_authors", { mode: "boolean" }).notNull().default(false),
autoLabelEnabled: integer("auto_label_enabled", { mode: "boolean" }).notNull().default(true),
gittensorLabel: text("gittensor_label").notNull().default("gittensor"),
// Label applied to a blacklisted contributor's PR/issue (#1425); configurable so the disposition works
Expand Down
5 changes: 5 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ declare global {
JOBS: Queue;
RATE_LIMITER?: DurableObjectNamespace;
AI?: Ai;
/** Self-host (RAG): a DEDICATED embedding provider, kept SEPARATE from the review chat chain so the reviewer
* stays frontier-only (claude-code/codex) while embeddings — which those CLIs cannot produce — route to a
* local/openai-compatible endpoint (ollama). Built at boot from AI_EMBED_BASE_URL/AI_EMBED_MODEL. Absent ⇒
* `createReviewAdapters` falls back to `env.AI` (byte-identical to before). */
AI_EMBED?: Ai;
/** Convergence (infra): Vectorize index for codebase RAG retrieval (Layer C). Optional — the review is
* fully fail-safe without it (absent ⇒ no RAG, review proceeds with no retrieved context). The index is
* created with bge-m3's 1024 dimensions. Unused until the per-module RAG wiring chunk; an unbound deploy
Expand Down
Loading
Loading