/**
 * SF Preferences -- loading, merging, and rendering.
 *
 * This module is the primary entry point for preference operations.
 * Type definitions live in ./preferences-types.js, validation in
 * ./preferences-validation.js, skill logic in ./preferences-skills.js,
 * and model logic in ./preferences-models.js.
 *
 * All symbols are re-exported here so that existing `import { ... } from "./preferences.js"`
 * statements continue to work without modification.
 */
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { normalizeStringArray } from "@singularity-forge/coding-agent";
import { parse as parseYaml } from "yaml";
import { sfRoot } from "./paths.js";
import {
	_initPrefsLoader,
	resolveProfileDefaults as _resolveProfileDefaults,
} from "./preferences-models.js";
import { upgradePreferencesFileIfDrifted } from "./preferences-template-upgrade.js";
import { formatSkillRef, MODE_DEFAULTS } from "./preferences-types.js";
import { validatePreferences } from "./preferences-validation.js";
import { sfHome } from "./sf-home.js";
import { logWarning } from "./workflow-logger.js";

// ─── Re-exports: types ──────────────────────────────────────────────────────
// Every type/interface that was previously exported from this file is
// re-exported so that downstream `import { Foo } from "./preferences.js"`
// statements keep compiling.
// ─── Re-exports: skills ─────────────────────────────────────────────────────
export { resolveAllSkillReferences } from "./preferences-skills.js";
// ─── Re-exports: validation ─────────────────────────────────────────────────
export { validatePreferences } from "./preferences-validation.js";
// These lived in preferences-skills.ts but imported loadEffectiveSFPreferences
// back from this file, creating a circular dependency. Moved here since they
// are trivial wrappers over loadEffectiveSFPreferences.
/**
 * Resolve the current skill discovery mode from effective preferences.
 */
export function resolveSkillDiscoveryMode() {
	const prefs = loadEffectiveSFPreferences();
	return prefs?.preferences.skill_discovery ?? "suggest";
}
/**
 * Resolve the skill staleness threshold in days from effective preferences.
 */
export function resolveSkillStalenessDays() {
	const prefs = loadEffectiveSFPreferences();
	return prefs?.preferences.skill_staleness_days ?? 60;
}
// ─── Re-exports: models ─────────────────────────────────────────────────────
export {
	filterModelsByProviderModelAllow,
	getNextFallbackModel,
	isProviderAllowedForAdvisor,
	isProviderModelAllowed,
	resolveAutoSupervisorConfig,
	resolveContextSelection,
	resolveDynamicRoutingConfig,
	resolveEffectiveProfile,
	resolveInlineLevel,
	resolveModelForUnit,
	resolveModelWithFallbacksForUnit,
	resolvePersistModelChanges,
	resolveProfileDefaults,
	resolveSearchProviderFromPreferences,
	updatePreferencesModels,
	validateModelId,
} from "./preferences-models.js";

// ─── Path Constants & Getters ───────────────────────────────────────────────

/**
 * Return the canonical path to the SF agent settings file (~/.sf/agent/settings.json).
 *
 * Purpose: provide a single source of truth for the settings path so that
 * notifications, permissions, and other modules don't each hard-code the same
 * homedir join.
 *
 * Consumer: notifications/notify.js, permissions/permission-core.js.
 */
export function getSfAgentSettingsPath() {
	return join(sfHome(), "agent", "settings.json");
}
// Canonical location — pure YAML, no frontmatter markers
function globalPreferencesYamlPath() {
	return join(sfHome(), "preferences.yaml");
}
function legacyGlobalPreferencesMarkdownPath() {
	return join(sfHome(), "preferences.md");
}
/**
 * Resolve the "project root" for preferences. When SF is running inside a
 * git worktree (e.g. `.sf/worktrees/M003/`), project-level prefs should
 * come from the MAIN worktree's `.sf/preferences.yaml`, not the milestone
 * branch's frozen copy. Otherwise a pref change on main never reaches an
 * in-flight milestone, and we saw this in practice: updating PREFERENCES
 * on main had no effect until the milestone branch merged main.
 *
 * Strategy: read `.git` in the current worktree. If it's a file of the
 * form `gitdir: <path>`, we're in a linked worktree — resolve the
 * commondir and walk up one level to reach the main worktree. Fall back
 * to cwd silently for non-worktree setups (bare clones, submodules, etc.
 * where the current logic is already correct).
 */
