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