Skip to main content

batty_cli/team/config/
types.rs

1//! Type definitions for team configuration.
2
3use std::collections::HashMap;
4
5use serde::Deserialize;
6
7use super::super::DEFAULT_EVENT_LOG_MAX_BYTES;
8
9#[derive(Debug, Clone, Deserialize)]
10pub struct TeamConfig {
11    pub name: String,
12    /// Team-level default agent backend. Individual roles can override this
13    /// with their own `agent` field. Resolution order:
14    /// role-level agent > team-level agent > "claude" (hardcoded default).
15    #[serde(default)]
16    pub agent: Option<String>,
17    #[serde(default = "default_workflow_mode")]
18    pub workflow_mode: WorkflowMode,
19    #[serde(default)]
20    pub board: BoardConfig,
21    #[serde(default)]
22    pub standup: StandupConfig,
23    #[serde(default)]
24    pub automation: AutomationConfig,
25    #[serde(default)]
26    pub automation_sender: Option<String>,
27    /// External senders (e.g. email-router, slack-bridge) that are allowed to
28    /// message any role even though they are not team members.
29    #[serde(default)]
30    pub external_senders: Vec<String>,
31    #[serde(default = "default_orchestrator_pane")]
32    pub orchestrator_pane: bool,
33    #[serde(default)]
34    pub orchestrator_position: OrchestratorPosition,
35    #[serde(default)]
36    pub layout: Option<LayoutConfig>,
37    #[serde(default)]
38    pub workflow_policy: WorkflowPolicy,
39    #[serde(default)]
40    pub cost: CostConfig,
41    #[serde(default)]
42    pub grafana: GrafanaConfig,
43    /// When true, agents are spawned as shim subprocesses instead of
44    /// directly in tmux panes. The shim manages PTY, state classification,
45    /// and message delivery over a structured channel.
46    #[serde(default)]
47    pub use_shim: bool,
48    /// When true and `use_shim` is enabled, crashed agents are automatically
49    /// respawned instead of escalating to the manager.
50    #[serde(default)]
51    pub auto_respawn_on_crash: bool,
52    /// Interval in seconds between Ping health checks sent to shim handles.
53    #[serde(default = "default_shim_health_check_interval_secs")]
54    pub shim_health_check_interval_secs: u64,
55    /// Seconds without a Pong response before a shim handle is considered stale.
56    #[serde(default = "default_shim_health_timeout_secs")]
57    pub shim_health_timeout_secs: u64,
58    /// Seconds to wait for graceful shutdown before sending Kill.
59    #[serde(default = "default_shim_shutdown_timeout_secs")]
60    pub shim_shutdown_timeout_secs: u32,
61    #[serde(default = "default_event_log_max_bytes")]
62    pub event_log_max_bytes: u64,
63    #[serde(default = "default_retro_min_duration_secs")]
64    pub retro_min_duration_secs: u64,
65    pub roles: Vec<RoleDef>,
66}
67
68#[derive(Debug, Clone, Deserialize)]
69pub struct GrafanaConfig {
70    #[serde(default)]
71    pub enabled: bool,
72    #[serde(default = "default_grafana_port")]
73    pub port: u16,
74}
75
76impl Default for GrafanaConfig {
77    fn default() -> Self {
78        Self {
79            enabled: false,
80            port: default_grafana_port(),
81        }
82    }
83}
84
85fn default_grafana_port() -> u16 {
86    3000
87}
88
89#[derive(Debug, Clone, Deserialize, Default)]
90pub struct CostConfig {
91    #[serde(default)]
92    pub models: HashMap<String, ModelPricing>,
93}
94
95#[derive(Debug, Clone, Deserialize)]
96pub struct ModelPricing {
97    pub input_usd_per_mtok: f64,
98    #[serde(default)]
99    pub cached_input_usd_per_mtok: f64,
100    #[serde(default)]
101    pub cache_creation_input_usd_per_mtok: Option<f64>,
102    #[serde(default)]
103    pub cache_creation_5m_input_usd_per_mtok: Option<f64>,
104    #[serde(default)]
105    pub cache_creation_1h_input_usd_per_mtok: Option<f64>,
106    #[serde(default)]
107    pub cache_read_input_usd_per_mtok: f64,
108    pub output_usd_per_mtok: f64,
109    #[serde(default)]
110    pub reasoning_output_usd_per_mtok: Option<f64>,
111}
112
113#[derive(Debug, Clone, Deserialize)]
114pub struct WorkflowPolicy {
115    #[serde(default)]
116    pub wip_limit_per_engineer: Option<u32>,
117    #[serde(default)]
118    pub wip_limit_per_reviewer: Option<u32>,
119    #[serde(default = "default_pipeline_starvation_threshold")]
120    pub pipeline_starvation_threshold: Option<usize>,
121    #[serde(default = "default_escalation_threshold_secs")]
122    pub escalation_threshold_secs: u64,
123    #[serde(default = "default_review_nudge_threshold_secs")]
124    pub review_nudge_threshold_secs: u64,
125    #[serde(default = "default_review_timeout_secs")]
126    pub review_timeout_secs: u64,
127    #[serde(default)]
128    pub review_timeout_overrides: HashMap<String, ReviewTimeoutOverride>,
129    #[serde(default)]
130    pub auto_archive_done_after_secs: Option<u64>,
131    #[serde(default)]
132    pub capability_overrides: HashMap<String, Vec<String>>,
133    #[serde(default = "default_stall_threshold_secs")]
134    pub stall_threshold_secs: u64,
135    #[serde(default = "default_max_stall_restarts")]
136    pub max_stall_restarts: u32,
137    #[serde(default = "default_health_check_interval_secs")]
138    pub health_check_interval_secs: u64,
139    #[serde(default = "default_uncommitted_warn_threshold")]
140    pub uncommitted_warn_threshold: usize,
141    #[serde(default)]
142    pub auto_merge: AutoMergePolicy,
143}
144
145/// Per-priority override for review timeout thresholds.
146/// When a task's priority matches a key in `review_timeout_overrides`,
147/// these values replace the global defaults.
148#[derive(Debug, Clone, Deserialize)]
149pub struct ReviewTimeoutOverride {
150    /// Nudge threshold override (seconds). Falls back to global if absent.
151    pub review_nudge_threshold_secs: Option<u64>,
152    /// Escalation threshold override (seconds). Falls back to global if absent.
153    pub review_timeout_secs: Option<u64>,
154}
155
156impl Default for WorkflowPolicy {
157    fn default() -> Self {
158        Self {
159            wip_limit_per_engineer: None,
160            wip_limit_per_reviewer: None,
161            pipeline_starvation_threshold: default_pipeline_starvation_threshold(),
162            escalation_threshold_secs: default_escalation_threshold_secs(),
163            review_nudge_threshold_secs: default_review_nudge_threshold_secs(),
164            review_timeout_secs: default_review_timeout_secs(),
165            review_timeout_overrides: HashMap::new(),
166            auto_archive_done_after_secs: None,
167            capability_overrides: HashMap::new(),
168            stall_threshold_secs: default_stall_threshold_secs(),
169            max_stall_restarts: default_max_stall_restarts(),
170            health_check_interval_secs: default_health_check_interval_secs(),
171            uncommitted_warn_threshold: default_uncommitted_warn_threshold(),
172            auto_merge: AutoMergePolicy::default(),
173        }
174    }
175}
176
177fn default_sensitive_paths() -> Vec<String> {
178    vec![
179        "Cargo.toml".to_string(),
180        "team.yaml".to_string(),
181        ".env".to_string(),
182    ]
183}
184
185#[derive(Debug, Clone, Deserialize)]
186pub struct AutoMergePolicy {
187    #[serde(default)]
188    pub enabled: bool,
189    #[serde(default = "default_max_diff_lines")]
190    pub max_diff_lines: usize,
191    #[serde(default = "default_max_files_changed")]
192    pub max_files_changed: usize,
193    #[serde(default = "default_max_modules_touched")]
194    pub max_modules_touched: usize,
195    #[serde(default = "default_sensitive_paths")]
196    pub sensitive_paths: Vec<String>,
197    #[serde(default = "default_confidence_threshold")]
198    pub confidence_threshold: f64,
199    #[serde(default = "default_require_tests_pass")]
200    pub require_tests_pass: bool,
201}
202
203fn default_max_diff_lines() -> usize {
204    200
205}
206fn default_max_files_changed() -> usize {
207    5
208}
209fn default_max_modules_touched() -> usize {
210    2
211}
212fn default_confidence_threshold() -> f64 {
213    0.8
214}
215fn default_require_tests_pass() -> bool {
216    true
217}
218
219impl Default for AutoMergePolicy {
220    fn default() -> Self {
221        Self {
222            enabled: false,
223            max_diff_lines: default_max_diff_lines(),
224            max_files_changed: default_max_files_changed(),
225            max_modules_touched: default_max_modules_touched(),
226            sensitive_paths: default_sensitive_paths(),
227            confidence_threshold: default_confidence_threshold(),
228            require_tests_pass: default_require_tests_pass(),
229        }
230    }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum WorkflowMode {
236    #[default]
237    Legacy,
238    Hybrid,
239    WorkflowFirst,
240}
241
242#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum OrchestratorPosition {
245    #[default]
246    Bottom,
247    Left,
248}
249
250impl WorkflowMode {
251    #[cfg_attr(not(test), allow(dead_code))]
252    pub fn legacy_runtime_enabled(self) -> bool {
253        matches!(self, Self::Legacy | Self::Hybrid)
254    }
255
256    #[cfg_attr(not(test), allow(dead_code))]
257    pub fn workflow_state_primary(self) -> bool {
258        matches!(self, Self::WorkflowFirst)
259    }
260
261    pub fn enables_runtime_surface(self) -> bool {
262        matches!(self, Self::Hybrid | Self::WorkflowFirst)
263    }
264
265    pub fn as_str(self) -> &'static str {
266        match self {
267            Self::Legacy => "legacy",
268            Self::Hybrid => "hybrid",
269            Self::WorkflowFirst => "workflow_first",
270        }
271    }
272}
273
274#[derive(Debug, Clone, Deserialize)]
275pub struct BoardConfig {
276    #[serde(default = "default_rotation_threshold")]
277    pub rotation_threshold: u32,
278    #[serde(default = "default_board_auto_dispatch")]
279    pub auto_dispatch: bool,
280    #[serde(default = "default_dispatch_stabilization_delay_secs")]
281    pub dispatch_stabilization_delay_secs: u64,
282    #[serde(default = "default_dispatch_dedup_window_secs")]
283    pub dispatch_dedup_window_secs: u64,
284    #[serde(default = "default_dispatch_manual_cooldown_secs")]
285    pub dispatch_manual_cooldown_secs: u64,
286}
287
288impl Default for BoardConfig {
289    fn default() -> Self {
290        Self {
291            rotation_threshold: default_rotation_threshold(),
292            auto_dispatch: default_board_auto_dispatch(),
293            dispatch_stabilization_delay_secs: default_dispatch_stabilization_delay_secs(),
294            dispatch_dedup_window_secs: default_dispatch_dedup_window_secs(),
295            dispatch_manual_cooldown_secs: default_dispatch_manual_cooldown_secs(),
296        }
297    }
298}
299
300#[derive(Debug, Clone, Deserialize)]
301pub struct StandupConfig {
302    #[serde(default = "default_standup_interval")]
303    pub interval_secs: u64,
304    #[serde(default = "default_output_lines")]
305    pub output_lines: u32,
306}
307
308impl Default for StandupConfig {
309    fn default() -> Self {
310        Self {
311            interval_secs: default_standup_interval(),
312            output_lines: default_output_lines(),
313        }
314    }
315}
316
317#[derive(Debug, Clone, Deserialize)]
318pub struct AutomationConfig {
319    #[serde(default = "default_enabled")]
320    pub timeout_nudges: bool,
321    #[serde(default = "default_enabled")]
322    pub standups: bool,
323    #[serde(default = "default_enabled")]
324    pub failure_pattern_detection: bool,
325    #[serde(default = "default_enabled")]
326    pub triage_interventions: bool,
327    #[serde(default = "default_enabled")]
328    pub review_interventions: bool,
329    #[serde(default = "default_enabled")]
330    pub owned_task_interventions: bool,
331    #[serde(default = "default_enabled")]
332    pub manager_dispatch_interventions: bool,
333    #[serde(default = "default_enabled")]
334    pub architect_utilization_interventions: bool,
335    #[serde(default)]
336    pub replenishment_threshold: Option<usize>,
337    #[serde(default = "default_intervention_idle_grace_secs")]
338    pub intervention_idle_grace_secs: u64,
339    #[serde(default = "default_intervention_cooldown_secs")]
340    pub intervention_cooldown_secs: u64,
341    #[serde(default = "default_utilization_recovery_interval_secs")]
342    pub utilization_recovery_interval_secs: u64,
343    #[serde(default = "default_enabled")]
344    pub commit_before_reset: bool,
345}
346
347impl Default for AutomationConfig {
348    fn default() -> Self {
349        Self {
350            timeout_nudges: default_enabled(),
351            standups: default_enabled(),
352            failure_pattern_detection: default_enabled(),
353            triage_interventions: default_enabled(),
354            review_interventions: default_enabled(),
355            owned_task_interventions: default_enabled(),
356            manager_dispatch_interventions: default_enabled(),
357            architect_utilization_interventions: default_enabled(),
358            replenishment_threshold: None,
359            intervention_idle_grace_secs: default_intervention_idle_grace_secs(),
360            intervention_cooldown_secs: default_intervention_cooldown_secs(),
361            utilization_recovery_interval_secs: default_utilization_recovery_interval_secs(),
362            commit_before_reset: default_enabled(),
363        }
364    }
365}
366
367#[derive(Debug, Clone, Deserialize)]
368pub struct LayoutConfig {
369    pub zones: Vec<ZoneDef>,
370}
371
372#[derive(Debug, Clone, Deserialize)]
373pub struct ZoneDef {
374    pub name: String,
375    pub width_pct: u32,
376    #[serde(default)]
377    pub split: Option<SplitDef>,
378}
379
380#[derive(Debug, Clone, Deserialize)]
381pub struct SplitDef {
382    pub horizontal: u32,
383}
384
385#[derive(Debug, Clone, Deserialize)]
386pub struct RoleDef {
387    pub name: String,
388    pub role_type: RoleType,
389    #[serde(default)]
390    pub agent: Option<String>,
391    #[serde(default = "default_instances")]
392    pub instances: u32,
393    #[serde(default)]
394    pub prompt: Option<String>,
395    #[serde(default)]
396    pub talks_to: Vec<String>,
397    #[serde(default)]
398    pub channel: Option<String>,
399    #[serde(default)]
400    pub channel_config: Option<ChannelConfig>,
401    #[serde(default)]
402    pub nudge_interval_secs: Option<u64>,
403    #[serde(default)]
404    pub receives_standup: Option<bool>,
405    #[serde(default)]
406    pub standup_interval_secs: Option<u64>,
407    #[serde(default)]
408    #[allow(dead_code)] // Parsed for future ownership semantics but not yet enforced.
409    pub owns: Vec<String>,
410    #[serde(default)]
411    pub use_worktrees: bool,
412}
413
414#[derive(Debug, Clone, Deserialize)]
415pub struct ChannelConfig {
416    pub target: String,
417    pub provider: String,
418    /// Telegram bot token for native API (optional; falls back to provider CLI).
419    /// Can also be set via `BATTY_TELEGRAM_BOT_TOKEN` env var.
420    #[serde(default)]
421    pub bot_token: Option<String>,
422    /// Telegram user IDs allowed to send messages (access control).
423    #[serde(default)]
424    pub allowed_user_ids: Vec<i64>,
425}
426
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
428#[serde(rename_all = "lowercase")]
429pub enum RoleType {
430    User,
431    Architect,
432    Manager,
433    Engineer,
434}
435
436// --- Default value functions ---
437
438fn default_rotation_threshold() -> u32 {
439    20
440}
441
442fn default_workflow_mode() -> WorkflowMode {
443    WorkflowMode::Legacy
444}
445
446fn default_board_auto_dispatch() -> bool {
447    true
448}
449
450fn default_dispatch_stabilization_delay_secs() -> u64 {
451    30
452}
453
454fn default_dispatch_dedup_window_secs() -> u64 {
455    60
456}
457
458fn default_dispatch_manual_cooldown_secs() -> u64 {
459    30
460}
461
462fn default_standup_interval() -> u64 {
463    300
464}
465
466fn default_pipeline_starvation_threshold() -> Option<usize> {
467    Some(1)
468}
469
470fn default_output_lines() -> u32 {
471    30
472}
473
474fn default_instances() -> u32 {
475    1
476}
477
478fn default_escalation_threshold_secs() -> u64 {
479    3600
480}
481
482fn default_review_nudge_threshold_secs() -> u64 {
483    1800
484}
485
486fn default_review_timeout_secs() -> u64 {
487    7200
488}
489
490fn default_stall_threshold_secs() -> u64 {
491    300
492}
493
494fn default_max_stall_restarts() -> u32 {
495    2
496}
497
498fn default_health_check_interval_secs() -> u64 {
499    60
500}
501
502fn default_uncommitted_warn_threshold() -> usize {
503    200
504}
505
506fn default_enabled() -> bool {
507    true
508}
509
510fn default_orchestrator_pane() -> bool {
511    true
512}
513
514fn default_intervention_idle_grace_secs() -> u64 {
515    60
516}
517
518fn default_intervention_cooldown_secs() -> u64 {
519    120
520}
521
522fn default_utilization_recovery_interval_secs() -> u64 {
523    1200
524}
525
526fn default_event_log_max_bytes() -> u64 {
527    DEFAULT_EVENT_LOG_MAX_BYTES
528}
529
530fn default_retro_min_duration_secs() -> u64 {
531    60
532}
533
534fn default_shim_health_check_interval_secs() -> u64 {
535    60
536}
537
538fn default_shim_health_timeout_secs() -> u64 {
539    120
540}
541
542fn default_shim_shutdown_timeout_secs() -> u32 {
543    30
544}