dx_forge/
orchestrator.rs

1//! Simple Orchestrator - Only controls WHEN to run tools
2//!
3//! Tools are self-contained and know:
4//! - What files to process
5//! - When they should run
6//! - What patterns to detect
7//!
8//! Forge just detects changes and asks: "Should you run?"
9
10use anyhow::Result;
11use parking_lot::RwLock;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet};
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16
17/// Tool execution context shared across all tools
18/// 
19/// The execution context provides tools with access to repository state,
20/// file changes, shared data, and traffic branch analysis. It serves as
21/// the communication hub between tools and the orchestrator.
22/// 
23/// # Fields
24/// 
25/// - `repo_root`: Absolute path to the repository root
26/// - `forge_path`: Path to Forge data directory (.dx/forge)
27/// - `current_branch`: Git branch name (if in a git repo)
28/// - `changed_files`: Files modified in this execution cycle
29/// - `shared_state`: Thread-safe storage for inter-tool communication
30/// - `traffic_analyzer`: Analyzes file changes for merge safety
31/// - `component_manager`: Manages component state for traffic branches
32#[derive(Clone)]
33pub struct ExecutionContext {
34    /// Repository root path
35    pub repo_root: PathBuf,
36
37    /// Forge storage path (.dx/forge)
38    pub forge_path: PathBuf,
39
40    /// Current Git branch
41    pub current_branch: Option<String>,
42
43    /// Changed files in this execution
44    pub changed_files: Vec<PathBuf>,
45
46    /// Shared state between tools
47    pub shared_state: Arc<RwLock<HashMap<String, serde_json::Value>>>,
48
49    /// Traffic branch analyzer
50    pub traffic_analyzer: Arc<dyn TrafficAnalyzer + Send + Sync>,
51
52    /// Component state manager for traffic branch system
53    pub component_manager: Option<Arc<RwLock<crate::context::ComponentStateManager>>>,
54}
55
56impl std::fmt::Debug for ExecutionContext {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        f.debug_struct("ExecutionContext")
59            .field("repo_root", &self.repo_root)
60            .field("forge_path", &self.forge_path)
61            .field("current_branch", &self.current_branch)
62            .field("changed_files", &self.changed_files)
63            .field("traffic_analyzer", &"<dyn TrafficAnalyzer>")
64            .finish()
65    }
66}
67
68impl ExecutionContext {
69    /// Create a new execution context
70    pub fn new(repo_root: PathBuf, forge_path: PathBuf) -> Self {
71        // Try to create component state manager
72        let component_manager = crate::context::ComponentStateManager::new(&forge_path)
73            .ok()
74            .map(|mgr| Arc::new(RwLock::new(mgr)));
75
76        Self {
77            repo_root,
78            forge_path,
79            current_branch: None,
80            changed_files: Vec::new(),
81            shared_state: Arc::new(RwLock::new(HashMap::new())),
82            traffic_analyzer: Arc::new(DefaultTrafficAnalyzer),
83            component_manager,
84        }
85    }
86
87    /// Set a shared value
88    pub fn set<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<()> {
89        let json = serde_json::to_value(value)?;
90        self.shared_state.write().insert(key.into(), json);
91        Ok(())
92    }
93
94    /// Get a shared value
95    pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
96        let state = self.shared_state.read();
97        if let Some(value) = state.get(key) {
98            let result = serde_json::from_value(value.clone())?;
99            Ok(Some(result))
100        } else {
101            Ok(None)
102        }
103    }
104
105    /// Find regex patterns in a file
106    pub fn find_patterns(&self, _pattern: &str) -> Result<Vec<PatternMatch>> {
107        // Implementation will be added
108        Ok(Vec::new())
109    }
110}
111
112/// Pattern match result
113#[derive(Debug, Clone)]
114pub struct PatternMatch {
115    pub file: PathBuf,
116    pub line: usize,
117    pub col: usize,
118    pub text: String,
119    pub captures: Vec<String>,
120}
121
122/// Output from tool execution
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct ToolOutput {
125    pub success: bool,
126    pub files_modified: Vec<PathBuf>,
127    pub files_created: Vec<PathBuf>,
128    pub files_deleted: Vec<PathBuf>,
129    pub message: String,
130    pub duration_ms: u64,
131}
132
133impl ToolOutput {
134    pub fn success() -> Self {
135        Self {
136            success: true,
137            files_modified: Vec::new(),
138            files_created: Vec::new(),
139            files_deleted: Vec::new(),
140            message: "Success".to_string(),
141            duration_ms: 0,
142        }
143    }
144
145    pub fn failure(message: impl Into<String>) -> Self {
146        Self {
147            success: false,
148            files_modified: Vec::new(),
149            files_created: Vec::new(),
150            files_deleted: Vec::new(),
151            message: message.into(),
152            duration_ms: 0,
153        }
154    }
155}
156
157/// Main DX tool trait - all tools must implement this
158/// 
159/// # Overview
160/// 
161/// The `DxTool` trait provides the core interface for all DX tools in the Forge ecosystem.
162/// Tools are self-contained units that know what files to process, when to run, and how to
163/// integrate with the broader toolchain.
164/// 
165/// # Lifecycle
166/// 
167/// Tool execution follows this lifecycle:
168/// 1. `should_run()` - Check if tool should execute
169/// 2. `before_execute()` - Setup and validation
170/// 3. `execute()` - Main tool logic
171/// 4. `after_execute()` - Cleanup and reporting (on success)
172/// 5. `on_error()` - Error handling (on failure)
173/// 
174/// # Example
175/// 
176/// ```rust,no_run
177/// use dx_forge::{DxTool, ExecutionContext, ToolOutput};
178/// use anyhow::Result;
179/// 
180/// struct MyCustomTool {
181///     enabled: bool,
182/// }
183/// 
184/// impl DxTool for MyCustomTool {
185///     fn name(&self) -> &str { "my-custom-tool" }
186///     fn version(&self) -> &str { "1.0.0" }
187///     fn priority(&self) -> u32 { 50 }
188///     
189///     fn execute(&mut self, ctx: &ExecutionContext) -> Result<ToolOutput> {
190///         // Your tool logic here
191///         Ok(ToolOutput::success())
192///     }
193///     
194///     fn should_run(&self, _ctx: &ExecutionContext) -> bool {
195///         self.enabled
196///     }
197/// }
198/// ```
199pub trait DxTool: Send + Sync {
200    /// Tool name (e.g., "dx-ui", "dx-style")
201    /// 
202    /// This should be a unique identifier for your tool. By convention,
203    /// DX tools use the format "dx-{category}" (e.g., dx-ui, dx-icons, dx-style).
204    fn name(&self) -> &str;
205
206    /// Tool version using semantic versioning
207    /// 
208    /// The version should follow semver format (e.g., "1.2.3").
209    /// This is used for dependency resolution and compatibility checking.
210    fn version(&self) -> &str;
211
212    /// Execution priority (lower number = executes earlier)
213    /// 
214    /// Tools are executed in priority order. Common priority values:
215    /// - 0-20: Infrastructure tools (code generation, schema validation)
216    /// - 21-50: Component tools (UI, icons, styles)
217    /// - 51-100: Post-processing tools (optimization, bundling)
218    /// 
219    /// Default priority is typically 50 for most tools.
220    fn priority(&self) -> u32;
221
222    /// Execute the tool's main logic
223    /// 
224    /// This is where the core functionality of your tool should be implemented.
225    /// The execution context provides access to repository state, file changes,
226    /// and shared state between tools.
227    /// 
228    /// # Arguments
229    /// 
230    /// * `context` - Execution context with repo info and shared state
231    /// 
232    /// # Returns
233    /// 
234    /// Returns a `ToolOutput` containing execution results, modified files, and status.
235    /// 
236    /// # Errors
237    /// 
238    /// Return an error if execution fails. The orchestrator will handle cleanup
239    /// and invoke the `on_error` hook.
240    fn execute(&mut self, context: &ExecutionContext) -> Result<ToolOutput>;
241
242    /// Check if tool should run (optional pre-check)
243    /// 
244    /// Override this method to implement custom logic for determining whether
245    /// the tool should execute. This is called before `before_execute()`.
246    /// 
247    /// # Arguments
248    /// 
249    /// * `_context` - Execution context for checking conditions
250    /// 
251    /// # Returns
252    /// 
253    /// `true` if the tool should execute, `false` to skip execution
254    /// 
255    /// # Example
256    /// 
257    /// ```rust,no_run
258    /// fn should_run(&self, ctx: &ExecutionContext) -> bool {
259    ///     // Only run if TypeScript files changed
260    ///     ctx.changed_files.iter().any(|f| f.extension().map_or(false, |e| e == "ts"))
261    /// }
262    /// ```
263    fn should_run(&self, _context: &ExecutionContext) -> bool {
264        true
265    }
266
267    /// Tool dependencies (must run after these tools)
268    /// 
269    /// Specify tools that must execute before this tool. Dependencies are validated
270    /// before execution begins, and circular dependencies are detected.
271    /// 
272    /// # Returns
273    /// 
274    /// Vector of tool names this tool depends on
275    /// 
276    /// # Example
277    /// 
278    /// ```rust,no_run
279    /// fn dependencies(&self) -> Vec<String> {
280    ///     vec!["dx-codegen".to_string(), "dx-schema".to_string()]
281    /// }
282    /// ```
283    fn dependencies(&self) -> Vec<String> {
284        Vec::new()
285    }
286
287    /// Before execution hook (setup, validation)
288    /// 
289    /// Called before `execute()`. Use this for:
290    /// - Validating preconditions
291    /// - Setting up temporary resources
292    /// - Checking file permissions
293    /// - Loading configuration
294    /// 
295    /// # Errors
296    /// 
297    /// Return an error to prevent execution and skip to `on_error`
298    fn before_execute(&mut self, _context: &ExecutionContext) -> Result<()> {
299        Ok(())
300    }
301
302    /// After execution hook (cleanup, reporting)
303    /// 
304    /// Called after successful `execute()`. Use this for:
305    /// - Cleaning up temporary files
306    /// - Generating reports
307    /// - Updating shared state
308    /// - Sending notifications
309    /// 
310    /// # Arguments
311    /// 
312    /// * `_output` - The output from successful execution
313    fn after_execute(&mut self, _context: &ExecutionContext, _output: &ToolOutput) -> Result<()> {
314        Ok(())
315    }
316
317    /// On error hook (rollback, cleanup)
318    /// 
319    /// Called when `execute()` or `before_execute()` fails. Use this for:
320    /// - Rolling back partial changes
321    /// - Cleaning up resources
322    /// - Logging detailed error info
323    /// - Sending error notifications
324    /// 
325    /// # Arguments
326    /// 
327    /// * `_error` - The error that occurred
328    fn on_error(&mut self, _context: &ExecutionContext, _error: &anyhow::Error) -> Result<()> {
329        Ok(())
330    }
331
332    /// Execution timeout in seconds (0 = no timeout)
333    /// 
334    /// Specifies the maximum time this tool should be allowed to run.
335    /// Note: Timeout enforcement for synchronous tools is not yet implemented.
336    /// Future versions will use thread-based timeouts or async execution.
337    /// 
338    /// # Returns
339    /// 
340    /// Timeout duration in seconds, or 0 for no timeout
341    fn timeout_seconds(&self) -> u64 {
342        60
343    }
344}
345
346// Tools are self-contained - no manifests needed
347// Each tool knows what to do and when to run
348
349/// Traffic branch analysis result
350/// 
351/// Forge uses a "traffic light" system to categorize file changes by risk level:
352/// 
353/// - **🟢 Green**: Safe to auto-merge (docs, tests, styles, assets)
354/// - **🟡 Yellow**: Reviewable conflicts (code changes that may conflict)
355/// - **🔴 Red**: Manual resolution required (API changes, schemas, migrations)
356/// 
357/// This system prevents breaking changes from being automatically merged while
358/// allowing safe updates to proceed without manual intervention.
359/// 
360/// # Example
361/// 
362/// ```rust,no_run
363/// use dx_forge::{TrafficBranch, TrafficAnalyzer, DefaultTrafficAnalyzer};
364/// use std::path::Path;
365/// 
366/// let analyzer = DefaultTrafficAnalyzer;
367/// let result = analyzer.analyze(Path::new("src/api/types.ts")).unwrap();
368/// 
369/// match result {
370///     TrafficBranch::Green => println!("Safe to auto-merge"),
371///     TrafficBranch::Yellow { conflicts } => {
372///         println!("Review {} potential conflicts", conflicts.len())
373///     }
374///     TrafficBranch::Red { conflicts } => {
375///         println!("Manual resolution required for {} conflicts", conflicts.len())
376///     }
377/// }
378/// ```
379#[derive(Debug, Clone, PartialEq)]
380pub enum TrafficBranch {
381    /// 🟢 Green: Safe to auto-update
382    Green,
383
384    /// 🟡 Yellow: Can merge with conflicts
385    Yellow { conflicts: Vec<Conflict> },
386
387    /// 🔴 Red: Manual resolution required
388    Red { conflicts: Vec<Conflict> },
389}
390
391#[derive(Debug, Clone, PartialEq)]
392pub struct Conflict {
393    pub path: PathBuf,
394    pub line: usize,
395    pub reason: String,
396}
397
398/// Traffic branch analyzer trait
399pub trait TrafficAnalyzer {
400    fn analyze(&self, file: &Path) -> Result<TrafficBranch>;
401    fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool;
402}
403
404/// Default traffic analyzer implementation
405pub struct DefaultTrafficAnalyzer;
406
407impl TrafficAnalyzer for DefaultTrafficAnalyzer {
408    fn analyze(&self, file: &Path) -> Result<TrafficBranch> {
409        // Analyze file to determine traffic branch
410        let extension = file
411            .extension()
412            .and_then(|e| e.to_str())
413            .unwrap_or("");
414
415        // 🟢 Green: Auto-update (safe files that don't affect APIs or types)
416        let green_patterns = [
417            "md", "txt", "json", // Documentation and config
418            "css", "scss", "less", // Styles
419            "png", "jpg", "svg", "ico", // Assets
420            "test.ts", "test.js", "spec.ts", "spec.js", // Tests
421        ];
422
423        // 🔴 Red: Manual resolution (breaking changes, API modifications)
424        let red_patterns = [
425            "proto", // Protocol buffers
426            "graphql", "gql", // GraphQL schemas
427            "sql", // Database migrations
428        ];
429
430        // Check if file matches green patterns
431        if green_patterns.iter().any(|p| extension.ends_with(p)) {
432            return Ok(TrafficBranch::Green);
433        }
434
435        // Check if file matches red patterns
436        if red_patterns.iter().any(|p| extension.ends_with(p)) {
437            let conflict = Conflict {
438                path: file.to_path_buf(),
439                line: 0,
440                reason: format!("Breaking change potential: {} file modification", extension),
441            };
442            return Ok(TrafficBranch::Red {
443                conflicts: vec![conflict],
444            });
445        }
446
447        // 🟡 Yellow: Merge required (code files that may have conflicts)
448        // ts, tsx, js, jsx, rs, go, py, etc.
449        if matches!(
450            extension,
451            "ts" | "tsx" | "js" | "jsx" | "rs" | "go" | "py" | "java" | "cpp" | "c" | "h"
452        ) {
453            // Check for API-related indicators in the file path
454            let path_str = file.to_string_lossy().to_lowercase();
455
456            if path_str.contains("api")
457                || path_str.contains("interface")
458                || path_str.contains("types")
459                || path_str.contains("schema")
460            {
461                // Potential API changes - Red
462                let conflict = Conflict {
463                    path: file.to_path_buf(),
464                    line: 0,
465                    reason: "API/Type definition file modification".to_string(),
466                };
467                return Ok(TrafficBranch::Red {
468                    conflicts: vec![conflict],
469                });
470            }
471
472            // Regular code file - Yellow (may have merge conflicts)
473            return Ok(TrafficBranch::Yellow {
474                conflicts: vec![],
475            });
476        }
477
478        // Default to Yellow for unknown file types
479        Ok(TrafficBranch::Yellow {
480            conflicts: vec![],
481        })
482    }
483
484    fn can_auto_merge(&self, conflicts: &[Conflict]) -> bool {
485        conflicts.is_empty()
486    }
487}
488
489/// Orchestration configuration
490#[derive(Debug, Clone)]
491pub struct OrchestratorConfig {
492    /// Enable parallel execution
493    pub parallel: bool,
494
495    /// Fail fast on first error
496    pub fail_fast: bool,
497
498    /// Maximum concurrent tools (for parallel mode)
499    pub max_concurrent: usize,
500
501    /// Enable traffic branch safety checks
502    pub traffic_branch_enabled: bool,
503}
504
505impl Default for OrchestratorConfig {
506    fn default() -> Self {
507        Self {
508            parallel: false,
509            fail_fast: true,
510            max_concurrent: 4,
511            traffic_branch_enabled: true,
512        }
513    }
514}
515
516/// Simple orchestrator - coordinates tool execution timing
517/// 
518/// The Orchestrator manages the execution lifecycle of DX tools, handling:
519/// 
520/// - **Tool Registration**: Register tools for execution
521/// - **Priority Ordering**: Execute tools in priority order (lower first)
522/// - **Dependency Resolution**: Validate and resolve tool dependencies
523/// - **Circular Dependency Detection**: Prevent infinite dependency loops
524/// - **Lifecycle Hooks**: Invoke before/after/error hooks
525/// - **Parallel Execution**: Support concurrent tool execution (optional)
526/// - **Traffic Branch Integration**: Analyze file changes for merge safety
527/// - **Error Handling**: Fail-fast or continue-on-error modes
528/// 
529/// # Example
530/// 
531/// ```rust,no_run
532/// use dx_forge::{Orchestrator, DxTool, ExecutionContext, ToolOutput};
533/// use anyhow::Result;
534/// 
535/// struct MyTool;
536/// impl DxTool for MyTool {
537///     fn name(&self) -> &str { "my-tool" }
538///     fn version(&self) -> &str { "1.0.0" }
539///     fn priority(&self) -> u32 { 50 }
540///     fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
541///         Ok(ToolOutput::success())
542///     }
543/// }
544/// 
545/// fn main() -> Result<()> {
546///     let mut orch = Orchestrator::new(".")?;
547///     orch.register_tool(Box::new(MyTool))?;
548///     let results = orch.execute_all()?;
549///     println!("Executed {} tools", results.len());
550///     Ok(())
551/// }
552/// ```
553pub struct Orchestrator {
554    tools: Vec<Box<dyn DxTool>>,
555    context: ExecutionContext,
556    config: OrchestratorConfig,
557}
558
559impl Orchestrator {
560    /// Create a new orchestrator
561    pub fn new(repo_root: impl Into<PathBuf>) -> Result<Self> {
562        let repo_root = repo_root.into();
563        let forge_path = repo_root.join(".dx/forge");
564
565        Ok(Self {
566            tools: Vec::new(),
567            context: ExecutionContext::new(repo_root, forge_path),
568            config: OrchestratorConfig::default(),
569        })
570    }
571
572    /// Create orchestrator with custom configuration
573    pub fn with_config(repo_root: impl Into<PathBuf>, config: OrchestratorConfig) -> Result<Self> {
574        let repo_root = repo_root.into();
575        let forge_path = repo_root.join(".dx/forge");
576
577        Ok(Self {
578            tools: Vec::new(),
579            context: ExecutionContext::new(repo_root, forge_path),
580            config,
581        })
582    }
583
584    /// Update configuration
585    pub fn set_config(&mut self, config: OrchestratorConfig) {
586        self.config = config;
587    }
588
589    /// Register a tool (tools configure themselves)
590    pub fn register_tool(&mut self, tool: Box<dyn DxTool>) -> Result<()> {
591        let name = tool.name().to_string();
592        tracing::info!(
593            "📦 Registered tool: {} v{} (priority: {})",
594            name,
595            tool.version(),
596            tool.priority()
597        );
598        self.tools.push(tool);
599        Ok(())
600    }
601
602    /// Execute all registered tools in priority order
603    pub fn execute_all(&mut self) -> Result<Vec<ToolOutput>> {
604        let start_time = std::time::Instant::now();
605        tracing::info!("🎼 Orchestrator starting execution of {} tools", self.tools.len());
606
607        // Sort tools by priority
608        self.tools.sort_by_key(|t| t.priority());
609
610        // Check dependencies
611        tracing::debug!("🔍 Validating tool dependencies...");
612        self.validate_dependencies()?;
613
614        // Check for circular dependencies
615        tracing::debug!("🔄 Checking for circular dependencies...");
616        self.check_circular_dependencies()?;
617
618        tracing::debug!(
619            "📋 Execution order: {}",
620            self.tools
621                .iter()
622                .map(|t| format!("{}(p:{})", t.name(), t.priority()))
623                .collect::<Vec<_>>()
624                .join(" → ")
625        );
626
627        // Execute tools based on parallel configuration
628        let outputs = if self.config.parallel {
629            self.execute_parallel()
630        } else {
631            self.execute_sequential()
632        }?;
633
634        let duration = start_time.elapsed();
635        let success_count = outputs.iter().filter(|o| o.success).count();
636        let failed_count = outputs.len() - success_count;
637
638        tracing::info!(
639            "🏁 Orchestration complete in {:.2}s: {} succeeded, {} failed",
640            duration.as_secs_f64(),
641            success_count,
642            failed_count
643        );
644
645        Ok(outputs)
646    }
647
648    /// Execute tools sequentially in priority order
649    fn execute_sequential(&mut self) -> Result<Vec<ToolOutput>> {
650        let mut outputs = Vec::new();
651        let context = self.context.clone();
652        let total_tools = self.tools.len();
653        let mut executed = 0;
654        let mut skipped = 0;
655        let mut failed = 0;
656
657        for tool in &mut self.tools {
658            if !tool.should_run(&context) {
659                tracing::info!("⏭️  Skipping {}: pre-check failed", tool.name());
660                skipped += 1;
661                continue;
662            }
663
664            tracing::info!(
665                "🚀 Executing: {} v{} (priority: {}, {}/{})",
666                tool.name(),
667                tool.version(),
668                tool.priority(),
669                executed + 1,
670                total_tools
671            );
672
673            // Execute with lifecycle hooks
674            match Self::execute_tool_with_hooks(tool, &context) {
675                Ok(output) => {
676                    if output.success {
677                        executed += 1;
678                        tracing::info!("✅ {} completed in {}ms", tool.name(), output.duration_ms);
679                    } else {
680                        failed += 1;
681                        tracing::error!("❌ {} failed: {}", tool.name(), output.message);
682                        
683                        if self.config.fail_fast {
684                            tracing::error!("💥 Fail-fast enabled, stopping orchestration");
685                            return Err(anyhow::anyhow!("Tool {} failed: {}", tool.name(), output.message));
686                        }
687                    }
688                    outputs.push(output);
689                }
690                Err(e) => {
691                    failed += 1;
692                    tracing::error!("💥 {} error: {}", tool.name(), e);
693                    
694                    if self.config.fail_fast {
695                        tracing::error!("💥 Fail-fast enabled, stopping orchestration");
696                        return Err(e);
697                    }
698                    
699                    outputs.push(ToolOutput::failure(format!("Error: {}", e)));
700                }
701            }
702        }
703
704        tracing::info!(
705            "📊 Sequential execution complete: {} executed, {} skipped, {} failed",
706            executed,
707            skipped,
708            failed
709        );
710
711        Ok(outputs)
712    }
713
714    /// Execute tools in parallel where possible, respecting dependencies
715    fn execute_parallel(&mut self) -> Result<Vec<ToolOutput>> {
716        tracing::info!("🚀 Parallel execution mode (max {} concurrent)", self.config.max_concurrent);
717        
718        // Build dependency graph
719        let dep_graph = self.build_dependency_graph();
720        
721        // Group tools into execution waves (tools that can run concurrently)
722        let waves = self.compute_execution_waves(&dep_graph)?;
723        
724        tracing::debug!("📊 Execution waves: {}", waves.len());
725        for (i, wave) in waves.iter().enumerate() {
726            tracing::debug!("  Wave {}: {} tools", i + 1, wave.len());
727        }
728        
729        let mut all_outputs = Vec::new();
730        let context = self.context.clone();
731        
732        // Execute each wave in parallel
733        for (wave_idx, wave_tools) in waves.into_iter().enumerate() {
734            tracing::info!("🌊 Executing wave {} with {} tools", wave_idx + 1, wave_tools.len());
735            
736            let mut wave_outputs = Vec::new();
737            
738            // For now, execute wave tools sequentially (true parallel requires async DxTool trait)
739            // Future enhancement: Use thread pool or async execution
740            for tool_idx in wave_tools {
741                let tool = &mut self.tools[tool_idx];
742                
743                if !tool.should_run(&context) {
744                    tracing::info!("⏭️  Skipping {}: pre-check failed", tool.name());
745                    continue;
746                }
747                
748                tracing::info!("🚀 Executing: {} v{}", tool.name(), tool.version());
749                
750                match Self::execute_tool_with_hooks(tool, &context) {
751                    Ok(output) => {
752                        if output.success {
753                            tracing::info!("✅ {} completed in {}ms", tool.name(), output.duration_ms);
754                        } else {
755                            tracing::error!("❌ {} failed: {}", tool.name(), output.message);
756                            
757                            if self.config.fail_fast {
758                                return Err(anyhow::anyhow!("Tool {} failed: {}", tool.name(), output.message));
759                            }
760                        }
761                        wave_outputs.push(output);
762                    }
763                    Err(e) => {
764                        tracing::error!("💥 {} error: {}", tool.name(), e);
765                        
766                        if self.config.fail_fast {
767                            return Err(e);
768                        }
769                        
770                        wave_outputs.push(ToolOutput::failure(format!("Error: {}", e)));
771                    }
772                }
773            }
774            
775            all_outputs.extend(wave_outputs);
776        }
777        
778        Ok(all_outputs)
779    }
780    
781    /// Build a dependency graph for tools
782    fn build_dependency_graph(&self) -> HashMap<String, HashSet<String>> {
783        let mut graph = HashMap::new();
784        
785        for tool in &self.tools {
786            let deps: HashSet<String> = tool.dependencies().into_iter().collect();
787            graph.insert(tool.name().to_string(), deps);
788        }
789        
790        graph
791    }
792    
793    /// Compute execution waves based on dependency graph
794    /// Tools in the same wave have no dependencies on each other
795    fn compute_execution_waves(&self, dep_graph: &HashMap<String, HashSet<String>>) -> Result<Vec<Vec<usize>>> {
796        let mut waves: Vec<Vec<usize>> = Vec::new();
797        let mut completed: HashSet<String> = HashSet::new();
798        let mut remaining: Vec<usize> = (0..self.tools.len()).collect();
799        
800        while !remaining.is_empty() {
801            let mut current_wave = Vec::new();
802            let mut next_remaining = Vec::new();
803            
804            for &idx in &remaining {
805                let tool = &self.tools[idx];
806                let tool_name = tool.name().to_string();
807                
808                // Check if all dependencies are completed
809                let deps = dep_graph.get(&tool_name).cloned().unwrap_or_default();
810                let all_deps_met = deps.iter().all(|dep| completed.contains(dep));
811                
812                if all_deps_met {
813                    current_wave.push(idx);
814                    completed.insert(tool_name);
815                } else {
816                    next_remaining.push(idx);
817                }
818            }
819            
820            if current_wave.is_empty() && !remaining.is_empty() {
821                // No progress - likely a circular dependency or missing dependency
822                let unmet: Vec<String> = remaining.iter()
823                    .map(|&idx| self.tools[idx].name().to_string())
824                    .collect();
825                return Err(anyhow::anyhow!(
826                    "Cannot resolve dependencies for tools: {}",
827                    unmet.join(", ")
828                ));
829            }
830            
831            if !current_wave.is_empty() {
832                waves.push(current_wave);
833            }
834            
835            remaining = next_remaining;
836        }
837        
838        Ok(waves)
839    }
840
841    /// Execute tool with lifecycle hooks and error handling
842    fn execute_tool_with_hooks(tool: &mut Box<dyn DxTool>, context: &ExecutionContext) -> Result<ToolOutput> {
843        let start = std::time::Instant::now();
844        let tool_name = tool.name().to_string();
845
846        // Before hook
847        tracing::debug!("📝 Running before_execute hook for {}", tool_name);
848        tool.before_execute(context)?;
849
850        // Execute with timeout
851        // Note: Since the DxTool trait's execute method is synchronous,
852        // we can't use async timeout without significant refactoring.
853        // Future improvement: make DxTool async or use thread-based timeout
854        let result = if tool.timeout_seconds() > 0 {
855            tracing::debug!(
856                "⏱️  Executing {} with {}s timeout (note: timeout monitoring not yet implemented for sync tools)",
857                tool_name,
858                tool.timeout_seconds()
859            );
860            tool.execute(context)
861        } else {
862            tracing::debug!("🚀 Executing {} without timeout", tool_name);
863            tool.execute(context)
864        };
865
866        // Handle result
867        match result {
868            Ok(mut output) => {
869                let duration = start.elapsed();
870                output.duration_ms = duration.as_millis() as u64;
871                
872                tracing::info!(
873                    "✅ {} completed successfully in {:.2}s",
874                    tool_name,
875                    duration.as_secs_f64()
876                );
877                
878                if !output.files_modified.is_empty() {
879                    tracing::debug!("  📝 Modified {} files", output.files_modified.len());
880                }
881                if !output.files_created.is_empty() {
882                    tracing::debug!("  ✨ Created {} files", output.files_created.len());
883                }
884                if !output.files_deleted.is_empty() {
885                    tracing::debug!("  🗑️  Deleted {} files", output.files_deleted.len());
886                }
887                
888                // After hook
889                tracing::debug!("📝 Running after_execute hook for {}", tool_name);
890                tool.after_execute(context, &output)?;
891                
892                Ok(output)
893            }
894            Err(e) => {
895                let duration = start.elapsed();
896                tracing::error!(
897                    "❌ {} failed after {:.2}s: {}",
898                    tool_name,
899                    duration.as_secs_f64(),
900                    e
901                );
902                
903                // Error hook
904                tracing::debug!("📝 Running on_error hook for {}", tool_name);
905                tool.on_error(context, &e)?;
906                Err(e)
907            }
908        }
909    }
910
911    /// Check for circular dependencies
912    fn check_circular_dependencies(&self) -> Result<()> {
913        let mut visited = HashSet::new();
914        let mut stack = HashSet::new();
915
916        for tool in &self.tools {
917            if !visited.contains(tool.name()) {
918                self.check_circular_deps_recursive(tool.name(), &mut visited, &mut stack)?;
919            }
920        }
921
922        Ok(())
923    }
924
925    fn check_circular_deps_recursive(
926        &self,
927        tool_name: &str,
928        visited: &mut HashSet<String>,
929        stack: &mut HashSet<String>,
930    ) -> Result<()> {
931        visited.insert(tool_name.to_string());
932        stack.insert(tool_name.to_string());
933
934        if let Some(tool) = self.tools.iter().find(|t| t.name() == tool_name) {
935            for dep in tool.dependencies() {
936                if !visited.contains(&dep) {
937                    self.check_circular_deps_recursive(&dep, visited, stack)?;
938                } else if stack.contains(&dep) {
939                    return Err(anyhow::anyhow!(
940                        "Circular dependency detected: {} -> {}",
941                        tool_name,
942                        dep
943                    ));
944                }
945            }
946        }
947
948        stack.remove(tool_name);
949        Ok(())
950    }
951
952    /// Validate tool dependencies
953    fn validate_dependencies(&self) -> Result<()> {
954        let tool_names: HashSet<String> = self.tools.iter().map(|t| t.name().to_string()).collect();
955
956        for tool in &self.tools {
957            for dep in tool.dependencies() {
958                if !tool_names.contains(&dep) {
959                    anyhow::bail!(
960                        "Tool '{}' requires '{}' but it's not registered",
961                        tool.name(),
962                        dep
963                    );
964                }
965            }
966        }
967
968        Ok(())
969    }
970
971    /// Get execution context
972    pub fn context(&self) -> &ExecutionContext {
973        &self.context
974    }
975
976    /// Get mutable context
977    pub fn context_mut(&mut self) -> &mut ExecutionContext {
978        &mut self.context
979    }
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985
986    struct MockTool {
987        name: String,
988        priority: u32,
989    }
990
991    impl DxTool for MockTool {
992        fn name(&self) -> &str {
993            &self.name
994        }
995
996        fn version(&self) -> &str {
997            "1.0.0"
998        }
999
1000        fn priority(&self) -> u32 {
1001            self.priority
1002        }
1003
1004        fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
1005            Ok(ToolOutput::success())
1006        }
1007    }
1008
1009    #[test]
1010    fn test_orchestrator_priority_order() {
1011        let mut orch = Orchestrator::new("/tmp/test").unwrap();
1012
1013        orch.register_tool(Box::new(MockTool {
1014            name: "tool-c".into(),
1015            priority: 30,
1016        }))
1017        .unwrap();
1018        orch.register_tool(Box::new(MockTool {
1019            name: "tool-a".into(),
1020            priority: 10,
1021        }))
1022        .unwrap();
1023        orch.register_tool(Box::new(MockTool {
1024            name: "tool-b".into(),
1025            priority: 20,
1026        }))
1027        .unwrap();
1028
1029        let outputs = orch.execute_all().unwrap();
1030
1031        assert_eq!(outputs.len(), 3);
1032        assert!(outputs.iter().all(|o| o.success));
1033    }
1034}