/**
 * Type definitions, constants, and configuration shapes for SF preferences.
 *
 * All interfaces, type aliases, and static lookup tables live here so that
 * both the validation and runtime modules can import them without pulling
 * in filesystem or loading logic.
 */

import type { GitPreferences } from "./git-service.js";
import type {
	DynamicRoutingConfig,
	ModelCapabilities,
} from "./model-router.js";
import type {
	BudgetEnforcementMode,
	ContextSelectionMode,
	GateEvaluationConfig,
	NotificationPreferences,
	ParallelConfig,
	PhaseSkipPreferences,
	PostUnitHookConfig,
	PreDispatchHookConfig,
	ReactiveExecutionConfig,
	TokenProfile,
} from "./types.js";

export interface ContextManagementConfig {
	observation_masking?: boolean; // default: true
	observation_mask_turns?: number; // default: 8, range: 1-50
	compaction_threshold_percent?: number; // default: 0.70, range: 0.5-0.95
	tool_result_max_chars?: number; // default: 800, range: 200-10000
}

export interface ContextModeConfig {
	/** Master switch. Default: true (opt-out via `enabled: false`). */
	enabled?: boolean;
	/** Per-invocation timeout in milliseconds. Default: 30_000. Range: 1_000–600_000. */
	exec_timeout_ms?: number;
	/** Cap on persisted stdout bytes per invocation. Default: 1_048_576 (1 MiB). Range: 4_096–16_777_216. */
	exec_stdout_cap_bytes?: number;
	/** Number of trailing stdout characters returned in the digest. Default: 300. Range: 0–4_000. */
	exec_digest_chars?: number;
	/** Environment variables forwarded to sandboxed processes. PATH and HOME are always forwarded. */
	exec_env_allowlist?: string[];
}

/**
 * Resolve whether context-mode features (sf_exec sandbox + compaction snapshot)
 * should be active. Default is ON: missing config or missing `enabled` is true.
 */
export function isContextModeEnabled(
	prefs: { context_mode?: ContextModeConfig } | null | undefined,
): boolean {
	return prefs?.context_mode?.enabled !== false;
}

import type { GitHubSyncConfig } from "../github-sync/types.js";

// ─── Workflow Modes ──────────────────────────────────────────────────────────

export type WorkflowMode = "solo" | "team";

/** Default preference values for each workflow mode. */
export const MODE_DEFAULTS: Record<WorkflowMode, Partial<SFPreferences>> = {
	solo: {
		git: {
			auto_push: true,
			push_branches: false,
			pre_merge_check: "auto",
			merge_strategy: "squash",
			isolation: "none",
		},
		unique_milestone_ids: false,
	},
	team: {
		git: {
			auto_push: false,
			push_branches: true,
			pre_merge_check: true,
			merge_strategy: "squash",
			isolation: "none",
		},
		unique_milestone_ids: true,
	},
};

/** All recognized top-level keys in SFPreferences. Used to detect typos / stale config. */
export const KNOWN_PREFERENCE_KEYS = new Set<string>([
	"version",
	"last_synced_with_sf",
	"sf_template_state",
	"sf_template_hash",
	"mode",
	"always_use_skills",
	"prefer_skills",
	"avoid_skills",
	"skill_rules",
	"custom_instructions",
	"models",
	"persist_model_changes",
	"skill_discovery",
	"skill_staleness_days",
	"auto_supervisor",
	"uat_dispatch",
	"unique_milestone_ids",
	"budget_ceiling",
	"budget_enforcement",
	"context_pause_threshold",
	"notifications",
	"cmux",
	"remote_questions",
	"git",
	"post_unit_hooks",
	"pre_dispatch_hooks",
	"dynamic_routing",
	"uok",
	"token_profile",
	"phases",
	"auto_visualize",
	"auto_report",
	"parallel",
	"verification_commands",
	"verification_auto_fix",
	"verification_max_retries",
	"search_provider",
	"context_selection",
	"widget_mode",
	"reactive_execution",
	"gate_evaluation",
	"github",
	"service_tier",
	"allowed_providers",
	"advisor_allowed_providers",
	"blocked_providers",
	"provider_preference",
	"provider_model_allow",
	"provider_model_block",
	"forensics_dedup",
	"show_token_cost",
	"stale_commit_threshold_minutes",
	"context_management",
	"experimental",
	"codebase",
	"slice_parallel",
	"safety_harness",
	"enhanced_verification",
	"enhanced_verification_pre",
	"enhanced_verification_post",
	"enhanced_verification_strict",
	"discuss_preparation",
	"discuss_web_research",
	"discuss_depth",
	"flat_rate_providers",
	"shell_wrapper",
	"workspace",
	"subscription",
	"allow_flat_rate_providers",
	"planning_depth",
]);