function projectPrefsRoot() {
	const cwd = process.cwd();
	try {
		const gitPath = join(cwd, ".git");
		if (!existsSync(gitPath)) return cwd;
		const stat = readFileSync(gitPath, "utf-8");
		// Linked worktrees have `.git` as a FILE: "gitdir: /abs/path/to/main/.git/worktrees/NAME"
		const m = /^gitdir:\s*(.+)$/m.exec(stat.trim());
		if (!m) return cwd;
		const gitdir = m[1].trim();
		// gitdir looks like /.../.git/worktrees/NAME — commondir is the /.../.git/ part;
		// the main worktree is the directory containing that .git dir.
		// Use the `commondir` pointer file inside the linked worktree's gitdir when present.
		const commondirFile = join(gitdir, "commondir");
		if (existsSync(commondirFile)) {
			const rel = readFileSync(commondirFile, "utf-8").trim();
			const commondir = resolve(gitdir, rel); // usually "../.." → /.../.git
			const mainRoot = dirname(commondir); // /.../ (main worktree root)
			if (existsSync(join(mainRoot, ".sf"))) return mainRoot;
		}
	} catch {
		/* non-fatal — fall back to cwd */
	}
	return cwd;
}
// Canonical location — pure YAML
function projectPreferencesYamlPath() {
	return join(sfRoot(projectPrefsRoot()), "preferences.yaml");
}
/**
 * Get the canonical path for the global SF preferences file (preferences.yaml).
 */
export function getGlobalSFPreferencesPath() {
	return globalPreferencesYamlPath();
}
/**
 * Get the canonical path for the project-level SF preferences file (preferences.yaml).
 */
export function getProjectSFPreferencesPath() {
	return projectPreferencesYamlPath();
}
// ─── Loading ────────────────────────────────────────────────────────────────
/**
 * Load global SF preferences from preferences.yaml.
 */
export function loadGlobalSFPreferences() {
	return (
		loadPreferencesFile(globalPreferencesYamlPath(), "global") ??
		loadPreferencesFile(legacyGlobalPreferencesMarkdownPath(), "global", {
			legacyMarkdown: true,
		})
	);
}
/**
 * Load project-level SF preferences from preferences.yaml.
 */
export function loadProjectSFPreferences() {
	return loadPreferencesFile(projectPreferencesYamlPath(), "project");
}
/**
 * Load and merge global and project preferences with profile defaults and mode defaults applied.
 */
export function loadEffectiveSFPreferences() {
	const globalPreferences = loadGlobalSFPreferences();
	const projectPreferences = loadProjectSFPreferences();
	if (!globalPreferences && !projectPreferences) return null;
	let result;
	if (!globalPreferences) {
		result = projectPreferences;
	} else if (!projectPreferences) {
		result = globalPreferences;
	} else {
		const mergedWarnings = [
			...(globalPreferences.warnings ?? []),
			...(projectPreferences.warnings ?? []),
		];
		result = {
			path: projectPreferences.path,
			scope: "project",
			preferences: mergePreferences(
				globalPreferences.preferences,
				projectPreferences.preferences,
			),
			...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
		};
	}
	// Apply token-profile defaults as the lowest-priority layer so that
	// `token_profile: budget` sets models and phase-skips automatically.
	// Explicit user preferences always override profile defaults.
	const profile = result.preferences.token_profile;
	if (profile) {
		const profileDefaults = _resolveProfileDefaults(profile);
		result = {
			...result,
			preferences: mergePreferences(profileDefaults, result.preferences),
		};
	}
	// Apply mode defaults as the lowest-priority layer
	if (result.preferences.mode) {
		result = {
			...result,
			preferences: applyModeDefaults(
				result.preferences.mode,
				result.preferences,
			),
		};
	}
	return result;
}
function loadPreferencesFile(path, scope, options = {}) {
	if (!existsSync(path)) return null;
	const raw = readFileSync(path, "utf-8");
	const preferences = options.legacyMarkdown
		? parsePreferencesMarkdown(raw)
		: parsePreferencesYaml(raw);
	if (!preferences) return null;
	const validation = validatePreferences(preferences);
	// Self-align: if the file's recorded sf version drifted from current,
	// silently re-render the frontmatter so subsequent reads match. No
	// human-facing warning — sf keeps its own files in sync. Body content
	// (anything after the frontmatter) is preserved verbatim.
	const aligned = options.legacyMarkdown
		? validation.preferences
		: upgradePreferencesFileIfDrifted(path, validation.preferences);
	const allWarnings = [...validation.warnings, ...validation.errors];
	return {
		path,
		scope,
		preferences: aligned,
		...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
	};
}
/** @internal Reset the warn-once flags — exported for testing only. */
export function _resetParseWarningFlag() {
	// No warn-once flags remain after removal of legacy markdown parser.
}
/**
 * Parse preferences from a pure YAML file (no frontmatter markers needed).
 * Used for preferences.yaml; the entire file content is YAML.
 *
 * @internal Exported for testing only
 */
