refactor(FN-5633): AI merge reviewer uses the project reviewer/validator model

Drop the bespoke `merger.reviewerModel` setting. The AI merge reviewer now
resolves its model via `resolveValidatorSettingsModel` — the same
reviewer/validator lane used elsewhere (project validator → global validator →
project default) — falling back to the merger model only if that lane is empty.
Removes the setting from the type, schema default, and the Settings UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-28 19:53:11 -07:00
parent 730cee8d61
commit 0e5ca34c61
4 changed files with 10 additions and 30 deletions

View File

@@ -262,7 +262,7 @@ export const DEFAULT_PROJECT_SETTINGS = {
],
prerebaseDivergenceThreshold: 50,
mergeConflictStrategy: "smart-prefer-main",
merger: { mode: "ai", reviewerModel: undefined, maxReviewPasses: 3 },
merger: { mode: "ai", maxReviewPasses: 3 },
mergeDiffVolumeMinLines: undefined,
mergeDiffVolumeThreshold: undefined,
mergeDiffVolumeAllowlist: undefined,

View File

@@ -336,10 +336,9 @@ export function normalizeMergerMode(value: unknown): MergerMode {
export interface MergerSettings {
/** Which merge path to use. Default: "ai". */
mode?: MergerMode;
/** Optional `provider/modelId` override for the read-only reviewer agent. */
reviewerModel?: string;
/** How many AI corrective rounds before landing the best result (advisory) or
* hard-failing (blocking). Default: 3. */
* hard-failing (blocking). Default: 3. The reviewer uses the project's
* validator/reviewer model lane — there is no merge-specific model setting. */
maxReviewPasses?: number;
}

View File

@@ -4626,20 +4626,7 @@ export function SettingsModal({
setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), maxReviewPasses: e.target.value === "" ? undefined : Number(e.target.value) } }))
}
/>
<small>AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3.</small>
</div>
<div className="form-group">
<label htmlFor="mergerReviewerModel">Reviewer model (optional)</label>
<input
id="mergerReviewerModel"
type="text"
placeholder="provider/modelId — defaults to the merger model"
value={form.merger?.reviewerModel ?? ""}
onChange={(e) =>
setForm((f) => ({ ...f, merger: { ...(f.merger ?? {}), reviewerModel: e.target.value || undefined } }))
}
/>
<small>Model for the read-only reviewer agent (a cheaper/faster model is fine). Leave blank to reuse the merger model.</small>
<small>AI corrective rounds before landing the best result (advisory concern) or hard-failing (unfixable correctness concern). Default 3. The reviewer uses your project&apos;s reviewer/validator model.</small>
</div>
</>
)}

View File

@@ -36,6 +36,7 @@ import { join } from "node:path";
import {
getTaskMergeBlocker,
resolveTaskMergeTarget,
resolveValidatorSettingsModel,
type MergeResult,
type Settings,
type Task,
@@ -346,10 +347,11 @@ function makeMutatingAgent(store: TaskStore, settings: Settings, taskId: string,
function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, options: MergerOptions, audit: RunAuditor) {
return async (cwd: string, prompt: string): Promise<string> => {
const override = settings.merger?.reviewerModel?.trim();
const model = override
? parseModelOverride(override, settings)
: resolveMergerSessionModel(settings);
// The reviewer uses the project's validator/reviewer model lane (the same
// one used elsewhere for review), falling back to the merger model only if
// that lane resolves to nothing.
const validator = resolveValidatorSettingsModel(settings);
const model = validator.provider && validator.modelId ? validator : resolveMergerSessionModel(settings);
let captured = "";
const { session } = await createResolvedAgentSession({
sessionPurpose: "merger",
@@ -384,14 +386,6 @@ function makeReviewAgent(store: TaskStore, settings: Settings, taskId: string, o
};
}
function parseModelOverride(value: string, settings: Settings): { provider: string | undefined; modelId: string | undefined } {
const slash = value.indexOf("/");
if (slash > 0 && slash < value.length - 1) {
return { provider: value.slice(0, slash), modelId: value.slice(slash + 1) };
}
return { provider: resolveMergerSessionModel(settings).provider, modelId: value };
}
// ---------------------------------------------------------------------------
// Local checkout sync
// ---------------------------------------------------------------------------