ito_config/config/
types.rs

1//! Serde models for Ito configuration.
2//!
3//! These types are deserialized from `config.json` (and merged across multiple
4//! layers) and also used to generate JSON schema for editor validation.
5
6use std::collections::BTreeMap;
7
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
13#[schemars(description = "Top-level Ito configuration")]
14/// Top-level Ito configuration object.
15pub struct ItoConfig {
16    #[serde(default, rename = "$schema", skip_serializing_if = "Option::is_none")]
17    #[schemars(description = "Optional JSON schema reference for editor validation")]
18    /// Optional `$schema` reference used by editors.
19    pub schema: Option<String>,
20
21    #[serde(default, rename = "projectPath")]
22    #[schemars(description = "Ito working directory name (defaults to .ito)")]
23    /// Override the Ito working directory name (defaults to `.ito`).
24    pub project_path: Option<String>,
25
26    #[serde(default)]
27    #[schemars(default, description = "Harness-specific configuration")]
28    /// Harness-specific configuration.
29    pub harnesses: HarnessesConfig,
30
31    #[serde(default)]
32    #[schemars(default, description = "Cache configuration")]
33    /// Cache configuration.
34    pub cache: CacheConfig,
35
36    #[serde(default)]
37    #[schemars(default, description = "Global defaults for workflow and tooling")]
38    /// Defaults for workflows and tooling.
39    pub defaults: DefaultsConfig,
40
41    #[serde(default)]
42    #[schemars(default, description = "Worktree workspace configuration")]
43    /// Worktree workspace configuration.
44    pub worktrees: WorktreesConfig,
45
46    #[serde(default)]
47    #[schemars(default, description = "Change coordination configuration")]
48    /// Change coordination configuration.
49    pub changes: ChangesConfig,
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
53#[schemars(description = "Change coordination settings")]
54/// Configuration for change coordination behavior.
55pub struct ChangesConfig {
56    #[serde(default)]
57    #[schemars(default, description = "Coordination branch settings")]
58    /// Coordination branch settings.
59    pub coordination_branch: CoordinationBranchConfig,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63#[schemars(description = "Dedicated branch used for proposal/task coordination")]
64/// Configuration for the internal change coordination branch.
65pub struct CoordinationBranchConfig {
66    #[serde(default = "CoordinationBranchConfig::default_enabled")]
67    #[schemars(
68        default = "CoordinationBranchConfig::default_enabled",
69        description = "Enable change coordination branch synchronization"
70    )]
71    /// Enable change coordination branch synchronization.
72    pub enabled: CoordinationBranchEnabled,
73
74    #[serde(default = "CoordinationBranchConfig::default_name")]
75    #[schemars(
76        default = "CoordinationBranchConfig::default_name",
77        description = "Name of the internal coordination branch"
78    )]
79    /// Name of the internal coordination branch.
80    pub name: String,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
84#[serde(transparent)]
85#[schemars(description = "Boolean wrapper for coordination branch enablement")]
86/// Type-safe wrapper for `coordination_branch.enabled`.
87pub struct CoordinationBranchEnabled(pub bool);
88
89impl CoordinationBranchConfig {
90    fn default_enabled() -> CoordinationBranchEnabled {
91        CoordinationBranchEnabled(true)
92    }
93
94    fn default_name() -> String {
95        "ito/internal/changes".to_string()
96    }
97}
98
99impl Default for CoordinationBranchConfig {
100    fn default() -> Self {
101        Self {
102            enabled: Self::default_enabled(),
103            name: Self::default_name(),
104        }
105    }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
109#[schemars(description = "Cache settings")]
110/// Cache configuration for Ito data.
111pub struct CacheConfig {
112    #[serde(default, rename = "ttl_hours")]
113    #[schemars(
114        default = "CacheConfig::default_ttl_hours",
115        description = "Model registry cache TTL in hours"
116    )]
117    /// Model registry cache TTL in hours.
118    pub ttl_hours: u64,
119}
120
121impl CacheConfig {
122    fn default_ttl_hours() -> u64 {
123        24
124    }
125}
126
127impl Default for CacheConfig {
128    fn default() -> Self {
129        Self {
130            ttl_hours: Self::default_ttl_hours(),
131        }
132    }
133}
134
135#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
136#[schemars(description = "Harness configurations")]
137/// Configuration grouped by harness.
138pub struct HarnessesConfig {
139    #[serde(default, rename = "opencode")]
140    #[schemars(default, description = "OpenCode harness settings")]
141    /// OpenCode harness settings.
142    pub opencode: OpenCodeHarnessConfig,
143
144    #[serde(default, rename = "claude-code")]
145    #[schemars(default, description = "Claude Code harness settings")]
146    /// Claude Code harness settings.
147    pub claude_code: ClaudeCodeHarnessConfig,
148
149    #[serde(default, rename = "codex")]
150    #[schemars(default, description = "OpenAI Codex harness settings")]
151    /// OpenAI Codex harness settings.
152    pub codex: CodexHarnessConfig,
153
154    #[serde(default, rename = "github-copilot")]
155    #[schemars(default, description = "GitHub Copilot harness settings")]
156    /// GitHub Copilot harness settings.
157    pub github_copilot: GitHubCopilotHarnessConfig,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161#[schemars(description = "OpenCode harness configuration")]
162/// Configuration for the OpenCode harness.
163pub struct OpenCodeHarnessConfig {
164    #[serde(default)]
165    #[schemars(description = "Optional provider constraint (null/omitted means any provider)")]
166    /// Optional provider constraint.
167    ///
168    /// When omitted, any provider is accepted.
169    pub provider: Option<String>,
170
171    #[serde(default = "OpenCodeHarnessConfig::default_agents")]
172    #[schemars(
173        default = "OpenCodeHarnessConfig::default_agents",
174        description = "Ito agent tier model mappings"
175    )]
176    /// Ito agent tier model mappings.
177    pub agents: AgentTiersConfig,
178}
179
180impl Default for OpenCodeHarnessConfig {
181    fn default() -> Self {
182        Self {
183            provider: None,
184            agents: Self::default_agents(),
185        }
186    }
187}
188
189impl OpenCodeHarnessConfig {
190    fn default_agents() -> AgentTiersConfig {
191        AgentTiersConfig {
192            ito_quick: AgentModelSetting::Model("anthropic/claude-haiku-4-5".to_string()),
193            ito_general: AgentModelSetting::Options(AgentModelOptions {
194                model: "openai/gpt-5.2-codex".to_string(),
195                variant: Some("high".to_string()),
196                temperature: Some(0.3),
197                ..AgentModelOptions::default()
198            }),
199            ito_thinking: AgentModelSetting::Options(AgentModelOptions {
200                model: "openai/gpt-5.2-codex".to_string(),
201                variant: Some("xhigh".to_string()),
202                temperature: Some(0.5),
203                ..AgentModelOptions::default()
204            }),
205        }
206    }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
210#[schemars(description = "Claude Code harness configuration")]
211/// Configuration for the Claude Code harness.
212pub struct ClaudeCodeHarnessConfig {
213    #[serde(default)]
214    #[schemars(description = "Provider constraint (if specified, must be anthropic)")]
215    /// Provider constraint.
216    ///
217    /// If specified, must be `anthropic`.
218    pub provider: Option<ProviderAnthropic>,
219
220    #[serde(default = "ClaudeCodeHarnessConfig::default_agents")]
221    #[schemars(
222        default = "ClaudeCodeHarnessConfig::default_agents",
223        description = "Ito agent tier model mappings"
224    )]
225    /// Ito agent tier model mappings.
226    pub agents: AgentTiersConfig,
227}
228
229impl Default for ClaudeCodeHarnessConfig {
230    fn default() -> Self {
231        Self {
232            provider: Some(ProviderAnthropic::Anthropic),
233            agents: Self::default_agents(),
234        }
235    }
236}
237
238impl ClaudeCodeHarnessConfig {
239    fn default_agents() -> AgentTiersConfig {
240        AgentTiersConfig {
241            ito_quick: AgentModelSetting::Model("haiku".to_string()),
242            ito_general: AgentModelSetting::Model("sonnet".to_string()),
243            ito_thinking: AgentModelSetting::Model("opus".to_string()),
244        }
245    }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
249#[schemars(description = "Codex harness configuration")]
250/// Configuration for the OpenAI Codex harness.
251pub struct CodexHarnessConfig {
252    #[serde(default)]
253    #[schemars(description = "Provider constraint (if specified, must be openai)")]
254    /// Provider constraint.
255    ///
256    /// If specified, must be `openai`.
257    pub provider: Option<ProviderOpenAi>,
258
259    #[serde(default = "CodexHarnessConfig::default_agents")]
260    #[schemars(
261        default = "CodexHarnessConfig::default_agents",
262        description = "Ito agent tier model mappings"
263    )]
264    /// Ito agent tier model mappings.
265    pub agents: AgentTiersConfig,
266}
267
268impl Default for CodexHarnessConfig {
269    fn default() -> Self {
270        Self {
271            provider: Some(ProviderOpenAi::OpenAi),
272            agents: Self::default_agents(),
273        }
274    }
275}
276
277impl CodexHarnessConfig {
278    fn default_agents() -> AgentTiersConfig {
279        AgentTiersConfig {
280            ito_quick: AgentModelSetting::Model("openai/gpt-5.1-codex-mini".to_string()),
281            ito_general: AgentModelSetting::Options(AgentModelOptions {
282                model: "openai/gpt-5.2-codex".to_string(),
283                reasoning_effort: Some(ReasoningEffort::High),
284                ..AgentModelOptions::default()
285            }),
286            ito_thinking: AgentModelSetting::Options(AgentModelOptions {
287                model: "openai/gpt-5.2-codex".to_string(),
288                reasoning_effort: Some(ReasoningEffort::XHigh),
289                ..AgentModelOptions::default()
290            }),
291        }
292    }
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296#[schemars(description = "GitHub Copilot harness configuration")]
297/// Configuration for the GitHub Copilot harness.
298pub struct GitHubCopilotHarnessConfig {
299    #[serde(default)]
300    #[schemars(description = "Provider constraint (if specified, must be github-copilot)")]
301    /// Provider constraint.
302    ///
303    /// If specified, must be `github-copilot`.
304    pub provider: Option<ProviderGitHubCopilot>,
305
306    #[serde(default = "GitHubCopilotHarnessConfig::default_agents")]
307    #[schemars(
308        default = "GitHubCopilotHarnessConfig::default_agents",
309        description = "Ito agent tier model mappings"
310    )]
311    /// Ito agent tier model mappings.
312    pub agents: AgentTiersConfig,
313}
314
315impl Default for GitHubCopilotHarnessConfig {
316    fn default() -> Self {
317        Self {
318            provider: Some(ProviderGitHubCopilot::GitHubCopilot),
319            agents: Self::default_agents(),
320        }
321    }
322}
323
324impl GitHubCopilotHarnessConfig {
325    fn default_agents() -> AgentTiersConfig {
326        AgentTiersConfig {
327            ito_quick: AgentModelSetting::Model("github-copilot/claude-haiku-4.5".to_string()),
328            ito_general: AgentModelSetting::Model("github-copilot/gpt-5.2-codex".to_string()),
329            ito_thinking: AgentModelSetting::Model("github-copilot/gpt-5.2-codex".to_string()),
330        }
331    }
332}
333
334#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
335#[serde(rename_all = "kebab-case")]
336/// Allowed providers for Anthropic-backed harnesses.
337pub enum ProviderAnthropic {
338    #[serde(rename = "anthropic")]
339    /// Anthropic provider.
340    Anthropic,
341}
342
343#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
344#[serde(rename_all = "kebab-case")]
345/// Allowed providers for OpenAI-backed harnesses.
346pub enum ProviderOpenAi {
347    #[serde(rename = "openai")]
348    /// OpenAI provider.
349    OpenAi,
350}
351
352#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
353#[serde(rename_all = "kebab-case")]
354/// Allowed providers for GitHub Copilot-backed harnesses.
355pub enum ProviderGitHubCopilot {
356    #[serde(rename = "github-copilot")]
357    /// GitHub Copilot provider.
358    GitHubCopilot,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
362#[schemars(description = "Agent tier to model mapping")]
363/// Mapping from Ito agent tiers to model settings.
364pub struct AgentTiersConfig {
365    #[serde(rename = "ito-quick")]
366    #[schemars(description = "Fast, cheap tier")]
367    /// Fast, cheap tier.
368    pub ito_quick: AgentModelSetting,
369
370    #[serde(rename = "ito-general")]
371    #[schemars(description = "Balanced tier")]
372    /// Balanced tier.
373    pub ito_general: AgentModelSetting,
374
375    #[serde(rename = "ito-thinking")]
376    #[schemars(description = "High-capability tier")]
377    /// High-capability tier.
378    pub ito_thinking: AgentModelSetting,
379}
380
381impl Default for AgentTiersConfig {
382    fn default() -> Self {
383        let empty = AgentModelSetting::Model(String::new());
384
385        Self {
386            ito_quick: empty.clone(),
387            ito_general: empty.clone(),
388            ito_thinking: empty,
389        }
390    }
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
394#[serde(untagged)]
395#[schemars(description = "Agent model setting: shorthand string or options object")]
396/// Agent model setting.
397///
398/// In JSON, this can be either a shorthand string (model id) or a richer
399/// options object.
400pub enum AgentModelSetting {
401    /// Shorthand setting using only a model identifier.
402    Model(String),
403    /// Extended options object.
404    Options(AgentModelOptions),
405}
406
407#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
408#[schemars(description = "Extended agent model options")]
409/// Extended options for a configured model.
410pub struct AgentModelOptions {
411    #[schemars(
412        description = "Model identifier",
413        example = "AgentModelOptions::example_model"
414    )]
415    /// Model identifier.
416    pub model: String,
417
418    #[serde(default)]
419    #[schemars(description = "Temperature (0.0-1.0)", range(min = 0.0, max = 1.0))]
420    /// Temperature (0.0-1.0).
421    pub temperature: Option<f64>,
422
423    #[serde(default)]
424    #[schemars(description = "Optional variant selector (OpenCode)")]
425    /// Optional variant selector (OpenCode).
426    pub variant: Option<String>,
427
428    #[serde(default, rename = "top_p")]
429    #[schemars(description = "Top-p sampling (0.0-1.0)", range(min = 0.0, max = 1.0))]
430    /// Top-p sampling (0.0-1.0).
431    pub top_p: Option<f64>,
432
433    #[serde(default)]
434    #[schemars(description = "Optional max steps for tool loops")]
435    /// Optional max steps for tool loops.
436    pub steps: Option<u64>,
437
438    #[serde(default, rename = "reasoningEffort")]
439    #[schemars(description = "Reasoning effort (OpenAI)")]
440    /// Reasoning effort (OpenAI).
441    pub reasoning_effort: Option<ReasoningEffort>,
442
443    #[serde(default, rename = "textVerbosity")]
444    #[schemars(description = "Text verbosity")]
445    /// Text verbosity.
446    pub text_verbosity: Option<TextVerbosity>,
447
448    #[serde(flatten, default)]
449    #[schemars(description = "Additional provider-specific options")]
450    /// Additional provider-specific options.
451    pub extra: BTreeMap<String, Value>,
452}
453
454impl AgentModelOptions {
455    fn example_model() -> &'static str {
456        "openai/gpt-5.2-codex"
457    }
458}
459
460#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
461#[serde(rename_all = "lowercase")]
462/// Preferred verbosity level for text output.
463pub enum TextVerbosity {
464    /// Minimal output.
465    Low,
466    /// Balanced output.
467    Medium,
468    /// Very detailed output.
469    High,
470}
471
472#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
473#[serde(rename_all = "lowercase")]
474/// Preferred reasoning effort for reasoning-capable models.
475pub enum ReasoningEffort {
476    /// No explicit reasoning mode.
477    None,
478    /// Minimal reasoning.
479    Minimal,
480    /// Low reasoning.
481    Low,
482    /// Medium reasoning.
483    Medium,
484    /// High reasoning.
485    High,
486    #[serde(rename = "xhigh")]
487    /// Extra-high reasoning.
488    XHigh,
489}
490
491#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
492#[schemars(description = "Defaults section")]
493/// Defaults applied when a config value is not explicitly set.
494pub struct DefaultsConfig {
495    #[serde(default)]
496    #[schemars(default, description = "Testing-related defaults")]
497    /// Testing-related defaults.
498    pub testing: TestingDefaults,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
502#[schemars(description = "Worktree workspace configuration")]
503/// Configuration for Git worktree-based workspace layouts.
504pub struct WorktreesConfig {
505    #[serde(default)]
506    #[schemars(default, description = "Enable worktree policy features")]
507    /// Enable worktree policy features.
508    pub enabled: bool,
509
510    #[serde(default = "WorktreesConfig::default_strategy")]
511    #[schemars(
512        default = "WorktreesConfig::default_strategy",
513        description = "Workspace topology strategy"
514    )]
515    /// Workspace topology strategy.
516    pub strategy: WorktreeStrategy,
517
518    #[serde(default)]
519    #[schemars(default, description = "Layout path configuration")]
520    /// Layout path configuration.
521    pub layout: WorktreeLayoutConfig,
522
523    #[serde(default)]
524    #[schemars(default, description = "Apply-time behavior configuration")]
525    /// Apply-time behavior configuration.
526    pub apply: WorktreeApplyConfig,
527
528    #[serde(default = "WorktreesConfig::default_branch")]
529    #[schemars(
530        default = "WorktreesConfig::default_branch",
531        description = "Branch used when creating/reusing the base worktree"
532    )]
533    /// Branch used when creating/reusing the base worktree.
534    pub default_branch: String,
535}
536
537impl Default for WorktreesConfig {
538    fn default() -> Self {
539        Self {
540            enabled: false,
541            strategy: Self::default_strategy(),
542            layout: WorktreeLayoutConfig::default(),
543            apply: WorktreeApplyConfig::default(),
544            default_branch: Self::default_branch(),
545        }
546    }
547}
548
549impl WorktreesConfig {
550    fn default_strategy() -> WorktreeStrategy {
551        WorktreeStrategy::CheckoutSubdir
552    }
553
554    fn default_branch() -> String {
555        "main".to_string()
556    }
557}
558
559#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
560#[serde(rename_all = "snake_case")]
561#[schemars(description = "Supported worktree workspace topology strategies")]
562/// Supported worktree workspace topology strategies.
563pub enum WorktreeStrategy {
564    /// Standard checkout with change worktrees under a gitignored subdirectory.
565    CheckoutSubdir,
566    /// Standard checkout with change worktrees in a sibling directory.
567    CheckoutSiblings,
568    /// Bare/control repo with `main` as a worktree and change worktrees as siblings.
569    BareControlSiblings,
570}
571
572impl WorktreeStrategy {
573    /// Return a stable string identifier for display.
574    pub fn as_str(self) -> &'static str {
575        match self {
576            WorktreeStrategy::CheckoutSubdir => "checkout_subdir",
577            WorktreeStrategy::CheckoutSiblings => "checkout_siblings",
578            WorktreeStrategy::BareControlSiblings => "bare_control_siblings",
579        }
580    }
581
582    /// All supported strategy values.
583    pub const ALL: &'static [&'static str] = &[
584        "checkout_subdir",
585        "checkout_siblings",
586        "bare_control_siblings",
587    ];
588
589    /// Parse a string into a strategy, returning `None` for invalid values.
590    pub fn parse_value(s: &str) -> Option<WorktreeStrategy> {
591        match s {
592            "checkout_subdir" => Some(WorktreeStrategy::CheckoutSubdir),
593            "checkout_siblings" => Some(WorktreeStrategy::CheckoutSiblings),
594            "bare_control_siblings" => Some(WorktreeStrategy::BareControlSiblings),
595            _ => None,
596        }
597    }
598}
599
600impl std::fmt::Display for WorktreeStrategy {
601    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602        f.write_str(self.as_str())
603    }
604}
605
606#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
607#[schemars(description = "Worktree layout path configuration")]
608/// Configuration for worktree directory layout.
609pub struct WorktreeLayoutConfig {
610    #[serde(default, skip_serializing_if = "Option::is_none")]
611    #[schemars(description = "Base path override for worktree directory placement")]
612    /// Base path override for worktree directory placement.
613    pub base_dir: Option<String>,
614
615    #[serde(default = "WorktreeLayoutConfig::default_dir_name")]
616    #[schemars(
617        default = "WorktreeLayoutConfig::default_dir_name",
618        description = "Name of the directory that holds change worktrees"
619    )]
620    /// Name of the directory that holds change worktrees.
621    pub dir_name: String,
622}
623
624impl Default for WorktreeLayoutConfig {
625    fn default() -> Self {
626        Self {
627            base_dir: None,
628            dir_name: Self::default_dir_name(),
629        }
630    }
631}
632
633impl WorktreeLayoutConfig {
634    fn default_dir_name() -> String {
635        "ito-worktrees".to_string()
636    }
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
640#[schemars(description = "Worktree apply-time behavior configuration")]
641/// Configuration controlling worktree behavior during apply instructions.
642pub struct WorktreeApplyConfig {
643    #[serde(default = "WorktreeApplyConfig::default_enabled")]
644    #[schemars(
645        default = "WorktreeApplyConfig::default_enabled",
646        description = "Enable worktree-specific setup in apply instructions"
647    )]
648    /// Enable worktree-specific setup in apply instructions.
649    pub enabled: bool,
650
651    #[serde(default = "WorktreeApplyConfig::default_integration_mode")]
652    #[schemars(
653        default = "WorktreeApplyConfig::default_integration_mode",
654        description = "Integration preference after implementation"
655    )]
656    /// Integration preference after implementation.
657    pub integration_mode: IntegrationMode,
658
659    #[serde(default = "WorktreeApplyConfig::default_copy_from_main")]
660    #[schemars(
661        default = "WorktreeApplyConfig::default_copy_from_main",
662        description = "Glob patterns for files to copy from main into the change worktree"
663    )]
664    /// Glob patterns for files to copy from main into the change worktree.
665    pub copy_from_main: Vec<String>,
666
667    #[serde(default)]
668    #[schemars(
669        default,
670        description = "Ordered shell commands to run in the change worktree before implementation"
671    )]
672    /// Ordered shell commands to run in the change worktree before implementation.
673    pub setup_commands: Vec<String>,
674}
675
676impl Default for WorktreeApplyConfig {
677    fn default() -> Self {
678        Self {
679            enabled: Self::default_enabled(),
680            integration_mode: Self::default_integration_mode(),
681            copy_from_main: Self::default_copy_from_main(),
682            setup_commands: Vec::new(),
683        }
684    }
685}
686
687impl WorktreeApplyConfig {
688    fn default_enabled() -> bool {
689        true
690    }
691
692    fn default_integration_mode() -> IntegrationMode {
693        IntegrationMode::CommitPr
694    }
695
696    fn default_copy_from_main() -> Vec<String> {
697        vec![
698            ".env".to_string(),
699            ".envrc".to_string(),
700            ".mise.local.toml".to_string(),
701        ]
702    }
703}
704
705#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
706#[serde(rename_all = "snake_case")]
707#[schemars(description = "Integration mode after implementation")]
708/// Integration mode preference for post-implementation workflow.
709pub enum IntegrationMode {
710    /// Commit and open a PR workflow.
711    CommitPr,
712    /// Merge into parent branch workflow.
713    MergeParent,
714}
715
716impl IntegrationMode {
717    /// Return a stable string identifier for display.
718    pub fn as_str(self) -> &'static str {
719        match self {
720            IntegrationMode::CommitPr => "commit_pr",
721            IntegrationMode::MergeParent => "merge_parent",
722        }
723    }
724
725    /// All supported integration mode values.
726    pub const ALL: &'static [&'static str] = &["commit_pr", "merge_parent"];
727
728    /// Parse a string into an integration mode, returning `None` for invalid values.
729    pub fn parse_value(s: &str) -> Option<IntegrationMode> {
730        match s {
731            "commit_pr" => Some(IntegrationMode::CommitPr),
732            "merge_parent" => Some(IntegrationMode::MergeParent),
733            _ => None,
734        }
735    }
736}
737
738impl std::fmt::Display for IntegrationMode {
739    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
740        f.write_str(self.as_str())
741    }
742}
743
744#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
745#[schemars(description = "Testing defaults")]
746/// Defaults that affect testing helpers.
747pub struct TestingDefaults {
748    #[serde(default)]
749    #[schemars(default, description = "TDD workflow defaults")]
750    /// Test-driven development defaults.
751    pub tdd: TddDefaults,
752
753    #[serde(default)]
754    #[schemars(default, description = "Coverage defaults")]
755    /// Coverage defaults.
756    pub coverage: CoverageDefaults,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
760#[schemars(description = "TDD defaults")]
761/// Defaults for the TDD workflow runner.
762pub struct TddDefaults {
763    #[serde(default)]
764    #[schemars(
765        default = "TddDefaults::default_workflow",
766        description = "TDD workflow name"
767    )]
768    /// Default workflow name.
769    pub workflow: String,
770}
771
772impl TddDefaults {
773    fn default_workflow() -> String {
774        "red-green-refactor".to_string()
775    }
776}
777
778impl Default for TddDefaults {
779    fn default() -> Self {
780        Self {
781            workflow: Self::default_workflow(),
782        }
783    }
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
787#[schemars(description = "Coverage defaults")]
788/// Defaults for coverage tooling.
789pub struct CoverageDefaults {
790    #[serde(default, rename = "target_percent")]
791    #[schemars(
792        default = "CoverageDefaults::default_target_percent",
793        description = "Target coverage percentage"
794    )]
795    /// Target coverage percentage.
796    pub target_percent: u64,
797}
798
799impl CoverageDefaults {
800    fn default_target_percent() -> u64 {
801        80
802    }
803}
804
805impl Default for CoverageDefaults {
806    fn default() -> Self {
807        Self {
808            target_percent: Self::default_target_percent(),
809        }
810    }
811}