xchecker_config/config/model.rs
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use xchecker_selectors::Selectors;
5use xchecker_utils::types::ConfigSource;
6
7/// Default timeout for hook execution in seconds
8pub const DEFAULT_HOOK_TIMEOUT_SECS: u64 = 60;
9
10/// Hook failure behavior
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum OnFail {
14 /// Log warning and continue (default)
15 #[default]
16 Warn,
17 /// Fail the phase on hook failure
18 Fail,
19}
20
21impl std::fmt::Display for OnFail {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 Self::Warn => write!(f, "warn"),
25 Self::Fail => write!(f, "fail"),
26 }
27 }
28}
29
30/// Hook type indicating when the hook runs
31/// Reserved for hooks integration; not wired in v1.0
32#[cfg_attr(not(test), allow(dead_code))]
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum HookType {
35 /// Runs before phase execution
36 PrePhase,
37 /// Runs after phase execution
38 PostPhase,
39}
40
41impl std::fmt::Display for HookType {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 match self {
44 Self::PrePhase => write!(f, "pre_phase"),
45 Self::PostPhase => write!(f, "post_phase"),
46 }
47 }
48}
49
50/// Configuration for a single hook
51#[derive(Debug, Clone, Deserialize, Serialize)]
52pub struct HookConfig {
53 /// Command to execute (can be a script path or shell command)
54 pub command: String,
55 /// Behavior on hook failure (default: warn)
56 #[serde(default)]
57 pub on_fail: OnFail,
58 /// Timeout in seconds (default: 60)
59 #[serde(default = "default_timeout")]
60 pub timeout: u64,
61}
62
63fn default_timeout() -> u64 {
64 DEFAULT_HOOK_TIMEOUT_SECS
65}
66
67/// Hooks configuration section from config.toml
68#[derive(Debug, Clone, Default, Deserialize, Serialize)]
69pub struct HooksConfig {
70 /// Pre-phase hooks keyed by phase name
71 #[serde(default)]
72 pub pre_phase: HashMap<String, HookConfig>,
73 /// Post-phase hooks keyed by phase name
74 #[serde(default)]
75 pub post_phase: HashMap<String, HookConfig>,
76}
77
78impl HooksConfig {
79 /// Get a pre-phase hook for the given phase
80 /// Reserved for hooks integration; not wired in v1.0
81 #[must_use]
82 #[cfg_attr(not(test), allow(dead_code))]
83 pub fn get_pre_phase_hook(&self, phase: crate::types::PhaseId) -> Option<&HookConfig> {
84 self.pre_phase.get(phase.as_str())
85 }
86
87 /// Get a post-phase hook for the given phase
88 /// Reserved for hooks integration; not wired in v1.0
89 #[must_use]
90 #[cfg_attr(not(test), allow(dead_code))]
91 pub fn get_post_phase_hook(&self, phase: crate::types::PhaseId) -> Option<&HookConfig> {
92 self.post_phase.get(phase.as_str())
93 }
94
95 /// Check if any hooks are configured
96 /// Reserved for hooks integration; not wired in v1.0
97 #[must_use]
98 #[cfg_attr(not(test), allow(dead_code))]
99 pub fn has_hooks(&self) -> bool {
100 !self.pre_phase.is_empty() || !self.post_phase.is_empty()
101 }
102}
103
104/// Configuration for xchecker operations.
105///
106/// `Config` provides hierarchical configuration with discovery and precedence:
107/// CLI arguments > config file > built-in defaults.
108///
109/// # Discovery
110///
111/// Use [`Config::discover()`] for CLI-like behavior that:
112/// - Searches for `.xchecker/config.toml` upward from current directory
113/// - Respects the `XCHECKER_HOME` environment variable
114/// - Applies built-in defaults for unspecified values
115///
116/// # Programmatic Configuration
117///
118/// For embedding scenarios where you need deterministic behavior independent
119/// of the user's environment, construct a `Config` directly or use
120/// `xchecker::OrchestratorHandle::from_config()`.
121///
122/// # Source Attribution
123///
124/// Each configuration value tracks its source (`cli`, `config`, `programmatic`, or `default`)
125/// for debugging and status display.
126///
127/// # Example
128///
129/// ```rust,no_run
130/// use xchecker_config::Config;
131/// use xchecker_config::CliArgs;
132///
133/// // Discover configuration using CLI semantics
134/// let config = Config::discover(&CliArgs::default())?;
135///
136/// // Access configuration values
137/// println!("Model: {:?}", config.defaults.model);
138/// println!("Max turns: {:?}", config.defaults.max_turns);
139/// # Ok::<(), Box<dyn std::error::Error>>(())
140/// ```
141///
142/// # Configuration File Format
143///
144/// Configuration files use TOML format with these sections:
145///
146/// ```toml
147/// [defaults]
148/// model = "haiku"
149/// max_turns = 6
150/// phase_timeout = 600
151///
152/// [selectors]
153/// include = ["**/*.md", "**/*.yaml"]
154/// exclude = ["target/**", "node_modules/**"]
155///
156/// [runner]
157/// mode = "auto"
158///
159/// [llm]
160/// provider = "claude-cli"
161/// ```
162#[derive(Debug, Clone)]
163pub struct Config {
164 /// Default values for various settings.
165 pub defaults: Defaults,
166 /// File selection patterns for packet building.
167 pub selectors: Selectors,
168 /// Runner configuration for cross-platform execution.
169 pub runner: RunnerConfig,
170 /// LLM provider configuration.
171 pub llm: LlmConfig,
172 /// Per-phase configuration overrides.
173 pub phases: PhasesConfig,
174 /// Hooks configuration for pre/post phase scripts.
175 // Reserved for hooks integration; not wired in v1.0
176 #[allow(dead_code)]
177 pub hooks: HooksConfig,
178 /// Security configuration for secret detection and redaction.
179 pub security: SecurityConfig,
180 /// Source attribution for each setting (for status display).
181 pub source_attribution: HashMap<String, ConfigSource>,
182}
183
184/// Default configuration values
185///
186/// # Model selection
187///
188/// - **Testing/Development**: Leave `model` unset to use `haiku` (fast, cost-effective)
189/// - **Production**: Set `model = "sonnet"` or `model = "default"` for best results
190/// - **Complex tasks**: Set `model = "opus"` for maximum capability
191///
192/// Specific model versions (e.g., `claude-sonnet-4-5-20250929`) can be used for
193/// reproducibility but simple aliases are recommended.
194#[derive(Debug, Clone, Deserialize, Serialize)]
195pub struct Defaults {
196 /// Model to use. Default: haiku (for testing). Use "sonnet" or "default" for production.
197 pub model: Option<String>,
198 pub max_turns: Option<u32>,
199 pub packet_max_bytes: Option<usize>,
200 pub packet_max_lines: Option<usize>,
201 pub output_format: Option<String>,
202 pub verbose: Option<bool>,
203 pub phase_timeout: Option<u64>,
204 pub stdout_cap_bytes: Option<usize>,
205 pub stderr_cap_bytes: Option<usize>,
206 pub lock_ttl_seconds: Option<u64>,
207 pub debug_packet: Option<bool>,
208 pub allow_links: Option<bool>,
209 /// Enable strict validation for phase outputs.
210 ///
211 /// When enabled, validation failures (meta-summaries, too-short output,
212 /// missing required sections) become hard errors that fail the phase.
213 /// When disabled (default), validation issues are logged as warnings only.
214 pub strict_validation: Option<bool>,
215}
216
217/// LLM provider configuration
218#[derive(Debug, Clone, Deserialize, Serialize)]
219pub struct LlmConfig {
220 pub provider: Option<String>,
221 pub fallback_provider: Option<String>,
222 pub claude: Option<ClaudeConfig>,
223 pub gemini: Option<GeminiConfig>,
224 pub openrouter: Option<OpenRouterConfig>,
225 pub anthropic: Option<AnthropicConfig>,
226 pub execution_strategy: Option<String>,
227 /// Prompt template to use for LLM interactions
228 ///
229 /// Available templates:
230 /// - "default": Universal template compatible with all providers
231 /// - "claude-optimized": Optimized for Claude CLI and Anthropic API
232 /// - "openai-compatible": Optimized for OpenRouter and OpenAI-compatible APIs
233 ///
234 /// If not specified, defaults to "default" which works with all providers.
235 pub prompt_template: Option<String>,
236}
237
238/// Claude CLI provider configuration
239#[derive(Debug, Clone, Deserialize, Serialize)]
240pub struct ClaudeConfig {
241 pub binary: Option<String>,
242}
243
244/// Gemini CLI provider configuration
245#[derive(Debug, Clone, Deserialize, Serialize)]
246pub struct GeminiConfig {
247 pub binary: Option<String>,
248 pub default_model: Option<String>,
249 pub profiles: Option<HashMap<String, GeminiProfileConfig>>,
250}
251
252/// Gemini profile configuration for per-phase model selection
253#[derive(Debug, Clone, Deserialize, Serialize)]
254pub struct GeminiProfileConfig {
255 pub model: Option<String>,
256 pub max_tokens: Option<u32>,
257}
258
259/// OpenRouter HTTP provider configuration
260#[derive(Debug, Clone, Deserialize, Serialize)]
261pub struct OpenRouterConfig {
262 pub api_key_env: Option<String>,
263 pub base_url: Option<String>,
264 pub model: Option<String>,
265 pub max_tokens: Option<u32>,
266 pub temperature: Option<f32>,
267 pub budget: Option<u32>,
268}
269
270/// Anthropic HTTP provider configuration
271#[derive(Debug, Clone, Deserialize, Serialize)]
272pub struct AnthropicConfig {
273 pub api_key_env: Option<String>,
274 pub base_url: Option<String>,
275 pub model: Option<String>,
276 pub max_tokens: Option<u32>,
277 pub temperature: Option<f32>,
278}
279
280/// Per-phase configuration overrides
281///
282/// Allows configuring model, timeout, and max_turns on a per-phase basis.
283/// Values set here override global defaults for that specific phase.
284#[derive(Debug, Clone, Deserialize, Serialize, Default)]
285pub struct PhaseConfig {
286 /// Model to use for this phase (overrides defaults.model)
287 pub model: Option<String>,
288 /// Maximum turns for this phase (overrides defaults.max_turns)
289 pub max_turns: Option<u32>,
290 /// Phase timeout in seconds (overrides defaults.phase_timeout)
291 pub phase_timeout: Option<u64>,
292}
293
294/// Phase-specific configuration section
295///
296/// Contains optional per-phase configuration overrides.
297/// If a phase is not specified or None, global defaults are used.
298///
299/// # Example
300///
301/// ```toml
302/// [defaults]
303/// model = "haiku"
304///
305/// [phases.design]
306/// model = "sonnet"
307///
308/// [phases.tasks]
309/// model = "sonnet"
310/// ```
311#[derive(Debug, Clone, Deserialize, Serialize, Default)]
312pub struct PhasesConfig {
313 pub requirements: Option<PhaseConfig>,
314 pub design: Option<PhaseConfig>,
315 pub tasks: Option<PhaseConfig>,
316 pub review: Option<PhaseConfig>,
317 pub fixup: Option<PhaseConfig>,
318 #[serde(rename = "final")]
319 pub final_: Option<PhaseConfig>,
320}
321
322/// Runner configuration for cross-platform execution
323#[derive(Debug, Clone, Deserialize, Serialize)]
324pub struct RunnerConfig {
325 pub mode: Option<String>,
326 pub distro: Option<String>,
327 pub claude_path: Option<String>,
328}
329
330/// Security configuration for secret detection and redaction
331///
332/// This section allows customizing secret detection patterns:
333/// - Add extra patterns to detect project-specific secrets
334/// - Ignore patterns that cause false positives
335///
336/// # Example
337///
338/// ```toml
339/// [security]
340/// extra_secret_patterns = ["SECRET_[A-Z0-9]{32}", "API_KEY_[A-Za-z0-9]{40}"]
341/// ignore_secret_patterns = ["github_pat"]
342/// ```
343#[derive(Debug, Clone, Deserialize, Serialize, Default)]
344pub struct SecurityConfig {
345 /// Additional regex patterns for secret detection.
346 ///
347 /// These patterns are added to the built-in patterns and will cause
348 /// secret detection to trigger if matched.
349 #[serde(default)]
350 pub extra_secret_patterns: Vec<String>,
351
352 /// Patterns to suppress from secret detection.
353 ///
354 /// Pattern IDs listed here will be ignored during secret scanning.
355 /// Use this to suppress false positives for known-safe patterns.
356 ///
357 /// **Warning:** Suppressing patterns reduces security. Only suppress
358 /// patterns if you're certain they won't match real secrets.
359 #[serde(default)]
360 pub ignore_secret_patterns: Vec<String>,
361}
362
363impl Default for Defaults {
364 fn default() -> Self {
365 Self {
366 model: None,
367 max_turns: Some(6),
368 packet_max_bytes: Some(65536),
369 packet_max_lines: Some(1200),
370 output_format: Some("stream-json".to_string()),
371 verbose: Some(false),
372 phase_timeout: Some(600), // 600 seconds = 10 minutes
373 stdout_cap_bytes: Some(2097152), // 2 MiB
374 stderr_cap_bytes: Some(262144), // 256 KiB
375 lock_ttl_seconds: Some(900), // 15 minutes
376 debug_packet: Some(false),
377 allow_links: Some(false),
378 strict_validation: None, // Default: soft validation (warnings only)
379 }
380 }
381}
382
383impl Default for RunnerConfig {
384 fn default() -> Self {
385 Self {
386 mode: Some("auto".to_string()),
387 distro: None,
388 claude_path: None,
389 }
390 }
391}