1use crate::agents::{AgentsConfigFile, ConfigInitResult};
16use crate::config::{ConfigEnvironment, RealConfigEnvironment};
17use crate::logger::Colors;
18use crate::templates::{get_template, list_templates, ALL_TEMPLATES};
19use std::io::IsTerminal;
20use std::path::Path;
21
22const MIN_SIMILARITY_PERCENT: u32 = 40;
24
25pub fn handle_init_global_with<R: ConfigEnvironment>(
40 colors: Colors,
41 env: &R,
42) -> anyhow::Result<bool> {
43 let global_path = env
44 .unified_config_path()
45 .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
46
47 if env.file_exists(&global_path) {
49 println!(
50 "{}Unified config already exists:{} {}",
51 colors.yellow(),
52 colors.reset(),
53 global_path.display()
54 );
55 println!("Edit the file to customize, or delete it to regenerate from defaults.");
56 println!();
57 println!("Next steps:");
58 println!(" 1. Create a PROMPT.md for your task:");
59 println!(" ralph --init <work-guide>");
60 println!(" ralph --list-work-guides # Show all Work Guides");
61 println!(" 2. Or run ralph directly with default settings:");
62 println!(" ralph \"your commit message\"");
63 return Ok(true);
64 }
65
66 env.write_file(&global_path, crate::config::unified::DEFAULT_UNIFIED_CONFIG)
68 .map_err(|e| {
69 anyhow::anyhow!(
70 "Failed to create config file {}: {}",
71 global_path.display(),
72 e
73 )
74 })?;
75
76 println!(
77 "{}Created unified config: {}{}{}\n",
78 colors.green(),
79 colors.bold(),
80 global_path.display(),
81 colors.reset()
82 );
83 println!("This is the primary configuration file for Ralph.");
84 println!();
85 println!("Features:");
86 println!(" - General settings (verbosity, iterations, etc.)");
87 println!(" - CCS aliases for Claude Code Switch integration");
88 println!(" - Custom agent definitions");
89 println!(" - Agent chain configuration with fallbacks");
90 println!();
91 println!("Environment variables (RALPH_*) override these settings.");
92 println!();
93 println!("Next steps:");
94 println!(" 1. Create a PROMPT.md for your task:");
95 println!(" ralph --init <work-guide>");
96 println!(" ralph --list-work-guides # Show all Work Guides");
97 println!(" 2. Or run ralph directly with default settings:");
98 println!(" ralph \"your commit message\"");
99 Ok(true)
100}
101
102pub fn handle_init_global(colors: Colors) -> anyhow::Result<bool> {
116 handle_init_global_with(colors, &RealConfigEnvironment)
117}
118
119pub fn handle_init_legacy(colors: Colors, agents_config_path: &Path) -> anyhow::Result<bool> {
123 match AgentsConfigFile::ensure_config_exists(agents_config_path) {
124 Ok(ConfigInitResult::Created) => {
125 println!(
126 "{}Created {}{}{}\n",
127 colors.green(),
128 colors.bold(),
129 agents_config_path.display(),
130 colors.reset()
131 );
132 println!("Edit the file to customize agent configurations, then run ralph again.");
133 println!("Or run ralph now to use the default settings.");
134 Ok(true)
135 }
136 Ok(ConfigInitResult::AlreadyExists) => {
137 println!(
138 "{}Config file already exists:{} {}",
139 colors.yellow(),
140 colors.reset(),
141 agents_config_path.display()
142 );
143 println!("Edit the file to customize, or delete it to regenerate from defaults.");
144 Ok(true)
145 }
146 Err(e) => Err(anyhow::anyhow!(
147 "Failed to create config file {}: {}",
148 agents_config_path.display(),
149 e
150 )),
151 }
152}
153
154fn can_prompt_user() -> bool {
163 prompt_output_target().is_some()
164}
165
166#[derive(Clone, Copy)]
167enum PromptOutputTarget {
168 Stdout,
169 Stderr,
170}
171
172fn prompt_output_target() -> Option<PromptOutputTarget> {
173 if !std::io::stdin().is_terminal() {
174 return None;
175 }
176
177 if std::io::stdout().is_terminal() {
178 return Some(PromptOutputTarget::Stdout);
179 }
180 if std::io::stderr().is_terminal() {
181 return Some(PromptOutputTarget::Stderr);
182 }
183
184 None
185}
186
187fn with_prompt_writer<T>(
188 target: PromptOutputTarget,
189 f: impl FnOnce(&mut dyn std::io::Write) -> anyhow::Result<T>,
190) -> anyhow::Result<T> {
191 use std::io;
192
193 match target {
194 PromptOutputTarget::Stdout => {
195 let mut out = io::stdout().lock();
196 f(&mut out)
197 }
198 PromptOutputTarget::Stderr => {
199 let mut err = io::stderr().lock();
200 f(&mut err)
201 }
202 }
203}
204
205fn prompt_overwrite_confirmation(prompt_path: &Path, colors: Colors) -> anyhow::Result<bool> {
206 use std::io;
207
208 let Some(target) = prompt_output_target() else {
209 return Ok(false);
210 };
211
212 with_prompt_writer(target, |w| {
213 writeln!(
214 w,
215 "{}PROMPT.md already exists:{} {}",
216 colors.yellow(),
217 colors.reset(),
218 prompt_path.display()
219 )?;
220 write!(w, "Do you want to overwrite it? [y/N]: ")?;
221 w.flush()?;
222 Ok(())
223 })?;
224
225 let mut input = String::new();
226 match io::stdin().read_line(&mut input) {
227 Ok(0) => return Ok(false),
228 Ok(_) => {}
229 Err(_) => return Ok(false),
230 }
231
232 let response = input.trim().to_lowercase();
233 Ok(response == "y" || response == "yes")
234}
235
236pub fn handle_init_prompt_with<R: ConfigEnvironment>(
253 template_name: &str,
254 force: bool,
255 colors: Colors,
256 env: &R,
257) -> anyhow::Result<bool> {
258 handle_init_prompt_at_path_with_env(template_name, &env.prompt_path(), force, colors, env)
259}
260
261pub fn handle_init_prompt(
277 template_name: &str,
278 force: bool,
279 colors: Colors,
280) -> anyhow::Result<bool> {
281 handle_init_prompt_with(template_name, force, colors, &RealConfigEnvironment)
282}
283
284fn print_common_work_guides(colors: Colors) {
288 println!("{}Common Work Guides:{}", colors.bold(), colors.reset());
289 println!(
290 " {}quick{} Quick/small changes (typos, minor fixes)",
291 colors.cyan(),
292 colors.reset()
293 );
294 println!(
295 " {}bug-fix{} Bug fix with investigation guidance",
296 colors.cyan(),
297 colors.reset()
298 );
299 println!(
300 " {}feature-spec{} Comprehensive product specification",
301 colors.cyan(),
302 colors.reset()
303 );
304 println!(
305 " {}refactor{} Code refactoring with behavior preservation",
306 colors.cyan(),
307 colors.reset()
308 );
309 println!();
310 println!(
311 "Use {}--list-work-guides{} for the complete list of Work Guides.",
312 colors.cyan(),
313 colors.reset()
314 );
315 println!();
316}
317
318fn print_template_category(category_name: &str, templates: &[(&str, &str)], colors: Colors) {
322 println!("{}{}:{}", colors.bold(), category_name, colors.reset());
323 for (name, description) in templates {
324 println!(
325 " {}{}{} {}",
326 colors.cyan(),
327 name,
328 colors.reset(),
329 description
330 );
331 }
332 println!();
333}
334
335pub fn handle_list_work_guides(colors: Colors) -> bool {
347 println!("PROMPT.md Work Guides (use: ralph --init <work-guide>)");
348 println!();
349
350 print_template_category(
352 "Common Templates",
353 &[
354 ("quick", "Quick/small changes (typos, minor fixes)"),
355 ("bug-fix", "Bug fix with investigation guidance"),
356 ("feature-spec", "Comprehensive product specification"),
357 ("refactor", "Code refactoring with behavior preservation"),
358 ],
359 colors,
360 );
361
362 print_template_category(
364 "Testing & Documentation",
365 &[
366 ("test", "Test writing with edge case considerations"),
367 ("docs", "Documentation update with completeness checklist"),
368 ("code-review", "Structured code review for pull requests"),
369 ],
370 colors,
371 );
372
373 print_template_category(
375 "Specialized Development",
376 &[
377 ("cli-tool", "CLI tool with argument parsing and completion"),
378 ("web-api", "REST/HTTP API with error handling"),
379 (
380 "ui-component",
381 "UI component with accessibility and responsive design",
382 ),
383 ("onboarding", "Learn a new codebase efficiently"),
384 ],
385 colors,
386 );
387
388 print_template_category(
390 "Advanced & Infrastructure",
391 &[
392 (
393 "performance-optimization",
394 "Performance optimization with benchmarking",
395 ),
396 (
397 "security-audit",
398 "Security audit with OWASP Top 10 coverage",
399 ),
400 (
401 "api-integration",
402 "API integration with retry logic and resilience",
403 ),
404 (
405 "database-migration",
406 "Database migration with zero-downtime strategies",
407 ),
408 (
409 "dependency-update",
410 "Dependency update with breaking change handling",
411 ),
412 ("data-pipeline", "Data pipeline with ETL and monitoring"),
413 ],
414 colors,
415 );
416
417 print_template_category(
419 "Maintenance & Operations",
420 &[
421 (
422 "debug-triage",
423 "Systematic issue investigation and diagnosis",
424 ),
425 (
426 "tech-debt",
427 "Technical debt refactoring with prioritization",
428 ),
429 (
430 "release",
431 "Release preparation with versioning and changelog",
432 ),
433 ],
434 colors,
435 );
436
437 println!("Usage: ralph --init <work-guide>");
438 println!(" ralph --init-prompt <work-guide>");
439 println!();
440 println!("Example:");
441 println!(" ralph --init bug-fix # Create bug fix Work Guide");
442 println!(" ralph --init feature-spec # Create feature spec Work Guide");
443 println!(" ralph --init quick # Create quick change Work Guide");
444 println!();
445 println!("{}Tip:{}", colors.yellow(), colors.reset());
446 println!(" Use --init without a value to auto-detect what you need.");
447 println!(" Use --force-overwrite to overwrite an existing PROMPT.md.");
448 println!(" Run ralph --extended-help to learn about Work Guides vs Agent Prompts.");
449
450 true
451}
452
453pub fn handle_smart_init_with<R: ConfigEnvironment>(
473 template_arg: Option<&str>,
474 force: bool,
475 colors: Colors,
476 env: &R,
477) -> anyhow::Result<bool> {
478 let config_path = env
479 .unified_config_path()
480 .ok_or_else(|| anyhow::anyhow!("Cannot determine config directory (no home directory)"))?;
481 let prompt_path = env.prompt_path();
482 handle_smart_init_at_paths_with_env(
483 template_arg,
484 force,
485 colors,
486 &config_path,
487 &prompt_path,
488 env,
489 )
490}
491
492pub fn handle_smart_init(
507 template_arg: Option<&str>,
508 force: bool,
509 colors: Colors,
510) -> anyhow::Result<bool> {
511 handle_smart_init_with(template_arg, force, colors, &RealConfigEnvironment)
512}
513
514fn handle_smart_init_at_paths_with_env<R: ConfigEnvironment>(
515 template_arg: Option<&str>,
516 force: bool,
517 colors: Colors,
518 config_path: &std::path::Path,
519 prompt_path: &Path,
520 env: &R,
521) -> anyhow::Result<bool> {
522 let config_exists = env.file_exists(config_path);
523 let prompt_exists = env.file_exists(prompt_path);
524
525 if let Some(template_name) = template_arg {
527 if !template_name.is_empty() {
528 return handle_init_template_arg_at_path_with_env(
529 template_name,
530 prompt_path,
531 force,
532 colors,
533 env,
534 );
535 }
536 }
538
539 handle_init_state_inference_with_env(
541 config_path,
542 prompt_path,
543 config_exists,
544 prompt_exists,
545 force,
546 colors,
547 env,
548 )
549}
550
551fn levenshtein_distance(a: &str, b: &str) -> usize {
556 let a_chars: Vec<char> = a.chars().collect();
557 let b_chars: Vec<char> = b.chars().collect();
558 let b_len = b_chars.len();
559
560 let mut prev_row: Vec<usize> = (0..=b_len).collect();
562 let mut curr_row = vec![0; b_len + 1];
563
564 for (i, a_char) in a_chars.iter().enumerate() {
565 curr_row[0] = i + 1;
566
567 for (j, b_char) in b_chars.iter().enumerate() {
568 let cost = usize::from(a_char != b_char);
569 curr_row[j + 1] = std::cmp::min(
570 std::cmp::min(
571 curr_row[j] + 1, prev_row[j + 1] + 1, ),
574 prev_row[j] + cost, );
576 }
577
578 std::mem::swap(&mut prev_row, &mut curr_row);
579 }
580
581 prev_row[b_len]
582}
583
584fn similarity_percentage(a: &str, b: &str) -> u32 {
588 if a == b {
589 return 100;
590 }
591 if a.is_empty() || b.is_empty() {
592 return 0;
593 }
594
595 let max_len = a.len().max(b.len());
596 let distance = levenshtein_distance(a, b);
597
598 if max_len == 0 {
599 return 100;
600 }
601
602 let diff = max_len.saturating_sub(distance);
605 u32::try_from((100 * diff) / max_len).unwrap_or(0)
607}
608
609fn find_similar_templates(input: &str) -> Vec<(&'static str, u32)> {
613 let input_lower = input.to_lowercase();
614 let mut matches: Vec<(&'static str, u32)> = ALL_TEMPLATES
615 .iter()
616 .map(|t| {
617 let name = t.name();
618 let sim = similarity_percentage(&input_lower, &name.to_lowercase());
619 (name, sim)
620 })
621 .filter(|(_, sim)| *sim >= MIN_SIMILARITY_PERCENT)
622 .collect();
623
624 matches.sort_by(|a, b| b.1.cmp(&a.1));
626
627 matches.truncate(3);
629 matches
630}
631
632fn prompt_for_template(colors: Colors) -> Option<String> {
637 use std::io;
638
639 let target = prompt_output_target()?;
640 if with_prompt_writer(target, |w| {
641 let _ = writeln!(
642 w,
643 "PROMPT.md contains your task specification for the AI agents."
644 );
645 let _ = write!(w, "Would you like to create one from a Work Guide? [Y/n]: ");
646 w.flush()?;
647 Ok(())
648 })
649 .is_err()
650 {
651 return None;
652 };
653
654 let mut input = String::new();
655 match io::stdin().read_line(&mut input) {
656 Ok(0) | Err(_) => return None,
657 Ok(_) => {}
658 }
659
660 let response = input.trim().to_lowercase();
661 if response == "n" || response == "no" || response == "skip" {
662 return None;
663 }
664
665 let templates: Vec<(&str, &str)> = list_templates();
667 if with_prompt_writer(target, |w| {
668 let _ = writeln!(w);
669 let _ = writeln!(w, "Available Work Guides:");
670
671 for (i, (name, description)) in templates.iter().enumerate() {
672 let _ = writeln!(
673 w,
674 " {}{}{} {}{}{}",
675 colors.cyan(),
676 name,
677 colors.reset(),
678 colors.dim(),
679 description,
680 colors.reset()
681 );
682 if (i + 1) % 5 == 0 {
683 let _ = writeln!(w); }
685 }
686
687 let _ = writeln!(w);
688 let _ = writeln!(w, "Common choices:");
689 let _ = writeln!(
690 w,
691 " {}quick{} - Quick/small changes (typos, minor fixes)",
692 colors.cyan(),
693 colors.reset()
694 );
695 let _ = writeln!(
696 w,
697 " {}bug-fix{} - Bug fix with investigation guidance",
698 colors.cyan(),
699 colors.reset()
700 );
701 let _ = writeln!(
702 w,
703 " {}feature-spec{} - Product specification",
704 colors.cyan(),
705 colors.reset()
706 );
707 let _ = writeln!(w);
708 let _ = write!(w, "Enter Work Guide name (or press Enter to use 'quick'): ");
709 w.flush()?;
710 Ok(())
711 })
712 .is_err()
713 {
714 return None;
715 };
716
717 let mut template_input = String::new();
718 match io::stdin().read_line(&mut template_input) {
719 Ok(0) | Err(_) => return None,
720 Ok(_) => {}
721 }
722
723 let template_name = template_input.trim();
724 if template_name.is_empty() {
725 return Some("quick".to_string());
727 }
728
729 if get_template(template_name).is_some() {
731 Some(template_name.to_string())
732 } else {
733 let _ = with_prompt_writer(target, |w| {
734 writeln!(
735 w,
736 "{}Unknown Work Guide: '{}'{}",
737 colors.red(),
738 template_name,
739 colors.reset()
740 )?;
741 writeln!(
742 w,
743 "Run 'ralph --list-work-guides' to see all available Work Guides."
744 )?;
745 Ok(())
746 });
747 None
748 }
749}
750
751fn create_minimal_prompt_md() -> String {
753 "# Task Description
754
755Describe what you want the AI agents to implement.
756
757## Example
758
759\"Fix the typo in the README file\"
760
761## Context
762
763Provide any relevant context about the task:
764- What problem are you trying to solve?
765- What are the acceptance criteria?
766- Are there any specific requirements or constraints?
767
768## Notes
769
770- This is a minimal PROMPT.md created by `ralph --init`
771- You can edit this file directly or use `ralph --init <work-guide>` to start from a Work Guide
772- Run `ralph --list-work-guides` to see all available Work Guides
773"
774 .to_string()
775}
776
777fn handle_init_both_exist(
779 config_path: &std::path::Path,
780 prompt_path: &Path,
781 force: bool,
782 colors: Colors,
783) -> bool {
784 if force {
786 println!(
787 "{}Note:{} --force-overwrite has no effect when not specifying a Work Guide.",
788 colors.yellow(),
789 colors.reset()
790 );
791 println!("Use: ralph --init <work-guide> --force-overwrite to overwrite PROMPT.md");
792 println!();
793 }
794
795 println!("{}Setup complete!{}", colors.green(), colors.reset());
796 println!();
797 println!(
798 " Config: {}{}{}",
799 colors.dim(),
800 config_path.display(),
801 colors.reset()
802 );
803 println!(
804 " PROMPT: {}{}{}",
805 colors.dim(),
806 prompt_path.display(),
807 colors.reset()
808 );
809 println!();
810 println!("You're ready to run Ralph:");
811 println!(" ralph \"your commit message\"");
812 println!();
813 println!("Other commands:");
814 println!(" ralph --list-work-guides # Show all Work Guides");
815 println!(" ralph --init <work-guide> --force-overwrite # Overwrite PROMPT.md");
816 true
817}
818
819fn handle_init_prompt_at_path_with_env<R: ConfigEnvironment>(
827 template_name: &str,
828 prompt_path: &Path,
829 force: bool,
830 colors: Colors,
831 env: &R,
832) -> anyhow::Result<bool> {
833 let Some(template) = get_template(template_name) else {
835 println!(
836 "{}Unknown Work Guide: '{}'{}",
837 colors.red(),
838 template_name,
839 colors.reset()
840 );
841 println!();
842 let similar = find_similar_templates(template_name);
843 if !similar.is_empty() {
844 println!("{}Did you mean?{}", colors.yellow(), colors.reset());
845 for (name, score) in similar {
846 println!(
847 " {}{}{} ({}% similar)",
848 colors.cyan(),
849 name,
850 colors.reset(),
851 score
852 );
853 }
854 println!();
855 }
856 println!("Commonly used Work Guides:");
857 print_common_work_guides(colors);
858 println!("Usage: ralph --init-prompt <work-guide>");
859 return Ok(true);
860 };
861
862 let content = template.content();
863
864 let file_exists = env.file_exists(prompt_path);
866
867 if force || !file_exists {
868 env.write_file(prompt_path, content)?;
870 } else {
871 if can_prompt_user() {
873 if !prompt_overwrite_confirmation(prompt_path, colors)? {
874 return Ok(true);
875 }
876 env.write_file(prompt_path, content)?;
877 } else {
878 return Err(anyhow::anyhow!(
879 "PROMPT.md already exists: {}\nRefusing to overwrite in non-interactive mode. Use --force-overwrite to overwrite, or delete/backup the existing file.",
880 prompt_path.display()
881 ));
882 }
883 }
884
885 println!(
886 "{}Created PROMPT.md from template: {}{}{}",
887 colors.green(),
888 colors.bold(),
889 template_name,
890 colors.reset()
891 );
892 println!();
893 println!(
894 "Template: {}{}{} {}",
895 colors.cyan(),
896 template.name(),
897 colors.reset(),
898 template.description()
899 );
900 println!();
901 println!("Next steps:");
902 println!(" 1. Edit PROMPT.md with your task details");
903 println!(" 2. Run: ralph \"your commit message\"");
904 println!();
905 println!("Tip: Use --list-work-guides to see all available Work Guides.");
906
907 Ok(true)
908}
909
910fn handle_init_template_arg_at_path_with_env<R: ConfigEnvironment>(
912 template_name: &str,
913 prompt_path: &Path,
914 force: bool,
915 colors: Colors,
916 env: &R,
917) -> anyhow::Result<bool> {
918 if get_template(template_name).is_some() {
919 return handle_init_prompt_at_path_with_env(template_name, prompt_path, force, colors, env);
920 }
921
922 println!(
924 "{}Unknown Work Guide: '{}'{}",
925 colors.red(),
926 template_name,
927 colors.reset()
928 );
929 println!();
930
931 let similar = find_similar_templates(template_name);
933 if !similar.is_empty() {
934 println!("{}Did you mean?{}", colors.yellow(), colors.reset());
935 for (name, score) in similar {
936 println!(
937 " {}{}{} ({}% similar)",
938 colors.cyan(),
939 name,
940 colors.reset(),
941 score
942 );
943 }
944 println!();
945 }
946
947 println!("Commonly used Work Guides:");
948 print_common_work_guides(colors);
949 println!("Usage: ralph --init=<work-guide>");
950 println!(" ralph --init # Smart init (infers intent)");
951 Ok(true)
952}
953
954fn handle_init_state_inference_with_env<R: ConfigEnvironment>(
956 config_path: &std::path::Path,
957 prompt_path: &Path,
958 config_exists: bool,
959 prompt_exists: bool,
960 force: bool,
961 colors: Colors,
962 env: &R,
963) -> anyhow::Result<bool> {
964 match (config_exists, prompt_exists) {
965 (false, false) => handle_init_none_exist_with_env(config_path, colors, env),
966 (true, false) => Ok(handle_init_only_config_exists_with_env(
967 config_path,
968 prompt_path,
969 force,
970 colors,
971 env,
972 )),
973 (false, true) => handle_init_only_prompt_exists_with_env(colors, env),
974 (true, true) => Ok(handle_init_both_exist(
975 config_path,
976 prompt_path,
977 force,
978 colors,
979 )),
980 }
981}
982
983fn handle_init_none_exist_with_env<R: ConfigEnvironment>(
985 _config_path: &std::path::Path,
986 colors: Colors,
987 env: &R,
988) -> anyhow::Result<bool> {
989 println!(
990 "{}No config found. Creating unified config...{}",
991 colors.dim(),
992 colors.reset()
993 );
994 println!();
995 handle_init_global_with(colors, env)?;
996 Ok(true)
997}
998
999fn handle_init_only_config_exists_with_env<R: ConfigEnvironment>(
1001 config_path: &std::path::Path,
1002 prompt_path: &Path,
1003 force: bool,
1004 colors: Colors,
1005 env: &R,
1006) -> bool {
1007 println!(
1008 "{}Config found at:{} {}",
1009 colors.green(),
1010 colors.reset(),
1011 config_path.display()
1012 );
1013 println!(
1014 "{}PROMPT.md not found in current directory.{}",
1015 colors.yellow(),
1016 colors.reset()
1017 );
1018 println!();
1019
1020 print_common_work_guides(colors);
1022
1023 if can_prompt_user() {
1025 if let Some(template_name) = prompt_for_template(colors) {
1027 match handle_init_prompt_at_path_with_env(
1028 &template_name,
1029 prompt_path,
1030 force,
1031 colors,
1032 env,
1033 ) {
1034 Ok(_) => return true,
1035 Err(e) => {
1036 println!(
1037 "{}Failed to create PROMPT.md: {}{}",
1038 colors.red(),
1039 e,
1040 colors.reset()
1041 );
1042 return true;
1043 }
1044 }
1045 }
1046 } else {
1048 let default_content = create_minimal_prompt_md();
1050
1051 if env.file_exists(prompt_path) {
1053 println!(
1054 "{}PROMPT.md already exists:{} {}",
1055 colors.yellow(),
1056 colors.reset(),
1057 prompt_path.display()
1058 );
1059 println!("Use --force-overwrite to overwrite, or delete/backup the existing file.");
1060 return true;
1061 }
1062
1063 match env.write_file(prompt_path, &default_content) {
1065 Ok(()) => {
1066 println!(
1067 "{}Created minimal PROMPT.md{}",
1068 colors.green(),
1069 colors.reset()
1070 );
1071 println!();
1072 println!("Next steps:");
1073 println!(" 1. Edit PROMPT.md with your task details");
1074 println!(" 2. Run: ralph \"your commit message\"");
1075 println!();
1076 println!("Tip: Use ralph --list-work-guides to see all available Work Guides.");
1077 return true;
1078 }
1079 Err(e) => {
1080 println!(
1081 "{}Failed to create PROMPT.md: {}{}",
1082 colors.red(),
1083 e,
1084 colors.reset()
1085 );
1086 return true;
1087 }
1088 }
1089 }
1090
1091 println!("Create a PROMPT.md from a Work Guide to get started:");
1093 println!();
1094
1095 for (name, description) in list_templates() {
1096 println!(
1097 " {}{}{} {}{}{}",
1098 colors.cyan(),
1099 name,
1100 colors.reset(),
1101 colors.dim(),
1102 description,
1103 colors.reset()
1104 );
1105 }
1106
1107 println!();
1108 println!("Usage: ralph --init <work-guide>");
1109 println!(" ralph --init-prompt <work-guide>");
1110 println!();
1111 println!("Example:");
1112 println!(" ralph --init bug-fix");
1113 println!(" ralph --init feature-spec");
1114 true
1115}
1116
1117fn handle_init_only_prompt_exists_with_env<R: ConfigEnvironment>(
1119 colors: Colors,
1120 env: &R,
1121) -> anyhow::Result<bool> {
1122 println!(
1123 "{}PROMPT.md found in current directory.{}",
1124 colors.green(),
1125 colors.reset()
1126 );
1127 println!(
1128 "{}No config found. Creating unified config...{}",
1129 colors.dim(),
1130 colors.reset()
1131 );
1132 println!();
1133 handle_init_global_with(colors, env)?;
1134 Ok(true)
1135}
1136
1137pub fn handle_extended_help() {
1142 println!(
1143 r#"RALPH EXTENDED HELP
1144═══════════════════════════════════════════════════════════════════════════════
1145
1146Ralph is a PROMPT-driven multi-agent orchestrator for git repos. It runs a
1147developer agent for code implementation, then a reviewer agent for quality
1148assurance, automatically staging and committing the final result.
1149
1150═══════════════════════════════════════════════════════════════════════════════
1151GETTING STARTED
1152═══════════════════════════════════════════════════════════════════════════════
1153
1154 1. Initialize config:
1155 ralph --init # Smart init (infers what you need)
1156
1157 2. Create a PROMPT.md from a Work Guide:
1158 ralph --init feature-spec # Or: bug-fix, refactor, quick, etc.
1159
1160 3. Edit PROMPT.md with your task details
1161
1162 4. Run Ralph:
1163 ralph "fix: my bug description" # Commit message for the final commit
1164
1165═══════════════════════════════════════════════════════════════════════════════
1166WORK GUIDES VS AGENT PROMPTS
1167═══════════════════════════════════════════════════════════════════════════════
1168
1169 Ralph has two types of templates - understanding the difference is key:
1170
1171 1. WORK GUIDES (for PROMPT.md - YOUR task descriptions)
1172 ─────────────────────────────────────────────────────
1173 These are templates for describing YOUR work to the AI.
1174 You fill them in with your specific task requirements.
1175
1176 Examples: quick, bug-fix, feature-spec, refactor, test, docs
1177
1178 Commands:
1179 ralph --init <work-guide> Create PROMPT.md from a Work Guide
1180 ralph --list-work-guides Show all available Work Guides
1181 ralph --init-prompt <name> Same as --init (legacy alias)
1182
1183 2. AGENT PROMPTS (backend AI behavior configuration)
1184 ─────────────────────────────────────────────────────
1185 These configure HOW the AI agents behave (internal system prompts).
1186 You probably don't need to touch these unless customizing agent behavior.
1187
1188 Commands:
1189 ralph --init-system-prompts Create default Agent Prompts
1190 ralph --list Show Agent Prompt templates
1191 ralph --show <name> Show a specific Agent Prompt
1192
1193═══════════════════════════════════════════════════════════════════════════════
1194PRESET MODES
1195═══════════════════════════════════════════════════════════════════════════════
1196
1197 Pick how thorough the AI should be:
1198
1199 -Q Quick: 1 dev iteration + 1 review (typos, small fixes)
1200 -U Rapid: 2 dev iterations + 1 review (minor changes)
1201 -S Standard: 5 dev iterations + 2 reviews (default for most tasks)
1202 -T Thorough: 10 dev iterations + 5 reviews (complex features)
1203 -L Long: 15 dev iterations + 10 reviews (most thorough)
1204
1205 Custom iterations:
1206 ralph -D 3 -R 2 "feat: feature" # 3 dev iterations, 2 review cycles
1207 ralph -D 10 -R 0 "feat: no review" # Skip review phase entirely
1208
1209═══════════════════════════════════════════════════════════════════════════════
1210COMMON OPTIONS
1211═══════════════════════════════════════════════════════════════════════════════
1212
1213 Iterations:
1214 -D N, --developer-iters N Set developer iterations
1215 -R N, --reviewer-reviews N Set review cycles (0 = skip review)
1216
1217 Agents:
1218 -a AGENT, --developer-agent AGENT Pick developer agent
1219 -r AGENT, --reviewer-agent AGENT Pick reviewer agent
1220
1221 Verbosity:
1222 -q, --quiet Quiet mode (minimal output)
1223 -f, --full Full output (no truncation)
1224 -v N, --verbosity N Set verbosity (0-4)
1225
1226 Other:
1227 -d, --diagnose Show system info and agent status
1228
1229═══════════════════════════════════════════════════════════════════════════════
1230ADVANCED OPTIONS
1231═══════════════════════════════════════════════════════════════════════════════
1232
1233 These options are hidden from the main --help to reduce clutter.
1234
1235 Initialization:
1236 --force-overwrite Overwrite PROMPT.md without prompting
1237 --init-prompt <name> Create PROMPT.md (legacy, use --init instead)
1238 -i, --interactive Prompt for PROMPT.md if missing
1239
1240 Git Control:
1241 --skip-rebase Skip automatic rebase to main branch
1242 --rebase-only Only rebase, then exit (no pipeline)
1243 --git-user-name <name> Override git user name for commits
1244 --git-user-email <email> Override git user email for commits
1245
1246 Recovery:
1247 --resume Resume from last checkpoint
1248 --dry-run Validate setup without running agents
1249
1250 Agent Prompt Management:
1251 --init-system-prompts Create default Agent Prompt templates
1252 --list List all Agent Prompt templates
1253 --show <name> Show Agent Prompt content
1254 --validate Validate Agent Prompt templates
1255 --variables <name> Extract variables from template
1256 --render <name> Test render a template
1257
1258 Debugging:
1259 --show-streaming-metrics Show JSON streaming quality metrics
1260 -c PATH, --config PATH Use specific config file
1261
1262═══════════════════════════════════════════════════════════════════════════════
1263SHELL COMPLETION
1264═══════════════════════════════════════════════════════════════════════════════
1265
1266 Enable tab-completion for faster command entry:
1267
1268 Bash:
1269 ralph --generate-completion=bash > ~/.local/share/bash-completion/completions/ralph
1270
1271 Zsh:
1272 ralph --generate-completion=zsh > ~/.zsh/completion/_ralph
1273
1274 Fish:
1275 ralph --generate-completion=fish > ~/.config/fish/completions/ralph.fish
1276
1277 Then restart your shell or source the file.
1278
1279═══════════════════════════════════════════════════════════════════════════════
1280TROUBLESHOOTING
1281═══════════════════════════════════════════════════════════════════════════════
1282
1283 Common issues:
1284
1285 "PROMPT.md not found"
1286 → Run: ralph --init <work-guide> (e.g., ralph --init bug-fix)
1287
1288 "No agents available"
1289 → Run: ralph -d (diagnose) to check agent status
1290 → Ensure at least one agent is installed (claude, codex, opencode)
1291
1292 "Config file not found"
1293 → Run: ralph --init to create ~/.config/ralph-workflow.toml
1294
1295 Resume after interruption:
1296 → Run: ralph --resume to continue from last checkpoint
1297
1298 Validate setup without running:
1299 → Run: ralph --dry-run
1300
1301═══════════════════════════════════════════════════════════════════════════════
1302EXAMPLES
1303═══════════════════════════════════════════════════════════════════════════════
1304
1305 ralph "fix: typo" Run with default settings
1306 ralph -Q "fix: small bug" Quick mode for tiny fixes
1307 ralph -U "feat: add button" Rapid mode for minor features
1308 ralph -a claude "fix: bug" Use specific agent
1309 ralph --list-work-guides See all Work Guides
1310 ralph --init bug-fix Create PROMPT.md from a Work Guide
1311 ralph --init bug-fix --force-overwrite Overwrite existing PROMPT.md
1312
1313═══════════════════════════════════════════════════════════════════════════════
1314"#
1315 );
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320 use super::*;
1321 use crate::config::MemoryConfigEnvironment;
1322
1323 fn test_env() -> MemoryConfigEnvironment {
1325 MemoryConfigEnvironment::new()
1326 .with_unified_config_path("/test/config/ralph-workflow.toml")
1327 .with_prompt_path("/test/repo/PROMPT.md")
1328 }
1329
1330 #[test]
1331 fn test_handle_smart_init_with_valid_template_creates_prompt_md() {
1332 let env = test_env();
1333 let colors = Colors::new();
1334
1335 let result = handle_smart_init_with(Some("quick"), false, colors, &env).unwrap();
1336 assert!(result);
1337
1338 let prompt_path = env.prompt_path();
1340 assert!(env.file_exists(&prompt_path));
1341
1342 let template = get_template("quick").unwrap();
1343 let content = env.read_file(&prompt_path).unwrap();
1344 assert_eq!(content, template.content());
1345 }
1346
1347 #[test]
1348 fn test_handle_smart_init_with_invalid_template_does_not_create_prompt_md() {
1349 let env = test_env();
1350 let colors = Colors::new();
1351
1352 let result =
1353 handle_smart_init_with(Some("nonexistent-template"), false, colors, &env).unwrap();
1354 assert!(result);
1355
1356 let prompt_path = env.prompt_path();
1358 assert!(!env.file_exists(&prompt_path));
1359 }
1360
1361 #[test]
1362 fn test_handle_init_prompt_does_not_overwrite_existing_prompt_without_force() {
1363 let env = test_env();
1364 let prompt_path = env.prompt_path();
1365
1366 env.write_file(&prompt_path, "original").unwrap();
1368
1369 let colors = Colors::new();
1370 let err = handle_init_prompt_with("quick", false, colors, &env).unwrap_err();
1371 assert!(err
1372 .to_string()
1373 .contains("Refusing to overwrite in non-interactive mode"));
1374
1375 let content = env.read_file(&prompt_path).unwrap();
1377 assert_eq!(content, "original");
1378 }
1379
1380 #[test]
1381 fn test_handle_init_prompt_overwrites_existing_prompt_with_force() {
1382 let env = test_env();
1383 let prompt_path = env.prompt_path();
1384
1385 env.write_file(&prompt_path, "original").unwrap();
1387
1388 let colors = Colors::new();
1389 let result = handle_init_prompt_with("quick", true, colors, &env).unwrap();
1390 assert!(result);
1391
1392 let template = get_template("quick").unwrap();
1393 let content = env.read_file(&prompt_path).unwrap();
1394 assert_eq!(content, template.content());
1395 }
1396
1397 #[test]
1398 fn test_template_name_validation() {
1399 assert!(get_template("bug-fix").is_some());
1401 assert!(get_template("feature-spec").is_some());
1402 assert!(get_template("refactor").is_some());
1403 assert!(get_template("test").is_some());
1404 assert!(get_template("docs").is_some());
1405 assert!(get_template("quick").is_some());
1406
1407 assert!(get_template("invalid").is_none());
1409 assert!(get_template("").is_none());
1410 }
1411
1412 #[test]
1413 fn test_levenshtein_distance() {
1414 assert_eq!(levenshtein_distance("test", "test"), 0);
1416
1417 assert_eq!(levenshtein_distance("test", "tast"), 1);
1419 assert_eq!(levenshtein_distance("test", "tests"), 1);
1420 assert_eq!(levenshtein_distance("test", "est"), 1);
1421
1422 assert_eq!(levenshtein_distance("test", "taste"), 2);
1424 assert_eq!(levenshtein_distance("test", "best"), 1);
1425
1426 assert_eq!(levenshtein_distance("abc", "xyz"), 3);
1428 }
1429
1430 #[test]
1431 fn test_similarity() {
1432 assert_eq!(similarity_percentage("test", "test"), 100);
1434
1435 assert!(similarity_percentage("bug-fix", "bugfix") > 80);
1437 assert!(similarity_percentage("feature-spec", "feature") > 50);
1438
1439 assert!(similarity_percentage("test", "xyz") < 50);
1441
1442 assert_eq!(similarity_percentage("", ""), 100);
1444 assert_eq!(similarity_percentage("test", ""), 0);
1445 assert_eq!(similarity_percentage("", "test"), 0);
1446 }
1447
1448 #[test]
1449 fn test_find_similar_templates() {
1450 let similar = find_similar_templates("bugfix");
1452 assert!(!similar.is_empty());
1453 assert!(similar.iter().any(|(name, _)| *name == "bug-fix"));
1454
1455 let similar = find_similar_templates("feature");
1457 assert!(!similar.is_empty());
1458 assert!(similar.iter().any(|(name, _)| name.contains("feature")));
1459
1460 let similar = find_similar_templates("xyzabc");
1462 assert!(similar.is_empty() || similar.iter().all(|(_, sim)| *sim < 50));
1464 }
1465}