/** Canonical list of all dispatch unit types. */
export const KNOWN_UNIT_TYPES = [
	"research-milestone",
	"roadmap-meeting",
	"plan-milestone",
	"research-slice",
	"plan-slice",
	"execute-task",
	"reactive-execute",
	"gate-evaluate",
	"complete-slice",
	"replan-slice",
	"reassess-roadmap",
	"run-uat",
	"complete-milestone",
	"validate-milestone",
	"rewrite-docs",
	"discuss-milestone",
	"discuss-slice",
	"worktree-merge",
] as const;
export type UnitType = (typeof KNOWN_UNIT_TYPES)[number];

export const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);

export interface SFSkillRule {
	when: string;
	use?: string[];
	prefer?: string[];
	avoid?: string[];
}

/**
 * Model configuration for a single phase.
 * Supports primary model with optional fallbacks for resilience.
 */
export interface SFPhaseModelConfig {
	/** Primary model ID (e.g., "claude-opus-4-6") */
	model: string;
	/** Provider name to disambiguate when the same model ID exists across providers (e.g., "bedrock", "anthropic") */
	provider?: string;
	/** Fallback models to try in order if primary fails (e.g., rate limits, credits exhausted) */
	fallbacks?: string[];
}

/**
 * Legacy model config -- simple string per phase.
 * Kept for backward compatibility; will be migrated to SFModelConfigV2 on load.
 */
export interface SFModelConfig {
	research?: string;
	planning?: string;
	discuss?: string;
	execution?: string;
	execution_simple?: string;
	completion?: string;
	validation?: string;
	subagent?: string;
}

/**
 * Extended model config with per-phase fallback support.
 * Each phase can specify a primary model and ordered fallbacks.
 */
export interface SFModelConfigV2 {
	research?: string | SFPhaseModelConfig;
	planning?: string | SFPhaseModelConfig;
	discuss?: string | SFPhaseModelConfig;
	execution?: string | SFPhaseModelConfig;
	execution_simple?: string | SFPhaseModelConfig;
	completion?: string | SFPhaseModelConfig;
	validation?: string | SFPhaseModelConfig;
	subagent?: string | SFPhaseModelConfig;
}

/** Normalized model selection with resolved fallbacks */
export interface ResolvedModelConfig {
	primary: string;
	fallbacks: string[];
}

export type SkillDiscoveryMode = "auto" | "suggest" | "off";

export interface AutoSupervisorConfig {
	model?: string;
	soft_timeout_minutes?: number;
	idle_timeout_minutes?: number;
	hard_timeout_minutes?: number;
	phase_timeout_minutes?: number;
	/** Tool-call count before nudging complete-slice agents to call sf_complete_slice. Default: 10. Set 0 to disable. */
	completion_nudge_after?: number;
	/** Enable the runaway guard that asks long-running active units for diagnosis before pausing. Default: true. */
	runaway_guard_enabled?: boolean;
	/** Unit tool-call count before runaway diagnosis. Default: 60. Set 0 to disable this signal. */
	runaway_tool_call_warning?: number;
	/** Unit token count before runaway diagnosis. Default: 1_000_000. Set 0 to disable this signal. */
	runaway_token_warning?: number;
	/** Unit elapsed minutes before runaway diagnosis. Default: 20. Set 0 to disable this signal. */
	runaway_elapsed_minutes?: number;
	/** Changed file count since unit start before runaway diagnosis. Default: 75. Set 0 to disable this signal. */
	runaway_changed_files_warning?: number;
	/** Number of diagnostic turns before pausing if budget keeps growing. Default: 2. */
	runaway_diagnostic_turns?: number;
	/** Pause auto-mode after diagnostic turns if budget keeps growing. Default: true. */
	runaway_hard_pause?: boolean;
	/** Enable supervised mode in headless auto - wait for stdin instead of exiting on questions. Default: true */
	supervised_mode?: boolean;
}

