1use 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
12pub const DEFAULT_UI_PORT: u16 = 31994;
14
15pub const DEFAULT_ID_WORDS: u8 = 4;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum IdCase {
22 #[default]
24 KebabCase,
25 SnakeCase,
27 CamelCase,
29 PascalCase,
31 Lowercase,
33 Uppercase,
35 TitleCase,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct IdsConfig {
42 #[serde(default = "default_id_words")]
44 pub task_id_words: u8,
45
46 #[serde(default = "default_id_words")]
48 pub agent_id_words: u8,
49
50 #[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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum UiMode {
100 #[default]
102 None,
103 Web,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UiConfig {
110 #[serde(default)]
112 pub mode: UiMode,
113
114 #[serde(default = "default_ui_port")]
116 pub port: u16,
117
118 #[serde(default = "default_retry_initial_ms")]
120 pub retry_initial_ms: u64,
121
122 #[serde(default = "default_retry_jitter_ms")]
124 pub retry_jitter_ms: u64,
125
126 #[serde(default = "default_retry_max_ms")]
128 pub retry_max_ms: u64,
129
130 #[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 }
155
156fn default_retry_jitter_ms() -> u64 {
157 5_000 }
159
160fn default_retry_max_ms() -> u64 {
161 240_000 }
163
164fn default_retry_multiplier() -> f64 {
165 2.0
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, Default)]
170pub struct AutoAdvanceConfig {
171 #[serde(default)]
173 pub enabled: bool,
174
175 #[serde(default)]
178 pub target_state: Option<String>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
183#[serde(rename_all = "snake_case")]
184pub enum UnknownKeyBehavior {
185 Allow,
187 #[default]
189 Warn,
190 Reject,
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
196#[serde(rename_all = "snake_case")]
197pub enum GateEnforcement {
198 Allow,
200 #[default]
202 Warn,
203 Reject,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct GateDefinition {
213 #[serde(rename = "type")]
215 pub gate_type: String,
216
217 #[serde(default)]
219 pub enforcement: GateEnforcement,
220
221 #[serde(default)]
223 pub description: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct AttachmentKeyDefinition {
229 pub mime: String,
231 #[serde(default = "default_append_mode")]
233 pub mode: String,
234}
235
236fn default_append_mode() -> String {
237 "append".to_string()
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct AttachmentsConfig {
243 #[serde(default)]
245 pub unknown_key: UnknownKeyBehavior,
246 #[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 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 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 pub fn get_definition(&self, key: &str) -> Option<&AttachmentKeyDefinition> {
391 self.definitions.get(key)
392 }
393
394 pub fn is_known_key(&self, key: &str) -> bool {
396 self.definitions.contains_key(key)
397 }
398
399 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct TagDefinition {
419 #[serde(default)]
421 pub category: Option<String>,
422 #[serde(default)]
424 pub description: Option<String>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize, Default)]
429pub struct TagsConfig {
430 #[serde(default)]
432 pub unknown_tag: UnknownKeyBehavior,
433 #[serde(default)]
435 pub definitions: HashMap<String, TagDefinition>,
436}
437
438impl TagsConfig {
439 pub fn is_known_tag(&self, tag: &str) -> bool {
441 self.definitions.contains_key(tag)
442 }
443
444 pub fn tag_names(&self) -> Vec<&str> {
446 self.definitions.keys().map(|s| s.as_str()).collect()
447 }
448
449 pub fn tags_in_category(&self, category: &str) -> Vec<&str> {
451 self.definitions
452 .iter()
453 .filter(|(_, def)| def.category.as_deref() == Some(category))
454 .map(|(name, _)| name.as_str())
455 .collect()
456 }
457
458 pub fn categories(&self) -> Vec<&str> {
460 let mut cats: Vec<&str> = self
461 .definitions
462 .values()
463 .filter_map(|def| def.category.as_deref())
464 .collect();
465 cats.sort();
466 cats.dedup();
467 cats
468 }
469
470 pub fn validate_tag(&self, tag: &str) -> Result<Option<String>> {
472 if self.is_known_tag(tag) {
473 return Ok(None);
474 }
475
476 match self.unknown_tag {
477 UnknownKeyBehavior::Allow => Ok(None),
478 UnknownKeyBehavior::Warn => Ok(Some(format!(
479 "Unknown tag '{}'. Known tags: {:?}",
480 tag,
481 self.tag_names()
482 ))),
483 UnknownKeyBehavior::Reject => Err(anyhow!(
484 "Unknown tag '{}'. Configure in tags.definitions or set unknown_tag to 'allow' or 'warn'. Known tags: {:?}",
485 tag,
486 self.tag_names()
487 )),
488 }
489 }
490
491 pub fn validate_tags(&self, tags: &[String]) -> Result<Vec<String>> {
493 let mut warnings = Vec::new();
494 for tag in tags {
495 if let Some(warning) = self.validate_tag(tag)? {
496 warnings.push(warning);
497 }
498 }
499 Ok(warnings)
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize, Default)]
505pub struct Config {
506 #[serde(default)]
507 pub server: ServerConfig,
508
509 #[serde(default)]
510 pub paths: PathsConfig,
511
512 #[serde(default)]
513 pub states: StatesConfig,
514
515 #[serde(default)]
516 pub dependencies: DependenciesConfig,
517
518 #[serde(default)]
519 pub auto_advance: AutoAdvanceConfig,
520
521 #[serde(default)]
522 pub attachments: AttachmentsConfig,
523
524 #[serde(default)]
525 pub phases: PhasesConfig,
526
527 #[serde(default)]
528 pub tags: TagsConfig,
529
530 #[serde(default)]
531 pub ids: IdsConfig,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct ServerPaths {
537 pub db_path: PathBuf,
539 pub media_dir: PathBuf,
541 pub log_dir: PathBuf,
543 pub config_path: Option<PathBuf>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct ServerConfig {
550 #[serde(default = "default_db_path")]
552 pub db_path: PathBuf,
553
554 #[serde(default = "default_media_dir")]
556 pub media_dir: PathBuf,
557
558 #[serde(default = "default_claim_limit")]
560 pub claim_limit: i32,
561
562 #[serde(default = "default_stale_timeout")]
564 pub stale_timeout_seconds: i64,
565
566 #[serde(default)]
568 pub default_format: OutputFormat,
569
570 #[serde(default = "default_skills_dir")]
572 pub skills_dir: PathBuf,
573
574 #[serde(default = "default_log_dir")]
576 pub log_dir: PathBuf,
577
578 #[serde(default)]
580 pub ui: UiConfig,
581
582 #[serde(default, skip_serializing_if = "Option::is_none")]
586 pub default_workflow: Option<String>,
587
588 #[serde(default = "default_page_size")]
591 pub default_page_size: i32,
592}
593
594impl Default for ServerConfig {
595 fn default() -> Self {
596 Self {
597 db_path: default_db_path(),
598 media_dir: default_media_dir(),
599 claim_limit: default_claim_limit(),
600 stale_timeout_seconds: default_stale_timeout(),
601 default_format: OutputFormat::default(),
602 skills_dir: default_skills_dir(),
603 log_dir: default_log_dir(),
604 ui: UiConfig::default(),
605 default_workflow: None,
606 default_page_size: default_page_size(),
607 }
608 }
609}
610
611fn default_db_path() -> PathBuf {
612 PathBuf::from("task-graph/tasks.db")
613}
614
615fn default_media_dir() -> PathBuf {
616 PathBuf::from("task-graph/media")
617}
618
619fn default_skills_dir() -> PathBuf {
620 PathBuf::from("task-graph/skills")
621}
622
623fn default_log_dir() -> PathBuf {
624 PathBuf::from("task-graph/logs")
625}
626
627fn default_paths_root() -> String {
628 ".".to_string()
629}
630
631fn default_claim_limit() -> i32 {
632 5
633}
634
635fn default_stale_timeout() -> i64 {
636 900 }
638
639fn default_page_size() -> i32 {
640 50
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct PathsConfig {
646 #[serde(default = "default_paths_root")]
648 pub root: String,
649
650 #[serde(default)]
652 pub style: PathStyle,
653
654 #[serde(default)]
656 pub map_windows_drives: bool,
657
658 #[serde(default)]
661 pub mappings: HashMap<String, String>,
662}
663
664impl Default for PathsConfig {
665 fn default() -> Self {
666 Self {
667 root: default_paths_root(),
668 style: PathStyle::Relative,
669 map_windows_drives: false,
670 mappings: HashMap::new(),
671 }
672 }
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
677#[serde(rename_all = "snake_case")]
678#[derive(Default)]
679pub enum PathStyle {
680 #[default]
682 Relative,
683 ProjectPrefixed,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct StatesConfig {
690 #[serde(default = "default_initial_state")]
692 pub initial: String,
693
694 #[serde(default = "default_disconnect_state")]
696 pub disconnect_state: String,
697
698 #[serde(default = "default_blocking_states")]
700 pub blocking_states: Vec<String>,
701
702 #[serde(default = "default_state_definitions")]
704 pub definitions: HashMap<String, StateDefinition>,
705}
706
707impl Default for StatesConfig {
708 fn default() -> Self {
709 Self {
710 initial: default_initial_state(),
711 disconnect_state: default_disconnect_state(),
712 blocking_states: default_blocking_states(),
713 definitions: default_state_definitions(),
714 }
715 }
716}
717
718fn default_initial_state() -> String {
719 "pending".to_string()
720}
721
722fn default_disconnect_state() -> String {
723 "pending".to_string()
724}
725
726fn default_blocking_states() -> Vec<String> {
727 vec![
728 "pending".to_string(),
729 "assigned".to_string(),
730 "working".to_string(),
731 "consult".to_string(),
732 ]
733}
734
735fn default_state_definitions() -> HashMap<String, StateDefinition> {
736 let mut defs = HashMap::new();
737
738 defs.insert(
739 "pending".to_string(),
740 StateDefinition {
741 exits: vec![
742 "assigned".to_string(),
743 "working".to_string(),
744 "cancelled".to_string(),
745 ],
746 timed: false,
747 },
748 );
749
750 defs.insert(
751 "assigned".to_string(),
752 StateDefinition {
753 exits: vec![
754 "working".to_string(),
755 "pending".to_string(),
756 "cancelled".to_string(),
757 ],
758 timed: false,
759 },
760 );
761
762 defs.insert(
763 "working".to_string(),
764 StateDefinition {
765 exits: vec![
766 "completed".to_string(),
767 "failed".to_string(),
768 "pending".to_string(),
769 "consult".to_string(),
770 ],
771 timed: true,
772 },
773 );
774
775 defs.insert(
776 "completed".to_string(),
777 StateDefinition {
778 exits: vec!["pending".to_string()],
779 timed: false,
780 },
781 );
782
783 defs.insert(
784 "failed".to_string(),
785 StateDefinition {
786 exits: vec!["pending".to_string()],
787 timed: false,
788 },
789 );
790
791 defs.insert(
792 "consult".to_string(),
793 StateDefinition {
794 exits: vec![
795 "working".to_string(),
796 "pending".to_string(),
797 "cancelled".to_string(),
798 ],
799 timed: false,
800 },
801 );
802
803 defs.insert(
804 "cancelled".to_string(),
805 StateDefinition {
806 exits: vec![],
807 timed: false,
808 },
809 );
810
811 defs
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize)]
816pub struct StateDefinition {
817 #[serde(default)]
819 pub exits: Vec<String>,
820
821 #[serde(default)]
823 pub timed: bool,
824}
825
826#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct DependenciesConfig {
829 #[serde(default = "default_dependency_definitions")]
831 pub definitions: HashMap<String, DependencyDefinition>,
832}
833
834impl Default for DependenciesConfig {
835 fn default() -> Self {
836 Self {
837 definitions: default_dependency_definitions(),
838 }
839 }
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
844pub struct DependencyDefinition {
845 pub display: DependencyDisplay,
847
848 pub blocks: BlockTarget,
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
854#[serde(rename_all = "snake_case")]
855pub enum DependencyDisplay {
856 Horizontal,
858 Vertical,
860}
861
862#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
864#[serde(rename_all = "snake_case")]
865pub enum BlockTarget {
866 None,
868 Start,
870 Completion,
872}
873
874fn default_dependency_definitions() -> HashMap<String, DependencyDefinition> {
875 let mut defs = HashMap::new();
876
877 defs.insert(
879 "blocks".to_string(),
880 DependencyDefinition {
881 display: DependencyDisplay::Horizontal,
882 blocks: BlockTarget::Start,
883 },
884 );
885
886 defs.insert(
887 "follows".to_string(),
888 DependencyDefinition {
889 display: DependencyDisplay::Horizontal,
890 blocks: BlockTarget::Start,
891 },
892 );
893
894 defs.insert(
895 "contains".to_string(),
896 DependencyDefinition {
897 display: DependencyDisplay::Vertical,
898 blocks: BlockTarget::Completion,
899 },
900 );
901
902 defs.insert(
904 "duplicate".to_string(),
905 DependencyDefinition {
906 display: DependencyDisplay::Horizontal,
907 blocks: BlockTarget::None,
908 },
909 );
910
911 defs.insert(
912 "see-also".to_string(),
913 DependencyDefinition {
914 display: DependencyDisplay::Horizontal,
915 blocks: BlockTarget::None,
916 },
917 );
918
919 defs.insert(
920 "relates-to".to_string(),
921 DependencyDefinition {
922 display: DependencyDisplay::Horizontal,
923 blocks: BlockTarget::None,
924 },
925 );
926
927 defs
928}
929
930impl DependenciesConfig {
931 pub fn is_valid_dep_type(&self, dep_type: &str) -> bool {
933 self.definitions.contains_key(dep_type)
934 }
935
936 pub fn get_definition(&self, dep_type: &str) -> Option<&DependencyDefinition> {
938 self.definitions.get(dep_type)
939 }
940
941 pub fn start_blocking_types(&self) -> Vec<&str> {
943 self.definitions
944 .iter()
945 .filter(|(_, def)| def.blocks == BlockTarget::Start)
946 .map(|(name, _)| name.as_str())
947 .collect()
948 }
949
950 pub fn completion_blocking_types(&self) -> Vec<&str> {
952 self.definitions
953 .iter()
954 .filter(|(_, def)| def.blocks == BlockTarget::Completion)
955 .map(|(name, _)| name.as_str())
956 .collect()
957 }
958
959 pub fn vertical_types(&self) -> Vec<&str> {
961 self.definitions
962 .iter()
963 .filter(|(_, def)| def.display == DependencyDisplay::Vertical)
964 .map(|(name, _)| name.as_str())
965 .collect()
966 }
967
968 pub fn dep_type_names(&self) -> Vec<&str> {
970 self.definitions.keys().map(|s| s.as_str()).collect()
971 }
972
973 pub fn validate(&self) -> anyhow::Result<()> {
975 if self.definitions.is_empty() {
976 return Err(anyhow::anyhow!(
977 "At least one dependency type must be defined"
978 ));
979 }
980
981 let has_start_blocking = self
983 .definitions
984 .values()
985 .any(|d| d.blocks == BlockTarget::Start);
986 if !has_start_blocking {
987 return Err(anyhow::anyhow!(
988 "At least one dependency type with blocks: start must be defined"
989 ));
990 }
991
992 Ok(())
993 }
994}
995
996impl StatesConfig {
997 pub fn is_valid_state(&self, state: &str) -> bool {
999 self.definitions.contains_key(state)
1000 }
1001
1002 pub fn is_valid_transition(&self, from: &str, to: &str) -> bool {
1004 if let Some(def) = self.definitions.get(from) {
1005 def.exits.contains(&to.to_string())
1006 } else {
1007 false
1008 }
1009 }
1010
1011 pub fn is_timed_state(&self, state: &str) -> bool {
1013 self.definitions
1014 .get(state)
1015 .map(|d| d.timed)
1016 .unwrap_or(false)
1017 }
1018
1019 pub fn is_terminal_state(&self, state: &str) -> bool {
1021 self.definitions
1022 .get(state)
1023 .map(|d| d.exits.is_empty())
1024 .unwrap_or(false)
1025 }
1026
1027 pub fn is_blocking_state(&self, state: &str) -> bool {
1029 self.blocking_states.contains(&state.to_string())
1030 }
1031
1032 pub fn state_names(&self) -> Vec<&str> {
1034 self.definitions.keys().map(|s| s.as_str()).collect()
1035 }
1036
1037 pub fn get_exits(&self, state: &str) -> Vec<&str> {
1039 self.definitions
1040 .get(state)
1041 .map(|d| d.exits.iter().map(|s| s.as_str()).collect())
1042 .unwrap_or_default()
1043 }
1044
1045 pub fn untimed_state_names(&self) -> Vec<&str> {
1047 self.definitions
1048 .iter()
1049 .filter(|(_, def)| !def.timed)
1050 .map(|(name, _)| name.as_str())
1051 .collect()
1052 }
1053
1054 pub fn validate(&self) -> Result<()> {
1056 if !self.definitions.contains_key(&self.initial) {
1058 return Err(anyhow!(
1059 "Initial state '{}' is not defined in state definitions",
1060 self.initial
1061 ));
1062 }
1063
1064 if !self.definitions.contains_key(&self.disconnect_state) {
1066 return Err(anyhow!(
1067 "Disconnect state '{}' is not defined in state definitions",
1068 self.disconnect_state
1069 ));
1070 }
1071 if self.is_timed_state(&self.disconnect_state) {
1072 return Err(anyhow!(
1073 "Disconnect state '{}' must not be a timed state",
1074 self.disconnect_state
1075 ));
1076 }
1077
1078 for state in &self.blocking_states {
1080 if !self.definitions.contains_key(state) {
1081 return Err(anyhow!(
1082 "Blocking state '{}' is not defined in state definitions",
1083 state
1084 ));
1085 }
1086 }
1087
1088 for (state_name, def) in &self.definitions {
1090 for exit in &def.exits {
1091 if !self.definitions.contains_key(exit) {
1092 return Err(anyhow!(
1093 "State '{}' has exit '{}' which is not defined",
1094 state_name,
1095 exit
1096 ));
1097 }
1098 }
1099 }
1100
1101 let has_terminal = self.definitions.values().any(|d| d.exits.is_empty());
1103 if !has_terminal {
1104 return Err(anyhow!(
1105 "At least one terminal state (with empty exits) must be defined"
1106 ));
1107 }
1108
1109 Ok(())
1110 }
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize)]
1115pub struct PhasesConfig {
1116 #[serde(default)]
1118 pub unknown_phase: UnknownKeyBehavior,
1119
1120 #[serde(default = "default_phases")]
1122 pub definitions: HashSet<String>,
1123}
1124
1125impl Default for PhasesConfig {
1126 fn default() -> Self {
1127 Self {
1128 unknown_phase: UnknownKeyBehavior::Warn,
1129 definitions: default_phases(),
1130 }
1131 }
1132}
1133
1134fn default_phases() -> HashSet<String> {
1135 [
1136 "deliver", "triage", "explore", "diagnose", "design", "plan", "implement", "test", "review", "security", "doc", "integrate", "deploy", "monitor", "optimize", ]
1152 .iter()
1153 .map(|s| s.to_string())
1154 .collect()
1155}
1156
1157impl PhasesConfig {
1158 pub fn is_known_phase(&self, phase: &str) -> bool {
1160 self.definitions.contains(phase)
1161 }
1162
1163 pub fn phase_names(&self) -> Vec<&str> {
1165 self.definitions.iter().map(|s| s.as_str()).collect()
1166 }
1167
1168 pub fn check_phase(&self, phase: &str) -> Result<Option<String>> {
1173 if self.is_known_phase(phase) {
1174 return Ok(None);
1175 }
1176
1177 match self.unknown_phase {
1178 UnknownKeyBehavior::Allow => Ok(None),
1179 UnknownKeyBehavior::Warn => Ok(Some(format!(
1180 "Unknown phase '{}'. Known phases: {:?}",
1181 phase,
1182 self.phase_names()
1183 ))),
1184 UnknownKeyBehavior::Reject => Err(anyhow!(
1185 "Unknown phase '{}'. Known phases: {:?}. Configure in phases.definitions or set unknown_phase to 'allow' or 'warn'.",
1186 phase,
1187 self.phase_names()
1188 )),
1189 }
1190 }
1191}
1192
1193impl Config {
1194 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
1196 let content = std::fs::read_to_string(path)?;
1197 let config: Config = serde_yaml::from_str(&content)?;
1198 Ok(config)
1199 }
1200
1201 pub fn load_or_default() -> Self {
1205 if let Ok(config_path) = std::env::var("TASK_GRAPH_CONFIG_PATH")
1207 && let Ok(config) = Self::load(&config_path)
1208 {
1209 return config;
1210 }
1211
1212 if let Ok(config) = Self::load("task-graph/config.yaml") {
1214 return config;
1215 }
1216
1217 if let Ok(config) = Self::load(".task-graph/config.yaml") {
1219 return config;
1220 }
1221
1222 let mut config = Self::default();
1224
1225 if let Ok(db_path) = std::env::var("TASK_GRAPH_DB_PATH") {
1226 config.server.db_path = PathBuf::from(db_path);
1227 }
1228
1229 if let Ok(media_dir) = std::env::var("TASK_GRAPH_MEDIA_DIR") {
1230 config.server.media_dir = PathBuf::from(media_dir);
1231 }
1232
1233 if let Ok(log_dir) = std::env::var("TASK_GRAPH_LOG_DIR") {
1234 config.server.log_dir = PathBuf::from(log_dir);
1235 }
1236
1237 config
1238 }
1239
1240 pub fn ensure_db_dir(&self) -> Result<()> {
1242 if let Some(parent) = self.server.db_path.parent() {
1243 std::fs::create_dir_all(parent)?;
1244 }
1245 Ok(())
1246 }
1247
1248 pub fn ensure_media_dir(&self) -> Result<()> {
1250 std::fs::create_dir_all(&self.server.media_dir)?;
1251 Ok(())
1252 }
1253
1254 pub fn ensure_log_dir(&self) -> Result<()> {
1256 std::fs::create_dir_all(&self.server.log_dir)?;
1257 Ok(())
1258 }
1259
1260 pub fn media_dir(&self) -> &Path {
1262 &self.server.media_dir
1263 }
1264
1265 pub fn log_dir(&self) -> &Path {
1267 &self.server.log_dir
1268 }
1269}
1270
1271#[derive(Debug, Clone, Serialize, Deserialize)]
1273pub struct ToolPrompt {
1274 pub description: String,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1279pub struct Prompts {
1280 pub instructions: Option<String>,
1282
1283 #[serde(default)]
1285 pub tools: HashMap<String, ToolPrompt>,
1286}
1287
1288impl Prompts {
1289 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
1291 let content = std::fs::read_to_string(path)?;
1292 let prompts: Option<Prompts> = serde_yaml::from_str(&content)?;
1294 Ok(prompts.unwrap_or_default())
1295 }
1296
1297 pub fn load_or_default() -> Self {
1301 if let Ok(prompts) = Self::load("task-graph/prompts.yaml") {
1303 return prompts;
1304 }
1305
1306 if let Ok(prompts) = Self::load(".task-graph/prompts.yaml") {
1308 return prompts;
1309 }
1310
1311 Self::default()
1312 }
1313
1314 pub fn get_tool_description(&self, name: &str) -> Option<&str> {
1316 self.tools.get(name).map(|t| t.description.as_str())
1317 }
1318}