ralph_workflow/prompts/
mod.rs

1//! Prompt Templates Module
2//!
3//! Provides context-controlled prompts for agents.
4//! Key design: reviewers get minimal context for "fresh eyes" perspective.
5//!
6//! Enhanced with language-specific review guidelines based on detected project stack.
7//!
8//! # Module Structure
9//!
10//! - [`types`] - Type definitions (`ContextLevel`, Role, Action)
11//! - [`developer`] - Developer prompts (iteration, planning)
12//! - [`reviewer`] - Reviewer prompts (review, comprehensive, security, incremental)
13//! - [`commit`] - Fix and commit message prompts
14//! - [`rebase`] - Conflict resolution prompts for auto-rebase
15//! - [`partials`] - Shared template partials for composition
16
17mod commit;
18mod developer;
19pub mod partials;
20mod rebase;
21pub mod reviewer;
22pub mod template_catalog;
23pub mod template_context;
24mod template_engine;
25mod template_macros;
26pub mod template_registry;
27mod template_validator;
28mod types;
29
30// Re-export all public items for backward compatibility
31pub use commit::{
32    prompt_emergency_commit_with_context, prompt_emergency_no_diff_commit_with_context,
33    prompt_file_list_only_commit_with_context, prompt_file_list_summary_only_commit_with_context,
34    prompt_fix_with_context, prompt_generate_commit_message_with_diff_with_context,
35    prompt_strict_json_commit_v2_with_context, prompt_strict_json_commit_with_context,
36    prompt_ultra_minimal_commit_v2_with_context, prompt_ultra_minimal_commit_with_context,
37};
38pub use developer::{prompt_developer_iteration_with_context, prompt_plan_with_context};
39pub use rebase::{
40    build_conflict_resolution_prompt_with_context, collect_conflict_info, FileConflict,
41};
42pub use reviewer::{
43    prompt_comprehensive_review_with_diff_with_context,
44    prompt_detailed_review_without_guidelines_with_diff_with_context,
45    prompt_incremental_review_with_diff_with_context,
46    prompt_reviewer_review_with_guidelines_and_diff_with_context,
47    prompt_security_focused_review_with_diff_with_context,
48    prompt_universal_review_with_diff_with_context,
49};
50
51// Re-export non-context variants for test compatibility
52#[cfg(test)]
53pub use commit::{prompt_fix, prompt_generate_commit_message_with_diff};
54#[cfg(test)]
55pub use developer::{prompt_developer_iteration, prompt_plan};
56pub use template_context::TemplateContext;
57pub use template_engine::Template;
58pub use template_validator::{
59    extract_metadata, extract_partials, extract_variables, validate_template, ValidationError,
60    ValidationWarning,
61};
62pub use types::{Action, ContextLevel, Role};
63
64/// Configuration for prompt generation.
65///
66/// Groups related parameters to reduce function argument count.
67#[derive(Debug, Clone, Default, PartialEq, Eq)]
68#[must_use]
69pub struct PromptConfig {
70    /// The current iteration number (for developer iteration prompts).
71    pub iteration: Option<u32>,
72    /// The total number of iterations (for developer iteration prompts).
73    pub total_iterations: Option<u32>,
74    /// PROMPT.md content for planning prompts.
75    pub prompt_md_content: Option<String>,
76    /// (PROMPT.md, PLAN.md) content tuple for developer iteration prompts.
77    pub prompt_and_plan: Option<(String, String)>,
78    /// (PROMPT.md, PLAN.md, ISSUES.md) content tuple for fix prompts.
79    pub prompt_plan_and_issues: Option<(String, String, String)>,
80}
81
82impl PromptConfig {
83    /// Create a new prompt configuration with default values.
84    #[must_use = "configuration is required for prompt generation"]
85    pub const fn new() -> Self {
86        Self {
87            iteration: None,
88            total_iterations: None,
89            prompt_md_content: None,
90            prompt_and_plan: None,
91            prompt_plan_and_issues: None,
92        }
93    }
94
95    /// Set iteration numbers for developer iteration prompts.
96    #[must_use = "returns the updated configuration for chaining"]
97    pub const fn with_iterations(mut self, iteration: u32, total: u32) -> Self {
98        self.iteration = Some(iteration);
99        self.total_iterations = Some(total);
100        self
101    }
102
103    /// Set PROMPT.md content for planning prompts.
104    #[must_use = "returns the updated configuration for chaining"]
105    pub fn with_prompt_md(mut self, content: String) -> Self {
106        self.prompt_md_content = Some(content);
107        self
108    }
109
110    /// Set (PROMPT.md, PLAN.md) content tuple for developer iteration prompts.
111    #[must_use = "returns the updated configuration for chaining"]
112    pub fn with_prompt_and_plan(mut self, prompt: String, plan: String) -> Self {
113        self.prompt_and_plan = Some((prompt, plan));
114        self
115    }
116
117    /// Set (PROMPT.md, PLAN.md, ISSUES.md) content tuple for fix prompts.
118    pub fn with_prompt_plan_and_issues(
119        mut self,
120        prompt: String,
121        plan: String,
122        issues: String,
123    ) -> Self {
124        self.prompt_plan_and_issues = Some((prompt, plan, issues));
125        self
126    }
127}
128
129/// Generate a prompt for any agent type.
130///
131/// This is the main dispatcher function that routes to the appropriate
132/// prompt generator based on role and action.
133///
134/// The config parameter allows providing:
135/// - Language-specific review guidance when the project stack has been detected
136/// - PROMPT.md content for planning prompts
137/// - PROMPT.md and PLAN.md content for developer iteration prompts
138///
139/// # Arguments
140///
141/// * `role` - The agent role (Developer, Reviewer, etc.)
142/// * `action` - The action to perform (Plan, Iterate, Fix, etc.)
143/// * `context` - The context level (minimal or normal)
144/// * `template_context` - Template context for user template overrides
145/// * `config` - Prompt configuration with content variables
146pub fn prompt_for_agent(
147    role: Role,
148    action: Action,
149    context: ContextLevel,
150    template_context: &TemplateContext,
151    config: PromptConfig,
152) -> String {
153    match (role, action) {
154        (_, Action::Plan) => {
155            prompt_plan_with_context(template_context, config.prompt_md_content.as_deref())
156        }
157        (Role::Developer | Role::Reviewer, Action::Iterate) => {
158            let (prompt_content, plan_content) = config
159                .prompt_and_plan
160                .unwrap_or((String::new(), String::new()));
161            prompt_developer_iteration_with_context(
162                template_context,
163                config.iteration.unwrap_or(1),
164                config.total_iterations.unwrap_or(1),
165                context,
166                &prompt_content,
167                &plan_content,
168            )
169        }
170        (_, Action::Fix) => {
171            let (prompt_content, plan_content, issues_content) = config
172                .prompt_plan_and_issues
173                .unwrap_or((String::new(), String::new(), String::new()));
174            prompt_fix_with_context(
175                template_context,
176                &prompt_content,
177                &plan_content,
178                &issues_content,
179            )
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::prompts::template_context::TemplateContext;
188
189    // Import non-context variants for test compatibility
190    use crate::prompts::reviewer::prompt_detailed_review_without_guidelines_with_diff;
191
192    #[test]
193    fn test_prompt_for_agent_developer() {
194        let template_context = TemplateContext::default();
195        let result = prompt_for_agent(
196            Role::Developer,
197            Action::Iterate,
198            ContextLevel::Normal,
199            &template_context,
200            PromptConfig::new()
201                .with_iterations(3, 10)
202                .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
203        );
204        // Agent should NOT be told to read PROMPT.md (orchestrator handles it)
205        assert!(!result.contains("PROMPT.md"));
206        assert!(result.contains("test prompt"));
207        assert!(result.contains("test plan"));
208    }
209
210    #[test]
211    fn test_prompt_for_agent_reviewer() {
212        let result = prompt_detailed_review_without_guidelines_with_diff(
213            ContextLevel::Minimal,
214            "sample diff",
215            "",
216            "",
217        );
218        assert!(result.contains("fresh eyes"));
219        assert!(result.contains("DETAILED REVIEW MODE"));
220    }
221
222    #[test]
223    fn test_prompt_for_agent_plan() {
224        let template_context = TemplateContext::default();
225        let result = prompt_for_agent(
226            Role::Developer,
227            Action::Plan,
228            ContextLevel::Normal,
229            &template_context,
230            PromptConfig::new().with_prompt_md("test requirements".to_string()),
231        );
232        // Plan is now returned as structured output, not written to file
233        assert!(result.contains("PLANNING MODE"));
234        assert!(result.contains("Implementation Steps"));
235    }
236
237    #[test]
238    fn test_prompts_are_agent_agnostic() {
239        // All prompts should be free of agent-specific references
240        // to ensure they work with any AI coding assistant
241        let agent_specific_terms = [
242            "claude", "codex", "opencode", "gemini", "aider", "goose", "cline", "continue",
243            "amazon-q", "gpt", "copilot",
244        ];
245
246        let prompts_to_check: Vec<String> = vec![
247            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
248            prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
249            prompt_detailed_review_without_guidelines_with_diff(
250                ContextLevel::Normal,
251                "sample diff",
252                "",
253                "",
254            ),
255            prompt_detailed_review_without_guidelines_with_diff(
256                ContextLevel::Minimal,
257                "sample diff",
258                "",
259                "",
260            ),
261            prompt_fix("", "", ""),
262            prompt_plan(None),
263            prompt_generate_commit_message_with_diff("diff --git a/a b/b"),
264        ];
265
266        for prompt in prompts_to_check {
267            let prompt_lower = prompt.to_lowercase();
268            for term in agent_specific_terms {
269                assert!(
270                    !prompt_lower.contains(term),
271                    "Prompt contains agent-specific term '{}': {}",
272                    term,
273                    &prompt[..prompt.len().min(100)]
274                );
275            }
276        }
277    }
278
279    #[test]
280    fn test_prompt_for_agent_fix() {
281        let template_context = TemplateContext::default();
282        let result = prompt_for_agent(
283            Role::Developer,
284            Action::Fix,
285            ContextLevel::Normal,
286            &template_context,
287            PromptConfig::new().with_prompt_plan_and_issues(
288                "test prompt".to_string(),
289                "test plan".to_string(),
290                "test issues".to_string(),
291            ),
292        );
293        assert!(result.contains("FIX MODE"));
294        assert!(result.contains("test issues"));
295        // Should include PROMPT and PLAN context
296        assert!(result.contains("test prompt"));
297        assert!(result.contains("test plan"));
298    }
299
300    #[test]
301    fn test_prompt_for_agent_fix_with_empty_context() {
302        let template_context = TemplateContext::default();
303        let result = prompt_for_agent(
304            Role::Developer,
305            Action::Fix,
306            ContextLevel::Normal,
307            &template_context,
308            PromptConfig::new(),
309        );
310        assert!(result.contains("FIX MODE"));
311        // Should still work with empty context
312        assert!(!result.is_empty());
313    }
314
315    #[test]
316    fn test_reviewer_can_use_iterate_action() {
317        // Edge case: Reviewer using Iterate action (fallback behavior)
318        let template_context = TemplateContext::default();
319        let result = prompt_for_agent(
320            Role::Reviewer,
321            Action::Iterate,
322            ContextLevel::Normal,
323            &template_context,
324            PromptConfig::new()
325                .with_iterations(1, 3)
326                .with_prompt_and_plan(String::new(), String::new()),
327        );
328        // Should fall back to developer iteration prompt
329        assert!(result.contains("IMPLEMENTATION MODE"));
330    }
331
332    #[test]
333    fn test_prompts_do_not_have_detailed_tracking_language() {
334        // Prompts should NOT contain detailed history tracking language
335        // to prevent context contamination in future runs
336        let detailed_tracking_terms = [
337            "iteration number",
338            "phase completed",
339            "previous iteration",
340            "history of",
341            "detailed log",
342        ];
343
344        let prompts_to_check = vec![
345            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
346            prompt_fix("", "", ""),
347        ];
348
349        for prompt in prompts_to_check {
350            let prompt_lower = prompt.to_lowercase();
351            for term in detailed_tracking_terms {
352                assert!(
353                    !prompt_lower.contains(term),
354                    "Prompt contains detailed tracking language '{}': {}",
355                    term,
356                    &prompt[..prompt.len().min(100)]
357                );
358            }
359        }
360    }
361
362    #[test]
363    fn test_developer_notes_md_not_referenced() {
364        // Developer prompt should NOT mention NOTES.md at all (isolation mode)
365        let developer_prompt = prompt_developer_iteration(1, 5, ContextLevel::Normal, "", "");
366        assert!(
367            !developer_prompt.contains("NOTES.md"),
368            "Developer prompt should not reference NOTES.md in isolation mode"
369        );
370    }
371
372    #[test]
373    fn test_all_prompts_isolate_agents_from_git() {
374        // AC3: "AI agent does not know that we have previous committed change"
375        // All prompts should NOT tell agents to run git commands
376        // Git operations are handled by the orchestrator via libgit2
377        let git_command_patterns = [
378            "git diff HEAD",
379            "git status",
380            "git commit",
381            "git add",
382            "git log",
383            "git show",
384            "git reset",
385            "git checkout",
386            "git branch",
387            "Run `git",
388            "execute git",
389        ];
390
391        let prompts_to_check: Vec<String> = vec![
392            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
393            prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
394            prompt_detailed_review_without_guidelines_with_diff(
395                ContextLevel::Normal,
396                "sample diff",
397                "",
398                "",
399            ),
400            prompt_detailed_review_without_guidelines_with_diff(
401                ContextLevel::Minimal,
402                "sample diff",
403                "",
404                "",
405            ),
406            prompt_fix("", "", ""),
407            prompt_plan(None),
408            prompt_generate_commit_message_with_diff("diff --git a/a b/b\n"),
409        ];
410
411        for prompt in prompts_to_check {
412            for pattern in git_command_patterns {
413                assert!(
414                    !prompt.contains(pattern),
415                    "Prompt contains git command pattern '{}': {}",
416                    pattern,
417                    &prompt[..prompt.len().min(100)]
418                );
419            }
420        }
421
422        // Verify the orchestrator-specific function for commit message generation
423        // DOES contain the diff content (orchestrator receives diff, not git commands).
424        // The orchestrator uses this function to pass diff to the LLM via stdin.
425        let orchestrator_prompt = prompt_generate_commit_message_with_diff("some diff");
426        assert!(
427            orchestrator_prompt.contains("DIFF:") || orchestrator_prompt.contains("diff"),
428            "Orchestrator prompt should contain the diff content for commit message generation"
429        );
430        // But the prompt should NOT tell the agent to run git commands (orchestrator handles git)
431        for pattern in git_command_patterns {
432            assert!(
433                !orchestrator_prompt.contains(pattern),
434                "Orchestrator prompt contains git command pattern '{pattern}'"
435            );
436        }
437    }
438}