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