Skip to main content

task_graph_mcp/config/
types.rs

1//! Configuration types and structures.
2//!
3//! This module contains all the configuration types used throughout the application.
4
5use crate::format::OutputFormat;
6use anyhow::{Result, anyhow};
7use heck::{ToKebabCase, ToLowerCamelCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase};
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::path::{Path, PathBuf};
11
12/// Default port for the web dashboard.
13pub const DEFAULT_UI_PORT: u16 = 31994;
14
15/// Default number of words for generated IDs.
16pub const DEFAULT_ID_WORDS: u8 = 4;
17
18/// Case style for generated IDs.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum IdCase {
22    /// kebab-case (default): happy-turtle-swift-fox
23    #[default]
24    KebabCase,
25    /// snake_case: happy_turtle_swift_fox
26    SnakeCase,
27    /// camelCase: happyTurtleSwiftFox
28    CamelCase,
29    /// PascalCase: HappyTurtleSwiftFox
30    PascalCase,
31    /// lowercase: happyturtleswiftfox
32    Lowercase,
33    /// UPPERCASE: HAPPYTURTLESWIFTFOX
34    Uppercase,
35    /// Title Case: Happy Turtle Swift Fox
36    TitleCase,
37}
38
39/// ID generation configuration.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct IdsConfig {
42    /// Number of words for generated task IDs (default: 4).
43    #[serde(default = "default_id_words")]
44    pub task_id_words: u8,
45
46    /// Number of words for generated agent IDs (default: 4).
47    #[serde(default = "default_id_words")]
48    pub agent_id_words: u8,
49
50    /// Case style for generated IDs (default: kebab-case).
51    #[serde(default)]
52    pub id_case: IdCase,
53}
54
55fn default_id_words() -> u8 {
56    DEFAULT_ID_WORDS
57}
58
59impl Default for IdsConfig {
60    fn default() -> Self {
61        Self {
62            task_id_words: DEFAULT_ID_WORDS,
63            agent_id_words: DEFAULT_ID_WORDS,
64            id_case: IdCase::default(),
65        }
66    }
67}
68
69impl IdCase {
70    /// Convert a kebab-case string to the target case style.
71    /// Input is expected to be lowercase words separated by hyphens.
72    pub fn convert(&self, input: &str) -> String {
73        match self {
74            IdCase::KebabCase => input.to_kebab_case(),
75            IdCase::SnakeCase => input.to_snake_case(),
76            IdCase::CamelCase => input.to_lower_camel_case(),
77            IdCase::PascalCase => input.to_upper_camel_case(),
78            IdCase::Lowercase => input.replace('-', "").to_lowercase(),
79            IdCase::Uppercase => input.replace('-', "").to_uppercase(),
80            IdCase::TitleCase => input.to_title_case(),
81        }
82    }
83
84    /// Get the separator for this case style (used in petname generation).
85    /// Returns None for cases that don't use separators (camelCase, PascalCase, etc.).
86    pub fn separator(&self) -> Option<&'static str> {
87        match self {
88            IdCase::KebabCase => Some("-"),
89            IdCase::SnakeCase => Some("_"),
90            IdCase::TitleCase => Some(" "),
91            IdCase::CamelCase | IdCase::PascalCase | IdCase::Lowercase | IdCase::Uppercase => None,
92        }
93    }
94}
95
96/// UI mode for the server.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum UiMode {
100    /// No UI, MCP server only (default)
101    #[default]
102    None,
103    /// Enable web dashboard UI
104    Web,
105}
106
107/// UI configuration for the web dashboard.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UiConfig {
110    /// UI mode: none (MCP only) or web (enable dashboard).
111    #[serde(default)]
112    pub mode: UiMode,
113
114    /// Port for the web dashboard (default: 31994).
115    #[serde(default = "default_ui_port")]
116    pub port: u16,
117
118    /// Initial retry delay in milliseconds when dashboard fails to start (default: 15000).
119    #[serde(default = "default_retry_initial_ms")]
120    pub retry_initial_ms: u64,
121
122    /// Jitter range in milliseconds for retry delay (default: 5000, meaning ±5s).
123    #[serde(default = "default_retry_jitter_ms")]
124    pub retry_jitter_ms: u64,
125
126    /// Maximum retry interval in milliseconds (default: 240000 = 4 minutes).
127    #[serde(default = "default_retry_max_ms")]
128    pub retry_max_ms: u64,
129
130    /// Exponential backoff multiplier (default: 2.0).
131    #[serde(default = "default_retry_multiplier")]
132    pub retry_multiplier: f64,
133}
134
135impl Default for UiConfig {
136    fn default() -> Self {
137        Self {
138            mode: UiMode::default(),
139            port: default_ui_port(),
140            retry_initial_ms: default_retry_initial_ms(),
141            retry_jitter_ms: default_retry_jitter_ms(),
142            retry_max_ms: default_retry_max_ms(),
143            retry_multiplier: default_retry_multiplier(),
144        }
145    }
146}
147
148fn default_ui_port() -> u16 {
149    DEFAULT_UI_PORT
150}
151
152fn default_retry_initial_ms() -> u64 {
153    15_000 // 15 seconds
154}
155
156fn default_retry_jitter_ms() -> u64 {
157    5_000 // ±5 seconds
158}
159
160fn default_retry_max_ms() -> u64 {
161    240_000 // 4 minutes
162}
163
164fn default_retry_multiplier() -> f64 {
165    2.0
166}
167
168/// Auto-advance configuration for automatically transitioning tasks when dependencies are satisfied.
169#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170pub struct AutoAdvanceConfig {
171    /// Enable auto-advance when dependencies are satisfied (default: false).
172    #[serde(default)]
173    pub enabled: bool,
174
175    /// Target state for auto-advanced tasks (e.g., "ready").
176    /// If None, tasks remain in their current state even when unblocked.
177    #[serde(default)]
178    pub target_state: Option<String>,
179}
180
181/// Behavior for unknown attachment keys.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "snake_case")]
184pub enum UnknownKeyBehavior {
185    /// Silently use default mime/mode.
186    Allow,
187    /// Use defaults but return a warning in the response (default).
188    #[default]
189    Warn,
190    /// Reject unknown keys with an error.
191    Reject,
192}
193
194/// Enforcement level for workflow gates (checklists that must be satisfied before status transitions).
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "snake_case")]
197pub enum GateEnforcement {
198    /// Advisory only, never blocks transitions. Unsatisfied gates are reported but do not prevent status changes.
199    Allow,
200    /// Blocks transition unless force=true (default). Requires explicit override to proceed with unsatisfied gates.
201    #[default]
202    Warn,
203    /// Hard block, cannot be forced. Transition is rejected until gate requirements are satisfied.
204    Reject,
205}
206
207/// Definition of a gate (checklist item) for status or phase exits.
208///
209/// Gates are checked when transitioning out of a status or phase. A gate is satisfied
210/// when the task has an attachment with a matching type (e.g., "gate/tests", "gate/commit").
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct GateDefinition {
213    /// Attachment type that satisfies this gate (e.g., "gate/tests", "gate/commit").
214    #[serde(rename = "type")]
215    pub gate_type: String,
216
217    /// Enforcement level for this gate.
218    #[serde(default)]
219    pub enforcement: GateEnforcement,
220
221    /// Human-readable description of what this gate requires.
222    #[serde(default)]
223    pub description: String,
224}
225
226/// Definition of a preconfigured attachment key.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct AttachmentKeyDefinition {
229    /// Default MIME type for this key.
230    pub mime: String,
231    /// Default mode: "append" or "replace".
232    #[serde(default = "default_append_mode")]
233    pub mode: String,
234}
235
236fn default_append_mode() -> String {
237    "append".to_string()
238}
239
240/// Attachments configuration with preconfigured key definitions.
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AttachmentsConfig {
243    /// Behavior for unknown attachment keys (allow, warn, reject).
244    #[serde(default)]
245    pub unknown_key: UnknownKeyBehavior,
246    /// Preconfigured attachment key definitions.
247    #[serde(default = "AttachmentsConfig::default_definitions")]
248    pub definitions: HashMap<String, AttachmentKeyDefinition>,
249}
250
251impl Default for AttachmentsConfig {
252    fn default() -> Self {
253        Self {
254            unknown_key: UnknownKeyBehavior::default(),
255            definitions: Self::default_definitions(),
256        }
257    }
258}
259
260impl AttachmentsConfig {
261    /// Default attachment key definitions.
262    pub fn default_definitions() -> HashMap<String, AttachmentKeyDefinition> {
263        let mut defs = HashMap::new();
264
265        defs.insert(
266            "commit".to_string(),
267            AttachmentKeyDefinition {
268                mime: "text/git.hash".to_string(),
269                mode: "append".to_string(),
270            },
271        );
272
273        defs.insert(
274            "checkin".to_string(),
275            AttachmentKeyDefinition {
276                mime: "text/p4.changelist".to_string(),
277                mode: "append".to_string(),
278            },
279        );
280
281        defs.insert(
282            "meta".to_string(),
283            AttachmentKeyDefinition {
284                mime: "application/json".to_string(),
285                mode: "replace".to_string(),
286            },
287        );
288
289        defs.insert(
290            "note".to_string(),
291            AttachmentKeyDefinition {
292                mime: "text/plain".to_string(),
293                mode: "append".to_string(),
294            },
295        );
296
297        defs.insert(
298            "log".to_string(),
299            AttachmentKeyDefinition {
300                mime: "text/plain".to_string(),
301                mode: "append".to_string(),
302            },
303        );
304
305        defs.insert(
306            "error".to_string(),
307            AttachmentKeyDefinition {
308                mime: "text/plain".to_string(),
309                mode: "append".to_string(),
310            },
311        );
312
313        defs.insert(
314            "output".to_string(),
315            AttachmentKeyDefinition {
316                mime: "text/plain".to_string(),
317                mode: "append".to_string(),
318            },
319        );
320
321        defs.insert(
322            "diff".to_string(),
323            AttachmentKeyDefinition {
324                mime: "text/x-diff".to_string(),
325                mode: "append".to_string(),
326            },
327        );
328
329        defs.insert(
330            "changelist".to_string(),
331            AttachmentKeyDefinition {
332                mime: "text/plain".to_string(),
333                mode: "append".to_string(),
334            },
335        );
336
337        defs.insert(
338            "plan".to_string(),
339            AttachmentKeyDefinition {
340                mime: "text/markdown".to_string(),
341                mode: "replace".to_string(),
342            },
343        );
344
345        defs.insert(
346            "result".to_string(),
347            AttachmentKeyDefinition {
348                mime: "application/json".to_string(),
349                mode: "replace".to_string(),
350            },
351        );
352
353        defs.insert(
354            "context".to_string(),
355            AttachmentKeyDefinition {
356                mime: "text/plain".to_string(),
357                mode: "replace".to_string(),
358            },
359        );
360
361        // Gate attachments - for workflow gate satisfaction
362        defs.insert(
363            "gate/tests".to_string(),
364            AttachmentKeyDefinition {
365                mime: "text/plain".to_string(),
366                mode: "append".to_string(),
367            },
368        );
369
370        defs.insert(
371            "gate/commit".to_string(),
372            AttachmentKeyDefinition {
373                mime: "text/plain".to_string(),
374                mode: "append".to_string(),
375            },
376        );
377
378        defs.insert(
379            "gate/review".to_string(),
380            AttachmentKeyDefinition {
381                mime: "text/plain".to_string(),
382                mode: "append".to_string(),
383            },
384        );
385
386        defs
387    }
388
389    /// Get the definition for a key, if it exists.
390    pub fn get_definition(&self, key: &str) -> Option<&AttachmentKeyDefinition> {
391        self.definitions.get(key)
392    }
393
394    /// Check if a key is a known/configured key.
395    pub fn is_known_key(&self, key: &str) -> bool {
396        self.definitions.contains_key(key)
397    }
398
399    /// Get the default MIME type for a key, or fallback to text/plain.
400    pub fn get_mime_default(&self, key: &str) -> &str {
401        self.definitions
402            .get(key)
403            .map(|d| d.mime.as_str())
404            .unwrap_or("text/plain")
405    }
406
407    /// Get the default mode for a key, or fallback to "append".
408    pub fn get_mode_default(&self, key: &str) -> &str {
409        self.definitions
410            .get(key)
411            .map(|d| d.mode.as_str())
412            .unwrap_or("append")
413    }
414}
415
416/// Definition of a preconfigured tag.
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct TagDefinition {
419    /// Category for grouping (e.g., "language", "domain", "type").
420    #[serde(default)]
421    pub category: Option<String>,
422    /// Human-readable description.
423    #[serde(default)]
424    pub description: Option<String>,
425}
426
427/// Tags configuration with preconfigured tag definitions.
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct TagsConfig {
430    /// Behavior for unknown tags (allow, warn, reject).
431    #[serde(default)]
432    pub unknown_tag: UnknownKeyBehavior,
433    /// Preconfigured tag definitions.
434    #[serde(default)]
435    pub definitions: HashMap<String, TagDefinition>,
436}
437
438impl Default for TagsConfig {
439    fn default() -> Self {
440        Self {
441            unknown_tag: UnknownKeyBehavior::default(),
442            definitions: HashMap::new(),
443        }
444    }
445}
446
447impl TagsConfig {
448    /// Check if a tag is a known/defined tag.
449    pub fn is_known_tag(&self, tag: &str) -> bool {
450        self.definitions.contains_key(tag)
451    }
452
453    /// Get all defined tag names.
454    pub fn tag_names(&self) -> Vec<&str> {
455        self.definitions.keys().map(|s| s.as_str()).collect()
456    }
457
458    /// Get all tags in a specific category.
459    pub fn tags_in_category(&self, category: &str) -> Vec<&str> {
460        self.definitions
461            .iter()
462            .filter(|(_, def)| def.category.as_deref() == Some(category))
463            .map(|(name, _)| name.as_str())
464            .collect()
465    }
466
467    /// Get all unique categories.
468    pub fn categories(&self) -> Vec<&str> {
469        let mut cats: Vec<&str> = self
470            .definitions
471            .values()
472            .filter_map(|def| def.category.as_deref())
473            .collect();
474        cats.sort();
475        cats.dedup();
476        cats
477    }
478
479    /// Validate a single tag, returning Ok(None) if valid, Ok(Some(warning)) for warn mode, or Err for reject.
480    pub fn validate_tag(&self, tag: &str) -> Result<Option<String>> {
481        if self.is_known_tag(tag) {
482            return Ok(None);
483        }
484
485        match self.unknown_tag {
486            UnknownKeyBehavior::Allow => Ok(None),
487            UnknownKeyBehavior::Warn => Ok(Some(format!(
488                "Unknown tag '{}'. Known tags: {:?}",
489                tag,
490                self.tag_names()
491            ))),
492            UnknownKeyBehavior::Reject => Err(anyhow!(
493                "Unknown tag '{}'. Configure in tags.definitions or set unknown_tag to 'allow' or 'warn'. Known tags: {:?}",
494                tag,
495                self.tag_names()
496            )),
497        }
498    }
499
500    /// Validate multiple tags, collecting warnings and stopping on first reject.
501    pub fn validate_tags(&self, tags: &[String]) -> Result<Vec<String>> {
502        let mut warnings = Vec::new();
503        for tag in tags {
504            if let Some(warning) = self.validate_tag(tag)? {
505                warnings.push(warning);
506            }
507        }
508        Ok(warnings)
509    }
510}
511
512/// Server configuration.
513#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514pub struct Config {
515    #[serde(default)]
516    pub server: ServerConfig,
517
518    #[serde(default)]
519    pub paths: PathsConfig,
520
521    #[serde(default)]
522    pub states: StatesConfig,
523
524    #[serde(default)]
525    pub dependencies: DependenciesConfig,
526
527    #[serde(default)]
528    pub auto_advance: AutoAdvanceConfig,
529
530    #[serde(default)]
531    pub attachments: AttachmentsConfig,
532
533    #[serde(default)]
534    pub phases: PhasesConfig,
535
536    #[serde(default)]
537    pub tags: TagsConfig,
538
539    #[serde(default)]
540    pub ids: IdsConfig,
541}
542
543/// Paths configured for the server, returned by connect.
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct ServerPaths {
546    /// Path to the SQLite database file.
547    pub db_path: PathBuf,
548    /// Path to the media directory for file attachments.
549    pub media_dir: PathBuf,
550    /// Path to the log directory.
551    pub log_dir: PathBuf,
552    /// Path to the configuration file (if one was loaded).
553    pub config_path: Option<PathBuf>,
554}
555
556/// Server-specific configuration.
557#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct ServerConfig {
559    /// Path to the SQLite database file.
560    #[serde(default = "default_db_path")]
561    pub db_path: PathBuf,
562
563    /// Path to the media directory for file attachments.
564    #[serde(default = "default_media_dir")]
565    pub media_dir: PathBuf,
566
567    /// Maximum claims per agent.
568    #[serde(default = "default_claim_limit")]
569    pub claim_limit: i32,
570
571    /// Timeout for stale claims in seconds.
572    #[serde(default = "default_stale_timeout")]
573    pub stale_timeout_seconds: i64,
574
575    /// Default output format for query results (json or markdown).
576    #[serde(default)]
577    pub default_format: OutputFormat,
578
579    /// Path to the skills directory for skill overrides.
580    #[serde(default = "default_skills_dir")]
581    pub skills_dir: PathBuf,
582
583    /// Path to the log directory.
584    #[serde(default = "default_log_dir")]
585    pub log_dir: PathBuf,
586
587    /// UI configuration for the web dashboard.
588    #[serde(default)]
589    pub ui: UiConfig,
590
591    /// Default workflow name (e.g., "swarm" to use workflow-swarm.yaml).
592    /// If set, this workflow is used as the default for workers that don't specify one.
593    /// The named workflow is also cached under both its name and "default".
594    #[serde(default, skip_serializing_if = "Option::is_none")]
595    pub default_workflow: Option<String>,
596}
597
598impl Default for ServerConfig {
599    fn default() -> Self {
600        Self {
601            db_path: default_db_path(),
602            media_dir: default_media_dir(),
603            claim_limit: default_claim_limit(),
604            stale_timeout_seconds: default_stale_timeout(),
605            default_format: OutputFormat::default(),
606            skills_dir: default_skills_dir(),
607            log_dir: default_log_dir(),
608            ui: UiConfig::default(),
609            default_workflow: None,
610        }
611    }
612}
613
614fn default_db_path() -> PathBuf {
615    PathBuf::from("task-graph/tasks.db")
616}
617
618fn default_media_dir() -> PathBuf {
619    PathBuf::from("task-graph/media")
620}
621
622fn default_skills_dir() -> PathBuf {
623    PathBuf::from("task-graph/skills")
624}
625
626fn default_log_dir() -> PathBuf {
627    PathBuf::from("task-graph/logs")
628}
629
630fn default_paths_root() -> String {
631    ".".to_string()
632}
633
634fn default_claim_limit() -> i32 {
635    5
636}
637
638fn default_stale_timeout() -> i64 {
639    900 // 15 minutes
640}
641
642/// Path handling configuration.
643#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct PathsConfig {
645    /// Root directory for sandboxing (default: ".")
646    #[serde(default = "default_paths_root")]
647    pub root: String,
648
649    /// Style for representing file paths.
650    #[serde(default)]
651    pub style: PathStyle,
652
653    /// Auto-map single-letter Windows drives (default: false)
654    #[serde(default)]
655    pub map_windows_drives: bool,
656
657    /// Prefix mappings (prefix -> path)
658    /// Values can be: literal path, $ENV_VAR, or ${config.path}
659    #[serde(default)]
660    pub mappings: HashMap<String, String>,
661}
662
663impl Default for PathsConfig {
664    fn default() -> Self {
665        Self {
666            root: default_paths_root(),
667            style: PathStyle::Relative,
668            map_windows_drives: false,
669            mappings: HashMap::new(),
670        }
671    }
672}
673
674/// Path style for file locks.
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
676#[serde(rename_all = "snake_case")]
677#[derive(Default)]
678pub enum PathStyle {
679    /// Relative paths (e.g., src/main.rs)
680    #[default]
681    Relative,
682    /// Project-prefixed paths (e.g., ${project}/src/main.rs)
683    ProjectPrefixed,
684}
685
686/// Task state configuration.
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub struct StatesConfig {
689    /// Default state for new tasks.
690    #[serde(default = "default_initial_state")]
691    pub initial: String,
692
693    /// Default state for tasks when their owner disconnects (must be untimed).
694    #[serde(default = "default_disconnect_state")]
695    pub disconnect_state: String,
696
697    /// States that block dependent tasks (tasks in these states count as "not done").
698    #[serde(default = "default_blocking_states")]
699    pub blocking_states: Vec<String>,
700
701    /// State definitions with allowed transitions and timing behavior.
702    #[serde(default = "default_state_definitions")]
703    pub definitions: HashMap<String, StateDefinition>,
704}
705
706impl Default for StatesConfig {
707    fn default() -> Self {
708        Self {
709            initial: default_initial_state(),
710            disconnect_state: default_disconnect_state(),
711            blocking_states: default_blocking_states(),
712            definitions: default_state_definitions(),
713        }
714    }
715}
716
717fn default_initial_state() -> String {
718    "pending".to_string()
719}
720
721fn default_disconnect_state() -> String {
722    "pending".to_string()
723}
724
725fn default_blocking_states() -> Vec<String> {
726    vec![
727        "pending".to_string(),
728        "assigned".to_string(),
729        "working".to_string(),
730    ]
731}
732
733fn default_state_definitions() -> HashMap<String, StateDefinition> {
734    let mut defs = HashMap::new();
735
736    defs.insert(
737        "pending".to_string(),
738        StateDefinition {
739            exits: vec![
740                "assigned".to_string(),
741                "working".to_string(),
742                "cancelled".to_string(),
743            ],
744            timed: false,
745        },
746    );
747
748    defs.insert(
749        "assigned".to_string(),
750        StateDefinition {
751            exits: vec![
752                "working".to_string(),
753                "pending".to_string(),
754                "cancelled".to_string(),
755            ],
756            timed: false,
757        },
758    );
759
760    defs.insert(
761        "working".to_string(),
762        StateDefinition {
763            exits: vec![
764                "completed".to_string(),
765                "failed".to_string(),
766                "pending".to_string(),
767            ],
768            timed: true,
769        },
770    );
771
772    defs.insert(
773        "completed".to_string(),
774        StateDefinition {
775            exits: vec!["pending".to_string()],
776            timed: false,
777        },
778    );
779
780    defs.insert(
781        "failed".to_string(),
782        StateDefinition {
783            exits: vec!["pending".to_string()],
784            timed: false,
785        },
786    );
787
788    defs.insert(
789        "cancelled".to_string(),
790        StateDefinition {
791            exits: vec![],
792            timed: false,
793        },
794    );
795
796    defs
797}
798
799/// Definition of a single task state.
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct StateDefinition {
802    /// Allowed states to transition to from this state.
803    #[serde(default)]
804    pub exits: Vec<String>,
805
806    /// Whether time spent in this state should be tracked (accumulated to time_actual_ms).
807    #[serde(default)]
808    pub timed: bool,
809}
810
811/// Dependency type configuration.
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct DependenciesConfig {
814    /// Dependency type definitions.
815    #[serde(default = "default_dependency_definitions")]
816    pub definitions: HashMap<String, DependencyDefinition>,
817}
818
819impl Default for DependenciesConfig {
820    fn default() -> Self {
821        Self {
822            definitions: default_dependency_definitions(),
823        }
824    }
825}
826
827/// Definition of a dependency type.
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct DependencyDefinition {
830    /// Display orientation: "horizontal" (same level) or "vertical" (parent-child).
831    pub display: DependencyDisplay,
832
833    /// What this dependency blocks: "start" (blocks claiming) or "completion" (blocks completing).
834    pub blocks: BlockTarget,
835}
836
837/// Display orientation for dependency visualization.
838#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
839#[serde(rename_all = "snake_case")]
840pub enum DependencyDisplay {
841    /// Same level dependencies (blocks, follows).
842    Horizontal,
843    /// Parent-child relationships (contains).
844    Vertical,
845}
846
847/// What a dependency blocks.
848#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
849#[serde(rename_all = "snake_case")]
850pub enum BlockTarget {
851    /// Does not block - informational link only.
852    None,
853    /// Blocks the task from being started/claimed.
854    Start,
855    /// Blocks the task from being completed.
856    Completion,
857}
858
859fn default_dependency_definitions() -> HashMap<String, DependencyDefinition> {
860    let mut defs = HashMap::new();
861
862    // Primary workflow types (blocking)
863    defs.insert(
864        "blocks".to_string(),
865        DependencyDefinition {
866            display: DependencyDisplay::Horizontal,
867            blocks: BlockTarget::Start,
868        },
869    );
870
871    defs.insert(
872        "follows".to_string(),
873        DependencyDefinition {
874            display: DependencyDisplay::Horizontal,
875            blocks: BlockTarget::Start,
876        },
877    );
878
879    defs.insert(
880        "contains".to_string(),
881        DependencyDefinition {
882            display: DependencyDisplay::Vertical,
883            blocks: BlockTarget::Completion,
884        },
885    );
886
887    // Non-blocking relationship types
888    defs.insert(
889        "duplicate".to_string(),
890        DependencyDefinition {
891            display: DependencyDisplay::Horizontal,
892            blocks: BlockTarget::None,
893        },
894    );
895
896    defs.insert(
897        "see-also".to_string(),
898        DependencyDefinition {
899            display: DependencyDisplay::Horizontal,
900            blocks: BlockTarget::None,
901        },
902    );
903
904    defs.insert(
905        "relates-to".to_string(),
906        DependencyDefinition {
907            display: DependencyDisplay::Horizontal,
908            blocks: BlockTarget::None,
909        },
910    );
911
912    defs
913}
914
915impl DependenciesConfig {
916    /// Check if a dependency type is valid.
917    pub fn is_valid_dep_type(&self, dep_type: &str) -> bool {
918        self.definitions.contains_key(dep_type)
919    }
920
921    /// Get the definition for a dependency type.
922    pub fn get_definition(&self, dep_type: &str) -> Option<&DependencyDefinition> {
923        self.definitions.get(dep_type)
924    }
925
926    /// Get all dependency types that block start.
927    pub fn start_blocking_types(&self) -> Vec<&str> {
928        self.definitions
929            .iter()
930            .filter(|(_, def)| def.blocks == BlockTarget::Start)
931            .map(|(name, _)| name.as_str())
932            .collect()
933    }
934
935    /// Get all dependency types that block completion.
936    pub fn completion_blocking_types(&self) -> Vec<&str> {
937        self.definitions
938            .iter()
939            .filter(|(_, def)| def.blocks == BlockTarget::Completion)
940            .map(|(name, _)| name.as_str())
941            .collect()
942    }
943
944    /// Get all vertical (parent-child) dependency types.
945    pub fn vertical_types(&self) -> Vec<&str> {
946        self.definitions
947            .iter()
948            .filter(|(_, def)| def.display == DependencyDisplay::Vertical)
949            .map(|(name, _)| name.as_str())
950            .collect()
951    }
952
953    /// Get all dependency type names.
954    pub fn dep_type_names(&self) -> Vec<&str> {
955        self.definitions.keys().map(|s| s.as_str()).collect()
956    }
957
958    /// Validate the dependencies configuration.
959    pub fn validate(&self) -> anyhow::Result<()> {
960        if self.definitions.is_empty() {
961            return Err(anyhow::anyhow!(
962                "At least one dependency type must be defined"
963            ));
964        }
965
966        // Check for at least one start-blocking type (for task sequencing)
967        let has_start_blocking = self
968            .definitions
969            .values()
970            .any(|d| d.blocks == BlockTarget::Start);
971        if !has_start_blocking {
972            return Err(anyhow::anyhow!(
973                "At least one dependency type with blocks: start must be defined"
974            ));
975        }
976
977        Ok(())
978    }
979}
980
981impl StatesConfig {
982    /// Check if a state is a valid defined state.
983    pub fn is_valid_state(&self, state: &str) -> bool {
984        self.definitions.contains_key(state)
985    }
986
987    /// Check if a transition from one state to another is allowed.
988    pub fn is_valid_transition(&self, from: &str, to: &str) -> bool {
989        if let Some(def) = self.definitions.get(from) {
990            def.exits.contains(&to.to_string())
991        } else {
992            false
993        }
994    }
995
996    /// Check if a state is timed (accumulates duration).
997    pub fn is_timed_state(&self, state: &str) -> bool {
998        self.definitions
999            .get(state)
1000            .map(|d| d.timed)
1001            .unwrap_or(false)
1002    }
1003
1004    /// Check if a state is terminal (has no exits).
1005    pub fn is_terminal_state(&self, state: &str) -> bool {
1006        self.definitions
1007            .get(state)
1008            .map(|d| d.exits.is_empty())
1009            .unwrap_or(false)
1010    }
1011
1012    /// Check if a state is a blocking state (blocks dependents).
1013    pub fn is_blocking_state(&self, state: &str) -> bool {
1014        self.blocking_states.contains(&state.to_string())
1015    }
1016
1017    /// Get all defined state names.
1018    pub fn state_names(&self) -> Vec<&str> {
1019        self.definitions.keys().map(|s| s.as_str()).collect()
1020    }
1021
1022    /// Get allowed exit states for a given state.
1023    pub fn get_exits(&self, state: &str) -> Vec<&str> {
1024        self.definitions
1025            .get(state)
1026            .map(|d| d.exits.iter().map(|s| s.as_str()).collect())
1027            .unwrap_or_default()
1028    }
1029
1030    /// Get all untimed state names (valid for disconnect final_state).
1031    pub fn untimed_state_names(&self) -> Vec<&str> {
1032        self.definitions
1033            .iter()
1034            .filter(|(_, def)| !def.timed)
1035            .map(|(name, _)| name.as_str())
1036            .collect()
1037    }
1038
1039    /// Validate the states configuration.
1040    pub fn validate(&self) -> Result<()> {
1041        // Check initial state exists
1042        if !self.definitions.contains_key(&self.initial) {
1043            return Err(anyhow!(
1044                "Initial state '{}' is not defined in state definitions",
1045                self.initial
1046            ));
1047        }
1048
1049        // Check disconnect_state exists and is not timed
1050        if !self.definitions.contains_key(&self.disconnect_state) {
1051            return Err(anyhow!(
1052                "Disconnect state '{}' is not defined in state definitions",
1053                self.disconnect_state
1054            ));
1055        }
1056        if self.is_timed_state(&self.disconnect_state) {
1057            return Err(anyhow!(
1058                "Disconnect state '{}' must not be a timed state",
1059                self.disconnect_state
1060            ));
1061        }
1062
1063        // Check all blocking_states exist
1064        for state in &self.blocking_states {
1065            if !self.definitions.contains_key(state) {
1066                return Err(anyhow!(
1067                    "Blocking state '{}' is not defined in state definitions",
1068                    state
1069                ));
1070            }
1071        }
1072
1073        // Check all exit targets exist
1074        for (state_name, def) in &self.definitions {
1075            for exit in &def.exits {
1076                if !self.definitions.contains_key(exit) {
1077                    return Err(anyhow!(
1078                        "State '{}' has exit '{}' which is not defined",
1079                        state_name,
1080                        exit
1081                    ));
1082                }
1083            }
1084        }
1085
1086        // Check at least one terminal state exists
1087        let has_terminal = self.definitions.values().any(|d| d.exits.is_empty());
1088        if !has_terminal {
1089            return Err(anyhow!(
1090                "At least one terminal state (with empty exits) must be defined"
1091            ));
1092        }
1093
1094        Ok(())
1095    }
1096}
1097
1098/// Phase configuration for categorizing type of work.
1099#[derive(Debug, Clone, Serialize, Deserialize)]
1100pub struct PhasesConfig {
1101    /// Behavior for unknown phase values (allow, warn, reject).
1102    #[serde(default)]
1103    pub unknown_phase: UnknownKeyBehavior,
1104
1105    /// Known phase definitions.
1106    #[serde(default = "default_phases")]
1107    pub definitions: HashSet<String>,
1108}
1109
1110impl Default for PhasesConfig {
1111    fn default() -> Self {
1112        Self {
1113            unknown_phase: UnknownKeyBehavior::Warn,
1114            definitions: default_phases(),
1115        }
1116    }
1117}
1118
1119fn default_phases() -> HashSet<String> {
1120    [
1121        "deliver",   // Top-level deliverable
1122        "triage",    // Initial assessment and prioritization
1123        "explore",   // Research and discovery
1124        "diagnose",  // Debugging and troubleshooting
1125        "design",    // Architecture and design
1126        "plan",      // Planning and specification
1127        "implement", // Implementation/coding
1128        "test",      // Testing and validation
1129        "review",    // Code review
1130        "security",  // Security review/audit
1131        "doc",       // Documentation
1132        "integrate", // Integration work
1133        "deploy",    // Release to staging/production
1134        "monitor",   // Observability and metrics
1135        "optimize",  // Performance tuning
1136    ]
1137    .iter()
1138    .map(|s| s.to_string())
1139    .collect()
1140}
1141
1142impl PhasesConfig {
1143    /// Check if a phase is a known/defined phase.
1144    pub fn is_known_phase(&self, phase: &str) -> bool {
1145        self.definitions.contains(phase)
1146    }
1147
1148    /// Get all defined phase names.
1149    pub fn phase_names(&self) -> Vec<&str> {
1150        self.definitions.iter().map(|s| s.as_str()).collect()
1151    }
1152
1153    /// Check a phase and return a warning message if unknown (based on unknown_phase behavior).
1154    /// Returns None if the phase is known or if behavior is Allow.
1155    /// Returns Some(warning) if behavior is Warn.
1156    /// Returns Err if behavior is Reject.
1157    pub fn check_phase(&self, phase: &str) -> Result<Option<String>> {
1158        if self.is_known_phase(phase) {
1159            return Ok(None);
1160        }
1161
1162        match self.unknown_phase {
1163            UnknownKeyBehavior::Allow => Ok(None),
1164            UnknownKeyBehavior::Warn => Ok(Some(format!(
1165                "Unknown phase '{}'. Known phases: {:?}",
1166                phase,
1167                self.phase_names()
1168            ))),
1169            UnknownKeyBehavior::Reject => Err(anyhow!(
1170                "Unknown phase '{}'. Known phases: {:?}. Configure in phases.definitions or set unknown_phase to 'allow' or 'warn'.",
1171                phase,
1172                self.phase_names()
1173            )),
1174        }
1175    }
1176}
1177
1178impl Config {
1179    /// Load configuration from file.
1180    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
1181        let content = std::fs::read_to_string(path)?;
1182        let config: Config = serde_yaml::from_str(&content)?;
1183        Ok(config)
1184    }
1185
1186    /// Load configuration from default locations or return defaults.
1187    ///
1188    /// **Deprecated**: Use `ConfigLoader::load()` instead for proper tier merging.
1189    pub fn load_or_default() -> Self {
1190        // Try TASK_GRAPH_CONFIG_PATH environment variable first
1191        if let Ok(config_path) = std::env::var("TASK_GRAPH_CONFIG_PATH")
1192            && let Ok(config) = Self::load(&config_path)
1193        {
1194            return config;
1195        }
1196
1197        // Try task-graph/config.yaml (new location)
1198        if let Ok(config) = Self::load("task-graph/config.yaml") {
1199            return config;
1200        }
1201
1202        // Try .task-graph/config.yaml (deprecated location)
1203        if let Ok(config) = Self::load(".task-graph/config.yaml") {
1204            return config;
1205        }
1206
1207        // Fall back to defaults with environment variable overrides
1208        let mut config = Self::default();
1209
1210        if let Ok(db_path) = std::env::var("TASK_GRAPH_DB_PATH") {
1211            config.server.db_path = PathBuf::from(db_path);
1212        }
1213
1214        if let Ok(media_dir) = std::env::var("TASK_GRAPH_MEDIA_DIR") {
1215            config.server.media_dir = PathBuf::from(media_dir);
1216        }
1217
1218        if let Ok(log_dir) = std::env::var("TASK_GRAPH_LOG_DIR") {
1219            config.server.log_dir = PathBuf::from(log_dir);
1220        }
1221
1222        config
1223    }
1224
1225    /// Ensure the database directory exists.
1226    pub fn ensure_db_dir(&self) -> Result<()> {
1227        if let Some(parent) = self.server.db_path.parent() {
1228            std::fs::create_dir_all(parent)?;
1229        }
1230        Ok(())
1231    }
1232
1233    /// Ensure the media directory exists.
1234    pub fn ensure_media_dir(&self) -> Result<()> {
1235        std::fs::create_dir_all(&self.server.media_dir)?;
1236        Ok(())
1237    }
1238
1239    /// Ensure the log directory exists.
1240    pub fn ensure_log_dir(&self) -> Result<()> {
1241        std::fs::create_dir_all(&self.server.log_dir)?;
1242        Ok(())
1243    }
1244
1245    /// Get the media directory path.
1246    pub fn media_dir(&self) -> &Path {
1247        &self.server.media_dir
1248    }
1249
1250    /// Get the log directory path.
1251    pub fn log_dir(&self) -> &Path {
1252        &self.server.log_dir
1253    }
1254}
1255
1256/// Tool description override.
1257#[derive(Debug, Clone, Serialize, Deserialize)]
1258pub struct ToolPrompt {
1259    pub description: String,
1260}
1261
1262/// LLM-facing prompts configuration.
1263#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1264pub struct Prompts {
1265    /// Server instructions shown to the LLM.
1266    pub instructions: Option<String>,
1267
1268    /// Tool description overrides by tool name.
1269    #[serde(default)]
1270    pub tools: HashMap<String, ToolPrompt>,
1271}
1272
1273impl Prompts {
1274    /// Load prompts from file.
1275    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
1276        let content = std::fs::read_to_string(path)?;
1277        // Handle empty or comment-only YAML files (which parse as null)
1278        let prompts: Option<Prompts> = serde_yaml::from_str(&content)?;
1279        Ok(prompts.unwrap_or_default())
1280    }
1281
1282    /// Load prompts from default location or return defaults.
1283    ///
1284    /// **Deprecated**: Use `ConfigLoader` for proper tier merging.
1285    pub fn load_or_default() -> Self {
1286        // Try task-graph/prompts.yaml (new location)
1287        if let Ok(prompts) = Self::load("task-graph/prompts.yaml") {
1288            return prompts;
1289        }
1290
1291        // Try .task-graph/prompts.yaml (deprecated location)
1292        if let Ok(prompts) = Self::load(".task-graph/prompts.yaml") {
1293            return prompts;
1294        }
1295
1296        Self::default()
1297    }
1298
1299    /// Get a tool description override if available.
1300    pub fn get_tool_description(&self, name: &str) -> Option<&str> {
1301        self.tools.get(name).map(|t| t.description.as_str())
1302    }
1303}