Skip to main content

codetether_agent/ralph/
ralph_loop.rs

1//! Ralph loop - the core autonomous execution loop
2
3use super::types::*;
4use crate::provider::Provider;
5use crate::swarm::run_agent_loop;
6use crate::tool::ToolRegistry;
7use crate::tui::ralph_view::{RalphEvent, RalphStoryInfo, RalphStoryStatus};
8use crate::tui::swarm_view::SwarmEvent;
9use crate::worktree::WorktreeManager;
10use std::path::PathBuf;
11use std::process::Command;
12use std::sync::Arc;
13use tokio::sync::mpsc;
14use tracing::{debug, info, warn};
15
16/// The main Ralph executor
17pub struct RalphLoop {
18    state: RalphState,
19    provider: Arc<dyn Provider>,
20    model: String,
21    config: RalphConfig,
22    event_tx: Option<mpsc::Sender<RalphEvent>>,
23}
24
25impl RalphLoop {
26    /// Create a new Ralph loop
27    pub async fn new(
28        prd_path: PathBuf,
29        provider: Arc<dyn Provider>,
30        model: String,
31        config: RalphConfig,
32    ) -> anyhow::Result<Self> {
33        let prd = Prd::load(&prd_path).await?;
34
35        // Get working directory - use parent of prd_path or current directory
36        let working_dir = if let Some(parent) = prd_path.parent() {
37            if parent.as_os_str().is_empty() {
38                std::env::current_dir()?
39            } else {
40                parent.to_path_buf()
41            }
42        } else {
43            std::env::current_dir()?
44        };
45
46        info!(
47            "Loaded PRD: {} - {} ({} stories)",
48            prd.project,
49            prd.feature,
50            prd.user_stories.len()
51        );
52
53        let state = RalphState {
54            prd,
55            current_iteration: 0,
56            max_iterations: config.max_iterations,
57            status: RalphStatus::Pending,
58            progress_log: Vec::new(),
59            prd_path: prd_path.clone(),
60            working_dir,
61        };
62
63        Ok(Self {
64            state,
65            provider,
66            model,
67            config,
68            event_tx: None,
69        })
70    }
71
72    /// Attach an event channel for TUI updates
73    pub fn with_event_tx(mut self, tx: mpsc::Sender<RalphEvent>) -> Self {
74        self.event_tx = Some(tx);
75        self
76    }
77
78    /// Non-blocking send of a Ralph event
79    fn try_send_event(&self, event: RalphEvent) {
80        if let Some(ref tx) = self.event_tx {
81            let _ = tx.try_send(event);
82        }
83    }
84
85    /// Create a bridge that forwards SwarmEvent → RalphEvent for a given story_id.
86    /// Returns the sender to pass to `run_agent_loop` and a join handle for the
87    /// forwarding task.
88    fn create_swarm_event_bridge(
89        ralph_tx: &mpsc::Sender<RalphEvent>,
90        story_id: String,
91    ) -> (mpsc::Sender<SwarmEvent>, tokio::task::JoinHandle<()>) {
92        let (swarm_tx, mut swarm_rx) = mpsc::channel::<SwarmEvent>(100);
93        let ralph_tx = ralph_tx.clone();
94        let handle = tokio::spawn(async move {
95            while let Some(event) = swarm_rx.recv().await {
96                let ralph_event = match event {
97                    SwarmEvent::AgentToolCall { tool_name, .. } => RalphEvent::StoryToolCall {
98                        story_id: story_id.clone(),
99                        tool_name,
100                    },
101                    SwarmEvent::AgentToolCallDetail { detail, .. } => {
102                        RalphEvent::StoryToolCallDetail {
103                            story_id: story_id.clone(),
104                            detail,
105                        }
106                    }
107                    SwarmEvent::AgentMessage { entry, .. } => RalphEvent::StoryMessage {
108                        story_id: story_id.clone(),
109                        entry,
110                    },
111                    SwarmEvent::AgentOutput { output, .. } => RalphEvent::StoryOutput {
112                        story_id: story_id.clone(),
113                        output,
114                    },
115                    SwarmEvent::AgentError { error, .. } => RalphEvent::StoryError {
116                        story_id: story_id.clone(),
117                        error,
118                    },
119                    _ => continue, // Skip swarm-specific events
120                };
121                if ralph_tx.send(ralph_event).await.is_err() {
122                    break;
123                }
124            }
125        });
126        (swarm_tx, handle)
127    }
128
129    /// Build initial RalphStoryInfo list from the PRD
130    fn build_story_infos(prd: &Prd) -> Vec<RalphStoryInfo> {
131        prd.user_stories
132            .iter()
133            .map(|s| RalphStoryInfo {
134                id: s.id.clone(),
135                title: s.title.clone(),
136                status: if s.passes {
137                    RalphStoryStatus::Passed
138                } else {
139                    RalphStoryStatus::Pending
140                },
141                priority: s.priority,
142                depends_on: s.depends_on.clone(),
143                quality_checks: Vec::new(),
144                tool_call_history: Vec::new(),
145                messages: Vec::new(),
146                output: None,
147                error: None,
148                merge_summary: None,
149                steps: 0,
150                current_tool: None,
151            })
152            .collect()
153    }
154
155    /// Run the Ralph loop until completion or max iterations
156    pub async fn run(&mut self) -> anyhow::Result<RalphState> {
157        self.state.status = RalphStatus::Running;
158
159        // Emit Started event
160        self.try_send_event(RalphEvent::Started {
161            project: self.state.prd.project.clone(),
162            feature: self.state.prd.feature.clone(),
163            stories: Self::build_story_infos(&self.state.prd),
164            max_iterations: self.state.max_iterations,
165        });
166
167        // Switch to feature branch
168        if !self.state.prd.branch_name.is_empty() {
169            info!("Switching to branch: {}", self.state.prd.branch_name);
170            self.git_checkout(&self.state.prd.branch_name)?;
171        }
172
173        // Choose execution mode
174        if self.config.parallel_enabled {
175            self.run_parallel().await?;
176        } else {
177            self.run_sequential().await?;
178        }
179
180        if self.state.status != RalphStatus::Completed
181            && self.state.current_iteration >= self.state.max_iterations
182        {
183            self.state.status = RalphStatus::MaxIterations;
184        }
185
186        // Clean up orphaned worktrees and branches
187        if self.config.worktree_enabled {
188            if let Ok(mgr) = WorktreeManager::new(&self.state.working_dir) {
189                match mgr.cleanup_all() {
190                    Ok(count) if count > 0 => {
191                        info!(cleaned = count, "Cleaned up orphaned worktrees/branches");
192                    }
193                    Ok(_) => {}
194                    Err(e) => {
195                        warn!(error = %e, "Failed to cleanup orphaned worktrees");
196                    }
197                }
198            }
199        }
200
201        info!(
202            "Ralph finished: {:?}, {}/{} stories passed",
203            self.state.status,
204            self.state.prd.passed_count(),
205            self.state.prd.user_stories.len()
206        );
207
208        // Emit Complete event
209        self.try_send_event(RalphEvent::Complete {
210            status: format!("{:?}", self.state.status),
211            passed: self.state.prd.passed_count(),
212            total: self.state.prd.user_stories.len(),
213        });
214
215        Ok(self.state.clone())
216    }
217
218    /// Run stories sequentially (original behavior)
219    async fn run_sequential(&mut self) -> anyhow::Result<()> {
220        while self.state.current_iteration < self.state.max_iterations {
221            self.state.current_iteration += 1;
222            info!(
223                "=== Ralph iteration {} of {} ===",
224                self.state.current_iteration, self.state.max_iterations
225            );
226
227            // Emit iteration event
228            self.try_send_event(RalphEvent::IterationStarted {
229                iteration: self.state.current_iteration,
230                max_iterations: self.state.max_iterations,
231            });
232
233            // Check if all stories are complete
234            if self.state.prd.is_complete() {
235                info!("All stories complete!");
236                self.state.status = RalphStatus::Completed;
237                break;
238            }
239
240            // Get next story to work on
241            let story = match self.state.prd.next_story() {
242                Some(s) => s.clone(),
243                None => {
244                    warn!("No available stories (dependencies not met)");
245                    break;
246                }
247            };
248
249            info!("Working on story: {} - {}", story.id, story.title);
250
251            // Emit StoryStarted event
252            self.try_send_event(RalphEvent::StoryStarted {
253                story_id: story.id.clone(),
254            });
255
256            // Build the prompt
257            let prompt = self.build_prompt(&story);
258
259            // Call the LLM
260            match self.call_llm(&story.id, &prompt).await {
261                Ok(response) => {
262                    // Log progress
263                    let entry = ProgressEntry {
264                        story_id: story.id.clone(),
265                        iteration: self.state.current_iteration,
266                        status: "completed".to_string(),
267                        learnings: self.extract_learnings(&response),
268                        files_changed: Vec::new(),
269                        timestamp: chrono::Utc::now().to_rfc3339(),
270                    };
271                    self.append_progress(&entry, &response)?;
272                    self.state.progress_log.push(entry);
273
274                    // Run quality gates
275                    if self.config.quality_checks_enabled {
276                        if self.run_quality_gates_with_events(&story.id).await? {
277                            info!("Story {} passed quality checks!", story.id);
278                            self.state.prd.mark_passed(&story.id);
279
280                            self.try_send_event(RalphEvent::StoryComplete {
281                                story_id: story.id.clone(),
282                                passed: true,
283                            });
284
285                            // Commit changes
286                            if self.config.auto_commit {
287                                self.commit_story(&story)?;
288                            }
289
290                            // Save updated PRD
291                            self.state.prd.save(&self.state.prd_path).await?;
292                        } else {
293                            warn!("Story {} failed quality checks", story.id);
294                            self.try_send_event(RalphEvent::StoryComplete {
295                                story_id: story.id.clone(),
296                                passed: false,
297                            });
298                        }
299                    } else {
300                        // No quality checks, just mark as passed
301                        self.state.prd.mark_passed(&story.id);
302                        self.state.prd.save(&self.state.prd_path).await?;
303
304                        self.try_send_event(RalphEvent::StoryComplete {
305                            story_id: story.id.clone(),
306                            passed: true,
307                        });
308                    }
309                }
310                Err(e) => {
311                    warn!("LLM call failed: {}", e);
312                    let entry = ProgressEntry {
313                        story_id: story.id.clone(),
314                        iteration: self.state.current_iteration,
315                        status: format!("failed: {}", e),
316                        learnings: Vec::new(),
317                        files_changed: Vec::new(),
318                        timestamp: chrono::Utc::now().to_rfc3339(),
319                    };
320                    self.state.progress_log.push(entry);
321
322                    self.try_send_event(RalphEvent::StoryError {
323                        story_id: story.id.clone(),
324                        error: format!("{}", e),
325                    });
326                }
327            }
328        }
329
330        Ok(())
331    }
332
333    /// Run stories in parallel by stage with worktree isolation
334    async fn run_parallel(&mut self) -> anyhow::Result<()> {
335        // Clone stages upfront to avoid borrow issues
336        let stages: Vec<Vec<UserStory>> = self
337            .state
338            .prd
339            .stages()
340            .into_iter()
341            .map(|stage| stage.into_iter().cloned().collect())
342            .collect();
343        let total_stages = stages.len();
344
345        info!(
346            "Parallel execution: {} stages, {} max concurrent stories",
347            total_stages, self.config.max_concurrent_stories
348        );
349
350        // Create worktree manager if enabled
351        let worktree_mgr = if self.config.worktree_enabled {
352            match WorktreeManager::new(&self.state.working_dir) {
353                Ok(mgr) => {
354                    info!("Worktree isolation enabled for parallel stories");
355                    Some(Arc::new(mgr))
356                }
357                Err(e) => {
358                    warn!(
359                        "Failed to create worktree manager: {}, falling back to sequential within stages",
360                        e
361                    );
362                    None
363                }
364            }
365        } else {
366            None
367        };
368
369        for (stage_idx, stage_stories) in stages.into_iter().enumerate() {
370            if self.state.prd.is_complete() {
371                info!("All stories complete!");
372                self.state.status = RalphStatus::Completed;
373                break;
374            }
375
376            if self.state.current_iteration >= self.state.max_iterations {
377                break;
378            }
379
380            let story_count = stage_stories.len();
381            info!(
382                "=== Stage {}/{}: {} stories in parallel ===",
383                stage_idx + 1,
384                total_stages,
385                story_count
386            );
387
388            // Stories are already cloned from stages()
389            let stories: Vec<UserStory> = stage_stories;
390
391            // Execute stories in parallel
392            let semaphore = Arc::new(tokio::sync::Semaphore::new(
393                self.config.max_concurrent_stories,
394            ));
395            let provider = Arc::clone(&self.provider);
396            let model = self.model.clone();
397            let prd_info = (
398                self.state.prd.project.clone(),
399                self.state.prd.feature.clone(),
400            );
401            let working_dir = self.state.working_dir.clone();
402            let progress_path = self.config.progress_path.clone();
403
404            let mut handles = Vec::new();
405
406            for story in stories {
407                let sem = Arc::clone(&semaphore);
408                let provider = Arc::clone(&provider);
409                let model = model.clone();
410                let prd_info = prd_info.clone();
411                let working_dir = working_dir.clone();
412                let worktree_mgr = worktree_mgr.clone();
413                let progress_path = progress_path.clone();
414                let ralph_tx = self.event_tx.clone();
415
416                let handle = tokio::spawn(async move {
417                    let _permit = sem.acquire().await.expect("semaphore closed");
418
419                    // Create worktree for this story if enabled
420                    let (story_working_dir, worktree_info) = if let Some(ref mgr) = worktree_mgr {
421                        match mgr.create(&story.id.to_lowercase().replace("-", "_")) {
422                            Ok(wt) => {
423                                // Inject [workspace] stub for hermetic isolation
424                                if let Err(e) = mgr.inject_workspace_stub(&wt.path) {
425                                    warn!(
426                                        story_id = %story.id,
427                                        error = %e,
428                                        "Failed to inject workspace stub"
429                                    );
430                                }
431                                info!(
432                                    story_id = %story.id,
433                                    worktree_path = %wt.path.display(),
434                                    "Created worktree for story"
435                                );
436                                (wt.path.clone(), Some(wt))
437                            }
438                            Err(e) => {
439                                warn!(
440                                    story_id = %story.id,
441                                    error = %e,
442                                    "Failed to create worktree, using main directory"
443                                );
444                                (working_dir.clone(), None)
445                            }
446                        }
447                    } else {
448                        (working_dir.clone(), None)
449                    };
450
451                    info!(
452                        "Working on story: {} - {} (in {:?})",
453                        story.id, story.title, story_working_dir
454                    );
455
456                    // Emit StoryStarted event
457                    if let Some(ref tx) = ralph_tx {
458                        let _ = tx
459                            .send(RalphEvent::StoryStarted {
460                                story_id: story.id.clone(),
461                            })
462                            .await;
463                    }
464
465                    // Build the prompt with worktree awareness
466                    let prompt = Self::build_story_prompt(&story, &prd_info, &story_working_dir);
467
468                    // Create event bridge for this story's sub-agent
469                    let (bridge_tx, _bridge_handle) = if let Some(ref tx) = ralph_tx {
470                        let (btx, handle) = Self::create_swarm_event_bridge(tx, story.id.clone());
471                        (Some(btx), Some(handle))
472                    } else {
473                        (None, None)
474                    };
475
476                    // Call the LLM
477                    let result = Self::call_llm_static(
478                        &provider,
479                        &model,
480                        &prompt,
481                        &story_working_dir,
482                        bridge_tx,
483                        story.id.clone(),
484                    )
485                    .await;
486
487                    let entry = match &result {
488                        Ok(response) => {
489                            // Append progress to worktree-local progress file
490                            let progress_file = story_working_dir.join(&progress_path);
491                            let _ = std::fs::write(&progress_file, response);
492
493                            ProgressEntry {
494                                story_id: story.id.clone(),
495                                iteration: 1,
496                                status: "completed".to_string(),
497                                learnings: Self::extract_learnings_static(response),
498                                files_changed: Vec::new(),
499                                timestamp: chrono::Utc::now().to_rfc3339(),
500                            }
501                        }
502                        Err(e) => {
503                            warn!("LLM call failed for story {}: {}", story.id, e);
504                            ProgressEntry {
505                                story_id: story.id.clone(),
506                                iteration: 1,
507                                status: format!("failed: {}", e),
508                                learnings: Vec::new(),
509                                files_changed: Vec::new(),
510                                timestamp: chrono::Utc::now().to_rfc3339(),
511                            }
512                        }
513                    };
514
515                    (story, result.is_ok(), entry, worktree_info, worktree_mgr)
516                });
517
518                handles.push(handle);
519            }
520
521            // Wait for all stories in this stage
522            for handle in handles {
523                match handle.await {
524                    Ok((story, success, entry, worktree_info, worktree_mgr)) => {
525                        self.state.current_iteration += 1;
526                        self.state.progress_log.push(entry);
527
528                        if success {
529                            // Run quality gates in the worktree (or main dir)
530                            let check_dir = worktree_info
531                                .as_ref()
532                                .map(|wt| wt.path.clone())
533                                .unwrap_or_else(|| self.state.working_dir.clone());
534
535                            let quality_passed = if self.config.quality_checks_enabled {
536                                self.run_quality_gates_in_dir_with_events(&check_dir, &story.id)
537                                    .await
538                                    .unwrap_or(false)
539                            } else {
540                                true
541                            };
542
543                            if quality_passed {
544                                info!("Story {} passed quality checks!", story.id);
545
546                                // Commit in worktree first
547                                if let Some(ref wt) = worktree_info {
548                                    let _ = Self::commit_in_dir(&wt.path, &story);
549                                }
550
551                                // Merge worktree back to main
552                                if let (Some(wt), Some(mgr)) = (&worktree_info, &worktree_mgr) {
553                                    match mgr.merge(wt) {
554                                        Ok(merge_result) => {
555                                            if merge_result.success {
556                                                info!(
557                                                    story_id = %story.id,
558                                                    files_changed = merge_result.files_changed,
559                                                    "Merged story changes successfully"
560                                                );
561                                                self.state.prd.mark_passed(&story.id);
562                                                self.try_send_event(RalphEvent::StoryMerge {
563                                                    story_id: story.id.clone(),
564                                                    success: true,
565                                                    summary: merge_result.summary.clone(),
566                                                });
567                                                self.try_send_event(RalphEvent::StoryComplete {
568                                                    story_id: story.id.clone(),
569                                                    passed: true,
570                                                });
571                                                // Cleanup worktree
572                                                let _ = mgr.cleanup(wt);
573                                            } else if !merge_result.conflicts.is_empty() {
574                                                // Real conflicts - spawn conflict resolver
575                                                info!(
576                                                    story_id = %story.id,
577                                                    num_conflicts = merge_result.conflicts.len(),
578                                                    "Spawning conflict resolver sub-agent"
579                                                );
580
581                                                // Try to resolve conflicts
582                                                match Self::resolve_conflicts_static(
583                                                    &provider,
584                                                    &model,
585                                                    &working_dir,
586                                                    &story,
587                                                    &merge_result.conflicts,
588                                                    &merge_result.conflict_diffs,
589                                                )
590                                                .await
591                                                {
592                                                    Ok(resolved) => {
593                                                        if resolved {
594                                                            // Complete the merge after resolution
595                                                            let commit_msg = format!(
596                                                                "Merge: resolved conflicts for {}",
597                                                                story.id
598                                                            );
599                                                            match mgr
600                                                                .complete_merge(wt, &commit_msg)
601                                                            {
602                                                                Ok(final_result) => {
603                                                                    if final_result.success {
604                                                                        info!(
605                                                                            story_id = %story.id,
606                                                                            "Merge completed after conflict resolution"
607                                                                        );
608                                                                        self.state
609                                                                            .prd
610                                                                            .mark_passed(&story.id);
611                                                                    } else {
612                                                                        warn!(
613                                                                            story_id = %story.id,
614                                                                            "Merge failed even after resolution"
615                                                                        );
616                                                                        let _ = mgr.abort_merge();
617                                                                    }
618                                                                }
619                                                                Err(e) => {
620                                                                    warn!(
621                                                                        story_id = %story.id,
622                                                                        error = %e,
623                                                                        "Failed to complete merge after resolution"
624                                                                    );
625                                                                    let _ = mgr.abort_merge();
626                                                                }
627                                                            }
628                                                        } else {
629                                                            warn!(
630                                                                story_id = %story.id,
631                                                                "Conflict resolver could not resolve all conflicts"
632                                                            );
633                                                            let _ = mgr.abort_merge();
634                                                        }
635                                                    }
636                                                    Err(e) => {
637                                                        warn!(
638                                                            story_id = %story.id,
639                                                            error = %e,
640                                                            "Conflict resolver failed"
641                                                        );
642                                                        let _ = mgr.abort_merge();
643                                                    }
644                                                }
645                                                // Cleanup worktree
646                                                let _ = mgr.cleanup(wt);
647                                            } else if merge_result.aborted {
648                                                // Non-conflict failure that was already aborted
649                                                warn!(
650                                                    story_id = %story.id,
651                                                    summary = %merge_result.summary,
652                                                    "Merge was aborted due to non-conflict failure"
653                                                );
654                                                // Cleanup worktree
655                                                let _ = mgr.cleanup(wt);
656                                            } else {
657                                                // Merge in progress state (should not reach here)
658                                                warn!(
659                                                    story_id = %story.id,
660                                                    summary = %merge_result.summary,
661                                                    "Merge failed but not aborted - manual intervention may be needed"
662                                                );
663                                                // Don't cleanup - leave for debugging
664                                            }
665                                        }
666                                        Err(e) => {
667                                            warn!(
668                                                story_id = %story.id,
669                                                error = %e,
670                                                "Failed to merge worktree"
671                                            );
672                                        }
673                                    }
674                                } else {
675                                    // No worktree, just mark passed
676                                    self.state.prd.mark_passed(&story.id);
677                                    self.try_send_event(RalphEvent::StoryComplete {
678                                        story_id: story.id.clone(),
679                                        passed: true,
680                                    });
681                                }
682                            } else {
683                                warn!("Story {} failed quality checks", story.id);
684                                self.try_send_event(RalphEvent::StoryComplete {
685                                    story_id: story.id.clone(),
686                                    passed: false,
687                                });
688                                // Cleanup worktree without merging
689                                if let (Some(wt), Some(mgr)) = (&worktree_info, &worktree_mgr) {
690                                    let _ = mgr.cleanup(wt);
691                                }
692                            }
693                        } else {
694                            // Failed - cleanup worktree without merging (keep for debugging)
695                            self.try_send_event(RalphEvent::StoryError {
696                                story_id: story.id.clone(),
697                                error: "LLM call failed".to_string(),
698                            });
699                            if let Some(ref wt) = worktree_info {
700                                info!(
701                                    story_id = %story.id,
702                                    worktree_path = %wt.path.display(),
703                                    "Keeping worktree for debugging (story failed)"
704                                );
705                            }
706                        }
707                    }
708                    Err(e) => {
709                        warn!("Story execution task failed: {}", e);
710                    }
711                }
712            }
713
714            // Save PRD after each stage
715            self.state.prd.save(&self.state.prd_path).await?;
716        }
717
718        Ok(())
719    }
720
721    /// Build prompt for a story (static version for parallel execution)
722    fn build_story_prompt(
723        story: &UserStory,
724        prd_info: &(String, String),
725        working_dir: &PathBuf,
726    ) -> String {
727        format!(
728            r#"# PRD: {} - {}
729
730## Working Directory: {}
731
732## Current Story: {} - {}
733
734{}
735
736### Acceptance Criteria:
737{}
738
739## WORKFLOW (follow this exactly):
740
7411. **EXPLORE** (2-4 tool calls): Use `glob` and `read` to understand existing code
7422. **IMPLEMENT** (5-15 tool calls): Use `write` or `edit` to make changes
7433. **VERIFY**: Run `bash` with command `cargo check 2>&1` to check for errors
7444. **FIX OR FINISH**:
745   - If no errors: Output `STORY_COMPLETE: {}` and STOP
746   - If errors: Parse the error, fix it, re-run cargo check (max 3 fix attempts)
747   - After 3 failed attempts: Output `STORY_BLOCKED: <error summary>` and STOP
748
749## UNDERSTANDING CARGO ERRORS:
750
751When `cargo check` fails, the output shows:
752```
753error[E0432]: unresolved import `crate::foo::bar`
754  --> src/file.rs:10:5
755   |
75610 | use crate::foo::bar;
757   |             ^^^ could not find `bar` in `foo`
758```
759
760Key parts:
761- `error[E0432]` = error code (search rustc --explain E0432 for details)
762- `src/file.rs:10:5` = file:line:column where error occurs
763- The message explains what's wrong
764
765COMMON FIXES:
766- "unresolved import" → module doesn't exist or isn't exported, check mod.rs
767- "cannot find" → typo in name or missing import
768- "mismatched types" → wrong type, check function signatures
769- "trait bound not satisfied" → missing impl or use statement
770
771## TOOL USAGE:
772- `read`: Read file content (always read before editing!)
773- `edit`: Modify files (MUST include 3+ lines before/after for unique context)
774- `write`: Create new files
775- `bash`: Run commands with `{{"command": "...", "cwd": "{}"}}`
776
777## CRITICAL RULES:
778- ALWAYS read a file before editing it
779- When edit fails with "ambiguous match", include MORE context lines
780- Do NOT add TODO/placeholder comments
781- Run `cargo check 2>&1` to see ALL errors including warnings
782- Count your fix attempts - STOP after 3 failures
783
784## TERMINATION:
785SUCCESS: Output `STORY_COMPLETE: {}`
786BLOCKED: Output `STORY_BLOCKED: <brief error description>`
787
788Do NOT keep iterating indefinitely. Stop when done or blocked.
789"#,
790            prd_info.0,
791            prd_info.1,
792            working_dir.display(),
793            story.id,
794            story.title,
795            story.description,
796            story
797                .acceptance_criteria
798                .iter()
799                .map(|c| format!("- {}", c))
800                .collect::<Vec<_>>()
801                .join("\n"),
802            story.id,
803            working_dir.display(),
804            story.id
805        )
806    }
807
808    /// Call LLM with agentic tool loop (static version for parallel execution)
809    async fn call_llm_static(
810        provider: &Arc<dyn Provider>,
811        model: &str,
812        prompt: &str,
813        working_dir: &PathBuf,
814        event_tx: Option<mpsc::Sender<SwarmEvent>>,
815        story_id: String,
816    ) -> anyhow::Result<String> {
817        // Build system prompt with AGENTS.md
818        let system_prompt = crate::agent::builtin::build_system_prompt(working_dir);
819
820        // Create tool registry with provider for file operations
821        let tool_registry =
822            ToolRegistry::with_provider_arc(Arc::clone(provider), model.to_string());
823
824        // Filter out 'question' tool - sub-agents must be autonomous, not interactive
825        let tool_definitions: Vec<_> = tool_registry
826            .definitions()
827            .into_iter()
828            .filter(|t| t.name != "question")
829            .collect();
830
831        info!(
832            "Ralph sub-agent starting with {} tools in {:?}",
833            tool_definitions.len(),
834            working_dir
835        );
836
837        // Run the agentic loop with tools
838        let (output, steps, tool_calls, _exit_reason) = run_agent_loop(
839            Arc::clone(provider),
840            model,
841            &system_prompt,
842            prompt,
843            tool_definitions,
844            tool_registry, // Already an Arc<ToolRegistry>
845            30,            // max steps per story (focused implementation)
846            180,           // 3 minute timeout per story
847            event_tx,
848            story_id,
849        )
850        .await?;
851
852        info!(
853            "Ralph sub-agent completed: {} steps, {} tool calls",
854            steps, tool_calls
855        );
856
857        Ok(output)
858    }
859
860    /// Resolve merge conflicts using a dedicated sub-agent
861    async fn resolve_conflicts_static(
862        provider: &Arc<dyn Provider>,
863        model: &str,
864        working_dir: &PathBuf,
865        story: &UserStory,
866        conflicts: &[String],
867        conflict_diffs: &[(String, String)],
868    ) -> anyhow::Result<bool> {
869        info!(
870            story_id = %story.id,
871            num_conflicts = conflicts.len(),
872            "Starting conflict resolution sub-agent"
873        );
874
875        // Build prompt with conflict context
876        let conflict_info = conflict_diffs
877            .iter()
878            .map(|(file, diff)| format!("### File: {}\n```diff\n{}\n```", file, diff))
879            .collect::<Vec<_>>()
880            .join("\n\n");
881
882        let prompt = format!(
883            r#"# CONFLICT RESOLUTION TASK
884
885## Story Context: {} - {}
886{}
887
888## Conflicting Files
889The following files have merge conflicts that need resolution:
890{}
891
892## Conflict Details
893{}
894
895## Your Task
8961. Read each conflicting file to see the conflict markers
8972. Understand what BOTH sides are trying to do:
898   - HEAD (main branch): the current state
899   - The incoming branch: the sub-agent's changes for story {}
9003. Resolve each conflict by:
901   - Keeping BOTH changes if they don't actually conflict
902   - Merging the logic if they touch the same code
903   - Preferring the sub-agent's changes if they implement the story requirement
9044. Remove ALL conflict markers (<<<<<<<, =======, >>>>>>>)
9055. Ensure the final code compiles: run `cargo check`
906
907## CRITICAL RULES
908- Do NOT leave any conflict markers in files
909- Do NOT just pick one side - understand and merge the intent
910- MUST run `cargo check` after resolving to verify
911- Stage resolved files with `git add <file>`
912
913## Termination
914SUCCESS: Output `CONFLICTS_RESOLVED` when all files are resolved and compile
915FAILED: Output `CONFLICTS_UNRESOLVED: <reason>` if you cannot resolve
916
917Working directory: {}
918"#,
919            story.id,
920            story.title,
921            story.description,
922            conflicts
923                .iter()
924                .map(|f| format!("- {}", f))
925                .collect::<Vec<_>>()
926                .join("\n"),
927            conflict_info,
928            story.id,
929            working_dir.display()
930        );
931
932        // Build system prompt
933        let system_prompt = crate::agent::builtin::build_system_prompt(working_dir);
934
935        // Create tool registry
936        let tool_registry =
937            ToolRegistry::with_provider_arc(Arc::clone(provider), model.to_string());
938
939        let tool_definitions: Vec<_> = tool_registry
940            .definitions()
941            .into_iter()
942            .filter(|t| t.name != "question")
943            .collect();
944
945        info!(
946            "Conflict resolver starting with {} tools",
947            tool_definitions.len()
948        );
949
950        // Run the resolver with smaller limits (conflicts should be quick to resolve)
951        let (output, steps, tool_calls, _exit_reason) = run_agent_loop(
952            Arc::clone(provider),
953            model,
954            &system_prompt,
955            &prompt,
956            tool_definitions,
957            tool_registry,
958            15,  // max 15 steps for conflict resolution
959            120, // 2 min per-step timeout (resets on progress)
960            None,
961            String::new(),
962        )
963        .await?;
964
965        info!(
966            story_id = %story.id,
967            steps = steps,
968            tool_calls = tool_calls,
969            "Conflict resolver completed"
970        );
971
972        // Check if resolution was successful
973        let resolved = output.contains("CONFLICTS_RESOLVED")
974            || (output.contains("resolved") && !output.contains("UNRESOLVED"));
975
976        if resolved {
977            info!(story_id = %story.id, "Conflicts resolved successfully");
978        } else {
979            warn!(
980                story_id = %story.id,
981                output = %output.chars().take(200).collect::<String>(),
982                "Conflict resolution may have failed"
983            );
984        }
985
986        Ok(resolved)
987    }
988
989    /// Extract learnings (static version)
990    fn extract_learnings_static(response: &str) -> Vec<String> {
991        response
992            .lines()
993            .filter(|line| {
994                line.contains("learned") || line.contains("Learning") || line.contains("# What")
995            })
996            .map(|line| line.trim().to_string())
997            .collect()
998    }
999
1000    /// Commit changes in a specific directory
1001    fn commit_in_dir(dir: &PathBuf, story: &UserStory) -> anyhow::Result<()> {
1002        // Stage all changes
1003        let _ = Command::new("git")
1004            .args(["add", "-A"])
1005            .current_dir(dir)
1006            .output();
1007
1008        // Commit with story reference
1009        let msg = format!("feat({}): {}", story.id.to_lowercase(), story.title);
1010        let _ = Command::new("git")
1011            .args(["commit", "-m", &msg])
1012            .current_dir(dir)
1013            .output();
1014
1015        Ok(())
1016    }
1017
1018    /// Run quality gates in a specific directory with event emission
1019    async fn run_quality_gates_in_dir_with_events(
1020        &self,
1021        dir: &PathBuf,
1022        story_id: &str,
1023    ) -> anyhow::Result<bool> {
1024        let checks = &self.state.prd.quality_checks;
1025        let mut all_passed = true;
1026
1027        for (name, cmd) in [
1028            ("typecheck", &checks.typecheck),
1029            ("lint", &checks.lint),
1030            ("test", &checks.test),
1031            ("build", &checks.build),
1032        ] {
1033            if let Some(command) = cmd {
1034                debug!("Running {} check in {:?}: {}", name, dir, command);
1035                let output = Command::new("/bin/sh")
1036                    .arg("-c")
1037                    .arg(command)
1038                    .current_dir(dir)
1039                    .output()
1040                    .map_err(|e| {
1041                        anyhow::anyhow!("Failed to run quality check '{}': {}", name, e)
1042                    })?;
1043
1044                let passed = output.status.success();
1045                self.try_send_event(RalphEvent::StoryQualityCheck {
1046                    story_id: story_id.to_string(),
1047                    check_name: name.to_string(),
1048                    passed,
1049                });
1050
1051                if !passed {
1052                    let stderr = String::from_utf8_lossy(&output.stderr);
1053                    let stdout = String::from_utf8_lossy(&output.stdout);
1054                    let combined = format!("{}\n{}", stdout, stderr);
1055                    let error_summary: String = combined
1056                        .lines()
1057                        .filter(|line| {
1058                            line.starts_with("error")
1059                                || line.contains("error:")
1060                                || line.contains("error[")
1061                        })
1062                        .take(5)
1063                        .collect::<Vec<_>>()
1064                        .join("\n");
1065                    warn!(
1066                        check = %name,
1067                        dir = %dir.display(),
1068                        error_summary = %error_summary.chars().take(300).collect::<String>(),
1069                        "{} check failed in {:?}",
1070                        name,
1071                        dir
1072                    );
1073                    all_passed = false;
1074                }
1075            }
1076        }
1077
1078        Ok(all_passed)
1079    }
1080
1081    /// Build the prompt for a story
1082    fn build_prompt(&self, story: &UserStory) -> String {
1083        let progress = self.load_progress().unwrap_or_default();
1084
1085        format!(
1086            r#"# PRD: {} - {}
1087
1088## Current Story: {} - {}
1089
1090{}
1091
1092### Acceptance Criteria:
1093{}
1094
1095## Previous Progress:
1096{}
1097
1098## Instructions:
10991. Implement the requirements for this story
11002. Write any necessary code changes
11013. Document what you learned
11024. End with `STORY_COMPLETE: {}` when done
1103
1104Respond with the implementation and any shell commands needed.
1105"#,
1106            self.state.prd.project,
1107            self.state.prd.feature,
1108            story.id,
1109            story.title,
1110            story.description,
1111            story
1112                .acceptance_criteria
1113                .iter()
1114                .map(|c| format!("- {}", c))
1115                .collect::<Vec<_>>()
1116                .join("\n"),
1117            if progress.is_empty() {
1118                "None yet".to_string()
1119            } else {
1120                progress
1121            },
1122            story.id
1123        )
1124    }
1125
1126    /// Call the LLM with a prompt using agentic tool loop
1127    async fn call_llm(&self, story_id: &str, prompt: &str) -> anyhow::Result<String> {
1128        // Build system prompt with AGENTS.md
1129        let system_prompt = crate::agent::builtin::build_system_prompt(&self.state.working_dir);
1130
1131        // Create tool registry with provider for file operations
1132        let tool_registry =
1133            ToolRegistry::with_provider_arc(Arc::clone(&self.provider), self.model.clone());
1134
1135        // Filter out 'question' tool - sub-agents must be autonomous, not interactive
1136        let tool_definitions: Vec<_> = tool_registry
1137            .definitions()
1138            .into_iter()
1139            .filter(|t| t.name != "question")
1140            .collect();
1141
1142        info!(
1143            "Ralph agent starting with {} tools in {:?}",
1144            tool_definitions.len(),
1145            self.state.working_dir
1146        );
1147
1148        // Create event bridge if we have an event channel
1149        let (bridge_tx, _bridge_handle) = if let Some(ref ralph_tx) = self.event_tx {
1150            let (tx, handle) = Self::create_swarm_event_bridge(ralph_tx, story_id.to_string());
1151            (Some(tx), Some(handle))
1152        } else {
1153            (None, None)
1154        };
1155
1156        // Run the agentic loop with tools
1157        let (output, steps, tool_calls, _exit_reason) = run_agent_loop(
1158            Arc::clone(&self.provider),
1159            &self.model,
1160            &system_prompt,
1161            prompt,
1162            tool_definitions,
1163            tool_registry, // Already an Arc<ToolRegistry>
1164            30,            // max steps per story (focused implementation)
1165            180,           // 3 minute timeout per story
1166            bridge_tx,
1167            story_id.to_string(),
1168        )
1169        .await?;
1170
1171        info!(
1172            "Ralph agent completed: {} steps, {} tool calls",
1173            steps, tool_calls
1174        );
1175
1176        Ok(output)
1177    }
1178
1179    /// Run quality gates and emit events for each check
1180    async fn run_quality_gates_with_events(&self, story_id: &str) -> anyhow::Result<bool> {
1181        let checks = &self.state.prd.quality_checks;
1182        let mut all_passed = true;
1183
1184        for (name, cmd) in [
1185            ("typecheck", &checks.typecheck),
1186            ("lint", &checks.lint),
1187            ("test", &checks.test),
1188            ("build", &checks.build),
1189        ] {
1190            if let Some(command) = cmd {
1191                debug!("Running {} check: {}", name, command);
1192                let output = Command::new("/bin/sh")
1193                    .arg("-c")
1194                    .arg(command)
1195                    .current_dir(&self.state.working_dir)
1196                    .output()
1197                    .map_err(|e| {
1198                        anyhow::anyhow!("Failed to run quality check '{}': {}", name, e)
1199                    })?;
1200
1201                let passed = output.status.success();
1202                self.try_send_event(RalphEvent::StoryQualityCheck {
1203                    story_id: story_id.to_string(),
1204                    check_name: name.to_string(),
1205                    passed,
1206                });
1207
1208                if !passed {
1209                    let stderr = String::from_utf8_lossy(&output.stderr);
1210                    let stdout = String::from_utf8_lossy(&output.stdout);
1211                    let combined = format!("{}\n{}", stdout, stderr);
1212                    let error_summary: String = combined
1213                        .lines()
1214                        .filter(|line| {
1215                            line.starts_with("error")
1216                                || line.contains("error:")
1217                                || line.contains("error[")
1218                        })
1219                        .take(5)
1220                        .collect::<Vec<_>>()
1221                        .join("\n");
1222                    warn!(
1223                        check = %name,
1224                        error_summary = %error_summary.chars().take(300).collect::<String>(),
1225                        "{} check failed",
1226                        name
1227                    );
1228                    all_passed = false;
1229                    // Don't return early — run all checks so we can show all results
1230                }
1231            }
1232        }
1233
1234        Ok(all_passed)
1235    }
1236
1237    /// Commit changes for a story
1238    fn commit_story(&self, story: &UserStory) -> anyhow::Result<()> {
1239        info!("Committing changes for story: {}", story.id);
1240
1241        // Stage all changes
1242        let _ = Command::new("git")
1243            .args(["add", "-A"])
1244            .current_dir(&self.state.working_dir)
1245            .output();
1246
1247        // Commit with story reference
1248        let msg = format!("feat({}): {}", story.id.to_lowercase(), story.title);
1249        match Command::new("git")
1250            .args(["commit", "-m", &msg])
1251            .current_dir(&self.state.working_dir)
1252            .output()
1253        {
1254            Ok(output) if output.status.success() => {
1255                info!("Committed: {}", msg);
1256            }
1257            Ok(output) => {
1258                warn!(
1259                    "Git commit had no changes or failed: {}",
1260                    String::from_utf8_lossy(&output.stderr)
1261                );
1262            }
1263            Err(e) => {
1264                warn!("Could not run git commit: {}", e);
1265            }
1266        }
1267
1268        Ok(())
1269    }
1270
1271    /// Git checkout
1272    fn git_checkout(&self, branch: &str) -> anyhow::Result<()> {
1273        // Try to checkout, create if doesn't exist
1274        let output = Command::new("git")
1275            .args(["checkout", branch])
1276            .current_dir(&self.state.working_dir)
1277            .output()?;
1278
1279        if !output.status.success() {
1280            Command::new("git")
1281                .args(["checkout", "-b", branch])
1282                .current_dir(&self.state.working_dir)
1283                .output()?;
1284        }
1285
1286        Ok(())
1287    }
1288
1289    /// Load progress file
1290    fn load_progress(&self) -> anyhow::Result<String> {
1291        let path = self.state.working_dir.join(&self.config.progress_path);
1292        Ok(std::fs::read_to_string(path).unwrap_or_default())
1293    }
1294
1295    /// Append to progress file
1296    fn append_progress(&self, entry: &ProgressEntry, response: &str) -> anyhow::Result<()> {
1297        let path = self.state.working_dir.join(&self.config.progress_path);
1298        let mut content = self.load_progress().unwrap_or_default();
1299
1300        content.push_str(&format!(
1301            "\n---\n\n## Iteration {} - {} ({})\n\n**Status:** {}\n\n### Summary\n{}\n",
1302            entry.iteration, entry.story_id, entry.timestamp, entry.status, response
1303        ));
1304
1305        std::fs::write(path, content)?;
1306        Ok(())
1307    }
1308
1309    /// Extract learnings from response
1310    fn extract_learnings(&self, response: &str) -> Vec<String> {
1311        let mut learnings = Vec::new();
1312
1313        for line in response.lines() {
1314            if line.contains("learned") || line.contains("Learning") || line.contains("# What") {
1315                learnings.push(line.trim().to_string());
1316            }
1317        }
1318
1319        learnings
1320    }
1321
1322    /// Get current status
1323    pub fn status(&self) -> &RalphState {
1324        &self.state
1325    }
1326
1327    /// Format status as markdown
1328    pub fn status_markdown(&self) -> String {
1329        let status = if self.state.prd.is_complete() {
1330            "# Ralph Complete!"
1331        } else {
1332            "# Ralph Status"
1333        };
1334
1335        let stories: Vec<String> = self
1336            .state
1337            .prd
1338            .user_stories
1339            .iter()
1340            .map(|s| {
1341                let check = if s.passes { "[x]" } else { "[ ]" };
1342                format!("- {} {}: {}", check, s.id, s.title)
1343            })
1344            .collect();
1345
1346        format!(
1347            "{}\n\n**Project:** {}\n**Feature:** {}\n**Progress:** {}/{} stories\n**Iterations:** {}/{}\n\n## Stories\n{}",
1348            status,
1349            self.state.prd.project,
1350            self.state.prd.feature,
1351            self.state.prd.passed_count(),
1352            self.state.prd.user_stories.len(),
1353            self.state.current_iteration,
1354            self.state.max_iterations,
1355            stories.join("\n")
1356        )
1357    }
1358}
1359
1360/// Create a sample PRD template
1361pub fn create_prd_template(project: &str, feature: &str) -> Prd {
1362    Prd {
1363        project: project.to_string(),
1364        feature: feature.to_string(),
1365        branch_name: format!("feature/{}", feature.to_lowercase().replace(' ', "-")),
1366        version: "1.0".to_string(),
1367        user_stories: vec![UserStory {
1368            id: "US-001".to_string(),
1369            title: "First user story".to_string(),
1370            description: "Description of what needs to be implemented".to_string(),
1371            acceptance_criteria: vec!["Criterion 1".to_string(), "Criterion 2".to_string()],
1372            passes: false,
1373            priority: 1,
1374            depends_on: Vec::new(),
1375            complexity: 3,
1376        }],
1377        technical_requirements: Vec::new(),
1378        quality_checks: QualityChecks {
1379            typecheck: Some("cargo check".to_string()),
1380            test: Some("cargo test".to_string()),
1381            lint: Some("cargo clippy".to_string()),
1382            build: Some("cargo build".to_string()),
1383        },
1384        created_at: chrono::Utc::now().to_rfc3339(),
1385        updated_at: chrono::Utc::now().to_rfc3339(),
1386    }
1387}