export interface RemoteQuestionsConfig {
	channel: "slack" | "discord" | "telegram";
	channel_id: string | number;
	/** Telegram user IDs allowed to answer prompts. Empty/omitted means any user in the configured chat may answer. */
	allowed_user_ids?: Array<string | number>;
	timeout_minutes?: number; // clamped to 1-30
	poll_interval_seconds?: number; // clamped to 2-30
	auto_resolve_on_timeout?: boolean;
	auto_resolve_strategy?: "recommended-option";
}

export interface DispatchExperimentPreferences {
	/**
	 * Explicit rule-name order override. Unknown names are ignored and omitted
	 * rules keep their original relative order after the listed rules.
	 */
	order?: string[];
	/**
	 * Named dispatch-order variants for lightweight A/B experimentation.
	 * Each variant is an order array with the same semantics as `order`.
	 */
	variants?: Record<string, string[]>;
	/** Active variant name. If set and found in `variants`, it wins over `order`. */
	active_variant?: string;
}

export interface CmuxPreferences {
	enabled?: boolean;
	notifications?: boolean;
	sidebar?: boolean;
	splits?: boolean;
	browser?: boolean;
}

export type UokTurnActionMode = "commit" | "snapshot" | "status-only";

export interface UokPreferences {
	enabled?: boolean;
	legacy_fallback?: {
		enabled?: boolean;
	};
	gates?: {
		enabled?: boolean;
	};
	model_policy?: {
		enabled?: boolean;
	};
	execution_graph?: {
		enabled?: boolean;
	};
	gitops?: {
		enabled?: boolean;
		turn_action?: UokTurnActionMode;
		turn_push?: boolean;
	};
	audit_envelope?: {
		enabled?: boolean;
	};
	/** @deprecated Use `audit_envelope` instead. */
	audit_unified?: {
		enabled?: boolean;
	};
	planning_flow?: {
		enabled?: boolean;
	};
	/** @deprecated Use `planning_flow` instead. */
	plan_v2?: {
		enabled?: boolean;
	};
}

/**
 * Opt-in experimental features. All features in this block are disabled by
 * default and must be explicitly enabled. They may change or be removed without
 * a deprecation cycle while in experimental status.
 */
export interface ExperimentalPreferences {
	/**
	 * Enable RTK (Real-Time Kompression) shell-command compression.
	 * RTK wraps shell commands to reduce token usage during command execution.
	 * Default: false (opt-in required).
	 */
	rtk?: boolean;
	dispatch_rules?: DispatchExperimentPreferences;
}

export type CodebaseIndexerBackendName = "projectRag" | "sift" | "none";

/** Configuration for the codebase map generator (/sf codebase). */
export interface CodebaseMapPreferences {
	/** Additional directory/file patterns to exclude (e.g. ["docs/", "fixtures/"]). Merged with built-in defaults. */
	exclude_patterns?: string[];
	/** Max files to include in the map. Default: 500. */
	max_files?: number;
	/** Files-per-directory threshold before collapsing to a summary line. Default: 20. */
	collapse_threshold?: number;
	/** Optional codebase-indexer backend. Default: sift when available; set projectRag explicitly to use MCP RAG. */
	indexer_backend?: CodebaseIndexerBackendName;
	/** Optional Brainwires/project-rag MCP backend. Default: "auto" (use if configured, never block if missing). */
	project_rag?: "auto" | "off" | "required";
	/** MCP server name for project-rag when it cannot be detected from command/args. */
	project_rag_server?: string;
	/** Whether prompts should prefer indexing before querying when project-rag is configured. Default: true. */
	project_rag_auto_index?: boolean;
}

/**
 * Subscription / flat-rate billing configuration.
 * When a provider is covered by a monthly subscription, SF can amortize the
 * monthly cost over tokens consumed to produce a meaningful "effective cost"
 * for routing and reporting instead of always showing $0.
 */
