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"
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 output {} when the objective is complete and all tasks are done.
837",
838 self.completion_promise
839 );
840
841 if self.memories_enabled {
843 section.push_str(
844 r"
845**Before declaring completion:**
8461. Run `ralph tools task ready` to check for open tasks
8472. If any tasks are open, complete them first
8483. Only output LOOP_COMPLETE when YOUR tasks are all closed
849
850Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
851
852You MUST NOT output LOOP_COMPLETE while tasks remain open.
853",
854 );
855 }
856
857 if let Some(obj) = objective {
859 section.push_str(&format!(
860 r"
861**Remember your objective:**
862> {}
863
864You MUST NOT declare completion until this objective is fully satisfied.
865",
866 obj
867 ));
868 }
869
870 section
871 }
872}
873
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::config::RalphConfig;
878
879 #[test]
880 fn test_prompt_without_hats() {
881 let config = RalphConfig::default();
882 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
884
885 let prompt = ralph.build_prompt("", &[]);
886
887 assert!(prompt.contains(
889 "You are Ralph. You are running in a loop. You have fresh context each iteration."
890 ));
891
892 assert!(prompt.contains("### 0a. ORIENTATION"));
894 assert!(prompt.contains("MUST complete only one atomic task"));
895
896 assert!(prompt.contains("### 0b. SCRATCHPAD"));
898 assert!(prompt.contains("auto-injected"));
899 assert!(prompt.contains("**Always append**"));
900
901 assert!(prompt.contains("## WORKFLOW"));
903 assert!(prompt.contains("### 1. Study the prompt"));
904 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
905 assert!(prompt.contains("### 2. PLAN"));
906 assert!(prompt.contains("### 3. IMPLEMENT"));
907 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
908 assert!(prompt.contains("### 4. COMMIT"));
909 assert!(prompt.contains("You MUST capture the why"));
910 assert!(prompt.contains("### 5. REPEAT"));
911
912 assert!(!prompt.contains("## HATS"));
914
915 assert!(prompt.contains("## EVENT WRITING"));
917 assert!(prompt.contains("You MUST use `ralph emit`"));
918 assert!(prompt.contains("You MUST NOT use echo/cat"));
919 assert!(prompt.contains("LOOP_COMPLETE"));
920 }
921
922 #[test]
923 fn test_prompt_with_hats() {
924 let yaml = r#"
926hats:
927 planner:
928 name: "Planner"
929 triggers: ["planning.start", "build.done", "build.blocked"]
930 publishes: ["build.task"]
931 builder:
932 name: "Builder"
933 triggers: ["build.task"]
934 publishes: ["build.done", "build.blocked"]
935"#;
936 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
937 let registry = HatRegistry::from_config(&config);
938 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
940
941 let prompt = ralph.build_prompt("", &[]);
942
943 assert!(prompt.contains(
945 "You are Ralph. You are running in a loop. You have fresh context each iteration."
946 ));
947
948 assert!(prompt.contains("### 0a. ORIENTATION"));
950 assert!(prompt.contains("### 0b. SCRATCHPAD"));
951
952 assert!(prompt.contains("## WORKFLOW"));
954 assert!(prompt.contains("### 1. PLAN"));
955 assert!(
956 prompt.contains("### 2. DELEGATE"),
957 "Multi-hat mode should have DELEGATE step"
958 );
959 assert!(
960 !prompt.contains("### 3. IMPLEMENT"),
961 "Multi-hat mode should NOT tell Ralph to implement"
962 );
963 assert!(
964 prompt.contains("You MUST stop working after publishing"),
965 "Should explicitly tell Ralph to stop after publishing event"
966 );
967
968 assert!(prompt.contains("## HATS"));
970 assert!(prompt.contains("Delegate via events"));
971 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
972
973 assert!(prompt.contains("## EVENT WRITING"));
975 assert!(prompt.contains("LOOP_COMPLETE"));
976 }
977
978 #[test]
979 fn test_should_handle_always_true() {
980 let config = RalphConfig::default();
981 let registry = HatRegistry::new();
982 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
983
984 assert!(ralph.should_handle(&Topic::new("any.topic")));
985 assert!(ralph.should_handle(&Topic::new("build.task")));
986 assert!(ralph.should_handle(&Topic::new("unknown.event")));
987 }
988
989 #[test]
990 fn test_rfc2119_patterns_present() {
991 let config = RalphConfig::default();
992 let registry = HatRegistry::new();
993 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
994
995 let prompt = ralph.build_prompt("", &[]);
996
997 assert!(
999 prompt.contains("You MUST study"),
1000 "Should use RFC2119 MUST with 'study' verb"
1001 );
1002 assert!(
1003 prompt.contains("You MUST complete only one atomic task"),
1004 "Should have RFC2119 MUST complete atomic task constraint"
1005 );
1006 assert!(
1007 prompt.contains("You MAY use parallel subagents"),
1008 "Should mention parallel subagents with MAY"
1009 );
1010 assert!(
1011 prompt.contains("You MUST NOT use more than 1 subagent"),
1012 "Should limit to 1 subagent for builds with MUST NOT"
1013 );
1014 assert!(
1015 prompt.contains("You MUST capture the why"),
1016 "Should emphasize 'why' in commits with MUST"
1017 );
1018
1019 assert!(
1021 prompt.contains("### GUARDRAILS"),
1022 "Should have guardrails section"
1023 );
1024 assert!(
1025 prompt.contains("999."),
1026 "Guardrails should use high numbers"
1027 );
1028 }
1029
1030 #[test]
1031 fn test_scratchpad_format_documented() {
1032 let config = RalphConfig::default();
1033 let registry = HatRegistry::new();
1034 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1035
1036 let prompt = ralph.build_prompt("", &[]);
1037
1038 assert!(prompt.contains("auto-injected"));
1040 assert!(prompt.contains("**Always append**"));
1041 }
1042
1043 #[test]
1044 fn test_starting_event_in_prompt() {
1045 let yaml = r#"
1047hats:
1048 tdd_writer:
1049 name: "TDD Writer"
1050 triggers: ["tdd.start"]
1051 publishes: ["test.written"]
1052"#;
1053 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1054 let registry = HatRegistry::from_config(&config);
1055 let ralph = HatlessRalph::new(
1056 "LOOP_COMPLETE",
1057 config.core.clone(),
1058 ®istry,
1059 Some("tdd.start".to_string()),
1060 );
1061
1062 let prompt = ralph.build_prompt("", &[]);
1063
1064 assert!(
1066 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1067 "Prompt should include starting_event delegation instruction"
1068 );
1069 }
1070
1071 #[test]
1072 fn test_no_starting_event_instruction_when_none() {
1073 let yaml = r#"
1075hats:
1076 some_hat:
1077 name: "Some Hat"
1078 triggers: ["some.event"]
1079"#;
1080 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1081 let registry = HatRegistry::from_config(&config);
1082 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1083
1084 let prompt = ralph.build_prompt("", &[]);
1085
1086 assert!(
1088 !prompt.contains("After coordination, publish"),
1089 "Prompt should NOT include starting_event delegation when None"
1090 );
1091 }
1092
1093 #[test]
1094 fn test_hat_instructions_propagated_to_prompt() {
1095 let yaml = r#"
1098hats:
1099 tdd_writer:
1100 name: "TDD Writer"
1101 triggers: ["tdd.start"]
1102 publishes: ["test.written"]
1103 instructions: |
1104 You are a Test-Driven Development specialist.
1105 Always write failing tests before implementation.
1106 Focus on edge cases and error handling.
1107"#;
1108 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1109 let registry = HatRegistry::from_config(&config);
1110 let ralph = HatlessRalph::new(
1111 "LOOP_COMPLETE",
1112 config.core.clone(),
1113 ®istry,
1114 Some("tdd.start".to_string()),
1115 );
1116
1117 let tdd_writer = registry
1119 .get(&ralph_proto::HatId::new("tdd_writer"))
1120 .unwrap();
1121 let prompt = ralph.build_prompt("", &[tdd_writer]);
1122
1123 assert!(
1125 prompt.contains("### TDD Writer Instructions"),
1126 "Prompt should include hat instructions section header"
1127 );
1128 assert!(
1129 prompt.contains("Test-Driven Development specialist"),
1130 "Prompt should include actual instructions content"
1131 );
1132 assert!(
1133 prompt.contains("Always write failing tests"),
1134 "Prompt should include full instructions"
1135 );
1136 }
1137
1138 #[test]
1139 fn test_empty_instructions_not_rendered() {
1140 let yaml = r#"
1142hats:
1143 builder:
1144 name: "Builder"
1145 triggers: ["build.task"]
1146 publishes: ["build.done"]
1147"#;
1148 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1149 let registry = HatRegistry::from_config(&config);
1150 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1151
1152 let prompt = ralph.build_prompt("", &[]);
1153
1154 assert!(
1156 !prompt.contains("### Builder Instructions"),
1157 "Prompt should NOT include instructions section for hat with empty instructions"
1158 );
1159 }
1160
1161 #[test]
1162 fn test_multiple_hats_with_instructions() {
1163 let yaml = r#"
1165hats:
1166 planner:
1167 name: "Planner"
1168 triggers: ["planning.start"]
1169 publishes: ["build.task"]
1170 instructions: "Plan carefully before implementation."
1171 builder:
1172 name: "Builder"
1173 triggers: ["build.task"]
1174 publishes: ["build.done"]
1175 instructions: "Focus on clean, testable code."
1176"#;
1177 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1178 let registry = HatRegistry::from_config(&config);
1179 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1180
1181 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1183 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1184 let prompt = ralph.build_prompt("", &[planner, builder]);
1185
1186 assert!(
1188 prompt.contains("### Planner Instructions"),
1189 "Prompt should include Planner instructions section"
1190 );
1191 assert!(
1192 prompt.contains("Plan carefully before implementation"),
1193 "Prompt should include Planner instructions content"
1194 );
1195 assert!(
1196 prompt.contains("### Builder Instructions"),
1197 "Prompt should include Builder instructions section"
1198 );
1199 assert!(
1200 prompt.contains("Focus on clean, testable code"),
1201 "Prompt should include Builder instructions content"
1202 );
1203 }
1204
1205 #[test]
1206 fn test_fast_path_with_starting_event() {
1207 let yaml = r#"
1210core:
1211 scratchpad: "/nonexistent/path/scratchpad.md"
1212hats:
1213 tdd_writer:
1214 name: "TDD Writer"
1215 triggers: ["tdd.start"]
1216 publishes: ["test.written"]
1217"#;
1218 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1219 let registry = HatRegistry::from_config(&config);
1220 let ralph = HatlessRalph::new(
1221 "LOOP_COMPLETE",
1222 config.core.clone(),
1223 ®istry,
1224 Some("tdd.start".to_string()),
1225 );
1226
1227 let prompt = ralph.build_prompt("", &[]);
1228
1229 assert!(
1231 prompt.contains("FAST PATH"),
1232 "Prompt should indicate fast path when starting_event set and no scratchpad"
1233 );
1234 assert!(
1235 prompt.contains("You MUST publish `tdd.start` immediately"),
1236 "Prompt should instruct immediate event publishing with MUST"
1237 );
1238 assert!(
1239 !prompt.contains("### 1. PLAN"),
1240 "Fast path should skip PLAN step"
1241 );
1242 }
1243
1244 #[test]
1245 fn test_events_context_included_in_prompt() {
1246 let config = RalphConfig::default();
1250 let registry = HatRegistry::new();
1251 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1252
1253 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1254[build.done] Build completed successfully";
1255
1256 let prompt = ralph.build_prompt(events_context, &[]);
1257
1258 assert!(
1259 prompt.contains("## PENDING EVENTS"),
1260 "Prompt should contain PENDING EVENTS section"
1261 );
1262 assert!(
1263 prompt.contains("Review this code for security vulnerabilities"),
1264 "Prompt should contain the user's task"
1265 );
1266 assert!(
1267 prompt.contains("Build completed successfully"),
1268 "Prompt should contain all events from context"
1269 );
1270 }
1271
1272 #[test]
1273 fn test_empty_context_no_pending_events_section() {
1274 let config = RalphConfig::default();
1278 let registry = HatRegistry::new();
1279 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1280
1281 let prompt = ralph.build_prompt("", &[]);
1282
1283 assert!(
1284 !prompt.contains("## PENDING EVENTS"),
1285 "Empty context should not produce PENDING EVENTS section"
1286 );
1287 }
1288
1289 #[test]
1290 fn test_whitespace_only_context_no_pending_events_section() {
1291 let config = RalphConfig::default();
1295 let registry = HatRegistry::new();
1296 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1297
1298 let prompt = ralph.build_prompt(" \n\t ", &[]);
1299
1300 assert!(
1301 !prompt.contains("## PENDING EVENTS"),
1302 "Whitespace-only context should not produce PENDING EVENTS section"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_events_section_before_workflow() {
1308 let config = RalphConfig::default();
1312 let registry = HatRegistry::new();
1313 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1314
1315 let events_context = "[task.start] Implement feature X";
1316 let prompt = ralph.build_prompt(events_context, &[]);
1317
1318 let events_pos = prompt
1319 .find("## PENDING EVENTS")
1320 .expect("Should have PENDING EVENTS");
1321 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1322
1323 assert!(
1324 events_pos < workflow_pos,
1325 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1326 events_pos,
1327 workflow_pos
1328 );
1329 }
1330
1331 #[test]
1334 fn test_only_active_hat_instructions_included() {
1335 let yaml = r#"
1337hats:
1338 security_reviewer:
1339 name: "Security Reviewer"
1340 triggers: ["review.security"]
1341 instructions: "Review code for security vulnerabilities."
1342 architecture_reviewer:
1343 name: "Architecture Reviewer"
1344 triggers: ["review.architecture"]
1345 instructions: "Review system design and architecture."
1346 correctness_reviewer:
1347 name: "Correctness Reviewer"
1348 triggers: ["review.correctness"]
1349 instructions: "Review logic and correctness."
1350"#;
1351 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1352 let registry = HatRegistry::from_config(&config);
1353 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1354
1355 let security_hat = registry
1357 .get(&ralph_proto::HatId::new("security_reviewer"))
1358 .unwrap();
1359 let active_hats = vec![security_hat];
1360
1361 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1362
1363 assert!(
1365 prompt.contains("### Security Reviewer Instructions"),
1366 "Should include Security Reviewer instructions section"
1367 );
1368 assert!(
1369 prompt.contains("Review code for security vulnerabilities"),
1370 "Should include Security Reviewer instructions content"
1371 );
1372
1373 assert!(
1375 !prompt.contains("### Architecture Reviewer Instructions"),
1376 "Should NOT include Architecture Reviewer instructions"
1377 );
1378 assert!(
1379 !prompt.contains("Review system design and architecture"),
1380 "Should NOT include Architecture Reviewer instructions content"
1381 );
1382 assert!(
1383 !prompt.contains("### Correctness Reviewer Instructions"),
1384 "Should NOT include Correctness Reviewer instructions"
1385 );
1386 }
1387
1388 #[test]
1389 fn test_multiple_active_hats_all_included() {
1390 let yaml = r#"
1392hats:
1393 security_reviewer:
1394 name: "Security Reviewer"
1395 triggers: ["review.security"]
1396 instructions: "Review code for security vulnerabilities."
1397 architecture_reviewer:
1398 name: "Architecture Reviewer"
1399 triggers: ["review.architecture"]
1400 instructions: "Review system design and architecture."
1401 correctness_reviewer:
1402 name: "Correctness Reviewer"
1403 triggers: ["review.correctness"]
1404 instructions: "Review logic and correctness."
1405"#;
1406 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1407 let registry = HatRegistry::from_config(&config);
1408 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1409
1410 let security_hat = registry
1412 .get(&ralph_proto::HatId::new("security_reviewer"))
1413 .unwrap();
1414 let arch_hat = registry
1415 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1416 .unwrap();
1417 let active_hats = vec![security_hat, arch_hat];
1418
1419 let prompt = ralph.build_prompt("Events", &active_hats);
1420
1421 assert!(
1423 prompt.contains("### Security Reviewer Instructions"),
1424 "Should include Security Reviewer instructions"
1425 );
1426 assert!(
1427 prompt.contains("Review code for security vulnerabilities"),
1428 "Should include Security Reviewer content"
1429 );
1430 assert!(
1431 prompt.contains("### Architecture Reviewer Instructions"),
1432 "Should include Architecture Reviewer instructions"
1433 );
1434 assert!(
1435 prompt.contains("Review system design and architecture"),
1436 "Should include Architecture Reviewer content"
1437 );
1438
1439 assert!(
1441 !prompt.contains("### Correctness Reviewer Instructions"),
1442 "Should NOT include Correctness Reviewer instructions"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_no_active_hats_no_instructions() {
1448 let yaml = r#"
1450hats:
1451 security_reviewer:
1452 name: "Security Reviewer"
1453 triggers: ["review.security"]
1454 instructions: "Review code for security vulnerabilities."
1455"#;
1456 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1457 let registry = HatRegistry::from_config(&config);
1458 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1459
1460 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1462
1463 let prompt = ralph.build_prompt("Events", &active_hats);
1464
1465 assert!(
1467 !prompt.contains("### Security Reviewer Instructions"),
1468 "Should NOT include instructions when no active hats"
1469 );
1470 assert!(
1471 !prompt.contains("Review code for security vulnerabilities"),
1472 "Should NOT include instructions content when no active hats"
1473 );
1474
1475 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1477 assert!(
1478 prompt.contains("| Hat | Triggers On | Publishes |"),
1479 "Should still have topology table"
1480 );
1481 }
1482
1483 #[test]
1484 fn test_topology_table_only_when_ralph_coordinating() {
1485 let yaml = r#"
1488hats:
1489 security_reviewer:
1490 name: "Security Reviewer"
1491 triggers: ["review.security"]
1492 instructions: "Security instructions."
1493 architecture_reviewer:
1494 name: "Architecture Reviewer"
1495 triggers: ["review.architecture"]
1496 instructions: "Architecture instructions."
1497"#;
1498 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1499 let registry = HatRegistry::from_config(&config);
1500 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1501
1502 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1504
1505 assert!(
1506 prompt_coordinating.contains("## HATS"),
1507 "Should have HATS section when coordinating"
1508 );
1509 assert!(
1510 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1511 "Should have topology table when coordinating"
1512 );
1513 assert!(
1514 prompt_coordinating.contains("```mermaid"),
1515 "Should have Mermaid diagram when coordinating"
1516 );
1517
1518 let security_hat = registry
1520 .get(&ralph_proto::HatId::new("security_reviewer"))
1521 .unwrap();
1522 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1523
1524 assert!(
1525 prompt_active.contains("## ACTIVE HAT"),
1526 "Should have ACTIVE HAT section when hat is active"
1527 );
1528 assert!(
1529 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1530 "Should NOT have topology table when hat is active"
1531 );
1532 assert!(
1533 !prompt_active.contains("```mermaid"),
1534 "Should NOT have Mermaid diagram when hat is active"
1535 );
1536 assert!(
1537 prompt_active.contains("### Security Reviewer Instructions"),
1538 "Should still have the active hat's instructions"
1539 );
1540 }
1541
1542 #[test]
1545 fn test_scratchpad_always_included() {
1546 let config = RalphConfig::default();
1548 let registry = HatRegistry::new();
1549 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1550
1551 let prompt = ralph.build_prompt("", &[]);
1552
1553 assert!(
1554 prompt.contains("### 0b. SCRATCHPAD"),
1555 "Scratchpad section should be included"
1556 );
1557 assert!(
1558 prompt.contains("`.ralph/agent/scratchpad.md`"),
1559 "Scratchpad path should be referenced"
1560 );
1561 assert!(
1562 prompt.contains("auto-injected"),
1563 "Auto-injection should be documented"
1564 );
1565 }
1566
1567 #[test]
1568 fn test_scratchpad_included_with_memories_enabled() {
1569 let config = RalphConfig::default();
1571 let registry = HatRegistry::new();
1572 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1573 .with_memories_enabled(true);
1574
1575 let prompt = ralph.build_prompt("", &[]);
1576
1577 assert!(
1579 prompt.contains("### 0b. SCRATCHPAD"),
1580 "Scratchpad section should be included even with memories enabled"
1581 );
1582 assert!(
1583 prompt.contains("**Always append**"),
1584 "Append instruction should be documented"
1585 );
1586
1587 assert!(
1589 !prompt.contains("### 0c. TASKS"),
1590 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1591 );
1592 }
1593
1594 #[test]
1595 fn test_no_tasks_section_in_core_prompt() {
1596 let config = RalphConfig::default();
1598 let registry = HatRegistry::new();
1599 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1600
1601 let prompt = ralph.build_prompt("", &[]);
1602
1603 assert!(
1605 !prompt.contains("### 0c. TASKS"),
1606 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1607 );
1608 }
1609
1610 #[test]
1611 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1612 let config = RalphConfig::default();
1614 let registry = HatRegistry::new();
1615 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1616 .with_memories_enabled(true);
1617
1618 let prompt = ralph.build_prompt("", &[]);
1619
1620 assert!(
1622 prompt.contains("update scratchpad"),
1623 "Workflow should reference scratchpad when memories enabled"
1624 );
1625 assert!(
1627 prompt.contains("ralph tools task"),
1628 "Workflow should reference tasks CLI when memories enabled"
1629 );
1630 }
1631
1632 #[test]
1633 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1634 let yaml = r#"
1636hats:
1637 builder:
1638 name: "Builder"
1639 triggers: ["build.task"]
1640 publishes: ["build.done"]
1641"#;
1642 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1643 let registry = HatRegistry::from_config(&config);
1644 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1645 .with_memories_enabled(true);
1646
1647 let prompt = ralph.build_prompt("", &[]);
1648
1649 assert!(
1651 prompt.contains("scratchpad"),
1652 "Multi-hat workflow should reference scratchpad when memories enabled"
1653 );
1654 assert!(
1656 prompt.contains("ralph tools task add"),
1657 "Multi-hat workflow should reference tasks CLI when memories enabled"
1658 );
1659 }
1660
1661 #[test]
1662 fn test_guardrails_adapt_to_memories_mode() {
1663 let config = RalphConfig::default();
1665 let registry = HatRegistry::new();
1666 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1667 .with_memories_enabled(true);
1668
1669 let prompt = ralph.build_prompt("", &[]);
1670
1671 assert!(
1675 prompt.contains("### GUARDRAILS"),
1676 "Guardrails section should be present"
1677 );
1678 }
1679
1680 #[test]
1681 fn test_guardrails_present_without_memories() {
1682 let config = RalphConfig::default();
1684 let registry = HatRegistry::new();
1685 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1686 let prompt = ralph.build_prompt("", &[]);
1689
1690 assert!(
1691 prompt.contains("### GUARDRAILS"),
1692 "Guardrails section should be present"
1693 );
1694 }
1695
1696 #[test]
1699 fn test_task_closure_verification_in_done_section() {
1700 let config = RalphConfig::default();
1703 let registry = HatRegistry::new();
1704 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1705 .with_memories_enabled(true);
1706
1707 let prompt = ralph.build_prompt("", &[]);
1708
1709 assert!(
1712 prompt.contains("ralph tools task ready"),
1713 "Should reference task ready command in DONE section"
1714 );
1715 assert!(
1716 prompt.contains("MUST NOT output LOOP_COMPLETE while tasks remain open"),
1717 "Should require tasks closed before completion"
1718 );
1719 }
1720
1721 #[test]
1722 fn test_workflow_verify_and_commit_step() {
1723 let config = RalphConfig::default();
1725 let registry = HatRegistry::new();
1726 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1727 .with_memories_enabled(true);
1728
1729 let prompt = ralph.build_prompt("", &[]);
1730
1731 assert!(
1733 prompt.contains("### 4. VERIFY & COMMIT"),
1734 "Should have VERIFY & COMMIT step in workflow"
1735 );
1736 assert!(
1737 prompt.contains("run tests and verify"),
1738 "Should require verification"
1739 );
1740 assert!(
1741 prompt.contains("ralph tools task close"),
1742 "Should reference task close command"
1743 );
1744 }
1745
1746 #[test]
1747 fn test_scratchpad_mode_still_has_commit_step() {
1748 let config = RalphConfig::default();
1750 let registry = HatRegistry::new();
1751 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1752 let prompt = ralph.build_prompt("", &[]);
1755
1756 assert!(
1758 prompt.contains("### 4. COMMIT"),
1759 "Should have COMMIT step in workflow"
1760 );
1761 assert!(
1762 prompt.contains("mark the task `[x]`"),
1763 "Should mark task in scratchpad"
1764 );
1765 assert!(
1767 !prompt.contains("### 0c. TASKS"),
1768 "Scratchpad mode should not have TASKS section"
1769 );
1770 }
1771
1772 #[test]
1775 fn test_objective_section_present_with_set_objective() {
1776 let config = RalphConfig::default();
1778 let registry = HatRegistry::new();
1779 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1780 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
1781
1782 let prompt = ralph.build_prompt("", &[]);
1783
1784 assert!(
1785 prompt.contains("## OBJECTIVE"),
1786 "Should have OBJECTIVE section when objective is set"
1787 );
1788 assert!(
1789 prompt.contains("Implement user authentication with JWT tokens"),
1790 "OBJECTIVE should contain the original user prompt"
1791 );
1792 assert!(
1793 prompt.contains("This is your primary goal"),
1794 "OBJECTIVE should emphasize this is the primary goal"
1795 );
1796 }
1797
1798 #[test]
1799 fn test_objective_reinforced_in_done_section() {
1800 let config = RalphConfig::default();
1803 let registry = HatRegistry::new();
1804 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1805 ralph.set_objective("Fix the login bug in auth module".to_string());
1806
1807 let prompt = ralph.build_prompt("", &[]);
1808
1809 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1811 let after_done = &prompt[done_pos..];
1812
1813 assert!(
1814 after_done.contains("Remember your objective"),
1815 "DONE section should remind about objective"
1816 );
1817 assert!(
1818 after_done.contains("Fix the login bug in auth module"),
1819 "DONE section should restate the objective"
1820 );
1821 }
1822
1823 #[test]
1824 fn test_objective_appears_before_pending_events() {
1825 let config = RalphConfig::default();
1827 let registry = HatRegistry::new();
1828 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1829 ralph.set_objective("Build feature X".to_string());
1830
1831 let context = "Event: task.start - Build feature X";
1832 let prompt = ralph.build_prompt(context, &[]);
1833
1834 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
1835 let events_pos = prompt
1836 .find("## PENDING EVENTS")
1837 .expect("Should have PENDING EVENTS");
1838
1839 assert!(
1840 objective_pos < events_pos,
1841 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
1842 objective_pos,
1843 events_pos
1844 );
1845 }
1846
1847 #[test]
1848 fn test_no_objective_when_not_set() {
1849 let config = RalphConfig::default();
1851 let registry = HatRegistry::new();
1852 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1853
1854 let context = "Event: build.done - Build completed successfully";
1855 let prompt = ralph.build_prompt(context, &[]);
1856
1857 assert!(
1858 !prompt.contains("## OBJECTIVE"),
1859 "Should NOT have OBJECTIVE section when objective not set"
1860 );
1861 }
1862
1863 #[test]
1864 fn test_objective_set_correctly() {
1865 let config = RalphConfig::default();
1867 let registry = HatRegistry::new();
1868 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1869 ralph.set_objective("Review this PR for security issues".to_string());
1870
1871 let prompt = ralph.build_prompt("", &[]);
1872
1873 assert!(
1874 prompt.contains("Review this PR for security issues"),
1875 "Should show the stored objective"
1876 );
1877 }
1878
1879 #[test]
1880 fn test_objective_with_events_context() {
1881 let config = RalphConfig::default();
1883 let registry = HatRegistry::new();
1884 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1885 ralph.set_objective("Implement feature Y".to_string());
1886
1887 let context =
1888 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
1889 let prompt = ralph.build_prompt(context, &[]);
1890
1891 assert!(
1892 prompt.contains("## OBJECTIVE"),
1893 "Should have OBJECTIVE section"
1894 );
1895 assert!(
1896 prompt.contains("Implement feature Y"),
1897 "OBJECTIVE should contain the stored objective"
1898 );
1899 }
1900
1901 #[test]
1902 fn test_done_section_without_objective() {
1903 let config = RalphConfig::default();
1905 let registry = HatRegistry::new();
1906 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1907
1908 let prompt = ralph.build_prompt("", &[]);
1909
1910 assert!(prompt.contains("## DONE"), "Should have DONE section");
1911 assert!(
1912 prompt.contains("LOOP_COMPLETE"),
1913 "DONE should mention completion promise"
1914 );
1915 assert!(
1916 !prompt.contains("Remember your objective"),
1917 "Should NOT have objective reinforcement without objective"
1918 );
1919 }
1920
1921 #[test]
1922 fn test_objective_persists_across_iterations() {
1923 let config = RalphConfig::default();
1926 let registry = HatRegistry::new();
1927 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1928 ralph.set_objective("Build a REST API with authentication".to_string());
1929
1930 let context = "Event: build.done - Build completed";
1932 let prompt = ralph.build_prompt(context, &[]);
1933
1934 assert!(
1935 prompt.contains("## OBJECTIVE"),
1936 "OBJECTIVE should persist even without task.start in context"
1937 );
1938 assert!(
1939 prompt.contains("Build a REST API with authentication"),
1940 "Stored objective should appear in later iterations"
1941 );
1942 }
1943
1944 #[test]
1945 fn test_done_section_suppressed_when_hat_active() {
1946 let yaml = r#"
1948hats:
1949 builder:
1950 name: "Builder"
1951 triggers: ["build.task"]
1952 publishes: ["build.done"]
1953 instructions: "Build the code."
1954"#;
1955 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1956 let registry = HatRegistry::from_config(&config);
1957 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1958 ralph.set_objective("Implement feature X".to_string());
1959
1960 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1961 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
1962
1963 assert!(
1964 !prompt.contains("## DONE"),
1965 "DONE section should be suppressed when a hat is active"
1966 );
1967 assert!(
1968 !prompt.contains("LOOP_COMPLETE"),
1969 "Completion promise should NOT appear when a hat is active"
1970 );
1971 assert!(
1973 prompt.contains("## OBJECTIVE"),
1974 "OBJECTIVE should still appear even when hat is active"
1975 );
1976 assert!(
1977 prompt.contains("Implement feature X"),
1978 "Objective content should be visible to active hat"
1979 );
1980 }
1981
1982 #[test]
1983 fn test_done_section_present_when_coordinating() {
1984 let yaml = r#"
1986hats:
1987 builder:
1988 name: "Builder"
1989 triggers: ["build.task"]
1990 publishes: ["build.done"]
1991"#;
1992 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1993 let registry = HatRegistry::from_config(&config);
1994 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1995 ralph.set_objective("Complete the TDD cycle".to_string());
1996
1997 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
1999
2000 assert!(
2001 prompt.contains("## DONE"),
2002 "DONE section should appear when Ralph is coordinating"
2003 );
2004 assert!(
2005 prompt.contains("LOOP_COMPLETE"),
2006 "Completion promise should appear when coordinating"
2007 );
2008 }
2009
2010 #[test]
2011 fn test_objective_in_done_section_when_coordinating() {
2012 let config = RalphConfig::default();
2014 let registry = HatRegistry::new();
2015 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2016 ralph.set_objective("Deploy the application".to_string());
2017
2018 let prompt = ralph.build_prompt("", &[]);
2019
2020 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2021 let after_done = &prompt[done_pos..];
2022
2023 assert!(
2024 after_done.contains("Remember your objective"),
2025 "DONE section should remind about objective when coordinating"
2026 );
2027 assert!(
2028 after_done.contains("Deploy the application"),
2029 "DONE section should contain the objective text"
2030 );
2031 }
2032
2033 #[test]
2036 fn test_event_publishing_guide_with_receivers() {
2037 let yaml = r#"
2040hats:
2041 builder:
2042 name: "Builder"
2043 description: "Builds and tests code"
2044 triggers: ["build.task"]
2045 publishes: ["build.done", "build.blocked"]
2046 confessor:
2047 name: "Confessor"
2048 description: "Produces a ConfessionReport; rewarded for honesty"
2049 triggers: ["build.done"]
2050 publishes: ["confession.done"]
2051"#;
2052 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2053 let registry = HatRegistry::from_config(&config);
2054 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2055
2056 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2058 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2059
2060 assert!(
2062 prompt.contains("### Event Publishing Guide"),
2063 "Should include Event Publishing Guide section"
2064 );
2065 assert!(
2066 prompt.contains("When you publish:"),
2067 "Guide should explain what happens when publishing"
2068 );
2069 assert!(
2071 prompt.contains("`build.done` → Received by: Confessor"),
2072 "Should show Confessor receives build.done"
2073 );
2074 assert!(
2075 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2076 "Should include receiver's description"
2077 );
2078 assert!(
2080 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2081 "Should show Ralph receives orphan events"
2082 );
2083 }
2084
2085 #[test]
2086 fn test_event_publishing_guide_no_publishes() {
2087 let yaml = r#"
2089hats:
2090 observer:
2091 name: "Observer"
2092 description: "Only observes"
2093 triggers: ["events.*"]
2094"#;
2095 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2096 let registry = HatRegistry::from_config(&config);
2097 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2098
2099 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2100 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2101
2102 assert!(
2104 !prompt.contains("### Event Publishing Guide"),
2105 "Should NOT include Event Publishing Guide when hat has no publishes"
2106 );
2107 }
2108
2109 #[test]
2110 fn test_event_publishing_guide_all_orphan_events() {
2111 let yaml = r#"
2113hats:
2114 solo:
2115 name: "Solo"
2116 triggers: ["solo.start"]
2117 publishes: ["solo.done", "solo.failed"]
2118"#;
2119 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2120 let registry = HatRegistry::from_config(&config);
2121 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2122
2123 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2124 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2125
2126 assert!(
2127 prompt.contains("### Event Publishing Guide"),
2128 "Should include guide even for orphan events"
2129 );
2130 assert!(
2131 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2132 "Orphan solo.done should go to Ralph"
2133 );
2134 assert!(
2135 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2136 "Orphan solo.failed should go to Ralph"
2137 );
2138 }
2139
2140 #[test]
2141 fn test_event_publishing_guide_multiple_receivers() {
2142 let yaml = r#"
2144hats:
2145 broadcaster:
2146 name: "Broadcaster"
2147 triggers: ["broadcast.start"]
2148 publishes: ["signal.sent"]
2149 listener1:
2150 name: "Listener1"
2151 description: "First listener"
2152 triggers: ["signal.sent"]
2153 listener2:
2154 name: "Listener2"
2155 description: "Second listener"
2156 triggers: ["signal.sent"]
2157"#;
2158 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2159 let registry = HatRegistry::from_config(&config);
2160 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2161
2162 let broadcaster = registry
2163 .get(&ralph_proto::HatId::new("broadcaster"))
2164 .unwrap();
2165 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2166
2167 assert!(
2168 prompt.contains("### Event Publishing Guide"),
2169 "Should include guide"
2170 );
2171 assert!(
2173 prompt.contains("Listener1 (First listener)"),
2174 "Should list Listener1 as receiver"
2175 );
2176 assert!(
2177 prompt.contains("Listener2 (Second listener)"),
2178 "Should list Listener2 as receiver"
2179 );
2180 }
2181
2182 #[test]
2183 fn test_event_publishing_guide_excludes_self() {
2184 let yaml = r#"
2186hats:
2187 looper:
2188 name: "Looper"
2189 triggers: ["loop.continue", "loop.start"]
2190 publishes: ["loop.continue"]
2191"#;
2192 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2193 let registry = HatRegistry::from_config(&config);
2194 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2195
2196 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2197 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2198
2199 assert!(
2200 prompt.contains("### Event Publishing Guide"),
2201 "Should include guide"
2202 );
2203 assert!(
2205 prompt.contains("`loop.continue` → Received by: Ralph (coordinates next steps)"),
2206 "Self-subscription should be excluded, falling back to Ralph"
2207 );
2208 }
2209
2210 #[test]
2211 fn test_event_publishing_guide_receiver_without_description() {
2212 let yaml = r#"
2214hats:
2215 sender:
2216 name: "Sender"
2217 triggers: ["send.start"]
2218 publishes: ["message.sent"]
2219 receiver:
2220 name: "NoDescReceiver"
2221 triggers: ["message.sent"]
2222"#;
2223 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2224 let registry = HatRegistry::from_config(&config);
2225 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2226
2227 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2228 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2229
2230 assert!(
2231 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2232 "Should show receiver name without parentheses when no description"
2233 );
2234 assert!(
2236 !prompt.contains("NoDescReceiver ()"),
2237 "Should NOT have empty parentheses for receiver without description"
2238 );
2239 }
2240
2241 #[test]
2244 fn test_constraint_lists_valid_events_when_coordinating() {
2245 let yaml = r#"
2248hats:
2249 test_writer:
2250 name: "Test Writer"
2251 triggers: ["tdd.start"]
2252 publishes: ["test.written"]
2253 implementer:
2254 name: "Implementer"
2255 triggers: ["test.written"]
2256 publishes: ["test.passing"]
2257"#;
2258 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2259 let registry = HatRegistry::from_config(&config);
2260 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2261
2262 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2264
2265 assert!(
2267 prompt.contains("**CONSTRAINT:**"),
2268 "Prompt should include CONSTRAINT when coordinating"
2269 );
2270 assert!(
2271 prompt.contains("tdd.start"),
2272 "CONSTRAINT should list tdd.start as valid event"
2273 );
2274 assert!(
2275 prompt.contains("test.written"),
2276 "CONSTRAINT should list test.written as valid event"
2277 );
2278 assert!(
2279 prompt.contains("Publishing other events will have no effect"),
2280 "CONSTRAINT should warn about invalid events"
2281 );
2282 }
2283
2284 #[test]
2285 fn test_no_constraint_when_hat_is_active() {
2286 let yaml = r#"
2289hats:
2290 builder:
2291 name: "Builder"
2292 triggers: ["build.task"]
2293 publishes: ["build.done"]
2294 instructions: "Build the code."
2295"#;
2296 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2297 let registry = HatRegistry::from_config(&config);
2298 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2299
2300 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2302 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2303
2304 assert!(
2306 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2307 "Active hat should NOT have coordinating CONSTRAINT"
2308 );
2309
2310 assert!(
2312 prompt.contains("### Event Publishing Guide"),
2313 "Active hat should have Event Publishing Guide"
2314 );
2315 }
2316
2317 #[test]
2318 fn test_no_constraint_when_no_hats() {
2319 let config = RalphConfig::default();
2321 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2323
2324 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2325
2326 assert!(
2328 !prompt.contains("**CONSTRAINT:**"),
2329 "Solo mode should NOT have CONSTRAINT"
2330 );
2331 }
2332
2333 #[test]
2336 fn test_single_guidance_injection() {
2337 let config = RalphConfig::default();
2339 let registry = HatRegistry::new();
2340 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2341 ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2342
2343 let prompt = ralph.build_prompt("", &[]);
2344
2345 assert!(
2346 prompt.contains("## ROBOT GUIDANCE"),
2347 "Should include ROBOT GUIDANCE section"
2348 );
2349 assert!(
2350 prompt.contains("Focus on error handling first"),
2351 "Should contain the guidance message"
2352 );
2353 assert!(
2355 !prompt.contains("1. Focus on error handling first"),
2356 "Single guidance should not be numbered"
2357 );
2358 }
2359
2360 #[test]
2361 fn test_multiple_guidance_squashing() {
2362 let config = RalphConfig::default();
2364 let registry = HatRegistry::new();
2365 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2366 ralph.set_robot_guidance(vec![
2367 "Focus on error handling".to_string(),
2368 "Use the existing retry pattern".to_string(),
2369 "Check edge cases for empty input".to_string(),
2370 ]);
2371
2372 let prompt = ralph.build_prompt("", &[]);
2373
2374 assert!(
2375 prompt.contains("## ROBOT GUIDANCE"),
2376 "Should include ROBOT GUIDANCE section"
2377 );
2378 assert!(
2379 prompt.contains("1. Focus on error handling"),
2380 "First guidance should be numbered 1"
2381 );
2382 assert!(
2383 prompt.contains("2. Use the existing retry pattern"),
2384 "Second guidance should be numbered 2"
2385 );
2386 assert!(
2387 prompt.contains("3. Check edge cases for empty input"),
2388 "Third guidance should be numbered 3"
2389 );
2390 }
2391
2392 #[test]
2393 fn test_guidance_appears_in_prompt_before_events() {
2394 let config = RalphConfig::default();
2396 let registry = HatRegistry::new();
2397 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2398 ralph.set_objective("Build feature X".to_string());
2399 ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2400
2401 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2402
2403 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2404 let guidance_pos = prompt
2405 .find("## ROBOT GUIDANCE")
2406 .expect("Should have ROBOT GUIDANCE");
2407 let events_pos = prompt
2408 .find("## PENDING EVENTS")
2409 .expect("Should have PENDING EVENTS");
2410
2411 assert!(
2412 objective_pos < guidance_pos,
2413 "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2414 objective_pos,
2415 guidance_pos
2416 );
2417 assert!(
2418 guidance_pos < events_pos,
2419 "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2420 guidance_pos,
2421 events_pos
2422 );
2423 }
2424
2425 #[test]
2426 fn test_guidance_cleared_after_injection() {
2427 let config = RalphConfig::default();
2429 let registry = HatRegistry::new();
2430 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2431 ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2432
2433 let prompt1 = ralph.build_prompt("", &[]);
2435 assert!(
2436 prompt1.contains("## ROBOT GUIDANCE"),
2437 "First prompt should have guidance"
2438 );
2439
2440 ralph.clear_robot_guidance();
2442
2443 let prompt2 = ralph.build_prompt("", &[]);
2445 assert!(
2446 !prompt2.contains("## ROBOT GUIDANCE"),
2447 "After clearing, prompt should not have guidance"
2448 );
2449 }
2450
2451 #[test]
2452 fn test_no_injection_when_no_guidance() {
2453 let config = RalphConfig::default();
2455 let registry = HatRegistry::new();
2456 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2457
2458 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2459
2460 assert!(
2461 !prompt.contains("## ROBOT GUIDANCE"),
2462 "Should NOT include ROBOT GUIDANCE when no guidance set"
2463 );
2464 }
2465}