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