export interface SubscriptionConfig {
	/**
	 * Fixed monthly cost in USD for the subscription (e.g. 100 for $100/month).
	 * Used as the numerator when computing effective per-token cost.
	 */
	monthly_cost_usd?: number;
	/**
	 * Provider ID that is covered by this subscription.
	 * Case-insensitive match against provider IDs from the model registry.
	 */
	provider?: string;
	/**
	 * Running total of tokens consumed this calendar month under the subscription.
	 * Updated automatically by SF whenever a unit dispatched to this provider completes.
	 */
	tokens_used_this_month?: number;
}

export interface SFPreferences {
	version?: number;
	/**
	 * SF version (semver) that last wrote this file via the init template.
	 * Stamped automatically by ensurePreferences(); used by drift detection
	 * to suggest a template refresh when sf has evolved meaningfully since
	 * the file was generated. Not user-set.
	 */
	last_synced_with_sf?: string;
	/**
	 * ADR-021 scaffold state for the PREFERENCES.md template body.
	 * `pending` — body still matches the shipped template hash (safe to
	 * upgrade silently). `editing` — body has drifted, propose changes.
	 * `completed` — body has been finalized; SF must not modify.
	 * Mirrors the inline marker semantics on Markdown scaffold files.
	 */
	sf_template_state?: "pending" | "editing" | "completed";
	/**
	 * sha256 of the PREFERENCES.md body (everything after the closing `---`
	 * of frontmatter) at last template stamp. Used to detect drift between
	 * the shipped body and the on-disk body. Not user-set.
	 */
	sf_template_hash?: string;
	/** Planning depth: "light" (default) or "deep" for full deep-planning-mode artifact gates. */
	planning_depth?: "light" | "deep";
	mode?: WorkflowMode;
	always_use_skills?: string[];
	prefer_skills?: string[];
	avoid_skills?: string[];
	skill_rules?: SFSkillRule[];
	custom_instructions?: string[];
	models?: SFModelConfig | SFModelConfigV2;
	/** Persist model changes to default provider/model. Default: true. */
	persist_model_changes?: boolean;
	skill_discovery?: SkillDiscoveryMode;
	skill_staleness_days?: number; // Skills unused for N days get deprioritized (#599). 0 = disabled. Default: 60.
	auto_supervisor?: AutoSupervisorConfig;
	uat_dispatch?: boolean;
	unique_milestone_ids?: boolean;
	budget_ceiling?: number;
	budget_enforcement?: BudgetEnforcementMode;
	context_pause_threshold?: number;
	notifications?: NotificationPreferences;
	cmux?: CmuxPreferences;
	remote_questions?: RemoteQuestionsConfig;
	git?: GitPreferences;
	post_unit_hooks?: PostUnitHookConfig[];
	pre_dispatch_hooks?: PreDispatchHookConfig[];
	dynamic_routing?: DynamicRoutingConfig;
	/** Orchestration kernel controls (all flags default off). */
	uok?: UokPreferences;
	/** Per-model capability overrides. Deep-merged with built-in profiles for capability-aware routing (ADR-004). */
	modelOverrides?: Record<
		string,
		{ capabilities?: Partial<ModelCapabilities> }
	>;
	context_management?: ContextManagementConfig;
	context_mode?: ContextModeConfig;
	token_profile?: TokenProfile;
	phases?: PhaseSkipPreferences;
	auto_visualize?: boolean;
	/** Generate HTML report snapshot after each milestone completion. Default: true. Set false to disable. */
	auto_report?: boolean;
	parallel?: ParallelConfig;
	verification_commands?: string[];
	verification_auto_fix?: boolean;
	verification_max_retries?: number;
	/** Search provider preference. "brave"/"tavily"/"minimax"/"serper"/"exa"/"ollama"/"combosearch" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */
	search_provider?:
		| "brave"
		| "tavily"
		| "minimax"
		| "serper"
		| "exa"
		| "ollama"
		| "combosearch"
		| "native"
		| "auto";
	/** Context selection mode for file inlining. "full" inlines entire files, "smart" uses semantic chunking. Default derived from token profile. */
	context_selection?: ContextSelectionMode;
	/** Default widget display mode for auto-mode dashboard. "full" | "small" | "min" | "off". Default: "full". */
	widget_mode?: "full" | "small" | "min" | "off";
	/** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
	reactive_execution?: ReactiveExecutionConfig;
	/** Parallel quality gate evaluation during slice planning. Disabled by default. */
	gate_evaluation?: GateEvaluationConfig;
	/** GitHub sync configuration. Opt-in: syncs SF events to GitHub Issues, Milestones, and PRs. */
	github?: GitHubSyncConfig;
	/**
	 * OpenAI service tier preference. Only affects gpt-5.4 models.
	 *   - "priority" — 2x cost, faster.
	 *   - "flex"     — 0.5x cost, slower.
	 *   - "off"      — explicitly disabled: no footer status event, no
	 *                  service_tier field in outbound requests. Use this on
	 *                  setups that will never run gpt-5.4 so the feature is
	 *                  fully dormant.
	 *   - undefined  — not configured (default).
	 */
	service_tier?: "priority" | "flex" | "off";
	/** Opt-in: search existing issues and PRs before filing from /sf forensics. Uses additional AI tokens. */
	forensics_dedup?: boolean;
	/** Opt-in: show per-prompt and cumulative session token cost in the footer. Default: false. */
	show_token_cost?: boolean;
	/**
	 * Minutes without a commit before flagging uncommitted changes as stale.
	 * When the threshold is exceeded and the working tree is dirty, doctor will
	 * auto-commit a safety snapshot tagged with `[sf safety]`. Default: 30.
	 * Set to 0 to disable.
	 */
	stale_commit_threshold_minutes?: number;
	/**
	 * Opt-in experimental features. All features here are disabled by default.
	 * See the preferences reference for details on each feature.
	 */
	experimental?: ExperimentalPreferences;
	/** Configuration for the codebase map generator (/sf codebase). */
	codebase?: CodebaseMapPreferences;
	/** Slice-level parallelism within a milestone. Disabled by default. */
	slice_parallel?: { enabled?: boolean; max_workers?: number };
	/** LLM safety harness configuration. Monitors, validates, and constrains LLM behavior during auto-mode. Enabled by default with warn-and-continue policy. */
	safety_harness?: {
		enabled?: boolean;
		evidence_collection?: boolean;
		file_change_validation?: boolean;
		evidence_cross_reference?: boolean;
		destructive_command_warnings?: boolean;
		content_validation?: boolean;
		checkpoints?: boolean;
		auto_rollback?: boolean;
		timeout_scale_cap?: number;
	};

