diff --git a/apps/gittensory-ui/public/openapi.json b/apps/gittensory-ui/public/openapi.json index fe05bb57f8..becbedf7eb 100644 --- a/apps/gittensory-ui/public/openapi.json +++ b/apps/gittensory-ui/public/openapi.json @@ -8017,6 +8017,14 @@ }, "firstTimeContributorGrace": { "type": "boolean" + }, + "manifestPolicyGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] } }, "required": [ @@ -8033,6 +8041,7 @@ "qualityGateMode", "slopGateMode", "mergeReadinessGateMode", + "manifestPolicyGateMode", "firstTimeContributorGrace", "slopAiAdvisory", "autoLabelEnabled", @@ -8624,6 +8633,14 @@ }, "firstTimeContributorGrace": { "type": "boolean" + }, + "manifestPolicyGateMode": { + "type": "string", + "enum": [ + "off", + "advisory", + "block" + ] } }, "required": [ @@ -8640,6 +8657,7 @@ "qualityGateMode", "slopGateMode", "mergeReadinessGateMode", + "manifestPolicyGateMode", "firstTimeContributorGrace", "autoLabelEnabled", "gittensorLabel", diff --git a/migrations/0040_manifest_policy_gate.sql b/migrations/0040_manifest_policy_gate.sql new file mode 100644 index 0000000000..9101b09528 --- /dev/null +++ b/migrations/0040_manifest_policy_gate.sql @@ -0,0 +1,6 @@ +-- Focus-manifest policy gate (#555). One tunable `manifest_policy_gate_mode`: off (default) | advisory | +-- block. When set to block, the focus manifest's declared policy (blocked paths, required-linked-issue, test +-- expectations) becomes an enforceable `Gittensory Gate` blocker — surfaced through the single required check. +-- An INDEPENDENT dimension, deliberately not folded into the merge-readiness composite. Default 'off' +-- preserves existing behavior for every current repo. +ALTER TABLE repository_settings ADD COLUMN manifest_policy_gate_mode TEXT NOT NULL DEFAULT 'off'; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index aef947574e..7460b1717e 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -402,6 +402,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise qualityGateMinScore: null, slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopGateMinScore: null, slopAiAdvisory: false, @@ -436,6 +437,7 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise qualityGateMinScore: normalizeQualityGateMinScore(row.qualityGateMinScore), slopGateMode: parseGateRuleMode(row.slopGateMode), mergeReadinessGateMode: parseGateRuleMode(row.mergeReadinessGateMode), + manifestPolicyGateMode: parseGateRuleMode(row.manifestPolicyGateMode), firstTimeContributorGrace: row.firstTimeContributorGrace, slopGateMinScore: normalizeQualityGateMinScore(row.slopGateMinScore), slopAiAdvisory: row.slopAiAdvisory, @@ -474,6 +476,7 @@ export async function upsertRepositorySettings(env: Env, settings: Partial> | null = null; + if (settings.slopGateMode !== "off" || settings.manifestPolicyGateMode !== "off") { + gateFiles = await listPullRequestFiles(env, repoFullName, pr.number); + } if (settings.slopGateMode !== "off") { - const slopFiles = await listPullRequestFiles(env, repoFullName, pr.number); + const slopFiles = gateFiles ?? []; const slop = buildSlopAssessment({ changedFiles: slopFiles.map((file) => ({ path: file.path, additions: file.additions, deletions: file.deletions })), description: pr.body, @@ -1240,6 +1247,33 @@ async function maybePublishPrPublicSurface( await runAiSlopForAdvisory(env, { settings, advisory, repoFullName, pr, author, files: slopFiles, deterministicBand: slop.band, confirmedContributor }); } } + // Focus-manifest policy (#555, opt-in via manifestPolicyGateMode). Reload the CACHED manifest (the + // settings resolver discards the raw manifest, but loadRepoFocusManifest is cached so this is cheap), + // recompute the guidance over the PR's changed files, and push ONLY the three enforceable policy + // findings into the advisory so isConfiguredGateBlocker can block under manifestPolicy: block. + if (settings.manifestPolicyGateMode !== "off") { + const manifestFiles = gateFiles ?? []; + const manifest = await loadRepoFocusManifest(env, repoFullName); + const guidance = buildFocusManifestGuidance({ + manifest, + changedPaths: manifestFiles.map((file) => file.path), + labels: pr.labels, + linkedIssueCount: pr.linkedIssues.length, + testFileCount: manifestFiles.filter((file) => isTestPath(file.path)).length, + passedValidationCount: 0, + }); + const policyCodes = new Set(["manifest_blocked_path", "manifest_linked_issue_required", "manifest_missing_tests"]); + for (const finding of guidance.findings) { + if (!policyCodes.has(finding.code)) continue; + advisory.findings.push({ + code: finding.code, + severity: finding.severity, + title: finding.title, + detail: finding.detail, + ...(finding.action !== undefined ? { action: finding.action } : {}), + }); + } + } // 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 diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts index 88ec3c3794..984371fdea 100644 --- a/src/rules/advisory.ts +++ b/src/rules/advisory.ts @@ -32,6 +32,11 @@ export type GateCheckPolicy = { * linked-issue, duplicate, quality/readiness, slop — to its mode, so a maintainer flips ONE switch instead * of four and `Gittensory Gate` stays the single required check. `off` = sub-gates use their own modes. */ mergeReadinessGateMode?: GateRuleMode | undefined; + /** Focus-manifest policy gate (#555). When `block`, the focus manifest's declared policy findings — + * `manifest_blocked_path`, `manifest_linked_issue_required`, `manifest_missing_tests` — become hard + * blockers. An INDEPENDENT dimension, deliberately NOT folded into the merge-readiness composite so #555 + * stays focused. `off`/`advisory` = the findings stay advisory (never block). Default off. */ + manifestPolicyGateMode?: GateRuleMode | undefined; /** First-time-contributor grace (#552). When true AND the author is a genuine newcomer (0 merged PRs in * this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs), a would-be BLOCK is softened to a * neutral/advisory gate. `undefined`/false = the grace rule does not apply and blockers gate normally. */ @@ -177,7 +182,7 @@ function isCodePath(path: string): boolean { return /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|cs|cpp|c|h|swift|kt|m|sql|yaml|yml|json|toml|md)$/i.test(path); } -function isTestPath(path: string): boolean { +export function isTestPath(path: string): boolean { return ( /(^|\/)(test|tests|spec|__tests__)\//i.test(path) || /\.(test|spec)\.(ts|tsx|js|jsx|py|go|rs)$/i.test(path) || @@ -604,6 +609,11 @@ function isConfiguredGateBlocker(code: string, policy: GateCheckPolicy): boolean // most conservative AI signal (two independent models, high confidence) but still confirmed-contributor // gated by evaluateGateCheck, and advisory by default. if (code === "ai_consensus_defect") return gateMode(policy.aiReviewGateMode ?? "advisory") === "block"; + // Focus-manifest policy (#555): the three enforceable manifest findings block ONLY when the maintainer + // opts into manifestPolicy: block. Default off/advisory keeps them advisory-only. + if (code === "manifest_blocked_path" || code === "manifest_linked_issue_required" || code === "manifest_missing_tests") { + return gateMode(policy.manifestPolicyGateMode ?? "off") === "block"; + } return false; } diff --git a/src/signals/focus-manifest.ts b/src/signals/focus-manifest.ts index f739456ee1..7c4006815a 100644 --- a/src/signals/focus-manifest.ts +++ b/src/signals/focus-manifest.ts @@ -30,6 +30,7 @@ export type FocusManifestGateConfig = { aiReviewProvider: "anthropic" | "openai" | null; aiReviewModel: string | null; mergeReadiness: GateRuleMode | null; + manifestPolicy: GateRuleMode | null; firstTimeContributorGrace: boolean | null; }; @@ -160,6 +161,7 @@ const EMPTY_GATE_CONFIG: FocusManifestGateConfig = { aiReviewProvider: null, aiReviewModel: null, mergeReadiness: null, + manifestPolicy: null, firstTimeContributorGrace: null, }; @@ -302,6 +304,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu aiReviewProvider: normalizeOptionalEnum(aiReviewRecord?.provider, "gate.aiReview.provider", ["anthropic", "openai"] as const, warnings), aiReviewModel: normalizeOptionalString(aiReviewRecord?.model, "gate.aiReview.model", warnings), mergeReadiness: normalizeOptionalGateMode(record.mergeReadiness, "gate.mergeReadiness", warnings), + manifestPolicy: normalizeOptionalGateMode(record.manifestPolicy, "gate.manifestPolicy", warnings), firstTimeContributorGrace: normalizeOptionalBoolean(record.firstTimeContributorGrace, "gate.firstTimeContributorGrace", warnings), }; gate.present = @@ -319,6 +322,7 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu gate.aiReviewProvider !== null || gate.aiReviewModel !== null || gate.mergeReadiness !== null || + gate.manifestPolicy !== null || gate.firstTimeContributorGrace !== null; return gate; } @@ -356,6 +360,7 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue { out.aiReview = aiReview; } if (gate.mergeReadiness !== null) out.mergeReadiness = gate.mergeReadiness; + if (gate.manifestPolicy !== null) out.manifestPolicy = gate.manifestPolicy; if (gate.firstTimeContributorGrace !== null) out.firstTimeContributorGrace = gate.firstTimeContributorGrace; return out; } @@ -503,6 +508,7 @@ export function resolveEffectiveSettings(dbSettings: RepositorySettings, manifes if (gate.aiReviewProvider !== null) effective.aiReviewProvider = gate.aiReviewProvider; if (gate.aiReviewModel !== null) effective.aiReviewModel = gate.aiReviewModel; if (gate.mergeReadiness !== null) effective.mergeReadinessGateMode = gate.mergeReadiness; + if (gate.manifestPolicy !== null) effective.manifestPolicyGateMode = gate.manifestPolicy; if (gate.firstTimeContributorGrace !== null) effective.firstTimeContributorGrace = gate.firstTimeContributorGrace; return effective; } diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts index f0fb9a7d90..91ab43a1b3 100644 --- a/src/signals/settings-preview.ts +++ b/src/signals/settings-preview.ts @@ -189,6 +189,7 @@ export type RepoSettingsPreview = { qualityGateMinScore?: number | null | undefined; slopGateMode: RepositorySettings["slopGateMode"]; mergeReadinessGateMode: RepositorySettings["mergeReadinessGateMode"]; + manifestPolicyGateMode: RepositorySettings["manifestPolicyGateMode"]; firstTimeContributorGrace: boolean; slopGateMinScore?: number | null | undefined; autoLabelEnabled: boolean; @@ -306,6 +307,7 @@ export function buildRepoSettingsPreview(args: { qualityGateMinScore: settings.qualityGateMinScore ?? null, slopGateMode: settings.slopGateMode, mergeReadinessGateMode: settings.mergeReadinessGateMode, + manifestPolicyGateMode: settings.manifestPolicyGateMode, firstTimeContributorGrace: settings.firstTimeContributorGrace, slopGateMinScore: settings.slopGateMinScore ?? null, autoLabelEnabled: settings.autoLabelEnabled, diff --git a/src/types.ts b/src/types.ts index de60f9b4f2..ae9fe67abd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -415,6 +415,10 @@ export type RepositorySettings = { slopGateMode: GateRuleMode; /** Merge-readiness gate (#merge-readiness). `off`/`advisory`/`block`. No min-score. Default `off`. */ mergeReadinessGateMode: GateRuleMode; + /** Focus-manifest policy gate (#555). When `block`, the focus manifest's declared policy (blocked paths, + * required-linked-issue, test expectations) becomes an enforceable `Gittensory Gate` blocker. An + * INDEPENDENT dimension, deliberately not folded into the merge-readiness composite. Default `off` — opt-in. */ + manifestPolicyGateMode: GateRuleMode; /** First-time-contributor grace (#552). When true, a would-be BLOCK is softened to a neutral/advisory gate * for a genuine newcomer (0 merged PRs in this repo) who is NOT a repeat offender (< 3 closed-unmerged PRs). * Repeat offenders and authors with merge history are gated normally. Default false — opt-in. */ diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index 34f704c143..331c102647 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -400,7 +400,7 @@ describe("compileFocusManifestPolicy", () => { issueDiscoveryPolicy: "neutral", maintainerNotes: [], publicNotes: ["Keep PRs focused.", "Maximize your reward payout"], - gate: { present: false, enabled: null, pack: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, slopMode: null, slopMinScore: null, slopAiAdvisory: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, mergeReadiness: null, firstTimeContributorGrace: null }, + gate: { present: false, enabled: null, pack: null, linkedIssue: null, duplicates: null, readinessMode: null, readinessMinScore: null, slopMode: null, slopMinScore: null, slopAiAdvisory: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, mergeReadiness: null, manifestPolicy: null, firstTimeContributorGrace: null }, settings: {}, review: { present: false, footerText: null, note: null, fields: {} }, warnings: [], @@ -688,7 +688,7 @@ describe("parseFocusManifest gate config", () => { it("parses a full gate section including the readiness block", () => { const m = parseFocusManifest({ gate: { linkedIssue: "block", duplicates: "advisory", readiness: { mode: "block", minScore: 70 } } }); expect(m.present).toBe(true); - expect(m.gate).toEqual({ present: true, enabled: null, pack: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "block", readinessMinScore: 70, slopMode: null, slopMinScore: null, slopAiAdvisory: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, mergeReadiness: null, firstTimeContributorGrace: null }); + expect(m.gate).toEqual({ present: true, enabled: null, pack: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "block", readinessMinScore: 70, slopMode: null, slopMinScore: null, slopAiAdvisory: null, aiReviewMode: null, aiReviewByok: null, aiReviewProvider: null, aiReviewModel: null, mergeReadiness: null, manifestPolicy: null, firstTimeContributorGrace: null }); }); it("parses gate.mergeReadiness + gate.firstTimeContributorGrace, round-trips them, and warns on bad values (#822)", () => { @@ -703,6 +703,17 @@ describe("parseFocusManifest gate config", () => { expect(bad.gate.present).toBe(false); }); + it("parses gate.manifestPolicy, round-trips it through gateConfigToJson, and warns + nulls on a bad value (#555)", () => { + const m = parseFocusManifest({ gate: { manifestPolicy: "block" } }); + expect(m.gate.present).toBe(true); + expect(m.gate.manifestPolicy).toBe("block"); + expect(gateConfigToJson(m.gate)).toMatchObject({ manifestPolicy: "block" }); + const bad = parseFocusManifest({ gate: { manifestPolicy: "sometimes" } }); + expect(bad.gate.manifestPolicy).toBeNull(); + expect(bad.gate.present).toBe(false); + expect(bad.warnings.some((w) => w.includes("gate.manifestPolicy"))).toBe(true); + }); + it("parses the gate.slop block, round-trips it, and warns on a non-mapping (#530/#532)", () => { const m = parseFocusManifest({ gate: { slop: { mode: "block", minScore: 55 } } }); expect(m.gate.present).toBe(true); diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts index a7dc097484..38f97addfa 100644 --- a/test/unit/gate-check-policy.test.ts +++ b/test/unit/gate-check-policy.test.ts @@ -277,3 +277,57 @@ describe("first-time-contributor grace (#552)", () => { expect(evaluateGateCheck(missingIssueAdvisory(), { ...policy, linkedIssueGateMode: "block" }).conclusion).toBe("neutral"); }); }); + +describe("focus-manifest policy gate (#555)", () => { + // The three enforceable manifest-policy findings buildFocusManifestGuidance emits. + const POLICY_FINDINGS = { + manifest_blocked_path: { code: "manifest_blocked_path", title: "Change touches a maintainer-blocked area", severity: "critical" as const, detail: "Changed paths match maintainer-blocked patterns.", action: "Move out of the blocked area." }, + manifest_linked_issue_required: { code: "manifest_linked_issue_required", title: "Maintainer requires a linked issue", severity: "warning" as const, detail: "Manifest requires a linked issue.", action: "Link the issue." }, + manifest_missing_tests: { code: "manifest_missing_tests", title: "Maintainer test expectations unmet", severity: "warning" as const, detail: "Manifest expects test evidence.", action: "Add tests." }, + }; + + function manifestAdvisory(code: keyof typeof POLICY_FINDINGS): Advisory { + return { ...missingIssueAdvisory(), findings: [POLICY_FINDINGS[code]] }; + } + + for (const code of Object.keys(POLICY_FINDINGS) as (keyof typeof POLICY_FINDINGS)[]) { + describe(code, () => { + it("blocks a confirmed contributor when manifestPolicy: block", () => { + const result = evaluateGateCheck(manifestAdvisory(code), { manifestPolicyGateMode: "block", confirmedContributor: true }); + expect(result.conclusion).toBe("failure"); + expect(result.blockers.map((finding) => finding.code)).toContain(code); + }); + + it("does not block when manifestPolicy: off (advisory-only)", () => { + expect(evaluateGateCheck(manifestAdvisory(code), { manifestPolicyGateMode: "off", confirmedContributor: true }).conclusion).toBe("success"); + }); + + it("does not block when manifestPolicy: advisory (advisory != block)", () => { + expect(evaluateGateCheck(manifestAdvisory(code), { manifestPolicyGateMode: "advisory", confirmedContributor: true }).conclusion).toBe("success"); + }); + + it("never blocks a non-confirmed contributor even with manifestPolicy: block", () => { + const result = evaluateGateCheck(manifestAdvisory(code), { manifestPolicyGateMode: "block", confirmedContributor: false }); + expect(result.conclusion).toBe("neutral"); + expect(result.blockers).toEqual([]); + }); + }); + } + + it("is an INDEPENDENT dimension: mergeReadiness: block does NOT promote a manifest-policy finding (kept out of the composite)", () => { + const eff = resolveEffectiveSettings(settings({ manifestPolicyGateMode: "off", mergeReadinessGateMode: "block" }), parseFocusManifest(null)); + expect(evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), gateCheckPolicy(eff, null, true)).conclusion).toBe("success"); + }); + + it("gateCheckPolicy threads manifestPolicyGateMode into the policy", () => { + expect(gateCheckPolicy(settings({ manifestPolicyGateMode: "block" }), null, true).manifestPolicyGateMode).toBe("block"); + }); + + it("end-to-end: a manifest gate.manifestPolicy: block sets effective.manifestPolicyGateMode and blocks a blockedPath PR", () => { + const eff = resolveEffectiveSettings(settings({ manifestPolicyGateMode: "off" }), parseFocusManifest({ gate: { manifestPolicy: "block" } })); + expect(eff.manifestPolicyGateMode).toBe("block"); + const result = evaluateGateCheck(manifestAdvisory("manifest_blocked_path"), gateCheckPolicy(eff, null, true)); + expect(result.conclusion).toBe("failure"); + expect(result.blockers.map((finding) => finding.code)).toContain("manifest_blocked_path"); + }); +}); diff --git a/test/unit/maintainer-activation.test.ts b/test/unit/maintainer-activation.test.ts index ffcead4a83..6ccbab7e03 100644 --- a/test/unit/maintainer-activation.test.ts +++ b/test/unit/maintainer-activation.test.ts @@ -34,6 +34,7 @@ function settings(overrides: Partial = {}): RepositorySettin qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index 88a3912600..e7e8298d35 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -68,6 +68,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, diff --git a/test/unit/self-dogfood-registration-pack.test.ts b/test/unit/self-dogfood-registration-pack.test.ts index b8f13151a9..44bab4b884 100644 --- a/test/unit/self-dogfood-registration-pack.test.ts +++ b/test/unit/self-dogfood-registration-pack.test.ts @@ -61,6 +61,7 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 7bbd6e3cf4..1dff213cac 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1531,6 +1531,7 @@ function repoSettings(repoFullName: string): RepositorySettings { qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 7637faceb9..a19ab07ad0 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -1625,6 +1625,7 @@ describe("v2 signal builders", () => { qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index d482ce4126..4ea84c2728 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -391,6 +391,7 @@ describe("world-class backend signals", () => { qualityGateMode: "advisory" as const, slopGateMode: "off" as const, mergeReadinessGateMode: "off" as const, + manifestPolicyGateMode: "off" as const, firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, @@ -441,6 +442,7 @@ describe("world-class backend signals", () => { qualityGateMode: "advisory" as const, slopGateMode: "off" as const, mergeReadinessGateMode: "off" as const, + manifestPolicyGateMode: "off" as const, firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, @@ -511,6 +513,7 @@ describe("world-class backend signals", () => { qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, @@ -602,6 +605,7 @@ describe("world-class backend signals", () => { qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, @@ -668,6 +672,7 @@ describe("world-class backend signals", () => { qualityGateMode: "advisory", slopGateMode: "off", mergeReadinessGateMode: "off", + manifestPolicyGateMode: "off", firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null,