export function parsePreferencesYaml(content) {
	try {
		const parsed = parseYaml(content);
		if (typeof parsed !== "object" || parsed === null) return {};
		return parsed;
	} catch (e) {
		logWarning("guided", `YAML parse error in preferences.yaml: ${e.message}`);
		return {};
	}
}
/**
 * @deprecated Use parsePreferencesYaml instead.
 * Retained for callers (e.g. auto-dashboard.js) that read .yaml content via this path.
 * Strips YAML frontmatter markers if present, then delegates to parsePreferencesYaml.
 */
export function parsePreferencesMarkdown(content) {
	// Strip optional frontmatter fences so legacy callers work with pure YAML too
	const fenced = /^---\r?\n([\s\S]*?)\n---(?:\r?\n[\s\S]*)?$/.exec(content);
	return parsePreferencesYaml(fenced ? fenced[1] : content);
}
// ─── Merging ────────────────────────────────────────────────────────────────
/**
 * Apply mode defaults as the lowest-priority layer.
 * Mode defaults fill in undefined fields; any explicit user value wins.
 */
/**
 * Apply mode defaults as the lowest-priority layer to preferences.
 */
export function applyModeDefaults(mode, prefs) {
	const defaults = MODE_DEFAULTS[mode];
	if (!defaults) return prefs;
	return mergePreferences(defaults, prefs);
}
function mergePreferences(base, override) {
	return {
		version: override.version ?? base.version,
		mode: override.mode ?? base.mode,
		always_use_skills: mergeStringLists(
			base.always_use_skills,
			override.always_use_skills,
		),
		prefer_skills: mergeStringLists(base.prefer_skills, override.prefer_skills),
		avoid_skills: mergeStringLists(base.avoid_skills, override.avoid_skills),
		skill_rules: [...(base.skill_rules ?? []), ...(override.skill_rules ?? [])],
		custom_instructions: mergeStringLists(
			base.custom_instructions,
			override.custom_instructions,
		),
		models: { ...(base.models ?? {}), ...(override.models ?? {}) },
		persist_model_changes:
			override.persist_model_changes ?? base.persist_model_changes,
		skill_discovery: override.skill_discovery ?? base.skill_discovery,
		skill_staleness_days:
			override.skill_staleness_days ?? base.skill_staleness_days,
		auto_supervisor: {
			...(base.auto_supervisor ?? {}),
			...(override.auto_supervisor ?? {}),
		},
		uat_dispatch: override.uat_dispatch ?? base.uat_dispatch,
		unique_milestone_ids:
			override.unique_milestone_ids ?? base.unique_milestone_ids,
		budget_ceiling: override.budget_ceiling ?? base.budget_ceiling,
		budget_enforcement: override.budget_enforcement ?? base.budget_enforcement,
		context_pause_threshold:
			override.context_pause_threshold ?? base.context_pause_threshold,
		notifications:
			base.notifications || override.notifications
				? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
				: undefined,
		cmux:
			base.cmux || override.cmux
				? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
				: undefined,
		remote_questions: override.remote_questions
			? { ...(base.remote_questions ?? {}), ...override.remote_questions }
			: base.remote_questions,
		git:
			base.git || override.git
				? { ...(base.git ?? {}), ...(override.git ?? {}) }
				: undefined,
		post_unit_hooks: mergePostUnitHooks(
			base.post_unit_hooks,
			override.post_unit_hooks,
		),
		pre_dispatch_hooks: mergePreDispatchHooks(
			base.pre_dispatch_hooks,
			override.pre_dispatch_hooks,
		),
		dynamic_routing:
			base.dynamic_routing || override.dynamic_routing
				? {
						...(base.dynamic_routing ?? {}),
						...(override.dynamic_routing ?? {}),
					}
				: undefined,
		uok:
			base.uok || override.uok
				? {
						enabled: override.uok?.enabled ?? base.uok?.enabled,
						gates:
							base.uok?.gates || override.uok?.gates
								? { ...(base.uok?.gates ?? {}), ...(override.uok?.gates ?? {}) }
								: undefined,
						model_policy:
							base.uok?.model_policy || override.uok?.model_policy
								? {
										...(base.uok?.model_policy ?? {}),
										...(override.uok?.model_policy ?? {}),
									}
								: undefined,
						execution_graph:
							base.uok?.execution_graph || override.uok?.execution_graph
								? {
										...(base.uok?.execution_graph ?? {}),
										...(override.uok?.execution_graph ?? {}),
									}
								: undefined,
						gitops:
							base.uok?.gitops || override.uok?.gitops
								? {
										...(base.uok?.gitops ?? {}),
										...(override.uok?.gitops ?? {}),
									}
								: undefined,
						audit_envelope:
							base.uok?.audit_envelope ||
							base.uok?.audit_unified ||
							override.uok?.audit_envelope ||
							override.uok?.audit_unified
								? {
										...(base.uok?.audit_envelope ??
											base.uok?.audit_unified ??
											{}),
										...(override.uok?.audit_envelope ??
											override.uok?.audit_unified ??
											{}),
									}
								: undefined,
						planning_flow:
							base.uok?.planning_flow ||
							base.uok?.plan_v2 ||
							override.uok?.planning_flow ||
							override.uok?.plan_v2
								? {
										...(base.uok?.planning_flow ?? base.uok?.plan_v2 ?? {}),
										...(override.uok?.planning_flow ??
											override.uok?.plan_v2 ??
											{}),
									}
								: undefined,
					}
				: undefined,
		token_profile: override.token_profile ?? base.token_profile,
		phases:
			base.phases || override.phases
				? { ...(base.phases ?? {}), ...(override.phases ?? {}) }
				: undefined,
		parallel:
			base.parallel || override.parallel
				? {
						...(base.parallel ?? {}),
						...(override.parallel ?? {}),
					}
				: undefined,
		verification_commands: mergeStringLists(
			base.verification_commands,
			override.verification_commands,
		),
		verification_auto_fix:
			override.verification_auto_fix ?? base.verification_auto_fix,
		verification_max_retries:
			override.verification_max_retries ?? base.verification_max_retries,
		verification_auto_defer_threshold:
			override.verification_auto_defer_threshold ??
			base.verification_auto_defer_threshold,
		enhanced_verification:
			override.enhanced_verification ?? base.enhanced_verification,
		enhanced_verification_pre:
			override.enhanced_verification_pre ?? base.enhanced_verification_pre,
		enhanced_verification_post:
			override.enhanced_verification_post ?? base.enhanced_verification_post,
		enhanced_verification_strict:
			override.enhanced_verification_strict ??
			base.enhanced_verification_strict,
		search_provider: override.search_provider ?? base.search_provider,
		context_selection: override.context_selection ?? base.context_selection,
		auto_visualize: override.auto_visualize ?? base.auto_visualize,
		auto_report: override.auto_report ?? base.auto_report,
		github:
			base.github || override.github
				? {
						...(base.github ?? {}),
						...(override.github ?? {}),
					}
				: undefined,
		experimental:
			base.experimental || override.experimental
				? { ...(base.experimental ?? {}), ...(override.experimental ?? {}) }
				: undefined,
		service_tier: override.service_tier ?? base.service_tier,
		forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
		show_token_cost: override.show_token_cost ?? base.show_token_cost,
		codebase:
			base.codebase || override.codebase
				? {
						...(base.codebase ?? {}),
						...(override.codebase ?? {}),
						// Merge exclude_patterns arrays rather than overriding
						exclude_patterns: [
							...(base.codebase?.exclude_patterns ?? []),
							...(override.codebase?.exclude_patterns ?? []),
						].filter(Boolean),
					}
				: undefined,
		slice_parallel:
			base.slice_parallel || override.slice_parallel
				? { ...(base.slice_parallel ?? {}), ...(override.slice_parallel ?? {}) }
				: undefined,
		// Fields previously validated but silently dropped here — same class
		// of latent bug as service_tier (fixed separately). Each gets a simple
		// override-wins merge so the preference actually reaches consumers.
		allowed_providers: mergeStringLists(
			base.allowed_providers,
			override.allowed_providers,
		),
		advisor_allowed_providers: mergeStringLists(
			base.advisor_allowed_providers,
			override.advisor_allowed_providers,
		),
		blocked_providers: mergeStringLists(
			base.blocked_providers,
			override.blocked_providers,
		),
		provider_preference:
			override.provider_preference ?? base.provider_preference,
		provider_model_allow: mergeProviderModelAllow(
			base.provider_model_allow,
			override.provider_model_allow,
		),
		provider_model_block: mergeProviderModelAllow(
			base.provider_model_block,
			override.provider_model_block,
		),
		flat_rate_providers: mergeStringLists(
			base.flat_rate_providers,
			override.flat_rate_providers,
		),
		stale_commit_threshold_minutes:
			override.stale_commit_threshold_minutes ??
			base.stale_commit_threshold_minutes,
		widget_mode: override.widget_mode ?? base.widget_mode,
		modelOverrides:
			base.modelOverrides || override.modelOverrides
				? { ...(base.modelOverrides ?? {}), ...(override.modelOverrides ?? {}) }
				: undefined,
		safety_harness:
			base.safety_harness || override.safety_harness
				? { ...(base.safety_harness ?? {}), ...(override.safety_harness ?? {}) }
				: undefined,
		// subscription: project-level wins over global (full replace, not merge),
		// so that a project can declare its own subscription context independently.
		subscription: override.subscription ?? base.subscription,
		allow_flat_rate_providers:
			override.allow_flat_rate_providers ?? base.allow_flat_rate_providers,
		// ── Production delivery ──
		deploy:
			base.deploy || override.deploy
				? { ...(base.deploy ?? {}), ...(override.deploy ?? {}) }
				: undefined,
	};
}
function mergeStringLists(base, override) {
	const merged = [
		...normalizeStringArray(base),
		...normalizeStringArray(override),
	]
		.map((item) => item.trim())
		.filter(Boolean);
	return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
}
function mergeProviderModelAllow(base, override) {
	if (!base && !override) return undefined;
	const merged = {};
	for (const [provider, models] of Object.entries(base ?? {})) {
		merged[provider] = [...models];
	}
	for (const [provider, models] of Object.entries(override ?? {})) {
		// Per-provider replace: a project entry replaces the global array for
		// that provider instead of appending to it.
		merged[provider] = [...models];
	}
	return Object.keys(merged).length > 0 ? merged : undefined;
}
function mergePostUnitHooks(base, override) {
	if (!base?.length && !override?.length) return undefined;
	const merged = [...(base ?? [])];
	for (const hook of override ?? []) {
		// Override hooks with same name replace base hooks
		const idx = merged.findIndex((h) => h.name === hook.name);
		if (idx >= 0) {
			merged[idx] = hook;
		} else {
			merged.push(hook);
		}
	}
	return merged.length > 0 ? merged : undefined;
}
function mergePreDispatchHooks(base, override) {
	if (!base?.length && !override?.length) return undefined;
	const merged = [...(base ?? [])];
	for (const hook of override ?? []) {
		const idx = merged.findIndex((h) => h.name === hook.name);
		if (idx >= 0) {
			merged[idx] = hook;
		} else {
			merged.push(hook);
		}
	}
	return merged.length > 0 ? merged : undefined;
}
// ─── System Prompt Rendering ──────────────────────────────────────────────────
/**
 * Render preferences as a formatted string for inclusion in system prompts.
 */