	// ─── Enhanced Verification ──────────────────────────────────────────────────
	/**
	 * Enable enhanced verification (both pre-execution and post-execution checks).
	 * Default: true (opt-out, not opt-in). Set false to disable all enhanced verification.
	 */
	enhanced_verification?: boolean;
	/**
	 * Enable pre-execution checks (package existence, file references, etc.).
	 * Only applies when enhanced_verification is true.
	 * Default: true.
	 */
	enhanced_verification_pre?: boolean;
	/**
	 * Enable post-execution checks (runtime error detection, audit warnings, etc.).
	 * Only applies when enhanced_verification is true.
	 * Default: true.
	 */
	enhanced_verification_post?: boolean;
	/**
	 * Strict mode: treat any pre-execution check failure as blocking.
	 * Default: false (warnings only for non-critical failures).
	 */
	enhanced_verification_strict?: boolean;
	/**
	 * Override the executor context window size (tokens). When set, overrides
	 * the model's default context window for budget calculations.
	 */
	context_window_override?: number;
	/**
	 * When true, skip reassess-roadmap dispatch for slices whose SUMMARY
	 * signals a structurally clean completion (ADR-003 §4, #4778). Default: false.
	 */
	skip_clean_reassess?: boolean;
	/**
	 * Enable the preparation phase before discussion sessions.
	 * Preparation analyzes the codebase, reviews prior context, and optionally researches the ecosystem.
	 * Default: true.
	 */
	discuss_preparation?: boolean;
	/**
	 * Enable web research during preparation phase.
	 * When enabled, searches for best practices and known issues for the detected tech stack.
	 * Requires a search API key (TAVILY_API_KEY or BRAVE_API_KEY).
	 * Default: true.
	 */
	discuss_web_research?: boolean;
	/**
	 * Depth of preparation analysis.
	 * - "quick": Minimal analysis, fastest (~10s)
	 * - "standard": Balanced analysis (~30s)
	 * - "thorough": Deep analysis with more file sampling (~60s)
	 * Default: "standard".
	 */
	discuss_depth?: "quick" | "standard" | "thorough";
	/**
	 * Extra provider IDs to treat as flat-rate (no cost benefit from dynamic
	 * routing).  Dynamic routing is suppressed for any provider listed here,
	 * in addition to the built-in list (github-copilot, copilot, claude-code)
	 * and any provider auto-detected via `authMode: "externalCli"`.
	 *
	 * Intended for private subscription-backed proxies, enterprise-gated
	 * deployments, and custom CLI wrappers where every request costs the
	 * same regardless of model.  Case-insensitive.
	 */
	flat_rate_providers?: string[];
	/**
	 * Hard allowlist of providers that auto-mode is permitted to dispatch to.
	 * Case-insensitive provider IDs (e.g. ["kimi-coding", "minimax", "zai",
	 * "ollama-cloud"]). When set, SF filters `modelRegistry.getAvailable()`
	 * down to only these providers before resolving `models.*` roles, running
	 * dynamic routing, or applying fallback chains — models from any other
	 * provider are invisible to selection logic.
	 *
	 * Undefined (default) = no filtering, every installed provider is in play.
	 *
	 * Pair with `models.*` using `provider/model-id` syntax to pin specific
	 * choices; the allowlist is the outer gate, the `models.*` block picks
	 * within it, and dynamic routing's `tier_models` stays inside the gate.
	 */
	allowed_providers?: string[];
	/**
	 * Additional provider allowlist for advisory subagents. Case-insensitive
	 * provider IDs. When set, advisory dispatch may use providers listed here
	 * even if they are not in the implementor `allowed_providers` pool; the
	 * global `blocked_providers` list still denies providers for every role.
	 *
	 * Undefined or empty = advisory dispatch falls back to `allowed_providers`.
	 */
	advisor_allowed_providers?: readonly string[];
	/**
	 * Hard provider blocklist. Case-insensitive provider IDs. Applied after
	 * `allowed_providers`, so deny wins if a provider appears in both lists.
	 *
	 * Use this to keep a provider installed/authenticated but unavailable to
	 * SF auto-selection, for example during quota incidents or vendor testing.
	 */
	blocked_providers?: string[];
	/**
	 * Provider ranking for benchmark-selector tie-breaking. When two models
	 * score identically (typically the same underlying weights served by
	 * different endpoints, e.g. `zai/glm-5.1` vs `opencode-go/glm-5.1`),
	 * the provider earlier in this list wins. Case-insensitive.
	 *
	 * Providers not in the list fall back to alphabetical order after all
	 * ranked providers, so partial lists work — rank only the ones you care
	 * about. Typical use: put direct/native providers first, re-servers
	 * (opencode, opencode-go, openrouter) later.
	 *
	 * Example:
	 *   provider_preference: [kimi-coding, minimax, zai, mistral, opencode-go, opencode]
	 */
	provider_preference?: string[];
	/**
	 * Per-provider model allow-list. When a provider has an entry here, only
	 * matching model IDs may be used from that provider; older variants in
	 * the registry are filtered out. Entries are exact model IDs by default,
	 * with `*` glob support and `:suffix` shorthand for suffix matches
	 * (for example `openrouter: [":free"]`). Providers absent from this block
	 * are unrestricted unless SF has a built-in provider policy. OpenRouter is
	 * always restricted to `:free` models, OpenCode is always restricted to
	 * zero-cost/free models, and OpenCode Go remains unrestricted because it is
	 * the subscribed tier.
	 *
	 * Project-level overrides global per provider (full replace inside the
	 * array, not merge).
	 *
	 * Example:
	 *   provider_model_allow:
	 *     minimax:
	 *       - MiniMax-M2.7
	 *       - MiniMax-M2.7-highspeed
	 *     openrouter:
	 *       - :free
	 */
	provider_model_allow?: Record<string, string[]>;
	/**
	 * Per-provider model block-list. Matching model IDs are never used from
	 * that provider, even if the provider/model also appears in an allow-list.
	 * Uses the same exact, `*` glob, and `:suffix` matching as
	 * `provider_model_allow`.
	 */
	provider_model_block?: Record<string, string[]>;

