1pub mod config_init;
20pub mod context;
21pub mod detection;
22pub mod finalization;
23pub mod plumbing;
24pub mod resume;
25pub mod validation;
26
27use crate::agents::AgentRegistry;
28use crate::app::finalization::finalize_pipeline;
29use crate::app::resume::{phase_rank, should_run_from};
30use crate::banner::print_welcome_banner;
31use crate::checkpoint::{save_checkpoint, PipelineCheckpoint, PipelinePhase};
32use crate::cli::{
33 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
34 handle_list_available_agents, handle_list_providers, handle_template_commands,
35 prompt_template_selection, Args,
36};
37use crate::common::utils;
38use crate::files::protection::monitoring::PromptMonitor;
39use crate::files::{
40 create_prompt_backup, ensure_files, make_prompt_read_only, reset_context_for_isolation,
41 update_status, validate_prompt_md,
42};
43use crate::git_helpers::{
44 abort_rebase, cleanup_orphaned_marker, continue_rebase, get_conflicted_files,
45 get_default_branch, get_repo_root, is_main_or_master_branch, rebase_onto, require_git_repo,
46 reset_start_commit, save_start_commit, start_agent_phase, RebaseResult,
47};
48use crate::logger::Colors;
49use crate::logger::Logger;
50use crate::phases::{run_development_phase, run_review_phase, PhaseContext};
51use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
52use crate::prompts::template_context::TemplateContext;
53use std::env;
54use std::process::Command;
55
56use config_init::initialize_config;
57use context::PipelineContext;
58use detection::detect_project_stack;
59use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
60use resume::handle_resume;
61use validation::{
62 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
63};
64
65pub fn run(args: Args) -> anyhow::Result<()> {
84 let colors = Colors::new();
85 let logger = Logger::new(colors);
86
87 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
89 return Ok(()); };
91
92 let config_init::ConfigInitResult {
93 config,
94 registry,
95 config_path,
96 config_sources,
97 } = init_result;
98
99 let validated = resolve_required_agents(&config)?;
101 let developer_agent = validated.developer_agent;
102 let reviewer_agent = validated.reviewer_agent;
103
104 if handle_listing_commands(&args, ®istry, colors) {
106 return Ok(());
107 }
108
109 if args.recovery.diagnose {
111 handle_diagnose(colors, &config, ®istry, &config_path, &config_sources);
112 return Ok(());
113 }
114
115 validate_agent_chains(®istry, colors);
117
118 if handle_plumbing_commands(&args, &logger, colors)? {
120 return Ok(());
121 }
122
123 let Some(repo_root) = validate_and_setup_agents(
125 &config,
126 ®istry,
127 &developer_agent,
128 &reviewer_agent,
129 &config_path,
130 colors,
131 &logger,
132 )?
133 else {
134 return Ok(());
135 };
136
137 if args.rebase_flags.rebase_only {
139 let template_context =
140 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
141 return handle_rebase_only(&args, &config, &template_context, &logger, colors);
142 }
143
144 (prepare_pipeline_or_exit(PipelinePreparationParams {
146 args,
147 config,
148 registry,
149 developer_agent,
150 reviewer_agent,
151 repo_root,
152 logger,
153 colors,
154 })?)
155 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
156}
157
158fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
162 if args.agent_list.list_agents {
163 handle_list_agents(registry);
164 return true;
165 }
166 if args.agent_list.list_available_agents {
167 handle_list_available_agents(registry);
168 return true;
169 }
170 if args.provider_list.list_providers {
171 handle_list_providers(colors);
172 return true;
173 }
174
175 let template_cmds = &args.template_commands;
177 if template_cmds.init_templates_enabled()
178 || template_cmds.validate
179 || template_cmds.show.is_some()
180 || template_cmds.list
181 || template_cmds.variables.is_some()
182 || template_cmds.render.is_some()
183 {
184 let _ = handle_template_commands(template_cmds, colors);
185 return true;
186 }
187
188 false
189}
190
191fn handle_plumbing_commands(args: &Args, logger: &Logger, colors: Colors) -> anyhow::Result<bool> {
196 if args.commit_display.show_commit_msg {
198 return handle_show_commit_msg().map(|()| true);
199 }
200
201 if args.commit_plumbing.apply_commit {
203 return handle_apply_commit(logger, colors).map(|()| true);
204 }
205
206 if args.commit_display.reset_start_commit {
208 require_git_repo()?;
209 let repo_root = get_repo_root()?;
210 env::set_current_dir(&repo_root)?;
211
212 return match reset_start_commit() {
213 Ok(()) => {
214 logger.success("Starting commit reference reset to current HEAD");
215 logger.info(".agent/start_commit has been updated");
216 Ok(true)
217 }
218 Err(e) => {
219 logger.error(&format!("Failed to reset starting commit: {e}"));
220 anyhow::bail!("Failed to reset starting commit");
221 }
222 };
223 }
224
225 Ok(false)
226}
227
228struct PipelinePreparationParams {
232 args: Args,
233 config: crate::config::Config,
234 registry: AgentRegistry,
235 developer_agent: String,
236 reviewer_agent: String,
237 repo_root: std::path::PathBuf,
238 logger: Logger,
239 colors: Colors,
240}
241
242fn prepare_pipeline_or_exit(
246 params: PipelinePreparationParams,
247) -> anyhow::Result<Option<PipelineContext>> {
248 let PipelinePreparationParams {
249 args,
250 config,
251 registry,
252 developer_agent,
253 reviewer_agent,
254 repo_root,
255 mut logger,
256 colors,
257 } = params;
258
259 ensure_files(config.isolation_mode)?;
260
261 if config.isolation_mode {
263 reset_context_for_isolation(&logger)?;
264 }
265
266 logger = logger.with_log_file(".agent/logs/pipeline.log");
267
268 if args.recovery.dry_run {
270 let developer_display = registry.display_name(&developer_agent);
271 let reviewer_display = registry.display_name(&reviewer_agent);
272 handle_dry_run(
273 &logger,
274 colors,
275 &config,
276 &developer_display,
277 &reviewer_display,
278 &repo_root,
279 )?;
280 return Ok(None);
281 }
282
283 let template_context =
285 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
286
287 if args.commit_plumbing.generate_commit_msg {
289 handle_generate_commit_msg(
290 &config,
291 &template_context,
292 ®istry,
293 &logger,
294 colors,
295 &developer_agent,
296 &reviewer_agent,
297 )?;
298 return Ok(None);
299 }
300
301 let developer_display = registry.display_name(&developer_agent);
303 let reviewer_display = registry.display_name(&reviewer_agent);
304
305 let ctx = PipelineContext {
307 args,
308 config,
309 registry,
310 developer_agent,
311 reviewer_agent,
312 developer_display,
313 reviewer_display,
314 repo_root,
315 logger,
316 colors,
317 template_context,
318 };
319 Ok(Some(ctx))
320}
321
322fn validate_and_setup_agents(
327 config: &crate::config::Config,
328 registry: &AgentRegistry,
329 developer_agent: &str,
330 reviewer_agent: &str,
331 config_path: &std::path::Path,
332 colors: Colors,
333 logger: &Logger,
334) -> anyhow::Result<Option<std::path::PathBuf>> {
335 validate_agent_commands(
337 config,
338 registry,
339 developer_agent,
340 reviewer_agent,
341 config_path,
342 )?;
343
344 validate_can_commit(
346 config,
347 registry,
348 developer_agent,
349 reviewer_agent,
350 config_path,
351 )?;
352
353 require_git_repo()?;
355 let repo_root = get_repo_root()?;
356 env::set_current_dir(&repo_root)?;
357
358 let should_continue = setup_git_and_prompt_file(config, colors, logger)?;
360 if should_continue.is_none() {
361 return Ok(None);
362 }
363
364 Ok(Some(repo_root))
365}
366
367fn setup_git_and_prompt_file(
372 config: &crate::config::Config,
373 colors: Colors,
374 logger: &Logger,
375) -> anyhow::Result<Option<()>> {
376 let prompt_exists = std::path::Path::new("PROMPT.md").exists();
377
378 if config.behavior.interactive && !prompt_exists {
381 if let Some(template_name) = prompt_template_selection(colors) {
382 create_prompt_from_template(&template_name, colors)?;
383 println!();
384 logger.info(
385 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
386 );
387 logger.info(&format!(
388 "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
389 config.commit_msg
390 ));
391 return Ok(None);
392 }
393 println!();
394 logger.error("PROMPT.md not found in current directory.");
395 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
396 println!();
397 logger.info("To get started:");
398 logger.info(" ralph --init # Smart setup wizard");
399 logger.info(" ralph --init bug-fix # Create from template");
400 logger.info(" ralph --list-templates # See all templates");
401 println!();
402 return Ok(None);
403 }
404
405 if !prompt_exists {
407 logger.error("PROMPT.md not found in current directory.");
408 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
409 println!();
410 logger.info("Quick start:");
411 logger.info(" ralph --init # Smart setup wizard");
412 logger.info(" ralph --init bug-fix # Create from template");
413 logger.info(" ralph --list-templates # See all templates");
414 println!();
415 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
416 println!();
417 return Ok(None);
418 }
419
420 Ok(Some(()))
421}
422
423fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
425 let resume_checkpoint = handle_resume(
427 &ctx.args,
428 &ctx.logger,
429 &ctx.developer_display,
430 &ctx.reviewer_display,
431 );
432
433 let mut git_helpers = crate::git_helpers::GitHelpers::new();
435 cleanup_orphaned_marker(&ctx.logger)?;
436 start_agent_phase(&mut git_helpers)?;
437 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
438
439 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
441 print_pipeline_info(ctx);
442 validate_prompt_and_setup_backup(ctx)?;
443
444 let mut prompt_monitor = setup_prompt_monitor(ctx);
446
447 let (_project_stack, review_guidelines) =
449 detect_project_stack(&ctx.config, &ctx.repo_root, &ctx.logger, ctx.colors);
450
451 print_review_guidelines(ctx, review_guidelines.as_ref());
452 println!();
453
454 let (mut timer, mut stats) = (Timer::new(), Stats::new());
456 let mut phase_ctx =
457 create_phase_context(ctx, &mut timer, &mut stats, review_guidelines.as_ref());
458 save_start_commit_or_warn(ctx);
459
460 run_initial_rebase(
462 &ctx.args,
463 &ctx.config,
464 &ctx.template_context,
465 &ctx.logger,
466 ctx.colors,
467 )?;
468
469 run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
471 check_prompt_restoration(ctx, &mut prompt_monitor, "development");
472 update_status("In progress.", ctx.config.isolation_mode)?;
473
474 run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
475 check_prompt_restoration(ctx, &mut prompt_monitor, "review");
476
477 run_post_review_rebase(
479 &ctx.args,
480 &ctx.config,
481 &ctx.template_context,
482 &ctx.logger,
483 ctx.colors,
484 )?;
485
486 update_status("In progress.", ctx.config.isolation_mode)?;
487
488 run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
489
490 finalize_pipeline(
492 &mut agent_phase_guard,
493 &ctx.logger,
494 ctx.colors,
495 &ctx.config,
496 &timer,
497 &stats,
498 prompt_monitor,
499 );
500 Ok(())
501}
502
503fn print_pipeline_info(ctx: &PipelineContext) {
505 ctx.logger.info(&format!(
506 "Working directory: {}{}{}",
507 ctx.colors.cyan(),
508 ctx.repo_root.display(),
509 ctx.colors.reset()
510 ));
511 ctx.logger.info(&format!(
512 "Commit message: {}{}{}",
513 ctx.colors.cyan(),
514 ctx.config.commit_msg,
515 ctx.colors.reset()
516 ));
517}
518
519fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
521 let prompt_validation =
522 validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
523 for err in &prompt_validation.errors {
524 ctx.logger.error(err);
525 }
526 for warn in &prompt_validation.warnings {
527 ctx.logger.warn(warn);
528 }
529 if !prompt_validation.is_valid() {
530 anyhow::bail!("PROMPT.md validation errors");
531 }
532
533 match create_prompt_backup() {
535 Ok(None) => {}
536 Ok(Some(warning)) => {
537 ctx.logger.warn(&format!(
538 "PROMPT.md backup created but: {warning}. Continuing anyway."
539 ));
540 }
541 Err(e) => {
542 ctx.logger.warn(&format!(
543 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
544 ));
545 }
546 }
547
548 match make_prompt_read_only() {
550 None => {}
551 Some(warning) => {
552 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
553 }
554 }
555
556 Ok(())
557}
558
559fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
561 match PromptMonitor::new() {
562 Ok(mut monitor) => {
563 if let Err(e) = monitor.start() {
564 ctx.logger.warn(&format!(
565 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
566 ));
567 None
568 } else {
569 if ctx.config.verbosity.is_debug() {
570 ctx.logger.info("Started real-time PROMPT.md monitoring");
571 }
572 Some(monitor)
573 }
574 }
575 Err(e) => {
576 ctx.logger.warn(&format!(
577 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
578 ));
579 None
580 }
581 }
582}
583
584fn print_review_guidelines(
586 ctx: &PipelineContext,
587 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
588) {
589 if let Some(guidelines) = review_guidelines {
590 ctx.logger.info(&format!(
591 "Review guidelines: {}{}{}",
592 ctx.colors.dim(),
593 guidelines.summary(),
594 ctx.colors.reset()
595 ));
596 }
597}
598
599fn create_phase_context<'ctx>(
601 ctx: &'ctx PipelineContext,
602 timer: &'ctx mut Timer,
603 stats: &'ctx mut Stats,
604 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
605) -> PhaseContext<'ctx> {
606 PhaseContext {
607 config: &ctx.config,
608 registry: &ctx.registry,
609 logger: &ctx.logger,
610 colors: &ctx.colors,
611 timer,
612 stats,
613 developer_agent: &ctx.developer_agent,
614 reviewer_agent: &ctx.reviewer_agent,
615 review_guidelines,
616 template_context: &ctx.template_context,
617 }
618}
619
620fn save_start_commit_or_warn(ctx: &PipelineContext) {
622 match save_start_commit() {
623 Ok(()) => {
624 if ctx.config.verbosity.is_debug() {
625 ctx.logger
626 .info("Saved starting commit for incremental diff generation");
627 }
628 }
629 Err(e) => {
630 ctx.logger.warn(&format!(
631 "Failed to save starting commit: {e}. \
632 Incremental diffs may be unavailable as a result."
633 ));
634 ctx.logger.info(
635 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
636 );
637 }
638 }
639}
640
641fn check_prompt_restoration(
643 ctx: &PipelineContext,
644 prompt_monitor: &mut Option<PromptMonitor>,
645 phase: &str,
646) {
647 if let Some(ref mut monitor) = prompt_monitor {
648 if monitor.check_and_restore() {
649 ctx.logger.warn(&format!(
650 "PROMPT.md was deleted and restored during {phase} phase"
651 ));
652 }
653 }
654}
655
656fn run_development(
658 ctx: &mut PhaseContext,
659 args: &Args,
660 resume_checkpoint: Option<&PipelineCheckpoint>,
661) -> anyhow::Result<()> {
662 ctx.logger
663 .header("PHASE 1: Development", crate::logger::Colors::blue);
664
665 let resume_phase = resume_checkpoint.map(|c| c.phase);
666 let resume_rank = resume_phase.map(phase_rank);
667
668 if resume_rank.is_some_and(|rank| rank >= phase_rank(PipelinePhase::Review)) {
669 ctx.logger
670 .info("Skipping development phase (checkpoint indicates it already completed)");
671 return Ok(());
672 }
673
674 if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
675 ctx.logger
676 .info("Skipping development phase (resuming from a later checkpoint phase)");
677 return Ok(());
678 }
679
680 let start_iter = match resume_phase {
681 Some(PipelinePhase::Planning | PipelinePhase::Development) => resume_checkpoint
682 .map_or(1, |c| c.iteration)
683 .clamp(1, ctx.config.developer_iters),
684 _ => 1,
685 };
686
687 let resuming_from_development =
688 args.recovery.resume && resume_phase == Some(PipelinePhase::Development);
689 let development_result = run_development_phase(ctx, start_iter, resuming_from_development)?;
690
691 if development_result.had_errors {
692 ctx.logger
693 .warn("Development phase completed with non-fatal errors");
694 }
695
696 Ok(())
697}
698
699fn run_review_and_fix(
701 ctx: &mut PhaseContext,
702 _args: &Args,
703 resume_checkpoint: Option<&PipelineCheckpoint>,
704) -> anyhow::Result<()> {
705 ctx.logger
706 .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
707
708 let resume_phase = resume_checkpoint.map(|c| c.phase);
709
710 let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
712 || should_run_from(PipelinePhase::Fix, resume_checkpoint)
713 || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
714 || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
715
716 let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
717 || resume_phase == Some(PipelinePhase::Fix)
718 || resume_phase == Some(PipelinePhase::ReviewAgain);
719
720 if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
721 let start_pass = match resume_phase {
722 Some(PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain) => {
723 resume_checkpoint
724 .map_or(1, |c| c.reviewer_pass)
725 .clamp(1, ctx.config.reviewer_reviews.max(1))
726 }
727 _ => 1,
728 };
729
730 let review_result = run_review_phase(ctx, start_pass)?;
731 if review_result.completed_early {
732 ctx.logger
733 .success("Review phase completed early (no issues found)");
734 }
735 } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
736 ctx.logger
737 .info("Skipping review phase (reviewer_reviews=0)");
738 } else if run_any_reviewer_phase {
739 ctx.logger
740 .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
741 }
742
743 Ok(())
747}
748
749fn run_final_validation(
751 ctx: &PhaseContext,
752 resume_checkpoint: Option<&PipelineCheckpoint>,
753) -> anyhow::Result<()> {
754 let Some(ref full_cmd) = ctx.config.full_check_cmd else {
755 return Ok(());
756 };
757
758 if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
759 ctx.logger
760 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
761 ctx.logger
762 .info("Skipping final validation (resuming from a later checkpoint phase)");
763 return Ok(());
764 }
765
766 let argv = utils::split_command(full_cmd)
767 .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
768 if argv.is_empty() {
769 ctx.logger
770 .warn("FULL_CHECK_CMD is empty; skipping final validation");
771 return Ok(());
772 }
773
774 if ctx.config.features.checkpoint_enabled {
775 let _ = save_checkpoint(&PipelineCheckpoint::new(
776 PipelinePhase::FinalValidation,
777 ctx.config.developer_iters,
778 ctx.config.developer_iters,
779 ctx.config.reviewer_reviews,
780 ctx.config.reviewer_reviews,
781 ctx.developer_agent,
782 ctx.reviewer_agent,
783 ));
784 }
785
786 ctx.logger
787 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
788 let display_cmd = utils::format_argv_for_log(&argv);
789 ctx.logger.info(&format!(
790 "Running full check: {}{}{}",
791 ctx.colors.dim(),
792 display_cmd,
793 ctx.colors.reset()
794 ));
795
796 let Some((program, arguments)) = argv.split_first() else {
797 ctx.logger
798 .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
799 return Ok(());
800 };
801 let status = Command::new(program).args(arguments).status()?;
802
803 if status.success() {
804 ctx.logger.success("Full check passed");
805 } else {
806 ctx.logger.error("Full check failed");
807 anyhow::bail!("Full check failed");
808 }
809
810 Ok(())
811}
812
813fn handle_rebase_only(
818 _args: &Args,
819 config: &crate::config::Config,
820 template_context: &TemplateContext,
821 logger: &Logger,
822 colors: Colors,
823) -> anyhow::Result<()> {
824 if is_main_or_master_branch()? {
826 logger.warn("Already on main/master branch - rebasing on main is not recommended");
827 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
828 logger.info(" git worktree add ../feature-branch feature-branch");
829 logger.info("This allows multiple AI agents to work on different features simultaneously.");
830 logger.info("Proceeding with rebase anyway as requested...");
831 }
832
833 logger.header("Rebase to default branch", Colors::cyan);
834
835 match run_rebase_to_default(logger, colors) {
836 Ok(RebaseResult::Success) => {
837 logger.success("Rebase completed successfully");
838 Ok(())
839 }
840 Ok(RebaseResult::NoOp) => {
841 logger.info("No rebase needed (already up-to-date)");
842 Ok(())
843 }
844 Ok(RebaseResult::Conflicts(_conflicts)) => {
845 let conflicted_files = get_conflicted_files()?;
847 if conflicted_files.is_empty() {
848 logger.warn("Rebase reported conflicts but no conflicted files found");
849 let _ = abort_rebase();
850 return Ok(());
851 }
852
853 logger.warn(&format!(
854 "Rebase resulted in {} conflict(s), attempting AI resolution",
855 conflicted_files.len()
856 ));
857
858 match try_resolve_conflicts_with_fallback(
860 &conflicted_files,
861 config,
862 template_context,
863 logger,
864 colors,
865 ) {
866 Ok(true) => {
867 logger.info("Continuing rebase after conflict resolution");
869 match continue_rebase() {
870 Ok(()) => {
871 logger.success("Rebase completed successfully after AI resolution");
872 Ok(())
873 }
874 Err(e) => {
875 logger.error(&format!("Failed to continue rebase: {e}"));
876 let _ = abort_rebase();
877 anyhow::bail!("Rebase failed after conflict resolution")
878 }
879 }
880 }
881 Ok(false) => {
882 logger.error("AI conflict resolution failed, aborting rebase");
884 let _ = abort_rebase();
885 anyhow::bail!("Rebase conflicts could not be resolved by AI")
886 }
887 Err(e) => {
888 logger.error(&format!("Conflict resolution error: {e}"));
889 let _ = abort_rebase();
890 anyhow::bail!("Rebase conflict resolution failed: {e}")
891 }
892 }
893 }
894 Err(e) => {
895 logger.error(&format!("Rebase failed: {e}"));
896 anyhow::bail!("Rebase failed: {e}")
897 }
898 }
899}
900
901fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
914 let default_branch = get_default_branch()?;
916 logger.info(&format!(
917 "Rebasing onto {}{}{}",
918 colors.cyan(),
919 default_branch,
920 colors.reset()
921 ));
922
923 rebase_onto(&default_branch)
925}
926
927fn run_initial_rebase(
932 _args: &Args,
933 config: &crate::config::Config,
934 template_context: &TemplateContext,
935 logger: &Logger,
936 colors: Colors,
937) -> anyhow::Result<()> {
938 logger.header("Pre-development rebase", Colors::cyan);
939
940 match run_rebase_to_default(logger, colors) {
941 Ok(RebaseResult::Success) => {
942 logger.success("Rebase completed successfully");
943 Ok(())
944 }
945 Ok(RebaseResult::NoOp) => {
946 logger.info("No rebase needed (already up-to-date or on main branch)");
947 Ok(())
948 }
949 Ok(RebaseResult::Conflicts(_conflicts)) => {
950 let conflicted_files = get_conflicted_files()?;
952 if conflicted_files.is_empty() {
953 logger.warn("Rebase reported conflicts but no conflicted files found");
954 let _ = abort_rebase();
955 return Ok(());
956 }
957
958 logger.warn(&format!(
959 "Rebase resulted in {} conflict(s), attempting AI resolution",
960 conflicted_files.len()
961 ));
962
963 match try_resolve_conflicts_with_fallback(
965 &conflicted_files,
966 config,
967 template_context,
968 logger,
969 colors,
970 ) {
971 Ok(true) => {
972 logger.info("Continuing rebase after conflict resolution");
974 match continue_rebase() {
975 Ok(()) => {
976 logger.success("Rebase completed successfully after AI resolution");
977 Ok(())
978 }
979 Err(e) => {
980 logger.warn(&format!("Failed to continue rebase: {e}"));
981 let _ = abort_rebase();
982 Ok(()) }
984 }
985 }
986 Ok(false) => {
987 logger.warn("AI conflict resolution failed, aborting rebase");
989 let _ = abort_rebase();
990 Ok(()) }
992 Err(e) => {
993 logger.error(&format!("Conflict resolution error: {e}"));
994 let _ = abort_rebase();
995 Ok(()) }
997 }
998 }
999 Err(e) => {
1000 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1001 Ok(())
1002 }
1003 }
1004}
1005
1006fn run_post_review_rebase(
1011 _args: &Args,
1012 config: &crate::config::Config,
1013 template_context: &TemplateContext,
1014 logger: &Logger,
1015 colors: Colors,
1016) -> anyhow::Result<()> {
1017 logger.header("Post-review rebase", Colors::cyan);
1018
1019 match run_rebase_to_default(logger, colors) {
1020 Ok(RebaseResult::Success) => {
1021 logger.success("Rebase completed successfully");
1022 Ok(())
1023 }
1024 Ok(RebaseResult::NoOp) => {
1025 logger.info("No rebase needed (already up-to-date or on main branch)");
1026 Ok(())
1027 }
1028 Ok(RebaseResult::Conflicts(_conflicts)) => {
1029 let conflicted_files = get_conflicted_files()?;
1031 if conflicted_files.is_empty() {
1032 logger.warn("Rebase reported conflicts but no conflicted files found");
1033 let _ = abort_rebase();
1034 return Ok(());
1035 }
1036
1037 logger.warn(&format!(
1038 "Rebase resulted in {} conflict(s), attempting AI resolution",
1039 conflicted_files.len()
1040 ));
1041
1042 match try_resolve_conflicts_with_fallback(
1044 &conflicted_files,
1045 config,
1046 template_context,
1047 logger,
1048 colors,
1049 ) {
1050 Ok(true) => {
1051 logger.info("Continuing rebase after conflict resolution");
1053 match continue_rebase() {
1054 Ok(()) => {
1055 logger.success("Rebase completed successfully after AI resolution");
1056 Ok(())
1057 }
1058 Err(e) => {
1059 logger.warn(&format!("Failed to continue rebase: {e}"));
1060 let _ = abort_rebase();
1061 Ok(()) }
1063 }
1064 }
1065 Ok(false) => {
1066 logger.warn("AI conflict resolution failed, aborting rebase");
1068 let _ = abort_rebase();
1069 Ok(()) }
1071 Err(e) => {
1072 logger.error(&format!("Conflict resolution error: {e}"));
1073 let _ = abort_rebase();
1074 Ok(()) }
1076 }
1077 }
1078 Err(e) => {
1079 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1080 Ok(())
1081 }
1082 }
1083}
1084
1085enum ConflictResolutionResult {
1089 WithJson(String),
1091 FileEditsOnly,
1093 Failed,
1095}
1096
1097fn try_resolve_conflicts_with_fallback(
1102 conflicted_files: &[String],
1103 config: &crate::config::Config,
1104 template_context: &TemplateContext,
1105 logger: &Logger,
1106 colors: Colors,
1107) -> anyhow::Result<bool> {
1108 if conflicted_files.is_empty() {
1109 return Ok(false);
1110 }
1111
1112 logger.info(&format!(
1113 "Attempting AI conflict resolution for {} file(s)",
1114 conflicted_files.len()
1115 ));
1116
1117 let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
1118 let resolution_prompt = build_resolution_prompt(&conflicts, template_context);
1119
1120 match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1121 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1122 let resolved_files = parse_and_validate_resolved_files(&resolved_content, logger)?;
1124 write_resolved_files(&resolved_files, logger)?;
1125
1126 let remaining_conflicts = get_conflicted_files()?;
1128 if remaining_conflicts.is_empty() {
1129 Ok(true)
1130 } else {
1131 logger.warn(&format!(
1132 "{} conflicts remain after AI resolution",
1133 remaining_conflicts.len()
1134 ));
1135 Ok(false)
1136 }
1137 }
1138 Ok(ConflictResolutionResult::FileEditsOnly) => {
1139 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1141
1142 let remaining_conflicts = get_conflicted_files()?;
1144 if remaining_conflicts.is_empty() {
1145 logger.success("All conflicts resolved via file edits");
1146 Ok(true)
1147 } else {
1148 logger.warn(&format!(
1149 "{} conflicts remain after AI resolution",
1150 remaining_conflicts.len()
1151 ));
1152 Ok(false)
1153 }
1154 }
1155 Ok(ConflictResolutionResult::Failed) => {
1156 logger.warn("AI conflict resolution failed");
1157 logger.info("Attempting to continue rebase anyway...");
1158
1159 match crate::git_helpers::continue_rebase() {
1161 Ok(()) => {
1162 logger.info("Successfully continued rebase");
1163 Ok(true)
1164 }
1165 Err(rebase_err) => {
1166 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1167 Ok(false) }
1169 }
1170 }
1171 Err(e) => {
1172 logger.warn(&format!("AI conflict resolution failed: {e}"));
1173 logger.info("Attempting to continue rebase anyway...");
1174
1175 match crate::git_helpers::continue_rebase() {
1177 Ok(()) => {
1178 logger.info("Successfully continued rebase");
1179 Ok(true)
1180 }
1181 Err(rebase_err) => {
1182 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1183 Ok(false) }
1185 }
1186 }
1187 }
1188}
1189
1190fn collect_conflict_info_or_error(
1192 conflicted_files: &[String],
1193 logger: &Logger,
1194) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
1195 use crate::prompts::collect_conflict_info;
1196
1197 let conflicts = match collect_conflict_info(conflicted_files) {
1198 Ok(c) => c,
1199 Err(e) => {
1200 logger.error(&format!("Failed to collect conflict info: {e}"));
1201 anyhow::bail!("Failed to collect conflict info");
1202 }
1203 };
1204 Ok(conflicts)
1205}
1206
1207fn build_resolution_prompt(
1209 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1210 template_context: &TemplateContext,
1211) -> String {
1212 use crate::prompts::build_conflict_resolution_prompt_with_context;
1213 use std::fs;
1214
1215 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
1216 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
1217
1218 build_conflict_resolution_prompt_with_context(
1219 template_context,
1220 conflicts,
1221 prompt_md_content.as_deref(),
1222 plan_content.as_deref(),
1223 )
1224}
1225
1226fn run_ai_conflict_resolution(
1231 resolution_prompt: &str,
1232 config: &crate::config::Config,
1233 logger: &Logger,
1234 colors: Colors,
1235) -> anyhow::Result<ConflictResolutionResult> {
1236 use crate::agents::AgentRegistry;
1237 use crate::files::result_extraction::extract_last_result;
1238 use crate::pipeline::{
1239 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
1240 };
1241 use std::io;
1242 use std::path::Path;
1243
1244 let log_dir = ".agent/logs/rebase_conflict_resolution";
1248
1249 let registry = AgentRegistry::new()?;
1250 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
1251
1252 let mut runtime = PipelineRuntime {
1253 timer: &mut crate::pipeline::Timer::new(),
1254 logger,
1255 colors: &colors,
1256 config,
1257 };
1258
1259 let validate_output: OutputValidator = |log_dir_path: &Path,
1262 validation_logger: &crate::logger::Logger|
1263 -> io::Result<bool> {
1264 match extract_last_result(log_dir_path) {
1265 Ok(Some(_)) => {
1266 Ok(true)
1268 }
1269 Ok(None) => {
1270 match crate::git_helpers::get_conflicted_files() {
1273 Ok(conflicts) if conflicts.is_empty() => {
1274 validation_logger
1275 .info("Agent resolved conflicts without JSON output (file edits only)");
1276 Ok(true) }
1278 Ok(conflicts) => {
1279 validation_logger.warn(&format!(
1280 "{} conflict(s) remain unresolved",
1281 conflicts.len()
1282 ));
1283 Ok(false) }
1285 Err(e) => {
1286 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
1287 Ok(false) }
1289 }
1290 }
1291 Err(e) => {
1292 validation_logger.warn(&format!("Output validation check failed: {e}"));
1293 Ok(false) }
1295 }
1296 };
1297
1298 let mut fallback_config = FallbackConfig {
1299 role: crate::agents::AgentRole::Reviewer,
1300 base_label: "conflict resolution",
1301 prompt: resolution_prompt,
1302 logfile_prefix: log_dir,
1303 runtime: &mut runtime,
1304 registry: ®istry,
1305 primary_agent: reviewer_agent,
1306 output_validator: Some(validate_output),
1307 };
1308
1309 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
1310
1311 if exit_code != 0 {
1312 return Ok(ConflictResolutionResult::Failed);
1313 }
1314
1315 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
1318
1319 if remaining_conflicts.is_empty() {
1320 match extract_last_result(Path::new(log_dir)) {
1322 Ok(Some(content)) => {
1323 logger.info("Agent provided JSON output with resolved files");
1324 Ok(ConflictResolutionResult::WithJson(content))
1325 }
1326 Ok(None) => {
1327 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1328 Ok(ConflictResolutionResult::FileEditsOnly)
1329 }
1330 Err(e) => {
1331 logger.warn(&format!(
1333 "Failed to extract JSON output but conflicts are resolved: {e}"
1334 ));
1335 Ok(ConflictResolutionResult::FileEditsOnly)
1336 }
1337 }
1338 } else {
1339 logger.warn(&format!(
1340 "{} conflict(s) remain after agent attempted resolution",
1341 remaining_conflicts.len()
1342 ));
1343 Ok(ConflictResolutionResult::Failed)
1344 }
1345}
1346
1347fn parse_and_validate_resolved_files(
1349 resolved_content: &str,
1350 logger: &Logger,
1351) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
1352 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|e| {
1353 logger.error(&format!("Failed to parse agent output as JSON: {e}"));
1354 anyhow::anyhow!("Failed to parse agent output as JSON")
1355 })?;
1356
1357 let resolved_files = match json.get("resolved_files") {
1358 Some(v) if v.is_object() => v.as_object().unwrap(),
1359 _ => {
1360 logger.error("Agent output missing 'resolved_files' object");
1361 anyhow::bail!("Agent output missing 'resolved_files' object");
1362 }
1363 };
1364
1365 if resolved_files.is_empty() {
1366 logger.error("No files were resolved by the agent");
1367 anyhow::bail!("No files were resolved by the agent");
1368 }
1369
1370 Ok(resolved_files.clone())
1371}
1372
1373fn write_resolved_files(
1375 resolved_files: &serde_json::Map<String, serde_json::Value>,
1376 logger: &Logger,
1377) -> anyhow::Result<usize> {
1378 use std::fs;
1379
1380 let mut files_written = 0;
1381 for (path, content) in resolved_files {
1382 if let Some(content_str) = content.as_str() {
1383 fs::write(path, content_str).map_err(|e| {
1384 logger.error(&format!("Failed to write {path}: {e}"));
1385 anyhow::anyhow!("Failed to write {path}: {e}")
1386 })?;
1387 logger.info(&format!("Resolved and wrote: {path}"));
1388 files_written += 1;
1389 if let Err(e) = crate::git_helpers::git_add_all() {
1391 logger.warn(&format!("Failed to stage {path}: {e}"));
1392 }
1393 }
1394 }
1395
1396 logger.success(&format!("Successfully resolved {files_written} file(s)"));
1397 Ok(files_written)
1398}