miyabi_worktree/
five_worlds.rs

1//! Five Worlds Manager for 5-Worlds Quality Assurance Strategy
2//!
3//! This module implements the FiveWorldsManager, which manages 5 parallel
4//! Git worktrees for the 5-Worlds execution strategy. Each world runs
5//! independently in its own worktree with different LLM parameters.
6
7use crate::git::GitError;
8use crate::manager::WorktreeInfo;
9use miyabi_types::error::MiyabiError;
10use miyabi_types::world::WorldId;
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14use tokio::process::Command;
15use tokio::sync::Mutex;
16use tracing::{debug, error, info, warn};
17
18/// Handle to a World's worktree
19#[derive(Debug, Clone)]
20pub struct WorldWorktreeHandle {
21    /// The WorldId this handle is for
22    pub world_id: WorldId,
23    /// Path to the worktree
24    pub path: PathBuf,
25    /// Branch name
26    pub branch: String,
27    /// Worktree info from WorktreeManager
28    pub info: WorktreeInfo,
29}
30
31/// Manager for 5 parallel world worktrees
32pub struct FiveWorldsManager {
33    /// Base path for all worktrees (e.g., "worktrees/")
34    base_path: PathBuf,
35    /// Path to the main Git repository
36    repo_path: PathBuf,
37    /// Currently active world worktrees
38    active_worlds: Arc<Mutex<HashMap<WorldId, WorldWorktreeHandle>>>,
39}
40
41impl FiveWorldsManager {
42    /// Creates a new FiveWorldsManager
43    ///
44    /// # Arguments
45    /// * `base_path` - Base directory for all worktrees
46    /// * `repo_path` - Path to the main Git repository
47    pub fn new(base_path: PathBuf, repo_path: PathBuf) -> Self {
48        Self {
49            base_path,
50            repo_path,
51            active_worlds: Arc::new(Mutex::new(HashMap::new())),
52        }
53    }
54
55    /// Spawns all 5 worlds for a given issue and task
56    ///
57    /// Creates 5 independent Git worktrees, one for each WorldId.
58    /// Each worktree is isolated and can be worked on in parallel.
59    ///
60    /// # Arguments
61    /// * `issue_number` - GitHub issue number
62    /// * `task_name` - Name of the task being executed
63    ///
64    /// # Returns
65    /// HashMap of WorldId -> WorldWorktreeHandle
66    ///
67    /// # Example
68    /// ```no_run
69    /// use miyabi_worktree::five_worlds::FiveWorldsManager;
70    /// use std::path::PathBuf;
71    ///
72    /// #[tokio::main]
73    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
74    ///     let manager = FiveWorldsManager::new(
75    ///         PathBuf::from("worktrees"),
76    ///         PathBuf::from(".")
77    ///     );
78    ///
79    ///     let handles = manager.spawn_all_worlds(270, "implement_feature").await?;
80    ///     println!("Spawned {} worlds", handles.len());
81    ///
82    ///     Ok(())
83    /// }
84    /// ```
85    pub async fn spawn_all_worlds(
86        &self,
87        issue_number: u64,
88        task_name: &str,
89    ) -> Result<HashMap<WorldId, WorldWorktreeHandle>, GitError> {
90        info!(issue_number = issue_number, task_name = task_name, "Spawning all 5 worlds");
91
92        let mut handles = HashMap::new();
93
94        // Spawn each world sequentially to avoid Git conflicts
95        for world_id in WorldId::all() {
96            match self.spawn_world(issue_number, task_name, world_id).await {
97                Ok(handle) => {
98                    debug!(
99                        world_id = ?world_id,
100                        path = ?handle.path,
101                        "World spawned successfully"
102                    );
103                    handles.insert(world_id, handle);
104                },
105                Err(e) => {
106                    error!(
107                        world_id = ?world_id,
108                        error = %e,
109                        "Failed to spawn world"
110                    );
111                    // Clean up already created worktrees
112                    self.cleanup_worlds(&handles).await;
113                    return Err(e);
114                },
115            }
116        }
117
118        info!("All 5 worlds spawned successfully");
119        Ok(handles)
120    }
121
122    /// Spawns a single world worktree
123    ///
124    /// # Arguments
125    /// * `issue_number` - GitHub issue number
126    /// * `task_name` - Name of the task
127    /// * `world_id` - The WorldId to spawn
128    pub async fn spawn_world(
129        &self,
130        issue_number: u64,
131        task_name: &str,
132        world_id: WorldId,
133    ) -> Result<WorldWorktreeHandle, GitError> {
134        // Generate branch name: world-alpha-issue-270-implement_feature
135        let branch_name = format!(
136            "world-{}-issue-{}-{}",
137            world_id.to_string().to_lowercase(),
138            issue_number,
139            task_name
140        );
141
142        // Generate worktree path: worktrees/world-alpha/issue-270/implement_feature
143        let worktree_path = self
144            .base_path
145            .join(format!("world-{}", world_id.to_string().to_lowercase()))
146            .join(format!("issue-{}", issue_number))
147            .join(task_name);
148
149        debug!(
150            world_id = ?world_id,
151            branch = %branch_name,
152            path = ?worktree_path,
153            "Creating world worktree"
154        );
155
156        // Create the worktree using direct Git command
157        self.create_worktree_direct(&worktree_path, &branch_name)
158            .await
159            .map_err(|e| GitError::CommandFailed(e.to_string()))?;
160
161        // Create WorktreeInfo manually
162        let info = WorktreeInfo {
163            id: uuid::Uuid::new_v4().to_string(),
164            issue_number,
165            path: worktree_path.clone(),
166            branch_name: branch_name.clone(),
167            created_at: chrono::Utc::now(),
168            status: crate::manager::WorktreeStatus::Active,
169        };
170
171        let handle = WorldWorktreeHandle {
172            world_id,
173            path: worktree_path.clone(),
174            branch: branch_name,
175            info,
176        };
177
178        // Register in active worlds
179        self.active_worlds.lock().await.insert(world_id, handle.clone());
180
181        Ok(handle)
182    }
183
184    /// Creates a worktree directly using Git command
185    async fn create_worktree_direct(
186        &self,
187        worktree_path: &Path,
188        branch_name: &str,
189    ) -> Result<(), MiyabiError> {
190        // Create parent directories if they don't exist
191        if let Some(parent) = worktree_path.parent() {
192            tokio::fs::create_dir_all(parent)
193                .await
194                .map_err(|e| MiyabiError::Git(format!("Failed to create parent dirs: {}", e)))?;
195        }
196
197        // Execute git worktree add
198        let output = Command::new("git")
199            .arg("worktree")
200            .arg("add")
201            .arg("-b")
202            .arg(branch_name)
203            .arg(worktree_path)
204            .arg("HEAD")
205            .current_dir(&self.repo_path)
206            .output()
207            .await
208            .map_err(|e| MiyabiError::Git(format!("Failed to execute git worktree add: {}", e)))?;
209
210        if !output.status.success() {
211            let stderr = String::from_utf8_lossy(&output.stderr);
212            return Err(MiyabiError::Git(format!("Failed to create worktree: {}", stderr)));
213        }
214
215        Ok(())
216    }
217
218    /// Cleans up a specific world's worktree
219    ///
220    /// # Arguments
221    /// * `world_id` - The WorldId to cleanup
222    pub async fn cleanup_world(&self, world_id: WorldId) -> Result<(), GitError> {
223        let handle = {
224            let mut active = self.active_worlds.lock().await;
225            active.remove(&world_id)
226        };
227
228        if let Some(handle) = handle {
229            debug!(
230                world_id = ?world_id,
231                path = ?handle.path,
232                "Cleaning up world worktree"
233            );
234
235            self.remove_worktree_direct(&handle.path)
236                .await
237                .map_err(|e| GitError::CommandFailed(e.to_string()))?;
238
239            info!(world_id = ?world_id, "World worktree cleaned up");
240        } else {
241            warn!(world_id = ?world_id, "World not found in active worlds");
242        }
243
244        Ok(())
245    }
246
247    /// Removes a worktree directly using Git command
248    async fn remove_worktree_direct(&self, worktree_path: &Path) -> Result<(), MiyabiError> {
249        let output = Command::new("git")
250            .arg("worktree")
251            .arg("remove")
252            .arg("--force")
253            .arg(worktree_path)
254            .current_dir(&self.repo_path)
255            .output()
256            .await
257            .map_err(|e| {
258                MiyabiError::Git(format!("Failed to execute git worktree remove: {}", e))
259            })?;
260
261        if !output.status.success() {
262            let stderr = String::from_utf8_lossy(&output.stderr);
263            return Err(MiyabiError::Git(format!("Failed to remove worktree: {}", stderr)));
264        }
265
266        Ok(())
267    }
268
269    /// Cleans up multiple worlds at once (parallel)
270    ///
271    /// # Arguments
272    /// * `handles` - HashMap of world handles to cleanup
273    async fn cleanup_worlds(&self, handles: &HashMap<WorldId, WorldWorktreeHandle>) {
274        use futures::stream::{FuturesUnordered, StreamExt};
275
276        // Parallelize cleanup operations for better performance
277        let mut cleanup_futures = FuturesUnordered::new();
278
279        for (world_id, handle) in handles {
280            let world_id = *world_id;
281            let path = handle.path.clone();
282            let repo_path = self.repo_path.clone();
283
284            cleanup_futures.push(async move {
285                // Execute git worktree remove directly
286                let result = Command::new("git")
287                    .arg("worktree")
288                    .arg("remove")
289                    .arg("--force")
290                    .arg(&path)
291                    .current_dir(&repo_path)
292                    .output()
293                    .await;
294
295                match result {
296                    Ok(output) if output.status.success() => {
297                        debug!(world_id = ?world_id, path = ?path, "World worktree cleaned up");
298                    },
299                    Ok(output) => {
300                        let stderr = String::from_utf8_lossy(&output.stderr);
301                        error!(
302                            world_id = ?world_id,
303                            path = ?path,
304                            error = %stderr,
305                            "Failed to cleanup world worktree"
306                        );
307                    },
308                    Err(e) => {
309                        error!(
310                            world_id = ?world_id,
311                            path = ?path,
312                            error = %e,
313                            "Failed to execute git worktree remove"
314                        );
315                    },
316                }
317            });
318        }
319
320        // Execute all cleanup operations in parallel
321        while cleanup_futures.next().await.is_some() {}
322    }
323
324    /// Cleans up all active worlds for a specific issue (parallel)
325    ///
326    /// # Arguments
327    /// * `issue_number` - GitHub issue number
328    pub async fn cleanup_all_worlds_for_issue(&self, issue_number: u64) -> Result<(), GitError> {
329        use futures::stream::{FuturesUnordered, StreamExt};
330
331        info!(issue_number = issue_number, "Cleaning up all worlds for issue");
332
333        let worlds_to_cleanup: Vec<(WorldId, Option<WorldWorktreeHandle>)> = {
334            let mut active = self.active_worlds.lock().await;
335            WorldId::all()
336                .into_iter()
337                .map(|world_id| {
338                    let handle = active.remove(&world_id);
339                    (world_id, handle)
340                })
341                .collect()
342        };
343
344        // Parallelize cleanup operations
345        let mut cleanup_futures = FuturesUnordered::new();
346
347        for (world_id, handle_opt) in worlds_to_cleanup {
348            if let Some(handle) = handle_opt {
349                let path = handle.path.clone();
350                let repo_path = self.repo_path.clone();
351
352                cleanup_futures.push(async move {
353                    let result = Command::new("git")
354                        .arg("worktree")
355                        .arg("remove")
356                        .arg("--force")
357                        .arg(&path)
358                        .current_dir(&repo_path)
359                        .output()
360                        .await;
361
362                    match result {
363                        Ok(output) if output.status.success() => {
364                            debug!(world_id = ?world_id, "World cleaned up");
365                        },
366                        Ok(output) => {
367                            let stderr = String::from_utf8_lossy(&output.stderr);
368                            warn!(
369                                world_id = ?world_id,
370                                error = %stderr,
371                                "Failed to cleanup world"
372                            );
373                        },
374                        Err(e) => {
375                            warn!(
376                                world_id = ?world_id,
377                                error = %e,
378                                "Failed to execute git worktree remove"
379                            );
380                        },
381                    }
382                });
383            }
384        }
385
386        // Execute all cleanup operations in parallel
387        while cleanup_futures.next().await.is_some() {}
388
389        info!(issue_number = issue_number, "All worlds cleaned up");
390        Ok(())
391    }
392
393    /// Gets the handle for a specific world
394    ///
395    /// # Arguments
396    /// * `world_id` - The WorldId to get
397    ///
398    /// # Returns
399    /// `Option<WorldWorktreeHandle>` - The handle if the world is active
400    pub async fn get_world_handle(&self, world_id: WorldId) -> Option<WorldWorktreeHandle> {
401        self.active_worlds.lock().await.get(&world_id).cloned()
402    }
403
404    /// Gets handles for all active worlds
405    ///
406    /// # Returns
407    /// HashMap of all active world handles
408    pub async fn get_all_world_handles(&self) -> HashMap<WorldId, WorldWorktreeHandle> {
409        self.active_worlds.lock().await.clone()
410    }
411
412    /// Checks if a world is active
413    ///
414    /// # Arguments
415    /// * `world_id` - The WorldId to check
416    pub async fn is_world_active(&self, world_id: WorldId) -> bool {
417        self.active_worlds.lock().await.contains_key(&world_id)
418    }
419
420    /// Gets the count of active worlds
421    pub async fn active_world_count(&self) -> usize {
422        self.active_worlds.lock().await.len()
423    }
424
425    /// Merges the winning world's changes back to the main branch
426    ///
427    /// # Arguments
428    /// * `world_id` - The winning WorldId
429    /// * `target_branch` - Target branch to merge into (usually "main")
430    pub async fn merge_winning_world(
431        &self,
432        world_id: WorldId,
433        target_branch: &str,
434    ) -> Result<(), GitError> {
435        let handle = self
436            .get_world_handle(world_id)
437            .await
438            .ok_or_else(|| GitError::InvalidPath(format!("World {:?} not found", world_id)))?;
439
440        info!(
441            world_id = ?world_id,
442            branch = %handle.branch,
443            target = target_branch,
444            "Merging winning world"
445        );
446
447        // The actual merge will be done by the orchestrator
448        // This just validates the world exists
449        // TODO: Implement actual merge logic when needed
450
451        Ok(())
452    }
453
454    /// Gets the base path for worktrees
455    pub fn base_path(&self) -> &Path {
456        &self.base_path
457    }
458
459    /// Gets statistics about active worlds
460    pub async fn get_statistics(&self) -> WorldStatistics {
461        let active = self.active_worlds.lock().await;
462
463        WorldStatistics {
464            total_active: active.len(),
465            worlds: active.iter().map(|(id, handle)| (*id, handle.path.clone())).collect(),
466        }
467    }
468}
469
470/// Statistics about active worlds
471#[derive(Debug, Clone)]
472pub struct WorldStatistics {
473    /// Total number of active worlds
474    pub total_active: usize,
475    /// Map of WorldId -> worktree path
476    pub worlds: HashMap<WorldId, PathBuf>,
477}
478
479impl WorldStatistics {
480    /// Checks if all 5 worlds are active
481    pub fn all_active(&self) -> bool {
482        self.total_active == 5
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use tempfile::TempDir;
490
491    async fn setup_test_manager() -> (FiveWorldsManager, TempDir, TempDir) {
492        let repo_dir = TempDir::new().unwrap();
493        let worktree_dir = TempDir::new().unwrap();
494
495        // Initialize Git repo
496        let repo = git2::Repository::init(repo_dir.path()).unwrap();
497        let sig = git2::Signature::now("Test", "test@example.com").unwrap();
498        let tree_id = {
499            let mut index = repo.index().unwrap();
500            index.write_tree().unwrap()
501        };
502        let tree = repo.find_tree(tree_id).unwrap();
503        repo.commit(Some("HEAD"), &sig, &sig, "Initial commit", &tree, &[]).unwrap();
504
505        let manager = FiveWorldsManager::new(
506            worktree_dir.path().to_path_buf(),
507            repo_dir.path().to_path_buf(),
508        );
509
510        (manager, repo_dir, worktree_dir)
511    }
512
513    #[tokio::test]
514    async fn test_spawn_single_world() {
515        let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
516
517        let handle = manager
518            .spawn_world(270, "test_task", WorldId::Alpha)
519            .await
520            .expect("Failed to spawn world");
521
522        assert_eq!(handle.world_id, WorldId::Alpha);
523        assert!(handle.path.to_string_lossy().contains("world-alpha"));
524        assert!(handle.branch.contains("world-alpha-issue-270"));
525
526        // Verify it's registered as active
527        assert!(manager.is_world_active(WorldId::Alpha).await);
528        assert_eq!(manager.active_world_count().await, 1);
529
530        // Cleanup
531        manager.cleanup_world(WorldId::Alpha).await.unwrap();
532        assert!(!manager.is_world_active(WorldId::Alpha).await);
533    }
534
535    #[tokio::test]
536    async fn test_spawn_all_worlds() {
537        let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
538
539        let handles = manager
540            .spawn_all_worlds(270, "test_task")
541            .await
542            .expect("Failed to spawn all worlds");
543
544        assert_eq!(handles.len(), 5);
545        assert!(handles.contains_key(&WorldId::Alpha));
546        assert!(handles.contains_key(&WorldId::Beta));
547        assert!(handles.contains_key(&WorldId::Gamma));
548        assert!(handles.contains_key(&WorldId::Delta));
549        assert!(handles.contains_key(&WorldId::Epsilon));
550
551        // Verify all are active
552        for world_id in WorldId::all() {
553            assert!(manager.is_world_active(world_id).await);
554        }
555
556        let stats = manager.get_statistics().await;
557        assert!(stats.all_active());
558
559        // Cleanup
560        manager.cleanup_all_worlds_for_issue(270).await.unwrap();
561        assert_eq!(manager.active_world_count().await, 0);
562    }
563
564    #[tokio::test]
565    async fn test_world_handle_retrieval() {
566        let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
567
568        manager.spawn_world(270, "test_task", WorldId::Alpha).await.unwrap();
569
570        // Get specific world handle
571        let handle = manager.get_world_handle(WorldId::Alpha).await;
572        assert!(handle.is_some());
573        assert_eq!(handle.unwrap().world_id, WorldId::Alpha);
574
575        // Get non-existent world
576        let no_handle = manager.get_world_handle(WorldId::Beta).await;
577        assert!(no_handle.is_none());
578
579        // Get all handles
580        let all_handles = manager.get_all_world_handles().await;
581        assert_eq!(all_handles.len(), 1);
582        assert!(all_handles.contains_key(&WorldId::Alpha));
583
584        // Cleanup
585        manager.cleanup_world(WorldId::Alpha).await.unwrap();
586    }
587
588    #[tokio::test]
589    async fn test_statistics() {
590        let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
591
592        // Initially empty
593        let stats = manager.get_statistics().await;
594        assert_eq!(stats.total_active, 0);
595        assert!(!stats.all_active());
596
597        // Spawn all worlds
598        manager.spawn_all_worlds(270, "test_task").await.unwrap();
599
600        let stats = manager.get_statistics().await;
601        assert_eq!(stats.total_active, 5);
602        assert!(stats.all_active());
603        assert_eq!(stats.worlds.len(), 5);
604
605        // Cleanup
606        manager.cleanup_all_worlds_for_issue(270).await.unwrap();
607    }
608
609    #[tokio::test]
610    async fn test_partial_cleanup() {
611        let (manager, _repo_dir, _worktree_dir) = setup_test_manager().await;
612
613        manager.spawn_all_worlds(270, "test_task").await.unwrap();
614        assert_eq!(manager.active_world_count().await, 5);
615
616        // Cleanup Alpha and Beta only
617        manager.cleanup_world(WorldId::Alpha).await.unwrap();
618        manager.cleanup_world(WorldId::Beta).await.unwrap();
619
620        assert_eq!(manager.active_world_count().await, 3);
621        assert!(!manager.is_world_active(WorldId::Alpha).await);
622        assert!(!manager.is_world_active(WorldId::Beta).await);
623        assert!(manager.is_world_active(WorldId::Gamma).await);
624
625        // Cleanup remaining
626        manager.cleanup_all_worlds_for_issue(270).await.unwrap();
627    }
628}