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