Skip to main content

roder_roadmap/
prompts.rs

1use crate::{DiagnosticSeverity, Document, Task, ValidationResult};
2
3/// Delegation contract injected into every orchestrator-facing roadmap prompt.
4///
5/// The orchestrator coordinates many workers; it never implements tasks in its
6/// own thread. Tool names must match the specs in `tools.rs`.
7pub 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}