1use crate::{DiagnosticSeverity, Document, Task, ValidationResult};
2
3pub const ORCHESTRATOR_RULES: &str = "Orchestrator contract:\n\
8- You are the roadmap orchestrator: triage tasks, delegate work, verify evidence, and keep the document current. Do not implement tasks in this thread.\n\
9- Fan out: spawn one worker per independent unchecked task with roadmap_thread_spawn, one call per task. Spawning several workers in a single turn is expected.\n\
10- Check roadmap_thread_list before spawning so a task does not get duplicate workers unless the user asks for redundancy.\n\
11- Parallelize tasks whose owned paths do not overlap; stagger tasks that share files.\n\
12- Keep roughly four workers active unless the user raises or lowers the budget.\n\
13- Steer or respawn a blocked worker instead of doing its work yourself.\n\
14- Mark a task done only through roadmap_set_task_state with non-empty evidence after its run commands and acceptance criteria pass.";
15
16#[derive(Debug, Clone)]
17pub struct RoadmapPromptInput<'a> {
18 pub document: &'a Document,
19 pub focused_task: Option<&'a Task>,
20 pub validation: Option<&'a ValidationResult>,
21 pub skill_body: Option<&'a str>,
22}
23
24pub fn roadmap_context_prompt(input: RoadmapPromptInput<'_>) -> String {
25 let mut sections = vec![
26 "You are in Roder roadmapping mode. Treat the roadmap Markdown document as the primary state; thread transcripts are supporting evidence.".to_string(),
27 ORCHESTRATOR_RULES.to_string(),
28 format!("Document: {}", input.document.title),
29 format!("Path: {}", input.document.path.display()),
30 format!("Goal: {}", input.document.goal),
31 ];
32 if let Some(task) = input.focused_task {
33 sections.push(format!(
34 "Focused task: {} [{}]",
35 task.heading,
36 if task.checked { "done" } else { "open" }
37 ));
38 if !task.run_blocks.is_empty() {
39 sections.push(format!(
40 "Focused task run commands:\n{}",
41 task.run_blocks.join("\n")
42 ));
43 }
44 }
45 if let Some(validation) = input.validation {
46 let diagnostics = validation
47 .diagnostics
48 .iter()
49 .map(|diagnostic| {
50 let severity = match diagnostic.severity {
51 DiagnosticSeverity::Error => "error",
52 DiagnosticSeverity::Warning => "warning",
53 };
54 format!(
55 "- {severity}: {}:{} {}",
56 diagnostic.path.display(),
57 diagnostic
58 .line
59 .map(|line| line.to_string())
60 .unwrap_or_else(|| "-".to_string()),
61 diagnostic.message
62 )
63 })
64 .collect::<Vec<_>>();
65 sections.push(if diagnostics.is_empty() {
66 "Validation: no diagnostics.".to_string()
67 } else {
68 format!("Validation diagnostics:\n{}", diagnostics.join("\n"))
69 });
70 }
71 if let Some(skill_body) = input.skill_body.filter(|body| !body.trim().is_empty()) {
72 sections.push(format!("Roadmap planning skill:\n{}", skill_body.trim()));
73 }
74 sections.join("\n\n")
75}