1pub mod config_init;
20pub mod context;
21pub mod detection;
22pub mod effect;
23pub mod effect_handler;
24pub mod effectful;
25pub mod event_loop;
26pub mod finalization;
27#[cfg(any(test, feature = "test-utils"))]
28pub mod mock_effect_handler;
29pub mod plumbing;
30pub mod resume;
31pub mod validation;
32
33use crate::agents::AgentRegistry;
34use crate::app::finalization::finalize_pipeline;
35use crate::banner::print_welcome_banner;
36use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
37
38use crate::checkpoint::{
39 save_checkpoint_with_workspace, CheckpointBuilder, PipelineCheckpoint, PipelinePhase,
40 RebaseState,
41};
42use crate::cli::{
43 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
44 handle_list_available_agents, handle_list_providers, handle_show_baseline,
45 handle_template_commands, prompt_template_selection, Args,
46};
47
48use crate::executor::ProcessExecutor;
49use crate::files::protection::monitoring::PromptMonitor;
50use crate::files::{
51 create_prompt_backup_with_workspace, make_prompt_read_only_with_workspace,
52 update_status_with_workspace, validate_prompt_md_with_workspace,
53};
54use crate::git_helpers::{
55 abort_rebase, continue_rebase, get_conflicted_files, get_default_branch,
56 is_main_or_master_branch, rebase_onto, reset_start_commit, RebaseResult,
57};
58#[cfg(not(feature = "test-utils"))]
59use crate::git_helpers::{
60 cleanup_orphaned_marker, get_start_commit_summary, save_start_commit, start_agent_phase,
61};
62use crate::logger::Colors;
63use crate::logger::Logger;
64use crate::phases::PhaseContext;
65use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
66use crate::prompts::{get_stored_or_generate_prompt, template_context::TemplateContext};
67
68use config_init::initialize_config;
69use context::PipelineContext;
70use detection::detect_project_stack;
71use plumbing::{handle_apply_commit, handle_generate_commit_msg, handle_show_commit_msg};
72use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
73use validation::{
74 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
75};
76
77pub fn run(args: Args, executor: std::sync::Arc<dyn ProcessExecutor>) -> anyhow::Result<()> {
97 let colors = Colors::new();
98 let logger = Logger::new(colors);
99
100 if let Some(ref override_dir) = args.working_dir_override {
103 std::env::set_current_dir(override_dir)?;
104 }
105
106 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
108 return Ok(()); };
110
111 let config_init::ConfigInitResult {
112 config,
113 registry,
114 config_path,
115 config_sources,
116 } = init_result;
117
118 let validated = resolve_required_agents(&config)?;
120 let developer_agent = validated.developer_agent;
121 let reviewer_agent = validated.reviewer_agent;
122
123 if handle_listing_commands(&args, ®istry, colors) {
125 return Ok(());
126 }
127
128 if args.recovery.diagnose {
130 handle_diagnose(
131 colors,
132 &config,
133 ®istry,
134 &config_path,
135 &config_sources,
136 &*executor,
137 );
138 return Ok(());
139 }
140
141 validate_agent_chains(®istry, colors);
143
144 let mut handler = effect_handler::RealAppEffectHandler::new();
148 if handle_plumbing_commands(&args, &logger, colors, &mut handler, None)? {
149 return Ok(());
150 }
151
152 let Some(repo_root) = validate_and_setup_agents(
154 AgentSetupParams {
155 config: &config,
156 registry: ®istry,
157 developer_agent: &developer_agent,
158 reviewer_agent: &reviewer_agent,
159 config_path: &config_path,
160 colors,
161 logger: &logger,
162 working_dir_override: args.working_dir_override.as_deref(),
163 },
164 &mut handler,
165 )?
166 else {
167 return Ok(());
168 };
169
170 let workspace: std::sync::Arc<dyn crate::workspace::Workspace> =
172 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()));
173
174 (prepare_pipeline_or_exit(PipelinePreparationParams {
176 args,
177 config,
178 registry,
179 developer_agent,
180 reviewer_agent,
181 repo_root,
182 logger,
183 colors,
184 executor,
185 handler: &mut handler,
186 workspace,
187 })?)
188 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
189}
190
191#[cfg(feature = "test-utils")]
211pub fn run_with_config(
212 args: Args,
213 executor: std::sync::Arc<dyn ProcessExecutor>,
214 config: crate::config::Config,
215 registry: AgentRegistry,
216) -> anyhow::Result<()> {
217 let mut handler = effect_handler::RealAppEffectHandler::new();
219 run_with_config_and_resolver(
220 args,
221 executor,
222 config,
223 registry,
224 &crate::config::RealConfigEnvironment,
225 &mut handler,
226 None, )
228}
229
230#[cfg(feature = "test-utils")]
253pub fn run_with_config_and_resolver<
254 P: crate::config::ConfigEnvironment,
255 H: effect::AppEffectHandler,
256>(
257 args: Args,
258 executor: std::sync::Arc<dyn ProcessExecutor>,
259 config: crate::config::Config,
260 registry: AgentRegistry,
261 path_resolver: &P,
262 handler: &mut H,
263 workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
264) -> anyhow::Result<()> {
265 use crate::cli::{
266 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
267 handle_list_work_guides, handle_smart_init_with,
268 };
269
270 let colors = Colors::new();
271 let logger = Logger::new(colors);
272
273 if let Some(ref override_dir) = args.working_dir_override {
275 std::env::set_current_dir(override_dir)?;
276 }
277
278 if args.recovery.extended_help {
280 handle_extended_help();
281 if args.work_guide_list.list_work_guides {
282 println!();
283 handle_list_work_guides(colors);
284 }
285 return Ok(());
286 }
287
288 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
290 return Ok(());
291 }
292
293 if let Some(ref template_name) = args.init_prompt {
295 if handle_init_prompt_with(
296 template_name,
297 args.unified_init.force_init,
298 colors,
299 path_resolver,
300 )? {
301 return Ok(());
302 }
303 }
304
305 if args.unified_init.init.is_some()
307 && handle_smart_init_with(
308 args.unified_init.init.as_deref(),
309 args.unified_init.force_init,
310 colors,
311 path_resolver,
312 )?
313 {
314 return Ok(());
315 }
316
317 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
319 return Ok(());
320 }
321
322 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
324 return Ok(());
325 }
326
327 if args.legacy_init.init_legacy {
329 let repo_root = match handler.execute(effect::AppEffect::GitGetRepoRoot) {
330 effect::AppEffectResult::Path(p) => Some(p),
331 _ => None,
332 };
333 let legacy_path = repo_root.map_or_else(
334 || std::path::PathBuf::from(".agent/agents.toml"),
335 |root| root.join(".agent/agents.toml"),
336 );
337 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
338 return Ok(());
339 }
340 }
341
342 let config_path = std::path::PathBuf::from("test-config");
344
345 let validated = resolve_required_agents(&config)?;
347 let developer_agent = validated.developer_agent;
348 let reviewer_agent = validated.reviewer_agent;
349
350 if handle_listing_commands(&args, ®istry, colors) {
352 return Ok(());
353 }
354
355 if args.recovery.diagnose {
357 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
358 return Ok(());
359 }
360
361 if handle_plumbing_commands(
364 &args,
365 &logger,
366 colors,
367 handler,
368 workspace.as_ref().map(|w| w.as_ref()),
369 )? {
370 return Ok(());
371 }
372
373 let Some(repo_root) = validate_and_setup_agents(
375 AgentSetupParams {
376 config: &config,
377 registry: ®istry,
378 developer_agent: &developer_agent,
379 reviewer_agent: &reviewer_agent,
380 config_path: &config_path,
381 colors,
382 logger: &logger,
383 working_dir_override: args.working_dir_override.as_deref(),
384 },
385 handler,
386 )?
387 else {
388 return Ok(());
389 };
390
391 let workspace = workspace.unwrap_or_else(|| {
393 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
394 });
395
396 (prepare_pipeline_or_exit(PipelinePreparationParams {
398 args,
399 config,
400 registry,
401 developer_agent,
402 reviewer_agent,
403 repo_root,
404 logger,
405 colors,
406 executor,
407 handler,
408 workspace,
409 })?)
410 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
411}
412
413#[cfg(feature = "test-utils")]
417pub struct RunWithHandlersParams<'a, 'ctx, P, A, E>
418where
419 P: crate::config::ConfigEnvironment,
420 A: effect::AppEffectHandler,
421 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
422{
423 pub args: Args,
424 pub executor: std::sync::Arc<dyn ProcessExecutor>,
425 pub config: crate::config::Config,
426 pub registry: AgentRegistry,
427 pub path_resolver: &'a P,
428 pub app_handler: &'a mut A,
429 pub effect_handler: &'a mut E,
430 pub workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
431 pub _marker: std::marker::PhantomData<&'ctx ()>,
433}
434
435#[cfg(feature = "test-utils")]
463pub fn run_with_config_and_handlers<'a, 'ctx, P, A, E>(
464 params: RunWithHandlersParams<'a, 'ctx, P, A, E>,
465) -> anyhow::Result<()>
466where
467 P: crate::config::ConfigEnvironment,
468 A: effect::AppEffectHandler,
469 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
470{
471 let RunWithHandlersParams {
472 args,
473 executor,
474 config,
475 registry,
476 path_resolver,
477 app_handler,
478 effect_handler,
479 workspace,
480 ..
481 } = params;
482 use crate::cli::{
483 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
484 handle_list_work_guides, handle_smart_init_with,
485 };
486
487 let colors = Colors::new();
488 let logger = Logger::new(colors);
489
490 if let Some(ref override_dir) = args.working_dir_override {
492 std::env::set_current_dir(override_dir)?;
493 }
494
495 if args.recovery.extended_help {
497 handle_extended_help();
498 if args.work_guide_list.list_work_guides {
499 println!();
500 handle_list_work_guides(colors);
501 }
502 return Ok(());
503 }
504
505 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
507 return Ok(());
508 }
509
510 if let Some(ref template_name) = args.init_prompt {
512 if handle_init_prompt_with(
513 template_name,
514 args.unified_init.force_init,
515 colors,
516 path_resolver,
517 )? {
518 return Ok(());
519 }
520 }
521
522 if args.unified_init.init.is_some()
524 && handle_smart_init_with(
525 args.unified_init.init.as_deref(),
526 args.unified_init.force_init,
527 colors,
528 path_resolver,
529 )?
530 {
531 return Ok(());
532 }
533
534 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
536 return Ok(());
537 }
538
539 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
541 return Ok(());
542 }
543
544 if args.legacy_init.init_legacy {
546 let repo_root = match app_handler.execute(effect::AppEffect::GitGetRepoRoot) {
547 effect::AppEffectResult::Path(p) => Some(p),
548 _ => None,
549 };
550 let legacy_path = repo_root.map_or_else(
551 || std::path::PathBuf::from(".agent/agents.toml"),
552 |root| root.join(".agent/agents.toml"),
553 );
554 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
555 return Ok(());
556 }
557 }
558
559 let config_path = std::path::PathBuf::from("test-config");
561
562 let validated = resolve_required_agents(&config)?;
564 let developer_agent = validated.developer_agent;
565 let reviewer_agent = validated.reviewer_agent;
566
567 if handle_listing_commands(&args, ®istry, colors) {
569 return Ok(());
570 }
571
572 if args.recovery.diagnose {
574 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
575 return Ok(());
576 }
577
578 if handle_plumbing_commands(
581 &args,
582 &logger,
583 colors,
584 app_handler,
585 workspace.as_ref().map(|w| w.as_ref()),
586 )? {
587 return Ok(());
588 }
589
590 let Some(repo_root) = validate_and_setup_agents(
592 AgentSetupParams {
593 config: &config,
594 registry: ®istry,
595 developer_agent: &developer_agent,
596 reviewer_agent: &reviewer_agent,
597 config_path: &config_path,
598 colors,
599 logger: &logger,
600 working_dir_override: args.working_dir_override.as_deref(),
601 },
602 app_handler,
603 )?
604 else {
605 return Ok(());
606 };
607
608 let workspace = workspace.unwrap_or_else(|| {
610 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
611 });
612
613 let ctx = prepare_pipeline_or_exit(PipelinePreparationParams {
615 args,
616 config,
617 registry,
618 developer_agent,
619 reviewer_agent,
620 repo_root,
621 logger,
622 colors,
623 executor,
624 handler: app_handler,
625 workspace,
626 })?;
627
628 match ctx {
630 Some(ctx) => run_pipeline_with_effect_handler(&ctx, effect_handler),
631 None => Ok(()),
632 }
633}
634
635fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
639 if args.agent_list.list_agents {
640 handle_list_agents(registry);
641 return true;
642 }
643 if args.agent_list.list_available_agents {
644 handle_list_available_agents(registry);
645 return true;
646 }
647 if args.provider_list.list_providers {
648 handle_list_providers(colors);
649 return true;
650 }
651
652 let template_cmds = &args.template_commands;
654 if template_cmds.init_templates_enabled()
655 || template_cmds.validate
656 || template_cmds.show.is_some()
657 || template_cmds.list
658 || template_cmds.list_all
659 || template_cmds.variables.is_some()
660 || template_cmds.render.is_some()
661 {
662 let _ = handle_template_commands(template_cmds, colors);
663 return true;
664 }
665
666 false
667}
668
669fn handle_plumbing_commands<H: effect::AppEffectHandler>(
680 args: &Args,
681 logger: &Logger,
682 colors: Colors,
683 handler: &mut H,
684 workspace: Option<&dyn crate::workspace::Workspace>,
685) -> anyhow::Result<bool> {
686 use plumbing::{handle_apply_commit_with_handler, handle_show_commit_msg_with_workspace};
687
688 fn setup_working_dir_via_handler<H: effect::AppEffectHandler>(
690 override_dir: Option<&std::path::Path>,
691 handler: &mut H,
692 ) -> anyhow::Result<()> {
693 use effect::{AppEffect, AppEffectResult};
694
695 if let Some(dir) = override_dir {
696 match handler.execute(AppEffect::SetCurrentDir {
697 path: dir.to_path_buf(),
698 }) {
699 AppEffectResult::Ok => Ok(()),
700 AppEffectResult::Error(e) => anyhow::bail!(e),
701 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
702 }
703 } else {
704 match handler.execute(AppEffect::GitRequireRepo) {
706 AppEffectResult::Ok => {}
707 AppEffectResult::Error(e) => anyhow::bail!(e),
708 other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
709 }
710 let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
712 AppEffectResult::Path(p) => p,
713 AppEffectResult::Error(e) => anyhow::bail!(e),
714 other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
715 };
716 match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
718 AppEffectResult::Ok => Ok(()),
719 AppEffectResult::Error(e) => anyhow::bail!(e),
720 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
721 }
722 }
723 }
724
725 if args.commit_display.show_commit_msg {
727 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
728 if let Some(ws) = workspace {
729 return handle_show_commit_msg_with_workspace(ws).map(|()| true);
730 }
731 return handle_show_commit_msg().map(|()| true);
732 }
733
734 if args.commit_plumbing.apply_commit {
736 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
737 if let Some(ws) = workspace {
738 return handle_apply_commit_with_handler(ws, handler, logger, colors).map(|()| true);
739 }
740 return handle_apply_commit(logger, colors).map(|()| true);
741 }
742
743 if args.commit_display.reset_start_commit {
745 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
746
747 return match handler.execute(effect::AppEffect::GitResetStartCommit) {
749 effect::AppEffectResult::String(oid) => {
750 let short_oid = &oid[..8.min(oid.len())];
752 logger.success(&format!("Starting commit reference reset ({})", short_oid));
753 logger.info(".agent/start_commit has been updated");
754 Ok(true)
755 }
756 effect::AppEffectResult::Error(e) => {
757 logger.error(&format!("Failed to reset starting commit: {e}"));
758 anyhow::bail!("Failed to reset starting commit");
759 }
760 other => {
761 drop(other);
764 match reset_start_commit() {
765 Ok(result) => {
766 let short_oid = &result.oid[..8.min(result.oid.len())];
767 if result.fell_back_to_head {
768 logger.success(&format!(
769 "Starting commit reference reset to current HEAD ({})",
770 short_oid
771 ));
772 logger.info("On main/master branch - using HEAD as baseline");
773 } else if let Some(ref branch) = result.default_branch {
774 logger.success(&format!(
775 "Starting commit reference reset to merge-base with '{}' ({})",
776 branch, short_oid
777 ));
778 logger.info("Baseline set to common ancestor with default branch");
779 } else {
780 logger.success(&format!(
781 "Starting commit reference reset ({})",
782 short_oid
783 ));
784 }
785 logger.info(".agent/start_commit has been updated");
786 Ok(true)
787 }
788 Err(e) => {
789 logger.error(&format!("Failed to reset starting commit: {e}"));
790 anyhow::bail!("Failed to reset starting commit");
791 }
792 }
793 }
794 };
795 }
796
797 if args.commit_display.show_baseline {
799 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
800
801 return match handle_show_baseline() {
802 Ok(()) => Ok(true),
803 Err(e) => {
804 logger.error(&format!("Failed to show baseline: {e}"));
805 anyhow::bail!("Failed to show baseline");
806 }
807 };
808 }
809
810 Ok(false)
811}
812
813struct PipelinePreparationParams<'a, H: effect::AppEffectHandler> {
817 args: Args,
818 config: crate::config::Config,
819 registry: AgentRegistry,
820 developer_agent: String,
821 reviewer_agent: String,
822 repo_root: std::path::PathBuf,
823 logger: Logger,
824 colors: Colors,
825 executor: std::sync::Arc<dyn ProcessExecutor>,
826 handler: &'a mut H,
827 workspace: std::sync::Arc<dyn crate::workspace::Workspace>,
832}
833
834fn prepare_pipeline_or_exit<H: effect::AppEffectHandler>(
838 params: PipelinePreparationParams<'_, H>,
839) -> anyhow::Result<Option<PipelineContext>> {
840 let PipelinePreparationParams {
841 args,
842 config,
843 registry,
844 developer_agent,
845 reviewer_agent,
846 repo_root,
847 mut logger,
848 colors,
849 executor,
850 handler,
851 workspace,
852 } = params;
853
854 effectful::ensure_files_effectful(handler, config.isolation_mode)
856 .map_err(|e| anyhow::anyhow!("{}", e))?;
857
858 if config.isolation_mode {
860 effectful::reset_context_for_isolation_effectful(handler)
861 .map_err(|e| anyhow::anyhow!("{}", e))?;
862 }
863
864 logger = logger.with_log_file(".agent/logs/pipeline.log");
865
866 if args.recovery.dry_run {
868 let developer_display = registry.display_name(&developer_agent);
869 let reviewer_display = registry.display_name(&reviewer_agent);
870 handle_dry_run(
871 &logger,
872 colors,
873 &config,
874 &developer_display,
875 &reviewer_display,
876 &repo_root,
877 )?;
878 return Ok(None);
879 }
880
881 let template_context =
883 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
884
885 if args.rebase_flags.rebase_only {
887 handle_rebase_only(
888 &args,
889 &config,
890 &template_context,
891 &logger,
892 colors,
893 std::sync::Arc::clone(&executor),
894 &repo_root,
895 )?;
896 return Ok(None);
897 }
898
899 if args.commit_plumbing.generate_commit_msg {
901 handle_generate_commit_msg(plumbing::CommitGenerationConfig {
902 config: &config,
903 template_context: &template_context,
904 workspace: &*workspace,
905 registry: ®istry,
906 logger: &logger,
907 colors,
908 developer_agent: &developer_agent,
909 _reviewer_agent: &reviewer_agent,
910 executor: std::sync::Arc::clone(&executor),
911 })?;
912 return Ok(None);
913 }
914
915 let developer_display = registry.display_name(&developer_agent);
917 let reviewer_display = registry.display_name(&reviewer_agent);
918
919 let ctx = PipelineContext {
921 args,
922 config,
923 registry,
924 developer_agent,
925 reviewer_agent,
926 developer_display,
927 reviewer_display,
928 repo_root,
929 workspace,
930 logger,
931 colors,
932 template_context,
933 executor,
934 };
935 Ok(Some(ctx))
936}
937
938struct AgentSetupParams<'a> {
940 config: &'a crate::config::Config,
941 registry: &'a AgentRegistry,
942 developer_agent: &'a str,
943 reviewer_agent: &'a str,
944 config_path: &'a std::path::Path,
945 colors: Colors,
946 logger: &'a Logger,
947 working_dir_override: Option<&'a std::path::Path>,
950}
951
952fn validate_and_setup_agents<H: effect::AppEffectHandler>(
957 params: AgentSetupParams<'_>,
958 handler: &mut H,
959) -> anyhow::Result<Option<std::path::PathBuf>> {
960 let AgentSetupParams {
961 config,
962 registry,
963 developer_agent,
964 reviewer_agent,
965 config_path,
966 colors,
967 logger,
968 working_dir_override,
969 } = params;
970 validate_agent_commands(
972 config,
973 registry,
974 developer_agent,
975 reviewer_agent,
976 config_path,
977 )?;
978
979 validate_can_commit(
981 config,
982 registry,
983 developer_agent,
984 reviewer_agent,
985 config_path,
986 )?;
987
988 let repo_root = if let Some(override_dir) = working_dir_override {
990 handler.execute(effect::AppEffect::SetCurrentDir {
992 path: override_dir.to_path_buf(),
993 });
994 override_dir.to_path_buf()
995 } else {
996 let require_result = handler.execute(effect::AppEffect::GitRequireRepo);
998 if let effect::AppEffectResult::Error(e) = require_result {
999 anyhow::bail!("Not in a git repository: {}", e);
1000 }
1001
1002 let root_result = handler.execute(effect::AppEffect::GitGetRepoRoot);
1003 let root = match root_result {
1004 effect::AppEffectResult::Path(p) => p,
1005 effect::AppEffectResult::Error(e) => {
1006 anyhow::bail!("Failed to get repo root: {}", e);
1007 }
1008 _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
1009 };
1010
1011 handler.execute(effect::AppEffect::SetCurrentDir { path: root.clone() });
1012 root
1013 };
1014
1015 let should_continue = setup_git_and_prompt_file(config, colors, logger, handler)?;
1017 if should_continue.is_none() {
1018 return Ok(None);
1019 }
1020
1021 Ok(Some(repo_root))
1022}
1023
1024fn setup_git_and_prompt_file<H: effect::AppEffectHandler>(
1029 config: &crate::config::Config,
1030 colors: Colors,
1031 logger: &Logger,
1032 handler: &mut H,
1033) -> anyhow::Result<Option<()>> {
1034 let prompt_exists =
1035 effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{}", e))?;
1036
1037 if config.behavior.interactive && !prompt_exists {
1040 if let Some(template_name) = prompt_template_selection(colors) {
1041 create_prompt_from_template(&template_name, colors)?;
1042 println!();
1043 logger.info(
1044 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
1045 );
1046 logger.info("Tip: Edit PROMPT.md, then run: ralph");
1047 return Ok(None);
1048 }
1049 println!();
1050 logger.error("PROMPT.md not found in current directory.");
1051 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1052 println!();
1053 logger.info("To get started:");
1054 logger.info(" ralph --init # Smart setup wizard");
1055 logger.info(" ralph --init bug-fix # Create from Work Guide");
1056 logger.info(" ralph --list-work-guides # See all Work Guides");
1057 println!();
1058 return Ok(None);
1059 }
1060
1061 if !prompt_exists {
1063 logger.error("PROMPT.md not found in current directory.");
1064 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1065 println!();
1066 logger.info("Quick start:");
1067 logger.info(" ralph --init # Smart setup wizard");
1068 logger.info(" ralph --init bug-fix # Create from Work Guide");
1069 logger.info(" ralph --list-work-guides # See all Work Guides");
1070 println!();
1071 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1072 println!();
1073 return Ok(None);
1074 }
1075
1076 Ok(Some(()))
1077}
1078
1079fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1081 run_pipeline_with_default_handler(ctx)
1083}
1084
1085fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1089 use crate::app::event_loop::EventLoopConfig;
1090 #[cfg(not(feature = "test-utils"))]
1091 use crate::reducer::MainEffectHandler;
1092 use crate::reducer::PipelineState;
1093
1094 let resume_result = offer_resume_if_checkpoint_exists(
1096 &ctx.args,
1097 &ctx.config,
1098 &ctx.registry,
1099 &ctx.logger,
1100 &ctx.developer_agent,
1101 &ctx.reviewer_agent,
1102 );
1103
1104 let resume_result = match resume_result {
1106 Some(result) => Some(result),
1107 None => handle_resume_with_validation(
1108 &ctx.args,
1109 &ctx.config,
1110 &ctx.registry,
1111 &ctx.logger,
1112 &ctx.developer_display,
1113 &ctx.reviewer_display,
1114 ),
1115 };
1116
1117 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1118
1119 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1121 use crate::checkpoint::RunContext;
1122 RunContext::from_checkpoint(checkpoint)
1123 } else {
1124 use crate::checkpoint::RunContext;
1125 RunContext::new()
1126 };
1127
1128 let config = if let Some(ref checkpoint) = resume_checkpoint {
1130 use crate::checkpoint::apply_checkpoint_to_config;
1131 let mut restored_config = ctx.config.clone();
1132 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1133 ctx.logger.info("Restored configuration from checkpoint:");
1134 if checkpoint.cli_args.developer_iters > 0 {
1135 ctx.logger.info(&format!(
1136 " Developer iterations: {} (from checkpoint)",
1137 checkpoint.cli_args.developer_iters
1138 ));
1139 }
1140 if checkpoint.cli_args.reviewer_reviews > 0 {
1141 ctx.logger.info(&format!(
1142 " Reviewer passes: {} (from checkpoint)",
1143 checkpoint.cli_args.reviewer_reviews
1144 ));
1145 }
1146 restored_config
1147 } else {
1148 ctx.config.clone()
1149 };
1150
1151 if let Some(ref checkpoint) = resume_checkpoint {
1153 use crate::checkpoint::restore::restore_environment_from_checkpoint;
1154 let restored_count = restore_environment_from_checkpoint(checkpoint);
1155 if restored_count > 0 {
1156 ctx.logger.info(&format!(
1157 " Restored {} environment variable(s) from checkpoint",
1158 restored_count
1159 ));
1160 }
1161 }
1162
1163 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1167
1168 #[cfg(feature = "test-utils")]
1169 {
1170 use crate::git_helpers::{
1171 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1172 };
1173 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1175 create_marker_with_workspace(&*ctx.workspace)?;
1176 }
1178 #[cfg(not(feature = "test-utils"))]
1179 {
1180 cleanup_orphaned_marker(&ctx.logger)?;
1181 start_agent_phase(&mut git_helpers)?;
1182 }
1183 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1184
1185 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1187 print_pipeline_info_with_config(ctx, &config);
1188 validate_prompt_and_setup_backup(ctx)?;
1189
1190 let mut prompt_monitor = setup_prompt_monitor(ctx);
1192
1193 let (_project_stack, review_guidelines) =
1195 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1196
1197 print_review_guidelines(ctx, review_guidelines.as_ref());
1198 println!();
1199
1200 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1202 let mut phase_ctx = create_phase_context_with_config(
1203 ctx,
1204 &config,
1205 &mut timer,
1206 &mut stats,
1207 review_guidelines.as_ref(),
1208 &run_context,
1209 resume_checkpoint.as_ref(),
1210 );
1211 save_start_commit_or_warn(ctx);
1212
1213 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1216 checkpoint.phase
1217 } else {
1218 PipelinePhase::Planning
1219 };
1220 setup_interrupt_context_for_pipeline(
1221 initial_phase,
1222 config.developer_iters,
1223 config.reviewer_reviews,
1224 &phase_ctx.execution_history,
1225 &phase_ctx.prompt_history,
1226 &run_context,
1227 );
1228
1229 let _interrupt_guard = defer_clear_interrupt_context();
1231
1232 let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1234 if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1236 !checkpoint.cli_args.skip_rebase
1237 } else {
1238 ctx.args.rebase_flags.with_rebase
1240 }
1241 } else {
1242 ctx.args.rebase_flags.with_rebase
1243 };
1244
1245 if should_run_rebase {
1247 run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1248 update_interrupt_context_from_phase(
1250 &phase_ctx,
1251 PipelinePhase::Planning,
1252 config.developer_iters,
1253 config.reviewer_reviews,
1254 &run_context,
1255 );
1256 } else {
1257 if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1259 let builder = CheckpointBuilder::new()
1260 .phase(PipelinePhase::Planning, 0, config.developer_iters)
1261 .reviewer_pass(0, config.reviewer_reviews)
1262 .skip_rebase(true) .capture_from_context(
1264 &config,
1265 &ctx.registry,
1266 &ctx.developer_agent,
1267 &ctx.reviewer_agent,
1268 &ctx.logger,
1269 &run_context,
1270 )
1271 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1272 .with_execution_history(phase_ctx.execution_history.clone())
1273 .with_prompt_history(phase_ctx.clone_prompt_history());
1274
1275 if let Some(checkpoint) = builder.build() {
1276 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1277 }
1278 }
1279 update_interrupt_context_from_phase(
1281 &phase_ctx,
1282 PipelinePhase::Planning,
1283 config.developer_iters,
1284 config.reviewer_reviews,
1285 &run_context,
1286 );
1287 }
1288
1289 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1295 PipelineState::from(checkpoint.clone())
1297 } else {
1298 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1300 };
1301
1302 let event_loop_config = EventLoopConfig {
1304 max_iterations: 1000,
1305 enable_checkpointing: config.features.checkpoint_enabled,
1306 };
1307
1308 let execution_history_before = phase_ctx.execution_history.clone();
1310 let prompt_history_before = phase_ctx.clone_prompt_history();
1311
1312 #[cfg(feature = "test-utils")]
1315 let loop_result = {
1316 use crate::app::event_loop::run_event_loop_with_handler;
1317 use crate::reducer::mock_effect_handler::MockEffectHandler;
1318 let mut handler = MockEffectHandler::new(initial_state.clone());
1319 let phase_ctx_ref = &mut phase_ctx;
1320 run_event_loop_with_handler(
1321 phase_ctx_ref,
1322 Some(initial_state),
1323 event_loop_config,
1324 &mut handler,
1325 )
1326 };
1327 #[cfg(not(feature = "test-utils"))]
1328 let loop_result = {
1329 use crate::app::event_loop::run_event_loop_with_handler;
1330 let mut handler = MainEffectHandler::new(initial_state.clone());
1331 let phase_ctx_ref = &mut phase_ctx;
1332 run_event_loop_with_handler(
1333 phase_ctx_ref,
1334 Some(initial_state),
1335 event_loop_config,
1336 &mut handler,
1337 )
1338 };
1339
1340 let loop_result = loop_result?;
1342 if loop_result.completed {
1343 ctx.logger
1344 .success("Pipeline completed successfully via reducer event loop");
1345 ctx.logger.info(&format!(
1346 "Total events processed: {}",
1347 loop_result.events_processed
1348 ));
1349 } else {
1350 ctx.logger.warn("Pipeline exited without completion marker");
1351 }
1352
1353 if config.features.checkpoint_enabled {
1355 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1356 let builder = CheckpointBuilder::new()
1357 .phase(
1358 PipelinePhase::Complete,
1359 config.developer_iters,
1360 config.developer_iters,
1361 )
1362 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1363 .skip_rebase(skip_rebase)
1364 .capture_from_context(
1365 &config,
1366 &ctx.registry,
1367 &ctx.developer_agent,
1368 &ctx.reviewer_agent,
1369 &ctx.logger,
1370 &run_context,
1371 )
1372 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1373
1374 let builder = builder
1375 .with_execution_history(execution_history_before)
1376 .with_prompt_history(prompt_history_before);
1377
1378 if let Some(checkpoint) = builder.build() {
1379 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1380 }
1381 }
1382
1383 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1385 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1386
1387 finalize_pipeline(
1389 &mut agent_phase_guard,
1390 &ctx.logger,
1391 ctx.colors,
1392 &config,
1393 finalization::RuntimeStats {
1394 timer: &timer,
1395 stats: &stats,
1396 },
1397 prompt_monitor,
1398 Some(&*ctx.workspace),
1399 );
1400 Ok(())
1401}
1402
1403#[cfg(feature = "test-utils")]
1417pub fn run_pipeline_with_effect_handler<'ctx, H>(
1418 ctx: &PipelineContext,
1419 effect_handler: &mut H,
1420) -> anyhow::Result<()>
1421where
1422 H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1423{
1424 use crate::app::event_loop::EventLoopConfig;
1425 use crate::reducer::PipelineState;
1426
1427 let resume_result = offer_resume_if_checkpoint_exists(
1429 &ctx.args,
1430 &ctx.config,
1431 &ctx.registry,
1432 &ctx.logger,
1433 &ctx.developer_agent,
1434 &ctx.reviewer_agent,
1435 );
1436
1437 let resume_result = match resume_result {
1439 Some(result) => Some(result),
1440 None => handle_resume_with_validation(
1441 &ctx.args,
1442 &ctx.config,
1443 &ctx.registry,
1444 &ctx.logger,
1445 &ctx.developer_display,
1446 &ctx.reviewer_display,
1447 ),
1448 };
1449
1450 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1451
1452 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1454 use crate::checkpoint::RunContext;
1455 RunContext::from_checkpoint(checkpoint)
1456 } else {
1457 use crate::checkpoint::RunContext;
1458 RunContext::new()
1459 };
1460
1461 let config = if let Some(ref checkpoint) = resume_checkpoint {
1463 use crate::checkpoint::apply_checkpoint_to_config;
1464 let mut restored_config = ctx.config.clone();
1465 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1466 restored_config
1467 } else {
1468 ctx.config.clone()
1469 };
1470
1471 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1475
1476 #[cfg(feature = "test-utils")]
1477 {
1478 use crate::git_helpers::{
1479 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1480 };
1481 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1483 create_marker_with_workspace(&*ctx.workspace)?;
1484 }
1486 #[cfg(not(feature = "test-utils"))]
1487 {
1488 cleanup_orphaned_marker(&ctx.logger)?;
1489 start_agent_phase(&mut git_helpers)?;
1490 }
1491 let mut agent_phase_guard = AgentPhaseGuard::new(&mut git_helpers, &ctx.logger);
1492
1493 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1495 print_pipeline_info_with_config(ctx, &config);
1496 validate_prompt_and_setup_backup(ctx)?;
1497
1498 let mut prompt_monitor = setup_prompt_monitor(ctx);
1500
1501 let (_project_stack, review_guidelines) =
1503 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1504
1505 print_review_guidelines(ctx, review_guidelines.as_ref());
1506 println!();
1507
1508 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1510 let mut phase_ctx = create_phase_context_with_config(
1511 ctx,
1512 &config,
1513 &mut timer,
1514 &mut stats,
1515 review_guidelines.as_ref(),
1516 &run_context,
1517 resume_checkpoint.as_ref(),
1518 );
1519 save_start_commit_or_warn(ctx);
1520
1521 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1523 checkpoint.phase
1524 } else {
1525 PipelinePhase::Planning
1526 };
1527 setup_interrupt_context_for_pipeline(
1528 initial_phase,
1529 config.developer_iters,
1530 config.reviewer_reviews,
1531 &phase_ctx.execution_history,
1532 &phase_ctx.prompt_history,
1533 &run_context,
1534 );
1535
1536 let _interrupt_guard = defer_clear_interrupt_context();
1538
1539 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1541 PipelineState::from(checkpoint.clone())
1542 } else {
1543 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1544 };
1545
1546 let event_loop_config = EventLoopConfig {
1548 max_iterations: 1000,
1549 enable_checkpointing: config.features.checkpoint_enabled,
1550 };
1551
1552 let execution_history_before = phase_ctx.execution_history.clone();
1554 let prompt_history_before = phase_ctx.clone_prompt_history();
1555
1556 effect_handler.update_state(initial_state.clone());
1558 let loop_result = {
1559 use crate::app::event_loop::run_event_loop_with_handler;
1560 let phase_ctx_ref = &mut phase_ctx;
1561 run_event_loop_with_handler(
1562 phase_ctx_ref,
1563 Some(initial_state),
1564 event_loop_config,
1565 effect_handler,
1566 )
1567 };
1568
1569 let loop_result = loop_result?;
1571 if loop_result.completed {
1572 ctx.logger
1573 .success("Pipeline completed successfully via reducer event loop");
1574 ctx.logger.info(&format!(
1575 "Total events processed: {}",
1576 loop_result.events_processed
1577 ));
1578 } else {
1579 ctx.logger.warn("Pipeline exited without completion marker");
1580 }
1581
1582 if config.features.checkpoint_enabled {
1584 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1585 let builder = CheckpointBuilder::new()
1586 .phase(
1587 PipelinePhase::Complete,
1588 config.developer_iters,
1589 config.developer_iters,
1590 )
1591 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1592 .skip_rebase(skip_rebase)
1593 .capture_from_context(
1594 &config,
1595 &ctx.registry,
1596 &ctx.developer_agent,
1597 &ctx.reviewer_agent,
1598 &ctx.logger,
1599 &run_context,
1600 )
1601 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1602
1603 let builder = builder
1604 .with_execution_history(execution_history_before)
1605 .with_prompt_history(prompt_history_before);
1606
1607 if let Some(checkpoint) = builder.build() {
1608 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1609 }
1610 }
1611
1612 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1614 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1615
1616 finalize_pipeline(
1618 &mut agent_phase_guard,
1619 &ctx.logger,
1620 ctx.colors,
1621 &config,
1622 finalization::RuntimeStats {
1623 timer: &timer,
1624 stats: &stats,
1625 },
1626 prompt_monitor,
1627 Some(&*ctx.workspace),
1628 );
1629 Ok(())
1630}
1631
1632fn setup_interrupt_context_for_pipeline(
1637 phase: PipelinePhase,
1638 total_iterations: u32,
1639 total_reviewer_passes: u32,
1640 execution_history: &crate::checkpoint::ExecutionHistory,
1641 prompt_history: &std::collections::HashMap<String, String>,
1642 run_context: &crate::checkpoint::RunContext,
1643) {
1644 use crate::interrupt::{set_interrupt_context, InterruptContext};
1645
1646 let (iteration, reviewer_pass) = match phase {
1648 PipelinePhase::Development => (1, 0),
1649 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1650 (total_iterations, 1)
1651 }
1652 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1653 (total_iterations, total_reviewer_passes)
1654 }
1655 _ => (0, 0),
1656 };
1657
1658 let context = InterruptContext {
1659 phase,
1660 iteration,
1661 total_iterations,
1662 reviewer_pass,
1663 total_reviewer_passes,
1664 run_context: run_context.clone(),
1665 execution_history: execution_history.clone(),
1666 prompt_history: prompt_history.clone(),
1667 };
1668
1669 set_interrupt_context(context);
1670}
1671
1672fn update_interrupt_context_from_phase(
1677 phase_ctx: &crate::phases::PhaseContext,
1678 phase: PipelinePhase,
1679 total_iterations: u32,
1680 total_reviewer_passes: u32,
1681 run_context: &crate::checkpoint::RunContext,
1682) {
1683 use crate::interrupt::{set_interrupt_context, InterruptContext};
1684
1685 let (iteration, reviewer_pass) = match phase {
1687 PipelinePhase::Development => {
1688 let iter = run_context.actual_developer_runs.max(1);
1690 (iter, 0)
1691 }
1692 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1693 (total_iterations, run_context.actual_reviewer_runs.max(1))
1694 }
1695 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1696 (total_iterations, total_reviewer_passes)
1697 }
1698 _ => (0, 0),
1699 };
1700
1701 let context = InterruptContext {
1702 phase,
1703 iteration,
1704 total_iterations,
1705 reviewer_pass,
1706 total_reviewer_passes,
1707 run_context: run_context.clone(),
1708 execution_history: phase_ctx.execution_history.clone(),
1709 prompt_history: phase_ctx.clone_prompt_history(),
1710 };
1711
1712 set_interrupt_context(context);
1713}
1714
1715fn defer_clear_interrupt_context() -> InterruptContextGuard {
1721 InterruptContextGuard
1722}
1723
1724struct InterruptContextGuard;
1730
1731impl Drop for InterruptContextGuard {
1732 fn drop(&mut self) {
1733 crate::interrupt::clear_interrupt_context();
1734 }
1735}
1736
1737fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1739 let prompt_validation = validate_prompt_md_with_workspace(
1740 &*ctx.workspace,
1741 ctx.config.behavior.strict_validation,
1742 ctx.args.interactive,
1743 );
1744 for err in &prompt_validation.errors {
1745 ctx.logger.error(err);
1746 }
1747 for warn in &prompt_validation.warnings {
1748 ctx.logger.warn(warn);
1749 }
1750 if !prompt_validation.is_valid() {
1751 anyhow::bail!("PROMPT.md validation errors");
1752 }
1753
1754 match create_prompt_backup_with_workspace(&*ctx.workspace) {
1756 Ok(None) => {}
1757 Ok(Some(warning)) => {
1758 ctx.logger.warn(&format!(
1759 "PROMPT.md backup created but: {warning}. Continuing anyway."
1760 ));
1761 }
1762 Err(e) => {
1763 ctx.logger.warn(&format!(
1764 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1765 ));
1766 }
1767 }
1768
1769 match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1771 None => {}
1772 Some(warning) => {
1773 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1774 }
1775 }
1776
1777 Ok(())
1778}
1779
1780fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1782 match PromptMonitor::new() {
1783 Ok(mut monitor) => {
1784 if let Err(e) = monitor.start() {
1785 ctx.logger.warn(&format!(
1786 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1787 ));
1788 None
1789 } else {
1790 if ctx.config.verbosity.is_debug() {
1791 ctx.logger.info("Started real-time PROMPT.md monitoring");
1792 }
1793 Some(monitor)
1794 }
1795 }
1796 Err(e) => {
1797 ctx.logger.warn(&format!(
1798 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1799 ));
1800 None
1801 }
1802 }
1803}
1804
1805fn print_review_guidelines(
1807 ctx: &PipelineContext,
1808 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1809) {
1810 if let Some(guidelines) = review_guidelines {
1811 ctx.logger.info(&format!(
1812 "Review guidelines: {}{}{}",
1813 ctx.colors.dim(),
1814 guidelines.summary(),
1815 ctx.colors.reset()
1816 ));
1817 }
1818}
1819
1820fn create_phase_context_with_config<'ctx>(
1822 ctx: &'ctx PipelineContext,
1823 config: &'ctx crate::config::Config,
1824 timer: &'ctx mut Timer,
1825 stats: &'ctx mut Stats,
1826 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1827 run_context: &'ctx crate::checkpoint::RunContext,
1828 resume_checkpoint: Option<&PipelineCheckpoint>,
1829) -> PhaseContext<'ctx> {
1830 let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1832 let exec_history = checkpoint
1833 .execution_history
1834 .clone()
1835 .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1836 let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1837 (exec_history, prompt_hist)
1838 } else {
1839 (
1840 crate::checkpoint::execution_history::ExecutionHistory::new(),
1841 std::collections::HashMap::new(),
1842 )
1843 };
1844
1845 PhaseContext {
1846 config,
1847 registry: &ctx.registry,
1848 logger: &ctx.logger,
1849 colors: &ctx.colors,
1850 timer,
1851 stats,
1852 developer_agent: &ctx.developer_agent,
1853 reviewer_agent: &ctx.reviewer_agent,
1854 review_guidelines,
1855 template_context: &ctx.template_context,
1856 run_context: run_context.clone(),
1857 execution_history,
1858 prompt_history,
1859 executor: &*ctx.executor,
1860 executor_arc: std::sync::Arc::clone(&ctx.executor),
1861 repo_root: &ctx.repo_root,
1862 workspace: &*ctx.workspace,
1863 }
1864}
1865
1866fn print_pipeline_info_with_config(ctx: &PipelineContext, _config: &crate::config::Config) {
1868 ctx.logger.info(&format!(
1869 "Working directory: {}{}{}",
1870 ctx.colors.cyan(),
1871 ctx.repo_root.display(),
1872 ctx.colors.reset()
1873 ));
1874}
1875
1876fn save_start_commit_or_warn(ctx: &PipelineContext) {
1880 #[cfg(feature = "test-utils")]
1883 {
1884 if ctx.config.verbosity.is_debug() {
1886 ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1887 }
1888 ctx.logger
1889 .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1890 }
1891
1892 #[cfg(not(feature = "test-utils"))]
1893 {
1894 match save_start_commit() {
1895 Ok(()) => {
1896 if ctx.config.verbosity.is_debug() {
1897 ctx.logger
1898 .info("Saved starting commit for incremental diff generation");
1899 }
1900 }
1901 Err(e) => {
1902 ctx.logger.warn(&format!(
1903 "Failed to save starting commit: {e}. \
1904 Incremental diffs may be unavailable as a result."
1905 ));
1906 ctx.logger.info(
1907 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
1908 );
1909 }
1910 }
1911
1912 match get_start_commit_summary() {
1914 Ok(summary) => {
1915 if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
1916 {
1917 ctx.logger.info(&summary.format_compact());
1918 if summary.is_stale {
1919 ctx.logger.warn(
1920 "Start commit is stale. Consider running: ralph --reset-start-commit",
1921 );
1922 } else if summary.commits_since > 5 {
1923 ctx.logger
1924 .info("Tip: Run 'ralph --show-baseline' for more details");
1925 }
1926 }
1927 }
1928 Err(e) => {
1929 if ctx.config.verbosity.is_debug() {
1931 ctx.logger
1932 .warn(&format!("Failed to get start commit summary: {e}"));
1933 }
1934 }
1935 }
1936 }
1937}
1938
1939fn check_prompt_restoration(
1941 ctx: &PipelineContext,
1942 prompt_monitor: &mut Option<PromptMonitor>,
1943 phase: &str,
1944) {
1945 if let Some(ref mut monitor) = prompt_monitor {
1946 if monitor.check_and_restore() {
1947 ctx.logger.warn(&format!(
1948 "PROMPT.md was deleted and restored during {phase} phase"
1949 ));
1950 }
1951 }
1952}
1953
1954pub fn handle_rebase_only(
1959 _args: &Args,
1960 config: &crate::config::Config,
1961 template_context: &TemplateContext,
1962 logger: &Logger,
1963 colors: Colors,
1964 executor: std::sync::Arc<dyn ProcessExecutor>,
1965 repo_root: &std::path::Path,
1966) -> anyhow::Result<()> {
1967 if is_main_or_master_branch()? {
1969 logger.warn("Already on main/master branch - rebasing on main is not recommended");
1970 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
1971 logger.info(" git worktree add ../feature-branch feature-branch");
1972 logger.info("This allows multiple AI agents to work on different features simultaneously.");
1973 logger.info("Proceeding with rebase anyway as requested...");
1974 }
1975
1976 logger.header("Rebase to default branch", Colors::cyan);
1977
1978 match run_rebase_to_default(logger, colors, &*executor) {
1979 Ok(RebaseResult::Success) => {
1980 logger.success("Rebase completed successfully");
1981 Ok(())
1982 }
1983 Ok(RebaseResult::NoOp { reason }) => {
1984 logger.info(&format!("No rebase needed: {reason}"));
1985 Ok(())
1986 }
1987 Ok(RebaseResult::Failed(err)) => {
1988 logger.error(&format!("Rebase failed: {err}"));
1989 anyhow::bail!("Rebase failed: {err}")
1990 }
1991 Ok(RebaseResult::Conflicts(_conflicts)) => {
1992 let conflicted_files = get_conflicted_files()?;
1994 if conflicted_files.is_empty() {
1995 logger.warn("Rebase reported conflicts but no conflicted files found");
1996 let _ = abort_rebase(&*executor);
1997 return Ok(());
1998 }
1999
2000 logger.warn(&format!(
2001 "Rebase resulted in {} conflict(s), attempting AI resolution",
2002 conflicted_files.len()
2003 ));
2004
2005 match try_resolve_conflicts_without_phase_ctx(
2007 &conflicted_files,
2008 config,
2009 template_context,
2010 logger,
2011 colors,
2012 std::sync::Arc::clone(&executor),
2013 repo_root,
2014 ) {
2015 Ok(true) => {
2016 logger.info("Continuing rebase after conflict resolution");
2018 match continue_rebase(&*executor) {
2019 Ok(()) => {
2020 logger.success("Rebase completed successfully after AI resolution");
2021 Ok(())
2022 }
2023 Err(e) => {
2024 logger.error(&format!("Failed to continue rebase: {e}"));
2025 let _ = abort_rebase(&*executor);
2026 anyhow::bail!("Rebase failed after conflict resolution")
2027 }
2028 }
2029 }
2030 Ok(false) => {
2031 logger.error("AI conflict resolution failed, aborting rebase");
2033 let _ = abort_rebase(&*executor);
2034 anyhow::bail!("Rebase conflicts could not be resolved by AI")
2035 }
2036 Err(e) => {
2037 logger.error(&format!("Conflict resolution error: {e}"));
2038 let _ = abort_rebase(&*executor);
2039 anyhow::bail!("Rebase conflict resolution failed: {e}")
2040 }
2041 }
2042 }
2043 Err(e) => {
2044 logger.error(&format!("Rebase failed: {e}"));
2045 anyhow::bail!("Rebase failed: {e}")
2046 }
2047 }
2048}
2049
2050fn run_rebase_to_default(
2063 logger: &Logger,
2064 colors: Colors,
2065 executor: &dyn ProcessExecutor,
2066) -> std::io::Result<RebaseResult> {
2067 let default_branch = get_default_branch()?;
2069 logger.info(&format!(
2070 "Rebasing onto {}{}{}",
2071 colors.cyan(),
2072 default_branch,
2073 colors.reset()
2074 ));
2075
2076 rebase_onto(&default_branch, executor)
2078}
2079
2080fn run_initial_rebase(
2094 ctx: &PipelineContext,
2095 phase_ctx: &mut PhaseContext<'_>,
2096 run_context: &crate::checkpoint::RunContext,
2097 executor: &dyn ProcessExecutor,
2098) -> anyhow::Result<()> {
2099 ctx.logger.header("Pre-development rebase", Colors::cyan);
2100
2101 let step = ExecutionStep::new(
2103 "PreRebase",
2104 0,
2105 "pre_rebase_start",
2106 StepOutcome::success(None, vec![]),
2107 );
2108 phase_ctx.execution_history.add_step(step);
2109
2110 if ctx.config.features.checkpoint_enabled {
2112 let default_branch = get_default_branch().unwrap_or_else(|_| "main".to_string());
2113 let mut builder = CheckpointBuilder::new()
2114 .phase(PipelinePhase::PreRebase, 0, ctx.config.developer_iters)
2115 .reviewer_pass(0, ctx.config.reviewer_reviews)
2116 .capture_from_context(
2117 &ctx.config,
2118 &ctx.registry,
2119 &ctx.developer_agent,
2120 &ctx.reviewer_agent,
2121 &ctx.logger,
2122 run_context,
2123 )
2124 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2125
2126 builder = builder
2128 .with_execution_history(phase_ctx.execution_history.clone())
2129 .with_prompt_history(phase_ctx.clone_prompt_history());
2130
2131 if let Some(mut checkpoint) = builder.build() {
2132 checkpoint.rebase_state = RebaseState::PreRebaseInProgress {
2133 upstream_branch: default_branch,
2134 };
2135 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2136 }
2137 }
2138
2139 match run_rebase_to_default(&ctx.logger, ctx.colors, &*ctx.executor) {
2140 Ok(RebaseResult::Success) => {
2141 ctx.logger.success("Rebase completed successfully");
2142 let step = ExecutionStep::new(
2144 "PreRebase",
2145 0,
2146 "pre_rebase_complete",
2147 StepOutcome::success(None, vec![]),
2148 );
2149 phase_ctx.execution_history.add_step(step);
2150
2151 if ctx.config.features.checkpoint_enabled {
2153 let builder = CheckpointBuilder::new()
2154 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2155 .reviewer_pass(0, ctx.config.reviewer_reviews)
2156 .skip_rebase(true) .capture_from_context(
2158 &ctx.config,
2159 &ctx.registry,
2160 &ctx.developer_agent,
2161 &ctx.reviewer_agent,
2162 &ctx.logger,
2163 run_context,
2164 )
2165 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2166 .with_execution_history(phase_ctx.execution_history.clone())
2167 .with_prompt_history(phase_ctx.clone_prompt_history());
2168
2169 if let Some(checkpoint) = builder.build() {
2170 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2171 }
2172 }
2173
2174 Ok(())
2175 }
2176 Ok(RebaseResult::NoOp { reason }) => {
2177 ctx.logger.info(&format!("No rebase needed: {reason}"));
2178 let step = ExecutionStep::new(
2180 "PreRebase",
2181 0,
2182 "pre_rebase_skipped",
2183 StepOutcome::skipped(reason.clone()),
2184 );
2185 phase_ctx.execution_history.add_step(step);
2186
2187 if ctx.config.features.checkpoint_enabled {
2189 let builder = CheckpointBuilder::new()
2190 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2191 .reviewer_pass(0, ctx.config.reviewer_reviews)
2192 .skip_rebase(true) .capture_from_context(
2194 &ctx.config,
2195 &ctx.registry,
2196 &ctx.developer_agent,
2197 &ctx.reviewer_agent,
2198 &ctx.logger,
2199 run_context,
2200 )
2201 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
2202 .with_execution_history(phase_ctx.execution_history.clone())
2203 .with_prompt_history(phase_ctx.clone_prompt_history());
2204
2205 if let Some(checkpoint) = builder.build() {
2206 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2207 }
2208 }
2209
2210 Ok(())
2211 }
2212 Ok(RebaseResult::Conflicts(_conflicts)) => {
2213 let conflicted_files = get_conflicted_files()?;
2215 if conflicted_files.is_empty() {
2216 ctx.logger
2217 .warn("Rebase reported conflicts but no conflicted files found");
2218 let _ = abort_rebase(executor);
2219 return Ok(());
2220 }
2221
2222 let step = ExecutionStep::new(
2224 "PreRebase",
2225 0,
2226 "pre_rebase_conflict",
2227 StepOutcome::partial(
2228 "Rebase started".to_string(),
2229 format!("{} conflicts detected", conflicted_files.len()),
2230 ),
2231 );
2232 phase_ctx.execution_history.add_step(step);
2233
2234 if ctx.config.features.checkpoint_enabled {
2236 let mut builder = CheckpointBuilder::new()
2237 .phase(
2238 PipelinePhase::PreRebaseConflict,
2239 0,
2240 ctx.config.developer_iters,
2241 )
2242 .reviewer_pass(0, ctx.config.reviewer_reviews)
2243 .capture_from_context(
2244 &ctx.config,
2245 &ctx.registry,
2246 &ctx.developer_agent,
2247 &ctx.reviewer_agent,
2248 &ctx.logger,
2249 run_context,
2250 )
2251 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
2252
2253 builder = builder
2255 .with_execution_history(phase_ctx.execution_history.clone())
2256 .with_prompt_history(phase_ctx.clone_prompt_history());
2257
2258 if let Some(mut checkpoint) = builder.build() {
2259 checkpoint.rebase_state = RebaseState::HasConflicts {
2260 files: conflicted_files.clone(),
2261 };
2262 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
2263 }
2264 }
2265
2266 ctx.logger.warn(&format!(
2267 "Rebase resulted in {} conflict(s), attempting AI resolution",
2268 conflicted_files.len()
2269 ));
2270
2271 let resolution_ctx = ConflictResolutionContext {
2273 config: &ctx.config,
2274 template_context: &ctx.template_context,
2275 logger: &ctx.logger,
2276 colors: ctx.colors,
2277 executor_arc: std::sync::Arc::clone(&ctx.executor),
2278 workspace: &*ctx.workspace,
2279 };
2280 match try_resolve_conflicts_with_fallback(
2281 &conflicted_files,
2282 resolution_ctx,
2283 phase_ctx,
2284 "PreRebase",
2285 &*ctx.executor,
2286 ) {
2287 Ok(true) => {
2288 ctx.logger
2290 .info("Continuing rebase after conflict resolution");
2291 match continue_rebase(executor) {
2292 Ok(()) => {
2293 ctx.logger
2294 .success("Rebase completed successfully after AI resolution");
2295 let step = ExecutionStep::new(
2297 "PreRebase",
2298 0,
2299 "pre_rebase_resolution",
2300 StepOutcome::success(None, vec![]),
2301 );
2302 phase_ctx.execution_history.add_step(step);
2303
2304 if ctx.config.features.checkpoint_enabled {
2306 let builder = CheckpointBuilder::new()
2307 .phase(PipelinePhase::Planning, 0, ctx.config.developer_iters)
2308 .reviewer_pass(0, ctx.config.reviewer_reviews)
2309 .skip_rebase(true) .capture_from_context(
2311 &ctx.config,
2312 &ctx.registry,
2313 &ctx.developer_agent,
2314 &ctx.reviewer_agent,
2315 &ctx.logger,
2316 run_context,
2317 )
2318 .with_executor_from_context(std::sync::Arc::clone(
2319 &ctx.executor,
2320 ))
2321 .with_execution_history(phase_ctx.execution_history.clone())
2322 .with_prompt_history(phase_ctx.clone_prompt_history());
2323
2324 if let Some(checkpoint) = builder.build() {
2325 let _ = save_checkpoint_with_workspace(
2326 &*ctx.workspace,
2327 &checkpoint,
2328 );
2329 }
2330 }
2331
2332 Ok(())
2333 }
2334 Err(e) => {
2335 ctx.logger.warn(&format!("Failed to continue rebase: {e}"));
2336 let _ = abort_rebase(executor);
2337 let step = ExecutionStep::new(
2339 "PreRebase",
2340 0,
2341 "pre_rebase_resolution",
2342 StepOutcome::partial(
2343 "Conflicts resolved by AI".to_string(),
2344 format!("Failed to continue rebase: {e}"),
2345 ),
2346 );
2347 phase_ctx.execution_history.add_step(step);
2348 Ok(()) }
2350 }
2351 }
2352 Ok(false) => {
2353 ctx.logger
2355 .warn("AI conflict resolution failed, aborting rebase");
2356 let _ = abort_rebase(executor);
2357 let step = ExecutionStep::new(
2359 "PreRebase",
2360 0,
2361 "pre_rebase_resolution",
2362 StepOutcome::failure("AI conflict resolution failed".to_string(), true),
2363 );
2364 phase_ctx.execution_history.add_step(step);
2365 Ok(()) }
2367 Err(e) => {
2368 ctx.logger.error(&format!("Conflict resolution error: {e}"));
2369 let _ = abort_rebase(executor);
2370 let step = ExecutionStep::new(
2372 "PreRebase",
2373 0,
2374 "pre_rebase_resolution",
2375 StepOutcome::failure(format!("Conflict resolution error: {e}"), true),
2376 );
2377 phase_ctx.execution_history.add_step(step);
2378 Ok(()) }
2380 }
2381 }
2382 Ok(RebaseResult::Failed(err)) => {
2383 ctx.logger.error(&format!("Rebase failed: {err}"));
2384 let _ = abort_rebase(&*ctx.executor);
2385 let step = ExecutionStep::new(
2387 "PreRebase",
2388 0,
2389 "pre_rebase_failed",
2390 StepOutcome::failure(format!("Rebase failed: {err}"), true),
2391 );
2392 phase_ctx.execution_history.add_step(step);
2393 Ok(()) }
2395 Err(e) => {
2396 ctx.logger
2397 .warn(&format!("Rebase failed, continuing without rebase: {e}"));
2398 let step = ExecutionStep::new(
2400 "PreRebase",
2401 0,
2402 "pre_rebase_error",
2403 StepOutcome::failure(format!("Rebase error: {e}"), true),
2404 );
2405 phase_ctx.execution_history.add_step(step);
2406 Ok(())
2407 }
2408 }
2409}
2410
2411enum ConflictResolutionResult {
2415 WithJson(String),
2417 FileEditsOnly,
2419 Failed,
2421}
2422
2423struct ConflictResolutionContext<'a> {
2428 config: &'a crate::config::Config,
2429 template_context: &'a TemplateContext,
2430 logger: &'a Logger,
2431 colors: Colors,
2432 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2433 workspace: &'a dyn crate::workspace::Workspace,
2434}
2435
2436fn try_resolve_conflicts_with_fallback(
2441 conflicted_files: &[String],
2442 ctx: ConflictResolutionContext<'_>,
2443 phase_ctx: &mut PhaseContext<'_>,
2444 phase: &str,
2445 executor: &dyn ProcessExecutor,
2446) -> anyhow::Result<bool> {
2447 if conflicted_files.is_empty() {
2448 return Ok(false);
2449 }
2450
2451 ctx.logger.info(&format!(
2452 "Attempting AI conflict resolution for {} file(s)",
2453 conflicted_files.len()
2454 ));
2455
2456 let conflicts = collect_conflict_info_or_error(conflicted_files, ctx.logger)?;
2457
2458 let prompt_key = format!("{}_conflict_resolution", phase.to_lowercase());
2461 let (resolution_prompt, was_replayed) =
2462 get_stored_or_generate_prompt(&prompt_key, &phase_ctx.prompt_history, || {
2463 build_resolution_prompt(&conflicts, ctx.template_context)
2464 });
2465
2466 if !was_replayed {
2468 phase_ctx.capture_prompt(&prompt_key, &resolution_prompt);
2469 } else {
2470 ctx.logger.info(&format!(
2471 "Using stored prompt from checkpoint for determinism: {}",
2472 prompt_key
2473 ));
2474 }
2475
2476 match run_ai_conflict_resolution(
2477 &resolution_prompt,
2478 ctx.config,
2479 ctx.logger,
2480 ctx.colors,
2481 std::sync::Arc::clone(&ctx.executor_arc),
2482 ctx.workspace,
2483 ) {
2484 Ok(ConflictResolutionResult::WithJson(resolved_content)) => {
2485 match parse_and_validate_resolved_files(&resolved_content, ctx.logger) {
2488 Ok(resolved_files) => {
2489 write_resolved_files(&resolved_files, ctx.logger)?;
2490 }
2491 Err(_) => {
2492 }
2496 }
2497
2498 let remaining_conflicts = get_conflicted_files()?;
2500 if remaining_conflicts.is_empty() {
2501 Ok(true)
2502 } else {
2503 ctx.logger.warn(&format!(
2504 "{} conflicts remain after AI resolution",
2505 remaining_conflicts.len()
2506 ));
2507 Ok(false)
2508 }
2509 }
2510 Ok(ConflictResolutionResult::FileEditsOnly) => {
2511 ctx.logger
2513 .info("Agent resolved conflicts via file edits (no JSON output)");
2514
2515 let remaining_conflicts = get_conflicted_files()?;
2517 if remaining_conflicts.is_empty() {
2518 ctx.logger.success("All conflicts resolved via file edits");
2519 Ok(true)
2520 } else {
2521 ctx.logger.warn(&format!(
2522 "{} conflicts remain after AI resolution",
2523 remaining_conflicts.len()
2524 ));
2525 Ok(false)
2526 }
2527 }
2528 Ok(ConflictResolutionResult::Failed) => {
2529 ctx.logger.warn("AI conflict resolution failed");
2530 ctx.logger.info("Attempting to continue rebase anyway...");
2531
2532 match crate::git_helpers::continue_rebase(executor) {
2534 Ok(()) => {
2535 ctx.logger.info("Successfully continued rebase");
2536 Ok(true)
2537 }
2538 Err(rebase_err) => {
2539 ctx.logger
2540 .warn(&format!("Failed to continue rebase: {rebase_err}"));
2541 Ok(false) }
2543 }
2544 }
2545 Err(e) => {
2546 ctx.logger
2547 .warn(&format!("AI conflict resolution failed: {e}"));
2548 ctx.logger.info("Attempting to continue rebase anyway...");
2549
2550 match crate::git_helpers::continue_rebase(executor) {
2552 Ok(()) => {
2553 ctx.logger.info("Successfully continued rebase");
2554 Ok(true)
2555 }
2556 Err(rebase_err) => {
2557 ctx.logger
2558 .warn(&format!("Failed to continue rebase: {rebase_err}"));
2559 Ok(false) }
2561 }
2562 }
2563 }
2564}
2565
2566fn try_resolve_conflicts_without_phase_ctx(
2571 conflicted_files: &[String],
2572 config: &crate::config::Config,
2573 template_context: &TemplateContext,
2574 logger: &Logger,
2575 colors: Colors,
2576 executor: std::sync::Arc<dyn ProcessExecutor>,
2577 repo_root: &std::path::Path,
2578) -> anyhow::Result<bool> {
2579 use crate::agents::AgentRegistry;
2580 use crate::checkpoint::execution_history::ExecutionHistory;
2581 use crate::checkpoint::RunContext;
2582 use crate::pipeline::{Stats, Timer};
2583
2584 let registry = AgentRegistry::new()?;
2586 let mut timer = Timer::new();
2587 let mut stats = Stats::default();
2588 let workspace = crate::workspace::WorkspaceFs::new(repo_root.to_path_buf());
2589
2590 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2591 let developer_agent = config.developer_agent.as_deref().unwrap_or("codex");
2592
2593 let executor_arc = std::sync::Arc::clone(&executor);
2595
2596 let mut phase_ctx = PhaseContext {
2597 config,
2598 registry: ®istry,
2599 logger,
2600 colors: &colors,
2601 timer: &mut timer,
2602 stats: &mut stats,
2603 developer_agent,
2604 reviewer_agent,
2605 review_guidelines: None,
2606 template_context,
2607 run_context: RunContext::new(),
2608 execution_history: ExecutionHistory::new(),
2609 prompt_history: std::collections::HashMap::new(),
2610 executor: &*executor,
2611 executor_arc: std::sync::Arc::clone(&executor_arc),
2612 repo_root,
2613 workspace: &workspace,
2614 };
2615
2616 let ctx = ConflictResolutionContext {
2617 config,
2618 template_context,
2619 logger,
2620 colors,
2621 executor_arc,
2622 workspace: &workspace,
2623 };
2624
2625 try_resolve_conflicts_with_fallback(
2626 conflicted_files,
2627 ctx,
2628 &mut phase_ctx,
2629 "RebaseOnly",
2630 &*executor,
2631 )
2632}
2633
2634fn collect_conflict_info_or_error(
2636 conflicted_files: &[String],
2637 logger: &Logger,
2638) -> anyhow::Result<std::collections::HashMap<String, crate::prompts::FileConflict>> {
2639 use crate::prompts::collect_conflict_info;
2640
2641 let conflicts = match collect_conflict_info(conflicted_files) {
2642 Ok(c) => c,
2643 Err(e) => {
2644 logger.error(&format!("Failed to collect conflict info: {e}"));
2645 anyhow::bail!("Failed to collect conflict info");
2646 }
2647 };
2648 Ok(conflicts)
2649}
2650
2651fn build_resolution_prompt(
2653 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2654 template_context: &TemplateContext,
2655) -> String {
2656 build_enhanced_resolution_prompt(conflicts, None::<()>, template_context)
2657 .unwrap_or_else(|_| String::new())
2658}
2659
2660fn build_enhanced_resolution_prompt(
2664 conflicts: &std::collections::HashMap<String, crate::prompts::FileConflict>,
2665 _branch_info: Option<()>, template_context: &TemplateContext,
2667) -> anyhow::Result<String> {
2668 use std::fs;
2669
2670 let prompt_md_content = fs::read_to_string("PROMPT.md").ok();
2671 let plan_content = fs::read_to_string(".agent/PLAN.md").ok();
2672
2673 Ok(
2675 crate::prompts::build_conflict_resolution_prompt_with_context(
2676 template_context,
2677 conflicts,
2678 prompt_md_content.as_deref(),
2679 plan_content.as_deref(),
2680 ),
2681 )
2682}
2683
2684fn run_ai_conflict_resolution(
2689 resolution_prompt: &str,
2690 config: &crate::config::Config,
2691 logger: &Logger,
2692 colors: Colors,
2693 executor_arc: std::sync::Arc<dyn crate::executor::ProcessExecutor>,
2694 workspace: &dyn crate::workspace::Workspace,
2695) -> anyhow::Result<ConflictResolutionResult> {
2696 use crate::agents::AgentRegistry;
2697 use crate::files::result_extraction::extract_last_result;
2698 use crate::pipeline::{
2699 run_with_fallback_and_validator, FallbackConfig, OutputValidator, PipelineRuntime,
2700 };
2701 use std::io;
2702 use std::path::Path;
2703
2704 let log_dir = ".agent/logs/rebase_conflict_resolution";
2708
2709 let registry = AgentRegistry::new()?;
2710 let reviewer_agent = config.reviewer_agent.as_deref().unwrap_or("codex");
2711
2712 let executor_ref: &dyn crate::executor::ProcessExecutor = &*executor_arc;
2714 let mut runtime = PipelineRuntime {
2715 timer: &mut crate::pipeline::Timer::new(),
2716 logger,
2717 colors: &colors,
2718 config,
2719 executor: executor_ref,
2720 executor_arc: std::sync::Arc::clone(&executor_arc),
2721 workspace,
2722 };
2723
2724 let validate_output: OutputValidator = |ws: &dyn crate::workspace::Workspace,
2727 log_dir_path: &Path,
2728 validation_logger: &crate::logger::Logger|
2729 -> io::Result<bool> {
2730 match extract_last_result(ws, log_dir_path) {
2731 Ok(Some(_)) => {
2732 Ok(true)
2734 }
2735 Ok(None) => {
2736 match crate::git_helpers::get_conflicted_files() {
2739 Ok(conflicts) if conflicts.is_empty() => {
2740 validation_logger
2741 .info("Agent resolved conflicts without JSON output (file edits only)");
2742 Ok(true) }
2744 Ok(conflicts) => {
2745 validation_logger.warn(&format!(
2746 "{} conflict(s) remain unresolved",
2747 conflicts.len()
2748 ));
2749 Ok(false) }
2751 Err(e) => {
2752 validation_logger.warn(&format!("Failed to check for conflicts: {e}"));
2753 Ok(false) }
2755 }
2756 }
2757 Err(e) => {
2758 validation_logger.warn(&format!("Output validation check failed: {e}"));
2759 Ok(false) }
2761 }
2762 };
2763
2764 let mut fallback_config = FallbackConfig {
2765 role: crate::agents::AgentRole::Reviewer,
2766 base_label: "conflict resolution",
2767 prompt: resolution_prompt,
2768 logfile_prefix: log_dir,
2769 runtime: &mut runtime,
2770 registry: ®istry,
2771 primary_agent: reviewer_agent,
2772 output_validator: Some(validate_output),
2773 workspace,
2774 };
2775
2776 let exit_code = run_with_fallback_and_validator(&mut fallback_config)?;
2777
2778 if exit_code != 0 {
2779 return Ok(ConflictResolutionResult::Failed);
2780 }
2781
2782 let remaining_conflicts = crate::git_helpers::get_conflicted_files()?;
2785
2786 if remaining_conflicts.is_empty() {
2787 match extract_last_result(workspace, Path::new(log_dir)) {
2789 Ok(Some(content)) => {
2790 logger.info("Agent provided JSON output with resolved files");
2791 Ok(ConflictResolutionResult::WithJson(content))
2792 }
2793 Ok(None) => {
2794 logger.info("Agent resolved conflicts via file edits (no JSON output)");
2795 Ok(ConflictResolutionResult::FileEditsOnly)
2796 }
2797 Err(e) => {
2798 logger.warn(&format!(
2800 "Failed to extract JSON output but conflicts are resolved: {e}"
2801 ));
2802 Ok(ConflictResolutionResult::FileEditsOnly)
2803 }
2804 }
2805 } else {
2806 logger.warn(&format!(
2807 "{} conflict(s) remain after agent attempted resolution",
2808 remaining_conflicts.len()
2809 ));
2810 Ok(ConflictResolutionResult::Failed)
2811 }
2812}
2813
2814fn parse_and_validate_resolved_files(
2820 resolved_content: &str,
2821 logger: &Logger,
2822) -> anyhow::Result<serde_json::Map<String, serde_json::Value>> {
2823 let json: serde_json::Value = serde_json::from_str(resolved_content).map_err(|_e| {
2824 anyhow::anyhow!("Agent did not provide JSON output (will verify via Git state)")
2827 })?;
2828
2829 let resolved_files = match json.get("resolved_files") {
2830 Some(v) if v.is_object() => v.as_object().unwrap(),
2831 _ => {
2832 logger.info("Agent output missing 'resolved_files' object");
2833 anyhow::bail!("Agent output missing 'resolved_files' object");
2834 }
2835 };
2836
2837 if resolved_files.is_empty() {
2838 logger.info("No resolved files in JSON output");
2839 anyhow::bail!("No files were resolved by the agent");
2840 }
2841
2842 Ok(resolved_files.clone())
2843}
2844
2845fn write_resolved_files(
2847 resolved_files: &serde_json::Map<String, serde_json::Value>,
2848 logger: &Logger,
2849) -> anyhow::Result<usize> {
2850 use std::fs;
2851
2852 let mut files_written = 0;
2853 for (path, content) in resolved_files {
2854 if let Some(content_str) = content.as_str() {
2855 fs::write(path, content_str).map_err(|e| {
2856 logger.error(&format!("Failed to write {path}: {e}"));
2857 anyhow::anyhow!("Failed to write {path}: {e}")
2858 })?;
2859 logger.info(&format!("Resolved and wrote: {path}"));
2860 files_written += 1;
2861 if let Err(e) = crate::git_helpers::git_add_all() {
2863 logger.warn(&format!("Failed to stage {path}: {e}"));
2864 }
2865 }
2866 }
2867
2868 logger.success(&format!("Successfully resolved {files_written} file(s)"));
2869 Ok(files_written)
2870}