1use crate::config::CoreConfig;
6use crate::hat_registry::HatRegistry;
7use ralph_proto::Topic;
8use std::collections::HashMap;
9use std::path::Path;
10
11pub struct HatlessRalph {
13 completion_promise: String,
14 core: CoreConfig,
15 hat_topology: Option<HatTopology>,
16 starting_event: Option<String>,
18 memories_enabled: bool,
21 objective: Option<String>,
24 skill_index: String,
27 robot_guidance: Vec<String>,
30}
31
32pub struct HatTopology {
34 hats: Vec<HatInfo>,
35}
36
37#[derive(Debug, Clone)]
39pub struct EventReceiver {
40 pub name: String,
41 pub description: String,
42}
43
44pub struct HatInfo {
46 pub name: String,
47 pub description: String,
48 pub subscribes_to: Vec<String>,
49 pub publishes: Vec<String>,
50 pub instructions: String,
51 pub event_receivers: HashMap<String, Vec<EventReceiver>>,
53}
54
55impl HatInfo {
56 pub fn event_publishing_guide(&self) -> Option<String> {
60 if self.publishes.is_empty() {
61 return None;
62 }
63
64 let mut guide = String::from(
65 "### Event Publishing Guide\n\n\
66 You MUST publish exactly ONE event when your work is complete.\n\
67 Publishing hands off to the next hat and starts a fresh iteration with clear context.\n\n\
68 When you publish:\n",
69 );
70
71 for pub_event in &self.publishes {
72 let receivers = self.event_receivers.get(pub_event);
73 let receiver_text = match receivers {
74 Some(r) if !r.is_empty() => r
75 .iter()
76 .map(|recv| {
77 if recv.description.is_empty() {
78 recv.name.clone()
79 } else {
80 format!("{} ({})", recv.name, recv.description)
81 }
82 })
83 .collect::<Vec<_>>()
84 .join(", "),
85 _ => "Ralph (coordinates next steps)".to_string(),
86 };
87 guide.push_str(&format!(
88 "- `{}` → Received by: {}\n",
89 pub_event, receiver_text
90 ));
91 }
92
93 Some(guide)
94 }
95}
96
97impl HatTopology {
98 pub fn from_registry(registry: &HatRegistry) -> Self {
100 let hats = registry
101 .all()
102 .map(|hat| {
103 let event_receivers: HashMap<String, Vec<EventReceiver>> = hat
105 .publishes
106 .iter()
107 .map(|pub_topic| {
108 let receivers: Vec<EventReceiver> = registry
109 .subscribers(pub_topic)
110 .into_iter()
111 .filter(|h| h.id != hat.id) .map(|h| EventReceiver {
113 name: h.name.clone(),
114 description: h.description.clone(),
115 })
116 .collect();
117 (pub_topic.as_str().to_string(), receivers)
118 })
119 .collect();
120
121 HatInfo {
122 name: hat.name.clone(),
123 description: hat.description.clone(),
124 subscribes_to: hat
125 .subscriptions
126 .iter()
127 .map(|t| t.as_str().to_string())
128 .collect(),
129 publishes: hat
130 .publishes
131 .iter()
132 .map(|t| t.as_str().to_string())
133 .collect(),
134 instructions: hat.instructions.clone(),
135 event_receivers,
136 }
137 })
138 .collect();
139
140 Self { hats }
141 }
142}
143
144impl HatlessRalph {
145 pub fn new(
153 completion_promise: impl Into<String>,
154 core: CoreConfig,
155 registry: &HatRegistry,
156 starting_event: Option<String>,
157 ) -> Self {
158 let hat_topology = if registry.is_empty() {
159 None
160 } else {
161 Some(HatTopology::from_registry(registry))
162 };
163
164 Self {
165 completion_promise: completion_promise.into(),
166 core,
167 hat_topology,
168 starting_event,
169 memories_enabled: false, objective: None,
171 skill_index: String::new(),
172 robot_guidance: Vec::new(),
173 }
174 }
175
176 pub fn with_memories_enabled(mut self, enabled: bool) -> Self {
181 self.memories_enabled = enabled;
182 self
183 }
184
185 pub fn with_skill_index(mut self, index: String) -> Self {
190 self.skill_index = index;
191 self
192 }
193
194 pub fn set_objective(&mut self, objective: String) {
200 self.objective = Some(objective);
201 }
202
203 pub fn set_robot_guidance(&mut self, guidance: Vec<String>) {
209 self.robot_guidance = guidance;
210 }
211
212 pub fn clear_robot_guidance(&mut self) {
216 self.robot_guidance.clear();
217 }
218
219 fn collect_robot_guidance(&self) -> String {
224 if self.robot_guidance.is_empty() {
225 return String::new();
226 }
227
228 let mut section = String::from("## ROBOT GUIDANCE\n\n");
229
230 if self.robot_guidance.len() == 1 {
231 section.push_str(&self.robot_guidance[0]);
232 } else {
233 for (i, guidance) in self.robot_guidance.iter().enumerate() {
234 section.push_str(&format!("{}. {}\n", i + 1, guidance));
235 }
236 }
237
238 section.push_str("\n\n");
239
240 section
241 }
242
243 pub fn build_prompt(&self, context: &str, active_hats: &[&ralph_proto::Hat]) -> String {
251 let mut prompt = self.core_prompt();
252
253 if !self.skill_index.is_empty() {
255 prompt.push_str(&self.skill_index);
256 prompt.push('\n');
257 }
258
259 if let Some(ref obj) = self.objective {
261 prompt.push_str(&self.objective_section(obj));
262 }
263
264 let guidance = self.collect_robot_guidance();
266 if !guidance.is_empty() {
267 prompt.push_str(&guidance);
268 }
269
270 if !context.trim().is_empty() {
272 prompt.push_str("## PENDING EVENTS\n\n");
273 prompt.push_str("You MUST handle these events in this iteration:\n\n");
274 prompt.push_str(context);
275 prompt.push_str("\n\n");
276 }
277
278 let has_custom_workflow = active_hats
281 .iter()
282 .any(|h| !h.instructions.trim().is_empty());
283
284 if !has_custom_workflow {
285 prompt.push_str(&self.workflow_section());
286 }
287
288 if let Some(topology) = &self.hat_topology {
289 prompt.push_str(&self.hats_section(topology, active_hats));
290 }
291
292 prompt.push_str(&self.event_writing_section());
293
294 if active_hats.is_empty() {
297 prompt.push_str(&self.done_section(self.objective.as_deref()));
298 }
299
300 prompt
301 }
302
303 fn objective_section(&self, objective: &str) -> String {
305 format!(
306 r"## OBJECTIVE
307
308**This is your primary goal. All work must advance this objective.**
309
310> {objective}
311
312You MUST keep this objective in mind throughout the iteration.
313You MUST NOT get distracted by workflow mechanics — they serve this goal.
314
315",
316 objective = objective
317 )
318 }
319
320 pub fn should_handle(&self, _topic: &Topic) -> bool {
322 true
323 }
324
325 fn is_fresh_start(&self) -> bool {
330 if self.starting_event.is_none() {
332 return false;
333 }
334
335 let path = Path::new(&self.core.scratchpad);
337 !path.exists()
338 }
339
340 fn core_prompt(&self) -> String {
341 let guardrails = self
343 .core
344 .guardrails
345 .iter()
346 .enumerate()
347 .map(|(i, g)| {
348 let guardrail = if self.memories_enabled && g.contains("scratchpad is memory") {
350 g.replace(
351 "scratchpad is memory",
352 "save learnings to memories for next time",
353 )
354 } else {
355 g.clone()
356 };
357 format!("{}. {guardrail}", 999 + i)
358 })
359 .collect::<Vec<_>>()
360 .join("\n");
361
362 let mut prompt = if self.memories_enabled {
363 r"
364### 0a. ORIENTATION
365You are Ralph. You are running in a loop. You have fresh context each iteration.
366You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
367
368**First thing every iteration:**
3691. Review your `<scratchpad>` (auto-injected above) for context on your thinking
3702. Review your `<ready-tasks>` (auto-injected above) to see what work exists
3713. If tasks exist, pick one. If not, create them from your plan.
372"
373 } else {
374 r"
375### 0a. ORIENTATION
376You are Ralph. You are running in a loop. You have fresh context each iteration.
377You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
378"
379 }
380 .to_string();
381
382 prompt.push_str(&format!(
384 r"### 0b. SCRATCHPAD
385`{scratchpad}` is your thinking journal for THIS objective.
386Its content is auto-injected in `<scratchpad>` tags at the top of your context each iteration.
387
388**Always append** new entries to the end of the file (most recent = bottom).
389
390**Use for:**
391- Current understanding and reasoning
392- Analysis notes and decisions
393- Plan narrative (the 'why' behind your approach)
394
395**Do NOT use for:**
396- Tracking what tasks exist or their status (use `ralph tools task`)
397- Checklists or todo lists (use `ralph tools task add`)
398
399",
400 scratchpad = self.core.scratchpad,
401 ));
402
403 prompt.push_str(&format!(
409 "### STATE MANAGEMENT\n\n\
410**Tasks** (`ralph tools task`) — What needs to be done:\n\
411- Work items, their status, priorities, and dependencies\n\
412- Source of truth for progress across iterations\n\
413- Auto-injected in `<ready-tasks>` tags at the top of your context\n\
414\n\
415**Scratchpad** (`{scratchpad}`) — Your thinking:\n\
416- Current understanding and reasoning\n\
417- Analysis notes, decisions, plan narrative\n\
418- NOT for checklists or status tracking\n\
419\n\
420**Memories** (`.ralph/agent/memories.md`) — Persistent learning:\n\
421- Codebase patterns and conventions\n\
422- Architectural decisions and rationale\n\
423- Recurring problem solutions\n\
424\n\
425**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
426- Analysis and temporary notes\n\
427- Read when relevant\n\
428\n\
429**Rule:** Work items go in tasks. Thinking goes in scratchpad. Learnings go in memories.\n\
430\n",
431 scratchpad = self.core.scratchpad,
432 ));
433
434 if let Ok(entries) = std::fs::read_dir(".ralph/agent") {
436 let md_files: Vec<String> = entries
437 .filter_map(|e| e.ok())
438 .filter_map(|e| {
439 let path = e.path();
440 if path.extension().and_then(|s| s.to_str()) == Some("md")
441 && path.file_name().and_then(|s| s.to_str()) != Some("memories.md")
442 {
443 path.file_name()
444 .and_then(|s| s.to_str())
445 .map(|s| s.to_string())
446 } else {
447 None
448 }
449 })
450 .collect();
451
452 if !md_files.is_empty() {
453 prompt.push_str("### AVAILABLE CONTEXT FILES\n\n");
454 prompt.push_str(
455 "Context files in `.ralph/agent/` (read if relevant to current work):\n",
456 );
457 for file in md_files {
458 prompt.push_str(&format!("- `.ralph/agent/{}`\n", file));
459 }
460 prompt.push('\n');
461 }
462 }
463
464 prompt.push_str(&format!(
465 r"### GUARDRAILS
466{guardrails}
467
468",
469 guardrails = guardrails,
470 ));
471
472 prompt
473 }
474
475 fn workflow_section(&self) -> String {
476 if self.hat_topology.is_some() {
478 if self.is_fresh_start() {
480 return format!(
482 r"## WORKFLOW
483
484**FAST PATH**: You MUST publish `{}` immediately to start the hat workflow.
485You MUST NOT plan or analyze — delegate now.
486
487",
488 self.starting_event.as_ref().unwrap()
489 );
490 }
491
492 if self.memories_enabled {
494 format!(
496 r"## WORKFLOW
497
498### 1. PLAN
499You MUST update `{scratchpad}` with your understanding and plan.
500You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).
501
502### 2. DELEGATE
503You MUST publish exactly ONE event to hand off to specialized hats.
504You MUST NOT do implementation work — delegation is your only job.
505
506",
507 scratchpad = self.core.scratchpad
508 )
509 } else {
510 format!(
512 r"## WORKFLOW
513
514### 1. PLAN
515You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
516
517### 2. DELEGATE
518You MUST publish exactly ONE event to hand off to specialized hats.
519You MUST NOT do implementation work — delegation is your only job.
520
521",
522 scratchpad = self.core.scratchpad
523 )
524 }
525 } else {
526 if self.memories_enabled {
528 format!(
530 r"## WORKFLOW
531
532### 1. Study the prompt.
533You MUST study, explore, and research what needs to be done.
534
535### 2. PLAN
536You MUST update `{scratchpad}` with your understanding and plan.
537You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).
538
539### 3. IMPLEMENT
540You MUST pick exactly ONE task from `<ready-tasks>` to implement.
541
542### 4. VERIFY & COMMIT
543You MUST run tests and verify the implementation works.
544You MUST commit after verification passes - one commit per task.
545You SHOULD run `git diff --cached` to review staged changes before committing.
546You MUST close the task with `ralph tools task close <id>` AFTER commit.
547You SHOULD save learnings to memories with `ralph tools memory add`.
548You MUST update scratchpad with what you learned (tasks track what remains).
549
550### 5. EXIT
551You MUST exit after completing ONE task.
552
553",
554 scratchpad = self.core.scratchpad
555 )
556 } else {
557 format!(
559 r"## WORKFLOW
560
561### 1. Study the prompt.
562You MUST study, explore, and research what needs to be done.
563You MAY use parallel subagents (up to 10) for searches.
564
565### 2. PLAN
566You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
567
568### 3. IMPLEMENT
569You MUST pick exactly ONE task to implement.
570You MUST NOT use more than 1 subagent for build/tests.
571
572### 4. COMMIT
573You MUST commit after completing each atomic unit of work.
574You MUST capture the why, not just the what.
575You SHOULD run `git diff` before committing to review changes.
576You MUST mark the task `[x]` in scratchpad when complete.
577
578### 5. REPEAT
579You MUST continue until all tasks are `[x]` or `[~]`.
580
581",
582 scratchpad = self.core.scratchpad
583 )
584 }
585 }
586 }
587
588 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
589 let mut section = String::new();
590
591 if active_hats.is_empty() {
594 section.push_str("## HATS\n\nDelegate via events.\n\n");
596
597 if let Some(ref starting_event) = self.starting_event {
599 section.push_str(&format!(
600 "**After coordination, publish `{}` to start the workflow.**\n\n",
601 starting_event
602 ));
603 }
604
605 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
609 let mut ralph_publishes: Vec<&str> = Vec::new();
610
611 for hat in &topology.hats {
612 for pub_event in &hat.publishes {
613 if !ralph_triggers.contains(&pub_event.as_str()) {
614 ralph_triggers.push(pub_event.as_str());
615 }
616 }
617 for sub_event in &hat.subscribes_to {
618 if !ralph_publishes.contains(&sub_event.as_str()) {
619 ralph_publishes.push(sub_event.as_str());
620 }
621 }
622 }
623
624 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
626 section.push_str("|-----|-------------|----------|-------------|\n");
627
628 section.push_str(&format!(
630 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
631 ralph_triggers.join(", "),
632 ralph_publishes.join(", ")
633 ));
634
635 for hat in &topology.hats {
637 let subscribes = hat.subscribes_to.join(", ");
638 let publishes = hat.publishes.join(", ");
639 section.push_str(&format!(
640 "| {} | {} | {} | {} |\n",
641 hat.name, subscribes, publishes, hat.description
642 ));
643 }
644
645 section.push('\n');
646
647 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
649 section.push('\n');
650
651 if !ralph_publishes.is_empty() {
653 section.push_str(&format!(
654 "**CONSTRAINT:** You MUST only publish events from this list: `{}`\n\
655 Publishing other events will have no effect - no hat will receive them.\n\n",
656 ralph_publishes.join("`, `")
657 ));
658 }
659
660 self.validate_topology_reachability(topology);
662 } else {
663 section.push_str("## ACTIVE HAT\n\n");
665
666 for active_hat in active_hats {
667 let hat_info = topology.hats.iter().find(|h| h.name == active_hat.name);
669
670 if !active_hat.instructions.trim().is_empty() {
671 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
672 section.push_str(&active_hat.instructions);
673 if !active_hat.instructions.ends_with('\n') {
674 section.push('\n');
675 }
676 section.push('\n');
677 }
678
679 if let Some(guide) = hat_info.and_then(|info| info.event_publishing_guide()) {
681 section.push_str(&guide);
682 section.push('\n');
683 }
684 }
685 }
686
687 section
688 }
689
690 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
692 let mut diagram = String::from("```mermaid\nflowchart LR\n");
693
694 diagram.push_str(" task.start((task.start)) --> Ralph\n");
696
697 for hat in &topology.hats {
699 for trigger in &hat.subscribes_to {
700 if ralph_publishes.contains(&trigger.as_str()) {
701 let node_id = hat
703 .name
704 .chars()
705 .filter(|c| c.is_alphanumeric())
706 .collect::<String>();
707 if node_id == hat.name {
708 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
709 } else {
710 diagram.push_str(&format!(
712 " Ralph -->|{}| {}[{}]\n",
713 trigger, node_id, hat.name
714 ));
715 }
716 }
717 }
718 }
719
720 for hat in &topology.hats {
722 let node_id = hat
723 .name
724 .chars()
725 .filter(|c| c.is_alphanumeric())
726 .collect::<String>();
727 for pub_event in &hat.publishes {
728 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
729 }
730 }
731
732 for source_hat in &topology.hats {
734 let source_id = source_hat
735 .name
736 .chars()
737 .filter(|c| c.is_alphanumeric())
738 .collect::<String>();
739 for pub_event in &source_hat.publishes {
740 for target_hat in &topology.hats {
741 if target_hat.name != source_hat.name
742 && target_hat.subscribes_to.contains(pub_event)
743 {
744 let target_id = target_hat
745 .name
746 .chars()
747 .filter(|c| c.is_alphanumeric())
748 .collect::<String>();
749 diagram.push_str(&format!(
750 " {} -->|{}| {}\n",
751 source_id, pub_event, target_id
752 ));
753 }
754 }
755 }
756 }
757
758 diagram.push_str("```\n");
759 diagram
760 }
761
762 fn validate_topology_reachability(&self, topology: &HatTopology) {
765 use std::collections::HashSet;
766 use tracing::warn;
767
768 let mut reachable_events: HashSet<&str> = HashSet::new();
770 reachable_events.insert("task.start");
771
772 for hat in &topology.hats {
774 for trigger in &hat.subscribes_to {
775 reachable_events.insert(trigger.as_str());
776 }
777 }
778
779 for hat in &topology.hats {
781 for pub_event in &hat.publishes {
782 reachable_events.insert(pub_event.as_str());
783 }
784 }
785
786 for hat in &topology.hats {
788 let hat_reachable = hat
789 .subscribes_to
790 .iter()
791 .any(|t| reachable_events.contains(t.as_str()));
792 if !hat_reachable {
793 warn!(
794 hat = %hat.name,
795 triggers = ?hat.subscribes_to,
796 "Hat has triggers that are never published - it may be unreachable"
797 );
798 }
799 }
800 }
801
802 fn event_writing_section(&self) -> String {
803 let detailed_output_hint = format!(
805 "You SHOULD write detailed output to `{}` and emit only a brief event.",
806 self.core.scratchpad
807 );
808
809 format!(
810 r#"## EVENT WRITING
811
812Events are routing signals, not data transport. You SHOULD keep payloads brief.
813
814You MUST use `ralph emit` to write events (handles JSON escaping correctly):
815```bash
816ralph emit "build.done" "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass"
817ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
818```
819
820You MUST NOT use echo/cat to write events because shell escaping breaks JSON.
821
822{detailed_output_hint}
823
824**Constraints:**
825- You MUST stop working after publishing an event because a new iteration will start with fresh context
826- You MUST NOT continue with additional work after publishing because the next iteration handles it with the appropriate hat persona
827"#,
828 detailed_output_hint = detailed_output_hint
829 )
830 }
831
832 fn done_section(&self, objective: Option<&str>) -> String {
833 let mut section = format!(
834 r"## DONE
835
836You MUST emit a completion event `{}` when the objective is complete and all tasks are done.
837You MUST use `ralph emit` (stdout text does NOT end the loop).
838",
839 self.completion_promise
840 );
841
842 if self.memories_enabled {
844 section.push_str(
845 r"
846**Before declaring completion:**
8471. Run `ralph tools task ready` to check for open tasks
8482. If any tasks are open, complete them first
8493. Only emit the completion event when YOUR tasks are all closed
850
851Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
852
853You MUST NOT emit the completion event while tasks remain open.
854",
855 );
856 }
857
858 if let Some(obj) = objective {
860 section.push_str(&format!(
861 r"
862**Remember your objective:**
863> {}
864
865You MUST NOT declare completion until this objective is fully satisfied.
866",
867 obj
868 ));
869 }
870
871 section
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use super::*;
878 use crate::config::RalphConfig;
879
880 #[test]
881 fn test_prompt_without_hats() {
882 let config = RalphConfig::default();
883 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
885
886 let prompt = ralph.build_prompt("", &[]);
887
888 assert!(prompt.contains(
890 "You are Ralph. You are running in a loop. You have fresh context each iteration."
891 ));
892
893 assert!(prompt.contains("### 0a. ORIENTATION"));
895 assert!(prompt.contains("MUST complete only one atomic task"));
896
897 assert!(prompt.contains("### 0b. SCRATCHPAD"));
899 assert!(prompt.contains("auto-injected"));
900 assert!(prompt.contains("**Always append**"));
901
902 assert!(prompt.contains("## WORKFLOW"));
904 assert!(prompt.contains("### 1. Study the prompt"));
905 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
906 assert!(prompt.contains("### 2. PLAN"));
907 assert!(prompt.contains("### 3. IMPLEMENT"));
908 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
909 assert!(prompt.contains("### 4. COMMIT"));
910 assert!(prompt.contains("You MUST capture the why"));
911 assert!(prompt.contains("### 5. REPEAT"));
912
913 assert!(!prompt.contains("## HATS"));
915
916 assert!(prompt.contains("## EVENT WRITING"));
918 assert!(prompt.contains("You MUST use `ralph emit`"));
919 assert!(prompt.contains("You MUST NOT use echo/cat"));
920 assert!(prompt.contains("LOOP_COMPLETE"));
921 }
922
923 #[test]
924 fn test_prompt_with_hats() {
925 let yaml = r#"
927hats:
928 planner:
929 name: "Planner"
930 triggers: ["planning.start", "build.done", "build.blocked"]
931 publishes: ["build.task"]
932 builder:
933 name: "Builder"
934 triggers: ["build.task"]
935 publishes: ["build.done", "build.blocked"]
936"#;
937 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
938 let registry = HatRegistry::from_config(&config);
939 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
941
942 let prompt = ralph.build_prompt("", &[]);
943
944 assert!(prompt.contains(
946 "You are Ralph. You are running in a loop. You have fresh context each iteration."
947 ));
948
949 assert!(prompt.contains("### 0a. ORIENTATION"));
951 assert!(prompt.contains("### 0b. SCRATCHPAD"));
952
953 assert!(prompt.contains("## WORKFLOW"));
955 assert!(prompt.contains("### 1. PLAN"));
956 assert!(
957 prompt.contains("### 2. DELEGATE"),
958 "Multi-hat mode should have DELEGATE step"
959 );
960 assert!(
961 !prompt.contains("### 3. IMPLEMENT"),
962 "Multi-hat mode should NOT tell Ralph to implement"
963 );
964 assert!(
965 prompt.contains("You MUST stop working after publishing"),
966 "Should explicitly tell Ralph to stop after publishing event"
967 );
968
969 assert!(prompt.contains("## HATS"));
971 assert!(prompt.contains("Delegate via events"));
972 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
973
974 assert!(prompt.contains("## EVENT WRITING"));
976 assert!(prompt.contains("LOOP_COMPLETE"));
977 }
978
979 #[test]
980 fn test_should_handle_always_true() {
981 let config = RalphConfig::default();
982 let registry = HatRegistry::new();
983 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
984
985 assert!(ralph.should_handle(&Topic::new("any.topic")));
986 assert!(ralph.should_handle(&Topic::new("build.task")));
987 assert!(ralph.should_handle(&Topic::new("unknown.event")));
988 }
989
990 #[test]
991 fn test_rfc2119_patterns_present() {
992 let config = RalphConfig::default();
993 let registry = HatRegistry::new();
994 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
995
996 let prompt = ralph.build_prompt("", &[]);
997
998 assert!(
1000 prompt.contains("You MUST study"),
1001 "Should use RFC2119 MUST with 'study' verb"
1002 );
1003 assert!(
1004 prompt.contains("You MUST complete only one atomic task"),
1005 "Should have RFC2119 MUST complete atomic task constraint"
1006 );
1007 assert!(
1008 prompt.contains("You MAY use parallel subagents"),
1009 "Should mention parallel subagents with MAY"
1010 );
1011 assert!(
1012 prompt.contains("You MUST NOT use more than 1 subagent"),
1013 "Should limit to 1 subagent for builds with MUST NOT"
1014 );
1015 assert!(
1016 prompt.contains("You MUST capture the why"),
1017 "Should emphasize 'why' in commits with MUST"
1018 );
1019
1020 assert!(
1022 prompt.contains("### GUARDRAILS"),
1023 "Should have guardrails section"
1024 );
1025 assert!(
1026 prompt.contains("999."),
1027 "Guardrails should use high numbers"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_scratchpad_format_documented() {
1033 let config = RalphConfig::default();
1034 let registry = HatRegistry::new();
1035 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1036
1037 let prompt = ralph.build_prompt("", &[]);
1038
1039 assert!(prompt.contains("auto-injected"));
1041 assert!(prompt.contains("**Always append**"));
1042 }
1043
1044 #[test]
1045 fn test_starting_event_in_prompt() {
1046 let yaml = r#"
1048hats:
1049 tdd_writer:
1050 name: "TDD Writer"
1051 triggers: ["tdd.start"]
1052 publishes: ["test.written"]
1053"#;
1054 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1055 let registry = HatRegistry::from_config(&config);
1056 let ralph = HatlessRalph::new(
1057 "LOOP_COMPLETE",
1058 config.core.clone(),
1059 ®istry,
1060 Some("tdd.start".to_string()),
1061 );
1062
1063 let prompt = ralph.build_prompt("", &[]);
1064
1065 assert!(
1067 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1068 "Prompt should include starting_event delegation instruction"
1069 );
1070 }
1071
1072 #[test]
1073 fn test_no_starting_event_instruction_when_none() {
1074 let yaml = r#"
1076hats:
1077 some_hat:
1078 name: "Some Hat"
1079 triggers: ["some.event"]
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 prompt = ralph.build_prompt("", &[]);
1086
1087 assert!(
1089 !prompt.contains("After coordination, publish"),
1090 "Prompt should NOT include starting_event delegation when None"
1091 );
1092 }
1093
1094 #[test]
1095 fn test_hat_instructions_propagated_to_prompt() {
1096 let yaml = r#"
1099hats:
1100 tdd_writer:
1101 name: "TDD Writer"
1102 triggers: ["tdd.start"]
1103 publishes: ["test.written"]
1104 instructions: |
1105 You are a Test-Driven Development specialist.
1106 Always write failing tests before implementation.
1107 Focus on edge cases and error handling.
1108"#;
1109 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1110 let registry = HatRegistry::from_config(&config);
1111 let ralph = HatlessRalph::new(
1112 "LOOP_COMPLETE",
1113 config.core.clone(),
1114 ®istry,
1115 Some("tdd.start".to_string()),
1116 );
1117
1118 let tdd_writer = registry
1120 .get(&ralph_proto::HatId::new("tdd_writer"))
1121 .unwrap();
1122 let prompt = ralph.build_prompt("", &[tdd_writer]);
1123
1124 assert!(
1126 prompt.contains("### TDD Writer Instructions"),
1127 "Prompt should include hat instructions section header"
1128 );
1129 assert!(
1130 prompt.contains("Test-Driven Development specialist"),
1131 "Prompt should include actual instructions content"
1132 );
1133 assert!(
1134 prompt.contains("Always write failing tests"),
1135 "Prompt should include full instructions"
1136 );
1137 }
1138
1139 #[test]
1140 fn test_empty_instructions_not_rendered() {
1141 let yaml = r#"
1143hats:
1144 builder:
1145 name: "Builder"
1146 triggers: ["build.task"]
1147 publishes: ["build.done"]
1148"#;
1149 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1150 let registry = HatRegistry::from_config(&config);
1151 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1152
1153 let prompt = ralph.build_prompt("", &[]);
1154
1155 assert!(
1157 !prompt.contains("### Builder Instructions"),
1158 "Prompt should NOT include instructions section for hat with empty instructions"
1159 );
1160 }
1161
1162 #[test]
1163 fn test_multiple_hats_with_instructions() {
1164 let yaml = r#"
1166hats:
1167 planner:
1168 name: "Planner"
1169 triggers: ["planning.start"]
1170 publishes: ["build.task"]
1171 instructions: "Plan carefully before implementation."
1172 builder:
1173 name: "Builder"
1174 triggers: ["build.task"]
1175 publishes: ["build.done"]
1176 instructions: "Focus on clean, testable code."
1177"#;
1178 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1179 let registry = HatRegistry::from_config(&config);
1180 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1181
1182 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1184 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1185 let prompt = ralph.build_prompt("", &[planner, builder]);
1186
1187 assert!(
1189 prompt.contains("### Planner Instructions"),
1190 "Prompt should include Planner instructions section"
1191 );
1192 assert!(
1193 prompt.contains("Plan carefully before implementation"),
1194 "Prompt should include Planner instructions content"
1195 );
1196 assert!(
1197 prompt.contains("### Builder Instructions"),
1198 "Prompt should include Builder instructions section"
1199 );
1200 assert!(
1201 prompt.contains("Focus on clean, testable code"),
1202 "Prompt should include Builder instructions content"
1203 );
1204 }
1205
1206 #[test]
1207 fn test_fast_path_with_starting_event() {
1208 let yaml = r#"
1211core:
1212 scratchpad: "/nonexistent/path/scratchpad.md"
1213hats:
1214 tdd_writer:
1215 name: "TDD Writer"
1216 triggers: ["tdd.start"]
1217 publishes: ["test.written"]
1218"#;
1219 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1220 let registry = HatRegistry::from_config(&config);
1221 let ralph = HatlessRalph::new(
1222 "LOOP_COMPLETE",
1223 config.core.clone(),
1224 ®istry,
1225 Some("tdd.start".to_string()),
1226 );
1227
1228 let prompt = ralph.build_prompt("", &[]);
1229
1230 assert!(
1232 prompt.contains("FAST PATH"),
1233 "Prompt should indicate fast path when starting_event set and no scratchpad"
1234 );
1235 assert!(
1236 prompt.contains("You MUST publish `tdd.start` immediately"),
1237 "Prompt should instruct immediate event publishing with MUST"
1238 );
1239 assert!(
1240 !prompt.contains("### 1. PLAN"),
1241 "Fast path should skip PLAN step"
1242 );
1243 }
1244
1245 #[test]
1246 fn test_events_context_included_in_prompt() {
1247 let config = RalphConfig::default();
1251 let registry = HatRegistry::new();
1252 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1253
1254 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1255[build.done] Build completed successfully";
1256
1257 let prompt = ralph.build_prompt(events_context, &[]);
1258
1259 assert!(
1260 prompt.contains("## PENDING EVENTS"),
1261 "Prompt should contain PENDING EVENTS section"
1262 );
1263 assert!(
1264 prompt.contains("Review this code for security vulnerabilities"),
1265 "Prompt should contain the user's task"
1266 );
1267 assert!(
1268 prompt.contains("Build completed successfully"),
1269 "Prompt should contain all events from context"
1270 );
1271 }
1272
1273 #[test]
1274 fn test_empty_context_no_pending_events_section() {
1275 let config = RalphConfig::default();
1279 let registry = HatRegistry::new();
1280 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1281
1282 let prompt = ralph.build_prompt("", &[]);
1283
1284 assert!(
1285 !prompt.contains("## PENDING EVENTS"),
1286 "Empty context should not produce PENDING EVENTS section"
1287 );
1288 }
1289
1290 #[test]
1291 fn test_whitespace_only_context_no_pending_events_section() {
1292 let config = RalphConfig::default();
1296 let registry = HatRegistry::new();
1297 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1298
1299 let prompt = ralph.build_prompt(" \n\t ", &[]);
1300
1301 assert!(
1302 !prompt.contains("## PENDING EVENTS"),
1303 "Whitespace-only context should not produce PENDING EVENTS section"
1304 );
1305 }
1306
1307 #[test]
1308 fn test_events_section_before_workflow() {
1309 let config = RalphConfig::default();
1313 let registry = HatRegistry::new();
1314 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1315
1316 let events_context = "[task.start] Implement feature X";
1317 let prompt = ralph.build_prompt(events_context, &[]);
1318
1319 let events_pos = prompt
1320 .find("## PENDING EVENTS")
1321 .expect("Should have PENDING EVENTS");
1322 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1323
1324 assert!(
1325 events_pos < workflow_pos,
1326 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1327 events_pos,
1328 workflow_pos
1329 );
1330 }
1331
1332 #[test]
1335 fn test_only_active_hat_instructions_included() {
1336 let yaml = r#"
1338hats:
1339 security_reviewer:
1340 name: "Security Reviewer"
1341 triggers: ["review.security"]
1342 instructions: "Review code for security vulnerabilities."
1343 architecture_reviewer:
1344 name: "Architecture Reviewer"
1345 triggers: ["review.architecture"]
1346 instructions: "Review system design and architecture."
1347 correctness_reviewer:
1348 name: "Correctness Reviewer"
1349 triggers: ["review.correctness"]
1350 instructions: "Review logic and correctness."
1351"#;
1352 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1353 let registry = HatRegistry::from_config(&config);
1354 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1355
1356 let security_hat = registry
1358 .get(&ralph_proto::HatId::new("security_reviewer"))
1359 .unwrap();
1360 let active_hats = vec![security_hat];
1361
1362 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1363
1364 assert!(
1366 prompt.contains("### Security Reviewer Instructions"),
1367 "Should include Security Reviewer instructions section"
1368 );
1369 assert!(
1370 prompt.contains("Review code for security vulnerabilities"),
1371 "Should include Security Reviewer instructions content"
1372 );
1373
1374 assert!(
1376 !prompt.contains("### Architecture Reviewer Instructions"),
1377 "Should NOT include Architecture Reviewer instructions"
1378 );
1379 assert!(
1380 !prompt.contains("Review system design and architecture"),
1381 "Should NOT include Architecture Reviewer instructions content"
1382 );
1383 assert!(
1384 !prompt.contains("### Correctness Reviewer Instructions"),
1385 "Should NOT include Correctness Reviewer instructions"
1386 );
1387 }
1388
1389 #[test]
1390 fn test_multiple_active_hats_all_included() {
1391 let yaml = r#"
1393hats:
1394 security_reviewer:
1395 name: "Security Reviewer"
1396 triggers: ["review.security"]
1397 instructions: "Review code for security vulnerabilities."
1398 architecture_reviewer:
1399 name: "Architecture Reviewer"
1400 triggers: ["review.architecture"]
1401 instructions: "Review system design and architecture."
1402 correctness_reviewer:
1403 name: "Correctness Reviewer"
1404 triggers: ["review.correctness"]
1405 instructions: "Review logic and correctness."
1406"#;
1407 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1408 let registry = HatRegistry::from_config(&config);
1409 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1410
1411 let security_hat = registry
1413 .get(&ralph_proto::HatId::new("security_reviewer"))
1414 .unwrap();
1415 let arch_hat = registry
1416 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1417 .unwrap();
1418 let active_hats = vec![security_hat, arch_hat];
1419
1420 let prompt = ralph.build_prompt("Events", &active_hats);
1421
1422 assert!(
1424 prompt.contains("### Security Reviewer Instructions"),
1425 "Should include Security Reviewer instructions"
1426 );
1427 assert!(
1428 prompt.contains("Review code for security vulnerabilities"),
1429 "Should include Security Reviewer content"
1430 );
1431 assert!(
1432 prompt.contains("### Architecture Reviewer Instructions"),
1433 "Should include Architecture Reviewer instructions"
1434 );
1435 assert!(
1436 prompt.contains("Review system design and architecture"),
1437 "Should include Architecture Reviewer content"
1438 );
1439
1440 assert!(
1442 !prompt.contains("### Correctness Reviewer Instructions"),
1443 "Should NOT include Correctness Reviewer instructions"
1444 );
1445 }
1446
1447 #[test]
1448 fn test_no_active_hats_no_instructions() {
1449 let yaml = r#"
1451hats:
1452 security_reviewer:
1453 name: "Security Reviewer"
1454 triggers: ["review.security"]
1455 instructions: "Review code for security vulnerabilities."
1456"#;
1457 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1458 let registry = HatRegistry::from_config(&config);
1459 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1460
1461 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1463
1464 let prompt = ralph.build_prompt("Events", &active_hats);
1465
1466 assert!(
1468 !prompt.contains("### Security Reviewer Instructions"),
1469 "Should NOT include instructions when no active hats"
1470 );
1471 assert!(
1472 !prompt.contains("Review code for security vulnerabilities"),
1473 "Should NOT include instructions content when no active hats"
1474 );
1475
1476 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1478 assert!(
1479 prompt.contains("| Hat | Triggers On | Publishes |"),
1480 "Should still have topology table"
1481 );
1482 }
1483
1484 #[test]
1485 fn test_topology_table_only_when_ralph_coordinating() {
1486 let yaml = r#"
1489hats:
1490 security_reviewer:
1491 name: "Security Reviewer"
1492 triggers: ["review.security"]
1493 instructions: "Security instructions."
1494 architecture_reviewer:
1495 name: "Architecture Reviewer"
1496 triggers: ["review.architecture"]
1497 instructions: "Architecture instructions."
1498"#;
1499 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1500 let registry = HatRegistry::from_config(&config);
1501 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1502
1503 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1505
1506 assert!(
1507 prompt_coordinating.contains("## HATS"),
1508 "Should have HATS section when coordinating"
1509 );
1510 assert!(
1511 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1512 "Should have topology table when coordinating"
1513 );
1514 assert!(
1515 prompt_coordinating.contains("```mermaid"),
1516 "Should have Mermaid diagram when coordinating"
1517 );
1518
1519 let security_hat = registry
1521 .get(&ralph_proto::HatId::new("security_reviewer"))
1522 .unwrap();
1523 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1524
1525 assert!(
1526 prompt_active.contains("## ACTIVE HAT"),
1527 "Should have ACTIVE HAT section when hat is active"
1528 );
1529 assert!(
1530 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1531 "Should NOT have topology table when hat is active"
1532 );
1533 assert!(
1534 !prompt_active.contains("```mermaid"),
1535 "Should NOT have Mermaid diagram when hat is active"
1536 );
1537 assert!(
1538 prompt_active.contains("### Security Reviewer Instructions"),
1539 "Should still have the active hat's instructions"
1540 );
1541 }
1542
1543 #[test]
1546 fn test_scratchpad_always_included() {
1547 let config = RalphConfig::default();
1549 let registry = HatRegistry::new();
1550 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1551
1552 let prompt = ralph.build_prompt("", &[]);
1553
1554 assert!(
1555 prompt.contains("### 0b. SCRATCHPAD"),
1556 "Scratchpad section should be included"
1557 );
1558 assert!(
1559 prompt.contains("`.ralph/agent/scratchpad.md`"),
1560 "Scratchpad path should be referenced"
1561 );
1562 assert!(
1563 prompt.contains("auto-injected"),
1564 "Auto-injection should be documented"
1565 );
1566 }
1567
1568 #[test]
1569 fn test_scratchpad_included_with_memories_enabled() {
1570 let config = RalphConfig::default();
1572 let registry = HatRegistry::new();
1573 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1574 .with_memories_enabled(true);
1575
1576 let prompt = ralph.build_prompt("", &[]);
1577
1578 assert!(
1580 prompt.contains("### 0b. SCRATCHPAD"),
1581 "Scratchpad section should be included even with memories enabled"
1582 );
1583 assert!(
1584 prompt.contains("**Always append**"),
1585 "Append instruction should be documented"
1586 );
1587
1588 assert!(
1590 !prompt.contains("### 0c. TASKS"),
1591 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1592 );
1593 }
1594
1595 #[test]
1596 fn test_no_tasks_section_in_core_prompt() {
1597 let config = RalphConfig::default();
1599 let registry = HatRegistry::new();
1600 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1601
1602 let prompt = ralph.build_prompt("", &[]);
1603
1604 assert!(
1606 !prompt.contains("### 0c. TASKS"),
1607 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1608 );
1609 }
1610
1611 #[test]
1612 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1613 let config = RalphConfig::default();
1615 let registry = HatRegistry::new();
1616 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1617 .with_memories_enabled(true);
1618
1619 let prompt = ralph.build_prompt("", &[]);
1620
1621 assert!(
1623 prompt.contains("update scratchpad"),
1624 "Workflow should reference scratchpad when memories enabled"
1625 );
1626 assert!(
1628 prompt.contains("ralph tools task"),
1629 "Workflow should reference tasks CLI when memories enabled"
1630 );
1631 }
1632
1633 #[test]
1634 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1635 let yaml = r#"
1637hats:
1638 builder:
1639 name: "Builder"
1640 triggers: ["build.task"]
1641 publishes: ["build.done"]
1642"#;
1643 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1644 let registry = HatRegistry::from_config(&config);
1645 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1646 .with_memories_enabled(true);
1647
1648 let prompt = ralph.build_prompt("", &[]);
1649
1650 assert!(
1652 prompt.contains("scratchpad"),
1653 "Multi-hat workflow should reference scratchpad when memories enabled"
1654 );
1655 assert!(
1657 prompt.contains("ralph tools task add"),
1658 "Multi-hat workflow should reference tasks CLI when memories enabled"
1659 );
1660 }
1661
1662 #[test]
1663 fn test_guardrails_adapt_to_memories_mode() {
1664 let config = RalphConfig::default();
1666 let registry = HatRegistry::new();
1667 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1668 .with_memories_enabled(true);
1669
1670 let prompt = ralph.build_prompt("", &[]);
1671
1672 assert!(
1676 prompt.contains("### GUARDRAILS"),
1677 "Guardrails section should be present"
1678 );
1679 }
1680
1681 #[test]
1682 fn test_guardrails_present_without_memories() {
1683 let config = RalphConfig::default();
1685 let registry = HatRegistry::new();
1686 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1687 let prompt = ralph.build_prompt("", &[]);
1690
1691 assert!(
1692 prompt.contains("### GUARDRAILS"),
1693 "Guardrails section should be present"
1694 );
1695 }
1696
1697 #[test]
1700 fn test_task_closure_verification_in_done_section() {
1701 let config = RalphConfig::default();
1704 let registry = HatRegistry::new();
1705 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1706 .with_memories_enabled(true);
1707
1708 let prompt = ralph.build_prompt("", &[]);
1709
1710 assert!(
1713 prompt.contains("ralph tools task ready"),
1714 "Should reference task ready command in DONE section"
1715 );
1716 assert!(
1717 prompt.contains("MUST NOT emit the completion event while tasks remain open"),
1718 "Should require tasks closed before completion"
1719 );
1720 }
1721
1722 #[test]
1723 fn test_workflow_verify_and_commit_step() {
1724 let config = RalphConfig::default();
1726 let registry = HatRegistry::new();
1727 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1728 .with_memories_enabled(true);
1729
1730 let prompt = ralph.build_prompt("", &[]);
1731
1732 assert!(
1734 prompt.contains("### 4. VERIFY & COMMIT"),
1735 "Should have VERIFY & COMMIT step in workflow"
1736 );
1737 assert!(
1738 prompt.contains("run tests and verify"),
1739 "Should require verification"
1740 );
1741 assert!(
1742 prompt.contains("ralph tools task close"),
1743 "Should reference task close command"
1744 );
1745 }
1746
1747 #[test]
1748 fn test_scratchpad_mode_still_has_commit_step() {
1749 let config = RalphConfig::default();
1751 let registry = HatRegistry::new();
1752 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1753 let prompt = ralph.build_prompt("", &[]);
1756
1757 assert!(
1759 prompt.contains("### 4. COMMIT"),
1760 "Should have COMMIT step in workflow"
1761 );
1762 assert!(
1763 prompt.contains("mark the task `[x]`"),
1764 "Should mark task in scratchpad"
1765 );
1766 assert!(
1768 !prompt.contains("### 0c. TASKS"),
1769 "Scratchpad mode should not have TASKS section"
1770 );
1771 }
1772
1773 #[test]
1776 fn test_objective_section_present_with_set_objective() {
1777 let config = RalphConfig::default();
1779 let registry = HatRegistry::new();
1780 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1781 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
1782
1783 let prompt = ralph.build_prompt("", &[]);
1784
1785 assert!(
1786 prompt.contains("## OBJECTIVE"),
1787 "Should have OBJECTIVE section when objective is set"
1788 );
1789 assert!(
1790 prompt.contains("Implement user authentication with JWT tokens"),
1791 "OBJECTIVE should contain the original user prompt"
1792 );
1793 assert!(
1794 prompt.contains("This is your primary goal"),
1795 "OBJECTIVE should emphasize this is the primary goal"
1796 );
1797 }
1798
1799 #[test]
1800 fn test_objective_reinforced_in_done_section() {
1801 let config = RalphConfig::default();
1804 let registry = HatRegistry::new();
1805 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1806 ralph.set_objective("Fix the login bug in auth module".to_string());
1807
1808 let prompt = ralph.build_prompt("", &[]);
1809
1810 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1812 let after_done = &prompt[done_pos..];
1813
1814 assert!(
1815 after_done.contains("Remember your objective"),
1816 "DONE section should remind about objective"
1817 );
1818 assert!(
1819 after_done.contains("Fix the login bug in auth module"),
1820 "DONE section should restate the objective"
1821 );
1822 }
1823
1824 #[test]
1825 fn test_objective_appears_before_pending_events() {
1826 let config = RalphConfig::default();
1828 let registry = HatRegistry::new();
1829 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1830 ralph.set_objective("Build feature X".to_string());
1831
1832 let context = "Event: task.start - Build feature X";
1833 let prompt = ralph.build_prompt(context, &[]);
1834
1835 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
1836 let events_pos = prompt
1837 .find("## PENDING EVENTS")
1838 .expect("Should have PENDING EVENTS");
1839
1840 assert!(
1841 objective_pos < events_pos,
1842 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
1843 objective_pos,
1844 events_pos
1845 );
1846 }
1847
1848 #[test]
1849 fn test_no_objective_when_not_set() {
1850 let config = RalphConfig::default();
1852 let registry = HatRegistry::new();
1853 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1854
1855 let context = "Event: build.done - Build completed successfully";
1856 let prompt = ralph.build_prompt(context, &[]);
1857
1858 assert!(
1859 !prompt.contains("## OBJECTIVE"),
1860 "Should NOT have OBJECTIVE section when objective not set"
1861 );
1862 }
1863
1864 #[test]
1865 fn test_objective_set_correctly() {
1866 let config = RalphConfig::default();
1868 let registry = HatRegistry::new();
1869 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1870 ralph.set_objective("Review this PR for security issues".to_string());
1871
1872 let prompt = ralph.build_prompt("", &[]);
1873
1874 assert!(
1875 prompt.contains("Review this PR for security issues"),
1876 "Should show the stored objective"
1877 );
1878 }
1879
1880 #[test]
1881 fn test_objective_with_events_context() {
1882 let config = RalphConfig::default();
1884 let registry = HatRegistry::new();
1885 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1886 ralph.set_objective("Implement feature Y".to_string());
1887
1888 let context =
1889 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
1890 let prompt = ralph.build_prompt(context, &[]);
1891
1892 assert!(
1893 prompt.contains("## OBJECTIVE"),
1894 "Should have OBJECTIVE section"
1895 );
1896 assert!(
1897 prompt.contains("Implement feature Y"),
1898 "OBJECTIVE should contain the stored objective"
1899 );
1900 }
1901
1902 #[test]
1903 fn test_done_section_without_objective() {
1904 let config = RalphConfig::default();
1906 let registry = HatRegistry::new();
1907 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1908
1909 let prompt = ralph.build_prompt("", &[]);
1910
1911 assert!(prompt.contains("## DONE"), "Should have DONE section");
1912 assert!(
1913 prompt.contains("LOOP_COMPLETE"),
1914 "DONE should mention completion event"
1915 );
1916 assert!(
1917 !prompt.contains("Remember your objective"),
1918 "Should NOT have objective reinforcement without objective"
1919 );
1920 }
1921
1922 #[test]
1923 fn test_objective_persists_across_iterations() {
1924 let config = RalphConfig::default();
1927 let registry = HatRegistry::new();
1928 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1929 ralph.set_objective("Build a REST API with authentication".to_string());
1930
1931 let context = "Event: build.done - Build completed";
1933 let prompt = ralph.build_prompt(context, &[]);
1934
1935 assert!(
1936 prompt.contains("## OBJECTIVE"),
1937 "OBJECTIVE should persist even without task.start in context"
1938 );
1939 assert!(
1940 prompt.contains("Build a REST API with authentication"),
1941 "Stored objective should appear in later iterations"
1942 );
1943 }
1944
1945 #[test]
1946 fn test_done_section_suppressed_when_hat_active() {
1947 let yaml = r#"
1949hats:
1950 builder:
1951 name: "Builder"
1952 triggers: ["build.task"]
1953 publishes: ["build.done"]
1954 instructions: "Build the code."
1955"#;
1956 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1957 let registry = HatRegistry::from_config(&config);
1958 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1959 ralph.set_objective("Implement feature X".to_string());
1960
1961 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1962 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
1963
1964 assert!(
1965 !prompt.contains("## DONE"),
1966 "DONE section should be suppressed when a hat is active"
1967 );
1968 assert!(
1969 !prompt.contains("LOOP_COMPLETE"),
1970 "Completion promise should NOT appear when a hat is active"
1971 );
1972 assert!(
1974 prompt.contains("## OBJECTIVE"),
1975 "OBJECTIVE should still appear even when hat is active"
1976 );
1977 assert!(
1978 prompt.contains("Implement feature X"),
1979 "Objective content should be visible to active hat"
1980 );
1981 }
1982
1983 #[test]
1984 fn test_done_section_present_when_coordinating() {
1985 let yaml = r#"
1987hats:
1988 builder:
1989 name: "Builder"
1990 triggers: ["build.task"]
1991 publishes: ["build.done"]
1992"#;
1993 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1994 let registry = HatRegistry::from_config(&config);
1995 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1996 ralph.set_objective("Complete the TDD cycle".to_string());
1997
1998 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
2000
2001 assert!(
2002 prompt.contains("## DONE"),
2003 "DONE section should appear when Ralph is coordinating"
2004 );
2005 assert!(
2006 prompt.contains("LOOP_COMPLETE"),
2007 "Completion promise should appear when coordinating"
2008 );
2009 }
2010
2011 #[test]
2012 fn test_objective_in_done_section_when_coordinating() {
2013 let config = RalphConfig::default();
2015 let registry = HatRegistry::new();
2016 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2017 ralph.set_objective("Deploy the application".to_string());
2018
2019 let prompt = ralph.build_prompt("", &[]);
2020
2021 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2022 let after_done = &prompt[done_pos..];
2023
2024 assert!(
2025 after_done.contains("Remember your objective"),
2026 "DONE section should remind about objective when coordinating"
2027 );
2028 assert!(
2029 after_done.contains("Deploy the application"),
2030 "DONE section should contain the objective text"
2031 );
2032 }
2033
2034 #[test]
2037 fn test_event_publishing_guide_with_receivers() {
2038 let yaml = r#"
2041hats:
2042 builder:
2043 name: "Builder"
2044 description: "Builds and tests code"
2045 triggers: ["build.task"]
2046 publishes: ["build.done", "build.blocked"]
2047 confessor:
2048 name: "Confessor"
2049 description: "Produces a ConfessionReport; rewarded for honesty"
2050 triggers: ["build.done"]
2051 publishes: ["confession.done"]
2052"#;
2053 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2054 let registry = HatRegistry::from_config(&config);
2055 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2056
2057 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2059 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2060
2061 assert!(
2063 prompt.contains("### Event Publishing Guide"),
2064 "Should include Event Publishing Guide section"
2065 );
2066 assert!(
2067 prompt.contains("When you publish:"),
2068 "Guide should explain what happens when publishing"
2069 );
2070 assert!(
2072 prompt.contains("`build.done` → Received by: Confessor"),
2073 "Should show Confessor receives build.done"
2074 );
2075 assert!(
2076 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2077 "Should include receiver's description"
2078 );
2079 assert!(
2081 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2082 "Should show Ralph receives orphan events"
2083 );
2084 }
2085
2086 #[test]
2087 fn test_event_publishing_guide_no_publishes() {
2088 let yaml = r#"
2090hats:
2091 observer:
2092 name: "Observer"
2093 description: "Only observes"
2094 triggers: ["events.*"]
2095"#;
2096 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2097 let registry = HatRegistry::from_config(&config);
2098 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2099
2100 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2101 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2102
2103 assert!(
2105 !prompt.contains("### Event Publishing Guide"),
2106 "Should NOT include Event Publishing Guide when hat has no publishes"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_event_publishing_guide_all_orphan_events() {
2112 let yaml = r#"
2114hats:
2115 solo:
2116 name: "Solo"
2117 triggers: ["solo.start"]
2118 publishes: ["solo.done", "solo.failed"]
2119"#;
2120 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2121 let registry = HatRegistry::from_config(&config);
2122 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2123
2124 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2125 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2126
2127 assert!(
2128 prompt.contains("### Event Publishing Guide"),
2129 "Should include guide even for orphan events"
2130 );
2131 assert!(
2132 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2133 "Orphan solo.done should go to Ralph"
2134 );
2135 assert!(
2136 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2137 "Orphan solo.failed should go to Ralph"
2138 );
2139 }
2140
2141 #[test]
2142 fn test_event_publishing_guide_multiple_receivers() {
2143 let yaml = r#"
2145hats:
2146 broadcaster:
2147 name: "Broadcaster"
2148 triggers: ["broadcast.start"]
2149 publishes: ["signal.sent"]
2150 listener1:
2151 name: "Listener1"
2152 description: "First listener"
2153 triggers: ["signal.sent"]
2154 listener2:
2155 name: "Listener2"
2156 description: "Second listener"
2157 triggers: ["signal.sent"]
2158"#;
2159 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2160 let registry = HatRegistry::from_config(&config);
2161 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2162
2163 let broadcaster = registry
2164 .get(&ralph_proto::HatId::new("broadcaster"))
2165 .unwrap();
2166 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2167
2168 assert!(
2169 prompt.contains("### Event Publishing Guide"),
2170 "Should include guide"
2171 );
2172 assert!(
2174 prompt.contains("Listener1 (First listener)"),
2175 "Should list Listener1 as receiver"
2176 );
2177 assert!(
2178 prompt.contains("Listener2 (Second listener)"),
2179 "Should list Listener2 as receiver"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_event_publishing_guide_excludes_self() {
2185 let yaml = r#"
2187hats:
2188 looper:
2189 name: "Looper"
2190 triggers: ["loop.continue", "loop.start"]
2191 publishes: ["loop.continue"]
2192"#;
2193 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2194 let registry = HatRegistry::from_config(&config);
2195 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2196
2197 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2198 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2199
2200 assert!(
2201 prompt.contains("### Event Publishing Guide"),
2202 "Should include guide"
2203 );
2204 assert!(
2206 prompt.contains("`loop.continue` → Received by: Ralph (coordinates next steps)"),
2207 "Self-subscription should be excluded, falling back to Ralph"
2208 );
2209 }
2210
2211 #[test]
2212 fn test_event_publishing_guide_receiver_without_description() {
2213 let yaml = r#"
2215hats:
2216 sender:
2217 name: "Sender"
2218 triggers: ["send.start"]
2219 publishes: ["message.sent"]
2220 receiver:
2221 name: "NoDescReceiver"
2222 triggers: ["message.sent"]
2223"#;
2224 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2225 let registry = HatRegistry::from_config(&config);
2226 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2227
2228 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2229 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2230
2231 assert!(
2232 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2233 "Should show receiver name without parentheses when no description"
2234 );
2235 assert!(
2237 !prompt.contains("NoDescReceiver ()"),
2238 "Should NOT have empty parentheses for receiver without description"
2239 );
2240 }
2241
2242 #[test]
2245 fn test_constraint_lists_valid_events_when_coordinating() {
2246 let yaml = r#"
2249hats:
2250 test_writer:
2251 name: "Test Writer"
2252 triggers: ["tdd.start"]
2253 publishes: ["test.written"]
2254 implementer:
2255 name: "Implementer"
2256 triggers: ["test.written"]
2257 publishes: ["test.passing"]
2258"#;
2259 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2260 let registry = HatRegistry::from_config(&config);
2261 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2262
2263 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2265
2266 assert!(
2268 prompt.contains("**CONSTRAINT:**"),
2269 "Prompt should include CONSTRAINT when coordinating"
2270 );
2271 assert!(
2272 prompt.contains("tdd.start"),
2273 "CONSTRAINT should list tdd.start as valid event"
2274 );
2275 assert!(
2276 prompt.contains("test.written"),
2277 "CONSTRAINT should list test.written as valid event"
2278 );
2279 assert!(
2280 prompt.contains("Publishing other events will have no effect"),
2281 "CONSTRAINT should warn about invalid events"
2282 );
2283 }
2284
2285 #[test]
2286 fn test_no_constraint_when_hat_is_active() {
2287 let yaml = r#"
2290hats:
2291 builder:
2292 name: "Builder"
2293 triggers: ["build.task"]
2294 publishes: ["build.done"]
2295 instructions: "Build the code."
2296"#;
2297 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2298 let registry = HatRegistry::from_config(&config);
2299 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2300
2301 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2303 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2304
2305 assert!(
2307 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2308 "Active hat should NOT have coordinating CONSTRAINT"
2309 );
2310
2311 assert!(
2313 prompt.contains("### Event Publishing Guide"),
2314 "Active hat should have Event Publishing Guide"
2315 );
2316 }
2317
2318 #[test]
2319 fn test_no_constraint_when_no_hats() {
2320 let config = RalphConfig::default();
2322 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2324
2325 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2326
2327 assert!(
2329 !prompt.contains("**CONSTRAINT:**"),
2330 "Solo mode should NOT have CONSTRAINT"
2331 );
2332 }
2333
2334 #[test]
2337 fn test_single_guidance_injection() {
2338 let config = RalphConfig::default();
2340 let registry = HatRegistry::new();
2341 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2342 ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2343
2344 let prompt = ralph.build_prompt("", &[]);
2345
2346 assert!(
2347 prompt.contains("## ROBOT GUIDANCE"),
2348 "Should include ROBOT GUIDANCE section"
2349 );
2350 assert!(
2351 prompt.contains("Focus on error handling first"),
2352 "Should contain the guidance message"
2353 );
2354 assert!(
2356 !prompt.contains("1. Focus on error handling first"),
2357 "Single guidance should not be numbered"
2358 );
2359 }
2360
2361 #[test]
2362 fn test_multiple_guidance_squashing() {
2363 let config = RalphConfig::default();
2365 let registry = HatRegistry::new();
2366 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2367 ralph.set_robot_guidance(vec![
2368 "Focus on error handling".to_string(),
2369 "Use the existing retry pattern".to_string(),
2370 "Check edge cases for empty input".to_string(),
2371 ]);
2372
2373 let prompt = ralph.build_prompt("", &[]);
2374
2375 assert!(
2376 prompt.contains("## ROBOT GUIDANCE"),
2377 "Should include ROBOT GUIDANCE section"
2378 );
2379 assert!(
2380 prompt.contains("1. Focus on error handling"),
2381 "First guidance should be numbered 1"
2382 );
2383 assert!(
2384 prompt.contains("2. Use the existing retry pattern"),
2385 "Second guidance should be numbered 2"
2386 );
2387 assert!(
2388 prompt.contains("3. Check edge cases for empty input"),
2389 "Third guidance should be numbered 3"
2390 );
2391 }
2392
2393 #[test]
2394 fn test_guidance_appears_in_prompt_before_events() {
2395 let config = RalphConfig::default();
2397 let registry = HatRegistry::new();
2398 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2399 ralph.set_objective("Build feature X".to_string());
2400 ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2401
2402 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2403
2404 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2405 let guidance_pos = prompt
2406 .find("## ROBOT GUIDANCE")
2407 .expect("Should have ROBOT GUIDANCE");
2408 let events_pos = prompt
2409 .find("## PENDING EVENTS")
2410 .expect("Should have PENDING EVENTS");
2411
2412 assert!(
2413 objective_pos < guidance_pos,
2414 "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2415 objective_pos,
2416 guidance_pos
2417 );
2418 assert!(
2419 guidance_pos < events_pos,
2420 "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2421 guidance_pos,
2422 events_pos
2423 );
2424 }
2425
2426 #[test]
2427 fn test_guidance_cleared_after_injection() {
2428 let config = RalphConfig::default();
2430 let registry = HatRegistry::new();
2431 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2432 ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2433
2434 let prompt1 = ralph.build_prompt("", &[]);
2436 assert!(
2437 prompt1.contains("## ROBOT GUIDANCE"),
2438 "First prompt should have guidance"
2439 );
2440
2441 ralph.clear_robot_guidance();
2443
2444 let prompt2 = ralph.build_prompt("", &[]);
2446 assert!(
2447 !prompt2.contains("## ROBOT GUIDANCE"),
2448 "After clearing, prompt should not have guidance"
2449 );
2450 }
2451
2452 #[test]
2453 fn test_no_injection_when_no_guidance() {
2454 let config = RalphConfig::default();
2456 let registry = HatRegistry::new();
2457 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2458
2459 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2460
2461 assert!(
2462 !prompt.contains("## ROBOT GUIDANCE"),
2463 "Should NOT include ROBOT GUIDANCE when no guidance set"
2464 );
2465 }
2466}