export function renderPreferencesForSystemPrompt(preferences, resolutions) {
	const validated = validatePreferences(preferences);
	const lines = ["## SF Skill Preferences"];
	if (validated.errors.length > 0) {
		lines.push(
			"- Validation: some preference values were ignored because they were invalid.",
		);
	}
	for (const warning of validated.warnings) {
		lines.push(`- Deprecation: ${warning}`);
	}
	preferences = validated.preferences;
	lines.push(
		"- Treat these as explicit skill-selection policy for SF work.",
		"- If a listed skill exists and is relevant, load and follow it instead of treating it as a vague suggestion.",
		"- Current user instructions still override these defaults.",
	);
	const fmt = (ref) => (resolutions ? formatSkillRef(ref, resolutions) : ref);
	if (
		preferences.always_use_skills &&
		preferences.always_use_skills.length > 0
	) {
		lines.push("- Always use these skills when relevant:");
		for (const skill of preferences.always_use_skills) {
			lines.push(`  - ${fmt(skill)}`);
		}
	}
	if (preferences.prefer_skills && preferences.prefer_skills.length > 0) {
		lines.push("- Prefer these skills when relevant:");
		for (const skill of preferences.prefer_skills) {
			lines.push(`  - ${fmt(skill)}`);
		}
	}
	if (preferences.avoid_skills && preferences.avoid_skills.length > 0) {
		lines.push("- Avoid these skills unless clearly needed:");
		for (const skill of preferences.avoid_skills) {
			lines.push(`  - ${fmt(skill)}`);
		}
	}
	if (preferences.skill_rules && preferences.skill_rules.length > 0) {
		lines.push("- Situational rules:");
		for (const rule of preferences.skill_rules) {
			lines.push(`  - When ${rule.when}:`);
			if (rule.use && rule.use.length > 0) {
				lines.push(`    - use: ${rule.use.map(fmt).join(", ")}`);
			}
			if (rule.prefer && rule.prefer.length > 0) {
				lines.push(`    - prefer: ${rule.prefer.map(fmt).join(", ")}`);
			}
			if (rule.avoid && rule.avoid.length > 0) {
				lines.push(`    - avoid: ${rule.avoid.map(fmt).join(", ")}`);
			}
		}
	}
	if (
		preferences.custom_instructions &&
		preferences.custom_instructions.length > 0
	) {
		lines.push("- Additional instructions:");
		for (const instruction of preferences.custom_instructions) {
			lines.push(`  - ${instruction}`);
		}
	}
	return lines.join("\n");
}
// ─── Hook Resolution ──────────────────────────────────────────────────────────
/**
 * Resolve enabled post-unit hooks from effective preferences.
 * Returns an empty array when no hooks are configured.
 */
