diff --git a/.gittensory.yml.example b/.gittensory.yml.example index a190d1c30d..7e3b499710 100644 --- a/.gittensory.yml.example +++ b/.gittensory.yml.example @@ -434,6 +434,14 @@ review: # null. Default: null/false — byte-identical. (#2995) # culture_profile: false + # Per-repo FORCE-OFF for the self-improvement/auto-tune cron pass (#4104) -- `false` excludes this repo from + # tuning even though it's otherwise agent-configured and the operator's GITTENSORY_REVIEW_SELFTUNE flag is on. + # Deliberately FORCE-OFF-ONLY (no `true` override): unlike most `review.*` toggles above, this does NOT live + # under a per-repo allowlist -- selftune's own scoping is whether the repo already opted into the + # acting-autonomy surface, a separate consent boundary this key must not bypass. Bool or null. Default: + # null/true -- no change to today's agent-configured-repos-only behavior. + # selftune: 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 GITTENSORY_REVIEW_MEMORY env flag to # be on -- this manifest field alone cannot enable it. When both are on, an advisory (non-blocking) AI finding @@ -889,6 +897,11 @@ settings: # # (recent_merged_pull_requests). Reference-only grounding, never a gate/scoring input; requires the operator # # flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or null. Default: null/false. (#2995) # culture_profile: false +# # Per-repo FORCE-OFF for the self-improvement/auto-tune cron pass (#4104) -- false excludes this repo from +# # tuning even though it's otherwise agent-configured and the operator flag is on. FORCE-OFF-ONLY, no true +# # override -- selftune's own scoping is the acting-autonomy consent boundary, not a per-repo allowlist. Bool +# # or null. Default: null/true -- no change to today's agent-configured-repos-only behavior. +# selftune: false # # When true (AND the operator's GITTENSORY_REVIEW_MEMORY env flag is also on), an advisory (non-blocking) # # AI finding is matched against this repo's stored review_suppression signals (a maintainer's own past # # false-positive dismissals) before it is surfaced, and demoted/dropped on a match. ADVISORY-ONLY: never diff --git a/config/examples/gittensory.full.yml b/config/examples/gittensory.full.yml index 60e1ac1abe..ac10a39870 100644 --- a/config/examples/gittensory.full.yml +++ b/config/examples/gittensory.full.yml @@ -447,6 +447,14 @@ review: # null. Default: null/false — byte-identical. (#2995) # culture_profile: false + # Per-repo FORCE-OFF for the self-improvement/auto-tune cron pass (#4104) -- `false` excludes this repo from + # tuning even though it's otherwise agent-configured and the operator's GITTENSORY_REVIEW_SELFTUNE flag is on. + # Deliberately FORCE-OFF-ONLY (no `true` override): unlike most `review.*` toggles above, this does NOT live + # under a per-repo allowlist -- selftune's own scoping is whether the repo already opted into the + # acting-autonomy surface, a separate consent boundary this key must not bypass. Bool or null. Default: + # null/true -- no change to today's agent-configured-repos-only behavior. + # selftune: 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 GITTENSORY_REVIEW_MEMORY env flag to # be on -- this manifest field alone cannot enable it. When both are on, an advisory (non-blocking) AI finding @@ -902,6 +910,11 @@ settings: # # (recent_merged_pull_requests). Reference-only grounding, never a gate/scoring input; requires the operator # # flag GITTENSORY_REVIEW_CULTURE_PROFILE. Bool or null. Default: null/false. (#2995) # culture_profile: false +# # Per-repo FORCE-OFF for the self-improvement/auto-tune cron pass (#4104) -- false excludes this repo from +# # tuning even though it's otherwise agent-configured and the operator flag is on. FORCE-OFF-ONLY, no true +# # override -- selftune's own scoping is the acting-autonomy consent boundary, not a per-repo allowlist. Bool +# # or null. Default: null/true -- no change to today's agent-configured-repos-only behavior. +# selftune: false # # When true (AND the operator's GITTENSORY_REVIEW_MEMORY env flag is also on), an advisory (non-blocking) # # AI finding is matched against this repo's stored review_suppression signals (a maintainer's own past # # false-positive dismissals) before it is surfaced, and demoted/dropped on a match. ADVISORY-ONLY: never diff --git a/packages/gittensory-engine/src/focus-manifest.ts b/packages/gittensory-engine/src/focus-manifest.ts index 8623c09b2d..2be9c9c754 100644 --- a/packages/gittensory-engine/src/focus-manifest.ts +++ b/packages/gittensory-engine/src/focus-manifest.ts @@ -175,7 +175,11 @@ export type FocusManifestGateConfig = { // shape as rag/reputation. `screenshots` has its own richer `visual:` block instead (review.visual.enabled, // #4083) since it carries more than a single boolean. contentLane got its own richer `contentLane:` block below // (#2435) instead of a boolean here, since it resolves to a whole RegistryLaneSpec, not an on/off toggle — see -// resolveRegistryLaneSpec in review/content-lane/spec-resolver.ts. +// resolveRegistryLaneSpec in review/content-lane/spec-resolver.ts. `selftune` (#4104) ALSO deliberately lives +// outside this block, as its own top-level `review.selftune` field below — it has no `GITTENSORY_REVIEW_REPOS` +// allowlist to fall back to (its own repo scoping is `isAgentConfigured`, a different consent boundary), so it +// doesn't fit this resolver's env-kill-switch → override → allowlist-default shape; see `selfTuneRepos` in +// `review/selftune-wire.ts`. export const CONVERGED_FEATURE_KEYS = ["rag", "reputation", "unifiedComment", "safety", "grounding"] as const; export type ConvergedFeatureKey = (typeof CONVERGED_FEATURE_KEYS)[number]; @@ -454,6 +458,16 @@ export type FocusManifestReviewConfig = { * field only opts THIS repo in once the capability itself is enabled). null/false (default, absent) = no * section appended = byte-identical behavior. */ cultureProfile: boolean | null; + /** `review.selftune` (#4104): explicit per-repo FORCE-OFF for the self-improvement/auto-tune cron pass + * (`runSelfTune`, `src/review/selftune-wire.ts`) — `false` excludes this repo from tuning even though it's + * otherwise agent-configured (`isAgentConfigured`) and the global `GITTENSORY_REVIEW_SELFTUNE` kill-switch is + * on. Deliberately FORCE-OFF-ONLY (no `true` override): forcing a NON-agent-configured repo INTO tuning would + * bypass that separate, broader acting-autonomy consent boundary, which this key must not touch. Unlike + * `impactMap`/`cultureProfile` above, there is no `GITTENSORY_REVIEW_REPOS` allowlist fallback for selftune — + * its own scoping is `isAgentConfigured`, not the cutover allowlist — so this does NOT live under the generic + * `features:` block/`resolveConvergedFeature` (see `CONVERGED_FEATURE_KEYS`'s own comment). null/true + * (default, absent) ⇒ no change to today's agent-configured-repos-only behavior. */ + selftune: 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 @@ -873,7 +887,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, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], 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, testGeneration: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, @@ -903,7 +917,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, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], 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, testGeneration: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { ...EMPTY_FEATURES_CONFIG }, contentLane: { ...EMPTY_CONTENT_LANE_CONFIG }, repoDocGeneration: { ...EMPTY_REPO_DOC_GENERATION_CONFIG }, @@ -1907,7 +1921,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, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], 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, testGeneration: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { ...EMPTY_MAX_FINDINGS_CONFIG }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], 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.`); @@ -1952,6 +1966,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo const testGeneration = normalizeOptionalBoolean(r.test_generation, "review.test_generation", warnings); const impactMap = normalizeOptionalBoolean(r.impact_map, "review.impact_map", warnings); const cultureProfile = normalizeOptionalBoolean(r.culture_profile, "review.culture_profile", warnings); + const selftune = normalizeOptionalBoolean(r.selftune, "review.selftune", warnings); const reviewMemory = normalizeOptionalBoolean(r.memory, "review.memory", warnings); const findingCategories = normalizeOptionalBoolean(r.finding_categories, "review.finding_categories", warnings); const inlineCommentsPerCategory = normalizeOptionalNonNegativeInt( @@ -1993,6 +2008,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo testGeneration !== null || impactMap !== null || cultureProfile !== null || + selftune !== null || reviewMemory !== null || findingCategories !== null || inlineCommentsPerCategory !== null || @@ -2031,6 +2047,7 @@ function parseReviewConfig(value: JsonValue | undefined, warnings: string[]): Fo effortScore, impactMap, cultureProfile, + selftune, reviewMemory, findingCategories, inlineCommentsPerCategory, @@ -2125,6 +2142,7 @@ function computeReviewConfigPresent(review: Omit { const repos = (await listRepositories(env)).filter((repo) => repo.isRegistered); const configured: string[] = []; for (const repo of repos) { try { const settings = await resolveRepositorySettings(env, repo.fullName); - if (isAgentConfigured(settings.autonomy)) configured.push(repo.fullName); + if (!isAgentConfigured(settings.autonomy)) continue; + const manifest = await loadRepoFocusManifest(env, repo.fullName).catch(() => null); + if (manifest?.review.selftune === false) continue; // explicit per-repo opt-out + configured.push(repo.fullName); } catch { /* a settings blip on one repo must not abort the whole tuning pass */ } diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 611311c417..fcef08492c 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -370,6 +370,7 @@ describe(".gittensory.yml.example field-exhaustiveness (#1670)", () => { testGeneration: "test_generation:", impactMap: "impact_map:", cultureProfile: "culture_profile:", + selftune: "selftune:", reviewMemory: "memory:", findingCategories: "finding_categories:", inlineCommentsPerCategory: "inline_comments_per_category:", @@ -801,7 +802,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, lockfileIntegrityMode: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, aiReviewAllAuthors: null, aiReviewCloseConfidence: null, aiReviewCombine: null, aiReviewOnMerge: null, aiReviewReviewers: null, mergeReadiness: null, selfAuthoredLinkedIssue: null, linkedIssueSatisfaction: null, manifestPolicy: null, dryRun: null, firstTimeContributorGrace: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: 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, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], 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, testGeneration: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { ...EMPTY_AUTO_REVIEW_CONFIG }, labelingRules: [], aiModel: { ...EMPTY_SELF_HOST_AI_MODEL_CONFIG }, visual: { ...EMPTY_VISUAL_CONFIG }, linkedIssueSatisfaction: null, sharedConfigSource: null }, features: { present: false, rag: null, reputation: null, unifiedComment: null, safety: null, grounding: null }, contentLane: { present: false, entryFileGlob: null, providerFileGlob: null, artifactGlob: null, collectionField: null, maxAppendedEntries: null, duplicateKeyFields: [], validatorId: null }, repoDocGeneration: { present: false, enabled: false, scope: ["agents"], allowOverwriteExisting: false, refreshIntervalDays: 7 }, diff --git a/test/unit/selftune-wiring.test.ts b/test/unit/selftune-wiring.test.ts index d602102d2b..6db17c62d6 100644 --- a/test/unit/selftune-wiring.test.ts +++ b/test/unit/selftune-wiring.test.ts @@ -13,6 +13,7 @@ import { SELFTUNE_BASE_CONFIDENCE_FLOOR, } from "../../src/review/selftune-wire"; import { computeTuningRecommendations } from "../../src/review/auto-tune"; +import { upsertRepoFocusManifest } from "../../src/signals/focus-manifest-loader"; import { createTestEnv } from "../helpers/d1"; // Wrap env.DB.prepare so any SQL matching `pattern` throws (exercising a fail-safe catch); all other @@ -258,6 +259,49 @@ describe("runSelfTune — shadow-soak over gittensory's own outcome data", () => }); }); +describe("selfTuneRepos — per-repo review.selftune FORCE-OFF (#4104)", () => { + it("REGRESSION: an explicit review.selftune: false excludes an otherwise agent-configured repo from the tuning pass entirely", async () => { + const env = createTestEnv({ GITTENSORY_REVIEW_SELFTUNE: "true" }); + await seedRegisteredRepo(env, "owner/opted-out", ACTING_AUTONOMY); + await seedRecommendationOutcomes(env, "owner/opted-out", 5, 10); // would otherwise be a clear tightening signal + await upsertRepoFocusManifest(env, "owner/opted-out", { review: { selftune: false } }); + + await runSelfTune(env); + + expect(await loadShadowOverride(env as never, "owner/opted-out")).toBeNull(); + expect(await loadOverride(env as never, "owner/opted-out")).toBeNull(); + expect((await listOverrideAudit(env as never, "owner/opted-out")).length).toBe(0); + }); + + it("unset review.selftune (the default) does not change today's behavior — an agent-configured repo still tunes normally", async () => { + const env = createTestEnv({ GITTENSORY_REVIEW_SELFTUNE: "true" }); + await seedRegisteredRepo(env, "owner/repo", ACTING_AUTONOMY); + await seedRecommendationOutcomes(env, "owner/repo", 5, 10); + // No manifest published at all for this repo -- byte-identical to every repo before this change. + + await runSelfTune(env); + + expect((await loadShadowOverride(env as never, "owner/repo"))?.override.confidenceFloor).toBeGreaterThan(0); + }); + + it("an explicit review.selftune: true is a no-op — it does not force a NON-agent-configured repo into the tuning pass", async () => { + const owner = "owner"; + const name = "no-autonomy"; + const env = createTestEnv({ GITTENSORY_REVIEW_SELFTUNE: "true" }); + await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, ?, ?, 1, 1)") + .bind(`${owner}/${name}`, owner, name) + .run(); + // Deliberately NOT opted into the acting-autonomy surface (no repository_settings row at all). + await seedRecommendationOutcomes(env, `${owner}/${name}`, 5, 10); + await upsertRepoFocusManifest(env, `${owner}/${name}`, { review: { selftune: true } }); + + await runSelfTune(env); + + // Still excluded -- review.selftune has no `true` override; isAgentConfigured is the only way in. + expect(await loadShadowOverride(env as never, `${owner}/${name}`)).toBeNull(); + }); +}); + // ── Config-application is DEFERRED — sanity: the base floor seam is the unset/loosest state ────────────────── describe("config-application deferred (documented seam)", () => { diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 14491baba2..2d90a7a396 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1138,7 +1138,7 @@ describe("signal coverage edge cases", () => { collisions: buildCollisionReport(directRepo.fullName, [], [currentPr]), preflight: buildPreflightResult({ repoFullName: directRepo.fullName, title: "Fix isolated issue", body: "Fixes #99", linkedIssues: [99] }, directRepo, [], [currentPr]), settings: gateSettings, - review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null }, linkedIssueSatisfaction: null, sharedConfigSource: null }, + review: { present: true, footerText: "Reviewed by the Acme maintainer bot.", note: "Run npm test before pushing.", fields: { relatedWork: false }, enrichmentAnalyzers: {}, profile: null, tone: null, securityFocus: null, inlineComments: null, fixHandoff: null, autoMergeSummary: null, suggestions: null, changedFilesSummary: null, effortScore: null, testGeneration: null, impactMap: null, cultureProfile: null, selftune: null, reviewMemory: null, findingCategories: null, inlineCommentsPerCategory: null, minFindingSeverity: null, maxFindings: { blockers: null, nits: null }, commentVerbosity: null, pathInstructions: [], instructions: null, excludePaths: [], pathFilters: [], preMergeChecks: [], autoReview: { skipDrafts: null, ignoreAuthors: [], ignoreTitleKeywords: [], skipLabels: [], skipDocsOnly: null, maxAddedLines: 0, maxFiles: 0, baseBranches: [], autoPauseAfterReviewedCommits: null }, labelingRules: [], aiModel: { claudeModel: null, claudeEffort: null, codexModel: null, codexEffort: null, ollamaModel: null, openaiModel: null, openaiCompatibleModel: null, anthropicModel: null }, visual: { preview: { urlTemplate: null }, routes: { paths: [], maxRoutes: null }, themes: [], gif: false, enabled: null }, linkedIssueSatisfaction: null, sharedConfigSource: null }, aiReview: { notes: "The change is focused.\n\n**Nits (2)**\n- Add a test for the edge case.\n- Keep the validator helper scoped." }, }); expect(customizedComment).toContain("Reviewed by the Acme maintainer bot."); // custom footer lead