	/**
	 * Shell wrapper prepended when SF spawns sub-processes (parallel milestone
	 * workers, slice workers). Useful for projects that require a specific shell
	 * environment such as a Nix development shell.
	 *
	 * Each element is a separate argument. The worker is launched as:
	 *   shell_wrapper[0] [...shell_wrapper.slice(1)] <node> <sf-loader> headless ...
	 *
	 * Example for NixOS projects:
	 *   shell_wrapper:
	 *     - nix
	 *     - develop
	 *     - --command
	 */
	shell_wrapper?: string[];

	/**
	 * Minimum interval (ms) between consecutive unit request dispatches.
	 * When set, the loop waits until at least this many milliseconds have
	 * elapsed since the last dispatch before sending the next one.
	 * Useful for rate-limiting high-frequency auto-mode runs.
	 * Default: 0 (no minimum interval).
	 */
	min_request_interval_ms?: number;

	/**
	 * Workspace lifecycle hooks. Shell scripts run at key points in the
	 * worktree lifecycle (inspired by Symphony's hooks model).
	 *
	 * Scripts run via `sh -c` in the worktree directory. Failures are logged;
	 * `after_create` failure is fatal to worktree creation, `after_run` is
	 * best-effort.
	 */
	workspace?: {
		/** Runs once when a new worktree is first created. Failure aborts creation. */
		after_create?: string;
		/** Runs before each task dispatch inside the worktree. Failure aborts the task. */
		before_run?: string;
		/** Runs after each task completes (success or failure). Best-effort. */
		after_run?: string;
	};

