1use crate::agents::AgentDef;
6use crate::commands::spawn::terminal::Harness;
7use crate::commands::swarm::session::WaveSummary;
8use crate::models::task::Task;
9use std::path::Path;
10
11#[derive(Debug, Clone)]
13pub struct ResolvedAgentConfig {
14 pub harness: Harness,
16 pub model: Option<String>,
18 pub prompt: String,
20 pub from_agent_def: bool,
22 pub agent_type: Option<String>,
24}
25
26pub fn resolve_agent_config(
39 task: &Task,
40 tag: &str,
41 default_harness: Harness,
42 default_model: Option<&str>,
43 working_dir: &Path,
44) -> ResolvedAgentConfig {
45 if let Some(ref agent_type) = task.agent_type {
46 match AgentDef::try_load(agent_type, working_dir) {
48 Some(agent_def) => {
49 let harness = agent_def.harness().unwrap_or(default_harness);
50 let model = agent_def
51 .model()
52 .map(String::from)
53 .or_else(|| default_model.map(String::from));
54
55 let prompt = match agent_def.prompt_template(working_dir) {
57 Some(template) => generate_prompt_with_template(task, tag, &template),
58 None => generate_prompt(task, tag),
59 };
60
61 ResolvedAgentConfig {
62 harness,
63 model,
64 prompt,
65 from_agent_def: true,
66 agent_type: Some(agent_type.clone()),
67 }
68 }
69 None => {
70 ResolvedAgentConfig {
72 harness: default_harness,
73 model: default_model.map(String::from),
74 prompt: generate_prompt(task, tag),
75 from_agent_def: false,
76 agent_type: Some(agent_type.clone()),
77 }
78 }
79 }
80 } else {
81 ResolvedAgentConfig {
83 harness: default_harness,
84 model: default_model.map(String::from),
85 prompt: generate_prompt(task, tag),
86 from_agent_def: false,
87 agent_type: None,
88 }
89 }
90}
91
92impl ResolvedAgentConfig {
93 pub fn display_info(&self) -> String {
95 let model_str = self
96 .model
97 .as_deref()
98 .map(|m| format!(":{}", m))
99 .unwrap_or_default();
100
101 if let Some(ref agent_type) = self.agent_type {
102 format!("{}{}@{}", self.harness.name(), model_str, agent_type)
103 } else {
104 format!("{}{}", self.harness.name(), model_str)
105 }
106 }
107}
108
109pub fn generate_prompt(task: &Task, tag: &str) -> String {
111 let mut prompt = format!(
112 r#"You are working on SCUD task {id}: {title}
113
114Tag: {tag}
115Complexity: {complexity}
116Priority: {priority:?}
117
118Description:
119{description}
120"#,
121 id = task.id,
122 title = task.title,
123 tag = tag,
124 complexity = task.complexity,
125 priority = task.priority,
126 description = task.description,
127 );
128
129 if let Some(ref details) = task.details {
131 prompt.push_str(&format!(
132 r#"
133Technical Details:
134{}
135"#,
136 details
137 ));
138 }
139
140 if let Some(ref test_strategy) = task.test_strategy {
142 prompt.push_str(&format!(
143 r#"
144Test Strategy:
145{}
146"#,
147 test_strategy
148 ));
149 }
150
151 if !task.dependencies.is_empty() {
153 prompt.push_str(&format!(
154 r#"
155Dependencies (should be done):
156{}
157"#,
158 task.dependencies.join(", ")
159 ));
160 }
161
162 prompt.push_str(&format!(
164 r#"
165Instructions:
1661. Check for discoveries from other agents: scud log-all --limit 10
1672. Explore the codebase to understand the context for this task
1683. Implement the task following project conventions and patterns
1694. Log important discoveries to share with other agents:
170 scud log {id} "Found X in Y, useful for Z"
1715. Write tests if applicable based on the test strategy
1726. When complete, run: scud set-status {id} done
1737. If blocked by issues, run: scud set-status {id} blocked
174
175Discovery Logging:
176- Log findings that other agents might benefit from (file locations, patterns, gotchas)
177- Keep logs concise but informative (1-3 sentences)
178- Example: scud log {id} "Auth helpers are in lib/auth.rs, not utils/"
179
180Begin by checking recent logs and exploring relevant code.
181"#,
182 id = task.id
183 ));
184
185 prompt
186}
187
188pub fn generate_minimal_prompt(task: &Task, tag: &str) -> String {
190 format!(
191 r#"SCUD Task {id}: {title}
192
193Tag: {tag}
194Description: {description}
195
196First: scud log-all --limit 5 (check recent discoveries)
197Log findings: scud log {id} "your discovery"
198When done: scud set-status {id} done
199If blocked: scud set-status {id} blocked
200"#,
201 id = task.id,
202 title = task.title,
203 tag = tag,
204 description = task.description
205 )
206}
207
208pub fn generate_prompt_with_template(task: &Task, tag: &str, template: &str) -> String {
221 let mut result = template.to_string();
222
223 result = result.replace("{task.id}", &task.id);
224 result = result.replace("{task.title}", &task.title);
225 result = result.replace("{task.description}", &task.description);
226 result = result.replace("{task.complexity}", &task.complexity.to_string());
227 result = result.replace("{task.priority}", &format!("{:?}", task.priority));
228 result = result.replace("{task.details}", task.details.as_deref().unwrap_or(""));
229 result = result.replace(
230 "{task.test_strategy}",
231 task.test_strategy.as_deref().unwrap_or(""),
232 );
233 result = result.replace("{task.dependencies}", &task.dependencies.join(", "));
234 result = result.replace("{tag}", tag);
235
236 result
237}
238
239pub fn generate_review_prompt(
241 summary: &WaveSummary,
242 tasks: &[(String, String)], review_all: bool,
244) -> String {
245 let tasks_str = if review_all {
246 tasks
247 .iter()
248 .map(|(id, title)| format!("- {} | {}", id, title))
249 .collect::<Vec<_>>()
250 .join("\n")
251 } else {
252 let sample: Vec<_> = if tasks.len() <= 3 {
254 tasks.iter().collect()
255 } else {
256 vec![&tasks[0], &tasks[tasks.len() / 2], &tasks[tasks.len() - 1]]
257 };
258 sample
259 .iter()
260 .map(|(id, title)| format!("- {} | {}", id, title))
261 .collect::<Vec<_>>()
262 .join("\n")
263 };
264
265 let files_str = if summary.files_changed.len() <= 10 {
266 summary.files_changed.join("\n")
267 } else {
268 let mut s = summary.files_changed[..10].join("\n");
269 s.push_str(&format!(
270 "\n... and {} more files",
271 summary.files_changed.len() - 10
272 ));
273 s
274 };
275
276 format!(
277 r#"You are reviewing SCUD wave {wave_number}.
278
279## Tasks to Review
280{tasks}
281
282## Files Changed
283{files}
284
285## Review Process
2861. For each task, run: scud show <task_id>
2872. Read the changed files relevant to each task
2883. Check implementation quality and correctness
289
290## Output Format
291For each task:
292 PASS: <task_id> - looks good
293 IMPROVE: <task_id> - <specific issue>
294
295When complete, create marker file:
296 echo "REVIEW_COMPLETE: ALL_PASS" > .scud/review-complete-{wave_number}
297Or if improvements needed:
298 echo "REVIEW_COMPLETE: IMPROVEMENTS_NEEDED" > .scud/review-complete-{wave_number}
299 echo "IMPROVE_TASKS: <comma-separated task IDs>" >> .scud/review-complete-{wave_number}
300"#,
301 wave_number = summary.wave_number,
302 tasks = tasks_str,
303 files = files_str,
304 )
305}
306
307pub fn generate_repair_prompt(
309 task_id: &str,
310 task_title: &str,
311 failed_command: &str,
312 error_output: &str,
313 task_files: &[String],
314 error_files: &[String],
315) -> String {
316 let task_files_str = task_files.join(", ");
317 let error_files_str = error_files.join(", ");
318
319 format!(
320 r#"You are a repair agent fixing validation failures for SCUD task {task_id}: {task_title}
321
322## Validation Failure
323The following validation command failed:
324{failed_command}
325
326Error output:
327{error_output}
328
329## Attribution
330This failure has been attributed to task {task_id} based on git blame analysis.
331Files changed by this task: {task_files}
332
333## Your Mission
3341. Analyze the error output to understand what went wrong
3352. Read the relevant files: {error_files}
3363. Fix the issue while preserving the task's intended functionality
3374. Run the validation command to verify the fix: {failed_command}
338
339## Important
340- Focus on fixing the specific error, don't refactor unrelated code
341- If the fix requires changes to other tasks' code, note it but don't modify
342- After fixing, commit with: scud commit -m "fix: {task_id} - <description>"
343- Log what you fixed for other agents: scud log {task_id} "Fixed: <brief description>"
344
345When the validation passes:
346 scud log {task_id} "Repair successful: <what was fixed>"
347 scud set-status {task_id} done
348 echo "REPAIR_COMPLETE: SUCCESS" > .scud/repair-complete-{task_id}
349
350If you cannot fix it:
351 scud log {task_id} "Repair blocked: <reason>"
352 scud set-status {task_id} blocked
353 echo "REPAIR_COMPLETE: BLOCKED" > .scud/repair-complete-{task_id}
354 echo "REASON: <explanation>" >> .scud/repair-complete-{task_id}
355"#,
356 task_id = task_id,
357 task_title = task_title,
358 failed_command = failed_command,
359 error_output = error_output,
360 task_files = task_files_str,
361 error_files = error_files_str,
362 )
363}
364
365pub fn generate_batch_repair_prompt(
367 tasks: &[(String, String, Vec<String>)], failed_command: &str,
369 error_output: &str,
370 error_locations: &[(String, Option<u32>)], ) -> String {
372 let tasks_str = tasks
373 .iter()
374 .map(|(id, title, files)| {
375 format!(
376 "- {} | {}\n Files: {}",
377 id,
378 title,
379 files.join(", ")
380 )
381 })
382 .collect::<Vec<_>>()
383 .join("\n");
384
385 let error_locations_str = error_locations
386 .iter()
387 .take(20) .map(|(file, line)| {
389 match line {
390 Some(l) => format!(" {}:{}", file, l),
391 None => format!(" {}", file),
392 }
393 })
394 .collect::<Vec<_>>()
395 .join("\n");
396
397 format!(
398 r#"You are a batch repair agent fixing validation failures for multiple SCUD tasks.
399
400## Validation Failure
401The following validation command failed:
402{failed_command}
403
404Error output:
405{error_output}
406
407## Error Locations
408{error_locations}
409
410## Responsible Tasks
411Based on git blame analysis, these tasks may be responsible:
412{tasks}
413
414## Your Mission
4151. Analyze the error output to understand ALL the issues
4162. Read the relevant files and understand what each task was trying to do
4173. Fix issues systematically - some errors may be related
4184. Run the validation command after each fix to check progress: {failed_command}
419
420## Process
421For each issue:
4221. Identify which task introduced it
4232. Read the task details: scud show <task_id>
4243. Fix the issue while preserving intended functionality
4254. Commit: scud commit -m "fix: <task_id> - <description>"
4265. Log: scud log <task_id> "Fixed: <brief description>"
427
428## Important
429- Fix ALL issues before signaling completion
430- Some issues may cascade - fix root causes first
431- If you cannot fix an issue, document why
432- Iterate until validation passes or you're truly blocked
433
434## Completion
435When ALL validation passes:
436 echo "BATCH_REPAIR_COMPLETE: SUCCESS" > .scud/batch-repair-complete
437 echo "FIXED_TASKS: <comma-separated task IDs that were fixed>" >> .scud/batch-repair-complete
438
439If blocked on some tasks:
440 echo "BATCH_REPAIR_COMPLETE: PARTIAL" > .scud/batch-repair-complete
441 echo "FIXED_TASKS: <task IDs fixed>" >> .scud/batch-repair-complete
442 echo "BLOCKED_TASKS: <task IDs blocked>" >> .scud/batch-repair-complete
443 echo "BLOCK_REASON: <explanation>" >> .scud/batch-repair-complete
444
445If completely blocked:
446 echo "BATCH_REPAIR_COMPLETE: BLOCKED" > .scud/batch-repair-complete
447 echo "REASON: <explanation>" >> .scud/batch-repair-complete
448"#,
449 failed_command = failed_command,
450 error_output = error_output,
451 error_locations = error_locations_str,
452 tasks = tasks_str,
453 )
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::models::task::Task;
460
461 #[test]
462 fn test_generate_prompt_basic() {
463 let task = Task::new(
464 "auth:1".to_string(),
465 "Implement login".to_string(),
466 "Add user authentication flow".to_string(),
467 );
468
469 let prompt = generate_prompt(&task, "auth");
470
471 assert!(prompt.contains("auth:1"));
472 assert!(prompt.contains("Implement login"));
473 assert!(prompt.contains("Tag: auth"));
474 assert!(prompt.contains("scud set-status auth:1 done"));
475 }
476
477 #[test]
478 fn test_generate_prompt_with_details() {
479 let mut task = Task::new(
480 "api:2".to_string(),
481 "Add endpoint".to_string(),
482 "Create REST endpoint".to_string(),
483 );
484 task.details = Some("Use Express.js router pattern".to_string());
485 task.test_strategy = Some("Unit test with Jest".to_string());
486
487 let prompt = generate_prompt(&task, "api");
488
489 assert!(prompt.contains("Technical Details:"));
490 assert!(prompt.contains("Express.js router"));
491 assert!(prompt.contains("Test Strategy:"));
492 assert!(prompt.contains("Unit test with Jest"));
493 }
494
495 #[test]
496 fn test_generate_minimal_prompt() {
497 let task = Task::new(
498 "fix:1".to_string(),
499 "Quick fix".to_string(),
500 "Fix typo".to_string(),
501 );
502
503 let prompt = generate_minimal_prompt(&task, "fix");
504
505 assert!(prompt.contains("fix:1"));
506 assert!(prompt.contains("Quick fix"));
507 assert!(!prompt.contains("Technical Details"));
508 }
509
510 #[test]
511 fn test_generate_prompt_with_template() {
512 let mut task = Task::new(
513 "auth:1".to_string(),
514 "Login Feature".to_string(),
515 "Implement login".to_string(),
516 );
517 task.complexity = 5;
518 task.details = Some("Use OAuth".to_string());
519
520 let template = "Task: {task.id} - {task.title}\nTag: {tag}\nDetails: {task.details}";
521 let prompt = generate_prompt_with_template(&task, "auth", template);
522
523 assert_eq!(
524 prompt,
525 "Task: auth:1 - Login Feature\nTag: auth\nDetails: Use OAuth"
526 );
527 }
528
529 #[test]
530 fn test_generate_prompt_with_template_missing_fields() {
531 let task = Task::new("1".to_string(), "Title".to_string(), "Desc".to_string());
532
533 let template = "Details: {task.details} | Strategy: {task.test_strategy}";
534 let prompt = generate_prompt_with_template(&task, "test", template);
535
536 assert_eq!(prompt, "Details: | Strategy: ");
537 }
538
539 #[test]
540 fn test_generate_review_prompt_all() {
541 let summary = WaveSummary {
542 wave_number: 1,
543 tasks_completed: vec!["auth:1".to_string(), "auth:2".to_string()],
544 files_changed: vec!["src/auth.rs".to_string(), "src/main.rs".to_string()],
545 };
546
547 let tasks = vec![
548 ("auth:1".to_string(), "Add login".to_string()),
549 ("auth:2".to_string(), "Add logout".to_string()),
550 ];
551
552 let prompt = generate_review_prompt(&summary, &tasks, true);
553
554 assert!(prompt.contains("wave 1"));
555 assert!(prompt.contains("auth:1 | Add login"));
556 assert!(prompt.contains("auth:2 | Add logout"));
557 assert!(prompt.contains("src/auth.rs"));
558 }
559
560 #[test]
561 fn test_generate_review_prompt_sampled() {
562 let summary = WaveSummary {
563 wave_number: 2,
564 tasks_completed: vec![
565 "t:1".to_string(),
566 "t:2".to_string(),
567 "t:3".to_string(),
568 "t:4".to_string(),
569 "t:5".to_string(),
570 ],
571 files_changed: vec!["a.rs".to_string()],
572 };
573
574 let tasks: Vec<_> = (1..=5)
575 .map(|i| (format!("t:{}", i), format!("Task {}", i)))
576 .collect();
577
578 let prompt = generate_review_prompt(&summary, &tasks, false);
579
580 assert!(prompt.contains("t:1"));
582 assert!(prompt.contains("t:3")); assert!(prompt.contains("t:5")); assert!(!prompt.contains("t:2 | Task 2"));
586 assert!(!prompt.contains("t:4 | Task 4"));
587 }
588
589 #[test]
590 fn test_generate_repair_prompt() {
591 let prompt = generate_repair_prompt(
592 "auth:1",
593 "Add login",
594 "cargo build",
595 "error: mismatched types at src/main.rs:42",
596 &["src/auth.rs".to_string()],
597 &["src/main.rs".to_string()],
598 );
599
600 assert!(prompt.contains("auth:1"));
601 assert!(prompt.contains("Add login"));
602 assert!(prompt.contains("cargo build"));
603 assert!(prompt.contains("mismatched types"));
604 assert!(prompt.contains("src/auth.rs"));
605 assert!(prompt.contains("src/main.rs"));
606 assert!(prompt.contains("REPAIR_COMPLETE"));
607 }
608
609 #[test]
614 fn test_resolve_agent_config_no_agent_type() {
615 let temp = tempfile::TempDir::new().unwrap();
616 let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
617
618 let config = resolve_agent_config(&task, "test", Harness::Claude, None, temp.path());
619
620 assert_eq!(config.harness, Harness::Claude);
621 assert_eq!(config.model, None);
622 assert!(!config.from_agent_def);
623 assert!(config.agent_type.is_none());
624 }
625
626 #[test]
627 fn test_resolve_agent_config_uses_default_model() {
628 let temp = tempfile::TempDir::new().unwrap();
629 let task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
630
631 let config =
632 resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
633
634 assert_eq!(config.harness, Harness::Claude);
635 assert_eq!(config.model, Some("opus".to_string()));
636 assert!(!config.from_agent_def);
637 }
638
639 #[test]
640 fn test_resolve_agent_config_agent_type_not_found() {
641 let temp = tempfile::TempDir::new().unwrap();
642 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
643 task.agent_type = Some("nonexistent".to_string());
644
645 let config =
646 resolve_agent_config(&task, "test", Harness::Claude, Some("sonnet"), temp.path());
647
648 assert_eq!(config.harness, Harness::Claude);
650 assert_eq!(config.model, Some("sonnet".to_string()));
651 assert!(!config.from_agent_def);
652 assert_eq!(config.agent_type, Some("nonexistent".to_string()));
653 }
654
655 #[test]
656 fn test_resolve_agent_config_from_agent_def() {
657 let temp = tempfile::TempDir::new().unwrap();
658 let agents_dir = temp.path().join(".scud").join("agents");
659 std::fs::create_dir_all(&agents_dir).unwrap();
660
661 let agent_file = agents_dir.join("fast-builder.toml");
663 std::fs::write(
664 &agent_file,
665 r#"
666[agent]
667name = "fast-builder"
668description = "Fast builder"
669
670[model]
671harness = "opencode"
672model = "xai/grok-code-fast-1"
673"#,
674 )
675 .unwrap();
676
677 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
678 task.agent_type = Some("fast-builder".to_string());
679
680 let config =
682 resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
683
684 assert_eq!(config.harness, Harness::OpenCode);
685 assert_eq!(config.model, Some("xai/grok-code-fast-1".to_string()));
686 assert!(config.from_agent_def);
687 assert_eq!(config.agent_type, Some("fast-builder".to_string()));
688 }
689
690 #[test]
691 fn test_resolve_agent_config_agent_def_without_model_uses_default() {
692 let temp = tempfile::TempDir::new().unwrap();
693 let agents_dir = temp.path().join(".scud").join("agents");
694 std::fs::create_dir_all(&agents_dir).unwrap();
695
696 let agent_file = agents_dir.join("custom.toml");
698 std::fs::write(
699 &agent_file,
700 r#"
701[agent]
702name = "custom"
703
704[model]
705harness = "opencode"
706"#,
707 )
708 .unwrap();
709
710 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
711 task.agent_type = Some("custom".to_string());
712
713 let config =
714 resolve_agent_config(&task, "test", Harness::Claude, Some("opus"), temp.path());
715
716 assert_eq!(config.harness, Harness::OpenCode);
718 assert_eq!(config.model, Some("opus".to_string()));
719 assert!(config.from_agent_def);
720 }
721
722 #[test]
723 fn test_resolve_agent_config_uses_custom_prompt_template() {
724 let temp = tempfile::TempDir::new().unwrap();
725 let agents_dir = temp.path().join(".scud").join("agents");
726 std::fs::create_dir_all(&agents_dir).unwrap();
727
728 let agent_file = agents_dir.join("templated.toml");
729 std::fs::write(
730 &agent_file,
731 r#"
732[agent]
733name = "templated"
734
735[model]
736harness = "claude"
737
738[prompt]
739template = "Custom: {task.title} in {tag}"
740"#,
741 )
742 .unwrap();
743
744 let mut task = Task::new("1".to_string(), "My Task".to_string(), "Desc".to_string());
745 task.agent_type = Some("templated".to_string());
746
747 let config = resolve_agent_config(&task, "my-tag", Harness::Claude, None, temp.path());
748
749 assert_eq!(config.prompt, "Custom: My Task in my-tag");
750 assert!(config.from_agent_def);
751 }
752
753 #[test]
754 fn test_resolved_agent_config_display_info() {
755 let config = ResolvedAgentConfig {
756 harness: Harness::OpenCode,
757 model: Some("xai/grok-code-fast-1".to_string()),
758 prompt: "test".to_string(),
759 from_agent_def: true,
760 agent_type: Some("fast-builder".to_string()),
761 };
762
763 assert_eq!(
764 config.display_info(),
765 "opencode:xai/grok-code-fast-1@fast-builder"
766 );
767
768 let config_no_model = ResolvedAgentConfig {
769 harness: Harness::Claude,
770 model: None,
771 prompt: "test".to_string(),
772 from_agent_def: false,
773 agent_type: None,
774 };
775
776 assert_eq!(config_no_model.display_info(), "claude");
777 }
778}