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;
30mod rebase;
31pub mod resume;
32pub mod validation;
33
34use crate::agents::AgentRegistry;
35use crate::app::finalization::finalize_pipeline;
36use crate::banner::print_welcome_banner;
37use crate::checkpoint::{
38 save_checkpoint_with_workspace, CheckpointBuilder, PipelineCheckpoint, PipelinePhase,
39};
40use crate::cli::{
41 create_prompt_from_template, handle_diagnose, handle_dry_run, handle_list_agents,
42 handle_list_available_agents, handle_list_providers, handle_show_baseline,
43 handle_template_commands, prompt_template_selection, Args,
44};
45
46use crate::executor::ProcessExecutor;
47use crate::files::protection::monitoring::PromptMonitor;
48use crate::files::{
49 create_prompt_backup_with_workspace, make_prompt_read_only_with_workspace,
50 update_status_with_workspace, validate_prompt_md_with_workspace,
51};
52use crate::git_helpers::{
53 abort_rebase, continue_rebase, get_conflicted_files, is_main_or_master_branch,
54 reset_start_commit, RebaseResult,
55};
56#[cfg(not(feature = "test-utils"))]
57use crate::git_helpers::{
58 cleanup_orphaned_marker, get_start_commit_summary, save_start_commit, start_agent_phase,
59};
60use crate::logger::Colors;
61use crate::logger::Logger;
62use crate::phases::PhaseContext;
63use crate::pipeline::{AgentPhaseGuard, Stats, Timer};
64use crate::prompts::template_context::TemplateContext;
65
66use config_init::initialize_config;
67use context::PipelineContext;
68use detection::detect_project_stack;
69use plumbing::handle_generate_commit_msg;
70use rebase::{run_initial_rebase, run_rebase_to_default, try_resolve_conflicts_without_phase_ctx};
71use resume::{handle_resume_with_validation, offer_resume_if_checkpoint_exists};
72use validation::{
73 resolve_required_agents, validate_agent_chains, validate_agent_commands, validate_can_commit,
74};
75
76fn discover_repo_root_for_workspace<H: effect::AppEffectHandler>(
77 override_dir: Option<&std::path::Path>,
78 handler: &mut H,
79) -> anyhow::Result<std::path::PathBuf> {
80 use effect::{AppEffect, AppEffectResult};
81
82 if let Some(dir) = override_dir {
83 match handler.execute(AppEffect::SetCurrentDir {
84 path: dir.to_path_buf(),
85 }) {
86 AppEffectResult::Ok => {}
87 AppEffectResult::Error(e) => anyhow::bail!(e),
88 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
89 }
90 }
91
92 match handler.execute(AppEffect::GitRequireRepo) {
93 AppEffectResult::Ok => {}
94 AppEffectResult::Error(e) => anyhow::bail!("Not in a git repository: {e}"),
95 other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
96 }
97
98 match handler.execute(AppEffect::GitGetRepoRoot) {
99 AppEffectResult::Path(p) => Ok(p),
100 AppEffectResult::Error(e) => anyhow::bail!("Failed to get repo root: {e}"),
101 other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
102 }
103}
104
105pub fn run(args: Args, executor: std::sync::Arc<dyn ProcessExecutor>) -> anyhow::Result<()> {
125 let colors = Colors::new();
126 let logger = Logger::new(colors);
127
128 if let Some(ref override_dir) = args.working_dir_override {
131 std::env::set_current_dir(override_dir)?;
132 }
133
134 let Some(init_result) = initialize_config(&args, colors, &logger)? else {
136 return Ok(()); };
138
139 let config_init::ConfigInitResult {
140 config,
141 registry,
142 config_path,
143 config_sources,
144 } = init_result;
145
146 let validated = resolve_required_agents(&config)?;
148 let developer_agent = validated.developer_agent;
149 let reviewer_agent = validated.reviewer_agent;
150
151 if handle_listing_commands(&args, ®istry, colors) {
153 return Ok(());
154 }
155
156 if args.recovery.diagnose {
158 handle_diagnose(
159 colors,
160 &config,
161 ®istry,
162 &config_path,
163 &config_sources,
164 &*executor,
165 );
166 return Ok(());
167 }
168
169 validate_agent_chains(®istry, colors);
171
172 let mut handler = effect_handler::RealAppEffectHandler::new();
174
175 let early_repo_root =
178 discover_repo_root_for_workspace(args.working_dir_override.as_deref(), &mut handler)?;
179
180 let workspace: std::sync::Arc<dyn crate::workspace::Workspace> =
182 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(early_repo_root));
183
184 if handle_plumbing_commands(
186 &args,
187 &logger,
188 colors,
189 &mut handler,
190 Some(workspace.as_ref()),
191 )? {
192 return Ok(());
193 }
194
195 let Some(repo_root) = validate_and_setup_agents(
199 AgentSetupParams {
200 config: &config,
201 registry: ®istry,
202 developer_agent: &developer_agent,
203 reviewer_agent: &reviewer_agent,
204 config_path: &config_path,
205 colors,
206 logger: &logger,
207 working_dir_override: args.working_dir_override.as_deref(),
208 },
209 &mut handler,
210 )?
211 else {
212 return Ok(());
213 };
214
215 (prepare_pipeline_or_exit(PipelinePreparationParams {
218 args,
219 config,
220 registry,
221 developer_agent,
222 reviewer_agent,
223 repo_root,
224 logger,
225 colors,
226 executor,
227 handler: &mut handler,
228 workspace,
229 })?)
230 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::app::effect::{AppEffect, AppEffectHandler, AppEffectResult};
237
238 #[derive(Debug)]
239 struct TestRepoRootHandler {
240 captured: Vec<AppEffect>,
241 repo_root: std::path::PathBuf,
242 }
243
244 impl TestRepoRootHandler {
245 fn new(repo_root: std::path::PathBuf) -> Self {
246 Self {
247 captured: Vec::new(),
248 repo_root,
249 }
250 }
251 }
252
253 impl AppEffectHandler for TestRepoRootHandler {
254 fn execute(&mut self, effect: AppEffect) -> AppEffectResult {
255 self.captured.push(effect.clone());
256 match effect {
257 AppEffect::SetCurrentDir { .. } => AppEffectResult::Ok,
258 AppEffect::GitRequireRepo => AppEffectResult::Ok,
259 AppEffect::GitGetRepoRoot => AppEffectResult::Path(self.repo_root.clone()),
260 other => panic!("unexpected effect in test handler: {other:?}"),
261 }
262 }
263 }
264
265 #[test]
266 fn discover_repo_root_for_workspace_prefers_git_repo_root_over_override_dir() {
267 let override_dir = std::path::PathBuf::from("/override/subdir");
268 let repo_root = std::path::PathBuf::from("/repo");
269 let mut handler = TestRepoRootHandler::new(repo_root.clone());
270
271 let got = discover_repo_root_for_workspace(Some(&override_dir), &mut handler).unwrap();
272 assert_eq!(got, repo_root);
273
274 assert!(matches!(
275 handler.captured.get(0),
276 Some(AppEffect::SetCurrentDir { .. })
277 ));
278 assert!(handler
279 .captured
280 .iter()
281 .any(|e| matches!(e, AppEffect::GitRequireRepo)));
282 assert!(handler
283 .captured
284 .iter()
285 .any(|e| matches!(e, AppEffect::GitGetRepoRoot)));
286 }
287}
288
289#[cfg(feature = "test-utils")]
309pub fn run_with_config(
310 args: Args,
311 executor: std::sync::Arc<dyn ProcessExecutor>,
312 config: crate::config::Config,
313 registry: AgentRegistry,
314) -> anyhow::Result<()> {
315 let mut handler = effect_handler::RealAppEffectHandler::new();
317 run_with_config_and_resolver(
318 args,
319 executor,
320 config,
321 registry,
322 &crate::config::RealConfigEnvironment,
323 &mut handler,
324 None, )
326}
327
328#[cfg(feature = "test-utils")]
351pub fn run_with_config_and_resolver<
352 P: crate::config::ConfigEnvironment,
353 H: effect::AppEffectHandler,
354>(
355 args: Args,
356 executor: std::sync::Arc<dyn ProcessExecutor>,
357 config: crate::config::Config,
358 registry: AgentRegistry,
359 path_resolver: &P,
360 handler: &mut H,
361 workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
362) -> anyhow::Result<()> {
363 use crate::cli::{
364 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
365 handle_list_work_guides, handle_smart_init_with,
366 };
367
368 let colors = Colors::new();
369 let logger = Logger::new(colors);
370
371 if let Some(ref override_dir) = args.working_dir_override {
373 std::env::set_current_dir(override_dir)?;
374 }
375
376 if args.recovery.extended_help {
378 handle_extended_help();
379 if args.work_guide_list.list_work_guides {
380 println!();
381 handle_list_work_guides(colors);
382 }
383 return Ok(());
384 }
385
386 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
388 return Ok(());
389 }
390
391 if let Some(ref template_name) = args.init_prompt {
393 if handle_init_prompt_with(
394 template_name,
395 args.unified_init.force_init,
396 colors,
397 path_resolver,
398 )? {
399 return Ok(());
400 }
401 }
402
403 if args.unified_init.init.is_some()
405 && handle_smart_init_with(
406 args.unified_init.init.as_deref(),
407 args.unified_init.force_init,
408 colors,
409 path_resolver,
410 )?
411 {
412 return Ok(());
413 }
414
415 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
417 return Ok(());
418 }
419
420 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
422 return Ok(());
423 }
424
425 if args.legacy_init.init_legacy {
427 let repo_root = match handler.execute(effect::AppEffect::GitGetRepoRoot) {
428 effect::AppEffectResult::Path(p) => Some(p),
429 _ => None,
430 };
431 let legacy_path = repo_root.map_or_else(
432 || std::path::PathBuf::from(".agent/agents.toml"),
433 |root| root.join(".agent/agents.toml"),
434 );
435 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
436 return Ok(());
437 }
438 }
439
440 let config_path = std::path::PathBuf::from("test-config");
442
443 let validated = resolve_required_agents(&config)?;
445 let developer_agent = validated.developer_agent;
446 let reviewer_agent = validated.reviewer_agent;
447
448 if handle_listing_commands(&args, ®istry, colors) {
450 return Ok(());
451 }
452
453 if args.recovery.diagnose {
455 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
456 return Ok(());
457 }
458
459 if handle_plumbing_commands(
462 &args,
463 &logger,
464 colors,
465 handler,
466 workspace.as_ref().map(|w| w.as_ref()),
467 )? {
468 return Ok(());
469 }
470
471 let Some(repo_root) = validate_and_setup_agents(
473 AgentSetupParams {
474 config: &config,
475 registry: ®istry,
476 developer_agent: &developer_agent,
477 reviewer_agent: &reviewer_agent,
478 config_path: &config_path,
479 colors,
480 logger: &logger,
481 working_dir_override: args.working_dir_override.as_deref(),
482 },
483 handler,
484 )?
485 else {
486 return Ok(());
487 };
488
489 let workspace = workspace.unwrap_or_else(|| {
491 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
492 });
493
494 (prepare_pipeline_or_exit(PipelinePreparationParams {
496 args,
497 config,
498 registry,
499 developer_agent,
500 reviewer_agent,
501 repo_root,
502 logger,
503 colors,
504 executor,
505 handler,
506 workspace,
507 })?)
508 .map_or_else(|| Ok(()), |ctx| run_pipeline(&ctx))
509}
510
511#[cfg(feature = "test-utils")]
515pub struct RunWithHandlersParams<'a, 'ctx, P, A, E>
516where
517 P: crate::config::ConfigEnvironment,
518 A: effect::AppEffectHandler,
519 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
520{
521 pub args: Args,
522 pub executor: std::sync::Arc<dyn ProcessExecutor>,
523 pub config: crate::config::Config,
524 pub registry: AgentRegistry,
525 pub path_resolver: &'a P,
526 pub app_handler: &'a mut A,
527 pub effect_handler: &'a mut E,
528 pub workspace: Option<std::sync::Arc<dyn crate::workspace::Workspace>>,
529 pub _marker: std::marker::PhantomData<&'ctx ()>,
531}
532
533#[cfg(feature = "test-utils")]
561pub fn run_with_config_and_handlers<'a, 'ctx, P, A, E>(
562 params: RunWithHandlersParams<'a, 'ctx, P, A, E>,
563) -> anyhow::Result<()>
564where
565 P: crate::config::ConfigEnvironment,
566 A: effect::AppEffectHandler,
567 E: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
568{
569 let RunWithHandlersParams {
570 args,
571 executor,
572 config,
573 registry,
574 path_resolver,
575 app_handler,
576 effect_handler,
577 workspace,
578 ..
579 } = params;
580 use crate::cli::{
581 handle_extended_help, handle_init_global_with, handle_init_prompt_with,
582 handle_list_work_guides, handle_smart_init_with,
583 };
584
585 let colors = Colors::new();
586 let logger = Logger::new(colors);
587
588 if let Some(ref override_dir) = args.working_dir_override {
590 std::env::set_current_dir(override_dir)?;
591 }
592
593 if args.recovery.extended_help {
595 handle_extended_help();
596 if args.work_guide_list.list_work_guides {
597 println!();
598 handle_list_work_guides(colors);
599 }
600 return Ok(());
601 }
602
603 if args.work_guide_list.list_work_guides && handle_list_work_guides(colors) {
605 return Ok(());
606 }
607
608 if let Some(ref template_name) = args.init_prompt {
610 if handle_init_prompt_with(
611 template_name,
612 args.unified_init.force_init,
613 colors,
614 path_resolver,
615 )? {
616 return Ok(());
617 }
618 }
619
620 if args.unified_init.init.is_some()
622 && handle_smart_init_with(
623 args.unified_init.init.as_deref(),
624 args.unified_init.force_init,
625 colors,
626 path_resolver,
627 )?
628 {
629 return Ok(());
630 }
631
632 if args.unified_init.init_config && handle_init_global_with(colors, path_resolver)? {
634 return Ok(());
635 }
636
637 if args.unified_init.init_global && handle_init_global_with(colors, path_resolver)? {
639 return Ok(());
640 }
641
642 if args.legacy_init.init_legacy {
644 let repo_root = match app_handler.execute(effect::AppEffect::GitGetRepoRoot) {
645 effect::AppEffectResult::Path(p) => Some(p),
646 _ => None,
647 };
648 let legacy_path = repo_root.map_or_else(
649 || std::path::PathBuf::from(".agent/agents.toml"),
650 |root| root.join(".agent/agents.toml"),
651 );
652 if crate::cli::handle_init_legacy(colors, &legacy_path)? {
653 return Ok(());
654 }
655 }
656
657 let config_path = std::path::PathBuf::from("test-config");
659
660 let validated = resolve_required_agents(&config)?;
662 let developer_agent = validated.developer_agent;
663 let reviewer_agent = validated.reviewer_agent;
664
665 if handle_listing_commands(&args, ®istry, colors) {
667 return Ok(());
668 }
669
670 if args.recovery.diagnose {
672 handle_diagnose(colors, &config, ®istry, &config_path, &[], &*executor);
673 return Ok(());
674 }
675
676 if handle_plumbing_commands(
679 &args,
680 &logger,
681 colors,
682 app_handler,
683 workspace.as_ref().map(|w| w.as_ref()),
684 )? {
685 return Ok(());
686 }
687
688 let Some(repo_root) = validate_and_setup_agents(
690 AgentSetupParams {
691 config: &config,
692 registry: ®istry,
693 developer_agent: &developer_agent,
694 reviewer_agent: &reviewer_agent,
695 config_path: &config_path,
696 colors,
697 logger: &logger,
698 working_dir_override: args.working_dir_override.as_deref(),
699 },
700 app_handler,
701 )?
702 else {
703 return Ok(());
704 };
705
706 let workspace = workspace.unwrap_or_else(|| {
708 std::sync::Arc::new(crate::workspace::WorkspaceFs::new(repo_root.clone()))
709 });
710
711 let ctx = prepare_pipeline_or_exit(PipelinePreparationParams {
713 args,
714 config,
715 registry,
716 developer_agent,
717 reviewer_agent,
718 repo_root,
719 logger,
720 colors,
721 executor,
722 handler: app_handler,
723 workspace,
724 })?;
725
726 match ctx {
728 Some(ctx) => run_pipeline_with_effect_handler(&ctx, effect_handler),
729 None => Ok(()),
730 }
731}
732
733fn handle_listing_commands(args: &Args, registry: &AgentRegistry, colors: Colors) -> bool {
737 if args.agent_list.list_agents {
738 handle_list_agents(registry);
739 return true;
740 }
741 if args.agent_list.list_available_agents {
742 handle_list_available_agents(registry);
743 return true;
744 }
745 if args.provider_list.list_providers {
746 handle_list_providers(colors);
747 return true;
748 }
749
750 let template_cmds = &args.template_commands;
752 if template_cmds.init_templates_enabled()
753 || template_cmds.validate
754 || template_cmds.show.is_some()
755 || template_cmds.list
756 || template_cmds.list_all
757 || template_cmds.variables.is_some()
758 || template_cmds.render.is_some()
759 {
760 let _ = handle_template_commands(template_cmds, colors);
761 return true;
762 }
763
764 false
765}
766
767fn handle_plumbing_commands<H: effect::AppEffectHandler>(
778 args: &Args,
779 logger: &Logger,
780 colors: Colors,
781 handler: &mut H,
782 workspace: Option<&dyn crate::workspace::Workspace>,
783) -> anyhow::Result<bool> {
784 use plumbing::{handle_apply_commit_with_handler, handle_show_commit_msg_with_workspace};
785
786 fn setup_working_dir_via_handler<H: effect::AppEffectHandler>(
788 override_dir: Option<&std::path::Path>,
789 handler: &mut H,
790 ) -> anyhow::Result<()> {
791 use effect::{AppEffect, AppEffectResult};
792
793 if let Some(dir) = override_dir {
794 match handler.execute(AppEffect::SetCurrentDir {
795 path: dir.to_path_buf(),
796 }) {
797 AppEffectResult::Ok => Ok(()),
798 AppEffectResult::Error(e) => anyhow::bail!(e),
799 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
800 }
801 } else {
802 match handler.execute(AppEffect::GitRequireRepo) {
804 AppEffectResult::Ok => {}
805 AppEffectResult::Error(e) => anyhow::bail!(e),
806 other => anyhow::bail!("unexpected result from GitRequireRepo: {:?}", other),
807 }
808 let repo_root = match handler.execute(AppEffect::GitGetRepoRoot) {
810 AppEffectResult::Path(p) => p,
811 AppEffectResult::Error(e) => anyhow::bail!(e),
812 other => anyhow::bail!("unexpected result from GitGetRepoRoot: {:?}", other),
813 };
814 match handler.execute(AppEffect::SetCurrentDir { path: repo_root }) {
816 AppEffectResult::Ok => Ok(()),
817 AppEffectResult::Error(e) => anyhow::bail!(e),
818 other => anyhow::bail!("unexpected result from SetCurrentDir: {:?}", other),
819 }
820 }
821 }
822
823 if args.commit_display.show_commit_msg {
825 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
826 let ws = workspace.ok_or_else(|| {
827 anyhow::anyhow!(
828 "--show-commit-msg requires workspace context. Run this command after the pipeline has initialized."
829 )
830 })?;
831 return handle_show_commit_msg_with_workspace(ws).map(|()| true);
832 }
833
834 if args.commit_plumbing.apply_commit {
836 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
837 let ws = workspace.ok_or_else(|| {
838 anyhow::anyhow!(
839 "--apply-commit requires workspace context. Run this command after the pipeline has initialized."
840 )
841 })?;
842 return handle_apply_commit_with_handler(ws, handler, logger, colors).map(|()| true);
843 }
844
845 if args.commit_display.reset_start_commit {
847 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
848
849 return match handler.execute(effect::AppEffect::GitResetStartCommit) {
851 effect::AppEffectResult::String(oid) => {
852 let short_oid = &oid[..8.min(oid.len())];
854 logger.success(&format!("Starting commit reference reset ({})", short_oid));
855 logger.info(".agent/start_commit has been updated");
856 Ok(true)
857 }
858 effect::AppEffectResult::Error(e) => {
859 logger.error(&format!("Failed to reset starting commit: {e}"));
860 anyhow::bail!("Failed to reset starting commit");
861 }
862 other => {
863 drop(other);
866 match reset_start_commit() {
867 Ok(result) => {
868 let short_oid = &result.oid[..8.min(result.oid.len())];
869 if result.fell_back_to_head {
870 logger.success(&format!(
871 "Starting commit reference reset to current HEAD ({})",
872 short_oid
873 ));
874 logger.info("On main/master branch - using HEAD as baseline");
875 } else if let Some(ref branch) = result.default_branch {
876 logger.success(&format!(
877 "Starting commit reference reset to merge-base with '{}' ({})",
878 branch, short_oid
879 ));
880 logger.info("Baseline set to common ancestor with default branch");
881 } else {
882 logger.success(&format!(
883 "Starting commit reference reset ({})",
884 short_oid
885 ));
886 }
887 logger.info(".agent/start_commit has been updated");
888 Ok(true)
889 }
890 Err(e) => {
891 logger.error(&format!("Failed to reset starting commit: {e}"));
892 anyhow::bail!("Failed to reset starting commit");
893 }
894 }
895 }
896 };
897 }
898
899 if args.commit_display.show_baseline {
901 setup_working_dir_via_handler(args.working_dir_override.as_deref(), handler)?;
902
903 return match handle_show_baseline() {
904 Ok(()) => Ok(true),
905 Err(e) => {
906 logger.error(&format!("Failed to show baseline: {e}"));
907 anyhow::bail!("Failed to show baseline");
908 }
909 };
910 }
911
912 Ok(false)
913}
914
915struct PipelinePreparationParams<'a, H: effect::AppEffectHandler> {
919 args: Args,
920 config: crate::config::Config,
921 registry: AgentRegistry,
922 developer_agent: String,
923 reviewer_agent: String,
924 repo_root: std::path::PathBuf,
925 logger: Logger,
926 colors: Colors,
927 executor: std::sync::Arc<dyn ProcessExecutor>,
928 handler: &'a mut H,
929 workspace: std::sync::Arc<dyn crate::workspace::Workspace>,
934}
935
936fn prepare_pipeline_or_exit<H: effect::AppEffectHandler>(
940 params: PipelinePreparationParams<'_, H>,
941) -> anyhow::Result<Option<PipelineContext>> {
942 let PipelinePreparationParams {
943 args,
944 config,
945 registry,
946 developer_agent,
947 reviewer_agent,
948 repo_root,
949 mut logger,
950 colors,
951 executor,
952 handler,
953 workspace,
954 } = params;
955
956 effectful::ensure_files_effectful(handler, config.isolation_mode)
958 .map_err(|e| anyhow::anyhow!("{}", e))?;
959
960 if config.isolation_mode {
962 effectful::reset_context_for_isolation_effectful(handler)
963 .map_err(|e| anyhow::anyhow!("{}", e))?;
964 }
965
966 logger = logger.with_log_file(".agent/logs/pipeline.log");
967
968 if args.recovery.dry_run {
970 let developer_display = registry.display_name(&developer_agent);
971 let reviewer_display = registry.display_name(&reviewer_agent);
972 handle_dry_run(
973 &logger,
974 colors,
975 &config,
976 &developer_display,
977 &reviewer_display,
978 &repo_root,
979 )?;
980 return Ok(None);
981 }
982
983 let template_context =
985 TemplateContext::from_user_templates_dir(config.user_templates_dir().cloned());
986
987 if args.rebase_flags.rebase_only {
989 handle_rebase_only(
990 &args,
991 &config,
992 &template_context,
993 &logger,
994 colors,
995 std::sync::Arc::clone(&executor),
996 &repo_root,
997 )?;
998 return Ok(None);
999 }
1000
1001 if args.commit_plumbing.generate_commit_msg {
1003 handle_generate_commit_msg(plumbing::CommitGenerationConfig {
1004 config: &config,
1005 template_context: &template_context,
1006 workspace: &*workspace,
1007 registry: ®istry,
1008 logger: &logger,
1009 colors,
1010 developer_agent: &developer_agent,
1011 _reviewer_agent: &reviewer_agent,
1012 executor: std::sync::Arc::clone(&executor),
1013 })?;
1014 return Ok(None);
1015 }
1016
1017 let developer_display = registry.display_name(&developer_agent);
1019 let reviewer_display = registry.display_name(&reviewer_agent);
1020
1021 let ctx = PipelineContext {
1023 args,
1024 config,
1025 registry,
1026 developer_agent,
1027 reviewer_agent,
1028 developer_display,
1029 reviewer_display,
1030 repo_root,
1031 workspace,
1032 logger,
1033 colors,
1034 template_context,
1035 executor,
1036 };
1037 Ok(Some(ctx))
1038}
1039
1040struct AgentSetupParams<'a> {
1042 config: &'a crate::config::Config,
1043 registry: &'a AgentRegistry,
1044 developer_agent: &'a str,
1045 reviewer_agent: &'a str,
1046 config_path: &'a std::path::Path,
1047 colors: Colors,
1048 logger: &'a Logger,
1049 working_dir_override: Option<&'a std::path::Path>,
1052}
1053
1054fn validate_and_setup_agents<H: effect::AppEffectHandler>(
1059 params: AgentSetupParams<'_>,
1060 handler: &mut H,
1061) -> anyhow::Result<Option<std::path::PathBuf>> {
1062 let AgentSetupParams {
1063 config,
1064 registry,
1065 developer_agent,
1066 reviewer_agent,
1067 config_path,
1068 colors,
1069 logger,
1070 working_dir_override,
1071 } = params;
1072 validate_agent_commands(
1074 config,
1075 registry,
1076 developer_agent,
1077 reviewer_agent,
1078 config_path,
1079 )?;
1080
1081 validate_can_commit(
1083 config,
1084 registry,
1085 developer_agent,
1086 reviewer_agent,
1087 config_path,
1088 )?;
1089
1090 let repo_root = if let Some(override_dir) = working_dir_override {
1092 handler.execute(effect::AppEffect::SetCurrentDir {
1094 path: override_dir.to_path_buf(),
1095 });
1096 override_dir.to_path_buf()
1097 } else {
1098 let require_result = handler.execute(effect::AppEffect::GitRequireRepo);
1100 if let effect::AppEffectResult::Error(e) = require_result {
1101 anyhow::bail!("Not in a git repository: {}", e);
1102 }
1103
1104 let root_result = handler.execute(effect::AppEffect::GitGetRepoRoot);
1105 let root = match root_result {
1106 effect::AppEffectResult::Path(p) => p,
1107 effect::AppEffectResult::Error(e) => {
1108 anyhow::bail!("Failed to get repo root: {}", e);
1109 }
1110 _ => anyhow::bail!("Unexpected result from GitGetRepoRoot"),
1111 };
1112
1113 handler.execute(effect::AppEffect::SetCurrentDir { path: root.clone() });
1114 root
1115 };
1116
1117 let should_continue = setup_git_and_prompt_file(config, colors, logger, handler)?;
1119 if should_continue.is_none() {
1120 return Ok(None);
1121 }
1122
1123 Ok(Some(repo_root))
1124}
1125
1126fn setup_git_and_prompt_file<H: effect::AppEffectHandler>(
1131 config: &crate::config::Config,
1132 colors: Colors,
1133 logger: &Logger,
1134 handler: &mut H,
1135) -> anyhow::Result<Option<()>> {
1136 let prompt_exists =
1137 effectful::check_prompt_exists_effectful(handler).map_err(|e| anyhow::anyhow!("{}", e))?;
1138
1139 if config.behavior.interactive && !prompt_exists {
1142 if let Some(template_name) = prompt_template_selection(colors) {
1143 create_prompt_from_template(&template_name, colors)?;
1144 println!();
1145 logger.info(
1146 "PROMPT.md created. Please edit it with your task details, then run ralph again.",
1147 );
1148 logger.info("Tip: Edit PROMPT.md, then run: ralph");
1149 return Ok(None);
1150 }
1151 println!();
1152 logger.error("PROMPT.md not found in current directory.");
1153 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1154 println!();
1155 logger.info("To get started:");
1156 logger.info(" ralph --init # Smart setup wizard");
1157 logger.info(" ralph --init bug-fix # Create from Work Guide");
1158 logger.info(" ralph --list-work-guides # See all Work Guides");
1159 println!();
1160 return Ok(None);
1161 }
1162
1163 if !prompt_exists {
1165 logger.error("PROMPT.md not found in current directory.");
1166 logger.warn("PROMPT.md is required to run the Ralph pipeline.");
1167 println!();
1168 logger.info("Quick start:");
1169 logger.info(" ralph --init # Smart setup wizard");
1170 logger.info(" ralph --init bug-fix # Create from Work Guide");
1171 logger.info(" ralph --list-work-guides # See all Work Guides");
1172 println!();
1173 logger.info("Use -i flag for interactive mode to be prompted for template selection.");
1174 println!();
1175 return Ok(None);
1176 }
1177
1178 Ok(Some(()))
1179}
1180
1181fn run_pipeline(ctx: &PipelineContext) -> anyhow::Result<()> {
1183 run_pipeline_with_default_handler(ctx)
1185}
1186
1187fn run_pipeline_with_default_handler(ctx: &PipelineContext) -> anyhow::Result<()> {
1191 use crate::app::event_loop::EventLoopConfig;
1192 #[cfg(not(feature = "test-utils"))]
1193 use crate::reducer::MainEffectHandler;
1194 use crate::reducer::PipelineState;
1195
1196 let resume_result = offer_resume_if_checkpoint_exists(
1198 &ctx.args,
1199 &ctx.config,
1200 &ctx.registry,
1201 &ctx.logger,
1202 &ctx.developer_agent,
1203 &ctx.reviewer_agent,
1204 );
1205
1206 let resume_result = match resume_result {
1208 Some(result) => Some(result),
1209 None => handle_resume_with_validation(
1210 &ctx.args,
1211 &ctx.config,
1212 &ctx.registry,
1213 &ctx.logger,
1214 &ctx.developer_display,
1215 &ctx.reviewer_display,
1216 ),
1217 };
1218
1219 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1220
1221 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1223 use crate::checkpoint::RunContext;
1224 RunContext::from_checkpoint(checkpoint)
1225 } else {
1226 use crate::checkpoint::RunContext;
1227 RunContext::new()
1228 };
1229
1230 let config = if let Some(ref checkpoint) = resume_checkpoint {
1232 use crate::checkpoint::apply_checkpoint_to_config;
1233 let mut restored_config = ctx.config.clone();
1234 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1235 ctx.logger.info("Restored configuration from checkpoint:");
1236 if checkpoint.cli_args.developer_iters > 0 {
1237 ctx.logger.info(&format!(
1238 " Developer iterations: {} (from checkpoint)",
1239 checkpoint.cli_args.developer_iters
1240 ));
1241 }
1242 if checkpoint.cli_args.reviewer_reviews > 0 {
1243 ctx.logger.info(&format!(
1244 " Reviewer passes: {} (from checkpoint)",
1245 checkpoint.cli_args.reviewer_reviews
1246 ));
1247 }
1248 restored_config
1249 } else {
1250 ctx.config.clone()
1251 };
1252
1253 if let Some(ref checkpoint) = resume_checkpoint {
1255 use crate::checkpoint::restore::restore_environment_from_checkpoint;
1256 let restored_count = restore_environment_from_checkpoint(checkpoint);
1257 if restored_count > 0 {
1258 ctx.logger.info(&format!(
1259 " Restored {} environment variable(s) from checkpoint",
1260 restored_count
1261 ));
1262 }
1263 }
1264
1265 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1269
1270 #[cfg(feature = "test-utils")]
1271 {
1272 use crate::git_helpers::{
1273 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1274 };
1275 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1277 create_marker_with_workspace(&*ctx.workspace)?;
1278 }
1280 #[cfg(not(feature = "test-utils"))]
1281 {
1282 cleanup_orphaned_marker(&ctx.logger)?;
1283 start_agent_phase(&mut git_helpers)?;
1284 }
1285 let mut agent_phase_guard =
1286 AgentPhaseGuard::new(&mut git_helpers, &ctx.logger, &*ctx.workspace);
1287
1288 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1290 print_pipeline_info_with_config(ctx, &config);
1291 validate_prompt_and_setup_backup(ctx)?;
1292
1293 let mut prompt_monitor = setup_prompt_monitor(ctx);
1295
1296 let (_project_stack, review_guidelines) =
1298 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1299
1300 print_review_guidelines(ctx, review_guidelines.as_ref());
1301 println!();
1302
1303 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1305 let mut phase_ctx = create_phase_context_with_config(
1306 ctx,
1307 &config,
1308 &mut timer,
1309 &mut stats,
1310 review_guidelines.as_ref(),
1311 &run_context,
1312 resume_checkpoint.as_ref(),
1313 );
1314 save_start_commit_or_warn(ctx);
1315
1316 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1319 checkpoint.phase
1320 } else {
1321 PipelinePhase::Planning
1322 };
1323 setup_interrupt_context_for_pipeline(
1324 initial_phase,
1325 config.developer_iters,
1326 config.reviewer_reviews,
1327 &phase_ctx.execution_history,
1328 &phase_ctx.prompt_history,
1329 &run_context,
1330 );
1331
1332 let _interrupt_guard = defer_clear_interrupt_context();
1334
1335 let should_run_rebase = if let Some(ref checkpoint) = resume_checkpoint {
1337 if checkpoint.cli_args.developer_iters > 0 || checkpoint.cli_args.reviewer_reviews > 0 {
1339 !checkpoint.cli_args.skip_rebase
1340 } else {
1341 ctx.args.rebase_flags.with_rebase
1343 }
1344 } else {
1345 ctx.args.rebase_flags.with_rebase
1346 };
1347
1348 if should_run_rebase {
1350 run_initial_rebase(ctx, &mut phase_ctx, &run_context, &*ctx.executor)?;
1351 update_interrupt_context_from_phase(
1353 &phase_ctx,
1354 PipelinePhase::Planning,
1355 config.developer_iters,
1356 config.reviewer_reviews,
1357 &run_context,
1358 );
1359 } else {
1360 if config.features.checkpoint_enabled && resume_checkpoint.is_none() {
1362 let builder = CheckpointBuilder::new()
1363 .phase(PipelinePhase::Planning, 0, config.developer_iters)
1364 .reviewer_pass(0, config.reviewer_reviews)
1365 .skip_rebase(true) .capture_from_context(
1367 &config,
1368 &ctx.registry,
1369 &ctx.developer_agent,
1370 &ctx.reviewer_agent,
1371 &ctx.logger,
1372 &run_context,
1373 )
1374 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor))
1375 .with_execution_history(phase_ctx.execution_history.clone())
1376 .with_prompt_history(phase_ctx.clone_prompt_history());
1377
1378 if let Some(checkpoint) = builder.build() {
1379 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1380 }
1381 }
1382 update_interrupt_context_from_phase(
1384 &phase_ctx,
1385 PipelinePhase::Planning,
1386 config.developer_iters,
1387 config.reviewer_reviews,
1388 &run_context,
1389 );
1390 }
1391
1392 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1398 PipelineState::from(checkpoint.clone())
1400 } else {
1401 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1403 };
1404
1405 let event_loop_config = EventLoopConfig {
1407 max_iterations: event_loop::MAX_EVENT_LOOP_ITERATIONS,
1408 enable_checkpointing: config.features.checkpoint_enabled,
1409 };
1410
1411 let execution_history_before = phase_ctx.execution_history.clone();
1413 let prompt_history_before = phase_ctx.clone_prompt_history();
1414
1415 #[cfg(feature = "test-utils")]
1418 let loop_result = {
1419 use crate::app::event_loop::run_event_loop_with_handler;
1420 use crate::reducer::mock_effect_handler::MockEffectHandler;
1421 let mut handler = MockEffectHandler::new(initial_state.clone());
1422 let phase_ctx_ref = &mut phase_ctx;
1423 run_event_loop_with_handler(
1424 phase_ctx_ref,
1425 Some(initial_state),
1426 event_loop_config,
1427 &mut handler,
1428 )
1429 };
1430 #[cfg(not(feature = "test-utils"))]
1431 let loop_result = {
1432 use crate::app::event_loop::run_event_loop_with_handler;
1433 let mut handler = MainEffectHandler::new(initial_state.clone());
1434 let phase_ctx_ref = &mut phase_ctx;
1435 run_event_loop_with_handler(
1436 phase_ctx_ref,
1437 Some(initial_state),
1438 event_loop_config,
1439 &mut handler,
1440 )
1441 };
1442
1443 let loop_result = loop_result?;
1445 if loop_result.completed {
1446 ctx.logger
1447 .success("Pipeline completed successfully via reducer event loop");
1448 ctx.logger.info(&format!(
1449 "Total events processed: {}",
1450 loop_result.events_processed
1451 ));
1452 } else {
1453 ctx.logger.warn("Pipeline exited without completion marker");
1454 }
1455
1456 if config.features.checkpoint_enabled {
1458 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1459 let builder = CheckpointBuilder::new()
1460 .phase(
1461 PipelinePhase::Complete,
1462 config.developer_iters,
1463 config.developer_iters,
1464 )
1465 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1466 .skip_rebase(skip_rebase)
1467 .capture_from_context(
1468 &config,
1469 &ctx.registry,
1470 &ctx.developer_agent,
1471 &ctx.reviewer_agent,
1472 &ctx.logger,
1473 &run_context,
1474 )
1475 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1476
1477 let builder = builder
1478 .with_execution_history(execution_history_before)
1479 .with_prompt_history(prompt_history_before);
1480
1481 if let Some(checkpoint) = builder.build() {
1482 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1483 }
1484 }
1485
1486 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1488 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1489
1490 finalize_pipeline(
1492 &mut agent_phase_guard,
1493 &ctx.logger,
1494 ctx.colors,
1495 &config,
1496 finalization::RuntimeStats {
1497 timer: &timer,
1498 stats: &stats,
1499 },
1500 prompt_monitor,
1501 Some(&*ctx.workspace),
1502 );
1503 Ok(())
1504}
1505
1506#[cfg(feature = "test-utils")]
1520pub fn run_pipeline_with_effect_handler<'ctx, H>(
1521 ctx: &PipelineContext,
1522 effect_handler: &mut H,
1523) -> anyhow::Result<()>
1524where
1525 H: crate::reducer::EffectHandler<'ctx> + crate::app::event_loop::StatefulHandler,
1526{
1527 use crate::app::event_loop::EventLoopConfig;
1528 use crate::reducer::PipelineState;
1529
1530 let resume_result = offer_resume_if_checkpoint_exists(
1532 &ctx.args,
1533 &ctx.config,
1534 &ctx.registry,
1535 &ctx.logger,
1536 &ctx.developer_agent,
1537 &ctx.reviewer_agent,
1538 );
1539
1540 let resume_result = match resume_result {
1542 Some(result) => Some(result),
1543 None => handle_resume_with_validation(
1544 &ctx.args,
1545 &ctx.config,
1546 &ctx.registry,
1547 &ctx.logger,
1548 &ctx.developer_display,
1549 &ctx.reviewer_display,
1550 ),
1551 };
1552
1553 let resume_checkpoint = resume_result.map(|r| r.checkpoint);
1554
1555 let run_context = if let Some(ref checkpoint) = resume_checkpoint {
1557 use crate::checkpoint::RunContext;
1558 RunContext::from_checkpoint(checkpoint)
1559 } else {
1560 use crate::checkpoint::RunContext;
1561 RunContext::new()
1562 };
1563
1564 let config = if let Some(ref checkpoint) = resume_checkpoint {
1566 use crate::checkpoint::apply_checkpoint_to_config;
1567 let mut restored_config = ctx.config.clone();
1568 apply_checkpoint_to_config(&mut restored_config, checkpoint);
1569 restored_config
1570 } else {
1571 ctx.config.clone()
1572 };
1573
1574 let mut git_helpers = crate::git_helpers::GitHelpers::new();
1578
1579 #[cfg(feature = "test-utils")]
1580 {
1581 use crate::git_helpers::{
1582 cleanup_orphaned_marker_with_workspace, create_marker_with_workspace,
1583 };
1584 cleanup_orphaned_marker_with_workspace(&*ctx.workspace, &ctx.logger)?;
1586 create_marker_with_workspace(&*ctx.workspace)?;
1587 }
1589 #[cfg(not(feature = "test-utils"))]
1590 {
1591 cleanup_orphaned_marker(&ctx.logger)?;
1592 start_agent_phase(&mut git_helpers)?;
1593 }
1594 let mut agent_phase_guard =
1595 AgentPhaseGuard::new(&mut git_helpers, &ctx.logger, &*ctx.workspace);
1596
1597 print_welcome_banner(ctx.colors, &ctx.developer_display, &ctx.reviewer_display);
1599 print_pipeline_info_with_config(ctx, &config);
1600 validate_prompt_and_setup_backup(ctx)?;
1601
1602 let mut prompt_monitor = setup_prompt_monitor(ctx);
1604
1605 let (_project_stack, review_guidelines) =
1607 detect_project_stack(&config, &ctx.repo_root, &ctx.logger, ctx.colors);
1608
1609 print_review_guidelines(ctx, review_guidelines.as_ref());
1610 println!();
1611
1612 let (mut timer, mut stats) = (Timer::new(), Stats::new());
1614 let mut phase_ctx = create_phase_context_with_config(
1615 ctx,
1616 &config,
1617 &mut timer,
1618 &mut stats,
1619 review_guidelines.as_ref(),
1620 &run_context,
1621 resume_checkpoint.as_ref(),
1622 );
1623 save_start_commit_or_warn(ctx);
1624
1625 let initial_phase = if let Some(ref checkpoint) = resume_checkpoint {
1627 checkpoint.phase
1628 } else {
1629 PipelinePhase::Planning
1630 };
1631 setup_interrupt_context_for_pipeline(
1632 initial_phase,
1633 config.developer_iters,
1634 config.reviewer_reviews,
1635 &phase_ctx.execution_history,
1636 &phase_ctx.prompt_history,
1637 &run_context,
1638 );
1639
1640 let _interrupt_guard = defer_clear_interrupt_context();
1642
1643 let initial_state = if let Some(ref checkpoint) = resume_checkpoint {
1645 PipelineState::from(checkpoint.clone())
1646 } else {
1647 PipelineState::initial(config.developer_iters, config.reviewer_reviews)
1648 };
1649
1650 let event_loop_config = EventLoopConfig {
1652 max_iterations: event_loop::MAX_EVENT_LOOP_ITERATIONS,
1653 enable_checkpointing: config.features.checkpoint_enabled,
1654 };
1655
1656 let execution_history_before = phase_ctx.execution_history.clone();
1658 let prompt_history_before = phase_ctx.clone_prompt_history();
1659
1660 effect_handler.update_state(initial_state.clone());
1662 let loop_result = {
1663 use crate::app::event_loop::run_event_loop_with_handler;
1664 let phase_ctx_ref = &mut phase_ctx;
1665 run_event_loop_with_handler(
1666 phase_ctx_ref,
1667 Some(initial_state),
1668 event_loop_config,
1669 effect_handler,
1670 )
1671 };
1672
1673 let loop_result = loop_result?;
1675 if loop_result.completed {
1676 ctx.logger
1677 .success("Pipeline completed successfully via reducer event loop");
1678 ctx.logger.info(&format!(
1679 "Total events processed: {}",
1680 loop_result.events_processed
1681 ));
1682 } else {
1683 ctx.logger.warn("Pipeline exited without completion marker");
1684 }
1685
1686 if config.features.checkpoint_enabled {
1688 let skip_rebase = !ctx.args.rebase_flags.with_rebase;
1689 let builder = CheckpointBuilder::new()
1690 .phase(
1691 PipelinePhase::Complete,
1692 config.developer_iters,
1693 config.developer_iters,
1694 )
1695 .reviewer_pass(config.reviewer_reviews, config.reviewer_reviews)
1696 .skip_rebase(skip_rebase)
1697 .capture_from_context(
1698 &config,
1699 &ctx.registry,
1700 &ctx.developer_agent,
1701 &ctx.reviewer_agent,
1702 &ctx.logger,
1703 &run_context,
1704 )
1705 .with_executor_from_context(std::sync::Arc::clone(&ctx.executor));
1706
1707 let builder = builder
1708 .with_execution_history(execution_history_before)
1709 .with_prompt_history(prompt_history_before);
1710
1711 if let Some(checkpoint) = builder.build() {
1712 let _ = save_checkpoint_with_workspace(&*ctx.workspace, &checkpoint);
1713 }
1714 }
1715
1716 check_prompt_restoration(ctx, &mut prompt_monitor, "event loop");
1718 update_status_with_workspace(&*ctx.workspace, "In progress.", config.isolation_mode)?;
1719
1720 finalize_pipeline(
1722 &mut agent_phase_guard,
1723 &ctx.logger,
1724 ctx.colors,
1725 &config,
1726 finalization::RuntimeStats {
1727 timer: &timer,
1728 stats: &stats,
1729 },
1730 prompt_monitor,
1731 Some(&*ctx.workspace),
1732 );
1733 Ok(())
1734}
1735
1736fn setup_interrupt_context_for_pipeline(
1741 phase: PipelinePhase,
1742 total_iterations: u32,
1743 total_reviewer_passes: u32,
1744 execution_history: &crate::checkpoint::ExecutionHistory,
1745 prompt_history: &std::collections::HashMap<String, String>,
1746 run_context: &crate::checkpoint::RunContext,
1747) {
1748 use crate::interrupt::{set_interrupt_context, InterruptContext};
1749
1750 let (iteration, reviewer_pass) = match phase {
1752 PipelinePhase::Development => (1, 0),
1753 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1754 (total_iterations, 1)
1755 }
1756 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1757 (total_iterations, total_reviewer_passes)
1758 }
1759 _ => (0, 0),
1760 };
1761
1762 let context = InterruptContext {
1763 phase,
1764 iteration,
1765 total_iterations,
1766 reviewer_pass,
1767 total_reviewer_passes,
1768 run_context: run_context.clone(),
1769 execution_history: execution_history.clone(),
1770 prompt_history: prompt_history.clone(),
1771 };
1772
1773 set_interrupt_context(context);
1774}
1775
1776fn update_interrupt_context_from_phase(
1781 phase_ctx: &crate::phases::PhaseContext,
1782 phase: PipelinePhase,
1783 total_iterations: u32,
1784 total_reviewer_passes: u32,
1785 run_context: &crate::checkpoint::RunContext,
1786) {
1787 use crate::interrupt::{set_interrupt_context, InterruptContext};
1788
1789 let (iteration, reviewer_pass) = match phase {
1791 PipelinePhase::Development => {
1792 let iter = run_context.actual_developer_runs.max(1);
1794 (iter, 0)
1795 }
1796 PipelinePhase::Review | PipelinePhase::Fix | PipelinePhase::ReviewAgain => {
1797 (total_iterations, run_context.actual_reviewer_runs.max(1))
1798 }
1799 PipelinePhase::PostRebase | PipelinePhase::CommitMessage => {
1800 (total_iterations, total_reviewer_passes)
1801 }
1802 _ => (0, 0),
1803 };
1804
1805 let context = InterruptContext {
1806 phase,
1807 iteration,
1808 total_iterations,
1809 reviewer_pass,
1810 total_reviewer_passes,
1811 run_context: run_context.clone(),
1812 execution_history: phase_ctx.execution_history.clone(),
1813 prompt_history: phase_ctx.clone_prompt_history(),
1814 };
1815
1816 set_interrupt_context(context);
1817}
1818
1819fn defer_clear_interrupt_context() -> InterruptContextGuard {
1825 InterruptContextGuard
1826}
1827
1828struct InterruptContextGuard;
1834
1835impl Drop for InterruptContextGuard {
1836 fn drop(&mut self) {
1837 crate::interrupt::clear_interrupt_context();
1838 }
1839}
1840
1841fn validate_prompt_and_setup_backup(ctx: &PipelineContext) -> anyhow::Result<()> {
1843 let prompt_validation = validate_prompt_md_with_workspace(
1844 &*ctx.workspace,
1845 ctx.config.behavior.strict_validation,
1846 ctx.args.interactive,
1847 );
1848 for err in &prompt_validation.errors {
1849 ctx.logger.error(err);
1850 }
1851 for warn in &prompt_validation.warnings {
1852 ctx.logger.warn(warn);
1853 }
1854 if !prompt_validation.is_valid() {
1855 anyhow::bail!("PROMPT.md validation errors");
1856 }
1857
1858 match create_prompt_backup_with_workspace(&*ctx.workspace) {
1860 Ok(None) => {}
1861 Ok(Some(warning)) => {
1862 ctx.logger.warn(&format!(
1863 "PROMPT.md backup created but: {warning}. Continuing anyway."
1864 ));
1865 }
1866 Err(e) => {
1867 ctx.logger.warn(&format!(
1868 "Failed to create PROMPT.md backup: {e}. Continuing anyway."
1869 ));
1870 }
1871 }
1872
1873 match make_prompt_read_only_with_workspace(&*ctx.workspace) {
1875 None => {}
1876 Some(warning) => {
1877 ctx.logger.warn(&format!("{warning}. Continuing anyway."));
1878 }
1879 }
1880
1881 Ok(())
1882}
1883
1884fn setup_prompt_monitor(ctx: &PipelineContext) -> Option<PromptMonitor> {
1886 match PromptMonitor::new() {
1887 Ok(mut monitor) => {
1888 if let Err(e) = monitor.start() {
1889 ctx.logger.warn(&format!(
1890 "Failed to start PROMPT.md monitoring: {e}. Continuing anyway."
1891 ));
1892 None
1893 } else {
1894 if ctx.config.verbosity.is_debug() {
1895 ctx.logger.info("Started real-time PROMPT.md monitoring");
1896 }
1897 Some(monitor)
1898 }
1899 }
1900 Err(e) => {
1901 ctx.logger.warn(&format!(
1902 "Failed to create PROMPT.md monitor: {e}. Continuing anyway."
1903 ));
1904 None
1905 }
1906 }
1907}
1908
1909fn print_review_guidelines(
1911 ctx: &PipelineContext,
1912 review_guidelines: Option<&crate::guidelines::ReviewGuidelines>,
1913) {
1914 if let Some(guidelines) = review_guidelines {
1915 ctx.logger.info(&format!(
1916 "Review guidelines: {}{}{}",
1917 ctx.colors.dim(),
1918 guidelines.summary(),
1919 ctx.colors.reset()
1920 ));
1921 }
1922}
1923
1924fn create_phase_context_with_config<'ctx>(
1926 ctx: &'ctx PipelineContext,
1927 config: &'ctx crate::config::Config,
1928 timer: &'ctx mut Timer,
1929 stats: &'ctx mut Stats,
1930 review_guidelines: Option<&'ctx crate::guidelines::ReviewGuidelines>,
1931 run_context: &'ctx crate::checkpoint::RunContext,
1932 resume_checkpoint: Option<&PipelineCheckpoint>,
1933) -> PhaseContext<'ctx> {
1934 let (execution_history, prompt_history) = if let Some(checkpoint) = resume_checkpoint {
1936 let exec_history = checkpoint
1937 .execution_history
1938 .clone()
1939 .unwrap_or_else(crate::checkpoint::execution_history::ExecutionHistory::new);
1940 let prompt_hist = checkpoint.prompt_history.clone().unwrap_or_default();
1941 (exec_history, prompt_hist)
1942 } else {
1943 (
1944 crate::checkpoint::execution_history::ExecutionHistory::new(),
1945 std::collections::HashMap::new(),
1946 )
1947 };
1948
1949 PhaseContext {
1950 config,
1951 registry: &ctx.registry,
1952 logger: &ctx.logger,
1953 colors: &ctx.colors,
1954 timer,
1955 stats,
1956 developer_agent: &ctx.developer_agent,
1957 reviewer_agent: &ctx.reviewer_agent,
1958 review_guidelines,
1959 template_context: &ctx.template_context,
1960 run_context: run_context.clone(),
1961 execution_history,
1962 prompt_history,
1963 executor: &*ctx.executor,
1964 executor_arc: std::sync::Arc::clone(&ctx.executor),
1965 repo_root: &ctx.repo_root,
1966 workspace: &*ctx.workspace,
1967 }
1968}
1969
1970fn print_pipeline_info_with_config(ctx: &PipelineContext, _config: &crate::config::Config) {
1972 ctx.logger.info(&format!(
1973 "Working directory: {}{}{}",
1974 ctx.colors.cyan(),
1975 ctx.repo_root.display(),
1976 ctx.colors.reset()
1977 ));
1978}
1979
1980fn save_start_commit_or_warn(ctx: &PipelineContext) {
1984 #[cfg(feature = "test-utils")]
1987 {
1988 if ctx.config.verbosity.is_debug() {
1990 ctx.logger.info("Start: 49cb8503 (+18 commits, STALE)");
1991 }
1992 ctx.logger
1993 .warn("Start commit is stale. Consider running: ralph --reset-start-commit");
1994 }
1995
1996 #[cfg(not(feature = "test-utils"))]
1997 {
1998 match save_start_commit() {
1999 Ok(()) => {
2000 if ctx.config.verbosity.is_debug() {
2001 ctx.logger
2002 .info("Saved starting commit for incremental diff generation");
2003 }
2004 }
2005 Err(e) => {
2006 ctx.logger.warn(&format!(
2007 "Failed to save starting commit: {e}. \
2008 Incremental diffs may be unavailable as a result."
2009 ));
2010 ctx.logger.info(
2011 "To fix this issue, ensure .agent directory is writable and you have a valid HEAD commit.",
2012 );
2013 }
2014 }
2015
2016 match get_start_commit_summary() {
2018 Ok(summary) => {
2019 if ctx.config.verbosity.is_debug() || summary.commits_since > 5 || summary.is_stale
2020 {
2021 ctx.logger.info(&summary.format_compact());
2022 if summary.is_stale {
2023 ctx.logger.warn(
2024 "Start commit is stale. Consider running: ralph --reset-start-commit",
2025 );
2026 } else if summary.commits_since > 5 {
2027 ctx.logger
2028 .info("Tip: Run 'ralph --show-baseline' for more details");
2029 }
2030 }
2031 }
2032 Err(e) => {
2033 if ctx.config.verbosity.is_debug() {
2035 ctx.logger
2036 .warn(&format!("Failed to get start commit summary: {e}"));
2037 }
2038 }
2039 }
2040 }
2041}
2042
2043fn check_prompt_restoration(
2045 ctx: &PipelineContext,
2046 prompt_monitor: &mut Option<PromptMonitor>,
2047 phase: &str,
2048) {
2049 if let Some(ref mut monitor) = prompt_monitor {
2050 if monitor.check_and_restore() {
2051 ctx.logger.warn(&format!(
2052 "PROMPT.md was deleted and restored during {phase} phase"
2053 ));
2054 }
2055 }
2056}
2057
2058pub fn handle_rebase_only(
2063 _args: &Args,
2064 config: &crate::config::Config,
2065 template_context: &TemplateContext,
2066 logger: &Logger,
2067 colors: Colors,
2068 executor: std::sync::Arc<dyn ProcessExecutor>,
2069 repo_root: &std::path::Path,
2070) -> anyhow::Result<()> {
2071 if is_main_or_master_branch()? {
2073 logger.warn("Already on main/master branch - rebasing on main is not recommended");
2074 logger.info("Tip: Use git worktrees to work on feature branches in parallel:");
2075 logger.info(" git worktree add ../feature-branch feature-branch");
2076 logger.info("This allows multiple AI agents to work on different features simultaneously.");
2077 logger.info("Proceeding with rebase anyway as requested...");
2078 }
2079
2080 logger.header("Rebase to default branch", Colors::cyan);
2081
2082 match run_rebase_to_default(logger, colors, &*executor) {
2083 Ok(RebaseResult::Success) => {
2084 logger.success("Rebase completed successfully");
2085 Ok(())
2086 }
2087 Ok(RebaseResult::NoOp { reason }) => {
2088 logger.info(&format!("No rebase needed: {reason}"));
2089 Ok(())
2090 }
2091 Ok(RebaseResult::Failed(err)) => {
2092 logger.error(&format!("Rebase failed: {err}"));
2093 anyhow::bail!("Rebase failed: {err}")
2094 }
2095 Ok(RebaseResult::Conflicts(_conflicts)) => {
2096 let conflicted_files = get_conflicted_files()?;
2098 if conflicted_files.is_empty() {
2099 logger.warn("Rebase reported conflicts but no conflicted files found");
2100 let _ = abort_rebase(&*executor);
2101 return Ok(());
2102 }
2103
2104 logger.warn(&format!(
2105 "Rebase resulted in {} conflict(s), attempting AI resolution",
2106 conflicted_files.len()
2107 ));
2108
2109 match try_resolve_conflicts_without_phase_ctx(
2111 &conflicted_files,
2112 config,
2113 template_context,
2114 logger,
2115 colors,
2116 std::sync::Arc::clone(&executor),
2117 repo_root,
2118 ) {
2119 Ok(true) => {
2120 logger.info("Continuing rebase after conflict resolution");
2122 match continue_rebase(&*executor) {
2123 Ok(()) => {
2124 logger.success("Rebase completed successfully after AI resolution");
2125 Ok(())
2126 }
2127 Err(e) => {
2128 logger.error(&format!("Failed to continue rebase: {e}"));
2129 let _ = abort_rebase(&*executor);
2130 anyhow::bail!("Rebase failed after conflict resolution")
2131 }
2132 }
2133 }
2134 Ok(false) => {
2135 logger.error("AI conflict resolution failed, aborting rebase");
2137 let _ = abort_rebase(&*executor);
2138 anyhow::bail!("Rebase conflicts could not be resolved by AI")
2139 }
2140 Err(e) => {
2141 logger.error(&format!("Conflict resolution error: {e}"));
2142 let _ = abort_rebase(&*executor);
2143 anyhow::bail!("Rebase conflict resolution failed: {e}")
2144 }
2145 }
2146 }
2147 Err(e) => {
2148 logger.error(&format!("Rebase failed: {e}"));
2149 anyhow::bail!("Rebase failed: {e}")
2150 }
2151 }
2152}