1use 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")]
14pub struct ItoConfig {
16 #[serde(default, rename = "$schema", skip_serializing_if = "Option::is_none")]
17 #[schemars(description = "Optional JSON schema reference for editor validation")]
18 pub schema: Option<String>,
20
21 #[serde(default, rename = "projectPath")]
22 #[schemars(description = "Ito working directory name (defaults to .ito)")]
23 pub project_path: Option<String>,
25
26 #[serde(default)]
27 #[schemars(default, description = "Harness-specific configuration")]
28 pub harnesses: HarnessesConfig,
30
31 #[serde(default)]
32 #[schemars(default, description = "Cache configuration")]
33 pub cache: CacheConfig,
35
36 #[serde(default)]
37 #[schemars(default, description = "Global defaults for workflow and tooling")]
38 pub defaults: DefaultsConfig,
40
41 #[serde(default)]
42 #[schemars(default, description = "Worktree workspace configuration")]
43 pub worktrees: WorktreesConfig,
45
46 #[serde(default)]
47 #[schemars(default, description = "Change coordination configuration")]
48 pub changes: ChangesConfig,
50}
51
52#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
53#[schemars(description = "Change coordination settings")]
54pub struct ChangesConfig {
56 #[serde(default)]
57 #[schemars(default, description = "Coordination branch settings")]
58 pub coordination_branch: CoordinationBranchConfig,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63#[schemars(description = "Dedicated branch used for proposal/task coordination")]
64pub struct CoordinationBranchConfig {
66 #[serde(default = "CoordinationBranchConfig::default_enabled")]
67 #[schemars(
68 default = "CoordinationBranchConfig::default_enabled",
69 description = "Enable change coordination branch synchronization"
70 )]
71 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 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")]
86pub 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")]
110pub 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 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")]
137pub struct HarnessesConfig {
139 #[serde(default, rename = "opencode")]
140 #[schemars(default, description = "OpenCode harness settings")]
141 pub opencode: OpenCodeHarnessConfig,
143
144 #[serde(default, rename = "claude-code")]
145 #[schemars(default, description = "Claude Code harness settings")]
146 pub claude_code: ClaudeCodeHarnessConfig,
148
149 #[serde(default, rename = "codex")]
150 #[schemars(default, description = "OpenAI Codex harness settings")]
151 pub codex: CodexHarnessConfig,
153
154 #[serde(default, rename = "github-copilot")]
155 #[schemars(default, description = "GitHub Copilot harness settings")]
156 pub github_copilot: GitHubCopilotHarnessConfig,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161#[schemars(description = "OpenCode harness configuration")]
162pub struct OpenCodeHarnessConfig {
164 #[serde(default)]
165 #[schemars(description = "Optional provider constraint (null/omitted means any provider)")]
166 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 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")]
211pub struct ClaudeCodeHarnessConfig {
213 #[serde(default)]
214 #[schemars(description = "Provider constraint (if specified, must be anthropic)")]
215 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 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")]
250pub struct CodexHarnessConfig {
252 #[serde(default)]
253 #[schemars(description = "Provider constraint (if specified, must be openai)")]
254 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 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")]
297pub struct GitHubCopilotHarnessConfig {
299 #[serde(default)]
300 #[schemars(description = "Provider constraint (if specified, must be github-copilot)")]
301 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 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")]
336pub enum ProviderAnthropic {
338 #[serde(rename = "anthropic")]
339 Anthropic,
341}
342
343#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
344#[serde(rename_all = "kebab-case")]
345pub enum ProviderOpenAi {
347 #[serde(rename = "openai")]
348 OpenAi,
350}
351
352#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
353#[serde(rename_all = "kebab-case")]
354pub enum ProviderGitHubCopilot {
356 #[serde(rename = "github-copilot")]
357 GitHubCopilot,
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
362#[schemars(description = "Agent tier to model mapping")]
363pub struct AgentTiersConfig {
365 #[serde(rename = "ito-quick")]
366 #[schemars(description = "Fast, cheap tier")]
367 pub ito_quick: AgentModelSetting,
369
370 #[serde(rename = "ito-general")]
371 #[schemars(description = "Balanced tier")]
372 pub ito_general: AgentModelSetting,
374
375 #[serde(rename = "ito-thinking")]
376 #[schemars(description = "High-capability tier")]
377 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")]
396pub enum AgentModelSetting {
401 Model(String),
403 Options(AgentModelOptions),
405}
406
407#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
408#[schemars(description = "Extended agent model options")]
409pub struct AgentModelOptions {
411 #[schemars(
412 description = "Model identifier",
413 example = "AgentModelOptions::example_model"
414 )]
415 pub model: String,
417
418 #[serde(default)]
419 #[schemars(description = "Temperature (0.0-1.0)", range(min = 0.0, max = 1.0))]
420 pub temperature: Option<f64>,
422
423 #[serde(default)]
424 #[schemars(description = "Optional variant selector (OpenCode)")]
425 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 pub top_p: Option<f64>,
432
433 #[serde(default)]
434 #[schemars(description = "Optional max steps for tool loops")]
435 pub steps: Option<u64>,
437
438 #[serde(default, rename = "reasoningEffort")]
439 #[schemars(description = "Reasoning effort (OpenAI)")]
440 pub reasoning_effort: Option<ReasoningEffort>,
442
443 #[serde(default, rename = "textVerbosity")]
444 #[schemars(description = "Text verbosity")]
445 pub text_verbosity: Option<TextVerbosity>,
447
448 #[serde(flatten, default)]
449 #[schemars(description = "Additional provider-specific options")]
450 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")]
462pub enum TextVerbosity {
464 Low,
466 Medium,
468 High,
470}
471
472#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
473#[serde(rename_all = "lowercase")]
474pub enum ReasoningEffort {
476 None,
478 Minimal,
480 Low,
482 Medium,
484 High,
486 #[serde(rename = "xhigh")]
487 XHigh,
489}
490
491#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
492#[schemars(description = "Defaults section")]
493pub struct DefaultsConfig {
495 #[serde(default)]
496 #[schemars(default, description = "Testing-related defaults")]
497 pub testing: TestingDefaults,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
502#[schemars(description = "Worktree workspace configuration")]
503pub struct WorktreesConfig {
505 #[serde(default)]
506 #[schemars(default, description = "Enable worktree policy features")]
507 pub enabled: bool,
509
510 #[serde(default = "WorktreesConfig::default_strategy")]
511 #[schemars(
512 default = "WorktreesConfig::default_strategy",
513 description = "Workspace topology strategy"
514 )]
515 pub strategy: WorktreeStrategy,
517
518 #[serde(default)]
519 #[schemars(default, description = "Layout path configuration")]
520 pub layout: WorktreeLayoutConfig,
522
523 #[serde(default)]
524 #[schemars(default, description = "Apply-time behavior configuration")]
525 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 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")]
562pub enum WorktreeStrategy {
564 CheckoutSubdir,
566 CheckoutSiblings,
568 BareControlSiblings,
570}
571
572impl WorktreeStrategy {
573 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 pub const ALL: &'static [&'static str] = &[
584 "checkout_subdir",
585 "checkout_siblings",
586 "bare_control_siblings",
587 ];
588
589 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")]
608pub struct WorktreeLayoutConfig {
610 #[serde(default, skip_serializing_if = "Option::is_none")]
611 #[schemars(description = "Base path override for worktree directory placement")]
612 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 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")]
641pub 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 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 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 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 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")]
708pub enum IntegrationMode {
710 CommitPr,
712 MergeParent,
714}
715
716impl IntegrationMode {
717 pub fn as_str(self) -> &'static str {
719 match self {
720 IntegrationMode::CommitPr => "commit_pr",
721 IntegrationMode::MergeParent => "merge_parent",
722 }
723 }
724
725 pub const ALL: &'static [&'static str] = &["commit_pr", "merge_parent"];
727
728 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")]
746pub struct TestingDefaults {
748 #[serde(default)]
749 #[schemars(default, description = "TDD workflow defaults")]
750 pub tdd: TddDefaults,
752
753 #[serde(default)]
754 #[schemars(default, description = "Coverage defaults")]
755 pub coverage: CoverageDefaults,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
760#[schemars(description = "TDD defaults")]
761pub struct TddDefaults {
763 #[serde(default)]
764 #[schemars(
765 default = "TddDefaults::default_workflow",
766 description = "TDD workflow name"
767 )]
768 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")]
788pub struct CoverageDefaults {
790 #[serde(default, rename = "target_percent")]
791 #[schemars(
792 default = "CoverageDefaults::default_target_percent",
793 description = "Target coverage percentage"
794 )]
795 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}