1use std::collections::HashMap;
4
5use serde::{Deserialize, Deserializer};
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)]
241 pub review_timeout_overrides: HashMap<String, ReviewTimeoutOverride>,
242 #[serde(default)]
243 pub auto_archive_done_after_secs: Option<u64>,
244 #[serde(default)]
245 pub capability_overrides: HashMap<String, Vec<String>>,
246 #[serde(default = "default_stall_threshold_secs")]
247 pub stall_threshold_secs: u64,
248 #[serde(default = "default_max_stall_restarts")]
249 pub max_stall_restarts: u32,
250 #[serde(default = "default_health_check_interval_secs")]
251 pub health_check_interval_secs: u64,
252 #[serde(default = "default_planning_cycle_cooldown_secs")]
253 pub planning_cycle_cooldown_secs: u64,
254 #[serde(default = "default_narration_detection_threshold")]
255 pub narration_detection_threshold: usize,
256 #[serde(default = "default_context_pressure_threshold_bytes")]
257 pub context_pressure_threshold_bytes: u64,
258 #[serde(default = "default_context_pressure_restart_delay_secs")]
259 pub context_pressure_restart_delay_secs: u64,
260 #[serde(default = "default_graceful_shutdown_timeout_secs")]
261 pub graceful_shutdown_timeout_secs: u64,
262 #[serde(default = "default_auto_commit_on_restart")]
263 pub auto_commit_on_restart: bool,
264 #[serde(default = "default_uncommitted_warn_threshold")]
265 pub uncommitted_warn_threshold: usize,
266 #[serde(default)]
267 pub test_command: Option<String>,
268 #[serde(default)]
269 pub auto_merge: AutoMergePolicy,
270 #[serde(default = "default_context_handoff_enabled")]
274 pub context_handoff_enabled: bool,
275 #[serde(default = "default_handoff_screen_history")]
277 pub handoff_screen_history: usize,
278}
279
280#[derive(Debug, Clone, Deserialize)]
284pub struct ReviewTimeoutOverride {
285 pub review_nudge_threshold_secs: Option<u64>,
287 pub review_timeout_secs: Option<u64>,
289}
290
291impl Default for WorkflowPolicy {
292 fn default() -> Self {
293 Self {
294 clean_room_mode: false,
295 barrier_groups: HashMap::new(),
296 handoff_directory: default_handoff_directory(),
297 wip_limit_per_engineer: None,
298 wip_limit_per_reviewer: None,
299 pipeline_starvation_threshold: default_pipeline_starvation_threshold(),
300 escalation_threshold_secs: default_escalation_threshold_secs(),
301 review_nudge_threshold_secs: default_review_nudge_threshold_secs(),
302 review_timeout_secs: default_review_timeout_secs(),
303 review_timeout_overrides: HashMap::new(),
304 auto_archive_done_after_secs: None,
305 capability_overrides: HashMap::new(),
306 stall_threshold_secs: default_stall_threshold_secs(),
307 max_stall_restarts: default_max_stall_restarts(),
308 health_check_interval_secs: default_health_check_interval_secs(),
309 planning_cycle_cooldown_secs: default_planning_cycle_cooldown_secs(),
310 narration_detection_threshold: default_narration_detection_threshold(),
311 context_pressure_threshold_bytes: default_context_pressure_threshold_bytes(),
312 context_pressure_restart_delay_secs: default_context_pressure_restart_delay_secs(),
313 graceful_shutdown_timeout_secs: default_graceful_shutdown_timeout_secs(),
314 auto_commit_on_restart: default_auto_commit_on_restart(),
315 uncommitted_warn_threshold: default_uncommitted_warn_threshold(),
316 test_command: None,
317 auto_merge: AutoMergePolicy::default(),
318 context_handoff_enabled: default_context_handoff_enabled(),
319 handoff_screen_history: default_handoff_screen_history(),
320 }
321 }
322}
323
324fn default_graceful_shutdown_timeout_secs() -> u64 {
325 5
326}
327
328fn default_auto_commit_on_restart() -> bool {
329 true
330}
331
332fn default_context_handoff_enabled() -> bool {
333 true
334}
335
336fn default_handoff_screen_history() -> usize {
337 20
338}
339
340fn default_handoff_directory() -> String {
341 ".batty/handoff".to_string()
342}
343
344fn default_planning_cycle_cooldown_secs() -> u64 {
345 300
346}
347
348fn default_context_pressure_threshold_bytes() -> u64 {
349 512_000
350}
351
352fn default_context_pressure_restart_delay_secs() -> u64 {
353 120
354}
355
356fn default_sensitive_paths() -> Vec<String> {
357 vec![
358 "Cargo.toml".to_string(),
359 "team.yaml".to_string(),
360 ".env".to_string(),
361 ]
362}
363
364#[derive(Debug, Clone, Deserialize)]
365pub struct AutoMergePolicy {
366 #[serde(default)]
367 pub enabled: bool,
368 #[serde(default = "default_max_diff_lines")]
369 pub max_diff_lines: usize,
370 #[serde(default = "default_max_files_changed")]
371 pub max_files_changed: usize,
372 #[serde(default = "default_max_modules_touched")]
373 pub max_modules_touched: usize,
374 #[serde(default = "default_sensitive_paths")]
375 pub sensitive_paths: Vec<String>,
376 #[serde(default = "default_confidence_threshold")]
377 pub confidence_threshold: f64,
378 #[serde(default = "default_require_tests_pass")]
379 pub require_tests_pass: bool,
380}
381
382fn default_max_diff_lines() -> usize {
383 200
384}
385fn default_max_files_changed() -> usize {
386 5
387}
388fn default_max_modules_touched() -> usize {
389 2
390}
391fn default_narration_detection_threshold() -> usize {
392 6
393}
394fn default_confidence_threshold() -> f64 {
395 0.8
396}
397fn default_require_tests_pass() -> bool {
398 true
399}
400
401impl Default for AutoMergePolicy {
402 fn default() -> Self {
403 Self {
404 enabled: false,
405 max_diff_lines: default_max_diff_lines(),
406 max_files_changed: default_max_files_changed(),
407 max_modules_touched: default_max_modules_touched(),
408 sensitive_paths: default_sensitive_paths(),
409 confidence_threshold: default_confidence_threshold(),
410 require_tests_pass: default_require_tests_pass(),
411 }
412 }
413}
414
415#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
416#[serde(rename_all = "snake_case")]
417pub enum WorkflowMode {
418 #[default]
419 Legacy,
420 Hybrid,
421 WorkflowFirst,
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
425#[serde(rename_all = "snake_case")]
426pub enum OrchestratorPosition {
427 #[default]
428 Left,
429 Bottom,
430}
431
432impl WorkflowMode {
433 #[cfg_attr(not(test), allow(dead_code))]
434 pub fn legacy_runtime_enabled(self) -> bool {
435 matches!(self, Self::Legacy | Self::Hybrid)
436 }
437
438 #[cfg_attr(not(test), allow(dead_code))]
439 pub fn workflow_state_primary(self) -> bool {
440 matches!(self, Self::WorkflowFirst)
441 }
442
443 pub fn enables_runtime_surface(self) -> bool {
444 matches!(self, Self::Hybrid | Self::WorkflowFirst)
445 }
446
447 pub fn as_str(self) -> &'static str {
448 match self {
449 Self::Legacy => "legacy",
450 Self::Hybrid => "hybrid",
451 Self::WorkflowFirst => "workflow_first",
452 }
453 }
454}
455
456#[derive(Debug, Clone, Deserialize)]
457pub struct BoardConfig {
458 #[serde(default = "default_rotation_threshold")]
459 pub rotation_threshold: u32,
460 #[serde(default = "default_board_auto_dispatch")]
461 pub auto_dispatch: bool,
462 #[serde(default = "default_board_auto_replenish")]
463 pub auto_replenish: bool,
464 #[serde(default = "default_state_reconciliation_interval_secs")]
465 pub state_reconciliation_interval_secs: u64,
466 #[serde(default = "default_dispatch_stabilization_delay_secs")]
467 pub dispatch_stabilization_delay_secs: u64,
468 #[serde(default = "default_dispatch_dedup_window_secs")]
469 pub dispatch_dedup_window_secs: u64,
470 #[serde(default = "default_dispatch_manual_cooldown_secs")]
471 pub dispatch_manual_cooldown_secs: u64,
472}
473
474impl Default for BoardConfig {
475 fn default() -> Self {
476 Self {
477 rotation_threshold: default_rotation_threshold(),
478 auto_dispatch: default_board_auto_dispatch(),
479 auto_replenish: default_board_auto_replenish(),
480 state_reconciliation_interval_secs: default_state_reconciliation_interval_secs(),
481 dispatch_stabilization_delay_secs: default_dispatch_stabilization_delay_secs(),
482 dispatch_dedup_window_secs: default_dispatch_dedup_window_secs(),
483 dispatch_manual_cooldown_secs: default_dispatch_manual_cooldown_secs(),
484 }
485 }
486}
487
488#[derive(Debug, Clone, Deserialize)]
489pub struct StandupConfig {
490 #[serde(default = "default_standup_interval")]
491 pub interval_secs: u64,
492 #[serde(default = "default_output_lines")]
493 pub output_lines: u32,
494}
495
496impl Default for StandupConfig {
497 fn default() -> Self {
498 Self {
499 interval_secs: default_standup_interval(),
500 output_lines: default_output_lines(),
501 }
502 }
503}
504
505#[derive(Debug, Clone, Deserialize)]
506pub struct AutomationConfig {
507 #[serde(default = "default_enabled")]
508 pub timeout_nudges: bool,
509 #[serde(default = "default_enabled")]
510 pub standups: bool,
511 #[serde(default)]
512 pub clean_room_mode: bool,
513 #[serde(default = "default_enabled")]
514 pub failure_pattern_detection: bool,
515 #[serde(default = "default_enabled")]
516 pub triage_interventions: bool,
517 #[serde(default = "default_enabled")]
518 pub review_interventions: bool,
519 #[serde(default = "default_enabled")]
520 pub owned_task_interventions: bool,
521 #[serde(default = "default_enabled")]
522 pub manager_dispatch_interventions: bool,
523 #[serde(default = "default_enabled")]
524 pub architect_utilization_interventions: bool,
525 #[serde(default)]
526 pub replenishment_threshold: Option<usize>,
527 #[serde(default = "default_intervention_idle_grace_secs")]
528 pub intervention_idle_grace_secs: u64,
529 #[serde(default = "default_intervention_cooldown_secs")]
530 pub intervention_cooldown_secs: u64,
531 #[serde(default = "default_utilization_recovery_interval_secs")]
532 pub utilization_recovery_interval_secs: u64,
533 #[serde(default = "default_enabled")]
534 pub commit_before_reset: bool,
535}
536
537impl Default for AutomationConfig {
538 fn default() -> Self {
539 Self {
540 timeout_nudges: default_enabled(),
541 standups: default_enabled(),
542 clean_room_mode: false,
543 failure_pattern_detection: default_enabled(),
544 triage_interventions: default_enabled(),
545 review_interventions: default_enabled(),
546 owned_task_interventions: default_enabled(),
547 manager_dispatch_interventions: default_enabled(),
548 architect_utilization_interventions: default_enabled(),
549 replenishment_threshold: None,
550 intervention_idle_grace_secs: default_intervention_idle_grace_secs(),
551 intervention_cooldown_secs: default_intervention_cooldown_secs(),
552 utilization_recovery_interval_secs: default_utilization_recovery_interval_secs(),
553 commit_before_reset: default_enabled(),
554 }
555 }
556}
557
558#[derive(Debug, Clone, Deserialize)]
559pub struct LayoutConfig {
560 pub zones: Vec<ZoneDef>,
561}
562
563#[derive(Debug, Clone, Deserialize)]
564pub struct ZoneDef {
565 pub name: String,
566 pub width_pct: u32,
567 #[serde(default)]
568 pub split: Option<SplitDef>,
569}
570
571#[derive(Debug, Clone, Deserialize)]
572pub struct SplitDef {
573 pub horizontal: u32,
574}
575
576#[derive(Debug, Clone, Deserialize)]
577pub struct RoleDef {
578 pub name: String,
579 pub role_type: RoleType,
580 #[serde(default)]
581 pub agent: Option<String>,
582 #[serde(default = "default_instances")]
583 pub instances: u32,
584 #[serde(default)]
585 pub prompt: Option<String>,
586 #[serde(default)]
587 pub talks_to: Vec<String>,
588 #[serde(default)]
589 pub channel: Option<String>,
590 #[serde(default)]
591 pub channel_config: Option<ChannelConfig>,
592 #[serde(default)]
593 pub nudge_interval_secs: Option<u64>,
594 #[serde(default)]
595 pub receives_standup: Option<bool>,
596 #[serde(default)]
597 pub standup_interval_secs: Option<u64>,
598 #[serde(default)]
599 #[allow(dead_code)] pub owns: Vec<String>,
601 #[serde(default)]
602 pub barrier_group: Option<String>,
603 #[serde(default)]
604 pub use_worktrees: bool,
605}
606
607#[derive(Debug, Clone, Deserialize)]
608pub struct ChannelConfig {
609 pub target: String,
610 pub provider: String,
611 #[serde(default)]
614 pub bot_token: Option<String>,
615 #[serde(default)]
617 pub allowed_user_ids: Vec<i64>,
618}
619
620#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
621#[serde(rename_all = "lowercase")]
622pub enum RoleType {
623 User,
624 Architect,
625 Manager,
626 Engineer,
627}
628
629fn default_rotation_threshold() -> u32 {
632 20
633}
634
635fn default_workflow_mode() -> WorkflowMode {
636 WorkflowMode::Legacy
637}
638
639fn default_board_auto_dispatch() -> bool {
640 true
641}
642
643fn default_board_auto_replenish() -> bool {
644 true
645}
646
647fn default_state_reconciliation_interval_secs() -> u64 {
648 30
649}
650
651fn default_dispatch_stabilization_delay_secs() -> u64 {
652 30
653}
654
655fn default_dispatch_dedup_window_secs() -> u64 {
656 60
657}
658
659fn default_dispatch_manual_cooldown_secs() -> u64 {
660 30
661}
662
663fn default_standup_interval() -> u64 {
664 300
665}
666
667fn default_pipeline_starvation_threshold() -> Option<usize> {
668 Some(1)
669}
670
671fn default_output_lines() -> u32 {
672 30
673}
674
675fn default_instances() -> u32 {
676 1
677}
678
679fn default_escalation_threshold_secs() -> u64 {
680 3600
681}
682
683fn default_review_nudge_threshold_secs() -> u64 {
684 1800
685}
686
687fn default_review_timeout_secs() -> u64 {
688 7200
689}
690
691fn default_stall_threshold_secs() -> u64 {
692 300
693}
694
695fn default_max_stall_restarts() -> u32 {
696 2
697}
698
699fn default_health_check_interval_secs() -> u64 {
700 60
701}
702
703fn default_uncommitted_warn_threshold() -> usize {
704 200
705}
706
707fn default_enabled() -> bool {
708 true
709}
710
711fn default_orchestrator_pane() -> bool {
712 true
713}
714
715fn default_intervention_idle_grace_secs() -> u64 {
716 60
717}
718
719fn default_intervention_cooldown_secs() -> u64 {
720 120
721}
722
723fn default_utilization_recovery_interval_secs() -> u64 {
724 1200
725}
726
727fn default_event_log_max_bytes() -> u64 {
728 DEFAULT_EVENT_LOG_MAX_BYTES
729}
730
731fn default_retro_min_duration_secs() -> u64 {
732 60
733}
734
735fn default_shim_health_check_interval_secs() -> u64 {
736 60
737}
738
739fn default_shim_health_timeout_secs() -> u64 {
740 120
741}
742
743fn default_shim_shutdown_timeout_secs() -> u32 {
744 30
745}
746
747fn default_shim_working_state_timeout_secs() -> u64 {
748 600 }
750
751fn default_pending_queue_max_age_secs() -> u64 {
752 600 }