import type { GlobalSettings, ProjectSettings, Settings } from "./types.js"; type CompleteSettings = { [K in keyof Required]: Required[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", "token-budget", ], ntfyDashboardHost: undefined, taskTokenBudget: undefined, failureNotificationDelayMs: 30000, failureNotificationMode: "sticky-only", webhookEnabled: false, webhookUrl: undefined, webhookFormat: "generic", webhookEvents: [], notificationProviders: [], customProviders: [], defaultProjectId: undefined, setupComplete: undefined, favoriteProviders: undefined, favoriteModels: undefined, openrouterModelSync: true, openrouterAppAttribution: undefined, openrouterModelFilters: undefined, openrouterProviderPreferences: undefined, 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, persistAgentThinkingLogPermanent: false, persistAgentThinkingLogEphemeral: false, persistAgentThinkingLog: false, agentMemoryInclusionMode: "full", 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; /** 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, autoClaimCandidatesInPrompt: 5, heartbeatScopeDiscipline: "strict", heartbeatPromptTemplate: "default", 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", worktreesDir: undefined, 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", postMergeAuditMode: "warn", mergeAuditAutoRecovery: "ai-assisted", autoRecovery: { mode: "deterministic-only", maxRetries: 3, }, reliabilityStatsResetAt: undefined, workflowStepTimeoutMs: 360_000, workflowStepScopeEnforcement: "block", planOnlyScopeLeakEnforcement: "warn", workflowRevisionForkOnScopeMismatch: true, strictScopeEnforcement: false, buildRetryCount: 0, verificationFixRetries: 3, buildTimeoutMs: 300_000, requirePlanApproval: false, ephemeralAgentsEnabled: true, agentProvisioning: {}, defaultAgentPermissionPolicy: undefined, specStalenessEnabled: false, specStalenessMaxAgeMs: 6 * 60 * 60 * 1000, taskStuckTimeoutMs: 600_000, stalePausedReviewThresholdMs: 24 * 60 * 60_000, // Capacity risk warning default: only warn once todo is meaningfully backlogged. capacityRiskBannerEnabled: false, capacityRiskTodoThreshold: 20, staleHighFanoutBlockerAgeThresholdMs: 2 * 60 * 60 * 1000, staleInProgressWarningMs: 4 * 60 * 60_000, staleInProgressCriticalMs: 24 * 60 * 60_000, staleInReviewWarningMs: 24 * 60 * 60_000, staleInReviewCriticalMs: 3 * 24 * 60 * 60_000, aiSessionTtlMs: 7 * 24 * 60 * 60 * 1000, aiSessionCleanupIntervalMs: 60 * 60 * 1000, autoUnpauseEnabled: true, autoUnpauseBaseDelayMs: 300_000, autoUnpauseMaxDelayMs: 3_600_000, maxStuckKills: 6, maxBranchConflictRecoveries: 5, maxReviewerContextRetries: 2, maxReviewerFallbackRetries: 2, maxTotalRetriesBeforeFail: 25, 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, taskTokenBudget: 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, chatRoomRecentVerbatimMessages: 12, chatRoomCompactionFetchLimit: 80, chatRoomSummaryMaxChars: 1_500, 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; /** * 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, ); /** Keys that belong to the project settings scope. */ export const PROJECT_SETTINGS_KEYS = Object.freeze( Object.keys(DEFAULT_PROJECT_SETTINGS) as Array, ); 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); } export function resolvePersistAgentThinkingLog( settings: Partial | undefined, opts: { ephemeral: boolean }, ): boolean { const granular = opts.ephemeral ? settings?.persistAgentThinkingLogEphemeral : settings?.persistAgentThinkingLogPermanent; if (typeof granular === "boolean") return granular; if (typeof settings?.persistAgentThinkingLog === "boolean") return settings.persistAgentThinkingLog; return false; }