From c70e8116fc89a79d4bfc136f11bf9a068f0a3f35 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:52:58 -0700 Subject: [PATCH] feat(selfhost): per-repo opt-out for the active-review reconciliation sweep Add review.activeReviewReconciliation: false as an explicit per-repo FORCE-OFF, mirroring review.prReconciliation/review.sweepWatchdog exactly -- config-as-code for this class of fleet-wide sweep is meant to be both globally toggleable AND fine-tunable per repo, which the initial cut missed. Wire the opt-out into runActiveReviewReconciliation so a repo's stale rows are skipped before any GitHub call is spent, failing open on a manifest-load error like every sibling check. Also fixes a pre-existing bug in the engine package's own test script found while re-verifying this change: `rm -rf dist-test` deleted the compiled test output but left `.test.tsbuildinfo` behind, so a second `npm run test` invocation could see the stale incremental cache, skip re-emitting entirely, and `node --test` would then report 0 tests found as a trivial success -- silently no-op-ing the engine's whole test suite while the gate still passed. Clearing both together forces a real recompile every run. And fixes a flaky timestamp-inequality assertion in the out-of-order-webhook regression test added in the prior commit (two upserts can land in the same test millisecond), matching the same caveat already documented on the sibling review-latency-metric tests in this file. --- .loopover.yml.example | 7 ++++ config/examples/loopover.full.yml | 7 ++++ packages/loopover-engine/package.json | 2 +- .../loopover-engine/src/focus-manifest.ts | 24 ++++++++++--- src/review/active-review-reconciliation.ts | 6 ++++ .../unit/active-review-reconciliation.test.ts | 36 +++++++++++++++++++ test/unit/db-parsers.test.ts | 5 ++- test/unit/focus-manifest.test.ts | 3 +- 8 files changed, 83 insertions(+), 7 deletions(-) diff --git a/.loopover.yml.example b/.loopover.yml.example index a490d030fe..d2e3077b13 100644 --- a/.loopover.yml.example +++ b/.loopover.yml.example @@ -539,6 +539,13 @@ review: # Bool or null. Default: null/true -- no change to today's watched-repo-set behavior. # prReconciliation: false + # Per-repo FORCE-OFF for the active-review-tracking reconciliation sweep (#webhook-reorder-clobber) -- + # `false` excludes just this repo's rows from the sweep's scan even though the operator's + # LOOPOVER_ACTIVE_REVIEW_RECONCILIATION flag is on. Deliberately FORCE-OFF-ONLY (no `true` override), + # mirroring `prReconciliation` immediately above. Bool or null. Default: null/true -- no change to today's + # scanned-row-set behavior. + # activeReviewReconciliation: false + # Repeat-false-positive suppression (#2179, part of #1964). Bool | null. Default: null/false — byte-identical # (no suppression-store read, no matching). Also requires the operator's LOOPOVER_REVIEW_MEMORY env flag to # be on -- this manifest field alone cannot enable it. When both are on, an advisory (non-blocking) AI finding diff --git a/config/examples/loopover.full.yml b/config/examples/loopover.full.yml index a256f4404f..1ee4ca0690 100644 --- a/config/examples/loopover.full.yml +++ b/config/examples/loopover.full.yml @@ -553,6 +553,13 @@ review: # Bool or null. Default: null/true -- no change to today's watched-repo-set behavior. # prReconciliation: false + # Per-repo FORCE-OFF for the active-review-tracking reconciliation sweep (#webhook-reorder-clobber) -- + # `false` excludes just this repo's rows from the sweep's scan even though the operator's + # LOOPOVER_ACTIVE_REVIEW_RECONCILIATION flag is on. Deliberately FORCE-OFF-ONLY (no `true` override), + # mirroring `prReconciliation` immediately above. Bool or null. Default: null/true -- no change to today's + # scanned-row-set behavior. + # activeReviewReconciliation: false + # Repeat-false-positive suppression (#2179, part of #1964). Bool | null. Default: null/false — byte-identical # (no suppression-store read, no matching). Also requires the operator's LOOPOVER_REVIEW_MEMORY env flag to # be on -- this manifest field alone cannot enable it. When both are on, an advisory (non-blocking) AI finding diff --git a/packages/loopover-engine/package.json b/packages/loopover-engine/package.json index ed22263e5d..1354230603 100644 --- a/packages/loopover-engine/package.json +++ b/packages/loopover-engine/package.json @@ -73,7 +73,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "test": "npm run build && rm -rf dist-test && tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"" + "test": "npm run build && rm -rf dist-test .test.tsbuildinfo && tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\"" }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.205", diff --git a/packages/loopover-engine/src/focus-manifest.ts b/packages/loopover-engine/src/focus-manifest.ts index 302031b317..534026273a 100644 --- a/packages/loopover-engine/src/focus-manifest.ts +++ b/packages/loopover-engine/src/focus-manifest.ts @@ -499,7 +499,9 @@ export type FocusManifestPrReconciliationConfig = { * (LOOPOVER_ACTIVE_REVIEW_RECONCILIATION), declared under top-level `activeReviewReconciliation:` * (#webhook-reorder-clobber). Same shape and precedence as `prReconciliation:` above -- the sweep re-checks * `active_review_tracking` rows a delayed webhook job left stuck "active" for a PR that already closed. - * Not present ⇒ the caller falls back to the LOOPOVER_ACTIVE_REVIEW_RECONCILIATION env var. + * Not present ⇒ the caller falls back to the LOOPOVER_ACTIVE_REVIEW_RECONCILIATION env var. Distinct from + * the per-repo FORCE-OFF under `review.activeReviewReconciliation` (which only excludes one repo's rows + * from the scan set once this fleet-wide gate is ON). */ export type FocusManifestActiveReviewReconciliationConfig = { present: boolean; @@ -793,6 +795,14 @@ export type FocusManifestReviewConfig = { * above, for the identical reason. A manifest-load error fails OPEN (the repo stays watched). null/true * (default, absent) ⇒ no change to today's watched-repo-set behavior. */ prReconciliation: boolean | null; + /** `review.activeReviewReconciliation` (#webhook-reorder-clobber): explicit per-repo FORCE-OFF for the + * active-review-tracking reconciliation sweep (`runActiveReviewReconciliation`, + * `src/review/active-review-reconciliation.ts`) — `false` excludes this repo's stale `active_review_tracking` + * rows from the sweep's scan even though the global `LOOPOVER_ACTIVE_REVIEW_RECONCILIATION` kill-switch is + * on. Deliberately FORCE-OFF-ONLY (no `true` override), mirroring `selftune`/`sweepWatchdog`/`prReconciliation` + * above, for the identical reason. A manifest-load error fails OPEN (the repo's rows stay eligible). null/true + * (default, absent) ⇒ no change to today's scanned-row-set behavior. */ + activeReviewReconciliation: boolean | null; /** `review.memory` (#2179, config slice of #1964): when true, gates repeat-false-positive SUPPRESSION — * before an advisory (non-blocking) AI finding is surfaced in the unified review comment, it is matched * against this repo's stored `review_suppression` signals (a maintainer's own past false-positive @@ -1412,7 +1422,7 @@ const EMPTY_MANIFEST: FocusManifest = { publicNotes: [], gate: { ...EMPTY_GATE_CONFIG }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, activeReviewReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, experimental: { ...EMPTY_EXPERIMENTAL_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, @@ -1453,7 +1463,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo warnings, gate: { ...EMPTY_GATE_CONFIG }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, activeReviewReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, experimental: { ...EMPTY_EXPERIMENTAL_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, @@ -2937,7 +2947,7 @@ function parsePublicSafeText(value: JsonValue | undefined, field: string, warnin * throws; invalid/unsafe values are dropped with warnings. */ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestReviewConfig { - const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }; + const empty: FocusManifestReviewConfig = { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, activeReviewReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }; if (value === undefined || value === null) return empty; if (typeof value !== "object" || Array.isArray(value)) { warnings.push(`Manifest field "review" must be a mapping; ignoring it.`); @@ -2984,6 +2994,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo const selftune = normalizeOptionalBoolean(r.selftune, "review.selftune", warnings); const sweepWatchdog = normalizeOptionalBoolean(r.sweepWatchdog, "review.sweepWatchdog", warnings); const prReconciliation = normalizeOptionalBoolean(r.prReconciliation, "review.prReconciliation", warnings); + const activeReviewReconciliation = normalizeOptionalBoolean(r.activeReviewReconciliation, "review.activeReviewReconciliation", warnings); const reviewMemory = normalizeOptionalBoolean(r.memory, "review.memory", warnings); const findingCategories = normalizeOptionalBoolean(r.finding_categories, "review.finding_categories", warnings); const inlineCommentsPerCategory = normalizeOptionalNonNegativeInt( @@ -3028,6 +3039,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo selftune !== null || sweepWatchdog !== null || prReconciliation !== null || + activeReviewReconciliation !== null || reviewMemory !== null || findingCategories !== null || inlineCommentsPerCategory !== null || @@ -3069,6 +3081,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo selftune, sweepWatchdog, prReconciliation, + activeReviewReconciliation, reviewMemory, findingCategories, inlineCommentsPerCategory, @@ -3174,6 +3187,7 @@ function computeReviewConfigPresent(review: Omit null); + if (manifest?.review.activeReviewReconciliation === false) continue; const token = (await createInstallationToken(env, repo.installationId).catch(() => undefined)) ?? env.GITHUB_PUBLIC_TOKEN; const admissionKey = githubRateLimitAdmissionKeyForToken(env, token, repo.installationId); const liveState = await fetchLivePullRequestState(env, row.repoFullName, row.pullNumber, token, admissionKey); diff --git a/test/unit/active-review-reconciliation.test.ts b/test/unit/active-review-reconciliation.test.ts index 990d182f01..1150558116 100644 --- a/test/unit/active-review-reconciliation.test.ts +++ b/test/unit/active-review-reconciliation.test.ts @@ -11,6 +11,7 @@ import * as repositoriesModule from "../../src/db/repositories"; import * as backfillModule from "../../src/github/backfill"; import { counterValue, resetMetrics } from "../../src/selfhost/metrics"; import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; +import * as focusManifestLoaderModule from "../../src/signals/focus-manifest-loader"; import { createTestEnv } from "../helpers/d1"; const SELF_REPO = "JSONbored/loopover"; @@ -177,6 +178,41 @@ describe("runActiveReviewReconciliation (#webhook-reorder-clobber)", () => { expect(liveSpy).not.toHaveBeenCalled(); }); + it("REGRESSION: an explicit review.activeReviewReconciliation: false excludes an otherwise-eligible repo's rows from the scan entirely", async () => { + const env = createTestEnv(); + await seedStaleActiveRow(env, "owner/opted-out", 9, 9507, STALE_ACTIVE_REVIEW_MIN_AGE_MS + 60_000); + await upsertRepoFocusManifest(env, "owner/opted-out", { review: { activeReviewReconciliation: false } }); + const liveSpy = vi.spyOn(backfillModule, "fetchLivePullRequestState"); + + const reconciled = await runActiveReviewReconciliation(env); + + expect(reconciled).toEqual([]); + expect(liveSpy).not.toHaveBeenCalled(); // opted-out before any GitHub call is ever spent + expect(await hasActiveReviewForHeadSha(env, "owner/opted-out", 9, "sha1")).toBe(true); // row untouched + }); + + it("an explicit review.activeReviewReconciliation: true is a no-op -- the repo's rows are scanned exactly as when unset", async () => { + const env = createTestEnv(); + await seedStaleActiveRow(env, "owner/opted-in", 10, 9508, STALE_ACTIVE_REVIEW_MIN_AGE_MS + 60_000); + await upsertRepoFocusManifest(env, "owner/opted-in", { review: { activeReviewReconciliation: true } }); + vi.spyOn(backfillModule, "fetchLivePullRequestState").mockResolvedValueOnce("closed"); + + const reconciled = await runActiveReviewReconciliation(env); + + expect(reconciled).toEqual([{ repoFullName: "owner/opted-in", pullNumber: 10 }]); + }); + + it("fails OPEN on a manifest-load error for the row's own repo -- a config-read blip must never silently exclude a row from reconciliation", async () => { + const env = createTestEnv(); + await seedStaleActiveRow(env, "owner/manifest-errors", 11, 9509, STALE_ACTIVE_REVIEW_MIN_AGE_MS + 60_000); + vi.spyOn(focusManifestLoaderModule, "loadRepoFocusManifest").mockRejectedValueOnce(new Error("manifest load failed")); + vi.spyOn(backfillModule, "fetchLivePullRequestState").mockResolvedValueOnce("closed"); + + const reconciled = await runActiveReviewReconciliation(env); + + expect(reconciled).toEqual([{ repoFullName: "owner/manifest-errors", pullNumber: 11 }]); + }); + it("fails safe per-row: an error on one row is logged and the scan continues to the next row", async () => { const env = createTestEnv(); await seedStaleActiveRow(env, "owner/erroring-repo", 6, 9504, STALE_ACTIVE_REVIEW_MIN_AGE_MS + 60_000); diff --git a/test/unit/db-parsers.test.ts b/test/unit/db-parsers.test.ts index 1c20c37953..768e84d18e 100644 --- a/test/unit/db-parsers.test.ts +++ b/test/unit/db-parsers.test.ts @@ -721,7 +721,10 @@ describe("database row parser hardening", () => { number: 36, title: "PR", state: "open", user: { login: "bob" }, head: { sha: "a2" }, labels: [], updated_at: "2026-07-21T12:05:00.000Z", }); expect(pushed.headSha).toBe("a2"); - expect(pushed.headShaObservedAt).not.toBe(first.headShaObservedAt); + // Not asserting inequality against first.headShaObservedAt (both could land in the same millisecond in + // a fast test run, matching the same caveat the review-latency-metric tests above already document) -- + // typeof "string" here plus the exact-preservation assertion below is the deterministic, non-flaky check. + expect(typeof pushed.headShaObservedAt).toBe("string"); // A delayed job for the OLDER `review_requested` event (before the push) finally dequeues, still carrying // the STALE head "a1". Without resolvedHeadSha driving headShaChanged, this would look like "a2 -> a1", a diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 573c57690a..b5dfc82c18 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -411,6 +411,7 @@ describe(".loopover.yml.example field-exhaustiveness (#1670)", () => { selftune: "selftune:", sweepWatchdog: "sweepWatchdog:", prReconciliation: "prReconciliation:", + activeReviewReconciliation: "activeReviewReconciliation:", reviewMemory: "memory:", findingCategories: "finding_categories:", inlineCommentsPerCategory: "inline_comments_per_category:", @@ -943,7 +944,7 @@ describe("compileFocusManifestPolicy", () => { publicNotes: ["Keep PRs focused.", "Maximize your reward payout"], gate: { present: false, enabled: null, checkMode: null, pack: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, slopMode: null, slopMinScore: null, slopAiAdvisory: null, sizeMode: null, sizeMaxFiles: null, sizeMaxLines: null, lockfileIntegrityMode: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, aiReviewAllAuthors: null, aiReviewCloseConfidence: null, aiReviewLowConfidenceDisposition: null, aiReviewCombine: null, aiReviewOnMerge: null, aiReviewReviewers: null, mergeReadiness: null, selfAuthoredLinkedIssue: null, linkedIssueSatisfaction: null, contentLaneDeliverable: null, manifestPolicy: null, dryRun: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, staleBaseAheadByThreshold: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: null, advisoryCheckRuns: null, aiJudgmentBlockersMode: null, copycatMode: null, copycatMinScore: null }, settings: {}, - review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, + review: { present: false, footerText: null, note: null, fields: {}, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, impactMap: null, cultureProfile: null, selftune: null, sweepWatchdog: null, prReconciliation: null, activeReviewReconciliation: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, e2eTestDelivery: null, e2eTestAutoTrigger: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { present: false, rag: null, reputation: null, safety: null, grounding: null, e2eTests: null, screenshots: null, improvementSignal: null, amsReputationBridge: null }, experimental: { present: false, gittensor: null }, contentLane: { present: false, entryFileGlob: null, providerFileGlob: null, artifactGlob: null, collectionField: null, maxAppendedEntries: null, duplicateKeyFields: [], validatorId: null },