1use crate::config::{CoreConfig, EventMetadata};
9use ralph_proto::Hat;
10use std::collections::HashMap;
11
12#[derive(Debug)]
17pub struct InstructionBuilder {
18 core: CoreConfig,
19 events: HashMap<String, EventMetadata>,
21}
22
23impl InstructionBuilder {
24 #[allow(unused_variables)]
26 pub fn new(completion_promise: impl Into<String>, core: CoreConfig) -> Self {
27 Self {
28 core,
29 events: HashMap::new(),
30 }
31 }
32
33 #[allow(unused_variables)]
35 pub fn with_events(
36 completion_promise: impl Into<String>,
37 core: CoreConfig,
38 events: HashMap<String, EventMetadata>,
39 ) -> Self {
40 Self { core, events }
41 }
42
43 fn derive_instructions_from_contract(&self, hat: &Hat) -> String {
52 let mut behaviors: Vec<String> = Vec::new();
53
54 for trigger in &hat.subscriptions {
56 let trigger_str = trigger.as_str();
57
58 if let Some(meta) = self.events.get(trigger_str)
60 && !meta.on_trigger.is_empty()
61 {
62 behaviors.push(format!("**On `{}`:** {}", trigger_str, meta.on_trigger));
63 continue;
64 }
65
66 let default_behavior = match trigger_str {
68 "task.start" | "task.resume" => {
69 Some("Analyze the task and create a plan in the scratchpad.")
70 }
71 "build.done" => Some("Review the completed work and decide next steps."),
72 "build.blocked" => Some(
73 "Analyze the blocker and decide how to unblock (simplify task, gather info, or escalate).",
74 ),
75 "build.task" => Some(
76 "Implement the assigned task. Follow existing patterns. Run backpressure (tests/checks). Commit atomically when tests pass.",
77 ),
78 "review.request" => Some(
79 "Review the recent changes for correctness, tests, patterns, errors, and security.",
80 ),
81 "review.approved" => Some("Mark the task complete `[x]` and proceed to next task."),
82 "review.changes_requested" => Some("Add fix tasks to scratchpad and dispatch."),
83 _ => None,
84 };
85
86 if let Some(behavior) = default_behavior {
87 behaviors.push(format!("**On `{}`:** {}", trigger_str, behavior));
88 }
89 }
90
91 for publish in &hat.publishes {
93 let publish_str = publish.as_str();
94
95 if let Some(meta) = self.events.get(publish_str)
97 && !meta.on_publish.is_empty()
98 {
99 behaviors.push(format!(
100 "**Publish `{}`:** {}",
101 publish_str, meta.on_publish
102 ));
103 continue;
104 }
105
106 let default_behavior = match publish_str {
108 "build.task" => Some("Dispatch ONE AT A TIME for pending `[ ]` tasks."),
109 "build.done" => Some("When implementation is finished and tests pass."),
110 "build.blocked" => Some("When stuck - include what you tried and why it failed."),
111 "review.request" => Some("After build completion, before marking done."),
112 "review.approved" => Some("If changes look good and meet requirements."),
113 "review.changes_requested" => Some("If issues found - include specific feedback."),
114 _ => None,
115 };
116
117 if let Some(behavior) = default_behavior {
118 behaviors.push(format!("**Publish `{}`:** {}", publish_str, behavior));
119 }
120 }
121
122 if !hat.publishes.is_empty() {
124 let topics: Vec<&str> = hat.publishes.iter().map(|t| t.as_str()).collect();
125 behaviors.push(format!(
126 "You MUST publish one of: `{}` every iteration or the loop will terminate.",
127 topics.join("`, `")
128 ));
129 }
130
131 if behaviors.is_empty() {
132 "Follow the incoming event instructions.".to_string()
133 } else {
134 format!("### Derived Behaviors\n\n{}", behaviors.join("\n\n"))
135 }
136 }
137
138 pub fn build_custom_hat(&self, hat: &Hat, events_context: &str) -> String {
143 let guardrails = self
144 .core
145 .guardrails
146 .iter()
147 .enumerate()
148 .map(|(i, g)| format!("{}. {g}", 999 + i))
149 .collect::<Vec<_>>()
150 .join("\n");
151
152 let role_instructions = if hat.instructions.is_empty() {
153 self.derive_instructions_from_contract(hat)
154 } else {
155 hat.instructions.clone()
156 };
157
158 let (publish_topics, must_publish) = if hat.publishes.is_empty() {
159 (String::new(), String::new())
160 } else {
161 let topics: Vec<&str> = hat.publishes.iter().map(|t| t.as_str()).collect();
162 let topics_list = topics.join(", ");
163 let topics_backticked = format!("`{}`", topics.join("`, `"));
164
165 (
166 format!("You publish to: {}", topics_list),
167 format!(
168 "\n\nYou MUST publish one of these events: {}\nYou MUST NOT end the iteration without publishing because this will terminate the loop.",
169 topics_backticked
170 ),
171 )
172 };
173
174 format!(
175 r"You are {name}. You have fresh context each iteration.
176
177### 0. ORIENTATION
178You MUST study the incoming event context.
179You MUST NOT assume work isn't done — verify first.
180
181### 1. EXECUTE
182{role_instructions}
183You MUST NOT use more than 1 subagent for build/tests.
184
185### 2. VERIFY
186You MUST run tests and verify implementation before reporting done.
187You MUST NOT report completion without evidence (test output, build success).
188You MUST NOT close tasks unless ALL conditions are met:
189- Implementation is actually complete (not partially done)
190- Tests pass (run them and verify output)
191- Build succeeds (if applicable)
192
193### 3. REPORT
194You MUST publish a result event with evidence.
195{publish_topics}{must_publish}
196
197### GUARDRAILS
198{guardrails}
199
200---
201You MUST handle these events:
202{events}",
203 name = hat.name,
204 role_instructions = role_instructions,
205 publish_topics = publish_topics,
206 must_publish = must_publish,
207 guardrails = guardrails,
208 events = events_context,
209 )
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 fn default_builder(promise: &str) -> InstructionBuilder {
218 InstructionBuilder::new(promise, CoreConfig::default())
219 }
220
221 #[test]
222 fn test_custom_hat_with_rfc2119_patterns() {
223 let builder = default_builder("DONE");
224 let hat = Hat::new("reviewer", "Code Reviewer")
225 .with_instructions("Review PRs for quality and correctness.");
226
227 let instructions = builder.build_custom_hat(&hat, "PR #123 ready for review");
228
229 assert!(instructions.contains("Code Reviewer"));
231 assert!(instructions.contains("You have fresh context each iteration"));
232
233 assert!(instructions.contains("### 0. ORIENTATION"));
235 assert!(instructions.contains("You MUST study the incoming event context"));
236 assert!(instructions.contains("You MUST NOT assume work isn't done"));
237
238 assert!(instructions.contains("### 1. EXECUTE"));
240 assert!(instructions.contains("Review PRs for quality"));
241 assert!(instructions.contains("You MUST NOT use more than 1 subagent for build/tests"));
242
243 assert!(instructions.contains("### 2. VERIFY"));
245 assert!(instructions.contains("You MUST run tests and verify implementation"));
246 assert!(instructions.contains("You MUST NOT close tasks unless"));
247
248 assert!(instructions.contains("### 3. REPORT"));
250 assert!(instructions.contains("You MUST publish a result event"));
251
252 assert!(instructions.contains("### GUARDRAILS"));
254 assert!(instructions.contains("999."));
255
256 assert!(instructions.contains("You MUST handle these events"));
258 assert!(instructions.contains("PR #123 ready for review"));
259 }
260
261 #[test]
262 fn test_custom_guardrails_injected() {
263 let custom_core = CoreConfig {
264 scratchpad: ".workspace/plan.md".to_string(),
265 specs_dir: "./specifications/".to_string(),
266 guardrails: vec!["Custom rule one".to_string(), "Custom rule two".to_string()],
267 workspace_root: std::path::PathBuf::from("."),
268 };
269 let builder = InstructionBuilder::new("DONE", custom_core);
270
271 let hat = Hat::new("worker", "Worker").with_instructions("Do the work.");
272 let instructions = builder.build_custom_hat(&hat, "context");
273
274 assert!(instructions.contains("999. Custom rule one"));
276 assert!(instructions.contains("1000. Custom rule two"));
277 }
278
279 #[test]
280 fn test_must_publish_injected_for_explicit_instructions() {
281 use ralph_proto::Topic;
282
283 let builder = default_builder("DONE");
284 let hat = Hat::new("reviewer", "Code Reviewer")
285 .with_instructions("Review PRs for quality and correctness.")
286 .with_publishes(vec![
287 Topic::new("review.approved"),
288 Topic::new("review.changes_requested"),
289 ]);
290
291 let instructions = builder.build_custom_hat(&hat, "PR #123 ready");
292
293 assert!(
295 instructions.contains("You MUST publish one of these events"),
296 "Must-publish rule should be injected for custom hats with publishes"
297 );
298 assert!(instructions.contains("`review.approved`"));
299 assert!(instructions.contains("`review.changes_requested`"));
300 assert!(instructions.contains("You MUST NOT end the iteration without publishing"));
301 }
302
303 #[test]
304 fn test_must_publish_not_injected_when_no_publishes() {
305 let builder = default_builder("DONE");
306 let hat = Hat::new("observer", "Silent Observer")
307 .with_instructions("Observe and log, but do not emit events.");
308
309 let instructions = builder.build_custom_hat(&hat, "Observe this");
310
311 assert!(
315 !instructions.contains("You MUST publish one of these events"),
316 "Specific must-publish list should NOT be injected when hat has no publishes"
317 );
318 }
319
320 #[test]
321 fn test_derived_behaviors_when_no_explicit_instructions() {
322 use ralph_proto::Topic;
323
324 let builder = default_builder("DONE");
325 let hat = Hat::new("builder", "Builder")
326 .subscribe("build.task")
327 .with_publishes(vec![Topic::new("build.done"), Topic::new("build.blocked")]);
328
329 let instructions = builder.build_custom_hat(&hat, "Implement feature X");
330
331 assert!(instructions.contains("Derived Behaviors"));
333 assert!(instructions.contains("build.task"));
334 }
335}