scud/commands/spawn/
agent.rs

1//! Agent prompt generation for Claude Code sessions
2//!
3//! Creates prompts that provide task context and instructions for Claude Code agents.
4
5use crate::commands::swarm::session::WaveSummary;
6use crate::models::task::Task;
7
8/// Generate a prompt for Claude Code with task context
9pub 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    // Add details if present
29    if let Some(ref details) = task.details {
30        prompt.push_str(&format!(
31            r#"
32Technical Details:
33{}
34"#,
35            details
36        ));
37    }
38
39    // Add test strategy if present
40    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    // Add dependencies info if any
51    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    // Add instructions
62    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
79/// Generate a shorter prompt for tasks with less context
80pub 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
94/// Generate a prompt using a custom template
95///
96/// Template placeholders:
97/// - {task.id} - Task ID
98/// - {task.title} - Task title
99/// - {task.description} - Task description
100/// - {task.complexity} - Complexity score
101/// - {task.priority} - Priority level
102/// - {task.details} - Technical details (empty if none)
103/// - {task.test_strategy} - Test strategy (empty if none)
104/// - {task.dependencies} - Comma-separated dependencies
105/// - {tag} - Phase/tag name
106pub 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
125/// Generate a prompt for wave review
126pub fn generate_review_prompt(
127    summary: &WaveSummary,
128    tasks: &[(String, String)], // (task_id, title)
129    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        // Sample: first task, last task, and one random middle task
139        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
193/// Generate a prompt for repair agent
194pub 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        // Should only include first, middle, and last (3 tasks sampled)
373        assert!(prompt.contains("t:1"));
374        assert!(prompt.contains("t:3")); // middle
375        assert!(prompt.contains("t:5")); // last
376                                         // t:2 and t:4 should not be present
377        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}