export function resolvePostUnitHooks() {
	const prefs = loadEffectiveSFPreferences();
	return (prefs?.preferences.post_unit_hooks ?? []).filter(
		(h) => h.enabled !== false,
	);
}
/**
 * Resolve enabled pre-dispatch hooks from effective preferences.
 * Returns an empty array when no hooks are configured.
 */
export function resolvePreDispatchHooks() {
	const prefs = loadEffectiveSFPreferences();
	return (prefs?.preferences.pre_dispatch_hooks ?? []).filter(
		(h) => h.enabled !== false,
	);
}
// ─── Isolation & Parallel ─────────────────────────────────────────────────────
/**
 * Resolve the effective git isolation mode from preferences.
 * Returns "none" (default), "worktree", or "branch".
 *
 * Default is "none" so SF works out of the box without preferences.yaml.
 * Worktree isolation requires explicit opt-in because it depends on git
 * branch infrastructure that must be set up before use.
 */
/**
 * Get the effective git isolation mode from preferences (worktree, branch, or none).
 */
export function getIsolationMode() {
	const prefs = loadEffectiveSFPreferences()?.preferences?.git;
	if (prefs?.isolation === "worktree") return "worktree";
	if (prefs?.isolation === "branch") return "branch";
	return "none"; // default — no isolation, work on current branch
}
/**
 * Resolve parallel execution configuration from preferences.
 */
