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