Skip to main content

ralph_core/
instructions.rs

1//! Instruction builder for Ralph agent prompts.
2//!
3//! Builds ghuntley-style prompts with numbered phases:
4//! - 0a, 0b: Orientation (study specs, study context)
5//! - 1, 2, 3: Workflow phases
6//! - 999+: Guardrails (higher = more important)
7
8use crate::config::{CoreConfig, EventMetadata};
9use ralph_proto::Hat;
10use std::collections::HashMap;
11
12/// Builds instructions for custom hats.
13///
14/// Uses ghuntley methodology: numbered phases, specific verbs ("study"),
15/// subagent limits (parallel for reads, single for builds).
16#[derive(Debug)]
17pub struct InstructionBuilder {
18    core: CoreConfig,
19    /// Event metadata for deriving instructions from pub/sub contracts.
20    events: HashMap<String, EventMetadata>,
21}
22
23impl InstructionBuilder {
24    /// Creates a new instruction builder with core configuration.
25    pub fn new(core: CoreConfig) -> Self {
26        Self {
27            core,
28            events: HashMap::new(),
29        }
30    }
31
32    /// Creates a new instruction builder with event metadata for custom hats.
33    pub fn with_events(core: CoreConfig, events: HashMap<String, EventMetadata>) -> Self {
34        Self { core, events }
35    }
36
37    /// Derives instructions from a hat's pub/sub contract and event metadata.
38    ///
39    /// For each event the hat triggers on or publishes:
40    /// 1. Check event metadata for on_trigger/on_publish instructions
41    /// 2. Fall back to built-in defaults for well-known events
42    ///
43    /// This allows users to define custom events with custom behaviors,
44    /// while still getting sensible defaults for standard events.
45    fn derive_instructions_from_contract(&self, hat: &Hat) -> String {
46        let mut behaviors: Vec<String> = Vec::new();
47
48        // Derive behaviors from triggers (what this hat responds to)
49        for trigger in &hat.subscriptions {
50            let trigger_str = trigger.as_str();
51
52            // First, check event metadata
53            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            // Fall back to built-in defaults for well-known events
61            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        // Derive behaviors from publishes (what this hat outputs)
86        for publish in &hat.publishes {
87            let publish_str = publish.as_str();
88
89            // First, check event metadata
90            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            // Fall back to built-in defaults for well-known events
101            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        // Add must-publish rule if hat has publishable events
117        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    /// Builds custom hat instructions for extended multi-agent configurations.
133    ///
134    /// Use this for hats beyond the default Ralph.
135    /// When instructions are empty, derives them from the pub/sub contract.
136    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        // Custom role with RFC2119 style identity
224        assert!(instructions.contains("Code Reviewer"));
225        assert!(instructions.contains("You have fresh context each iteration"));
226
227        // Numbered orientation phase with RFC2119
228        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        // Numbered execute phase with RFC2119
233        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        // Verify phase with RFC2119 (task completion verification)
238        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        // Report phase with RFC2119
243        assert!(instructions.contains("### 3. REPORT"));
244        assert!(instructions.contains("You MUST publish a result event"));
245
246        // Guardrails section with high numbers
247        assert!(instructions.contains("### GUARDRAILS"));
248        assert!(instructions.contains("999."));
249
250        // Event context is included with RFC2119 directive
251        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        // Custom guardrails are injected with 999+ numbering
269        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        // Must-publish rule should be injected even with explicit instructions (RFC2119)
288        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        // No must-publish rule when hat has no publishes
306        // Note: The prompt says "You MUST publish a result event" in the REPORT section,
307        // but the specific "You MUST publish one of these events:" list should not appear
308        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        // Should derive behaviors from pub/sub contract
326        assert!(instructions.contains("Derived Behaviors"));
327        assert!(instructions.contains("build.task"));
328    }
329}