1use std::collections::HashMap;
4
5use serde::{Deserialize, Deserializer, de};
6
7use super::super::DEFAULT_EVENT_LOG_MAX_BYTES;
8
9#[derive(Debug, Clone)]
10pub struct TeamConfig {
11 pub name: String,
12 pub agent: Option<String>,
16 pub workflow_mode: WorkflowMode,
17 pub board: BoardConfig,
18 pub standup: StandupConfig,
19 pub automation: AutomationConfig,
20 pub automation_sender: Option<String>,
21 pub external_senders: Vec<String>,
24 pub orchestrator_pane: bool,
25 pub orchestrator_position: OrchestratorPosition,
26 pub layout: Option<LayoutConfig>,
27 pub workflow_policy: WorkflowPolicy,
28 pub cost: CostConfig,
29 pub grafana: GrafanaConfig,
30 pub use_shim: bool,
34 pub use_sdk_mode: bool,
38 pub auto_respawn_on_crash: bool,
43 pub shim_health_check_interval_secs: u64,
45 pub shim_health_timeout_secs: u64,
47 pub shim_shutdown_timeout_secs: u32,
49 pub shim_working_state_timeout_secs: u64,
54 pub pending_queue_max_age_secs: u64,
58 pub event_log_max_bytes: u64,
59 pub retro_min_duration_secs: u64,
60 pub roles: Vec<RoleDef>,
61}
62
63#[derive(Debug, Deserialize)]
64struct TeamConfigWire {
65 pub name: String,
66 #[serde(default)]
67 pub agent: Option<String>,
68 #[serde(default)]
69 pub workflow_mode: Option<WorkflowMode>,
70 #[serde(default)]
71 pub board: BoardConfig,
72 #[serde(default)]
73 pub standup: StandupConfig,
74 #[serde(default)]
75 pub automation: AutomationConfig,
76 #[serde(default)]
77 pub automation_sender: Option<String>,
78 #[serde(default)]
79 pub external_senders: Vec<String>,
80 #[serde(default)]
81 pub orchestrator_pane: Option<bool>,
82 #[serde(default)]
83 pub orchestrator_position: OrchestratorPosition,
84 #[serde(default)]
85 pub layout: Option<LayoutConfig>,
86 #[serde(default)]
87 pub workflow_policy: WorkflowPolicy,
88 #[serde(default)]
89 pub cost: CostConfig,
90 #[serde(default)]
91 pub grafana: GrafanaConfig,
92 #[serde(default)]
93 pub use_shim: bool,
94 #[serde(default = "default_use_sdk_mode")]
95 pub use_sdk_mode: bool,
96 #[serde(default = "default_auto_respawn_on_crash")]
97 pub auto_respawn_on_crash: bool,
98 #[serde(default = "default_shim_health_check_interval_secs")]
99 pub shim_health_check_interval_secs: u64,
100 #[serde(default = "default_shim_health_timeout_secs")]
101 pub shim_health_timeout_secs: u64,
102 #[serde(default = "default_shim_shutdown_timeout_secs")]
103 pub shim_shutdown_timeout_secs: u32,
104 #[serde(default = "default_shim_working_state_timeout_secs")]
105 pub shim_working_state_timeout_secs: u64,
106 #[serde(default = "default_pending_queue_max_age_secs")]
107 pub pending_queue_max_age_secs: u64,
108 #[serde(default = "default_event_log_max_bytes")]
109 pub event_log_max_bytes: u64,
110 #[serde(default = "default_retro_min_duration_secs")]
111 pub retro_min_duration_secs: u64,
112 pub roles: Vec<RoleDef>,
113}
114
115impl From<TeamConfigWire> for TeamConfig {
116 fn from(wire: TeamConfigWire) -> Self {
117 let orchestrator_pane = wire
118 .orchestrator_pane
119 .unwrap_or_else(default_orchestrator_pane);
120 let workflow_mode = wire.workflow_mode.unwrap_or_else(|| {
121 if matches!(wire.orchestrator_pane, Some(true)) {
122 WorkflowMode::Hybrid
123 } else {
124 default_workflow_mode()
125 }
126 });
127
128 Self {
129 name: wire.name,
130 agent: wire.agent,
131 workflow_mode,
132 board: wire.board,
133 standup: wire.standup,
134 automation: wire.automation,
135 automation_sender: wire.automation_sender,
136 external_senders: wire.external_senders,
137 orchestrator_pane,
138 orchestrator_position: wire.orchestrator_position,
139 layout: wire.layout,
140 workflow_policy: wire.workflow_policy,
141 cost: wire.cost,
142 grafana: wire.grafana,
143 use_shim: wire.use_shim,
144 use_sdk_mode: wire.use_sdk_mode,
145 auto_respawn_on_crash: wire.auto_respawn_on_crash,
146 shim_health_check_interval_secs: wire.shim_health_check_interval_secs,
147 shim_health_timeout_secs: wire.shim_health_timeout_secs,
148 shim_shutdown_timeout_secs: wire.shim_shutdown_timeout_secs,
149 shim_working_state_timeout_secs: wire.shim_working_state_timeout_secs,
150 pending_queue_max_age_secs: wire.pending_queue_max_age_secs,
151 event_log_max_bytes: wire.event_log_max_bytes,
152 retro_min_duration_secs: wire.retro_min_duration_secs,
153 roles: wire.roles,
154 }
155 }
156}
157
158impl<'de> Deserialize<'de> for TeamConfig {
159 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
160 where
161 D: Deserializer<'de>,
162 {
163 TeamConfigWire::deserialize(deserializer).map(Into::into)
164 }
165}
166
167#[derive(Debug, Clone, Deserialize)]
168pub struct GrafanaConfig {
169 #[serde(default)]
170 pub enabled: bool,
171 #[serde(default = "default_grafana_port")]
172 pub port: u16,
173}
174
175impl Default for GrafanaConfig {
176 fn default() -> Self {
177 Self {
178 enabled: false,
179 port: default_grafana_port(),
180 }
181 }
182}
183
184fn default_grafana_port() -> u16 {
185 3000
186}
187
188fn default_use_sdk_mode() -> bool {
189 true
190}
191
192fn default_auto_respawn_on_crash() -> bool {
193 true
194}
195
196#[derive(Debug, Clone, Deserialize, Default)]
197pub struct CostConfig {
198 #[serde(default)]
199 pub models: HashMap<String, ModelPricing>,
200}
201
202#[derive(Debug, Clone, Deserialize)]
203pub struct ModelPricing {
204 pub input_usd_per_mtok: f64,
205 #[serde(default)]
206 pub cached_input_usd_per_mtok: f64,
207 #[serde(default)]
208 pub cache_creation_input_usd_per_mtok: Option<f64>,
209 #[serde(default)]
210 pub cache_creation_5m_input_usd_per_mtok: Option<f64>,
211 #[serde(default)]
212 pub cache_creation_1h_input_usd_per_mtok: Option<f64>,
213 #[serde(default)]
214 pub cache_read_input_usd_per_mtok: f64,
215 pub output_usd_per_mtok: f64,
216 #[serde(default)]
217 pub reasoning_output_usd_per_mtok: Option<f64>,
218}
219
220#[derive(Debug, Clone, Deserialize)]
221pub struct WorkflowPolicy {
222 #[serde(default)]
223 pub clean_room_mode: bool,
224 #[serde(default)]
225 pub barrier_groups: HashMap<String, Vec<String>>,
226 #[serde(default = "default_handoff_directory")]
227 pub handoff_directory: String,
228 #[serde(default)]
229 pub wip_limit_per_engineer: Option<u32>,
230 #[serde(default)]
231 pub wip_limit_per_reviewer: Option<u32>,
232 #[serde(default = "default_pipeline_starvation_threshold")]
233 pub pipeline_starvation_threshold: Option<usize>,
234 #[serde(default = "default_escalation_threshold_secs")]
235 pub escalation_threshold_secs: u64,
236 #[serde(default = "default_review_nudge_threshold_secs")]
237 pub review_nudge_threshold_secs: u64,
238 #[serde(default = "default_review_timeout_secs")]
239 pub review_timeout_secs: u64,
240 #[serde(default = "default_stale_in_progress_hours")]
241 pub stale_in_progress_hours: u64,
242 #[serde(default = "default_aged_todo_hours")]
243 pub aged_todo_hours: u64,
244 #[serde(default = "default_stale_review_hours")]
245 pub stale_review_hours: u64,
246 #[serde(default)]
247 pub review_timeout_overrides: HashMap<String, ReviewTimeoutOverride>,
248 #[serde(default)]
249 pub auto_archive_done_after_secs: Option<u64>,
250 #[serde(default)]
251 pub capability_overrides: HashMap<String, Vec<String>>,
252 #[serde(default = "default_stall_threshold_secs")]
253 pub stall_threshold_secs: u64,
254 #[serde(default = "default_max_stall_restarts")]
255 pub max_stall_restarts: u32,
256 #[serde(default = "default_health_check_interval_secs")]
257 pub health_check_interval_secs: u64,
258 #[serde(default = "default_planning_cycle_cooldown_secs")]
259 pub planning_cycle_cooldown_secs: u64,
260 #[serde(default = "default_narration_threshold")]
261 pub narration_threshold: f64,
262 #[serde(default = "default_narration_nudge_max")]
263 pub narration_nudge_max: u32,
264 #[serde(default = "default_narration_detection_enabled")]
265 pub narration_detection_enabled: bool,
266 #[serde(default = "default_narration_threshold_polls")]
267 pub narration_threshold_polls: u32,
268 #[serde(default = "default_context_pressure_threshold")]
269 pub context_pressure_threshold: u64,
270 #[serde(default = "default_context_pressure_threshold_bytes")]
271 pub context_pressure_threshold_bytes: u64,
272 #[serde(default = "default_context_pressure_restart_delay_secs")]
273 pub context_pressure_restart_delay_secs: u64,
274 #[serde(default = "default_graceful_shutdown_timeout_secs")]
275 pub graceful_shutdown_timeout_secs: u64,
276 #[serde(default = "default_auto_commit_on_restart")]
277 pub auto_commit_on_restart: bool,
278 #[serde(default = "default_uncommitted_warn_threshold")]
279 pub uncommitted_warn_threshold: usize,
280 #[serde(default)]
283 pub test_command: Option<String>,
284 #[serde(default)]
285 pub verification: VerificationPolicy,
286 #[serde(default)]
287 pub claim_ttl: ClaimTtlPolicy,
288 #[serde(default)]
289 pub allocation: AllocationPolicy,
290 #[serde(default)]
291 pub auto_merge: AutoMergePolicy,
292 #[serde(default = "default_context_handoff_enabled")]
296 pub context_handoff_enabled: bool,
297 #[serde(default = "default_handoff_screen_history")]
299 pub handoff_screen_history: usize,
300}
301
302#[derive(Debug, Clone, Deserialize)]
303pub struct ClaimTtlPolicy {
304 #[serde(default = "default_claim_ttl_default_secs")]
305 pub default_secs: u64,
306 #[serde(default = "default_claim_ttl_critical_secs")]
307 pub critical_secs: u64,
308 #[serde(default = "default_claim_ttl_max_extensions")]
309 pub max_extensions: u32,
310 #[serde(default = "default_claim_ttl_progress_check_interval_secs")]
311 pub progress_check_interval_secs: u64,
312 #[serde(default = "default_claim_ttl_warning_secs")]
313 pub warning_secs: u64,
314}
315
316impl Default for ClaimTtlPolicy {
317 fn default() -> Self {
318 Self {
319 default_secs: default_claim_ttl_default_secs(),
320 critical_secs: default_claim_ttl_critical_secs(),
321 max_extensions: default_claim_ttl_max_extensions(),
322 progress_check_interval_secs: default_claim_ttl_progress_check_interval_secs(),
323 warning_secs: default_claim_ttl_warning_secs(),
324 }
325 }
326}
327
328#[derive(Debug, Clone, Deserialize)]
332pub struct ReviewTimeoutOverride {
333 pub review_nudge_threshold_secs: Option<u64>,
335 pub review_timeout_secs: Option<u64>,
337}
338
339impl Default for WorkflowPolicy {
340 fn default() -> Self {
341 Self {
342 clean_room_mode: false,
343 barrier_groups: HashMap::new(),
344 handoff_directory: default_handoff_directory(),
345 wip_limit_per_engineer: None,
346 wip_limit_per_reviewer: None,
347 pipeline_starvation_threshold: default_pipeline_starvation_threshold(),
348 escalation_threshold_secs: default_escalation_threshold_secs(),
349 review_nudge_threshold_secs: default_review_nudge_threshold_secs(),
350 review_timeout_secs: default_review_timeout_secs(),
351 stale_in_progress_hours: default_stale_in_progress_hours(),
352 aged_todo_hours: default_aged_todo_hours(),
353 stale_review_hours: default_stale_review_hours(),
354 review_timeout_overrides: HashMap::new(),
355 auto_archive_done_after_secs: None,
356 capability_overrides: HashMap::new(),
357 stall_threshold_secs: default_stall_threshold_secs(),
358 max_stall_restarts: default_max_stall_restarts(),
359 health_check_interval_secs: default_health_check_interval_secs(),
360 planning_cycle_cooldown_secs: default_planning_cycle_cooldown_secs(),
361 narration_threshold: default_narration_threshold(),
362 narration_nudge_max: default_narration_nudge_max(),
363 narration_detection_enabled: default_narration_detection_enabled(),
364 narration_threshold_polls: default_narration_threshold_polls(),
365 context_pressure_threshold: default_context_pressure_threshold(),
366 context_pressure_threshold_bytes: default_context_pressure_threshold_bytes(),
367 context_pressure_restart_delay_secs: default_context_pressure_restart_delay_secs(),
368 graceful_shutdown_timeout_secs: default_graceful_shutdown_timeout_secs(),
369 auto_commit_on_restart: default_auto_commit_on_restart(),
370 uncommitted_warn_threshold: default_uncommitted_warn_threshold(),
371 test_command: None,
372 verification: VerificationPolicy::default(),
373 claim_ttl: ClaimTtlPolicy::default(),
374 allocation: AllocationPolicy::default(),
375 auto_merge: AutoMergePolicy::default(),
376 context_handoff_enabled: default_context_handoff_enabled(),
377 handoff_screen_history: default_handoff_screen_history(),
378 }
379 }
380}
381
382#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
383#[serde(rename_all = "snake_case")]
384pub enum AllocationStrategy {
385 RoundRobin,
386 Scored,
387}
388
389#[derive(Debug, Clone, Deserialize)]
390pub struct AllocationPolicy {
391 #[serde(default = "default_allocation_strategy")]
392 pub strategy: AllocationStrategy,
393 #[serde(default = "default_allocation_tag_weight")]
394 pub tag_weight: i32,
395 #[serde(default = "default_allocation_file_overlap_weight")]
396 pub file_overlap_weight: i32,
397 #[serde(default = "default_allocation_load_penalty")]
398 pub load_penalty: i32,
399 #[serde(default = "default_allocation_conflict_penalty")]
400 pub conflict_penalty: i32,
401 #[serde(default = "default_allocation_experience_bonus")]
402 pub experience_bonus: i32,
403}
404
405impl Default for AllocationPolicy {
406 fn default() -> Self {
407 Self {
408 strategy: default_allocation_strategy(),
409 tag_weight: default_allocation_tag_weight(),
410 file_overlap_weight: default_allocation_file_overlap_weight(),
411 load_penalty: default_allocation_load_penalty(),
412 conflict_penalty: default_allocation_conflict_penalty(),
413 experience_bonus: default_allocation_experience_bonus(),
414 }
415 }
416}
417
418#[derive(Debug, Clone, Deserialize)]
419pub struct VerificationPolicy {
420 #[serde(default = "default_verification_max_iterations")]
421 pub max_iterations: u32,
422 #[serde(default = "default_verification_auto_run_tests")]
423 pub auto_run_tests: bool,
424 #[serde(default = "default_verification_require_evidence")]
425 pub require_evidence: bool,
426 #[serde(default)]
427 pub test_command: Option<String>,
428}
429
430impl Default for VerificationPolicy {
431 fn default() -> Self {
432 Self {
433 max_iterations: default_verification_max_iterations(),
434 auto_run_tests: default_verification_auto_run_tests(),
435 require_evidence: default_verification_require_evidence(),
436 test_command: None,
437 }
438 }
439}
440
441fn default_graceful_shutdown_timeout_secs() -> u64 {
442 30
443}
444
445fn default_verification_max_iterations() -> u32 {
446 5
447}
448
449fn default_verification_auto_run_tests() -> bool {
450 true
451}
452
453fn default_verification_require_evidence() -> bool {
454 true
455}
456
457fn default_claim_ttl_default_secs() -> u64 {
458 1800
459}
460
461fn default_claim_ttl_critical_secs() -> u64 {
462 900
463}
464
465fn default_claim_ttl_max_extensions() -> u32 {
466 2
467}
468
469fn default_claim_ttl_progress_check_interval_secs() -> u64 {
470 120
471}
472
473fn default_claim_ttl_warning_secs() -> u64 {
474 300
475}
476
477fn default_allocation_strategy() -> AllocationStrategy {
478 AllocationStrategy::Scored
479}
480
481fn default_allocation_tag_weight() -> i32 {
482 15
483}
484
485fn default_allocation_file_overlap_weight() -> i32 {
486 10
487}
488
489fn default_allocation_load_penalty() -> i32 {
490 8
491}
492
493fn default_allocation_conflict_penalty() -> i32 {
494 12
495}
496
497fn default_allocation_experience_bonus() -> i32 {
498 3
499}
500
501fn default_auto_commit_on_restart() -> bool {
502 true
503}
504
505fn default_context_handoff_enabled() -> bool {
506 true
507}
508
509fn default_handoff_screen_history() -> usize {
510 20
511}
512
513fn default_handoff_directory() -> String {
514 ".batty/handoff".to_string()
515}
516
517fn default_planning_cycle_cooldown_secs() -> u64 {
518 300
519}
520
521fn default_context_pressure_threshold() -> u64 {
522 100
523}
524
525fn default_context_pressure_threshold_bytes() -> u64 {
526 512_000
527}
528
529fn default_context_pressure_restart_delay_secs() -> u64 {
530 120
531}
532
533fn default_sensitive_paths() -> Vec<String> {
534 vec![
535 "Cargo.toml".to_string(),
536 "team.yaml".to_string(),
537 ".env".to_string(),
538 ]
539}
540
541#[derive(Debug, Clone, Deserialize)]
542pub struct AutoMergePolicy {
543 #[serde(default = "default_auto_merge_enabled")]
544 pub enabled: bool,
545 #[serde(default = "default_max_diff_lines")]
546 pub max_diff_lines: usize,
547 #[serde(default = "default_max_files_changed")]
548 pub max_files_changed: usize,
549 #[serde(default = "default_max_modules_touched")]
550 pub max_modules_touched: usize,
551 #[serde(default = "default_sensitive_paths")]
552 pub sensitive_paths: Vec<String>,
553 #[serde(default = "default_confidence_threshold")]
554 pub confidence_threshold: f64,
555 #[serde(default = "default_require_tests_pass")]
556 pub require_tests_pass: bool,
557 #[serde(default = "default_post_merge_verify")]
558 pub post_merge_verify: bool,
559}
560
561fn default_max_diff_lines() -> usize {
562 2000 }
564fn default_auto_merge_enabled() -> bool {
565 true
566}
567fn default_max_files_changed() -> usize {
568 30 }
570fn default_max_modules_touched() -> usize {
571 10 }
573fn default_narration_threshold() -> f64 {
574 0.8
575}
576fn default_narration_nudge_max() -> u32 {
577 2
578}
579fn default_narration_detection_enabled() -> bool {
580 true
581}
582fn default_narration_threshold_polls() -> u32 {
583 5
584}
585fn default_confidence_threshold() -> f64 {
586 0.0 }
588fn default_require_tests_pass() -> bool {
589 true
590}
591fn default_post_merge_verify() -> bool {
592 true
593}
594
595impl Default for AutoMergePolicy {
596 fn default() -> Self {
597 Self {
598 enabled: true,
599 max_diff_lines: default_max_diff_lines(),
600 max_files_changed: default_max_files_changed(),
601 max_modules_touched: default_max_modules_touched(),
602 sensitive_paths: default_sensitive_paths(),
603 confidence_threshold: default_confidence_threshold(),
604 require_tests_pass: default_require_tests_pass(),
605 post_merge_verify: default_post_merge_verify(),
606 }
607 }
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
611#[serde(rename_all = "snake_case")]
612pub enum WorkflowMode {
613 #[default]
614 Legacy,
615 Hybrid,
616 WorkflowFirst,
617 BoardFirst,
618}
619
620#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
621#[serde(rename_all = "snake_case")]
622pub enum OrchestratorPosition {
623 #[default]
624 Left,
625 Bottom,
626}
627
628impl WorkflowMode {
629 #[cfg_attr(not(test), allow(dead_code))]
630 pub fn legacy_runtime_enabled(self) -> bool {
631 matches!(self, Self::Legacy | Self::Hybrid)
632 }
633
634 #[cfg_attr(not(test), allow(dead_code))]
635 pub fn workflow_state_primary(self) -> bool {
636 matches!(self, Self::WorkflowFirst | Self::BoardFirst)
637 }
638
639 pub fn enables_runtime_surface(self) -> bool {
640 matches!(self, Self::Hybrid | Self::WorkflowFirst | Self::BoardFirst)
641 }
642
643 pub fn suppresses_manager_relay(self) -> bool {
644 matches!(self, Self::BoardFirst)
645 }
646
647 pub fn as_str(self) -> &'static str {
648 match self {
649 Self::Legacy => "legacy",
650 Self::Hybrid => "hybrid",
651 Self::WorkflowFirst => "workflow_first",
652 Self::BoardFirst => "board_first",
653 }
654 }
655}
656
657#[derive(Debug, Clone, Deserialize)]
658pub struct BoardConfig {
659 #[serde(default = "default_rotation_threshold")]
660 pub rotation_threshold: u32,
661 #[serde(default = "default_board_auto_dispatch")]
662 pub auto_dispatch: bool,
663 #[serde(default = "default_worktree_stale_rebase_threshold")]
664 pub worktree_stale_rebase_threshold: u32,
665 #[serde(default = "default_board_auto_replenish")]
666 pub auto_replenish: bool,
667 #[serde(default = "default_state_reconciliation_interval_secs")]
668 pub state_reconciliation_interval_secs: u64,
669 #[serde(default = "default_dispatch_stabilization_delay_secs")]
670 pub dispatch_stabilization_delay_secs: u64,
671 #[serde(default = "default_dispatch_dedup_window_secs")]
672 pub dispatch_dedup_window_secs: u64,
673 #[serde(default = "default_dispatch_manual_cooldown_secs")]
674 pub dispatch_manual_cooldown_secs: u64,
675}
676
677impl Default for BoardConfig {
678 fn default() -> Self {
679 Self {
680 rotation_threshold: default_rotation_threshold(),
681 auto_dispatch: default_board_auto_dispatch(),
682 worktree_stale_rebase_threshold: default_worktree_stale_rebase_threshold(),
683 auto_replenish: default_board_auto_replenish(),
684 state_reconciliation_interval_secs: default_state_reconciliation_interval_secs(),
685 dispatch_stabilization_delay_secs: default_dispatch_stabilization_delay_secs(),
686 dispatch_dedup_window_secs: default_dispatch_dedup_window_secs(),
687 dispatch_manual_cooldown_secs: default_dispatch_manual_cooldown_secs(),
688 }
689 }
690}
691
692#[derive(Debug, Clone, Deserialize)]
693pub struct StandupConfig {
694 #[serde(default = "default_standup_interval")]
695 pub interval_secs: u64,
696 #[serde(default = "default_output_lines")]
697 pub output_lines: u32,
698}
699
700impl Default for StandupConfig {
701 fn default() -> Self {
702 Self {
703 interval_secs: default_standup_interval(),
704 output_lines: default_output_lines(),
705 }
706 }
707}
708
709#[derive(Debug, Clone, Deserialize)]
710pub struct AutomationConfig {
711 #[serde(default = "default_enabled")]
712 pub timeout_nudges: bool,
713 #[serde(default = "default_enabled")]
714 pub standups: bool,
715 #[serde(default)]
716 pub clean_room_mode: bool,
717 #[serde(default = "default_enabled")]
718 pub failure_pattern_detection: bool,
719 #[serde(default = "default_enabled")]
720 pub triage_interventions: bool,
721 #[serde(default = "default_enabled")]
722 pub review_interventions: bool,
723 #[serde(default = "default_enabled")]
724 pub owned_task_interventions: bool,
725 #[serde(default = "default_enabled")]
726 pub manager_dispatch_interventions: bool,
727 #[serde(default = "default_enabled")]
728 pub architect_utilization_interventions: bool,
729 #[serde(default)]
730 pub replenishment_threshold: Option<usize>,
731 #[serde(default = "default_intervention_idle_grace_secs")]
732 pub intervention_idle_grace_secs: u64,
733 #[serde(default = "default_intervention_cooldown_secs")]
734 pub intervention_cooldown_secs: u64,
735 #[serde(default = "default_utilization_recovery_interval_secs")]
736 pub utilization_recovery_interval_secs: u64,
737 #[serde(default = "default_enabled")]
738 pub commit_before_reset: bool,
739 #[serde(default)]
740 pub disk_hygiene: DiskHygieneConfig,
741}
742
743impl Default for AutomationConfig {
744 fn default() -> Self {
745 Self {
746 timeout_nudges: default_enabled(),
747 standups: default_enabled(),
748 clean_room_mode: false,
749 failure_pattern_detection: default_enabled(),
750 triage_interventions: default_enabled(),
751 review_interventions: default_enabled(),
752 owned_task_interventions: default_enabled(),
753 manager_dispatch_interventions: default_enabled(),
754 architect_utilization_interventions: default_enabled(),
755 replenishment_threshold: None,
756 intervention_idle_grace_secs: default_intervention_idle_grace_secs(),
757 intervention_cooldown_secs: default_intervention_cooldown_secs(),
758 utilization_recovery_interval_secs: default_utilization_recovery_interval_secs(),
759 commit_before_reset: default_enabled(),
760 disk_hygiene: DiskHygieneConfig::default(),
761 }
762 }
763}
764
765#[derive(Debug, Clone, Deserialize)]
766pub struct LayoutConfig {
767 pub zones: Vec<ZoneDef>,
768}
769
770#[derive(Debug, Clone, Deserialize)]
771pub struct ZoneDef {
772 pub name: String,
773 pub width_pct: u32,
774 #[serde(default)]
775 pub split: Option<SplitDef>,
776}
777
778#[derive(Debug, Clone, Deserialize)]
779pub struct SplitDef {
780 pub horizontal: u32,
781}
782
783#[derive(Debug, Clone, Deserialize)]
784pub struct RoleDef {
785 pub name: String,
786 pub role_type: RoleType,
787 #[serde(default)]
788 pub agent: Option<String>,
789 #[serde(default)]
790 pub model: Option<String>,
791 #[serde(default)]
792 pub auth_mode: Option<ClaudeAuthMode>,
793 #[serde(default)]
794 pub auth_env: Vec<String>,
795 #[serde(default = "default_instances")]
796 pub instances: u32,
797 #[serde(default)]
798 pub prompt: Option<String>,
799 #[serde(default)]
800 pub posture: Option<String>,
801 #[serde(default)]
802 pub model_class: Option<String>,
803 #[serde(default)]
804 pub provider_overlay: Option<String>,
805 #[serde(default)]
806 pub instance_overrides: HashMap<String, RoleInstanceOverride>,
807 #[serde(default)]
808 pub talks_to: Vec<String>,
809 #[serde(default)]
810 pub channel: Option<String>,
811 #[serde(default)]
812 pub channel_config: Option<ChannelConfig>,
813 #[serde(default)]
814 pub nudge_interval_secs: Option<u64>,
815 #[serde(default)]
816 pub receives_standup: Option<bool>,
817 #[serde(default)]
818 pub standup_interval_secs: Option<u64>,
819 #[serde(default)]
820 #[allow(dead_code)] pub owns: Vec<String>,
822 #[serde(default)]
823 pub barrier_group: Option<String>,
824 #[serde(default)]
825 pub use_worktrees: bool,
826}
827
828impl Default for RoleDef {
829 fn default() -> Self {
830 Self {
831 name: String::new(),
832 role_type: RoleType::Engineer,
833 agent: None,
834 model: None,
835 auth_mode: None,
836 auth_env: Vec::new(),
837 instances: default_instances(),
838 prompt: None,
839 posture: None,
840 model_class: None,
841 provider_overlay: None,
842 instance_overrides: HashMap::new(),
843 talks_to: Vec::new(),
844 channel: None,
845 channel_config: None,
846 nudge_interval_secs: None,
847 receives_standup: None,
848 standup_interval_secs: None,
849 owns: Vec::new(),
850 barrier_group: None,
851 use_worktrees: false,
852 }
853 }
854}
855
856#[derive(Debug, Clone, Default, Deserialize)]
857pub struct RoleInstanceOverride {
858 #[serde(default)]
859 pub agent: Option<String>,
860 #[serde(default)]
861 pub model: Option<String>,
862 #[serde(default)]
863 pub prompt: Option<String>,
864 #[serde(default)]
865 pub posture: Option<String>,
866 #[serde(default)]
867 pub model_class: Option<String>,
868 #[serde(default)]
869 pub provider_overlay: Option<String>,
870}
871
872#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
873#[serde(rename_all = "snake_case")]
874pub enum ClaudeAuthMode {
875 #[default]
876 Oauth,
877 ApiKey,
878 Custom,
879}
880
881#[derive(Debug, Clone, Default, Deserialize)]
882pub struct ChannelConfig {
883 #[serde(default)]
884 pub target: String,
885 #[serde(default)]
886 pub provider: String,
887 #[serde(default)]
890 pub bot_token: Option<String>,
891 #[serde(default, deserialize_with = "deserialize_user_id_list")]
896 pub allowed_user_ids: Vec<i64>,
897 #[serde(default)]
898 pub events_channel_id: Option<String>,
899 #[serde(default)]
900 pub agents_channel_id: Option<String>,
901 #[serde(default)]
902 pub commands_channel_id: Option<String>,
903 #[serde(default)]
904 pub board_channel_id: Option<String>,
905}
906
907#[derive(Deserialize)]
908#[serde(untagged)]
909enum UserIdValue {
910 Integer(i64),
911 String(String),
912}
913
914fn deserialize_user_id_list<'de, D>(deserializer: D) -> Result<Vec<i64>, D::Error>
915where
916 D: Deserializer<'de>,
917{
918 let values = Vec::<UserIdValue>::deserialize(deserializer)?;
919 values
920 .into_iter()
921 .map(|value| match value {
922 UserIdValue::Integer(id) => Ok(id),
923 UserIdValue::String(raw) => raw
924 .parse::<i64>()
925 .map_err(|error| de::Error::custom(format!("invalid user id '{raw}': {error}"))),
926 })
927 .collect()
928}
929
930#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
931#[serde(rename_all = "lowercase")]
932pub enum RoleType {
933 User,
934 Architect,
935 Manager,
936 Engineer,
937}
938
939fn default_rotation_threshold() -> u32 {
942 20
943}
944
945fn default_workflow_mode() -> WorkflowMode {
946 WorkflowMode::Legacy
947}
948
949fn default_board_auto_dispatch() -> bool {
950 true
951}
952
953fn default_worktree_stale_rebase_threshold() -> u32 {
954 5
955}
956
957fn default_board_auto_replenish() -> bool {
958 true
959}
960
961fn default_state_reconciliation_interval_secs() -> u64 {
962 30
963}
964
965fn default_dispatch_stabilization_delay_secs() -> u64 {
966 30
967}
968
969fn default_dispatch_dedup_window_secs() -> u64 {
970 900 }
972
973fn default_dispatch_manual_cooldown_secs() -> u64 {
974 30
975}
976
977fn default_standup_interval() -> u64 {
978 300
979}
980
981fn default_pipeline_starvation_threshold() -> Option<usize> {
982 Some(1)
983}
984
985fn default_output_lines() -> u32 {
986 30
987}
988
989fn default_instances() -> u32 {
990 1
991}
992
993fn default_escalation_threshold_secs() -> u64 {
994 3600
995}
996
997fn default_review_nudge_threshold_secs() -> u64 {
998 1800
999}
1000
1001fn default_review_timeout_secs() -> u64 {
1002 7200
1003}
1004
1005fn default_stale_in_progress_hours() -> u64 {
1006 4
1007}
1008
1009fn default_aged_todo_hours() -> u64 {
1010 48
1011}
1012
1013fn default_stale_review_hours() -> u64 {
1014 1
1015}
1016
1017fn default_stall_threshold_secs() -> u64 {
1018 300
1019}
1020
1021fn default_max_stall_restarts() -> u32 {
1022 2
1023}
1024
1025fn default_health_check_interval_secs() -> u64 {
1026 60
1027}
1028
1029fn default_uncommitted_warn_threshold() -> usize {
1030 200
1031}
1032
1033fn default_enabled() -> bool {
1034 true
1035}
1036
1037fn default_orchestrator_pane() -> bool {
1038 true
1039}
1040
1041fn default_intervention_idle_grace_secs() -> u64 {
1042 60
1043}
1044
1045fn default_intervention_cooldown_secs() -> u64 {
1046 120
1047}
1048
1049fn default_utilization_recovery_interval_secs() -> u64 {
1050 1200
1051}
1052
1053fn default_event_log_max_bytes() -> u64 {
1054 DEFAULT_EVENT_LOG_MAX_BYTES
1055}
1056
1057fn default_retro_min_duration_secs() -> u64 {
1058 60
1059}
1060
1061fn default_disk_hygiene_check_interval_secs() -> u64 {
1062 600
1063}
1064
1065fn default_disk_hygiene_min_free_gb() -> u64 {
1066 10
1067}
1068
1069fn default_disk_hygiene_max_shared_target_gb() -> u64 {
1070 4
1071}
1072
1073fn default_disk_hygiene_log_rotation_hours() -> u64 {
1074 24
1075}
1076
1077#[derive(Debug, Clone, Deserialize)]
1079pub struct DiskHygieneConfig {
1080 #[serde(default = "default_enabled")]
1082 pub enabled: bool,
1083 #[serde(default = "default_disk_hygiene_check_interval_secs")]
1085 pub check_interval_secs: u64,
1086 #[serde(default = "default_disk_hygiene_min_free_gb")]
1088 pub min_free_gb: u64,
1089 #[serde(default = "default_disk_hygiene_max_shared_target_gb")]
1091 pub max_shared_target_gb: u64,
1092 #[serde(default = "default_disk_hygiene_log_rotation_hours")]
1094 pub log_rotation_hours: u64,
1095 #[serde(default = "default_enabled")]
1097 pub post_merge_cleanup: bool,
1098 #[serde(default = "default_enabled")]
1100 pub prune_merged_branches: bool,
1101}
1102
1103impl Default for DiskHygieneConfig {
1104 fn default() -> Self {
1105 Self {
1106 enabled: default_enabled(),
1107 check_interval_secs: default_disk_hygiene_check_interval_secs(),
1108 min_free_gb: default_disk_hygiene_min_free_gb(),
1109 max_shared_target_gb: default_disk_hygiene_max_shared_target_gb(),
1110 log_rotation_hours: default_disk_hygiene_log_rotation_hours(),
1111 post_merge_cleanup: default_enabled(),
1112 prune_merged_branches: default_enabled(),
1113 }
1114 }
1115}
1116
1117fn default_shim_health_check_interval_secs() -> u64 {
1118 60
1119}
1120
1121fn default_shim_health_timeout_secs() -> u64 {
1122 120
1123}
1124
1125fn default_shim_shutdown_timeout_secs() -> u32 {
1126 30
1127}
1128
1129fn default_shim_working_state_timeout_secs() -> u64 {
1130 7200 }
1132
1133fn default_pending_queue_max_age_secs() -> u64 {
1134 600 }