1use crate::errors::{CoreError, CoreResult};
8use crate::validate;
9use ito_domain::changes::{ChangeRepository as DomainChangeRepository, ChangeTargetResolution};
10use ito_domain::modules::ModuleRepository as DomainModuleRepository;
11use std::path::Path;
12
13use ito_common::paths;
14
15pub struct BuildPromptOptions {
17 pub change_id: Option<String>,
19 pub module_id: Option<String>,
21
22 pub iteration: Option<u32>,
24 pub max_iterations: Option<u32>,
26 pub min_iterations: u32,
28
29 pub completion_promise: String,
31
32 pub context_content: Option<String>,
34
35 pub validation_failure: Option<String>,
39}
40
41pub fn build_prompt_preamble(
46 iteration: u32,
47 max_iterations: Option<u32>,
48 min_iterations: u32,
49 completion_promise: &str,
50 context_content: Option<&str>,
51 validation_failure: Option<&str>,
52 task: &str,
53) -> String {
54 let has_finite_max = max_iterations.is_some_and(|v| v > 0);
55 let normalized_context = context_content.unwrap_or("").trim();
56 let context_section = if normalized_context.is_empty() {
57 String::new()
58 } else {
59 format!(
60 "\n## Additional Context (added by user mid-loop)\n\n{c}\n\n---\n",
61 c = normalized_context
62 )
63 };
64
65 let normalized_validation = validation_failure.unwrap_or("").trim();
66 let validation_section = if normalized_validation.is_empty() {
67 String::new()
68 } else {
69 format!(
70 "\n## Validation Failure (completion rejected)\n\nRalph detected a completion promise, but it was rejected because validation failed. Fix the issues below and try again.\n\n{v}\n\n---\n",
71 v = normalized_validation
72 )
73 };
74
75 let max_str = if has_finite_max {
76 format!(" / {}", max_iterations.unwrap())
77 } else {
78 " (unlimited)".to_string()
79 };
80
81 format!(
82 "# Ralph Wiggum Loop - Iteration {iteration}\n\nYou are in an iterative development loop. Work on the task below until you can genuinely complete it.\n\nImportant: Ralph validates completion promises before exiting (tasks + project checks/tests).\n{context_section}{validation_section}## Your Task\n\n{task}\n\n## Instructions\n\n1. Read the current state of files to understand what's been done\n2. **Update your todo list** - Use the TodoWrite tool to track progress and plan remaining work\n3. Make progress on the task\n4. Run tests/verification if applicable\n5. When the task is GENUINELY COMPLETE, output:\n <promise>{completion_promise}</promise>\n\n## Critical Rules\n\n- ONLY output <promise>{completion_promise}</promise> when the task is truly done\n- Do NOT lie or output false promises to exit the loop\n- If stuck, try a different approach\n- Check your work before claiming completion\n- The loop will continue until you succeed\n- **IMPORTANT**: Update your todo list at the start of each iteration to show progress\n\n## AUTONOMY REQUIREMENTS (CRITICAL)\n\n- **DO NOT ASK QUESTIONS** - This is an autonomous loop with no human interaction\n- **DO NOT USE THE QUESTION TOOL** - Work independently without prompting for input\n- Make reasonable assumptions when information is missing\n- Use your best judgment to resolve ambiguities\n- If multiple approaches exist, choose the most reasonable one and proceed\n- The orchestrator cannot respond to questions - you must be self-sufficient\n- Trust your training and make decisions autonomously\n\n## Current Iteration: {iteration}{max_str} (min: {min_iterations})\n\nNow, work on the task autonomously. Good luck!",
83 iteration = iteration,
84 context_section = context_section,
85 validation_section = validation_section,
86 task = task,
87 completion_promise = completion_promise,
88 max_str = max_str,
89 min_iterations = min_iterations,
90 )
91}
92
93pub fn build_ralph_prompt(
97 ito_path: &Path,
98 change_repo: &impl DomainChangeRepository,
99 module_repo: &impl DomainModuleRepository,
100 user_prompt: &str,
101 options: BuildPromptOptions,
102) -> CoreResult<String> {
103 let mut sections: Vec<String> = Vec::new();
104
105 if let Some(change_id) = options.change_id.as_deref()
106 && let Some(ctx) = load_change_context(ito_path, change_repo, change_id)?
107 {
108 sections.push(ctx);
109 }
110
111 if let Some(module_id) = options.module_id.as_deref()
112 && let Some(ctx) = load_module_context(ito_path, module_repo, module_id)?
113 {
114 sections.push(ctx);
115 }
116
117 sections.push(user_prompt.to_string());
118 let task = sections.join("\n\n---\n\n");
119
120 if let Some(iteration) = options.iteration {
121 Ok(build_prompt_preamble(
122 iteration,
123 options.max_iterations,
124 options.min_iterations,
125 &options.completion_promise,
126 options.context_content.as_deref(),
127 options.validation_failure.as_deref(),
128 &task,
129 )
130 .trim()
131 .to_string())
132 } else {
133 Ok(task)
134 }
135}
136
137fn load_change_context(
138 ito_path: &Path,
139 change_repo: &impl DomainChangeRepository,
140 change_id: &str,
141) -> CoreResult<Option<String>> {
142 let changes_dir = paths::changes_dir(ito_path);
143 let resolved = resolve_change_id(change_repo, change_id)?;
144 let Some(resolved) = resolved else {
145 return Ok(None);
146 };
147
148 let proposal_path = changes_dir.join(&resolved).join("proposal.md");
149 if !proposal_path.exists() {
150 return Ok(None);
151 }
152
153 let proposal = ito_common::io::read_to_string_std(&proposal_path)
154 .map_err(|e| CoreError::io(format!("reading {}", proposal_path.display()), e))?;
155 Ok(Some(format!(
156 "## Change Proposal ({id})\n\n{proposal}",
157 id = resolved,
158 proposal = proposal
159 )))
160}
161
162fn resolve_change_id(
163 change_repo: &impl DomainChangeRepository,
164 input: &str,
165) -> CoreResult<Option<String>> {
166 match change_repo.resolve_target(input) {
167 ChangeTargetResolution::Unique(id) => Ok(Some(id)),
168 ChangeTargetResolution::NotFound => Ok(None),
169 ChangeTargetResolution::Ambiguous(matches) => Err(CoreError::Validation(format!(
170 "Ambiguous change id '{input}'. Matches: {matches}",
171 input = input,
172 matches = matches.join(", ")
173 ))),
174 }
175}
176
177fn load_module_context(
178 ito_path: &Path,
179 module_repo: &impl DomainModuleRepository,
180 module_id: &str,
181) -> CoreResult<Option<String>> {
182 let resolved = validate::resolve_module(module_repo, ito_path, module_id)?;
183 let Some(resolved) = resolved else {
184 return Ok(None);
185 };
186
187 if !resolved.module_md.exists() {
188 return Ok(None);
189 }
190
191 let module_content = ito_common::io::read_to_string_std(&resolved.module_md)
192 .map_err(|e| CoreError::io(format!("reading {}", resolved.module_md.display()), e))?;
193 Ok(Some(format!(
194 "## Module ({id})\n\n{content}",
195 id = resolved.id,
196 content = module_content
197 )))
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn build_prompt_preamble_includes_iteration() {
206 let result = build_prompt_preamble(3, Some(10), 1, "DONE_TOKEN", None, None, "Test task");
207 assert!(result.contains("3"));
208 assert!(result.contains("10"));
209 }
210
211 #[test]
212 fn build_prompt_preamble_includes_completion_promise() {
213 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
214 assert!(result.contains("DONE_TOKEN"));
215 }
216
217 #[test]
218 fn build_prompt_preamble_includes_context() {
219 let result = build_prompt_preamble(
220 1,
221 Some(5),
222 1,
223 "DONE_TOKEN",
224 Some("extra context"),
225 None,
226 "Test task",
227 );
228 assert!(result.contains("extra context"));
229 }
230
231 #[test]
232 fn build_prompt_preamble_includes_validation_failure() {
233 let result = build_prompt_preamble(
234 1,
235 Some(5),
236 1,
237 "DONE_TOKEN",
238 None,
239 Some("task X not done"),
240 "Test task",
241 );
242 assert!(result.contains("task X not done"));
243 }
244
245 #[test]
246 fn build_prompt_preamble_omits_context_when_none() {
247 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
248 assert!(!result.contains("Additional Context"));
249 }
250
251 #[test]
252 fn build_prompt_preamble_omits_validation_when_none() {
253 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
254 assert!(!result.contains("Validation Failure"));
255 }
256}