Files
fusion/packages/core/src/settings-schema.ts
Fusion 8fa3e8bfd1 feat(FN-4072): add diff volume gate to merger
Added a diff volume gate to the merger that blocks or warns on large diffs before merge completes. The feature includes a new `merger-diff-volume-gate` module wired into the merger, configurable via project settings, with tests covering all gate paths. Also updated docs and the changeset for the `@r

Fusion-Task-Id: FN-4072
2026-05-12 18:26:58 -07:00

363 lines
11 KiB
TypeScript

import type { GlobalSettings, ProjectSettings, Settings } from "./types.js";
type CompleteSettings<T> = { [K in keyof Required<T>]: Required<T>[K] | undefined };
/**
* Settings schema source of truth.
*
* The default objects intentionally include optional keys with `undefined`
* values so `Object.keys()` can derive complete scope key lists. This keeps
* persistence filters, UI save splitting, and parity tests aligned.
*/
/** Default values for global (user-level) settings. */
export const DEFAULT_GLOBAL_SETTINGS = {
themeMode: "dark",
colorTheme: "default",
dashboardFontScalePct: 100,
defaultProvider: undefined,
defaultModelId: undefined,
fallbackProvider: undefined,
fallbackModelId: undefined,
defaultThinkingLevel: undefined,
ntfyEnabled: false,
ntfyTopic: undefined,
ntfyBaseUrl: undefined,
ntfyAccessToken: undefined,
ntfyEvents: [
"in-review",
"merged",
"failed",
"awaiting-approval",
"awaiting-user-review",
"planning-awaiting-input",
"message:agent-to-user",
"message:agent-to-agent",
"message:room",
"gridlock",
"fallback-used",
"memory-dreams-processed",
],
ntfyDashboardHost: undefined,
webhookEnabled: false,
webhookUrl: undefined,
webhookFormat: "generic",
webhookEvents: [],
notificationProviders: [],
customProviders: [],
defaultProjectId: undefined,
setupComplete: undefined,
favoriteProviders: undefined,
favoriteModels: undefined,
openrouterModelSync: true,
opencodeGoModelSync: true,
updateCheckEnabled: true,
fnBinaryCheckEnabled: true,
updateCheckFrequency: "daily",
autoReloadOnVersionChange: true,
showGitHubStarButton: true,
githubTrackingDefaultRepo: undefined,
modelOnboardingComplete: undefined,
useClaudeCli: undefined,
useDroidCli: undefined,
useLlamaCpp: undefined,
// Global baseline lanes for per-role model selection
executionGlobalProvider: undefined,
executionGlobalModelId: undefined,
planningGlobalProvider: undefined,
planningGlobalModelId: undefined,
validatorGlobalProvider: undefined,
validatorGlobalModelId: undefined,
titleSummarizerGlobalProvider: undefined,
titleSummarizerGlobalModelId: undefined,
// Daemon mode settings
daemonToken: undefined,
daemonPort: 4040,
daemonHost: "127.0.0.1",
// Node settings sync
settingsSyncEnabled: false,
settingsSyncAuth: false,
settingsSyncInterval: 900000,
settingsSyncConflictResolution: "last-write-wins",
// Dashboard session state (persisted to global settings for PWA/offline restore)
dashboardCurrentNodeId: undefined,
dashboardCurrentProjectIdByNode: undefined,
// Dashboard TUI memory guard
vitestAutoKillEnabled: true,
vitestKillThresholdPct: 90,
// Agent log persistence controls
persistAgentToolOutput: true,
persistAgentThinkingLog: false,
researchGlobalDefaults: {
searchProvider: undefined,
synthesisProvider: undefined,
synthesisModelId: undefined,
enabledSources: {
webSearch: true,
pageFetch: true,
github: false,
localDocs: true,
llmSynthesis: true,
},
maxSourcesPerRun: 20,
defaultExportFormat: "markdown",
},
researchGlobalEnabled: true,
researchGlobalMaxConcurrentRuns: 3,
researchGlobalDefaultTimeout: 300000,
researchGlobalMaxSourcesPerRun: 20,
researchGlobalMaxSynthesisRounds: 2,
researchGlobalWebSearchProvider: "builtin",
researchGlobalSearxngUrl: undefined,
researchGlobalBraveApiKey: undefined,
researchGlobalGoogleSearchApiKey: undefined,
researchGlobalGoogleSearchCx: undefined,
researchGlobalTavilyApiKey: undefined,
researchGlobalGitHubEnabled: false,
researchGlobalLocalDocsEnabled: true,
researchGlobalMaxSearchResults: 10,
researchGlobalFetchTimeoutMs: 30_000,
researchGlobalUserAgent: "FusionResearchBot/1.0",
remoteAccess: {
activeProvider: null,
providers: {
tailscale: {
enabled: false,
hostname: "",
targetPort: 0,
acceptRoutes: false,
},
cloudflare: {
enabled: false,
quickTunnel: true,
tunnelName: "",
tunnelToken: null,
ingressUrl: "",
},
},
tokenStrategy: {
persistent: {
enabled: true,
token: null,
},
shortLived: {
enabled: false,
ttlMs: 900000,
maxTtlMs: 86400000,
},
},
lifecycle: {
rememberLastRunning: false,
wasRunningOnShutdown: false,
lastRunningProvider: null,
},
},
experimentalFeatures: {},
} satisfies CompleteSettings<GlobalSettings>;
/** Default values for project-level settings. */
export const DEFAULT_PROJECT_SETTINGS = {
globalPause: false,
globalPauseReason: undefined,
enginePaused: false,
maxConcurrent: 2,
maxTriageConcurrent: 2,
globalMaxConcurrent: 4,
maxWorktrees: 4,
pollIntervalMs: 15000,
heartbeatMultiplier: 1,
groupOverlappingFiles: true,
overlapIgnorePaths: [],
autoMerge: true,
mergeStrategy: "direct",
directMergeCommitStrategy: "auto",
requirePrApproval: false,
pushAfterMerge: false,
pushRemote: "origin",
unavailableNodePolicy: "block",
defaultNodeId: undefined,
worktreeInitCommand: undefined,
testCommand: undefined,
buildCommand: undefined,
recycleWorktrees: false,
executorAllowSiblingBranchRename: false,
worktreeNaming: "random",
taskPrefix: "FN",
includeTaskIdInCommit: true,
commitAuthorEnabled: true,
commitAuthorName: "Fusion",
commitAuthorEmail: "noreply@runfusion.ai",
planningProvider: undefined,
planningModelId: undefined,
planningFallbackProvider: undefined,
planningFallbackModelId: undefined,
// Project-level default override and execution lane
defaultProviderOverride: undefined,
defaultModelIdOverride: undefined,
executionProvider: undefined,
executionModelId: undefined,
validatorProvider: undefined,
validatorModelId: undefined,
validatorFallbackProvider: undefined,
validatorFallbackModelId: undefined,
modelPresets: [],
autoSelectModelPreset: false,
completionDocumentationMode: "off",
defaultPresetBySize: {},
autoResolveConflicts: true,
smartConflictResolution: true,
mergerAutostashMaxAgeHours: 24,
worktreeRebaseBeforeMerge: true,
worktreeRebaseRemote: "",
worktreeRebaseLocalBase: true,
mergeConflictStrategy: "smart-prefer-main",
mergeDiffVolumeMinLines: undefined,
mergeDiffVolumeThreshold: undefined,
mergeDiffVolumeAllowlist: undefined,
mergeStrategyOverlapBehavior: "flip-to-prefer-branch",
workflowStepTimeoutMs: 360_000,
workflowRevisionForkOnScopeMismatch: true,
strictScopeEnforcement: false,
buildRetryCount: 0,
verificationFixRetries: 3,
buildTimeoutMs: 300_000,
requirePlanApproval: false,
agentProvisioning: {},
specStalenessEnabled: false,
specStalenessMaxAgeMs: 6 * 60 * 60 * 1000,
taskStuckTimeoutMs: 600_000,
staleHighFanoutBlockerAgeThresholdMs: 2 * 60 * 60 * 1000,
aiSessionTtlMs: 7 * 24 * 60 * 60 * 1000,
aiSessionCleanupIntervalMs: 60 * 60 * 1000,
autoUnpauseEnabled: true,
autoUnpauseBaseDelayMs: 300_000,
autoUnpauseMaxDelayMs: 3_600_000,
maxStuckKills: 6,
preserveProgressOnStuckRequeue: true,
maxPostReviewFixes: 1,
maxSpawnedAgentsPerParent: 5,
maxSpawnedAgentsGlobal: 20,
// Run maintenance (including WAL checkpointing) every 5 minutes by default.
maintenanceIntervalMs: 300_000,
autoArchiveDoneTasksEnabled: true,
autoArchiveDoneAfterMs: 48 * 60 * 60 * 1000,
archiveAgentLogMode: "compact",
autoUpdatePrStatus: false,
githubCommentOnDone: false,
githubCommentTemplate: undefined,
githubTrackingEnabledByDefault: false,
githubTrackingDefaultRepo: undefined,
githubAuthMode: "gh-cli",
githubAuthToken: undefined,
autoBackupEnabled: false,
autoBackupSchedule: "0 2 * * *",
autoBackupRetention: 7,
autoBackupDir: ".fusion/backups",
memoryBackupEnabled: false,
memoryBackupSchedule: "0 3 * * *",
memoryBackupRetention: 14,
memoryBackupDir: ".fusion/backups/memory",
memoryBackupScope: "all" as const,
autoSummarizeTitles: false,
useAiMergeCommitSummary: true,
titleSummarizerProvider: undefined,
titleSummarizerModelId: undefined,
titleSummarizerFallbackProvider: undefined,
titleSummarizerFallbackModelId: undefined,
scripts: undefined,
setupScript: undefined,
insightExtractionEnabled: false,
insightExtractionSchedule: "0 2 * * *",
insightExtractionMinIntervalMs: 86_400_000,
taskEvaluationEnabled: false,
taskEvaluationSchedule: "0 5 * * *",
taskEvaluationProvider: undefined,
taskEvaluationModelId: undefined,
taskEvaluationFollowUpPolicy: "off",
taskEvaluationRetention: undefined,
memoryEnabled: true,
memoryBackendType: "qmd",
memoryAutoSummarizeEnabled: false,
memoryAutoSummarizeThresholdChars: 50_000,
memoryAutoSummarizeSchedule: "0 3 * * *",
memoryDreamsEnabled: false,
memoryDreamsSchedule: "0 4 * * *",
tokenCap: undefined,
runStepsInNewSessions: false,
maxParallelSteps: 2,
missionStaleThresholdMs: 600_000,
missionMaxTaskRetries: 3,
missionHealthCheckIntervalMs: 300_000,
agentPrompts: undefined,
promptOverrides: undefined,
reflectionEnabled: false,
reflectionIntervalMs: 3_600_000,
reflectionAfterTask: true,
reviewHandoffPolicy: "disabled",
showQuickChatFAB: false,
researchSettings: {
enabled: true,
searchProvider: undefined,
synthesisProvider: undefined,
synthesisModelId: undefined,
enabledSources: {
webSearch: true,
pageFetch: true,
github: false,
localDocs: true,
llmSynthesis: true,
},
limits: {
maxConcurrentRuns: 3,
maxSourcesPerRun: 20,
maxDurationMs: 300000,
requestTimeoutMs: 30000,
},
},
evalSettings: {
enabled: false,
intervalMs: 86_400_000,
evaluatorProvider: undefined,
evaluatorModelId: undefined,
followUpPolicy: "suggest-only",
retentionDays: 30,
},
researchEnabled: true,
researchMaxConcurrentRuns: 3,
researchDefaultTimeout: 300000,
researchMaxSourcesPerRun: 20,
researchMaxSynthesisRounds: 2,
} satisfies CompleteSettings<ProjectSettings>;
/**
* Merged default settings (backward compatible).
* This combines global and project defaults into a single object
* that matches the legacy `DEFAULT_SETTINGS` shape.
*/
export const DEFAULT_SETTINGS: Settings = {
...DEFAULT_GLOBAL_SETTINGS,
...DEFAULT_PROJECT_SETTINGS,
};
/** Keys that belong to the global settings scope. */
export const GLOBAL_SETTINGS_KEYS = Object.freeze(
Object.keys(DEFAULT_GLOBAL_SETTINGS) as Array<keyof GlobalSettings>,
);
/** Keys that belong to the project settings scope. */
export const PROJECT_SETTINGS_KEYS = Object.freeze(
Object.keys(DEFAULT_PROJECT_SETTINGS) as Array<keyof ProjectSettings>,
);
export function isGlobalSettingsKey(key: string): key is keyof GlobalSettings {
return (GLOBAL_SETTINGS_KEYS as readonly string[]).includes(key);
}
export function isProjectSettingsKey(key: string): key is keyof ProjectSettings {
return (PROJECT_SETTINGS_KEYS as readonly string[]).includes(key);
}
export function isGlobalOnlySettingsKey(key: string): key is keyof GlobalSettings {
return isGlobalSettingsKey(key) && !isProjectSettingsKey(key);
}