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