diff --git a/.loopover.yml.example b/.loopover.yml.example index f75d8e380e..d47037d32e 100644 --- a/.loopover.yml.example +++ b/.loopover.yml.example @@ -325,14 +325,6 @@ gate: # conclusion; it does NOT stop the agent action layer from real writes. dryRun: false - # First-time-contributor grace. RESERVED / currently INERT: this value is - # parsed and stored, but the gate does not read it — a first-time - # contributor with a real blocker is one-shot closed the same as a - # repeat contributor (blocker findings must remain closure outcomes). - # Setting this to true has no effect today; kept for potential future - # use. Bool. Default: false. - firstTimeContributorGrace: false - # Live premerge migrations/** collision recheck (#2550, anti-abuse-adjacent safety net). When true, a PR # touching migrations/** gets a fresh GitHub read of the base branch's CURRENT migration filenames # immediately before an agent-driven merge (not just at CI time against this PR's own branch snapshot) — diff --git a/apps/loopover-ui/content/docs/self-hosting-configuration.mdx b/apps/loopover-ui/content/docs/self-hosting-configuration.mdx index 17e93dd61e..7e3f0fa408 100644 --- a/apps/loopover-ui/content/docs/self-hosting-configuration.mdx +++ b/apps/loopover-ui/content/docs/self-hosting-configuration.mdx @@ -350,7 +350,6 @@ For a repo that has never been configured, the default is `disabled`; an already - `gate.premergeContentRecheck` — when `true`, a PR touching `migrations/**` gets a fresh GitHub read of the base branch's current migration filenames immediately before an agent-driven merge, catching a different PR that merged a same-numbered migration in the meantime. A live collision holds the PR instead of merging blind. Default `false` — costs one extra GitHub API call per migrations-touching PR. - `gate.requireFreshRebaseWindow` — when the base branch has advanced within this many minutes of the actual merge decision, forces an `update_branch` + fresh CI recheck before merging, instead of trusting a possibly-stale `mergeable_state: clean` read. A bounded retry cap prevents a fast-moving base from live-locking the PR. Default `null` (never force). - `gate.dryRun` — when `true`, the posted check conclusion remains the real non-enforcing verdict while comments/check text may also show the would-be stricter verdict for AI-review blocker mode. It does not disable downstream merge/close planning for failures from already-enforced gates. Default `false`. -- `gate.firstTimeContributorGrace` — reserved and currently inert: parsed and stored, but the gate does not read it. A first-time contributor with a real blocker is one-shot closed the same as a repeat contributor. Kept for potential future use. ### settings.closeOwnerAuthors and blockedPaths diff --git a/apps/loopover-ui/public/openapi.json b/apps/loopover-ui/public/openapi.json index a1e6f52e35..27442de87e 100644 --- a/apps/loopover-ui/public/openapi.json +++ b/apps/loopover-ui/public/openapi.json @@ -8928,10 +8928,6 @@ "block" ] }, - "firstTimeContributorGrace": { - "type": "boolean", - "description": "Reserved (#2266) -- the gate evaluator never reads this field. Currently has no effect on gate decisions." - }, "slopGateMinScore": { "type": "number", "nullable": true @@ -9717,7 +9713,6 @@ "manifestPolicyGateMode", "selfAuthoredLinkedIssueGateMode", "linkedIssueSatisfactionGateMode", - "firstTimeContributorGrace", "slopAiAdvisory", "aiReviewMode", "aiReviewByok", @@ -10316,10 +10311,6 @@ "block" ] }, - "firstTimeContributorGrace": { - "type": "boolean", - "description": "Reserved (#2266) -- the gate evaluator never reads this field. Currently has no effect on gate decisions." - }, "slopGateMinScore": { "type": "number", "nullable": true @@ -10444,7 +10435,6 @@ "manifestPolicyGateMode", "selfAuthoredLinkedIssueGateMode", "linkedIssueSatisfactionGateMode", - "firstTimeContributorGrace", "autoLabelEnabled", "typeLabelsEnabled", "gittensorLabel", diff --git a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx index 34e87a6ef4..31d635deac 100644 --- a/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/gate-ramp-control.test.tsx @@ -29,7 +29,6 @@ const ADVISORY_SETTINGS = { qualityGateMinScore: null, mergeReadinessGateMode: "off" as const, manifestPolicyGateMode: "off" as const, - firstTimeContributorGrace: false, slopGateMode: "off" as const, slopGateMinScore: null, slopAiAdvisory: false, diff --git a/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx b/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx index 6433bd1c54..606bf0ac15 100644 --- a/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx +++ b/apps/loopover-ui/src/components/site/app-panels/maintainer-settings.tsx @@ -56,8 +56,8 @@ type ToggleFieldDef = { kind: "toggle"; hint?: string; // Renders greyed-out and non-interactive with the hint as the disclosure -- for a field that is real - // and DB-backed but currently wired to nothing (e.g. firstTimeContributorGrace, #2266/#2411), so a - // maintainer can't be misled into thinking the toggle has an effect. + // and DB-backed but currently wired to nothing, so a maintainer can't be misled into thinking the + // toggle has an effect. disabled?: boolean; }; type NumberFieldDef = { @@ -116,13 +116,6 @@ const GATE_FIELDS: FieldDef[] = [ kind: "select", options: GATE_MODE_OPTIONS, }, - { - key: "firstTimeContributorGrace", - label: "First-time-contributor grace", - kind: "toggle", - hint: "Reserved — currently has no effect on gate decisions (#2266)", - disabled: true, - }, ]; const SLOP_FIELDS: FieldDef[] = [ diff --git a/apps/loopover-ui/src/lib/maintainer-settings-editable.test.ts b/apps/loopover-ui/src/lib/maintainer-settings-editable.test.ts index 9d0a6ab111..a91f5966af 100644 --- a/apps/loopover-ui/src/lib/maintainer-settings-editable.test.ts +++ b/apps/loopover-ui/src/lib/maintainer-settings-editable.test.ts @@ -15,7 +15,6 @@ const SETTINGS: MaintainerSettingsEditable = { qualityGateMinScore: null, mergeReadinessGateMode: "off", manifestPolicyGateMode: "off", - firstTimeContributorGrace: false, slopGateMode: "off", slopGateMinScore: null, slopAiAdvisory: false, diff --git a/apps/loopover-ui/src/lib/maintainer-settings-editable.ts b/apps/loopover-ui/src/lib/maintainer-settings-editable.ts index b1f6f022c7..8f6aedcd39 100644 --- a/apps/loopover-ui/src/lib/maintainer-settings-editable.ts +++ b/apps/loopover-ui/src/lib/maintainer-settings-editable.ts @@ -27,7 +27,6 @@ export type MaintainerSettingsEditable = { qualityGateMinScore: number | null; mergeReadinessGateMode: GateMode; manifestPolicyGateMode: GateMode; - firstTimeContributorGrace: boolean; slopGateMode: GateMode; slopGateMinScore: number | null; slopAiAdvisory: boolean; @@ -53,7 +52,6 @@ export const MAINTAINER_SETTINGS_EDITABLE_KEYS: Array reviewCheckMode alias in applyGateConfigOverrides) -- it does NOT gate spend, merge, comment, // label, or close behavior, and it cannot express checkMode's "visible" state. checkMode always wins when @@ -1692,7 +1682,6 @@ function parseGateConfig(value: JsonValue | undefined, warnings: string[]): Focu gate.selfAuthoredLinkedIssue !== null || gate.linkedIssueSatisfaction !== null || gate.dryRun !== null || - gate.firstTimeContributorGrace !== null || gate.premergeContentRecheck !== null || gate.requireFreshRebaseWindowMinutes !== null || gate.claMode !== null || @@ -1774,7 +1763,6 @@ export function gateConfigToJson(gate: FocusManifestGateConfig): JsonValue { if (gate.selfAuthoredLinkedIssue !== null) out.selfAuthoredLinkedIssue = gate.selfAuthoredLinkedIssue; if (gate.linkedIssueSatisfaction !== null) out.linkedIssueSatisfaction = gate.linkedIssueSatisfaction; if (gate.dryRun !== null) out.dryRun = gate.dryRun; - if (gate.firstTimeContributorGrace !== null) out.firstTimeContributorGrace = gate.firstTimeContributorGrace; if (gate.premergeContentRecheck !== null) out.premergeContentRecheck = gate.premergeContentRecheck; if (gate.requireFreshRebaseWindowMinutes !== null) out.requireFreshRebaseWindow = gate.requireFreshRebaseWindowMinutes; if (gate.claMode !== null) out.claMode = gate.claMode; diff --git a/packages/loopover-engine/src/predicted-gate.ts b/packages/loopover-engine/src/predicted-gate.ts index a42e584f1a..bed85a7ee5 100644 --- a/packages/loopover-engine/src/predicted-gate.ts +++ b/packages/loopover-engine/src/predicted-gate.ts @@ -299,7 +299,6 @@ export function buildPredictedGateVerdict(args: { claGateMode: gate.claMode ?? undefined, readinessScore: readiness.total, confirmedContributor: effectiveConfirmedContributor, - firstTimeContributorGrace: gate.firstTimeContributorGrace ?? undefined, authorMergedPrCount: authorHistory.filter((pr) => pr.state === "merged" || pr.mergedAt).length, authorClosedUnmergedPrCount: authorHistory.filter((pr) => pr.state === "closed" && !pr.mergedAt).length, // Size-hold + guardrail-hold parity (#2458): only meaningful when changed paths were supplied — changedPaths diff --git a/packages/loopover-engine/src/types/manifest-deps-types.ts b/packages/loopover-engine/src/types/manifest-deps-types.ts index 27cc08aefa..bd3c09b28e 100644 --- a/packages/loopover-engine/src/types/manifest-deps-types.ts +++ b/packages/loopover-engine/src/types/manifest-deps-types.ts @@ -264,11 +264,6 @@ export type RepositorySettings = { * opened the linked issue (`pr.authorLogin === issue.authorLogin`). Defaults to `advisory` — the finding * is surfaced in the review panel but never blocks unless the maintainer opts in. */ selfAuthoredLinkedIssueGateMode: GateRuleMode; - /** First-time-contributor grace (#552). RESERVED / currently INERT (#2266): parsed, clamped, and threaded - * end-to-end, but the gate evaluator never reads it — a genuine newcomer with a real blocker is still - * one-shot closed exactly like a repeat contributor (blocker findings must remain closure outcomes). - * Setting this true has no runtime effect today; kept for potential future use. Default false. */ - firstTimeContributorGrace: boolean; /** Slop-risk threshold (0-100) at/above which `slopGateMode: block` blocks. Default 60 (the `high` band). */ slopGateMinScore?: number | null | undefined; /** AI-assisted slop advisory (the `slopAiAdvisory` capability). When true AND `slopGateMode != off`, a diff --git a/packages/loopover-engine/src/types/predicted-gate-types.ts b/packages/loopover-engine/src/types/predicted-gate-types.ts index 170fec2e97..a01d065e21 100644 --- a/packages/loopover-engine/src/types/predicted-gate-types.ts +++ b/packages/loopover-engine/src/types/predicted-gate-types.ts @@ -309,7 +309,6 @@ export type FocusManifestGateConfig = { manifestPolicy: GateRuleMode | null; selfAuthoredLinkedIssue: GateRuleMode | null; dryRun: boolean | null; - firstTimeContributorGrace: boolean | null; premergeContentRecheck: boolean | null; requireFreshRebaseWindowMinutes: number | null; claMode: GateRuleMode | null; diff --git a/src/api/routes.ts b/src/api/routes.ts index 9c8991e9f3..0bc8897318 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -712,9 +712,9 @@ const maintainerSettingsSchema = z linkedIssueSatisfactionGateMode: z.enum(["off", "advisory", "block"]), // #6443: mergeTrainMode/gittensorLabel/blacklistLabel/createMissingLabel removed -- no longer DB-backed, // config-as-code only via .loopover.yml's settings: block now. - firstTimeContributorGrace: z - .boolean() - .describe("Reserved (#2266) -- the gate evaluator never reads this field. Currently has no effect on gate decisions."), + // #6446: firstTimeContributorGrace removed -- a dead, never-wired RESERVED/INERT field (#2266); deleted + // rather than wired in, since the gate's one-shot design deliberately never softens a blocker for a + // newcomer. slopGateMode: z.enum(["off", "advisory", "block"]), slopGateMinScore: z.number().int().min(0).max(100).nullable(), slopAiAdvisory: z.boolean(), diff --git a/src/db/repositories.ts b/src/db/repositories.ts index 43f040a348..384ad49ed0 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -591,7 +591,6 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopGateMinScore: null, slopAiAdvisory: false, aiReviewMode: "off", @@ -676,7 +675,6 @@ export async function getRepositorySettings(env: Env, fullName: string): Promise manifestPolicyGateMode: parseGateRuleMode(row.manifestPolicyGateMode), selfAuthoredLinkedIssueGateMode: parseGateRuleMode(row.selfAuthoredLinkedIssueGateMode), linkedIssueSatisfactionGateMode: parseGateRuleMode(row.linkedIssueSatisfactionGateMode), - firstTimeContributorGrace: row.firstTimeContributorGrace, slopGateMinScore: normalizeQualityGateMinScore(row.slopGateMinScore), slopAiAdvisory: row.slopAiAdvisory, aiReviewMode: parseGateRuleMode(row.aiReviewMode), @@ -809,7 +807,6 @@ export async function upsertRepositorySettings(env: Env, settings: Partial { // malformed-body guard. const settingsAdminUpdate = await app.request( "/v1/repos/entrius/allways-ui/settings", - { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ manifestPolicyGateMode: "advisory", firstTimeContributorGrace: true }) }, + { method: "PUT", headers: apiHeaders(env), body: JSON.stringify({ manifestPolicyGateMode: "advisory" }) }, env, ); expect(settingsAdminUpdate.status).toBe(200); - await expect(settingsAdminUpdate.json()).resolves.toMatchObject({ manifestPolicyGateMode: "advisory", firstTimeContributorGrace: true }); + await expect(settingsAdminUpdate.json()).resolves.toMatchObject({ manifestPolicyGateMode: "advisory" }); const settingsMalformed = await app.request("/v1/repos/entrius/allways-ui/settings", { method: "PUT", headers: apiHeaders(env), body: "{" }, env); expect(settingsMalformed.status).toBe(400); diff --git a/test/unit/focus-manifest.test.ts b/test/unit/focus-manifest.test.ts index c75a2a0d1b..20499ae42d 100644 --- a/test/unit/focus-manifest.test.ts +++ b/test/unit/focus-manifest.test.ts @@ -285,7 +285,6 @@ describe(".loopover.yml.example field-exhaustiveness (#1670)", () => { selfAuthoredLinkedIssue: "selfAuthoredLinkedIssue:", linkedIssueSatisfaction: "linkedIssueSatisfaction:", dryRun: "dryRun:", - firstTimeContributorGrace: "firstTimeContributorGrace:", premergeContentRecheck: "premergeContentRecheck:", requireFreshRebaseWindowMinutes: "requireFreshRebaseWindow:", claMode: "claMode:", @@ -933,7 +932,7 @@ describe("compileFocusManifestPolicy", () => { issueDiscoveryPolicy: "neutral", maintainerNotes: [], 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, manifestPolicy: null, dryRun: null, firstTimeContributorGrace: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: null, advisoryCheckRuns: null, aiJudgmentBlockersMode: null, copycatMode: null, copycatMinScore: null }, + 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, manifestPolicy: null, dryRun: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: 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 }, features: { present: false, rag: null, reputation: null, safety: null, grounding: null, e2eTests: null, screenshots: null, improvementSignal: null, amsReputationBridge: null }, @@ -1097,41 +1096,26 @@ describe("parseFocusManifest gate config", () => { // the block→advisory deprecation-downgrade behavior itself is covered separately below. const m = parseFocusManifest({ gate: { linkedIssue: "block", duplicates: "advisory", readiness: { mode: "advisory", minScore: 70 } } }); expect(m.present).toBe(true); - expect(m.gate).toEqual({ present: true, enabled: null, checkMode: null, pack: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "advisory", readinessMinScore: 70, 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, manifestPolicy: null, dryRun: null, firstTimeContributorGrace: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: null, advisoryCheckRuns: null, aiJudgmentBlockersMode: null, copycatMode: null, copycatMinScore: null }); + expect(m.gate).toEqual({ present: true, enabled: null, checkMode: null, pack: null, linkedIssue: "block", duplicates: "advisory", readinessMode: "advisory", readinessMinScore: 70, 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, manifestPolicy: null, dryRun: null, premergeContentRecheck: null, requireFreshRebaseWindowMinutes: null, claMode: null, claConsentPhrase: null, claCheckRunName: null, claCheckRunAppSlug: null, expectedCiContexts: null, advisoryCheckRuns: null, aiJudgmentBlockersMode: null, copycatMode: null, copycatMinScore: null }); }); - it("parses gate.mergeReadiness + gate.firstTimeContributorGrace, round-trips them, and warns on bad values (#822)", () => { - const m = parseFocusManifest({ gate: { mergeReadiness: "block", firstTimeContributorGrace: true } }); + it("parses gate.mergeReadiness, round-trips it, and warns on a bad value (#822)", () => { + const m = parseFocusManifest({ gate: { mergeReadiness: "block" } }); expect(m.gate.present).toBe(true); expect(m.gate.mergeReadiness).toBe("block"); - expect(m.gate.firstTimeContributorGrace).toBe(true); - expect(gateConfigToJson(m.gate)).toMatchObject({ mergeReadiness: "block", firstTimeContributorGrace: true }); - const bad = parseFocusManifest({ gate: { mergeReadiness: "sometimes", firstTimeContributorGrace: "yes" } }); + expect(gateConfigToJson(m.gate)).toMatchObject({ mergeReadiness: "block" }); + const bad = parseFocusManifest({ gate: { mergeReadiness: "sometimes" } }); expect(bad.gate.mergeReadiness).toBeNull(); - expect(bad.gate.firstTimeContributorGrace).toBeNull(); expect(bad.gate.present).toBe(false); }); - it("warns that gate.firstTimeContributorGrace is reserved/inert when explicitly set true (#2266)", () => { - const m = parseFocusManifest({ gate: { firstTimeContributorGrace: true } }); - expect(m.gate.firstTimeContributorGrace).toBe(true); - expect(m.warnings.some((w) => /gate\.firstTimeContributorGrace.*reserved\/inert/i.test(w))).toBe(true); - }); - - it("does not warn about firstTimeContributorGrace when left unset or explicitly false (matches the inert default)", () => { - const unset = parseFocusManifest({ gate: { linkedIssue: "block" } }); - expect(unset.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false); - const explicitFalse = parseFocusManifest({ gate: { firstTimeContributorGrace: false } }); - expect(explicitFalse.warnings.some((w) => /firstTimeContributorGrace/i.test(w))).toBe(false); - }); - // #5355: gate.enabled only controls whether the check-run publishes -- it does not gate spend, merge, // comment, label, or close behavior. Caused two real incidents under this exact ambiguity. it("warns that gate.enabled is ambiguous when set without an explicit gate.checkMode, regardless of value (#5355)", () => { const enabledTrue = parseFocusManifest({ gate: { enabled: true } }); expect(enabledTrue.warnings.some((w) => /gate\.enabled.*only controls whether the LoopOver Orb Review Agent check-run publishes/.test(w))).toBe(true); - // Unlike firstTimeContributorGrace, both true AND false are equally ambiguous here -- both map through - // the same silent enabled -> reviewCheckMode alias, so an explicit false is just as worth flagging. + // Both true AND false are equally ambiguous here -- both map through the same silent enabled -> + // reviewCheckMode alias, so an explicit false is just as worth flagging. const enabledFalse = parseFocusManifest({ gate: { enabled: false } }); expect(enabledFalse.warnings.some((w) => /gate\.enabled.*only controls whether the LoopOver Orb Review Agent check-run publishes/.test(w))).toBe(true); }); diff --git a/test/unit/gate-check-policy.test.ts b/test/unit/gate-check-policy.test.ts index 7482b55c6e..cfdb602ebd 100644 --- a/test/unit/gate-check-policy.test.ts +++ b/test/unit/gate-check-policy.test.ts @@ -88,13 +88,6 @@ describe(".loopover.yml settings override (resolveEffectiveSettings)", () => { expect(evaluateGateCheck(missingIssueAdvisory(), gateCheckPolicy(eff, null, true)).conclusion).toBe("failure"); }); - it("end-to-end: manifest gate.firstTimeContributorGrace:true no longer softens a newcomer's blocker (#822/#552)", () => { - const eff = resolveEffectiveSettings(settings({ linkedIssueGateMode: "block", firstTimeContributorGrace: false }), parseFocusManifest({ gate: { firstTimeContributorGrace: true } })); - expect(eff.firstTimeContributorGrace).toBe(true); - const newcomerPolicy = gateCheckPolicy(eff, null, true, null, { mergedPrCount: 0, closedUnmergedPrCount: 0 }); - expect(evaluateGateCheck(missingIssueAdvisory(), newcomerPolicy).conclusion).toBe("failure"); - }); - it("blocks a non-confirmed contributor identically to a confirmed one (#gate-nonconfirmed)", () => { const eff = resolveEffectiveSettings(settings({ linkedIssueGateMode: "advisory" }), parseFocusManifest({ gate: { linkedIssue: "block" } })); // Non-confirmed now gates NORMALLY: a configured blocker → failure, the same verdict a confirmed author gets. @@ -567,14 +560,16 @@ describe("merge-readiness composite gate (#551)", () => { }); }); -describe("first-time-contributor grace compatibility (#552)", () => { +describe("author-history blockers stay unsoftened (#552/#2266/#2411)", () => { // A would-be hard blocker for a confirmed contributor (linked-issue: block trips on the missing-issue PR). + // #2411 deliberately removed the old first-time-contributor-grace softening path entirely (#6446): blocker + // findings must remain closure/rejection outcomes regardless of the author's merge/close history, whether + // they're a genuine newcomer or a repeat offender. const blockingPolicy = { linkedIssueGateMode: "block" as const, confirmedContributor: true }; - it("(a) does not soften blockers for a genuine newcomer (0 merged, 0 closed-unmerged)", () => { + it("does not soften blockers for a genuine newcomer (0 merged, 0 closed-unmerged)", () => { const result = evaluateGateCheck(missingIssueAdvisory(), { ...blockingPolicy, - firstTimeContributorGrace: true, authorMergedPrCount: 0, authorClosedUnmergedPrCount: 0, }); @@ -582,10 +577,9 @@ describe("first-time-contributor grace compatibility (#552)", () => { expect(result.blockers.map((finding) => finding.code)).toEqual(["missing_linked_issue"]); }); - it("(b) still blocks a repeat offender (0 merged, >= 3 closed-unmerged) — grace does not apply", () => { + it("still blocks a repeat offender (0 merged, >= 3 closed-unmerged)", () => { const result = evaluateGateCheck(missingIssueAdvisory(), { ...blockingPolicy, - firstTimeContributorGrace: true, authorMergedPrCount: 0, authorClosedUnmergedPrCount: 3, }); @@ -593,21 +587,9 @@ describe("first-time-contributor grace compatibility (#552)", () => { expect(result.blockers.map((finding) => finding.code)).toEqual(["missing_linked_issue"]); }); - it("(c) blocks normally when the grace setting is off, even for a newcomer", () => { - const result = evaluateGateCheck(missingIssueAdvisory(), { - ...blockingPolicy, - firstTimeContributorGrace: false, - authorMergedPrCount: 0, - authorClosedUnmergedPrCount: 0, - }); - expect(result.conclusion).toBe("failure"); - expect(result.blockers.map((finding) => finding.code)).toEqual(["missing_linked_issue"]); - }); - - it("(d) blocks an author with merge history (not a newcomer) even with grace on", () => { + it("blocks an author with merge history (not a newcomer) the same way", () => { const result = evaluateGateCheck(missingIssueAdvisory(), { ...blockingPolicy, - firstTimeContributorGrace: true, authorMergedPrCount: 2, authorClosedUnmergedPrCount: 0, }); @@ -615,9 +597,8 @@ describe("first-time-contributor grace compatibility (#552)", () => { expect(result.blockers.map((finding) => finding.code)).toEqual(["missing_linked_issue"]); }); - it("gateCheckPolicy threads firstTimeContributorGrace + the author's per-repo history into the policy", () => { - const policy = gateCheckPolicy(settings({ firstTimeContributorGrace: true }), null, true, null, { mergedPrCount: 0, closedUnmergedPrCount: 1 }); - expect(policy.firstTimeContributorGrace).toBe(true); + it("gateCheckPolicy threads the author's per-repo history into the policy", () => { + const policy = gateCheckPolicy(settings({}), null, true, null, { mergedPrCount: 0, closedUnmergedPrCount: 1 }); expect(policy.authorMergedPrCount).toBe(0); expect(policy.authorClosedUnmergedPrCount).toBe(1); expect(evaluateGateCheck(missingIssueAdvisory(), { ...policy, linkedIssueGateMode: "block" }).conclusion).toBe("failure"); diff --git a/test/unit/maintainer-activation.test.ts b/test/unit/maintainer-activation.test.ts index baca1ca834..7e55ed6642 100644 --- a/test/unit/maintainer-activation.test.ts +++ b/test/unit/maintainer-activation.test.ts @@ -38,7 +38,6 @@ function settings(overrides: Partial = {}): RepositorySettin manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/policy-sanitizer.test.ts b/test/unit/policy-sanitizer.test.ts index fd7a79ed2c..66e920cc30 100644 --- a/test/unit/policy-sanitizer.test.ts +++ b/test/unit/policy-sanitizer.test.ts @@ -71,7 +71,6 @@ function settingsFor(repoFullName: string, overrides: Partial { expect(noGate.blockers.some((b) => b.code === "missing_linked_issue")).toBe(false); }); - it("does not let public gate.firstTimeContributorGrace soften duplicate blockers", () => { - const newcomer = verdict({ - gate: { duplicates: "block", firstTimeContributorGrace: true }, - pullRequests: [openPr(42, "Retry uploads on 5xx responses", [7], "someone-else")], - }); - expect(newcomer.conclusion).toBe("failure"); - expect(newcomer.blockers.some((b) => b.code === "duplicate_pr_risk")).toBe(true); - - const returning = verdict({ - gate: { duplicates: "block", firstTimeContributorGrace: true }, - pullRequests: [ - openPr(42, "Retry uploads on 5xx responses", [7], "someone-else"), - { ...openPr(9, "Earlier fix", [], "miner1"), state: "merged", mergedAt: "2026-06-01T00:00:00.000Z" }, - ], - }); - expect(returning.conclusion).toBe("failure"); - expect(returning.blockers.some((b) => b.code === "duplicate_pr_risk")).toBe(true); - }); - it("matches author history case-insensitively, like the live gate (#audit-§4)", () => { // The merged PR's author is "MINER1" (different case from the contributor "miner1"). The predictor still - // counts it as history, but blocker disposition no longer depends on first-time grace. + // counts it as history, but blocker disposition never depends on it (#2411). const mixedCase = verdict({ - gate: { duplicates: "block", firstTimeContributorGrace: true }, + gate: { duplicates: "block" }, pullRequests: [ openPr(42, "Retry uploads on 5xx responses", [7], "someone-else"), { ...openPr(9, "Earlier fix", [], "MINER1"), state: "merged", mergedAt: "2026-06-01T00:00:00.000Z" }, @@ -209,7 +190,7 @@ describe("buildPredictedGateVerdict", () => { state: "closed", }); const result = verdict({ - gate: { duplicates: "block", firstTimeContributorGrace: true }, + gate: { duplicates: "block" }, pullRequests: [ openPr(42, "Retry uploads on 5xx responses", [7], "someone-else"), closedUnmerged(11, "Abandoned attempt one"), @@ -225,7 +206,7 @@ describe("buildPredictedGateVerdict", () => { // The prior PR has state "closed" yet carries a mergedAt timestamp, so it is still counted as merge history. // Blocker disposition no longer depends on first-time grace, so the gate blocks either way. const result = verdict({ - gate: { duplicates: "block", firstTimeContributorGrace: true }, + gate: { duplicates: "block" }, pullRequests: [ openPr(42, "Retry uploads on 5xx responses", [7], "someone-else"), { ...openPr(9, "Earlier merged fix", [], "miner1"), state: "closed", mergedAt: "2026-06-01T00:00:00.000Z" }, diff --git a/test/unit/registration-readiness.test.ts b/test/unit/registration-readiness.test.ts index 94d0750580..00d8250732 100644 --- a/test/unit/registration-readiness.test.ts +++ b/test/unit/registration-readiness.test.ts @@ -52,7 +52,6 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/repository-settings-enforcement.test.ts b/test/unit/repository-settings-enforcement.test.ts index 7e77587027..70dd9caf70 100644 --- a/test/unit/repository-settings-enforcement.test.ts +++ b/test/unit/repository-settings-enforcement.test.ts @@ -26,7 +26,6 @@ function settings(over: Partial = {}): RepositorySettings { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/self-dogfood-registration-pack.test.ts b/test/unit/self-dogfood-registration-pack.test.ts index 8c34ff58ec..7e31f9129a 100644 --- a/test/unit/self-dogfood-registration-pack.test.ts +++ b/test/unit/self-dogfood-registration-pack.test.ts @@ -65,7 +65,6 @@ function settingsFor(repoFullName: string, overrides: Partial = {}): RepositorySettin manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 1b43b606e0..352454af85 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -2388,7 +2388,6 @@ function repoSettings(repoFullName: string): RepositorySettings { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 05a129634c..9baba08e42 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -2115,7 +2115,6 @@ describe("v2 signal builders", () => { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index d186de36ed..e108ceb10a 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -542,7 +542,6 @@ describe("world-class backend signals", () => { manifestPolicyGateMode: "off" as const, selfAuthoredLinkedIssueGateMode: "advisory" as const, linkedIssueSatisfactionGateMode: "off" as const, - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, @@ -603,7 +602,6 @@ describe("world-class backend signals", () => { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, @@ -686,7 +684,6 @@ describe("world-class backend signals", () => { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, @@ -802,7 +799,6 @@ describe("world-class backend signals", () => { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true, diff --git a/test/unit/unified-comment-parity.test.ts b/test/unit/unified-comment-parity.test.ts index c99fbc214b..325b111ee3 100644 --- a/test/unit/unified-comment-parity.test.ts +++ b/test/unit/unified-comment-parity.test.ts @@ -63,7 +63,6 @@ const settings: RepositorySettings = { manifestPolicyGateMode: "off", selfAuthoredLinkedIssueGateMode: "advisory", linkedIssueSatisfactionGateMode: "off", - firstTimeContributorGrace: false, slopAiAdvisory: false, qualityGateMinScore: null, autoLabelEnabled: true,