1use crate::commands::swarm::session::WaveSummary;
6use crate::models::task::Task;
7
8pub fn generate_prompt(task: &Task, tag: &str) -> String {
10 let mut prompt = format!(
11 r#"You are working on SCUD task {id}: {title}
12
13Tag: {tag}
14Complexity: {complexity}
15Priority: {priority:?}
16
17Description:
18{description}
19"#,
20 id = task.id,
21 title = task.title,
22 tag = tag,
23 complexity = task.complexity,
24 priority = task.priority,
25 description = task.description,
26 );
27
28 if let Some(ref details) = task.details {
30 prompt.push_str(&format!(
31 r#"
32Technical Details:
33{}
34"#,
35 details
36 ));
37 }
38
39 if let Some(ref test_strategy) = task.test_strategy {
41 prompt.push_str(&format!(
42 r#"
43Test Strategy:
44{}
45"#,
46 test_strategy
47 ));
48 }
49
50 if !task.dependencies.is_empty() {
52 prompt.push_str(&format!(
53 r#"
54Dependencies (should be done):
55{}
56"#,
57 task.dependencies.join(", ")
58 ));
59 }
60
61 prompt.push_str(&format!(
63 r#"
64Instructions:
651. First, explore the codebase to understand the context for this task
662. Implement the task following project conventions and patterns
673. Write tests if applicable based on the test strategy
684. When complete, run: scud set-status {} done
695. If blocked by issues, run: scud set-status {} blocked
70
71Begin by understanding what needs to be done and exploring relevant code.
72"#,
73 task.id, task.id
74 ));
75
76 prompt
77}
78
79pub fn generate_minimal_prompt(task: &Task, tag: &str) -> String {
81 format!(
82 r#"SCUD Task {}: {}
83
84Tag: {}
85Description: {}
86
87When done: scud set-status {} done
88If blocked: scud set-status {} blocked
89"#,
90 task.id, task.title, tag, task.description, task.id, task.id
91 )
92}
93
94pub fn generate_prompt_with_template(task: &Task, tag: &str, template: &str) -> String {
107 let mut result = template.to_string();
108
109 result = result.replace("{task.id}", &task.id);
110 result = result.replace("{task.title}", &task.title);
111 result = result.replace("{task.description}", &task.description);
112 result = result.replace("{task.complexity}", &task.complexity.to_string());
113 result = result.replace("{task.priority}", &format!("{:?}", task.priority));
114 result = result.replace("{task.details}", task.details.as_deref().unwrap_or(""));
115 result = result.replace(
116 "{task.test_strategy}",
117 task.test_strategy.as_deref().unwrap_or(""),
118 );
119 result = result.replace("{task.dependencies}", &task.dependencies.join(", "));
120 result = result.replace("{tag}", tag);
121
122 result
123}
124
125pub fn generate_review_prompt(
127 summary: &WaveSummary,
128 tasks: &[(String, String)], review_all: bool,
130) -> String {
131 let tasks_str = if review_all {
132 tasks
133 .iter()
134 .map(|(id, title)| format!("- {} | {}", id, title))
135 .collect::<Vec<_>>()
136 .join("\n")
137 } else {
138 let sample: Vec<_> = if tasks.len() <= 3 {
140 tasks.iter().collect()
141 } else {
142 vec![&tasks[0], &tasks[tasks.len() / 2], &tasks[tasks.len() - 1]]
143 };
144 sample
145 .iter()
146 .map(|(id, title)| format!("- {} | {}", id, title))
147 .collect::<Vec<_>>()
148 .join("\n")
149 };
150
151 let files_str = if summary.files_changed.len() <= 10 {
152 summary.files_changed.join("\n")
153 } else {
154 let mut s = summary.files_changed[..10].join("\n");
155 s.push_str(&format!(
156 "\n... and {} more files",
157 summary.files_changed.len() - 10
158 ));
159 s
160 };
161
162 format!(
163 r#"You are reviewing SCUD wave {wave_number}.
164
165## Tasks to Review
166{tasks}
167
168## Files Changed
169{files}
170
171## Review Process
1721. For each task, run: scud show <task_id>
1732. Read the changed files relevant to each task
1743. Check implementation quality and correctness
175
176## Output Format
177For each task:
178 PASS: <task_id> - looks good
179 IMPROVE: <task_id> - <specific issue>
180
181When complete, create marker file:
182 echo "REVIEW_COMPLETE: ALL_PASS" > .scud/review-complete-{wave_number}
183Or if improvements needed:
184 echo "REVIEW_COMPLETE: IMPROVEMENTS_NEEDED" > .scud/review-complete-{wave_number}
185 echo "IMPROVE_TASKS: <comma-separated task IDs>" >> .scud/review-complete-{wave_number}
186"#,
187 wave_number = summary.wave_number,
188 tasks = tasks_str,
189 files = files_str,
190 )
191}
192
193pub fn generate_repair_prompt(
195 task_id: &str,
196 task_title: &str,
197 failed_command: &str,
198 error_output: &str,
199 task_files: &[String],
200 error_files: &[String],
201) -> String {
202 let task_files_str = task_files.join(", ");
203 let error_files_str = error_files.join(", ");
204
205 format!(
206 r#"You are a repair agent fixing validation failures for SCUD task {task_id}: {task_title}
207
208## Validation Failure
209The following validation command failed:
210{failed_command}
211
212Error output:
213{error_output}
214
215## Attribution
216This failure has been attributed to task {task_id} based on git blame analysis.
217Files changed by this task: {task_files}
218
219## Your Mission
2201. Analyze the error output to understand what went wrong
2212. Read the relevant files: {error_files}
2223. Fix the issue while preserving the task's intended functionality
2234. Run the validation command to verify the fix: {failed_command}
224
225## Important
226- Focus on fixing the specific error, don't refactor unrelated code
227- If the fix requires changes to other tasks' code, note it but don't modify
228- After fixing, commit with: scud commit -m "fix: {task_id} - <description>"
229
230When the validation passes:
231 scud set-status {task_id} done
232 echo "REPAIR_COMPLETE: SUCCESS" > .scud/repair-complete-{task_id}
233
234If you cannot fix it:
235 scud set-status {task_id} blocked
236 echo "REPAIR_COMPLETE: BLOCKED" > .scud/repair-complete-{task_id}
237 echo "REASON: <explanation>" >> .scud/repair-complete-{task_id}
238"#,
239 task_id = task_id,
240 task_title = task_title,
241 failed_command = failed_command,
242 error_output = error_output,
243 task_files = task_files_str,
244 error_files = error_files_str,
245 )
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::models::task::Task;
252
253 #[test]
254 fn test_generate_prompt_basic() {
255 let task = Task::new(
256 "auth:1".to_string(),
257 "Implement login".to_string(),
258 "Add user authentication flow".to_string(),
259 );
260
261 let prompt = generate_prompt(&task, "auth");
262
263 assert!(prompt.contains("auth:1"));
264 assert!(prompt.contains("Implement login"));
265 assert!(prompt.contains("Tag: auth"));
266 assert!(prompt.contains("scud set-status auth:1 done"));
267 }
268
269 #[test]
270 fn test_generate_prompt_with_details() {
271 let mut task = Task::new(
272 "api:2".to_string(),
273 "Add endpoint".to_string(),
274 "Create REST endpoint".to_string(),
275 );
276 task.details = Some("Use Express.js router pattern".to_string());
277 task.test_strategy = Some("Unit test with Jest".to_string());
278
279 let prompt = generate_prompt(&task, "api");
280
281 assert!(prompt.contains("Technical Details:"));
282 assert!(prompt.contains("Express.js router"));
283 assert!(prompt.contains("Test Strategy:"));
284 assert!(prompt.contains("Unit test with Jest"));
285 }
286
287 #[test]
288 fn test_generate_minimal_prompt() {
289 let task = Task::new(
290 "fix:1".to_string(),
291 "Quick fix".to_string(),
292 "Fix typo".to_string(),
293 );
294
295 let prompt = generate_minimal_prompt(&task, "fix");
296
297 assert!(prompt.contains("fix:1"));
298 assert!(prompt.contains("Quick fix"));
299 assert!(!prompt.contains("Technical Details"));
300 }
301
302 #[test]
303 fn test_generate_prompt_with_template() {
304 let mut task = Task::new(
305 "auth:1".to_string(),
306 "Login Feature".to_string(),
307 "Implement login".to_string(),
308 );
309 task.complexity = 5;
310 task.details = Some("Use OAuth".to_string());
311
312 let template = "Task: {task.id} - {task.title}\nTag: {tag}\nDetails: {task.details}";
313 let prompt = generate_prompt_with_template(&task, "auth", template);
314
315 assert_eq!(
316 prompt,
317 "Task: auth:1 - Login Feature\nTag: auth\nDetails: Use OAuth"
318 );
319 }
320
321 #[test]
322 fn test_generate_prompt_with_template_missing_fields() {
323 let task = Task::new("1".to_string(), "Title".to_string(), "Desc".to_string());
324
325 let template = "Details: {task.details} | Strategy: {task.test_strategy}";
326 let prompt = generate_prompt_with_template(&task, "test", template);
327
328 assert_eq!(prompt, "Details: | Strategy: ");
329 }
330
331 #[test]
332 fn test_generate_review_prompt_all() {
333 let summary = WaveSummary {
334 wave_number: 1,
335 tasks_completed: vec!["auth:1".to_string(), "auth:2".to_string()],
336 files_changed: vec!["src/auth.rs".to_string(), "src/main.rs".to_string()],
337 };
338
339 let tasks = vec![
340 ("auth:1".to_string(), "Add login".to_string()),
341 ("auth:2".to_string(), "Add logout".to_string()),
342 ];
343
344 let prompt = generate_review_prompt(&summary, &tasks, true);
345
346 assert!(prompt.contains("wave 1"));
347 assert!(prompt.contains("auth:1 | Add login"));
348 assert!(prompt.contains("auth:2 | Add logout"));
349 assert!(prompt.contains("src/auth.rs"));
350 }
351
352 #[test]
353 fn test_generate_review_prompt_sampled() {
354 let summary = WaveSummary {
355 wave_number: 2,
356 tasks_completed: vec![
357 "t:1".to_string(),
358 "t:2".to_string(),
359 "t:3".to_string(),
360 "t:4".to_string(),
361 "t:5".to_string(),
362 ],
363 files_changed: vec!["a.rs".to_string()],
364 };
365
366 let tasks: Vec<_> = (1..=5)
367 .map(|i| (format!("t:{}", i), format!("Task {}", i)))
368 .collect();
369
370 let prompt = generate_review_prompt(&summary, &tasks, false);
371
372 assert!(prompt.contains("t:1"));
374 assert!(prompt.contains("t:3")); assert!(prompt.contains("t:5")); assert!(!prompt.contains("t:2 | Task 2"));
378 assert!(!prompt.contains("t:4 | Task 4"));
379 }
380
381 #[test]
382 fn test_generate_repair_prompt() {
383 let prompt = generate_repair_prompt(
384 "auth:1",
385 "Add login",
386 "cargo build",
387 "error: mismatched types at src/main.rs:42",
388 &["src/auth.rs".to_string()],
389 &["src/main.rs".to_string()],
390 );
391
392 assert!(prompt.contains("auth:1"));
393 assert!(prompt.contains("Add login"));
394 assert!(prompt.contains("cargo build"));
395 assert!(prompt.contains("mismatched types"));
396 assert!(prompt.contains("src/auth.rs"));
397 assert!(prompt.contains("src/main.rs"));
398 assert!(prompt.contains("REPAIR_COMPLETE"));
399 }
400}