	/**
	 * Subscription / flat-rate billing configuration.
	 * When set, SF uses the monthly cost amortized over consumed tokens to
	 * produce a meaningful "effective cost" for routing and journal events,
	 * rather than always reporting $0 for subscription-backed providers.
	 * Tokens consumed from the subscription provider are automatically tracked
	 * in `subscription.tokens_used_this_month`.
	 */
	subscription?: SubscriptionConfig;

	/**
	 * When false, model routing skips models from flat-rate / subscription-tier
	 * providers (providers where every request costs the same regardless of
	 * model, such as GitHub Copilot or claude-code).  Default: true (flat-rate
	 * providers are included in candidate selection).
	 *
	 * Set to false to force routing to strictly metered API providers only —
	 * useful when accurate cost accounting is required and flat-rate providers
	 * would otherwise contaminate cost reports with $0 entries.
	 */
	allow_flat_rate_providers?: boolean;
}

export interface LoadedSFPreferences {
	path: string;
	scope: "global" | "project";
	preferences: SFPreferences;
	/** Validation warnings (unknown keys, type mismatches, deprecations). Empty when preferences are clean. */
	warnings?: string[];
}

export interface SkillResolution {
	/** The original reference from preferences (bare name or path). */
	original: string;
	/** The resolved absolute path to the SKILL.md file, or null if unresolved. */
	resolvedPath: string | null;
	/** How it was resolved. */
	method:
		| "absolute-path"
		| "absolute-dir"
		| "user-skill"
		| "project-skill"
		| "unresolved";
}

export interface SkillResolutionReport {
	/** All resolution results, keyed by original reference. */
	resolutions: Map<string, SkillResolution>;
	/** References that could not be resolved. */
	warnings: string[];
}

/**
 * Format a skill reference for the system prompt.
 * If resolved, shows the path so the agent knows exactly where to read.
 * If unresolved, marks it clearly.
 */
export function formatSkillRef(
	ref: string,
	resolutions: Map<string, SkillResolution>,
): string {
	const resolution = resolutions.get(ref);
	if (!resolution || resolution.method === "unresolved") {
		return `${ref} (⚠ not found — check skill name or path)`;
	}
	if (
		resolution.method === "absolute-path" ||
		resolution.method === "absolute-dir"
	) {
		return ref;
	}
	return `${ref} → \`${resolution.resolvedPath}\``;
}