export function resolveParallelConfig(prefs) {
	return {
		enabled: prefs?.parallel?.enabled ?? false,
		max_workers: Math.max(1, Math.min(4, prefs?.parallel?.max_workers ?? 2)),
		budget_ceiling: prefs?.parallel?.budget_ceiling,
		merge_strategy: prefs?.parallel?.merge_strategy ?? "per-milestone",
		auto_merge: prefs?.parallel?.auto_merge ?? "confirm",
		worker_model: prefs?.parallel?.worker_model,
		worker_timeout_minutes: prefs?.parallel?.worker_timeout_minutes,
		shell_wrapper: prefs?.shell_wrapper,
		stop_on_failure: prefs?.parallel?.stop_on_failure ?? false,
		spawn_failure_timeout_ms:
			prefs?.parallel?.spawn_failure_timeout_ms ?? 30_000,
		spawn_failure_grace_period_ms:
			prefs?.parallel?.spawn_failure_grace_period_ms ?? 10_000,
		max_retries: prefs?.parallel?.max_retries ?? 3,
	};
}

// ─────────────────────────────────────────────────────────────────────────
// Execution Timeouts & Limits (Tier 1.4 Config Schema Alignment)
// ─────────────────────────────────────────────────────────────────────────

/**
 * Get context compaction threshold (tokens). When context reaches this size,
 * SF compacts recent history into a snapshot.
 *
 * Default varies by mode: solo 20000, team 25000.
 */
