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, .. } => 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, };
121 if ralph_tx.send(ralph_event).await.is_err() {
122 break;
123 }
124 }
125 });
126 (swarm_tx, handle)
127 }
128
129 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 pub async fn run(&mut self) -> anyhow::Result<RalphState> {
157 self.state.status = RalphStatus::Running;
158
159 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 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 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 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 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 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 self.try_send_event(RalphEvent::IterationStarted {
229 iteration: self.state.current_iteration,
230 max_iterations: self.state.max_iterations,
231 });
232
233 if self.state.prd.is_complete() {
235 info!("All stories complete!");
236 self.state.status = RalphStatus::Completed;
237 break;
238 }
239
240 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 self.try_send_event(RalphEvent::StoryStarted {
253 story_id: story.id.clone(),
254 });
255
256 let prompt = self.build_prompt(&story);
258
259 match self.call_llm(&story.id, &prompt).await {
261 Ok(response) => {
262 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 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 if self.config.auto_commit {
287 self.commit_story(&story)?;
288 }
289
290 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 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 async fn run_parallel(&mut self) -> anyhow::Result<()> {
335 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 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 let stories: Vec<UserStory> = stage_stories;
390
391 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 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 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 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 let prompt = Self::build_story_prompt(&story, &prd_info, &story_working_dir);
467
468 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 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 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 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 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 if let Some(ref wt) = worktree_info {
548 let _ = Self::commit_in_dir(&wt.path, &story);
549 }
550
551 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 let _ = mgr.cleanup(wt);
573 } else if !merge_result.conflicts.is_empty() {
574 info!(
576 story_id = %story.id,
577 num_conflicts = merge_result.conflicts.len(),
578 "Spawning conflict resolver sub-agent"
579 );
580
581 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 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 let _ = mgr.cleanup(wt);
647 } else if merge_result.aborted {
648 warn!(
650 story_id = %story.id,
651 summary = %merge_result.summary,
652 "Merge was aborted due to non-conflict failure"
653 );
654 let _ = mgr.cleanup(wt);
656 } else {
657 warn!(
659 story_id = %story.id,
660 summary = %merge_result.summary,
661 "Merge failed but not aborted - manual intervention may be needed"
662 );
663 }
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 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 if let (Some(wt), Some(mgr)) = (&worktree_info, &worktree_mgr) {
690 let _ = mgr.cleanup(wt);
691 }
692 }
693 } else {
694 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 self.state.prd.save(&self.state.prd_path).await?;
716 }
717
718 Ok(())
719 }
720
721 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 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 let system_prompt = crate::agent::builtin::build_system_prompt(working_dir);
819
820 let tool_registry =
822 ToolRegistry::with_provider_arc(Arc::clone(provider), model.to_string());
823
824 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 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, 30, 180, 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 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 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 let system_prompt = crate::agent::builtin::build_system_prompt(working_dir);
934
935 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 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, 120, 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 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 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 fn commit_in_dir(dir: &PathBuf, story: &UserStory) -> anyhow::Result<()> {
1002 let _ = Command::new("git")
1004 .args(["add", "-A"])
1005 .current_dir(dir)
1006 .output();
1007
1008 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 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 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 async fn call_llm(&self, story_id: &str, prompt: &str) -> anyhow::Result<String> {
1128 let system_prompt = crate::agent::builtin::build_system_prompt(&self.state.working_dir);
1130
1131 let tool_registry =
1133 ToolRegistry::with_provider_arc(Arc::clone(&self.provider), self.model.clone());
1134
1135 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 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 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, 30, 180, 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 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 }
1231 }
1232 }
1233
1234 Ok(all_passed)
1235 }
1236
1237 fn commit_story(&self, story: &UserStory) -> anyhow::Result<()> {
1239 info!("Committing changes for story: {}", story.id);
1240
1241 let _ = Command::new("git")
1243 .args(["add", "-A"])
1244 .current_dir(&self.state.working_dir)
1245 .output();
1246
1247 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 fn git_checkout(&self, branch: &str) -> anyhow::Result<()> {
1273 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 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 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 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 pub fn status(&self) -> &RalphState {
1324 &self.state
1325 }
1326
1327 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
1360pub 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}