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 let example_topic = topics.first().copied().unwrap_or("event.name");
159
160 (
161 format!("You publish to: {}", topics_list),
162 format!(
163 "\n\nYou MUST emit exactly ONE of these events via `ralph emit \"<topic>\" \"<summary>\"`: {}\nUse `ralph emit \"{}\" \"<summary>\"` as the pattern.\nPlain-language summaries do NOT count as event publication.\nYou MUST stop immediately after emitting.\nYou MUST NOT end the iteration without publishing because this will terminate the loop.",
164 topics_backticked, example_topic
165 ),
166 )
167 };
168
169 format!(
170 r"You are {name}. You have fresh context each iteration.
171
172### 0. ORIENTATION
173You MUST study the incoming event context.
174You MUST NOT assume work isn't done — verify first.
175
176### 0b. TOOL DISCIPLINE
177Runtime work state lives in `ralph tools task`, not in ad hoc markdown checklists.
178You MUST check `<ready-tasks>` before creating more tasks.
179If this iteration creates or discovers durable work, you MUST represent it with `ralph tools task ensure`, `start`, `close`, `reopen`, or `fail` as appropriate.
180If you are entering an unfamiliar area, you SHOULD search memories with `ralph tools memory search` before acting.
181You SHOULD assume the workflow commands are available when the loop is already running and use the task-specific command you actually need.
182The loop sets `$RALPH_BIN` to the current Ralph executable. Prefer `$RALPH_BIN emit ...` and `$RALPH_BIN tools ...` when you need a direct command form.
183Do not spend iterations on shell or tool-availability diagnosis unless the task is explicitly about the runtime environment.
184If a command's stdout is empty or terse, verify the intended side effect in the task/event state or in the files and artifacts the command should have changed.
185Keep temporary artifacts where later steps can still inspect them, such as a repo-local `logs/` directory or `/var/tmp` when needed.
186If a command fails, a dependency is missing, or you become blocked, you MUST record a `fix` memory with `ralph tools memory add`.
187If the issue is not resolved in the same iteration, you MUST fail or reopen the relevant runtime task before stopping.
188If your confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
189If this turn is likely to run longer than a few minutes, you SHOULD send a non-blocking progress update with `ralph tools interact progress`.
190
191### 1. EXECUTE
192{role_instructions}
193You MUST NOT use more than 1 subagent for build/tests.
194
195### 2. VERIFY
196You MUST run tests and verify implementation before reporting done.
197You MUST NOT report completion without evidence (test output, build success).
198You MUST NOT close tasks unless ALL conditions are met:
199- Implementation is actually complete (not partially done)
200- Tests pass (run them and verify output)
201- Build succeeds (if applicable)
202
203### 3. REPORT
204You MUST publish a result event with evidence using `ralph emit`.
205{publish_topics}{must_publish}
206
207### GUARDRAILS
208{guardrails}
209
210---
211You MUST handle these events:
212{events}",
213 name = hat.name,
214 role_instructions = role_instructions,
215 publish_topics = publish_topics,
216 must_publish = must_publish,
217 guardrails = guardrails,
218 events = events_context,
219 )
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::config::ScratchpadConfig;
227
228 fn default_builder() -> InstructionBuilder {
229 InstructionBuilder::new(CoreConfig::default())
230 }
231
232 #[test]
233 fn test_custom_hat_with_rfc2119_patterns() {
234 let builder = default_builder();
235 let hat = Hat::new("reviewer", "Code Reviewer")
236 .with_instructions("Review PRs for quality and correctness.");
237
238 let instructions = builder.build_custom_hat(&hat, "PR #123 ready for review");
239
240 assert!(instructions.contains("Code Reviewer"));
242 assert!(instructions.contains("You have fresh context each iteration"));
243
244 assert!(instructions.contains("### 0. ORIENTATION"));
246 assert!(instructions.contains("You MUST study the incoming event context"));
247 assert!(instructions.contains("You MUST NOT assume work isn't done"));
248
249 assert!(instructions.contains("### 0b. TOOL DISCIPLINE"));
250 assert!(instructions.contains("You MUST check `<ready-tasks>` before creating more tasks"));
251 assert!(
252 instructions
253 .contains("`ralph tools task ensure`, `start`, `close`, `reopen`, or `fail`")
254 );
255 assert!(instructions.contains("`ralph tools memory search`"));
256 assert!(instructions.contains("`ralph tools memory add`"));
257 assert!(instructions.contains(".ralph/agent/decisions.md"));
258 assert!(instructions.contains("ralph tools interact progress"));
259
260 assert!(instructions.contains("### 1. EXECUTE"));
262 assert!(instructions.contains("Review PRs for quality"));
263 assert!(instructions.contains("You MUST NOT use more than 1 subagent for build/tests"));
264
265 assert!(instructions.contains("### 2. VERIFY"));
267 assert!(instructions.contains("You MUST run tests and verify implementation"));
268 assert!(instructions.contains("You MUST NOT close tasks unless"));
269
270 assert!(instructions.contains("### 3. REPORT"));
272 assert!(
273 instructions
274 .contains("You MUST publish a result event with evidence using `ralph emit`")
275 );
276
277 assert!(instructions.contains("### GUARDRAILS"));
279 assert!(instructions.contains("999."));
280
281 assert!(instructions.contains("You MUST handle these events"));
283 assert!(instructions.contains("PR #123 ready for review"));
284 }
285
286 #[test]
287 fn test_custom_guardrails_injected() {
288 let custom_core = CoreConfig {
289 scratchpad: ScratchpadConfig {
290 enabled: true,
291 path: ".workspace/plan.md".to_string(),
292 },
293 specs_dir: "./specifications/".to_string(),
294 guardrails: vec!["Custom rule one".to_string(), "Custom rule two".to_string()],
295 workspace_root: std::path::PathBuf::from("."),
296 };
297 let builder = InstructionBuilder::new(custom_core);
298
299 let hat = Hat::new("worker", "Worker").with_instructions("Do the work.");
300 let instructions = builder.build_custom_hat(&hat, "context");
301
302 assert!(instructions.contains("999. Custom rule one"));
304 assert!(instructions.contains("1000. Custom rule two"));
305 }
306
307 #[test]
308 fn test_must_publish_injected_for_explicit_instructions() {
309 use ralph_proto::Topic;
310
311 let builder = default_builder();
312 let hat = Hat::new("reviewer", "Code Reviewer")
313 .with_instructions("Review PRs for quality and correctness.")
314 .with_publishes(vec![
315 Topic::new("review.approved"),
316 Topic::new("review.changes_requested"),
317 ]);
318
319 let instructions = builder.build_custom_hat(&hat, "PR #123 ready");
320
321 assert!(
323 instructions.contains("You MUST emit exactly ONE of these events via `ralph emit"),
324 "Must-publish rule should be injected for custom hats with publishes"
325 );
326 assert!(instructions.contains("`review.approved`"));
327 assert!(instructions.contains("`review.changes_requested`"));
328 assert!(
329 instructions.contains("Plain-language summaries do NOT count as event publication")
330 );
331 assert!(instructions.contains("You MUST stop immediately after emitting"));
332 assert!(instructions.contains("You MUST NOT end the iteration without publishing"));
333 }
334
335 #[test]
336 fn test_must_publish_not_injected_when_no_publishes() {
337 let builder = default_builder();
338 let hat = Hat::new("observer", "Silent Observer")
339 .with_instructions("Observe and log, but do not emit events.");
340
341 let instructions = builder.build_custom_hat(&hat, "Observe this");
342
343 assert!(
347 !instructions.contains("You MUST emit exactly ONE of these events via `ralph emit"),
348 "Specific must-publish list should NOT be injected when hat has no publishes"
349 );
350 }
351
352 #[test]
353 fn test_derived_behaviors_when_no_explicit_instructions() {
354 use ralph_proto::Topic;
355
356 let builder = default_builder();
357 let hat = Hat::new("builder", "Builder")
358 .subscribe("build.task")
359 .with_publishes(vec![Topic::new("build.done"), Topic::new("build.blocked")]);
360
361 let instructions = builder.build_custom_hat(&hat, "Implement feature X");
362
363 assert!(instructions.contains("Derived Behaviors"));
365 assert!(instructions.contains("build.task"));
366 }
367}