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 if config.behavior.interactive && !std::path::Path::new("PROMPT.md").exists() {
379 if let Some(template_name) = prompt_template_selection(colors) {
380 create_prompt_from_template(&template_name, colors)?;
381 println!();
382 logger.info(
383 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
384 );
385 logger.info(&format!(
386 "Tip: Edit PROMPT.md, then run: ralph \"{}\"",
387 config.commit_msg
388 ));
389 return Ok(None);
390 }
391 println!();
392 logger.info("PROMPT.md is required to run the pipeline.");
393 logger.info(
394 "Create one with 'ralph --init-prompt <template>' (see: 'ralph --list-templates'), then rerun.",
395 );
396 return Ok(None);
397 }
398 Ok(Some(()))
399}
400
401fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
403 let resume_checkpoint = handle_resume(
405 &ctx.args,
406 &ctx.logger,
407 &ctx.developer_display,
408 &ctx.reviewer_display,
409 );
410
411 let mut git_helpers = crate::git_helpers::GitHelpers::new();
413 cleanup_orphaned_marker(&ctx.logger)?;
414 start_agent_phase(&mut git_helpers)?;
415 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
416
417 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
419 print_pipeline_info(ctx);
420 validate_prompt_and_setup_backup(ctx)?;
421
422 let mut prompt_monitor = setup_prompt_monitor(ctx);
424
425 let (_project_stack, review_guidelines) =
427 detect_project_stack(&ctx.config, &ctx.repo_root, &ctx.logger, ctx.colors);
428
429 print_review_guidelines(ctx, review_guidelines.as_ref());
430 println!();
431
432 let (mut timer, mut stats) = (Timer::new(), Stats::new());
434 let mut phase_ctx =
435 create_phase_context(ctx, &mut timer, &mut stats, review_guidelines.as_ref());
436 save_start_commit_or_warn(ctx);
437
438 run_initial_rebase(
440 &ctx.args,
441 &ctx.config,
442 &ctx.template_context,
443 &ctx.logger,
444 ctx.colors,
445 )?;
446
447 run_development(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
449 check_prompt_restoration(ctx, &mut prompt_monitor, "development");
450 update_status("In progress.", ctx.config.isolation_mode)?;
451
452 run_review_and_fix(&mut phase_ctx, &ctx.args, resume_checkpoint.as_ref())?;
453 check_prompt_restoration(ctx, &mut prompt_monitor, "review");
454
455 run_post_review_rebase(
457 &ctx.args,
458 &ctx.config,
459 &ctx.template_context,
460 &ctx.logger,
461 ctx.colors,
462 )?;
463
464 update_status("In progress.", ctx.config.isolation_mode)?;
465
466 run_final_validation(&phase_ctx, resume_checkpoint.as_ref())?;
467
468 finalize_pipeline(
470 &mut agent_phase_guard,
471 &ctx.logger,
472 ctx.colors,
473 &ctx.config,
474 &timer,
475 &stats,
476 prompt_monitor,
477 );
478 Ok(())
479}
480
481fn print_pipeline_info(ctx: &PipelineContext) {
483 ctx.logger.info(&format!(
484 "Working directory: {}{}{}",
485 ctx.colors.cyan(),
486 ctx.repo_root.display(),
487 ctx.colors.reset()
488 ));
489 ctx.logger.info(&format!(
490 "Commit message: {}{}{}",
491 ctx.colors.cyan(),
492 ctx.config.commit_msg,
493 ctx.colors.reset()
494 ));
495}
496
497fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
499 let prompt_validation =
500 validate_prompt_md(ctx.config.behavior.strict_validation, ctx.args.interactive);
501 for err in &prompt_validation.errors {
502 ctx.logger.error(err);
503 }
504 for warn in &prompt_validation.warnings {
505 ctx.logger.warn(warn);
506 }
507 if !prompt_validation.is_valid() {
508 anyhow::bail!("PROMPT.md validation errors");
509 }
510
511 match create_prompt_backup() {
513 Ok(None) => {}
514 Ok(Some(warning)) => {
515 ctx.logger.warn(&format!(
516 "PROMPT.md backup created but: {warning}. Continuing anyway."
517 ));
518 }
519 Err(e) => {
520 ctx.logger.warn(&format!(
521 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
522 ));
523 }
524 }
525
526 match make_prompt_read_only() {
528 None => {}
529 Some(warning) => {
530 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
531 }
532 }
533
534 Ok(())
535}
536
537fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
539 match PromptMonitor::new() {
540 Ok(mut monitor) => {
541 if let Err(e) = monitor.start() {
542 ctx.logger.warn(&format!(
543 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
544 ));
545 None
546 } else {
547 if ctx.config.verbosity.is_debug() {
548 ctx.logger.info("Started real-time PROMPT.md monitoring");
549 }
550 Some(monitor)
551 }
552 }
553 Err(e) => {
554 ctx.logger.warn(&format!(
555 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
556 ));
557 None
558 }
559 }
560}
561
562fn print_review_guidelines(
564 ctx: &PipelineContext,
565 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
566) {
567 if let Some(guidelines) = review_guidelines {
568 ctx.logger.info(&format!(
569 "Review guidelines: {}{}{}",
570 ctx.colors.dim(),
571 guidelines.summary(),
572 ctx.colors.reset()
573 ));
574 }
575}
576
577fn create_phase_context<'ctx>(
579 ctx: &'ctx PipelineContext,
580 timer: &'ctx mut Timer,
581 stats: &'ctx mut Stats,
582 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
583) -> PhaseContext<'ctx> {
584 PhaseContext {
585 config: &ctx.config,
586 registry: &ctx.registry,
587 logger: &ctx.logger,
588 colors: &ctx.colors,
589 timer,
590 stats,
591 developer_agent: &ctx.developer_agent,
592 reviewer_agent: &ctx.reviewer_agent,
593 review_guidelines,
594 template_context: &ctx.template_context,
595 }
596}
597
598fn save_start_commit_or_warn(ctx: &PipelineContext) {
600 match save_start_commit() {
601 Ok(()) => {
602 if ctx.config.verbosity.is_debug() {
603 ctx.logger
604 .info("Saved starting commit for incremental diff generation");
605 }
606 }
607 Err(e) => {
608 ctx.logger.warn(&format!(
609 "Failed to save starting commit: {e}. \
610 Incremental diffs may be unavailable as a result."
611 ));
612 ctx.logger.info(
613 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
614 );
615 }
616 }
617}
618
619fn check_prompt_restoration(
621 ctx: &PipelineContext,
622 prompt_monitor: &mut Option<PromptMonitor>,
623 phase: &str,
624) {
625 if let Some(ref mut monitor) = prompt_monitor {
626 if monitor.check_and_restore() {
627 ctx.logger.warn(&format!(
628 "PROMPT.md was deleted and restored during {phase} phase"
629 ));
630 }
631 }
632}
633
634fn run_development(
636 ctx: &mut PhaseContext,
637 args: &Args,
638 resume_checkpoint: Option<&PipelineCheckpoint>,
639) -> anyhow::Result<()> {
640 ctx.logger
641 .header("PHASE 1: Development", crate::logger::Colors::blue);
642
643 let resume_phase = resume_checkpoint.map(|c| c.phase);
644 let resume_rank = resume_phase.map(phase_rank);
645
646 if resume_rank.is_some_and(|rank| rank >= phase_rank(PipelinePhase::Review)) {
647 ctx.logger
648 .info("Skipping development phase (checkpoint indicates it already completed)");
649 return Ok(());
650 }
651
652 if !should_run_from(PipelinePhase::Planning, resume_checkpoint) {
653 ctx.logger
654 .info("Skipping development phase (resuming from a later checkpoint phase)");
655 return Ok(());
656 }
657
658 let start_iter = match resume_phase {
659 Some(PipelinePhase::Planning | PipelinePhase::Development) => resume_checkpoint
660 .map_or(1, |c| c.iteration)
661 .clamp(1, ctx.config.developer_iters),
662 _ => 1,
663 };
664
665 let resuming_from_development =
666 args.recovery.resume && resume_phase == Some(PipelinePhase::Development);
667 let development_result = run_development_phase(ctx, start_iter, resuming_from_development)?;
668
669 if development_result.had_errors {
670 ctx.logger
671 .warn("Development phase completed with non-fatal errors");
672 }
673
674 Ok(())
675}
676
677fn run_review_and_fix(
679 ctx: &mut PhaseContext,
680 _args: &Args,
681 resume_checkpoint: Option<&PipelineCheckpoint>,
682) -> anyhow::Result<()> {
683 ctx.logger
684 .header("PHASE 2: Review & Fix", crate::logger::Colors::magenta);
685
686 let resume_phase = resume_checkpoint.map(|c| c.phase);
687
688 let run_any_reviewer_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
690 || should_run_from(PipelinePhase::Fix, resume_checkpoint)
691 || should_run_from(PipelinePhase::ReviewAgain, resume_checkpoint)
692 || should_run_from(PipelinePhase::CommitMessage, resume_checkpoint);
693
694 let should_run_review_phase = should_run_from(PipelinePhase::Review, resume_checkpoint)
695 || resume_phase == Some(PipelinePhase::Fix)
696 || resume_phase == Some(PipelinePhase::ReviewAgain);
697
698 if should_run_review_phase && ctx.config.reviewer_reviews > 0 {
699 let start_pass = match resume_phase {
700 Some(PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain) => {
701 resume_checkpoint
702 .map_or(1, |c| c.reviewer_pass)
703 .clamp(1, ctx.config.reviewer_reviews.max(1))
704 }
705 _ => 1,
706 };
707
708 let review_result = run_review_phase(ctx, start_pass)?;
709 if review_result.completed_early {
710 ctx.logger
711 .success("Review phase completed early (no issues found)");
712 }
713 } else if run_any_reviewer_phase && ctx.config.reviewer_reviews == 0 {
714 ctx.logger
715 .info("Skipping review phase (reviewer_reviews=0)");
716 } else if run_any_reviewer_phase {
717 ctx.logger
718 .info("Skipping review-fix cycles (resuming from a later checkpoint phase)");
719 }
720
721 Ok(())
725}
726
727fn run_final_validation(
729 ctx: &PhaseContext,
730 resume_checkpoint: Option<&PipelineCheckpoint>,
731) -> anyhow::Result<()> {
732 let Some(ref full_cmd) = ctx.config.full_check_cmd else {
733 return Ok(());
734 };
735
736 if !should_run_from(PipelinePhase::FinalValidation, resume_checkpoint) {
737 ctx.logger
738 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
739 ctx.logger
740 .info("Skipping final validation (resuming from a later checkpoint phase)");
741 return Ok(());
742 }
743
744 let argv = utils::split_command(full_cmd)
745 .map_err(|e| anyhow::anyhow!("FULL_CHECK_CMD parse error: {e}"))?;
746 if argv.is_empty() {
747 ctx.logger
748 .warn("FULL_CHECK_CMD is empty; skipping final validation");
749 return Ok(());
750 }
751
752 if ctx.config.features.checkpoint_enabled {
753 let _ = save_checkpoint(&PipelineCheckpoint::new(
754 PipelinePhase::FinalValidation,
755 ctx.config.developer_iters,
756 ctx.config.developer_iters,
757 ctx.config.reviewer_reviews,
758 ctx.config.reviewer_reviews,
759 ctx.developer_agent,
760 ctx.reviewer_agent,
761 ));
762 }
763
764 ctx.logger
765 .header("PHASE 3: Final Validation", crate::logger::Colors::yellow);
766 let display_cmd = utils::format_argv_for_log(&argv);
767 ctx.logger.info(&format!(
768 "Running full check: {}{}{}",
769 ctx.colors.dim(),
770 display_cmd,
771 ctx.colors.reset()
772 ));
773
774 let Some((program, arguments)) = argv.split_first() else {
775 ctx.logger
776 .error("FULL_CHECK_CMD is empty after parsing; skipping final validation");
777 return Ok(());
778 };
779 let status = Command::new(program).args(arguments).status()?;
780
781 if status.success() {
782 ctx.logger.success("Full check passed");
783 } else {
784 ctx.logger.error("Full check failed");
785 anyhow::bail!("Full check failed");
786 }
787
788 Ok(())
789}
790
791fn handle_rebase_only(
796 _args: &Args,
797 config: &crate::config::Config,
798 template_context: &TemplateContext,
799 logger: &Logger,
800 colors: Colors,
801) -> anyhow::Result<()> {
802 if is_main_or_master_branch()? {
804 logger.warn("Already on main/master branch - rebasing on main is not recommended");
805 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
806 logger.info(" git worktree add ../feature-branch feature-branch");
807 logger.info("This allows multiple AI agents to work on different features simultaneously.");
808 logger.info("Proceeding with rebase anyway as requested...");
809 }
810
811 logger.header("Rebase to default branch", Colors::cyan);
812
813 match run_rebase_to_default(logger, colors) {
814 Ok(RebaseResult::Success) => {
815 logger.success("Rebase completed successfully");
816 Ok(())
817 }
818 Ok(RebaseResult::NoOp) => {
819 logger.info("No rebase needed (already up-to-date)");
820 Ok(())
821 }
822 Ok(RebaseResult::Conflicts(_conflicts)) => {
823 let conflicted_files = get_conflicted_files()?;
825 if conflicted_files.is_empty() {
826 logger.warn("Rebase reported conflicts but no conflicted files found");
827 let _ = abort_rebase();
828 return Ok(());
829 }
830
831 logger.warn(&format!(
832 "Rebase resulted in {} conflict(s), attempting AI resolution",
833 conflicted_files.len()
834 ));
835
836 match try_resolve_conflicts_with_fallback(
838 &conflicted_files,
839 config,
840 template_context,
841 logger,
842 colors,
843 ) {
844 Ok(true) => {
845 logger.info("Continuing rebase after conflict resolution");
847 match continue_rebase() {
848 Ok(()) => {
849 logger.success("Rebase completed successfully after AI resolution");
850 Ok(())
851 }
852 Err(e) => {
853 logger.error(&format!("Failed to continue rebase: {e}"));
854 let _ = abort_rebase();
855 anyhow::bail!("Rebase failed after conflict resolution")
856 }
857 }
858 }
859 Ok(false) => {
860 logger.error("AI conflict resolution failed, aborting rebase");
862 let _ = abort_rebase();
863 anyhow::bail!("Rebase conflicts could not be resolved by AI")
864 }
865 Err(e) => {
866 logger.error(&format!("Conflict resolution error: {e}"));
867 let _ = abort_rebase();
868 anyhow::bail!("Rebase conflict resolution failed: {e}")
869 }
870 }
871 }
872 Err(e) => {
873 logger.error(&format!("Rebase failed: {e}"));
874 anyhow::bail!("Rebase failed: {e}")
875 }
876 }
877}
878
879fn run_rebase_to_default(logger: &Logger, colors: Colors) -> std::io::Result<RebaseResult> {
892 let default_branch = get_default_branch()?;
894 logger.info(&format!(
895 "Rebasing onto {}{}{}",
896 colors.cyan(),
897 default_branch,
898 colors.reset()
899 ));
900
901 rebase_onto(&default_branch)
903}
904
905fn run_initial_rebase(
910 _args: &Args,
911 config: &crate::config::Config,
912 template_context: &TemplateContext,
913 logger: &Logger,
914 colors: Colors,
915) -> anyhow::Result<()> {
916 logger.header("Pre-development rebase", Colors::cyan);
917
918 match run_rebase_to_default(logger, colors) {
919 Ok(RebaseResult::Success) => {
920 logger.success("Rebase completed successfully");
921 Ok(())
922 }
923 Ok(RebaseResult::NoOp) => {
924 logger.info("No rebase needed (already up-to-date or on main branch)");
925 Ok(())
926 }
927 Ok(RebaseResult::Conflicts(_conflicts)) => {
928 let conflicted_files = get_conflicted_files()?;
930 if conflicted_files.is_empty() {
931 logger.warn("Rebase reported conflicts but no conflicted files found");
932 let _ = abort_rebase();
933 return Ok(());
934 }
935
936 logger.warn(&format!(
937 "Rebase resulted in {} conflict(s), attempting AI resolution",
938 conflicted_files.len()
939 ));
940
941 match try_resolve_conflicts_with_fallback(
943 &conflicted_files,
944 config,
945 template_context,
946 logger,
947 colors,
948 ) {
949 Ok(true) => {
950 logger.info("Continuing rebase after conflict resolution");
952 match continue_rebase() {
953 Ok(()) => {
954 logger.success("Rebase completed successfully after AI resolution");
955 Ok(())
956 }
957 Err(e) => {
958 logger.warn(&format!("Failed to continue rebase: {e}"));
959 let _ = abort_rebase();
960 Ok(()) }
962 }
963 }
964 Ok(false) => {
965 logger.warn("AI conflict resolution failed, aborting rebase");
967 let _ = abort_rebase();
968 Ok(()) }
970 Err(e) => {
971 logger.error(&format!("Conflict resolution error: {e}"));
972 let _ = abort_rebase();
973 Ok(()) }
975 }
976 }
977 Err(e) => {
978 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
979 Ok(())
980 }
981 }
982}
983
984fn run_post_review_rebase(
989 _args: &Args,
990 config: &crate::config::Config,
991 template_context: &TemplateContext,
992 logger: &Logger,
993 colors: Colors,
994) -> anyhow::Result<()> {
995 logger.header("Post-review rebase", Colors::cyan);
996
997 match run_rebase_to_default(logger, colors) {
998 Ok(RebaseResult::Success) => {
999 logger.success("Rebase completed successfully");
1000 Ok(())
1001 }
1002 Ok(RebaseResult::NoOp) => {
1003 logger.info("No rebase needed (already up-to-date or on main branch)");
1004 Ok(())
1005 }
1006 Ok(RebaseResult::Conflicts(_conflicts)) => {
1007 let conflicted_files = get_conflicted_files()?;
1009 if conflicted_files.is_empty() {
1010 logger.warn("Rebase reported conflicts but no conflicted files found");
1011 let _ = abort_rebase();
1012 return Ok(());
1013 }
1014
1015 logger.warn(&format!(
1016 "Rebase resulted in {} conflict(s), attempting AI resolution",
1017 conflicted_files.len()
1018 ));
1019
1020 match try_resolve_conflicts_with_fallback(
1022 &conflicted_files,
1023 config,
1024 template_context,
1025 logger,
1026 colors,
1027 ) {
1028 Ok(true) => {
1029 logger.info("Continuing rebase after conflict resolution");
1031 match continue_rebase() {
1032 Ok(()) => {
1033 logger.success("Rebase completed successfully after AI resolution");
1034 Ok(())
1035 }
1036 Err(e) => {
1037 logger.warn(&format!("Failed to continue rebase: {e}"));
1038 let _ = abort_rebase();
1039 Ok(()) }
1041 }
1042 }
1043 Ok(false) => {
1044 logger.warn("AI conflict resolution failed, aborting rebase");
1046 let _ = abort_rebase();
1047 Ok(()) }
1049 Err(e) => {
1050 logger.error(&format!("Conflict resolution error: {e}"));
1051 let _ = abort_rebase();
1052 Ok(()) }
1054 }
1055 }
1056 Err(e) => {
1057 logger.warn(&format!("Rebase failed, continuing without rebase: {e}"));
1058 Ok(())
1059 }
1060 }
1061}
1062
1063enum ConflictResolutionResult {
1067 WithJson(String),
1069 FileEditsOnly,
1071 Failed,
1073}
1074
1075fn try_resolve_conflicts_with_fallback(
1080 conflicted_files: &[String],
1081 config: &crate::config::Config,
1082 template_context: &TemplateContext,
1083 logger: &Logger,
1084 colors: Colors,
1085) -> anyhow::Result<bool> {
1086 if conflicted_files.is_empty() {
1087 return Ok(false);
1088 }
1089
1090 logger.info(&format!(
1091 "Attempting AI conflict resolution for {} file(s)",
1092 conflicted_files.len()
1093 ));
1094
1095 let conflicts = collect_conflict_info_or_error(conflicted_files, logger)?;
1096 let resolution_prompt = build_resolution_prompt(&conflicts, template_context);
1097
1098 match run_ai_conflict_resolution(&resolution_prompt, config, logger, colors) {
1099 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
1100 let resolved_files = parse_and_validate_resolved_files(&resolved_content, logger)?;
1102 write_resolved_files(&resolved_files, logger)?;
1103
1104 let remaining_conflicts = get_conflicted_files()?;
1106 if remaining_conflicts.is_empty() {
1107 Ok(true)
1108 } else {
1109 logger.warn(&format!(
1110 "{} conflicts remain after AI resolution",
1111 remaining_conflicts.len()
1112 ));
1113 Ok(false)
1114 }
1115 }
1116 Ok(ConflictResolutionResult::FileEditsOnly) => {
1117 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1119
1120 let remaining_conflicts = get_conflicted_files()?;
1122 if remaining_conflicts.is_empty() {
1123 logger.success("All conflicts resolved via file edits");
1124 Ok(true)
1125 } else {
1126 logger.warn(&format!(
1127 "{} conflicts remain after AI resolution",
1128 remaining_conflicts.len()
1129 ));
1130 Ok(false)
1131 }
1132 }
1133 Ok(ConflictResolutionResult::Failed) => {
1134 logger.warn("AI conflict resolution failed");
1135 logger.info("Attempting to continue rebase anyway...");
1136
1137 match crate::git_helpers::continue_rebase() {
1139 Ok(()) => {
1140 logger.info("Successfully continued rebase");
1141 Ok(true)
1142 }
1143 Err(rebase_err) => {
1144 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1145 Ok(false) }
1147 }
1148 }
1149 Err(e) => {
1150 logger.warn(&format!("AI conflict resolution failed: {e}"));
1151 logger.info("Attempting to continue rebase anyway...");
1152
1153 match crate::git_helpers::continue_rebase() {
1155 Ok(()) => {
1156 logger.info("Successfully continued rebase");
1157 Ok(true)
1158 }
1159 Err(rebase_err) => {
1160 logger.warn(&format!("Failed to continue rebase: {rebase_err}"));
1161 Ok(false) }
1163 }
1164 }
1165 }
1166}
1167
1168fn collect_conflict_info_or_error(
1170 conflicted_files: &[String],
1171 logger: &Logger,
1172) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
1173 use crate::prompts::collect_conflict_info;
1174
1175 let conflicts = match collect_conflict_info(conflicted_files) {
1176 Ok(c) => c,
1177 Err(e) => {
1178 logger.error(&format!("Failed to collect conflict info: {e}"));
1179 anyhow::bail!("Failed to collect conflict info");
1180 }
1181 };
1182 Ok(conflicts)
1183}
1184
1185fn build_resolution_prompt(
1187 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
1188 template_context: &TemplateContext,
1189) -> String {
1190 use crate::prompts::build_conflict_resolution_prompt_with_context;
1191 use std::fs;
1192
1193 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
1194 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
1195
1196 build_conflict_resolution_prompt_with_context(
1197 template_context,
1198 conflicts,
1199 prompt_md_content.as_deref(),
1200 plan_content.as_deref(),
1201 )
1202}
1203
1204fn run_ai_conflict_resolution(
1209 resolution_prompt: &str,
1210 config: &crate::config::Config,
1211 logger: &Logger,
1212 colors: Colors,
1213) -> anyhow::Result<ConflictResolutionResult> {
1214 use crate::agents::AgentRegistry;
1215 use crate::files::result_extraction::extract_last_result;
1216 use crate::pipeline::{
1217 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
1218 };
1219 use std::io;
1220 use std::path::Path;
1221
1222 let log_dir = ".agent/logs/rebase_conflict_resolution";
1226
1227 let registry = AgentRegistry::new()?;
1228 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
1229
1230 let mut runtime = PipelineRuntime {
1231 timer: &mut crate::pipeline::Timer::new(),
1232 logger,
1233 colors: &colors,
1234 config,
1235 };
1236
1237 let validate_output: OutputValidator = |log_dir_path: &Path,
1240 validation_logger: &crate::logger::Logger|
1241 -> io::Result<bool> {
1242 match extract_last_result(log_dir_path) {
1243 Ok(Some(_)) => {
1244 Ok(true)
1246 }
1247 Ok(None) => {
1248 match crate::git_helpers::get_conflicted_files() {
1251 Ok(conflicts) if conflicts.is_empty() => {
1252 validation_logger
1253 .info("Agent resolved conflicts without JSON output (file edits only)");
1254 Ok(true) }
1256 Ok(conflicts) => {
1257 validation_logger.warn(&format!(
1258 "{} conflict(s) remain unresolved",
1259 conflicts.len()
1260 ));
1261 Ok(false) }
1263 Err(e) => {
1264 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
1265 Ok(false) }
1267 }
1268 }
1269 Err(e) => {
1270 validation_logger.warn(&format!("Output validation check failed: {e}"));
1271 Ok(false) }
1273 }
1274 };
1275
1276 let mut fallback_config = FallbackConfig {
1277 role: crate::agents::AgentRole::Reviewer,
1278 base_label: "conflict resolution",
1279 prompt: resolution_prompt,
1280 logfile_prefix: log_dir,
1281 runtime: &mut runtime,
1282 registry: ®istry,
1283 primary_agent: reviewer_agent,
1284 output_validator: Some(validate_output),
1285 };
1286
1287 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
1288
1289 if exit_code != 0 {
1290 return Ok(ConflictResolutionResult::Failed);
1291 }
1292
1293 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
1296
1297 if remaining_conflicts.is_empty() {
1298 match extract_last_result(Path::new(log_dir)) {
1300 Ok(Some(content)) => {
1301 logger.info("Agent provided JSON output with resolved files");
1302 Ok(ConflictResolutionResult::WithJson(content))
1303 }
1304 Ok(None) => {
1305 logger.info("Agent resolved conflicts via file edits (no JSON output)");
1306 Ok(ConflictResolutionResult::FileEditsOnly)
1307 }
1308 Err(e) => {
1309 logger.warn(&format!(
1311 "Failed to extract JSON output but conflicts are resolved: {e}"
1312 ));
1313 Ok(ConflictResolutionResult::FileEditsOnly)
1314 }
1315 }
1316 } else {
1317 logger.warn(&format!(
1318 "{} conflict(s) remain after agent attempted resolution",
1319 remaining_conflicts.len()
1320 ));
1321 Ok(ConflictResolutionResult::Failed)
1322 }
1323}
1324
1325fn parse_and_validate_resolved_files(
1327 resolved_content: &str,
1328 logger: &Logger,
1329) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
1330 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|e| {
1331 logger.error(&format!("Failed to parse agent output as JSON: {e}"));
1332 anyhow::anyhow!("Failed to parse agent output as JSON")
1333 })?;
1334
1335 let resolved_files = match json.get("resolved_files") {
1336 Some(v) if v.is_object() => v.as_object().unwrap(),
1337 _ => {
1338 logger.error("Agent output missing 'resolved_files' object");
1339 anyhow::bail!("Agent output missing 'resolved_files' object");
1340 }
1341 };
1342
1343 if resolved_files.is_empty() {
1344 logger.error("No files were resolved by the agent");
1345 anyhow::bail!("No files were resolved by the agent");
1346 }
1347
1348 Ok(resolved_files.clone())
1349}
1350
1351fn write_resolved_files(
1353 resolved_files: &serde_json::Map<String, serde_json::Value>,
1354 logger: &Logger,
1355) -> anyhow::Result<usize> {
1356 use std::fs;
1357
1358 let mut files_written = 0;
1359 for (path, content) in resolved_files {
1360 if let Some(content_str) = content.as_str() {
1361 fs::write(path, content_str).map_err(|e| {
1362 logger.error(&format!("Failed to write {path}: {e}"));
1363 anyhow::anyhow!("Failed to write {path}: {e}")
1364 })?;
1365 logger.info(&format!("Resolved and wrote: {path}"));
1366 files_written += 1;
1367 if let Err(e) = crate::git_helpers::git_add_all() {
1369 logger.warn(&format!("Failed to stage {path}: {e}"));
1370 }
1371 }
1372 }
1373
1374 logger.success(&format!("Successfully resolved {files_written} file(s)"));
1375 Ok(files_written)
1376}