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 ResumeContext for use in prompts
31pub use crate::checkpoint::restore::ResumeContext;
32
33// Re-export all public items for backward compatibility
34pub use commit::{
35    prompt_fix_with_context, prompt_generate_commit_message_with_diff_with_context,
36    prompt_simplified_commit_with_context, prompt_xsd_retry_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};
42
43#[cfg(any(test, feature = "test-utils"))]
44pub use rebase::build_enhanced_conflict_resolution_prompt;
45
46// Types only used in tests
47#[cfg(any(test, feature = "test-utils"))]
48pub use rebase::{collect_branch_info, BranchInfo};
49pub use reviewer::{
50    prompt_comprehensive_review_with_diff_with_context,
51    prompt_detailed_review_without_guidelines_with_diff_with_context,
52    prompt_incremental_review_with_diff_with_context,
53    prompt_reviewer_review_with_guidelines_and_diff_with_context,
54    prompt_security_focused_review_with_diff_with_context,
55    prompt_universal_review_with_diff_with_context,
56};
57
58// Re-export non-context variants for test compatibility
59#[cfg(test)]
60pub use commit::{prompt_fix, prompt_generate_commit_message_with_diff};
61#[cfg(test)]
62pub use developer::{prompt_developer_iteration, prompt_plan};
63pub use template_context::TemplateContext;
64pub use template_engine::Template;
65pub use template_validator::{
66    extract_metadata, extract_partials, extract_variables, validate_template, ValidationError,
67    ValidationWarning,
68};
69pub use types::{Action, ContextLevel, Role};
70
71/// Configuration for prompt generation.
72///
73/// Groups related parameters to reduce function argument count.
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
75#[must_use]
76pub struct PromptConfig {
77    /// The current iteration number (for developer iteration prompts).
78    pub iteration: Option<u32>,
79    /// The total number of iterations (for developer iteration prompts).
80    pub total_iterations: Option<u32>,
81    /// PROMPT.md content for planning prompts.
82    pub prompt_md_content: Option<String>,
83    /// (PROMPT.md, PLAN.md) content tuple for developer iteration prompts.
84    pub prompt_and_plan: Option<(String, String)>,
85    /// (PROMPT.md, PLAN.md, ISSUES.md) content tuple for fix prompts.
86    pub prompt_plan_and_issues: Option<(String, String, String)>,
87    /// Whether this is a resumed session (from a checkpoint).
88    pub is_resume: bool,
89    /// Rich resume context if available.
90    pub resume_context: Option<ResumeContext>,
91}
92
93impl PromptConfig {
94    /// Create a new prompt configuration with default values.
95    #[must_use = "configuration is required for prompt generation"]
96    pub const fn new() -> Self {
97        Self {
98            iteration: None,
99            total_iterations: None,
100            prompt_md_content: None,
101            prompt_and_plan: None,
102            prompt_plan_and_issues: None,
103            is_resume: false,
104            resume_context: None,
105        }
106    }
107
108    /// Set iteration numbers for developer iteration prompts.
109    #[must_use = "returns the updated configuration for chaining"]
110    pub const fn with_iterations(mut self, iteration: u32, total: u32) -> Self {
111        self.iteration = Some(iteration);
112        self.total_iterations = Some(total);
113        self
114    }
115
116    /// Set PROMPT.md content for planning prompts.
117    #[must_use = "returns the updated configuration for chaining"]
118    pub fn with_prompt_md(mut self, content: String) -> Self {
119        self.prompt_md_content = Some(content);
120        self
121    }
122
123    /// Set (PROMPT.md, PLAN.md) content tuple for developer iteration prompts.
124    #[must_use = "returns the updated configuration for chaining"]
125    pub fn with_prompt_and_plan(mut self, prompt: String, plan: String) -> Self {
126        self.prompt_and_plan = Some((prompt, plan));
127        self
128    }
129
130    /// Set (PROMPT.md, PLAN.md, ISSUES.md) content tuple for fix prompts.
131    pub fn with_prompt_plan_and_issues(
132        mut self,
133        prompt: String,
134        plan: String,
135        issues: String,
136    ) -> Self {
137        self.prompt_plan_and_issues = Some((prompt, plan, issues));
138        self
139    }
140
141    /// Set whether this is a resumed session.
142    #[cfg(test)]
143    #[must_use = "returns the updated configuration for chaining"]
144    pub const fn with_resume(mut self, is_resume: bool) -> Self {
145        self.is_resume = is_resume;
146        self
147    }
148
149    /// Set rich resume context for resumed sessions.
150    #[must_use = "returns the updated configuration for chaining"]
151    pub fn with_resume_context(mut self, context: ResumeContext) -> Self {
152        self.resume_context = Some(context);
153        self.is_resume = true;
154        self
155    }
156}
157
158/// Generate a rich resume note from resume context.
159///
160/// Creates a detailed, context-aware note that helps agents understand
161/// where they are in the pipeline when resuming from a checkpoint.
162///
163/// The note includes:
164/// - Phase and iteration information
165/// - Recent execution history (files modified, issues found/fixed)
166/// - Git commits made during the session
167/// - Guidance on what to focus on
168pub fn generate_resume_note(context: &ResumeContext) -> String {
169    let mut note = String::from("SESSION RESUME CONTEXT\n");
170    note.push_str("====================\n\n");
171
172    // Add phase information with specific context based on phase type
173    match context.phase {
174        crate::checkpoint::state::PipelinePhase::Development => {
175            note.push_str(&format!(
176                "Resuming DEVELOPMENT phase (iteration {} of {})\n",
177                context.iteration + 1,
178                context.total_iterations
179            ));
180        }
181        crate::checkpoint::state::PipelinePhase::Review => {
182            note.push_str(&format!(
183                "Resuming REVIEW phase (pass {} of {})\n",
184                context.reviewer_pass + 1,
185                context.total_reviewer_passes
186            ));
187        }
188        crate::checkpoint::state::PipelinePhase::ReviewAgain => {
189            note.push_str(&format!(
190                "Resuming VERIFICATION REVIEW phase (pass {} of {})\n",
191                context.reviewer_pass + 1,
192                context.total_reviewer_passes
193            ));
194        }
195        crate::checkpoint::state::PipelinePhase::Fix => {
196            note.push_str("Resuming FIX phase\n");
197        }
198        _ => {
199            note.push_str(&format!("Resuming from phase: {}\n", context.phase_name()));
200        }
201    }
202
203    // Add resume count if this has been resumed before
204    if context.resume_count > 0 {
205        note.push_str(&format!(
206            "This session has been resumed {} time(s)\n",
207            context.resume_count
208        ));
209    }
210
211    // Add rebase state if applicable
212    if !matches!(
213        context.rebase_state,
214        crate::checkpoint::state::RebaseState::NotStarted
215    ) {
216        note.push_str(&format!("Rebase state: {:?}\n", context.rebase_state));
217    }
218
219    note.push('\n');
220
221    // Add execution history summary if available
222    if let Some(ref history) = context.execution_history {
223        if !history.steps.is_empty() {
224            note.push_str("RECENT ACTIVITY:\n");
225            note.push_str("----------------\n");
226
227            // Show recent execution steps (last 5)
228            let recent_steps: Vec<_> = history
229                .steps
230                .iter()
231                .rev()
232                .take(5)
233                .collect::<Vec<_>>()
234                .into_iter()
235                .rev()
236                .collect();
237
238            for step in &recent_steps {
239                note.push_str(&format!(
240                    "- [{}] {} (iteration {}): {}\n",
241                    step.step_type,
242                    step.phase,
243                    step.iteration,
244                    step.outcome.brief_description()
245                ));
246
247                // Add files modified count if available
248                if let Some(ref detail) = step.modified_files_detail {
249                    let total_files =
250                        detail.added.len() + detail.modified.len() + detail.deleted.len();
251                    if total_files > 0 {
252                        note.push_str(&format!("  Files: {} changed", total_files));
253                        if !detail.added.is_empty() {
254                            note.push_str(&format!(" ({} added)", detail.added.len()));
255                        }
256                        if !detail.modified.is_empty() {
257                            note.push_str(&format!(" ({} modified)", detail.modified.len()));
258                        }
259                        if !detail.deleted.is_empty() {
260                            note.push_str(&format!(" ({} deleted)", detail.deleted.len()));
261                        }
262                        note.push('\n');
263                    }
264                }
265
266                // Add issues summary if available
267                if let Some(ref issues) = step.issues_summary {
268                    if issues.found > 0 || issues.fixed > 0 {
269                        note.push_str(&format!(
270                            "  Issues: {} found, {} fixed",
271                            issues.found, issues.fixed
272                        ));
273                        if let Some(ref desc) = issues.description {
274                            note.push_str(&format!(" ({})", desc));
275                        }
276                        note.push('\n');
277                    }
278                }
279
280                // Add git commit if available
281                if let Some(ref oid) = step.git_commit_oid {
282                    note.push_str(&format!("  Commit: {}\n", oid));
283                }
284            }
285
286            note.push('\n');
287        }
288    }
289
290    note.push_str("Previous progress is preserved in git history.\n");
291    note.push_str("Check 'git log' for details about what was done before.\n");
292
293    // Add helpful guidance about what the agent should focus on
294    note.push_str("\nGUIDANCE:\n");
295    note.push_str("--------\n");
296    match context.phase {
297        crate::checkpoint::state::PipelinePhase::Development => {
298            note.push_str("Continue working on the implementation tasks from your plan.\n");
299        }
300        crate::checkpoint::state::PipelinePhase::Review
301        | crate::checkpoint::state::PipelinePhase::ReviewAgain => {
302            note.push_str("Review the code changes and provide feedback.\n");
303        }
304        crate::checkpoint::state::PipelinePhase::Fix => {
305            note.push_str("Focus on addressing the issues identified in the review.\n");
306        }
307        _ => {}
308    }
309
310    note.push('\n');
311    note
312}
313
314// Helper trait for brief outcome descriptions
315trait BriefDescription {
316    fn brief_description(&self) -> String;
317}
318
319impl BriefDescription for crate::checkpoint::execution_history::StepOutcome {
320    fn brief_description(&self) -> String {
321        match self {
322            Self::Success {
323                files_modified,
324                output,
325                ..
326            } => {
327                if let Some(ref out) = output {
328                    if !out.is_empty() {
329                        format!("Success - {}", out.lines().next().unwrap_or(""))
330                    } else if !files_modified.is_empty() {
331                        format!("Success - {} files modified", files_modified.len())
332                    } else {
333                        "Success".to_string()
334                    }
335                } else if !files_modified.is_empty() {
336                    format!("Success - {} files modified", files_modified.len())
337                } else {
338                    "Success".to_string()
339                }
340            }
341            Self::Failure {
342                error, recoverable, ..
343            } => {
344                if *recoverable {
345                    format!("Recoverable error - {}", error.lines().next().unwrap_or(""))
346                } else {
347                    format!("Failed - {}", error.lines().next().unwrap_or(""))
348                }
349            }
350            Self::Partial {
351                completed,
352                remaining,
353                ..
354            } => {
355                format!("Partial - {} done, {}", completed, remaining)
356            }
357            Self::Skipped { reason } => {
358                format!("Skipped - {}", reason)
359            }
360        }
361    }
362}
363
364/// Generate a prompt for any agent type.
365///
366/// This is the main dispatcher function that routes to the appropriate
367/// prompt generator based on role and action.
368///
369/// The config parameter allows providing:
370/// - Language-specific review guidance when the project stack has been detected
371/// - PROMPT.md content for planning prompts
372/// - PROMPT.md and PLAN.md content for developer iteration prompts
373///
374/// # Arguments
375///
376/// * `role` - The agent role (Developer, Reviewer, etc.)
377/// * `action` - The action to perform (Plan, Iterate, Fix, etc.)
378/// * `context` - The context level (minimal or normal)
379/// * `template_context` - Template context for user template overrides
380/// * `config` - Prompt configuration with content variables
381pub fn prompt_for_agent(
382    role: Role,
383    action: Action,
384    context: ContextLevel,
385    template_context: &TemplateContext,
386    config: PromptConfig,
387) -> String {
388    let resume_note = if let Some(resume_ctx) = &config.resume_context {
389        generate_resume_note(resume_ctx)
390    } else if config.is_resume {
391        // Fallback for backward compatibility when no rich context is available
392        "\nNOTE: This session is resuming from a previous run. Previous progress is preserved in git history. You can check 'git log' for context about what was done before.\n\n".to_string()
393    } else {
394        String::new()
395    };
396
397    let base_prompt = match (role, action) {
398        (_, Action::Plan) => {
399            prompt_plan_with_context(template_context, config.prompt_md_content.as_deref())
400        }
401        (Role::Developer | Role::Reviewer, Action::Iterate) => {
402            let (prompt_content, plan_content) = config
403                .prompt_and_plan
404                .unwrap_or((String::new(), String::new()));
405            prompt_developer_iteration_with_context(
406                template_context,
407                config.iteration.unwrap_or(1),
408                config.total_iterations.unwrap_or(1),
409                context,
410                &prompt_content,
411                &plan_content,
412            )
413        }
414        (_, Action::Fix) => {
415            let (prompt_content, plan_content, issues_content) = config
416                .prompt_plan_and_issues
417                .unwrap_or((String::new(), String::new(), String::new()));
418            prompt_fix_with_context(
419                template_context,
420                &prompt_content,
421                &plan_content,
422                &issues_content,
423            )
424        }
425    };
426
427    // Prepend resume note if applicable
428    if config.is_resume {
429        format!("{}{}", resume_note, base_prompt)
430    } else {
431        base_prompt
432    }
433}
434
435/// Get a stored prompt from history or generate a new one.
436///
437/// This function implements prompt replay for hardened resume functionality.
438/// When resuming from a checkpoint, it checks if a prompt was already used
439/// and returns the stored prompt for deterministic behavior. Otherwise, it
440/// generates a new prompt using the provided generator function.
441///
442/// # Arguments
443///
444/// * `prompt_key` - Unique key identifying this prompt (e.g., "development_1", "review_2")
445/// * `prompt_history` - The prompt history from the checkpoint (if available)
446/// * `generator` - Function to generate the prompt if not found in history
447///
448/// # Returns
449///
450/// A tuple of (prompt, was_replayed) where:
451/// - `prompt` is the prompt string (either replayed or newly generated)
452/// - `was_replayed` is true if the prompt came from history, false if newly generated
453///
454/// # Example
455///
456/// ```rust
457/// let (prompt, was_replayed) = get_stored_or_generate_prompt(
458///     "development_1",
459///     &ctx.prompt_history,
460///     || prompt_for_agent(role, action, context, template_context, config),
461/// );
462/// if was_replayed {
463///     logger.info("Using stored prompt from checkpoint for determinism");
464/// }
465/// ```
466pub fn get_stored_or_generate_prompt<F>(
467    prompt_key: &str,
468    prompt_history: &std::collections::HashMap<String, String>,
469    generator: F,
470) -> (String, bool)
471where
472    F: FnOnce() -> String,
473{
474    if let Some(stored_prompt) = prompt_history.get(prompt_key) {
475        (stored_prompt.clone(), true)
476    } else {
477        (generator(), false)
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::prompts::template_context::TemplateContext;
485
486    // Import non-context variants for test compatibility
487    use crate::prompts::reviewer::prompt_detailed_review_without_guidelines_with_diff;
488
489    #[test]
490    fn test_prompt_for_agent_developer() {
491        let template_context = TemplateContext::default();
492        let result = prompt_for_agent(
493            Role::Developer,
494            Action::Iterate,
495            ContextLevel::Normal,
496            &template_context,
497            PromptConfig::new()
498                .with_iterations(3, 10)
499                .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
500        );
501        // Agent should NOT be told to read PROMPT.md (orchestrator handles it)
502        assert!(!result.contains("PROMPT.md"));
503        assert!(result.contains("test prompt"));
504        assert!(result.contains("test plan"));
505    }
506
507    #[test]
508    fn test_prompt_for_agent_reviewer() {
509        let result = prompt_detailed_review_without_guidelines_with_diff(
510            ContextLevel::Minimal,
511            "sample diff",
512            "",
513            "",
514        );
515        // NOTE: The detailed_review template has been deprecated and now uses standard_review
516        // The test should verify the new template behavior
517        assert!(result.contains("REVIEW MODE"));
518        assert!(result.contains("CRITICAL CONSTRAINTS"));
519    }
520
521    #[test]
522    fn test_prompt_for_agent_plan() {
523        let template_context = TemplateContext::default();
524        let result = prompt_for_agent(
525            Role::Developer,
526            Action::Plan,
527            ContextLevel::Normal,
528            &template_context,
529            PromptConfig::new().with_prompt_md("test requirements".to_string()),
530        );
531        // Plan is now returned as structured output, not written to file
532        assert!(result.contains("PLANNING MODE"));
533        assert!(result.contains("Implementation Steps"));
534    }
535
536    #[test]
537    fn test_prompts_are_agent_agnostic() {
538        // All prompts should be free of agent-specific references
539        // to ensure they work with any AI coding assistant
540        let agent_specific_terms = [
541            "claude", "codex", "opencode", "gemini", "aider", "goose", "cline", "continue",
542            "amazon-q", "gpt", "copilot",
543        ];
544
545        let prompts_to_check: Vec<String> = vec![
546            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
547            prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
548            prompt_detailed_review_without_guidelines_with_diff(
549                ContextLevel::Normal,
550                "sample diff",
551                "",
552                "",
553            ),
554            prompt_detailed_review_without_guidelines_with_diff(
555                ContextLevel::Minimal,
556                "sample diff",
557                "",
558                "",
559            ),
560            prompt_fix("", "", ""),
561            prompt_plan(None),
562            prompt_generate_commit_message_with_diff("diff --git a/a b/b"),
563        ];
564
565        for prompt in prompts_to_check {
566            let prompt_lower = prompt.to_lowercase();
567            for term in agent_specific_terms {
568                assert!(
569                    !prompt_lower.contains(term),
570                    "Prompt contains agent-specific term '{}': {}",
571                    term,
572                    &prompt[..prompt.len().min(100)]
573                );
574            }
575        }
576    }
577
578    #[test]
579    fn test_prompt_for_agent_fix() {
580        let template_context = TemplateContext::default();
581        let result = prompt_for_agent(
582            Role::Developer,
583            Action::Fix,
584            ContextLevel::Normal,
585            &template_context,
586            PromptConfig::new().with_prompt_plan_and_issues(
587                "test prompt".to_string(),
588                "test plan".to_string(),
589                "test issues".to_string(),
590            ),
591        );
592        assert!(result.contains("FIX MODE"));
593        assert!(result.contains("test issues"));
594        // Should include PROMPT and PLAN context
595        assert!(result.contains("test prompt"));
596        assert!(result.contains("test plan"));
597    }
598
599    #[test]
600    fn test_prompt_for_agent_fix_with_empty_context() {
601        let template_context = TemplateContext::default();
602        let result = prompt_for_agent(
603            Role::Developer,
604            Action::Fix,
605            ContextLevel::Normal,
606            &template_context,
607            PromptConfig::new(),
608        );
609        assert!(result.contains("FIX MODE"));
610        // Should still work with empty context
611        assert!(!result.is_empty());
612    }
613
614    #[test]
615    fn test_reviewer_can_use_iterate_action() {
616        // Edge case: Reviewer using Iterate action (fallback behavior)
617        let template_context = TemplateContext::default();
618        let result = prompt_for_agent(
619            Role::Reviewer,
620            Action::Iterate,
621            ContextLevel::Normal,
622            &template_context,
623            PromptConfig::new()
624                .with_iterations(1, 3)
625                .with_prompt_and_plan(String::new(), String::new()),
626        );
627        // Should fall back to developer iteration prompt
628        assert!(result.contains("IMPLEMENTATION MODE"));
629    }
630
631    #[test]
632    fn test_prompts_do_not_have_detailed_tracking_language() {
633        // Prompts should NOT contain detailed history tracking language
634        // to prevent context contamination in future runs
635        let detailed_tracking_terms = [
636            "iteration number",
637            "phase completed",
638            "previous iteration",
639            "history of",
640            "detailed log",
641        ];
642
643        let prompts_to_check = vec![
644            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
645            prompt_fix("", "", ""),
646        ];
647
648        for prompt in prompts_to_check {
649            let prompt_lower = prompt.to_lowercase();
650            for term in detailed_tracking_terms {
651                assert!(
652                    !prompt_lower.contains(term),
653                    "Prompt contains detailed tracking language '{}': {}",
654                    term,
655                    &prompt[..prompt.len().min(100)]
656                );
657            }
658        }
659    }
660
661    #[test]
662    fn test_developer_notes_md_not_referenced() {
663        // Developer prompt should NOT mention NOTES.md at all (isolation mode)
664        let developer_prompt = prompt_developer_iteration(1, 5, ContextLevel::Normal, "", "");
665        assert!(
666            !developer_prompt.contains("NOTES.md"),
667            "Developer prompt should not reference NOTES.md in isolation mode"
668        );
669    }
670
671    #[test]
672    fn test_all_prompts_isolate_agents_from_git() {
673        // AC3: "AI agent does not know that we have previous committed change"
674        // All prompts should NOT tell agents to run git commands
675        // Git operations are handled by the orchestrator via libgit2
676
677        // These patterns indicate the agent is being instructed to RUN git commands
678        // We exclude patterns that are part of constraint lists (like "MUST NOT run X, Y, Z")
679        let instructive_git_patterns = [
680            "Run `git",
681            "run git",
682            "execute git",
683            "Try: git",
684            "you can git",
685            "should run git",
686            "please run git",
687            "\ngit ", // Command starting at line beginning after newline
688        ];
689
690        // Context patterns that indicate the command is being FORBIDDEN, not instructed
691        // These should be excluded from the check
692        let forbid_contexts = [
693            "MUST NOT run",
694            "DO NOT run",
695            "must not run",
696            "do not run",
697            "NOT run commands",
698            "commands (",
699            "commands:",
700            "including:",
701            "such as",
702        ];
703
704        // Special case: "Use git" is allowed in fix_mode.txt for fault tolerance
705        // when issue descriptions lack file context - the fixer needs to find the relevant code
706        // This is part of the recovery mechanism for vague issues
707
708        let prompts_to_check: Vec<String> = vec![
709            prompt_developer_iteration(1, 5, ContextLevel::Normal, "", ""),
710            prompt_developer_iteration(1, 5, ContextLevel::Minimal, "", ""),
711            prompt_detailed_review_without_guidelines_with_diff(
712                ContextLevel::Normal,
713                "sample diff",
714                "",
715                "",
716            ),
717            prompt_detailed_review_without_guidelines_with_diff(
718                ContextLevel::Minimal,
719                "sample diff",
720                "",
721                "",
722            ),
723            // Note: fix_mode.txt is intentionally excluded from "Use git" check
724            // because it contains "Use git grep/rg ONLY when issue descriptions lack file context"
725            // which is part of the fault tolerance design
726            prompt_fix("", "", ""),
727            prompt_plan(None),
728            prompt_generate_commit_message_with_diff("diff --git a/a b/b\n"),
729        ];
730
731        for prompt in prompts_to_check {
732            for pattern in instructive_git_patterns {
733                if prompt.contains(pattern) {
734                    // Check if this is in a "forbidden" context
735                    let is_forbidden = forbid_contexts.iter().any(|ctx| {
736                        if let Some(pos) = prompt.find(ctx) {
737                            // Check if the pattern appears after the forbid context
738                            if let Some(pattern_pos) = prompt[pos..].find(pattern) {
739                                // Pattern is within reasonable proximity (200 chars) of forbid context
740                                pattern_pos < 200
741                            } else {
742                                false
743                            }
744                        } else {
745                            false
746                        }
747                    });
748
749                    if !is_forbidden {
750                        panic!(
751                            "Prompt contains instructive git command pattern '{}': {}",
752                            pattern,
753                            &prompt[..prompt.len().min(150)]
754                        );
755                    }
756                }
757            }
758        }
759
760        // Verify the orchestrator-specific function for commit message generation
761        // DOES contain the diff content (orchestrator receives diff, not git commands).
762        // The orchestrator uses this function to pass diff to the LLM via stdin.
763        let orchestrator_prompt = prompt_generate_commit_message_with_diff("some diff");
764        assert!(
765            orchestrator_prompt.contains("DIFF:") || orchestrator_prompt.contains("diff"),
766            "Orchestrator prompt should contain the diff content for commit message generation"
767        );
768        // But the prompt should NOT tell the agent to run git commands (orchestrator handles git)
769        for pattern in instructive_git_patterns {
770            if orchestrator_prompt.contains(pattern) {
771                // Check if this is in a "forbidden" context
772                let is_forbidden = forbid_contexts.iter().any(|ctx| {
773                    if let Some(pos) = orchestrator_prompt.find(ctx) {
774                        if let Some(pattern_pos) = orchestrator_prompt[pos..].find(pattern) {
775                            pattern_pos < 200
776                        } else {
777                            false
778                        }
779                    } else {
780                        false
781                    }
782                });
783
784                assert!(
785                    is_forbidden,
786                    "Orchestrator prompt contains instructive git command pattern '{pattern}'"
787                );
788            }
789        }
790    }
791
792    #[test]
793    fn test_prompt_with_resume_context() {
794        let template_context = TemplateContext::default();
795        let result = prompt_for_agent(
796            Role::Developer,
797            Action::Iterate,
798            ContextLevel::Normal,
799            &template_context,
800            PromptConfig::new()
801                .with_resume(true)
802                .with_iterations(2, 5)
803                .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
804        );
805        // Should include resume note
806        assert!(result.contains("resuming from a previous run"));
807        assert!(result.contains("git log"));
808    }
809
810    #[test]
811    fn test_prompt_with_rich_resume_context_development() {
812        use crate::checkpoint::state::{PipelinePhase, RebaseState};
813
814        let template_context = TemplateContext::default();
815
816        // Create a resume context for development phase
817        let resume_context = ResumeContext {
818            phase: PipelinePhase::Development,
819            iteration: 2,
820            total_iterations: 5,
821            reviewer_pass: 0,
822            total_reviewer_passes: 3,
823            resume_count: 1,
824            rebase_state: RebaseState::NotStarted,
825            run_id: "test-run-id".to_string(),
826            prompt_history: None,
827            execution_history: None,
828        };
829
830        let result = prompt_for_agent(
831            Role::Developer,
832            Action::Iterate,
833            ContextLevel::Normal,
834            &template_context,
835            PromptConfig::new()
836                .with_resume_context(resume_context)
837                .with_iterations(3, 5)
838                .with_prompt_and_plan("test prompt".to_string(), "test plan".to_string()),
839        );
840
841        // Should include rich resume context
842        assert!(result.contains("SESSION RESUME CONTEXT"));
843        assert!(result.contains("DEVELOPMENT phase"));
844        assert!(result.contains("iteration 3 of 5"));
845        assert!(result.contains("has been resumed 1 time"));
846        assert!(result.contains("Continue working on the implementation"));
847    }
848
849    #[test]
850    fn test_prompt_with_rich_resume_context_review() {
851        use crate::checkpoint::state::{PipelinePhase, RebaseState};
852
853        let template_context = TemplateContext::default();
854
855        // Create a resume context for review phase
856        let resume_context = ResumeContext {
857            phase: PipelinePhase::Review,
858            iteration: 5,
859            total_iterations: 5,
860            reviewer_pass: 1,
861            total_reviewer_passes: 3,
862            resume_count: 2,
863            rebase_state: RebaseState::NotStarted,
864            run_id: "test-run-id".to_string(),
865            prompt_history: None,
866            execution_history: None,
867        };
868
869        let result = prompt_for_agent(
870            Role::Reviewer,
871            Action::Fix,
872            ContextLevel::Normal,
873            &template_context,
874            PromptConfig::new()
875                .with_resume_context(resume_context)
876                .with_prompt_plan_and_issues(
877                    "test prompt".to_string(),
878                    "test plan".to_string(),
879                    "test issues".to_string(),
880                ),
881        );
882
883        // Should include rich resume context for review
884        assert!(result.contains("SESSION RESUME CONTEXT"));
885        assert!(result.contains("REVIEW phase"));
886        assert!(result.contains("pass 2 of 3"));
887        assert!(result.contains("has been resumed 2 time"));
888    }
889
890    #[test]
891    fn test_prompt_with_rich_resume_context_fix() {
892        use crate::checkpoint::state::{PipelinePhase, RebaseState};
893
894        let template_context = TemplateContext::default();
895
896        // Create a resume context for fix phase
897        let resume_context = ResumeContext {
898            phase: PipelinePhase::Fix,
899            iteration: 5,
900            total_iterations: 5,
901            reviewer_pass: 1,
902            total_reviewer_passes: 3,
903            resume_count: 0,
904            rebase_state: RebaseState::NotStarted,
905            run_id: "test-run-id".to_string(),
906            prompt_history: None,
907            execution_history: None,
908        };
909
910        let result = prompt_for_agent(
911            Role::Reviewer,
912            Action::Fix,
913            ContextLevel::Normal,
914            &template_context,
915            PromptConfig::new()
916                .with_resume_context(resume_context)
917                .with_prompt_plan_and_issues(
918                    "test prompt".to_string(),
919                    "test plan".to_string(),
920                    "test issues".to_string(),
921                ),
922        );
923
924        // Should include rich resume context for fix
925        assert!(result.contains("SESSION RESUME CONTEXT"));
926        assert!(result.contains("FIX phase"));
927        assert!(result.contains("Focus on addressing the issues"));
928    }
929
930    #[test]
931    fn test_get_stored_or_generate_prompt_replays_when_available() {
932        let mut history = std::collections::HashMap::new();
933        history.insert("test_key".to_string(), "stored prompt".to_string());
934
935        let (prompt, was_replayed) =
936            get_stored_or_generate_prompt("test_key", &history, || "generated prompt".to_string());
937
938        assert_eq!(prompt, "stored prompt");
939        assert!(was_replayed, "Should have replayed the stored prompt");
940    }
941
942    #[test]
943    fn test_get_stored_or_generate_prompt_generates_when_not_available() {
944        let history = std::collections::HashMap::new();
945
946        let (prompt, was_replayed) = get_stored_or_generate_prompt("missing_key", &history, || {
947            "generated prompt".to_string()
948        });
949
950        assert_eq!(prompt, "generated prompt");
951        assert!(!was_replayed, "Should have generated a new prompt");
952    }
953
954    #[test]
955    fn test_get_stored_or_generate_prompt_with_empty_history() {
956        let history = std::collections::HashMap::new();
957
958        let (prompt, was_replayed) =
959            get_stored_or_generate_prompt("any_key", &history, || "fresh prompt".to_string());
960
961        assert_eq!(prompt, "fresh prompt");
962        assert!(
963            !was_replayed,
964            "Should have generated a new prompt for empty history"
965        );
966    }
967}