1use crate::config::CoreConfig;
6use crate::hat_registry::HatRegistry;
7use ralph_proto::Topic;
8use std::path::Path;
9
10pub struct HatlessRalph {
12 completion_promise: String,
13 core: CoreConfig,
14 hat_topology: Option<HatTopology>,
15 starting_event: Option<String>,
17}
18
19pub struct HatTopology {
21 hats: Vec<HatInfo>,
22}
23
24pub struct HatInfo {
26 pub name: String,
27 pub description: String,
28 pub subscribes_to: Vec<String>,
29 pub publishes: Vec<String>,
30 pub instructions: String,
31}
32
33impl HatTopology {
34 pub fn from_registry(registry: &HatRegistry) -> Self {
36 let hats = registry
37 .all()
38 .map(|hat| HatInfo {
39 name: hat.name.clone(),
40 description: hat.description.clone(),
41 subscribes_to: hat.subscriptions.iter().map(|t| t.as_str().to_string()).collect(),
42 publishes: hat.publishes.iter().map(|t| t.as_str().to_string()).collect(),
43 instructions: hat.instructions.clone(),
44 })
45 .collect();
46
47 Self { hats }
48 }
49}
50
51impl HatlessRalph {
52 pub fn new(
60 completion_promise: impl Into<String>,
61 core: CoreConfig,
62 registry: &HatRegistry,
63 starting_event: Option<String>,
64 ) -> Self {
65 let hat_topology = if registry.is_empty() {
66 None
67 } else {
68 Some(HatTopology::from_registry(registry))
69 };
70
71 Self {
72 completion_promise: completion_promise.into(),
73 core,
74 hat_topology,
75 starting_event,
76 }
77 }
78
79 pub fn build_prompt(
87 &self,
88 context: &str,
89 active_hats: &[&ralph_proto::Hat],
90 ) -> String {
91 let mut prompt = self.core_prompt();
92
93 if !context.trim().is_empty() {
95 prompt.push_str("## PENDING EVENTS\n\n");
96 prompt.push_str(context);
97 prompt.push_str("\n\n");
98 }
99
100 let has_custom_workflow = active_hats
103 .iter()
104 .any(|h| !h.instructions.trim().is_empty());
105
106 if !has_custom_workflow {
107 prompt.push_str(&self.workflow_section());
108 }
109
110 if let Some(topology) = &self.hat_topology {
111 prompt.push_str(&self.hats_section(topology, active_hats));
112 }
113
114 prompt.push_str(&self.event_writing_section());
115 prompt.push_str(&self.done_section());
116
117 prompt
118 }
119
120 pub fn should_handle(&self, _topic: &Topic) -> bool {
122 true
123 }
124
125 fn is_fresh_start(&self) -> bool {
130 if self.starting_event.is_none() {
132 return false;
133 }
134
135 let path = Path::new(&self.core.scratchpad);
137 !path.exists()
138 }
139
140 fn core_prompt(&self) -> String {
141 let guardrails = self
142 .core
143 .guardrails
144 .iter()
145 .enumerate()
146 .map(|(i, g)| format!("{}. {g}", 999 + i))
147 .collect::<Vec<_>>()
148 .join("\n");
149
150 format!(
151 r"I'm Ralph. Fresh context each iteration.
152
153### 0a. ORIENTATION
154Study `{specs_dir}` to understand requirements.
155Don't assume features aren't implemented—search first.
156
157### 0b. SCRATCHPAD
158Study `{scratchpad}`. It's shared state. It's memory.
159
160Task markers:
161- `[ ]` pending
162- `[x]` done
163- `[~]` cancelled (with reason)
164
165### GUARDRAILS
166{guardrails}
167
168",
169 scratchpad = self.core.scratchpad,
170 specs_dir = self.core.specs_dir,
171 guardrails = guardrails,
172 )
173 }
174
175 fn workflow_section(&self) -> String {
176 if self.hat_topology.is_some() {
178 if self.is_fresh_start() {
180 return format!(
182 r"## WORKFLOW
183
184**FAST PATH**: Publish `{}` immediately to start the hat workflow.
185Do not plan or analyze — delegate now.
186
187",
188 self.starting_event.as_ref().unwrap()
189 );
190 }
191
192 format!(
194 r"## WORKFLOW
195
196### 1. PLAN
197Update `{scratchpad}` with prioritized tasks.
198
199### 2. DELEGATE
200You have one job. Publish ONE event to hand off to specialized hats. Do
201NOT do any work.
202
203",
204 scratchpad = self.core.scratchpad
205 )
206 } else {
207 format!(
209 r"## WORKFLOW
210
211### 1. Study the prompt.
212Study, explore, and research what needs to be done. Use parallel subagents (up to 10) for searches.
213
214### 2. PLAN
215Update `{scratchpad}` with prioritized tasks.
216
217### 3. IMPLEMENT
218Pick ONE task. Only 1 subagent for build/tests.
219
220### 4. COMMIT
221Capture the why, not just the what. Mark `[x]` in scratchpad.
222
223### 5. REPEAT
224Until all tasks `[x]` or `[~]`.
225
226",
227 scratchpad = self.core.scratchpad
228 )
229 }
230 }
231
232 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
233 let mut section = String::from("## HATS\n\nDelegate via events.\n\n");
234
235 if let Some(ref starting_event) = self.starting_event {
237 section.push_str(&format!(
238 "**After coordination, publish `{}` to start the workflow.**\n\n",
239 starting_event
240 ));
241 }
242
243 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
247 let mut ralph_publishes: Vec<&str> = Vec::new();
248
249 for hat in &topology.hats {
250 for pub_event in &hat.publishes {
251 if !ralph_triggers.contains(&pub_event.as_str()) {
252 ralph_triggers.push(pub_event.as_str());
253 }
254 }
255 for sub_event in &hat.subscribes_to {
256 if !ralph_publishes.contains(&sub_event.as_str()) {
257 ralph_publishes.push(sub_event.as_str());
258 }
259 }
260 }
261
262 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
264 section.push_str("|-----|-------------|----------|-------------|\n");
265
266 section.push_str(&format!(
268 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
269 ralph_triggers.join(", "),
270 ralph_publishes.join(", ")
271 ));
272
273 for hat in &topology.hats {
275 let subscribes = hat.subscribes_to.join(", ");
276 let publishes = hat.publishes.join(", ");
277 section.push_str(&format!(
278 "| {} | {} | {} | {} |\n",
279 hat.name, subscribes, publishes, hat.description
280 ));
281 }
282
283 section.push('\n');
284
285 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
287 section.push('\n');
288
289 self.validate_topology_reachability(topology);
291
292 for active_hat in active_hats {
295 if !active_hat.instructions.trim().is_empty() {
296 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
297 section.push_str(&active_hat.instructions);
298 if !active_hat.instructions.ends_with('\n') {
299 section.push('\n');
300 }
301 section.push('\n');
302 }
303 }
304
305 section
306 }
307
308 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
310 let mut diagram = String::from("```mermaid\nflowchart LR\n");
311
312 diagram.push_str(" task.start((task.start)) --> Ralph\n");
314
315 for hat in &topology.hats {
317 for trigger in &hat.subscribes_to {
318 if ralph_publishes.contains(&trigger.as_str()) {
319 let node_id = hat.name.chars().filter(|c| c.is_alphanumeric()).collect::<String>();
321 if node_id == hat.name {
322 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
323 } else {
324 diagram.push_str(&format!(
326 " Ralph -->|{}| {}[{}]\n",
327 trigger, node_id, hat.name
328 ));
329 }
330 }
331 }
332 }
333
334 for hat in &topology.hats {
336 let node_id = hat.name.chars().filter(|c| c.is_alphanumeric()).collect::<String>();
337 for pub_event in &hat.publishes {
338 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
339 }
340 }
341
342 for source_hat in &topology.hats {
344 let source_id = source_hat.name.chars().filter(|c| c.is_alphanumeric()).collect::<String>();
345 for pub_event in &source_hat.publishes {
346 for target_hat in &topology.hats {
347 if target_hat.name != source_hat.name && target_hat.subscribes_to.contains(pub_event) {
348 let target_id = target_hat.name.chars().filter(|c| c.is_alphanumeric()).collect::<String>();
349 diagram.push_str(&format!(
350 " {} -->|{}| {}\n",
351 source_id, pub_event, target_id
352 ));
353 }
354 }
355 }
356 }
357
358 diagram.push_str("```\n");
359 diagram
360 }
361
362 fn validate_topology_reachability(&self, topology: &HatTopology) {
365 use std::collections::HashSet;
366 use tracing::warn;
367
368 let mut reachable_events: HashSet<&str> = HashSet::new();
370 reachable_events.insert("task.start");
371
372 for hat in &topology.hats {
374 for trigger in &hat.subscribes_to {
375 reachable_events.insert(trigger.as_str());
376 }
377 }
378
379 for hat in &topology.hats {
381 for pub_event in &hat.publishes {
382 reachable_events.insert(pub_event.as_str());
383 }
384 }
385
386 for hat in &topology.hats {
388 let hat_reachable = hat.subscribes_to.iter().any(|t| reachable_events.contains(t.as_str()));
389 if !hat_reachable {
390 warn!(
391 hat = %hat.name,
392 triggers = ?hat.subscribes_to,
393 "Hat has triggers that are never published - it may be unreachable"
394 );
395 }
396 }
397 }
398
399 fn event_writing_section(&self) -> String {
400 format!(
401 r#"## EVENT WRITING
402
403Events are **routing signals**, not data transport. Keep payloads brief.
404
405**Use `ralph emit` to write events** (handles JSON escaping correctly):
406```bash
407ralph emit "build.done" "tests: pass, lint: pass"
408ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
409```
410
411⚠️ **NEVER use echo/cat to write events** — shell escaping breaks JSON.
412
413For detailed output, write to `{scratchpad}` and emit a brief event.
414
415**CRITICAL: STOP after publishing the event.** A new iteration will start
416with fresh context to handle the work. Do NOT continue working in this
417iteration — let the next iteration handle the event with the appropriate
418hat persona. By doing the work now, you won't be wearing the correct hat
419the specialty to do an even better job.
420"#,
421 scratchpad = self.core.scratchpad
422 )
423 }
424
425 fn done_section(&self) -> String {
426 format!(
427 r"## DONE
428
429Output {} when all tasks complete.
430",
431 self.completion_promise
432 )
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::config::RalphConfig;
440
441 #[test]
442 fn test_prompt_without_hats() {
443 let config = RalphConfig::default();
444 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
446
447 let prompt = ralph.build_prompt("", &[]);
448
449 assert!(prompt.contains("I'm Ralph. Fresh context each iteration."));
451
452 assert!(prompt.contains("### 0a. ORIENTATION"));
454 assert!(prompt.contains("Study"));
455 assert!(prompt.contains("Don't assume features aren't implemented"));
456
457 assert!(prompt.contains("### 0b. SCRATCHPAD"));
459 assert!(prompt.contains("Task markers:"));
460 assert!(prompt.contains("- `[ ]` pending"));
461 assert!(prompt.contains("- `[x]` done"));
462 assert!(prompt.contains("- `[~]` cancelled"));
463
464 assert!(prompt.contains("## WORKFLOW"));
466 assert!(prompt.contains("### 1. Study the prompt"));
467 assert!(prompt.contains("Use parallel subagents (up to 10)"));
468 assert!(prompt.contains("### 2. PLAN"));
469 assert!(prompt.contains("### 3. IMPLEMENT"));
470 assert!(prompt.contains("Only 1 subagent for build/tests"));
471 assert!(prompt.contains("### 4. COMMIT"));
472 assert!(prompt.contains("Capture the why"));
473 assert!(prompt.contains("### 5. REPEAT"));
474
475 assert!(!prompt.contains("## HATS"));
477
478 assert!(prompt.contains("## EVENT WRITING"));
480 assert!(prompt.contains("ralph emit"));
481 assert!(prompt.contains("NEVER use echo/cat"));
482 assert!(prompt.contains("LOOP_COMPLETE"));
483 }
484
485 #[test]
486 fn test_prompt_with_hats() {
487 let yaml = r#"
489hats:
490 planner:
491 name: "Planner"
492 triggers: ["planning.start", "build.done", "build.blocked"]
493 publishes: ["build.task"]
494 builder:
495 name: "Builder"
496 triggers: ["build.task"]
497 publishes: ["build.done", "build.blocked"]
498"#;
499 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
500 let registry = HatRegistry::from_config(&config);
501 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
503
504 let prompt = ralph.build_prompt("", &[]);
505
506 assert!(prompt.contains("I'm Ralph. Fresh context each iteration."));
508
509 assert!(prompt.contains("### 0a. ORIENTATION"));
511 assert!(prompt.contains("### 0b. SCRATCHPAD"));
512
513 assert!(prompt.contains("## WORKFLOW"));
515 assert!(prompt.contains("### 1. PLAN"));
516 assert!(prompt.contains("### 2. DELEGATE"), "Multi-hat mode should have DELEGATE step");
517 assert!(
518 !prompt.contains("### 3. IMPLEMENT"),
519 "Multi-hat mode should NOT tell Ralph to implement"
520 );
521 assert!(
522 prompt.contains("CRITICAL: STOP after publishing"),
523 "Should explicitly tell Ralph to stop after publishing event"
524 );
525
526 assert!(prompt.contains("## HATS"));
528 assert!(prompt.contains("Delegate via events"));
529 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
530
531 assert!(prompt.contains("## EVENT WRITING"));
533 assert!(prompt.contains("LOOP_COMPLETE"));
534 }
535
536 #[test]
537 fn test_should_handle_always_true() {
538 let config = RalphConfig::default();
539 let registry = HatRegistry::new();
540 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
541
542 assert!(ralph.should_handle(&Topic::new("any.topic")));
543 assert!(ralph.should_handle(&Topic::new("build.task")));
544 assert!(ralph.should_handle(&Topic::new("unknown.event")));
545 }
546
547 #[test]
548 fn test_ghuntley_patterns_present() {
549 let config = RalphConfig::default();
550 let registry = HatRegistry::new();
551 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
552
553 let prompt = ralph.build_prompt("", &[]);
554
555 assert!(prompt.contains("Study"), "Should use 'study' verb");
557 assert!(
558 prompt.contains("Don't assume features aren't implemented"),
559 "Should have 'don't assume' guardrail"
560 );
561 assert!(
562 prompt.contains("parallel subagents"),
563 "Should mention parallel subagents for reads"
564 );
565 assert!(
566 prompt.contains("Only 1 subagent"),
567 "Should limit to 1 subagent for builds"
568 );
569 assert!(
570 prompt.contains("Capture the why"),
571 "Should emphasize 'why' in commits"
572 );
573
574 assert!(prompt.contains("### GUARDRAILS"), "Should have guardrails section");
576 assert!(prompt.contains("999."), "Guardrails should use high numbers");
577 }
578
579 #[test]
580 fn test_scratchpad_format_documented() {
581 let config = RalphConfig::default();
582 let registry = HatRegistry::new();
583 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
584
585 let prompt = ralph.build_prompt("", &[]);
586
587 assert!(prompt.contains("- `[ ]` pending"));
589 assert!(prompt.contains("- `[x]` done"));
590 assert!(prompt.contains("- `[~]` cancelled (with reason)"));
591 }
592
593 #[test]
594 fn test_starting_event_in_prompt() {
595 let yaml = r#"
597hats:
598 tdd_writer:
599 name: "TDD Writer"
600 triggers: ["tdd.start"]
601 publishes: ["test.written"]
602"#;
603 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
604 let registry = HatRegistry::from_config(&config);
605 let ralph = HatlessRalph::new(
606 "LOOP_COMPLETE",
607 config.core.clone(),
608 ®istry,
609 Some("tdd.start".to_string()),
610 );
611
612 let prompt = ralph.build_prompt("", &[]);
613
614 assert!(
616 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
617 "Prompt should include starting_event delegation instruction"
618 );
619 }
620
621 #[test]
622 fn test_no_starting_event_instruction_when_none() {
623 let yaml = r#"
625hats:
626 some_hat:
627 name: "Some Hat"
628 triggers: ["some.event"]
629"#;
630 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
631 let registry = HatRegistry::from_config(&config);
632 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
633
634 let prompt = ralph.build_prompt("", &[]);
635
636 assert!(
638 !prompt.contains("After coordination, publish"),
639 "Prompt should NOT include starting_event delegation when None"
640 );
641 }
642
643 #[test]
644 fn test_hat_instructions_propagated_to_prompt() {
645 let yaml = r#"
648hats:
649 tdd_writer:
650 name: "TDD Writer"
651 triggers: ["tdd.start"]
652 publishes: ["test.written"]
653 instructions: |
654 You are a Test-Driven Development specialist.
655 Always write failing tests before implementation.
656 Focus on edge cases and error handling.
657"#;
658 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
659 let registry = HatRegistry::from_config(&config);
660 let ralph = HatlessRalph::new(
661 "LOOP_COMPLETE",
662 config.core.clone(),
663 ®istry,
664 Some("tdd.start".to_string()),
665 );
666
667 let tdd_writer = registry.get(&ralph_proto::HatId::new("tdd_writer")).unwrap();
669 let prompt = ralph.build_prompt("", &[tdd_writer]);
670
671 assert!(
673 prompt.contains("### TDD Writer Instructions"),
674 "Prompt should include hat instructions section header"
675 );
676 assert!(
677 prompt.contains("Test-Driven Development specialist"),
678 "Prompt should include actual instructions content"
679 );
680 assert!(
681 prompt.contains("Always write failing tests"),
682 "Prompt should include full instructions"
683 );
684 }
685
686 #[test]
687 fn test_empty_instructions_not_rendered() {
688 let yaml = r#"
690hats:
691 builder:
692 name: "Builder"
693 triggers: ["build.task"]
694 publishes: ["build.done"]
695"#;
696 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
697 let registry = HatRegistry::from_config(&config);
698 let ralph = HatlessRalph::new(
699 "LOOP_COMPLETE",
700 config.core.clone(),
701 ®istry,
702 None,
703 );
704
705 let prompt = ralph.build_prompt("", &[]);
706
707 assert!(
709 !prompt.contains("### Builder Instructions"),
710 "Prompt should NOT include instructions section for hat with empty instructions"
711 );
712 }
713
714 #[test]
715 fn test_multiple_hats_with_instructions() {
716 let yaml = r#"
718hats:
719 planner:
720 name: "Planner"
721 triggers: ["planning.start"]
722 publishes: ["build.task"]
723 instructions: "Plan carefully before implementation."
724 builder:
725 name: "Builder"
726 triggers: ["build.task"]
727 publishes: ["build.done"]
728 instructions: "Focus on clean, testable code."
729"#;
730 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
731 let registry = HatRegistry::from_config(&config);
732 let ralph = HatlessRalph::new(
733 "LOOP_COMPLETE",
734 config.core.clone(),
735 ®istry,
736 None,
737 );
738
739 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
741 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
742 let prompt = ralph.build_prompt("", &[planner, builder]);
743
744 assert!(
746 prompt.contains("### Planner Instructions"),
747 "Prompt should include Planner instructions section"
748 );
749 assert!(
750 prompt.contains("Plan carefully before implementation"),
751 "Prompt should include Planner instructions content"
752 );
753 assert!(
754 prompt.contains("### Builder Instructions"),
755 "Prompt should include Builder instructions section"
756 );
757 assert!(
758 prompt.contains("Focus on clean, testable code"),
759 "Prompt should include Builder instructions content"
760 );
761 }
762
763 #[test]
764 fn test_fast_path_with_starting_event() {
765 let yaml = r#"
768core:
769 scratchpad: "/nonexistent/path/scratchpad.md"
770hats:
771 tdd_writer:
772 name: "TDD Writer"
773 triggers: ["tdd.start"]
774 publishes: ["test.written"]
775"#;
776 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
777 let registry = HatRegistry::from_config(&config);
778 let ralph = HatlessRalph::new(
779 "LOOP_COMPLETE",
780 config.core.clone(),
781 ®istry,
782 Some("tdd.start".to_string()),
783 );
784
785 let prompt = ralph.build_prompt("", &[]);
786
787 assert!(
789 prompt.contains("FAST PATH"),
790 "Prompt should indicate fast path when starting_event set and no scratchpad"
791 );
792 assert!(
793 prompt.contains("Publish `tdd.start` immediately"),
794 "Prompt should instruct immediate event publishing"
795 );
796 assert!(
797 !prompt.contains("### 1. PLAN"),
798 "Fast path should skip PLAN step"
799 );
800 }
801
802 #[test]
803 fn test_events_context_included_in_prompt() {
804 let config = RalphConfig::default();
808 let registry = HatRegistry::new();
809 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
810
811 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
812[build.done] Build completed successfully";
813
814 let prompt = ralph.build_prompt(events_context, &[]);
815
816 assert!(
817 prompt.contains("## PENDING EVENTS"),
818 "Prompt should contain PENDING EVENTS section"
819 );
820 assert!(
821 prompt.contains("Review this code for security vulnerabilities"),
822 "Prompt should contain the user's task"
823 );
824 assert!(
825 prompt.contains("Build completed successfully"),
826 "Prompt should contain all events from context"
827 );
828 }
829
830 #[test]
831 fn test_empty_context_no_pending_events_section() {
832 let config = RalphConfig::default();
836 let registry = HatRegistry::new();
837 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
838
839 let prompt = ralph.build_prompt("", &[]);
840
841 assert!(
842 !prompt.contains("## PENDING EVENTS"),
843 "Empty context should not produce PENDING EVENTS section"
844 );
845 }
846
847 #[test]
848 fn test_whitespace_only_context_no_pending_events_section() {
849 let config = RalphConfig::default();
853 let registry = HatRegistry::new();
854 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
855
856 let prompt = ralph.build_prompt(" \n\t ", &[]);
857
858 assert!(
859 !prompt.contains("## PENDING EVENTS"),
860 "Whitespace-only context should not produce PENDING EVENTS section"
861 );
862 }
863
864 #[test]
865 fn test_events_section_before_workflow() {
866 let config = RalphConfig::default();
870 let registry = HatRegistry::new();
871 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
872
873 let events_context = "[task.start] Implement feature X";
874 let prompt = ralph.build_prompt(events_context, &[]);
875
876 let events_pos = prompt.find("## PENDING EVENTS").expect("Should have PENDING EVENTS");
877 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
878
879 assert!(
880 events_pos < workflow_pos,
881 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
882 events_pos,
883 workflow_pos
884 );
885 }
886
887 #[test]
890 fn test_only_active_hat_instructions_included() {
891 let yaml = r#"
893hats:
894 security_reviewer:
895 name: "Security Reviewer"
896 triggers: ["review.security"]
897 instructions: "Review code for security vulnerabilities."
898 architecture_reviewer:
899 name: "Architecture Reviewer"
900 triggers: ["review.architecture"]
901 instructions: "Review system design and architecture."
902 correctness_reviewer:
903 name: "Correctness Reviewer"
904 triggers: ["review.correctness"]
905 instructions: "Review logic and correctness."
906"#;
907 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
908 let registry = HatRegistry::from_config(&config);
909 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
910
911 let security_hat = registry.get(&ralph_proto::HatId::new("security_reviewer")).unwrap();
913 let active_hats = vec![security_hat];
914
915 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
916
917 assert!(
919 prompt.contains("### Security Reviewer Instructions"),
920 "Should include Security Reviewer instructions section"
921 );
922 assert!(
923 prompt.contains("Review code for security vulnerabilities"),
924 "Should include Security Reviewer instructions content"
925 );
926
927 assert!(
929 !prompt.contains("### Architecture Reviewer Instructions"),
930 "Should NOT include Architecture Reviewer instructions"
931 );
932 assert!(
933 !prompt.contains("Review system design and architecture"),
934 "Should NOT include Architecture Reviewer instructions content"
935 );
936 assert!(
937 !prompt.contains("### Correctness Reviewer Instructions"),
938 "Should NOT include Correctness Reviewer instructions"
939 );
940 }
941
942 #[test]
943 fn test_multiple_active_hats_all_included() {
944 let yaml = r#"
946hats:
947 security_reviewer:
948 name: "Security Reviewer"
949 triggers: ["review.security"]
950 instructions: "Review code for security vulnerabilities."
951 architecture_reviewer:
952 name: "Architecture Reviewer"
953 triggers: ["review.architecture"]
954 instructions: "Review system design and architecture."
955 correctness_reviewer:
956 name: "Correctness Reviewer"
957 triggers: ["review.correctness"]
958 instructions: "Review logic and correctness."
959"#;
960 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
961 let registry = HatRegistry::from_config(&config);
962 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
963
964 let security_hat = registry.get(&ralph_proto::HatId::new("security_reviewer")).unwrap();
966 let arch_hat = registry.get(&ralph_proto::HatId::new("architecture_reviewer")).unwrap();
967 let active_hats = vec![security_hat, arch_hat];
968
969 let prompt = ralph.build_prompt("Events", &active_hats);
970
971 assert!(
973 prompt.contains("### Security Reviewer Instructions"),
974 "Should include Security Reviewer instructions"
975 );
976 assert!(
977 prompt.contains("Review code for security vulnerabilities"),
978 "Should include Security Reviewer content"
979 );
980 assert!(
981 prompt.contains("### Architecture Reviewer Instructions"),
982 "Should include Architecture Reviewer instructions"
983 );
984 assert!(
985 prompt.contains("Review system design and architecture"),
986 "Should include Architecture Reviewer content"
987 );
988
989 assert!(
991 !prompt.contains("### Correctness Reviewer Instructions"),
992 "Should NOT include Correctness Reviewer instructions"
993 );
994 }
995
996 #[test]
997 fn test_no_active_hats_no_instructions() {
998 let yaml = r#"
1000hats:
1001 security_reviewer:
1002 name: "Security Reviewer"
1003 triggers: ["review.security"]
1004 instructions: "Review code for security vulnerabilities."
1005"#;
1006 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1007 let registry = HatRegistry::from_config(&config);
1008 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1009
1010 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1012
1013 let prompt = ralph.build_prompt("Events", &active_hats);
1014
1015 assert!(
1017 !prompt.contains("### Security Reviewer Instructions"),
1018 "Should NOT include instructions when no active hats"
1019 );
1020 assert!(
1021 !prompt.contains("Review code for security vulnerabilities"),
1022 "Should NOT include instructions content when no active hats"
1023 );
1024
1025 assert!(
1027 prompt.contains("## HATS"),
1028 "Should still have HATS section"
1029 );
1030 assert!(
1031 prompt.contains("| Hat | Triggers On | Publishes |"),
1032 "Should still have topology table"
1033 );
1034 }
1035
1036 #[test]
1037 fn test_topology_table_always_present() {
1038 let yaml = r#"
1040hats:
1041 security_reviewer:
1042 name: "Security Reviewer"
1043 triggers: ["review.security"]
1044 instructions: "Security instructions."
1045 architecture_reviewer:
1046 name: "Architecture Reviewer"
1047 triggers: ["review.architecture"]
1048 instructions: "Architecture instructions."
1049"#;
1050 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1051 let registry = HatRegistry::from_config(&config);
1052 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1053
1054 let security_hat = registry.get(&ralph_proto::HatId::new("security_reviewer")).unwrap();
1056 let active_hats = vec![security_hat];
1057
1058 let prompt = ralph.build_prompt("Events", &active_hats);
1059
1060 assert!(
1062 prompt.contains("| Security Reviewer |"),
1063 "Topology table should include Security Reviewer"
1064 );
1065 assert!(
1066 prompt.contains("| Architecture Reviewer |"),
1067 "Topology table should include Architecture Reviewer even though inactive"
1068 );
1069 assert!(
1070 prompt.contains("review.security"),
1071 "Topology table should show triggers"
1072 );
1073 assert!(
1074 prompt.contains("review.architecture"),
1075 "Topology table should show all triggers"
1076 );
1077 }
1078}