1use crate::errors::{CoreError, CoreResult};
8use crate::tasks::{get_next_task_from_summary, get_task_status_from_repository};
9use crate::validate;
10use ito_domain::changes::{ChangeRepository as DomainChangeRepository, ChangeTargetResolution};
11use ito_domain::modules::ModuleRepository as DomainModuleRepository;
12use ito_domain::tasks::TaskRepository as DomainTaskRepository;
13use std::path::Path;
14
15use ito_common::paths;
16
17pub struct BuildPromptOptions {
19 pub change_id: Option<String>,
21 pub module_id: Option<String>,
23
24 pub iteration: Option<u32>,
26 pub max_iterations: Option<u32>,
28 pub min_iterations: u32,
30
31 pub completion_promise: String,
33
34 pub context_content: Option<String>,
36
37 pub validation_failure: Option<String>,
41}
42
43pub fn build_prompt_preamble(
48 iteration: u32,
49 max_iterations: Option<u32>,
50 min_iterations: u32,
51 completion_promise: &str,
52 context_content: Option<&str>,
53 validation_failure: Option<&str>,
54 task: &str,
55) -> String {
56 let has_finite_max = max_iterations.is_some_and(|v| v > 0);
57 let normalized_context = context_content.unwrap_or("").trim();
58 let context_section = if normalized_context.is_empty() {
59 String::new()
60 } else {
61 format!(
62 "\n## Additional Context (added by user mid-loop)\n\n{c}\n\n---\n",
63 c = normalized_context
64 )
65 };
66
67 let normalized_validation = validation_failure.unwrap_or("").trim();
68 let validation_section = if normalized_validation.is_empty() {
69 String::new()
70 } else {
71 format!(
72 "\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",
73 v = normalized_validation
74 )
75 };
76
77 let max_str = if has_finite_max {
78 format!(" / {}", max_iterations.unwrap())
79 } else {
80 " (unlimited)".to_string()
81 };
82
83 format!(
84 "# 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!",
85 iteration = iteration,
86 context_section = context_section,
87 validation_section = validation_section,
88 task = task,
89 completion_promise = completion_promise,
90 max_str = max_str,
91 min_iterations = min_iterations,
92 )
93}
94
95pub fn build_ralph_prompt(
99 ito_path: &Path,
100 change_repo: &(impl DomainChangeRepository + ?Sized),
101 task_repo: &(impl DomainTaskRepository + ?Sized),
102 module_repo: &(impl DomainModuleRepository + ?Sized),
103 user_prompt: &str,
104 options: BuildPromptOptions,
105) -> CoreResult<String> {
106 let mut sections: Vec<String> = Vec::new();
107
108 if let Some(change_id) = options.change_id.as_deref()
109 && let Some(ctx) = load_change_context(ito_path, change_repo, change_id)?
110 {
111 sections.push(ctx);
112 }
113
114 if let Some(change_id) = options.change_id.as_deref()
115 && let Some(ctx) = load_task_context(task_repo, change_id)?
116 {
117 sections.push(ctx);
118 }
119
120 if let Some(module_id) = options.module_id.as_deref()
121 && let Some(ctx) = load_module_context(ito_path, module_repo, module_id)?
122 {
123 sections.push(ctx);
124 }
125
126 sections.push(user_prompt.to_string());
127 let task = sections.join("\n\n---\n\n");
128
129 if let Some(iteration) = options.iteration {
130 Ok(build_prompt_preamble(
131 iteration,
132 options.max_iterations,
133 options.min_iterations,
134 &options.completion_promise,
135 options.context_content.as_deref(),
136 options.validation_failure.as_deref(),
137 &task,
138 )
139 .trim()
140 .to_string())
141 } else {
142 Ok(task)
143 }
144}
145
146fn load_task_context(
147 task_repo: &(impl DomainTaskRepository + ?Sized),
148 change_id: &str,
149) -> CoreResult<Option<String>> {
150 let summary = match get_task_status_from_repository(task_repo, change_id) {
151 Ok(summary) => summary,
152 Err(CoreError::NotFound(_)) => return Ok(None),
153 Err(err) => return Err(err),
154 };
155
156 let next_task = get_next_task_from_summary(&summary, "tasks.md")?;
157 let ready_tasks = summary
158 .ready
159 .iter()
160 .take(3)
161 .map(|task| format!("- {} {}", task.id, task.name))
162 .collect::<Vec<_>>();
163 let next_task_section = match next_task {
164 Some(task) => format!(
165 "## Next Actionable Task\n\n- Current task: {} {}\n- Action: {}\n",
166 task.id, task.name, task.action
167 ),
168 None => "## Next Actionable Task\n\n- No ready task found. Finish remaining in-progress work or repair the task file if it is blocked.\n"
169 .to_string(),
170 };
171 let ready_section = if ready_tasks.is_empty() {
172 String::new()
173 } else {
174 format!("\nReady queue:\n{}\n", ready_tasks.join("\n"))
175 };
176
177 Ok(Some(format!(
178 "## Change Task Status ({change_id})\n\n- Progress: {complete}/{total} complete, {in_progress} in progress, {pending} pending, {shelved} shelved\n{ready_section}\n{next_task_section}\n## Execution Guidance\n\n- Work the next actionable task first and keep the change tasks/specs aligned with the code.\n- Use `ito tasks` commands when task state needs to change.\n- Before claiming completion, make sure project validation and change task validation will pass.\n",
179 change_id = change_id,
180 complete = summary.progress.complete,
181 total = summary.progress.total,
182 in_progress = summary.progress.in_progress,
183 pending = summary.progress.pending,
184 shelved = summary.progress.shelved,
185 ready_section = ready_section,
186 next_task_section = next_task_section,
187 )))
188}
189
190fn load_change_context(
191 ito_path: &Path,
192 change_repo: &(impl DomainChangeRepository + ?Sized),
193 change_id: &str,
194) -> CoreResult<Option<String>> {
195 let changes_dir = paths::changes_dir(ito_path);
196 let resolved = resolve_change_id(change_repo, change_id)?;
197 let Some(resolved) = resolved else {
198 return Ok(None);
199 };
200
201 let proposal_path = changes_dir.join(&resolved).join("proposal.md");
202 if !proposal_path.exists() {
203 return Ok(None);
204 }
205
206 let proposal = ito_common::io::read_to_string_std(&proposal_path)
207 .map_err(|e| CoreError::io(format!("reading {}", proposal_path.display()), e))?;
208 Ok(Some(format!(
209 "## Change Proposal ({id})\n\n{proposal}",
210 id = resolved,
211 proposal = proposal
212 )))
213}
214
215fn resolve_change_id(
216 change_repo: &(impl DomainChangeRepository + ?Sized),
217 input: &str,
218) -> CoreResult<Option<String>> {
219 match change_repo.resolve_target(input) {
220 ChangeTargetResolution::Unique(id) => Ok(Some(id)),
221 ChangeTargetResolution::NotFound => Ok(None),
222 ChangeTargetResolution::Ambiguous(matches) => Err(CoreError::Validation(format!(
223 "Ambiguous change id '{input}'. Matches: {matches}",
224 input = input,
225 matches = matches.join(", ")
226 ))),
227 }
228}
229
230fn load_module_context(
231 ito_path: &Path,
232 module_repo: &(impl DomainModuleRepository + ?Sized),
233 module_id: &str,
234) -> CoreResult<Option<String>> {
235 let resolved = validate::resolve_module(module_repo, ito_path, module_id)?;
236 let Some(resolved) = resolved else {
237 return Ok(None);
238 };
239
240 if !resolved.module_md.exists() {
241 return Ok(None);
242 }
243
244 let module_content = ito_common::io::read_to_string_std(&resolved.module_md)
245 .map_err(|e| CoreError::io(format!("reading {}", resolved.module_md.display()), e))?;
246 Ok(Some(format!(
247 "## Module ({id})\n\n{content}",
248 id = resolved.id,
249 content = module_content
250 )))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn build_prompt_preamble_includes_iteration() {
259 let result = build_prompt_preamble(3, Some(10), 1, "DONE_TOKEN", None, None, "Test task");
260 assert!(result.contains("3"));
261 assert!(result.contains("10"));
262 }
263
264 #[test]
265 fn build_prompt_preamble_includes_completion_promise() {
266 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
267 assert!(result.contains("DONE_TOKEN"));
268 }
269
270 #[test]
271 fn build_prompt_preamble_includes_context() {
272 let result = build_prompt_preamble(
273 1,
274 Some(5),
275 1,
276 "DONE_TOKEN",
277 Some("extra context"),
278 None,
279 "Test task",
280 );
281 assert!(result.contains("extra context"));
282 }
283
284 #[test]
285 fn build_prompt_preamble_includes_validation_failure() {
286 let result = build_prompt_preamble(
287 1,
288 Some(5),
289 1,
290 "DONE_TOKEN",
291 None,
292 Some("task X not done"),
293 "Test task",
294 );
295 assert!(result.contains("task X not done"));
296 }
297
298 #[test]
299 fn build_prompt_preamble_omits_context_when_none() {
300 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
301 assert!(!result.contains("Additional Context"));
302 }
303
304 #[test]
305 fn build_prompt_preamble_omits_validation_when_none() {
306 let result = build_prompt_preamble(1, Some(5), 1, "DONE_TOKEN", None, None, "Test task");
307 assert!(!result.contains("Validation Failure"));
308 }
309}