export function getContextCompactThreshold() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.context_compact_at !== undefined) return prefs.context_compact_at;
	const mode = prefs?.mode ?? "solo";
	const MODE_DEFAULTS = {
		solo: 20000,
		team: 25000,
	};
	return MODE_DEFAULTS[mode] ?? 20000;
}

/**
 * Get absolute context hard limit (tokens). If context exceeds this,
 * the unit fails rather than continuing.
 *
 * Default varies by mode: solo 35000, team 40000.
 */
export function getContextHardLimit() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.context_hard_limit !== undefined) return prefs.context_hard_limit;
	const mode = prefs?.mode ?? "solo";
	const MODE_DEFAULTS = {
		solo: 35000,
		team: 40000,
	};
	return MODE_DEFAULTS[mode] ?? 35000;
}

/**
 * Get default unit execution timeout (seconds).
 *
 * Default: 300 seconds (5 minutes).
 */
export function getUnitTimeout() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	return prefs?.unit_timeout ?? 300;
}

/**
 * Get phase-specific unit timeout (seconds).
 *
 * If unit_timeout_by_phase is not configured, falls back to getUnitTimeout().
 */
export function getUnitTimeoutForPhase(phase) {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.unit_timeout_by_phase && prefs.unit_timeout_by_phase[phase]) {
		return prefs.unit_timeout_by_phase[phase];
	}
	return getUnitTimeout();
}

/**
 * Get max parallel agents for a given phase.
 *
 * Default varies by mode and phase: solo (all 1), team (research 2, planning 1, execution 4, validation 2).
 */
export function getMaxAgentsForPhase(phase) {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.max_agents_by_phase && prefs.max_agents_by_phase[phase]) {
		return prefs.max_agents_by_phase[phase];
	}
	const mode = prefs?.mode ?? "solo";
	const MODE_DEFAULTS = {
		solo: { research: 1, planning: 1, execution: 1, validation: 1 },
		team: { research: 2, planning: 1, execution: 4, validation: 2 },
	};
	return MODE_DEFAULTS[mode]?.[phase] ?? 1;
}

/**
 * Get whether explicit user input turn is required before continuing.
 *
 * Default varies by mode: solo false, team true.
 */
export function isTurnInputRequired() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.turn_input_required !== undefined)
		return prefs.turn_input_required;
	const mode = prefs?.mode ?? "solo";
	return mode === "team";
}

/**
 * Get worktree management mode (none, auto, manual).
 *
 * Default varies by mode: solo auto, team manual.
 */
export function getWorktreeMode() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.worktree_mode !== undefined) return prefs.worktree_mode;
	const mode = prefs?.mode ?? "solo";
	return mode === "team" ? "manual" : "auto";
}

/**
 * Get grace period before forcefully aborting tool calls (milliseconds).
 *
 * Default varies by mode: solo 5000ms, team 8000ms.
 */
export function getToolAbortGrace() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.tool_abort_grace !== undefined) return prefs.tool_abort_grace;
	const mode = prefs?.mode ?? "solo";
	return mode === "team" ? 8000 : 5000;
}

/**
 * Get max turns allowed in a single unit attempt before retry.
 *
 * Default varies by mode: solo 50, team 60.
 */
export function getMaxTurnsPerAttempt() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.max_turns_per_attempt !== undefined)
		return prefs.max_turns_per_attempt;
	const mode = prefs?.mode ?? "solo";
	return mode === "team" ? 60 : 50;
}

/**
 * Get number of recent turns to keep in hot cache for perf.
 *
 * Default varies by mode: solo 5, team 10.
 */
export function getHotCacheTurns() {
	const prefs = loadEffectiveSFPreferences()?.preferences;
	if (prefs?.hot_cache_turns !== undefined) return prefs.hot_cache_turns;
	const mode = prefs?.mode ?? "solo";
	return mode === "team" ? 10 : 5;
}

// Inject the loader functions into preferences-models.js to break the
// preferences.js ↔ preferences-models.js circular import.  This runs after
// all exports in this module are defined, so the injected references are live.
_initPrefsLoader(loadEffectiveSFPreferences, getGlobalSFPreferencesPath);
