From df22386cd9228a726144cbf79186dd24b8d3d154 Mon Sep 17 00:00:00 2001 From: St0rmz1 Date: Mon, 20 Jul 2026 09:13:05 -0700 Subject: [PATCH 1/6] WIP wiring up council configs per repo config --- .../code-review-status/[reviewId]/route.ts | 32 ++++++++++++ .../council/finalize-council-result.test.ts | 50 ++++++++++++++++++- .../council/finalize-council-result.ts | 9 +++- .../webhook-handlers/pull-request-handler.ts | 21 ++++++++ packages/db/src/schema-types.ts | 6 +++ .../src/code-review-council.test.ts | 26 ++++++++-- .../worker-utils/src/code-review-council.ts | 30 +++++++---- 7 files changed, 158 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts b/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts index 1613b08937..bc2638eadf 100644 --- a/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts +++ b/apps/web/src/app/api/internal/code-review-status/[reviewId]/route.ts @@ -107,6 +107,8 @@ import { getManualCodeReviewConfig, shouldPublishCodeReviewToProvider, } from '@/lib/code-reviews/manual-config'; +import { getAgentConfigForOwner } from '@/lib/agent-config/db/agent-configs'; +import type { CodeReviewAgentConfig } from '@kilocode/db/schema-types'; const CallbackTextTruncationSchema = z .object({ @@ -535,6 +537,25 @@ function isSupersededReview(review: CloudAgentCodeReview): boolean { return review.terminal_reason === 'superseded'; } +/** + * Resolves the org/user Code Reviewer config for a review, used as the council-config source for + * AUTOMATED (webhook) council reviews, which carry no `manual_config`. Manual reviews resolve + * their council from `manual_config` and never need this. Returns null on any gap (no owner, no + * config) so council finalization fails safe rather than throwing. + */ +async function resolveOrgCodeReviewAgentConfig( + review: CloudAgentCodeReview +): Promise { + const owner = review.owned_by_organization_id + ? ({ type: 'org', id: review.owned_by_organization_id } as const) + : review.owned_by_user_id + ? ({ type: 'user', id: review.owned_by_user_id } as const) + : null; + if (!owner) return null; + const agentConfig = await getAgentConfigForOwner(owner, 'code_review', review.platform); + return agentConfig ? (agentConfig.config as CodeReviewAgentConfig) : null; +} + async function resolveTerminalOwner( review: CloudAgentCodeReview, reviewId: string @@ -1060,6 +1081,15 @@ export async function POST( // Code-owned council outcome for a completed council run. Set on whichever completion path // runs below, then used to drive the merge gate (the council LLM never sets `gateResult`). let councilResult: CodeReviewCouncilResult | null = null; + // Council-config source for AUTOMATED (webhook) council reviews, which carry no `manual_config`. + // Fetched once (only for completed council reviews without a manual config) and reused on + // whichever completion path runs. Manual/standard reviews skip the lookup. + const councilOrgAgentConfig: CodeReviewAgentConfig | null = + status === 'completed' && + review.review_type === 'council' && + getManualCodeReviewConfig(review) === null + ? await resolveOrgCodeReviewAgentConfig(review) + : null; if ( status === 'completed' && @@ -1076,6 +1106,7 @@ export async function POST( councilResult = computeCouncilResultForReview({ review, lastAssistantMessageText: rawPayload.lastAssistantMessageText, + orgAgentConfig: councilOrgAgentConfig, }); const completionResult = await finalizeCompletedCodeReviewWithAnalytics({ codeReviewId: reviewId, @@ -1370,6 +1401,7 @@ export async function POST( councilResult = await finalizeCouncilResultForReview({ review, lastAssistantMessageText: rawPayload.lastAssistantMessageText, + orgAgentConfig: councilOrgAgentConfig, }); } diff --git a/apps/web/src/lib/code-reviews/council/finalize-council-result.test.ts b/apps/web/src/lib/code-reviews/council/finalize-council-result.test.ts index cb46bd7361..efa170d187 100644 --- a/apps/web/src/lib/code-reviews/council/finalize-council-result.test.ts +++ b/apps/web/src/lib/code-reviews/council/finalize-council-result.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from '@jest/globals'; import { COUNCIL_RESULT_MARKER_TAG } from '@kilocode/worker-utils/code-review-council'; -import type { CodeReviewCouncilConfig } from '@kilocode/db/schema-types'; -import { buildCouncilResult } from './finalize-council-result'; +import type { CloudAgentCodeReview } from '@kilocode/db/schema'; +import type { CodeReviewAgentConfig, CodeReviewCouncilConfig } from '@kilocode/db/schema-types'; +import { buildCouncilResult, computeCouncilResultForReview } from './finalize-council-result'; const council: CodeReviewCouncilConfig = { enabled: true, @@ -185,3 +186,48 @@ describe('buildCouncilResult', () => { expect(result.specialists.every(s => s.vote === null)).toBe(true); }); }); + +describe('computeCouncilResultForReview — automated (webhook) council config source', () => { + // Webhook council reviews carry no manual_config; the council config comes from the org config. + const webhookReview = { + review_type: 'council', + manual_config: null, + } as unknown as CloudAgentCodeReview; + const orgAgentConfig = { model_slug: 'base/model', council } as unknown as CodeReviewAgentConfig; + const manifest = manifestText([ + { specialistId: 'security', findings: crit }, + { specialistId: 'performance', findings: [] }, + ]); + + it('falls back to the org agent config when there is no manual_config', () => { + const result = computeCouncilResultForReview({ + review: webhookReview, + lastAssistantMessageText: manifest, + orgAgentConfig, + }); + expect(result?.decision).toBe('block'); // unanimous + one critical + expect(result?.specialists).toHaveLength(2); + expect(result?.specialists[0].model).toBe('anthropic/x'); // configured per-specialist model + expect(result?.specialists[1].model).toBe('base/model'); // inherits the org base model + }); + + it('returns null for a webhook council review when no org config is available (fail-safe)', () => { + expect( + computeCouncilResultForReview({ + review: webhookReview, + lastAssistantMessageText: manifest, + orgAgentConfig: null, + }) + ).toBeNull(); + }); + + it('returns null for a non-council review even with an org council config', () => { + expect( + computeCouncilResultForReview({ + review: { review_type: 'standard', manual_config: null } as unknown as CloudAgentCodeReview, + lastAssistantMessageText: manifest, + orgAgentConfig, + }) + ).toBeNull(); + }); +}); diff --git a/apps/web/src/lib/code-reviews/council/finalize-council-result.ts b/apps/web/src/lib/code-reviews/council/finalize-council-result.ts index 9c28afde8f..a199fb64e3 100644 --- a/apps/web/src/lib/code-reviews/council/finalize-council-result.ts +++ b/apps/web/src/lib/code-reviews/council/finalize-council-result.ts @@ -2,6 +2,7 @@ import 'server-only'; import type { CloudAgentCodeReview } from '@kilocode/db/schema'; import type { + CodeReviewAgentConfig, CodeReviewCouncilConfig, CodeReviewCouncilResult, CouncilResultSpecialist, @@ -101,10 +102,15 @@ export function buildCouncilResult(params: { export function computeCouncilResultForReview(params: { review: CloudAgentCodeReview; lastAssistantMessageText: string | null | undefined; + // Council-config source for AUTOMATED (webhook) reviews, which carry no `manual_config`. + // Manual reviews resolve their council from `manual_config` and ignore this. Resolved + passed + // by the caller (the status callback) because this function is pure/DB-free and also runs inside + // the analytics completion transaction. Absent → automated council reviews persist no result. + orgAgentConfig?: CodeReviewAgentConfig | null; }): CodeReviewCouncilResult | null { const { review, lastAssistantMessageText } = params; if (review.review_type !== 'council') return null; - const agentConfig = getManualCodeReviewConfig(review)?.agentConfig; + const agentConfig = getManualCodeReviewConfig(review)?.agentConfig ?? params.orgAgentConfig ?? null; const council = agentConfig?.council; if (!council) return null; @@ -134,6 +140,7 @@ export function computeCouncilResultForReview(params: { export async function finalizeCouncilResultForReview(params: { review: CloudAgentCodeReview; lastAssistantMessageText: string | null | undefined; + orgAgentConfig?: CodeReviewAgentConfig | null; }): Promise { const councilResult = computeCouncilResultForReview(params); if (!councilResult) return null; diff --git a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/pull-request-handler.ts b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/pull-request-handler.ts index a45920ab54..873235b370 100644 --- a/apps/web/src/lib/integrations/platforms/github/webhook-handlers/pull-request-handler.ts +++ b/apps/web/src/lib/integrations/platforms/github/webhook-handlers/pull-request-handler.ts @@ -13,6 +13,11 @@ import { } from '@/lib/code-reviews/db/code-reviews'; import { tryDispatchPendingReviews } from '@/lib/code-reviews/dispatch/dispatch-pending-reviews'; import { getAgentConfigForOwner } from '@/lib/agent-config/db/agent-configs'; +import { + determineAutomatedReviewType, + isCouncilActive, +} from '@kilocode/worker-utils/code-review-council'; +import { isCouncilEntitledForOwner } from '@/lib/code-reviews/core/council-entitlement'; import type { PlatformIntegration } from '@kilocode/db/schema'; import type { Owner } from '@/lib/code-reviews/core'; import { getBotUserId } from '@/lib/bot-users/bot-user-service'; @@ -297,9 +302,25 @@ export async function handlePullRequestCodeReview( ); } + // 6b. Decide standard vs council for this automated review. Council is a per-repo opt-in and + // requires an active council config + entitlement. The entitlement lookup is a DB call, so it + // is gated behind the two cheap local checks (most webhooks are standard and skip it). Falls + // back to 'standard' if any condition is missing, so a bad/absent council config never blocks. + const councilConfigActive = isCouncilActive(config.council); + const councilEnabledForRepo = + Array.isArray(config.council_enabled_repository_ids) && + config.council_enabled_repository_ids.includes(repository.id); + const councilEntitled = + councilConfigActive && councilEnabledForRepo ? await isCouncilEntitledForOwner(owner) : false; + const reviewType = determineAutomatedReviewType( + { isDraft: pull_request.draft ?? false, author: pull_request.user.login }, + { councilEntitled, councilConfigActive, councilEnabledForRepo } + ); + // 7. Create review record (session_id will be updated async) const reviewId = await createCodeReview({ owner, + reviewType, platformIntegrationId: integration.id, repoFullName: repository.full_name, prNumber: pull_request.number, diff --git a/packages/db/src/schema-types.ts b/packages/db/src/schema-types.ts index 807e823d89..419edb869b 100644 --- a/packages/db/src/schema-types.ts +++ b/packages/db/src/schema-types.ts @@ -1492,6 +1492,12 @@ export const CodeReviewAgentConfigSchema = z.object({ manually_added_repositories: z.array(ManuallyAddedRepositorySchema).optional(), // Per-repository model overrides. Absent/empty = every repo uses the global model_slug. repository_model_overrides: z.array(RepositoryModelOverrideSchema).optional(), + // Per-repository council opt-in. A repository whose ID is listed here runs the COUNCIL review + // type on automated (webhook) reviews, using the single org-level `council` config above. + // Absent/empty = no repo runs council automatically. Only meaningful when the org is + // council-entitled and `council` is configured + active; the automated trigger re-checks both. + // Matched against the platform repository ID the same way as `selected_repository_ids`. + council_enabled_repository_ids: z.array(z.union([z.number(), z.string()])).optional(), disable_review_md: z.boolean().optional(), // Controls when the PR gate check (GitHub Check Run / GitLab commit status) // reports a failure based on review findings. diff --git a/packages/worker-utils/src/code-review-council.test.ts b/packages/worker-utils/src/code-review-council.test.ts index 590597c101..1fd6d87c15 100644 --- a/packages/worker-utils/src/code-review-council.test.ts +++ b/packages/worker-utils/src/code-review-council.test.ts @@ -551,12 +551,32 @@ describe('presets', () => { }); describe('determineAutomatedReviewType', () => { - it('is a safe stub that always returns standard', () => { - expect(determineAutomatedReviewType({}, { councilAvailable: true })).toBe('standard'); + const ALL_ON = { + councilEntitled: true, + councilConfigActive: true, + councilEnabledForRepo: true, + }; + + it('returns council only when entitled AND config active AND repo opted in', () => { + expect(determineAutomatedReviewType({}, ALL_ON)).toBe('council'); + }); + + it('falls back to standard when any condition is missing (fail-safe)', () => { + expect(determineAutomatedReviewType({}, { ...ALL_ON, councilEntitled: false })).toBe('standard'); + expect(determineAutomatedReviewType({}, { ...ALL_ON, councilConfigActive: false })).toBe( + 'standard' + ); + expect(determineAutomatedReviewType({}, { ...ALL_ON, councilEnabledForRepo: false })).toBe( + 'standard' + ); + }); + + it('does not use SCM-controlled PR facts (a "council" label cannot force council)', () => { + // Entitlement/config/opt-in are Kilo-side; a dev-controlled label must not upgrade the type. expect( determineAutomatedReviewType( { isDraft: false, labels: ['council'], changedFileCount: 40 }, - { councilAvailable: true } + { councilEntitled: false, councilConfigActive: false, councilEnabledForRepo: false } ) ).toBe('standard'); }); diff --git a/packages/worker-utils/src/code-review-council.ts b/packages/worker-utils/src/code-review-council.ts index 29a3fcb960..af6223d120 100644 --- a/packages/worker-utils/src/code-review-council.ts +++ b/packages/worker-utils/src/code-review-council.ts @@ -716,20 +716,30 @@ export type AutomatedReviewPrFacts = { }; /** - * Determines the review type for an AUTOMATED (webhook) run from PR facts. + * Determines the review type for an AUTOMATED (webhook) run. * - * STUB (intentional, phased plan): the real standard-vs-council determination is later - * work — it must be configured/evaluated Kilo-side and resistant to SCM-side abuse (a - * dev must not be able to force paid council reviews via a PR label). For now this is a - * safe stub that always returns `'standard'`, so automated reviews behave exactly as - * they do today. The plumbing (passing full PR facts + `councilAvailable`) is defined - * here so the logic can be filled in at the webhook step without further wiring. + * Plan A: an automated review runs the COUNCIL type only when ALL of these hold, otherwise it + * falls back to `'standard'` (fail-safe — a bad or missing council config never blocks reviews): + * - `councilEntitled` — the org is council-entitled (enterprise + active entitlement). This is + * the abuse-resistant gate: it is decided Kilo-side, never from an SCM-controlled signal. + * - `councilConfigActive` — the org has a configured, active council (>= min specialists). + * - `councilEnabledForRepo` — the target repo explicitly opted in + * (`config.council_enabled_repository_ids`). Council is per-repo opt-in, not org-wide. * - * Manual runs never call this — they carry an explicit user-selected review type. + * PR-facts based gating (draft/size/labels) is intentionally NOT applied yet: every PR on an + * opted-in repo gets a council review. `prFacts` is threaded through so that policy can be added + * here later without re-wiring the webhook handlers. Manual runs never call this — they carry an + * explicit user-selected review type. */ export function determineAutomatedReviewType( _prFacts: AutomatedReviewPrFacts, - _options: { councilAvailable: boolean } + options: { + councilEntitled: boolean; + councilConfigActive: boolean; + councilEnabledForRepo: boolean; + } ): CodeReviewType { - return 'standard'; + const useCouncil = + options.councilEntitled && options.councilConfigActive && options.councilEnabledForRepo; + return useCouncil ? 'council' : 'standard'; } From 33a310323fb92df58d9b96b0fd43a811408dcd8e Mon Sep 17 00:00:00 2001 From: St0rmz1 Date: Mon, 20 Jul 2026 11:56:58 -0700 Subject: [PATCH 2/6] feat(code-reviwer) adds advanced settings to config screen --- .../code-reviews/ReviewAgentPageClient.tsx | 10 +- .../code-reviews/ReviewConfigForm.tsx | 489 ++++++++++++------ .../core/council-selection.test.ts | 33 ++ .../code-reviews/core/council-selection.ts | 26 +- .../council/finalize-council-result.ts | 3 +- apps/web/src/routers/code-reviews-router.ts | 6 + .../organization-code-reviews-router.ts | 27 + .../src/code-review-council.test.ts | 4 +- 8 files changed, 434 insertions(+), 164 deletions(-) diff --git a/apps/web/src/app/(app)/organizations/[id]/code-reviews/ReviewAgentPageClient.tsx b/apps/web/src/app/(app)/organizations/[id]/code-reviews/ReviewAgentPageClient.tsx index a7e4d61eec..acf8a2ba9a 100644 --- a/apps/web/src/app/(app)/organizations/[id]/code-reviews/ReviewAgentPageClient.tsx +++ b/apps/web/src/app/(app)/organizations/[id]/code-reviews/ReviewAgentPageClient.tsx @@ -74,6 +74,9 @@ export function ReviewAgentPageClient({ ) ); const councilEntitled = councilEntitlement?.entitled ?? false; + // Same gate as the manual council UI: local dev, or an entitled org behind the rollout flag. + const councilUiEnabled = + localCodeReviewDevelopmentEnabled || (councilEntitled && !!councilFlagEnabled); const handlePlatformChange = (platform: Platform) => { const params = new URLSearchParams(); @@ -297,7 +300,11 @@ export function ReviewAgentPageClient({ - + @@ -397,6 +404,7 @@ export function ReviewAgentPageClient({ >(new Map()); - // Opt-in toggle for the per-repository model section. - const [overridesEnabled, setOverridesEnabled] = useState(false); + // Org-level council config (shared by every council-enabled repo) + per-repo opt-in set. + // Council is "on" when at least one repo opts in — there is no separate global toggle. + const [councilAggregation, setCouncilAggregation] = useState( + DEFAULT_COUNCIL_AGGREGATION_STRATEGY + ); + const [councilSelections, setCouncilSelections] = useState< + Record + >(() => defaultCouncilSelections()); + const [councilEnabledRepositoryIds, setCouncilEnabledRepositoryIds] = useState>( + new Set() + ); const [useReviewMd, setUseReviewMd] = useState(true); // GitLab-specific: auto-configure webhooks const [autoConfigureWebhooks, setAutoConfigureWebhooks] = useState(true); @@ -365,7 +394,20 @@ export function ReviewConfigForm({ } } setRepositoryModelOverrides(overridesMap); - setOverridesEnabled(overridesMap.size > 0); + const loadedCouncil = configData.council ?? null; + setCouncilAggregation( + loadedCouncil?.aggregation_strategy ?? DEFAULT_COUNCIL_AGGREGATION_STRATEGY + ); + setCouncilSelections( + loadedCouncil ? councilSelectionsFromConfig(loadedCouncil) : defaultCouncilSelections() + ); + setCouncilEnabledRepositoryIds( + new Set( + (configData.councilEnabledRepositoryIds ?? []).filter( + (repositoryId): repositoryId is number => typeof repositoryId === 'number' + ) + ) + ); setUseReviewMd(!(configData.disableReviewMd ?? false)); } }, [configData, isGitLab]); @@ -494,6 +536,32 @@ export function ReviewConfigForm({ } }; + // Master "Simple vs Advanced" switch: Advanced reveals per-repository overrides (repo selection, + // per-repo model/effort, per-repo council). GitLab has no "all" mode, so it is always Advanced. + const perRepoOverridesEnabled = isGitLab || repositorySelectionMode === 'selected'; + const handlePerRepoOverridesToggle = (enabled: boolean) => { + setRepositorySelectionMode(enabled ? 'selected' : 'all'); + }; + + // Council is on when 1+ repos opt in; the shared specialist picker then applies to all of them. + const councilEnabled = councilEnabledRepositoryIds.size > 0; + const councilEnabledCount = countEnabledSelections(councilSelections); + const councilBelowMin = councilEnabled && councilEnabledCount < COUNCIL_MIN_SPECIALISTS; + // Only repositories the user actually selected can be configured per-repo (model + council), + // so the per-repo pickers never offer a repo that isn't being reviewed. + const selectedRepositories = selectableRepositories.filter(repo => + selectedRepositoryIds.includes(repo.id) + ); + + const handleCouncilRepoToggle = (repositoryId: number, enabled: boolean) => { + setCouncilEnabledRepositoryIds(prev => { + const next = new Set(prev); + if (enabled) next.add(repositoryId); + else next.delete(repositoryId); + return next; + }); + }; + const handleSave = () => { // Clear previous webhook sync result setWebhookSyncResult(null); @@ -507,7 +575,7 @@ export function ReviewConfigForm({ // Overrides are independent of the trigger selection mode — they apply whether // reviews run on all or selected repositories. Sent only when the per-repository // section is toggled on; toggling it off clears the overrides on save. - const repositoryModelOverridesPayload = overridesEnabled + const repositoryModelOverridesPayload = perRepoOverridesEnabled ? Array.from(repositoryModelOverrides.entries()).map(([repositoryId, value]) => ({ repositoryId, repoFullName: value.repoFullName, @@ -516,6 +584,23 @@ export function ReviewConfigForm({ })) : []; + // Council is org-only (requires an enterprise org). Persist the built config when the section + // is visible + enabled; otherwise clear it. Block the save if council is on but under-staffed. + if (councilUiEnabled && councilBelowMin) { + toast.error('Council needs more specialists', { + description: `Select at least ${COUNCIL_MIN_SPECIALISTS} council specialists.`, + }); + return; + } + const councilPayload = + councilUiEnabled && councilEnabled + ? { + enabled: true, + aggregation_strategy: councilAggregation, + specialists: buildCouncilSpecialists(councilSelections), + } + : null; + if (organizationId) { orgSaveMutation.mutate({ organizationId, @@ -530,6 +615,8 @@ export function ReviewConfigForm({ selectedRepositoryIds, manuallyAddedRepositories, repositoryModelOverrides: repositoryModelOverridesPayload, + council: councilPayload, + councilEnabledRepositoryIds: Array.from(councilEnabledRepositoryIds), disableReviewMd: !useReviewMd, // GitLab-specific: auto-configure webhooks autoConfigureWebhooks: isGitLab ? autoConfigureWebhooks : undefined, @@ -621,6 +708,14 @@ export function ReviewConfigForm({ {/* Configuration Fields */}
+ {/* Global Settings — defaults inherited by every repository unless overridden below. */} +
+

Global Settings

+

+ Defaults applied to every repository unless overridden per repository below. +

+
+ {/* Default AI Model Selection */}
- {/* Repository Selection */} + {/* Focus Areas (global) */} +
+ +

+ Select specific areas for the agent to pay special attention to +

+
+ {FOCUS_AREAS.map(area => ( +
+ handleFocusAreaToggle(area.id)} + /> +
+ +

{area.description}

+
+
+ ))} +
+
+ + {/* Custom Instructions (global) */}
+ +