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}
25
26pub struct HatTopology {
28 hats: Vec<HatInfo>,
29}
30
31#[derive(Debug, Clone)]
33pub struct EventReceiver {
34 pub name: String,
35 pub description: String,
36}
37
38pub struct HatInfo {
40 pub name: String,
41 pub description: String,
42 pub subscribes_to: Vec<String>,
43 pub publishes: Vec<String>,
44 pub instructions: String,
45 pub event_receivers: HashMap<String, Vec<EventReceiver>>,
47}
48
49impl HatInfo {
50 pub fn event_publishing_guide(&self) -> Option<String> {
54 if self.publishes.is_empty() {
55 return None;
56 }
57
58 let mut guide = String::from(
59 "### Event Publishing Guide\n\n\
60 You MUST publish exactly ONE event when your work is complete.\n\
61 Publishing hands off to the next hat and starts a fresh iteration with clear context.\n\n\
62 When you publish:\n",
63 );
64
65 for pub_event in &self.publishes {
66 let receivers = self.event_receivers.get(pub_event);
67 let receiver_text = match receivers {
68 Some(r) if !r.is_empty() => r
69 .iter()
70 .map(|recv| {
71 if recv.description.is_empty() {
72 recv.name.clone()
73 } else {
74 format!("{} ({})", recv.name, recv.description)
75 }
76 })
77 .collect::<Vec<_>>()
78 .join(", "),
79 _ => "Ralph (coordinates next steps)".to_string(),
80 };
81 guide.push_str(&format!(
82 "- `{}` → Received by: {}\n",
83 pub_event, receiver_text
84 ));
85 }
86
87 Some(guide)
88 }
89}
90
91impl HatTopology {
92 pub fn from_registry(registry: &HatRegistry) -> Self {
94 let hats = registry
95 .all()
96 .map(|hat| {
97 let event_receivers: HashMap<String, Vec<EventReceiver>> = hat
99 .publishes
100 .iter()
101 .map(|pub_topic| {
102 let receivers: Vec<EventReceiver> = registry
103 .subscribers(pub_topic)
104 .into_iter()
105 .filter(|h| h.id != hat.id) .map(|h| EventReceiver {
107 name: h.name.clone(),
108 description: h.description.clone(),
109 })
110 .collect();
111 (pub_topic.as_str().to_string(), receivers)
112 })
113 .collect();
114
115 HatInfo {
116 name: hat.name.clone(),
117 description: hat.description.clone(),
118 subscribes_to: hat
119 .subscriptions
120 .iter()
121 .map(|t| t.as_str().to_string())
122 .collect(),
123 publishes: hat
124 .publishes
125 .iter()
126 .map(|t| t.as_str().to_string())
127 .collect(),
128 instructions: hat.instructions.clone(),
129 event_receivers,
130 }
131 })
132 .collect();
133
134 Self { hats }
135 }
136}
137
138impl HatlessRalph {
139 pub fn new(
147 completion_promise: impl Into<String>,
148 core: CoreConfig,
149 registry: &HatRegistry,
150 starting_event: Option<String>,
151 ) -> Self {
152 let hat_topology = if registry.is_empty() {
153 None
154 } else {
155 Some(HatTopology::from_registry(registry))
156 };
157
158 Self {
159 completion_promise: completion_promise.into(),
160 core,
161 hat_topology,
162 starting_event,
163 memories_enabled: false, objective: None,
165 }
166 }
167
168 pub fn with_memories_enabled(mut self, enabled: bool) -> Self {
173 self.memories_enabled = enabled;
174 self
175 }
176
177 pub fn set_objective(&mut self, objective: String) {
183 self.objective = Some(objective);
184 }
185
186 pub fn build_prompt(&self, context: &str, active_hats: &[&ralph_proto::Hat]) -> String {
194 let mut prompt = self.core_prompt();
195
196 if let Some(ref obj) = self.objective {
198 prompt.push_str(&self.objective_section(obj));
199 }
200
201 if !context.trim().is_empty() {
203 prompt.push_str("## PENDING EVENTS\n\n");
204 prompt.push_str("You MUST handle these events in this iteration:\n\n");
205 prompt.push_str(context);
206 prompt.push_str("\n\n");
207 }
208
209 let has_custom_workflow = active_hats
212 .iter()
213 .any(|h| !h.instructions.trim().is_empty());
214
215 if !has_custom_workflow {
216 prompt.push_str(&self.workflow_section());
217 }
218
219 if let Some(topology) = &self.hat_topology {
220 prompt.push_str(&self.hats_section(topology, active_hats));
221 }
222
223 prompt.push_str(&self.event_writing_section());
224
225 if active_hats.is_empty() {
228 prompt.push_str(&self.done_section(self.objective.as_deref()));
229 }
230
231 prompt
232 }
233
234 fn objective_section(&self, objective: &str) -> String {
236 format!(
237 r"## OBJECTIVE
238
239**This is your primary goal. All work must advance this objective.**
240
241> {objective}
242
243You MUST keep this objective in mind throughout the iteration.
244You MUST NOT get distracted by workflow mechanics — they serve this goal.
245
246",
247 objective = objective
248 )
249 }
250
251 pub fn should_handle(&self, _topic: &Topic) -> bool {
253 true
254 }
255
256 fn is_fresh_start(&self) -> bool {
261 if self.starting_event.is_none() {
263 return false;
264 }
265
266 let path = Path::new(&self.core.scratchpad);
268 !path.exists()
269 }
270
271 fn core_prompt(&self) -> String {
272 let guardrails = self
274 .core
275 .guardrails
276 .iter()
277 .enumerate()
278 .map(|(i, g)| {
279 let guardrail = if self.memories_enabled && g.contains("scratchpad is memory") {
281 g.replace(
282 "scratchpad is memory",
283 "save learnings to memories for next time",
284 )
285 } else {
286 g.clone()
287 };
288 format!("{}. {guardrail}", 999 + i)
289 })
290 .collect::<Vec<_>>()
291 .join("\n");
292
293 let mut prompt = r"
294### 0a. ORIENTATION
295You are Ralph. You are running in a loop. You have fresh context each iteration.
296You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
297"
298 .to_string();
299
300 prompt.push_str(&format!(
302 r"### 0b. SCRATCHPAD
303`{scratchpad}` is your working memory for THIS objective.
304
305**Use for:**
306- Current objective understanding
307- Notes and reasoning for current work
308- Progress tracking and next steps
309
310Task markers:
311- `[ ]` pending
312- `[x]` done
313- `[~]` cancelled (with reason)
314
315",
316 scratchpad = self.core.scratchpad,
317 ));
318
319 if self.memories_enabled {
321 prompt.push_str(
322 "### 0c. TASKS
323Runtime work tracking via CLI (preferred over scratchpad markers):
324
325```bash
326ralph tools task add 'Title' -p 2 # Create (priority 1-5, 1=highest)
327ralph tools task add 'X' --blocked-by Y # With dependency
328ralph tools task ready # Unblocked tasks only
329ralph tools task close <id> # Mark complete (ONLY after verification)
330```
331
332**CRITICAL:** Only close tasks after verification (tests pass, build succeeds).
333
334",
335 );
336 }
337
338 prompt.push_str(
340 "### TASK BREAKDOWN\n\n\
341- One task = one testable unit of work\n\
342- Tasks should be completable in 1-2 iterations\n\
343- Break large features into smaller tasks\n\
344\n",
345 );
346
347 prompt.push_str(&format!(
349 "### STATE MANAGEMENT\n\n\
350**Scratchpad** (`{scratchpad}`) — Objective working memory:\n\
351- Current objective and immediate plan\n\
352- Notes and reasoning for current work\n\
353- Progress tracking (ephemeral)\n\
354\n\
355**Memories** (`.ralph/agent/memories.md`) — Persistent learning:\n\
356- Codebase patterns and conventions\n\
357- Architectural decisions and rationale\n\
358- Recurring problem solutions\n\
359\n\
360**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
361- Analysis and temporary notes\n\
362- Read when relevant\n\
363\n\
364**Rule:** Notes for current objective go in scratchpad. Learnings for future objectives go in memories.\n\
365\n",
366 scratchpad = self.core.scratchpad,
367 ));
368
369 if let Ok(entries) = std::fs::read_dir(".ralph/agent") {
371 let md_files: Vec<String> = entries
372 .filter_map(|e| e.ok())
373 .filter_map(|e| {
374 let path = e.path();
375 if path.extension().and_then(|s| s.to_str()) == Some("md")
376 && path.file_name().and_then(|s| s.to_str()) != Some("memories.md")
377 {
378 path.file_name()
379 .and_then(|s| s.to_str())
380 .map(|s| s.to_string())
381 } else {
382 None
383 }
384 })
385 .collect();
386
387 if !md_files.is_empty() {
388 prompt.push_str("### AVAILABLE CONTEXT FILES\n\n");
389 prompt.push_str(
390 "Context files in `.ralph/agent/` (read if relevant to current work):\n",
391 );
392 for file in md_files {
393 prompt.push_str(&format!("- `.ralph/agent/{}`\n", file));
394 }
395 prompt.push('\n');
396 }
397 }
398
399 prompt.push_str(&format!(
400 r"### GUARDRAILS
401{guardrails}
402
403",
404 guardrails = guardrails,
405 ));
406
407 prompt
408 }
409
410 fn workflow_section(&self) -> String {
411 if self.hat_topology.is_some() {
413 if self.is_fresh_start() {
415 return format!(
417 r"## WORKFLOW
418
419**FAST PATH**: You MUST publish `{}` immediately to start the hat workflow.
420You MUST NOT plan or analyze — delegate now.
421
422",
423 self.starting_event.as_ref().unwrap()
424 );
425 }
426
427 if self.memories_enabled {
429 format!(
431 r"## WORKFLOW
432
433### 1. PLAN
434You MUST update `{scratchpad}` with your understanding and plan.
435You SHOULD create tasks with `ralph tools task add` for trackable work items.
436
437### 2. DELEGATE
438You MUST publish exactly ONE event to hand off to specialized hats.
439You MUST NOT do implementation work — delegation is your only job.
440
441",
442 scratchpad = self.core.scratchpad
443 )
444 } else {
445 format!(
447 r"## WORKFLOW
448
449### 1. PLAN
450You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
451
452### 2. DELEGATE
453You MUST publish exactly ONE event to hand off to specialized hats.
454You MUST NOT do implementation work — delegation is your only job.
455
456",
457 scratchpad = self.core.scratchpad
458 )
459 }
460 } else {
461 if self.memories_enabled {
463 format!(
465 r"## WORKFLOW
466
467### 1. Study the prompt.
468You MUST study, explore, and research what needs to be done.
469
470### 2. PLAN
471You MUST update `{scratchpad}` with your understanding and plan.
472You SHOULD create tasks with `ralph tools task add` for trackable work items.
473
474### 3. IMPLEMENT
475You MUST pick exactly ONE task to implement.
476
477### 4. VERIFY & COMMIT
478You MUST run tests and verify the implementation works.
479You MUST commit after verification passes - one commit per task.
480You SHOULD run `git diff --cached` to review staged changes before committing.
481You MUST close the task with `ralph tools task close` AFTER commit.
482You SHOULD save learnings to memories with `ralph tools memory add`.
483You MUST update scratchpad to reflect progress.
484
485### 5. EXIT
486You MUST exit after completing ONE task.
487
488",
489 scratchpad = self.core.scratchpad
490 )
491 } else {
492 format!(
494 r"## WORKFLOW
495
496### 1. Study the prompt.
497You MUST study, explore, and research what needs to be done.
498You MAY use parallel subagents (up to 10) for searches.
499
500### 2. PLAN
501You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
502
503### 3. IMPLEMENT
504You MUST pick exactly ONE task to implement.
505You MUST NOT use more than 1 subagent for build/tests.
506
507### 4. COMMIT
508You MUST commit after completing each atomic unit of work.
509You MUST capture the why, not just the what.
510You SHOULD run `git diff` before committing to review changes.
511You MUST mark the task `[x]` in scratchpad when complete.
512
513### 5. REPEAT
514You MUST continue until all tasks are `[x]` or `[~]`.
515
516",
517 scratchpad = self.core.scratchpad
518 )
519 }
520 }
521 }
522
523 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
524 let mut section = String::new();
525
526 if active_hats.is_empty() {
529 section.push_str("## HATS\n\nDelegate via events.\n\n");
531
532 if let Some(ref starting_event) = self.starting_event {
534 section.push_str(&format!(
535 "**After coordination, publish `{}` to start the workflow.**\n\n",
536 starting_event
537 ));
538 }
539
540 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
544 let mut ralph_publishes: Vec<&str> = Vec::new();
545
546 for hat in &topology.hats {
547 for pub_event in &hat.publishes {
548 if !ralph_triggers.contains(&pub_event.as_str()) {
549 ralph_triggers.push(pub_event.as_str());
550 }
551 }
552 for sub_event in &hat.subscribes_to {
553 if !ralph_publishes.contains(&sub_event.as_str()) {
554 ralph_publishes.push(sub_event.as_str());
555 }
556 }
557 }
558
559 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
561 section.push_str("|-----|-------------|----------|-------------|\n");
562
563 section.push_str(&format!(
565 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
566 ralph_triggers.join(", "),
567 ralph_publishes.join(", ")
568 ));
569
570 for hat in &topology.hats {
572 let subscribes = hat.subscribes_to.join(", ");
573 let publishes = hat.publishes.join(", ");
574 section.push_str(&format!(
575 "| {} | {} | {} | {} |\n",
576 hat.name, subscribes, publishes, hat.description
577 ));
578 }
579
580 section.push('\n');
581
582 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
584 section.push('\n');
585
586 if !ralph_publishes.is_empty() {
588 section.push_str(&format!(
589 "**CONSTRAINT:** You MUST only publish events from this list: `{}`\n\
590 Publishing other events will have no effect - no hat will receive them.\n\n",
591 ralph_publishes.join("`, `")
592 ));
593 }
594
595 self.validate_topology_reachability(topology);
597 } else {
598 section.push_str("## ACTIVE HAT\n\n");
600
601 for active_hat in active_hats {
602 let hat_info = topology.hats.iter().find(|h| h.name == active_hat.name);
604
605 if !active_hat.instructions.trim().is_empty() {
606 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
607 section.push_str(&active_hat.instructions);
608 if !active_hat.instructions.ends_with('\n') {
609 section.push('\n');
610 }
611 section.push('\n');
612 }
613
614 if let Some(guide) = hat_info.and_then(|info| info.event_publishing_guide()) {
616 section.push_str(&guide);
617 section.push('\n');
618 }
619 }
620 }
621
622 section
623 }
624
625 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
627 let mut diagram = String::from("```mermaid\nflowchart LR\n");
628
629 diagram.push_str(" task.start((task.start)) --> Ralph\n");
631
632 for hat in &topology.hats {
634 for trigger in &hat.subscribes_to {
635 if ralph_publishes.contains(&trigger.as_str()) {
636 let node_id = hat
638 .name
639 .chars()
640 .filter(|c| c.is_alphanumeric())
641 .collect::<String>();
642 if node_id == hat.name {
643 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
644 } else {
645 diagram.push_str(&format!(
647 " Ralph -->|{}| {}[{}]\n",
648 trigger, node_id, hat.name
649 ));
650 }
651 }
652 }
653 }
654
655 for hat in &topology.hats {
657 let node_id = hat
658 .name
659 .chars()
660 .filter(|c| c.is_alphanumeric())
661 .collect::<String>();
662 for pub_event in &hat.publishes {
663 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
664 }
665 }
666
667 for source_hat in &topology.hats {
669 let source_id = source_hat
670 .name
671 .chars()
672 .filter(|c| c.is_alphanumeric())
673 .collect::<String>();
674 for pub_event in &source_hat.publishes {
675 for target_hat in &topology.hats {
676 if target_hat.name != source_hat.name
677 && target_hat.subscribes_to.contains(pub_event)
678 {
679 let target_id = target_hat
680 .name
681 .chars()
682 .filter(|c| c.is_alphanumeric())
683 .collect::<String>();
684 diagram.push_str(&format!(
685 " {} -->|{}| {}\n",
686 source_id, pub_event, target_id
687 ));
688 }
689 }
690 }
691 }
692
693 diagram.push_str("```\n");
694 diagram
695 }
696
697 fn validate_topology_reachability(&self, topology: &HatTopology) {
700 use std::collections::HashSet;
701 use tracing::warn;
702
703 let mut reachable_events: HashSet<&str> = HashSet::new();
705 reachable_events.insert("task.start");
706
707 for hat in &topology.hats {
709 for trigger in &hat.subscribes_to {
710 reachable_events.insert(trigger.as_str());
711 }
712 }
713
714 for hat in &topology.hats {
716 for pub_event in &hat.publishes {
717 reachable_events.insert(pub_event.as_str());
718 }
719 }
720
721 for hat in &topology.hats {
723 let hat_reachable = hat
724 .subscribes_to
725 .iter()
726 .any(|t| reachable_events.contains(t.as_str()));
727 if !hat_reachable {
728 warn!(
729 hat = %hat.name,
730 triggers = ?hat.subscribes_to,
731 "Hat has triggers that are never published - it may be unreachable"
732 );
733 }
734 }
735 }
736
737 fn event_writing_section(&self) -> String {
738 let detailed_output_hint = format!(
740 "You SHOULD write detailed output to `{}` and emit only a brief event.",
741 self.core.scratchpad
742 );
743
744 format!(
745 r#"## EVENT WRITING
746
747Events are routing signals, not data transport. You SHOULD keep payloads brief.
748
749You MUST use `ralph emit` to write events (handles JSON escaping correctly):
750```bash
751ralph emit "build.done" "tests: pass, lint: pass"
752ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
753```
754
755You MUST NOT use echo/cat to write events because shell escaping breaks JSON.
756
757{detailed_output_hint}
758
759**Constraints:**
760- You MUST stop working after publishing an event because a new iteration will start with fresh context
761- You MUST NOT continue with additional work after publishing because the next iteration handles it with the appropriate hat persona
762"#,
763 detailed_output_hint = detailed_output_hint
764 )
765 }
766
767 fn done_section(&self, objective: Option<&str>) -> String {
768 let mut section = format!(
769 r"## DONE
770
771You MUST output {} when the objective is complete and all tasks are done.
772",
773 self.completion_promise
774 );
775
776 if self.memories_enabled {
778 section.push_str(
779 r"
780**Before declaring completion:**
7811. Run `ralph tools task ready` to check for open tasks
7822. If any tasks are open, complete them first
7833. Only output LOOP_COMPLETE when YOUR tasks are all closed
784
785Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
786
787You MUST NOT output LOOP_COMPLETE while tasks remain open.
788",
789 );
790 }
791
792 if let Some(obj) = objective {
794 section.push_str(&format!(
795 r"
796**Remember your objective:**
797> {}
798
799You MUST NOT declare completion until this objective is fully satisfied.
800",
801 obj
802 ));
803 }
804
805 section
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::config::RalphConfig;
813
814 #[test]
815 fn test_prompt_without_hats() {
816 let config = RalphConfig::default();
817 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
819
820 let prompt = ralph.build_prompt("", &[]);
821
822 assert!(prompt.contains(
824 "You are Ralph. You are running in a loop. You have fresh context each iteration."
825 ));
826
827 assert!(prompt.contains("### 0a. ORIENTATION"));
829 assert!(prompt.contains("MUST complete only one atomic task"));
830
831 assert!(prompt.contains("### 0b. SCRATCHPAD"));
833 assert!(prompt.contains("Task markers:"));
834 assert!(prompt.contains("- `[ ]` pending"));
835 assert!(prompt.contains("- `[x]` done"));
836 assert!(prompt.contains("- `[~]` cancelled"));
837
838 assert!(prompt.contains("## WORKFLOW"));
840 assert!(prompt.contains("### 1. Study the prompt"));
841 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
842 assert!(prompt.contains("### 2. PLAN"));
843 assert!(prompt.contains("### 3. IMPLEMENT"));
844 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
845 assert!(prompt.contains("### 4. COMMIT"));
846 assert!(prompt.contains("You MUST capture the why"));
847 assert!(prompt.contains("### 5. REPEAT"));
848
849 assert!(!prompt.contains("## HATS"));
851
852 assert!(prompt.contains("## EVENT WRITING"));
854 assert!(prompt.contains("You MUST use `ralph emit`"));
855 assert!(prompt.contains("You MUST NOT use echo/cat"));
856 assert!(prompt.contains("LOOP_COMPLETE"));
857 }
858
859 #[test]
860 fn test_prompt_with_hats() {
861 let yaml = r#"
863hats:
864 planner:
865 name: "Planner"
866 triggers: ["planning.start", "build.done", "build.blocked"]
867 publishes: ["build.task"]
868 builder:
869 name: "Builder"
870 triggers: ["build.task"]
871 publishes: ["build.done", "build.blocked"]
872"#;
873 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
874 let registry = HatRegistry::from_config(&config);
875 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
877
878 let prompt = ralph.build_prompt("", &[]);
879
880 assert!(prompt.contains(
882 "You are Ralph. You are running in a loop. You have fresh context each iteration."
883 ));
884
885 assert!(prompt.contains("### 0a. ORIENTATION"));
887 assert!(prompt.contains("### 0b. SCRATCHPAD"));
888
889 assert!(prompt.contains("## WORKFLOW"));
891 assert!(prompt.contains("### 1. PLAN"));
892 assert!(
893 prompt.contains("### 2. DELEGATE"),
894 "Multi-hat mode should have DELEGATE step"
895 );
896 assert!(
897 !prompt.contains("### 3. IMPLEMENT"),
898 "Multi-hat mode should NOT tell Ralph to implement"
899 );
900 assert!(
901 prompt.contains("You MUST stop working after publishing"),
902 "Should explicitly tell Ralph to stop after publishing event"
903 );
904
905 assert!(prompt.contains("## HATS"));
907 assert!(prompt.contains("Delegate via events"));
908 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
909
910 assert!(prompt.contains("## EVENT WRITING"));
912 assert!(prompt.contains("LOOP_COMPLETE"));
913 }
914
915 #[test]
916 fn test_should_handle_always_true() {
917 let config = RalphConfig::default();
918 let registry = HatRegistry::new();
919 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
920
921 assert!(ralph.should_handle(&Topic::new("any.topic")));
922 assert!(ralph.should_handle(&Topic::new("build.task")));
923 assert!(ralph.should_handle(&Topic::new("unknown.event")));
924 }
925
926 #[test]
927 fn test_rfc2119_patterns_present() {
928 let config = RalphConfig::default();
929 let registry = HatRegistry::new();
930 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
931
932 let prompt = ralph.build_prompt("", &[]);
933
934 assert!(
936 prompt.contains("You MUST study"),
937 "Should use RFC2119 MUST with 'study' verb"
938 );
939 assert!(
940 prompt.contains("You MUST complete only one atomic task"),
941 "Should have RFC2119 MUST complete atomic task constraint"
942 );
943 assert!(
944 prompt.contains("You MAY use parallel subagents"),
945 "Should mention parallel subagents with MAY"
946 );
947 assert!(
948 prompt.contains("You MUST NOT use more than 1 subagent"),
949 "Should limit to 1 subagent for builds with MUST NOT"
950 );
951 assert!(
952 prompt.contains("You MUST capture the why"),
953 "Should emphasize 'why' in commits with MUST"
954 );
955
956 assert!(
958 prompt.contains("### GUARDRAILS"),
959 "Should have guardrails section"
960 );
961 assert!(
962 prompt.contains("999."),
963 "Guardrails should use high numbers"
964 );
965 }
966
967 #[test]
968 fn test_scratchpad_format_documented() {
969 let config = RalphConfig::default();
970 let registry = HatRegistry::new();
971 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
972
973 let prompt = ralph.build_prompt("", &[]);
974
975 assert!(prompt.contains("- `[ ]` pending"));
977 assert!(prompt.contains("- `[x]` done"));
978 assert!(prompt.contains("- `[~]` cancelled (with reason)"));
979 }
980
981 #[test]
982 fn test_starting_event_in_prompt() {
983 let yaml = r#"
985hats:
986 tdd_writer:
987 name: "TDD Writer"
988 triggers: ["tdd.start"]
989 publishes: ["test.written"]
990"#;
991 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
992 let registry = HatRegistry::from_config(&config);
993 let ralph = HatlessRalph::new(
994 "LOOP_COMPLETE",
995 config.core.clone(),
996 ®istry,
997 Some("tdd.start".to_string()),
998 );
999
1000 let prompt = ralph.build_prompt("", &[]);
1001
1002 assert!(
1004 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1005 "Prompt should include starting_event delegation instruction"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_no_starting_event_instruction_when_none() {
1011 let yaml = r#"
1013hats:
1014 some_hat:
1015 name: "Some Hat"
1016 triggers: ["some.event"]
1017"#;
1018 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1019 let registry = HatRegistry::from_config(&config);
1020 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1021
1022 let prompt = ralph.build_prompt("", &[]);
1023
1024 assert!(
1026 !prompt.contains("After coordination, publish"),
1027 "Prompt should NOT include starting_event delegation when None"
1028 );
1029 }
1030
1031 #[test]
1032 fn test_hat_instructions_propagated_to_prompt() {
1033 let yaml = r#"
1036hats:
1037 tdd_writer:
1038 name: "TDD Writer"
1039 triggers: ["tdd.start"]
1040 publishes: ["test.written"]
1041 instructions: |
1042 You are a Test-Driven Development specialist.
1043 Always write failing tests before implementation.
1044 Focus on edge cases and error handling.
1045"#;
1046 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1047 let registry = HatRegistry::from_config(&config);
1048 let ralph = HatlessRalph::new(
1049 "LOOP_COMPLETE",
1050 config.core.clone(),
1051 ®istry,
1052 Some("tdd.start".to_string()),
1053 );
1054
1055 let tdd_writer = registry
1057 .get(&ralph_proto::HatId::new("tdd_writer"))
1058 .unwrap();
1059 let prompt = ralph.build_prompt("", &[tdd_writer]);
1060
1061 assert!(
1063 prompt.contains("### TDD Writer Instructions"),
1064 "Prompt should include hat instructions section header"
1065 );
1066 assert!(
1067 prompt.contains("Test-Driven Development specialist"),
1068 "Prompt should include actual instructions content"
1069 );
1070 assert!(
1071 prompt.contains("Always write failing tests"),
1072 "Prompt should include full instructions"
1073 );
1074 }
1075
1076 #[test]
1077 fn test_empty_instructions_not_rendered() {
1078 let yaml = r#"
1080hats:
1081 builder:
1082 name: "Builder"
1083 triggers: ["build.task"]
1084 publishes: ["build.done"]
1085"#;
1086 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1087 let registry = HatRegistry::from_config(&config);
1088 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1089
1090 let prompt = ralph.build_prompt("", &[]);
1091
1092 assert!(
1094 !prompt.contains("### Builder Instructions"),
1095 "Prompt should NOT include instructions section for hat with empty instructions"
1096 );
1097 }
1098
1099 #[test]
1100 fn test_multiple_hats_with_instructions() {
1101 let yaml = r#"
1103hats:
1104 planner:
1105 name: "Planner"
1106 triggers: ["planning.start"]
1107 publishes: ["build.task"]
1108 instructions: "Plan carefully before implementation."
1109 builder:
1110 name: "Builder"
1111 triggers: ["build.task"]
1112 publishes: ["build.done"]
1113 instructions: "Focus on clean, testable code."
1114"#;
1115 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1116 let registry = HatRegistry::from_config(&config);
1117 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1118
1119 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1121 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1122 let prompt = ralph.build_prompt("", &[planner, builder]);
1123
1124 assert!(
1126 prompt.contains("### Planner Instructions"),
1127 "Prompt should include Planner instructions section"
1128 );
1129 assert!(
1130 prompt.contains("Plan carefully before implementation"),
1131 "Prompt should include Planner instructions content"
1132 );
1133 assert!(
1134 prompt.contains("### Builder Instructions"),
1135 "Prompt should include Builder instructions section"
1136 );
1137 assert!(
1138 prompt.contains("Focus on clean, testable code"),
1139 "Prompt should include Builder instructions content"
1140 );
1141 }
1142
1143 #[test]
1144 fn test_fast_path_with_starting_event() {
1145 let yaml = r#"
1148core:
1149 scratchpad: "/nonexistent/path/scratchpad.md"
1150hats:
1151 tdd_writer:
1152 name: "TDD Writer"
1153 triggers: ["tdd.start"]
1154 publishes: ["test.written"]
1155"#;
1156 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1157 let registry = HatRegistry::from_config(&config);
1158 let ralph = HatlessRalph::new(
1159 "LOOP_COMPLETE",
1160 config.core.clone(),
1161 ®istry,
1162 Some("tdd.start".to_string()),
1163 );
1164
1165 let prompt = ralph.build_prompt("", &[]);
1166
1167 assert!(
1169 prompt.contains("FAST PATH"),
1170 "Prompt should indicate fast path when starting_event set and no scratchpad"
1171 );
1172 assert!(
1173 prompt.contains("You MUST publish `tdd.start` immediately"),
1174 "Prompt should instruct immediate event publishing with MUST"
1175 );
1176 assert!(
1177 !prompt.contains("### 1. PLAN"),
1178 "Fast path should skip PLAN step"
1179 );
1180 }
1181
1182 #[test]
1183 fn test_events_context_included_in_prompt() {
1184 let config = RalphConfig::default();
1188 let registry = HatRegistry::new();
1189 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1190
1191 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1192[build.done] Build completed successfully";
1193
1194 let prompt = ralph.build_prompt(events_context, &[]);
1195
1196 assert!(
1197 prompt.contains("## PENDING EVENTS"),
1198 "Prompt should contain PENDING EVENTS section"
1199 );
1200 assert!(
1201 prompt.contains("Review this code for security vulnerabilities"),
1202 "Prompt should contain the user's task"
1203 );
1204 assert!(
1205 prompt.contains("Build completed successfully"),
1206 "Prompt should contain all events from context"
1207 );
1208 }
1209
1210 #[test]
1211 fn test_empty_context_no_pending_events_section() {
1212 let config = RalphConfig::default();
1216 let registry = HatRegistry::new();
1217 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1218
1219 let prompt = ralph.build_prompt("", &[]);
1220
1221 assert!(
1222 !prompt.contains("## PENDING EVENTS"),
1223 "Empty context should not produce PENDING EVENTS section"
1224 );
1225 }
1226
1227 #[test]
1228 fn test_whitespace_only_context_no_pending_events_section() {
1229 let config = RalphConfig::default();
1233 let registry = HatRegistry::new();
1234 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1235
1236 let prompt = ralph.build_prompt(" \n\t ", &[]);
1237
1238 assert!(
1239 !prompt.contains("## PENDING EVENTS"),
1240 "Whitespace-only context should not produce PENDING EVENTS section"
1241 );
1242 }
1243
1244 #[test]
1245 fn test_events_section_before_workflow() {
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 = "[task.start] Implement feature X";
1254 let prompt = ralph.build_prompt(events_context, &[]);
1255
1256 let events_pos = prompt
1257 .find("## PENDING EVENTS")
1258 .expect("Should have PENDING EVENTS");
1259 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1260
1261 assert!(
1262 events_pos < workflow_pos,
1263 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1264 events_pos,
1265 workflow_pos
1266 );
1267 }
1268
1269 #[test]
1272 fn test_only_active_hat_instructions_included() {
1273 let yaml = r#"
1275hats:
1276 security_reviewer:
1277 name: "Security Reviewer"
1278 triggers: ["review.security"]
1279 instructions: "Review code for security vulnerabilities."
1280 architecture_reviewer:
1281 name: "Architecture Reviewer"
1282 triggers: ["review.architecture"]
1283 instructions: "Review system design and architecture."
1284 correctness_reviewer:
1285 name: "Correctness Reviewer"
1286 triggers: ["review.correctness"]
1287 instructions: "Review logic and correctness."
1288"#;
1289 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1290 let registry = HatRegistry::from_config(&config);
1291 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1292
1293 let security_hat = registry
1295 .get(&ralph_proto::HatId::new("security_reviewer"))
1296 .unwrap();
1297 let active_hats = vec![security_hat];
1298
1299 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1300
1301 assert!(
1303 prompt.contains("### Security Reviewer Instructions"),
1304 "Should include Security Reviewer instructions section"
1305 );
1306 assert!(
1307 prompt.contains("Review code for security vulnerabilities"),
1308 "Should include Security Reviewer instructions content"
1309 );
1310
1311 assert!(
1313 !prompt.contains("### Architecture Reviewer Instructions"),
1314 "Should NOT include Architecture Reviewer instructions"
1315 );
1316 assert!(
1317 !prompt.contains("Review system design and architecture"),
1318 "Should NOT include Architecture Reviewer instructions content"
1319 );
1320 assert!(
1321 !prompt.contains("### Correctness Reviewer Instructions"),
1322 "Should NOT include Correctness Reviewer instructions"
1323 );
1324 }
1325
1326 #[test]
1327 fn test_multiple_active_hats_all_included() {
1328 let yaml = r#"
1330hats:
1331 security_reviewer:
1332 name: "Security Reviewer"
1333 triggers: ["review.security"]
1334 instructions: "Review code for security vulnerabilities."
1335 architecture_reviewer:
1336 name: "Architecture Reviewer"
1337 triggers: ["review.architecture"]
1338 instructions: "Review system design and architecture."
1339 correctness_reviewer:
1340 name: "Correctness Reviewer"
1341 triggers: ["review.correctness"]
1342 instructions: "Review logic and correctness."
1343"#;
1344 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1345 let registry = HatRegistry::from_config(&config);
1346 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1347
1348 let security_hat = registry
1350 .get(&ralph_proto::HatId::new("security_reviewer"))
1351 .unwrap();
1352 let arch_hat = registry
1353 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1354 .unwrap();
1355 let active_hats = vec![security_hat, arch_hat];
1356
1357 let prompt = ralph.build_prompt("Events", &active_hats);
1358
1359 assert!(
1361 prompt.contains("### Security Reviewer Instructions"),
1362 "Should include Security Reviewer instructions"
1363 );
1364 assert!(
1365 prompt.contains("Review code for security vulnerabilities"),
1366 "Should include Security Reviewer content"
1367 );
1368 assert!(
1369 prompt.contains("### Architecture Reviewer Instructions"),
1370 "Should include Architecture Reviewer instructions"
1371 );
1372 assert!(
1373 prompt.contains("Review system design and architecture"),
1374 "Should include Architecture Reviewer content"
1375 );
1376
1377 assert!(
1379 !prompt.contains("### Correctness Reviewer Instructions"),
1380 "Should NOT include Correctness Reviewer instructions"
1381 );
1382 }
1383
1384 #[test]
1385 fn test_no_active_hats_no_instructions() {
1386 let yaml = r#"
1388hats:
1389 security_reviewer:
1390 name: "Security Reviewer"
1391 triggers: ["review.security"]
1392 instructions: "Review code for security vulnerabilities."
1393"#;
1394 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1395 let registry = HatRegistry::from_config(&config);
1396 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1397
1398 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1400
1401 let prompt = ralph.build_prompt("Events", &active_hats);
1402
1403 assert!(
1405 !prompt.contains("### Security Reviewer Instructions"),
1406 "Should NOT include instructions when no active hats"
1407 );
1408 assert!(
1409 !prompt.contains("Review code for security vulnerabilities"),
1410 "Should NOT include instructions content when no active hats"
1411 );
1412
1413 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1415 assert!(
1416 prompt.contains("| Hat | Triggers On | Publishes |"),
1417 "Should still have topology table"
1418 );
1419 }
1420
1421 #[test]
1422 fn test_topology_table_only_when_ralph_coordinating() {
1423 let yaml = r#"
1426hats:
1427 security_reviewer:
1428 name: "Security Reviewer"
1429 triggers: ["review.security"]
1430 instructions: "Security instructions."
1431 architecture_reviewer:
1432 name: "Architecture Reviewer"
1433 triggers: ["review.architecture"]
1434 instructions: "Architecture instructions."
1435"#;
1436 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1437 let registry = HatRegistry::from_config(&config);
1438 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1439
1440 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1442
1443 assert!(
1444 prompt_coordinating.contains("## HATS"),
1445 "Should have HATS section when coordinating"
1446 );
1447 assert!(
1448 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1449 "Should have topology table when coordinating"
1450 );
1451 assert!(
1452 prompt_coordinating.contains("```mermaid"),
1453 "Should have Mermaid diagram when coordinating"
1454 );
1455
1456 let security_hat = registry
1458 .get(&ralph_proto::HatId::new("security_reviewer"))
1459 .unwrap();
1460 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1461
1462 assert!(
1463 prompt_active.contains("## ACTIVE HAT"),
1464 "Should have ACTIVE HAT section when hat is active"
1465 );
1466 assert!(
1467 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1468 "Should NOT have topology table when hat is active"
1469 );
1470 assert!(
1471 !prompt_active.contains("```mermaid"),
1472 "Should NOT have Mermaid diagram when hat is active"
1473 );
1474 assert!(
1475 prompt_active.contains("### Security Reviewer Instructions"),
1476 "Should still have the active hat's instructions"
1477 );
1478 }
1479
1480 #[test]
1483 fn test_scratchpad_always_included() {
1484 let config = RalphConfig::default();
1486 let registry = HatRegistry::new();
1487 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1488
1489 let prompt = ralph.build_prompt("", &[]);
1490
1491 assert!(
1492 prompt.contains("### 0b. SCRATCHPAD"),
1493 "Scratchpad section should be included"
1494 );
1495 assert!(
1496 prompt.contains("`.ralph/agent/scratchpad.md`"),
1497 "Scratchpad path should be referenced"
1498 );
1499 assert!(
1500 prompt.contains("Task markers:"),
1501 "Task markers should be documented"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_scratchpad_included_with_memories_enabled() {
1507 let config = RalphConfig::default();
1509 let registry = HatRegistry::new();
1510 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1511 .with_memories_enabled(true);
1512
1513 let prompt = ralph.build_prompt("", &[]);
1514
1515 assert!(
1517 prompt.contains("### 0b. SCRATCHPAD"),
1518 "Scratchpad section should be included even with memories enabled"
1519 );
1520 assert!(
1521 prompt.contains("Task markers:"),
1522 "Task markers should still be documented"
1523 );
1524
1525 assert!(
1527 prompt.contains("### 0c. TASKS"),
1528 "Tasks section should be included when memories enabled"
1529 );
1530 assert!(
1531 prompt.contains("ralph tools task"),
1532 "Tasks CLI commands should be documented"
1533 );
1534 }
1535
1536 #[test]
1537 fn test_no_tasks_section_without_memories() {
1538 let config = RalphConfig::default();
1540 let registry = HatRegistry::new();
1541 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1542 let prompt = ralph.build_prompt("", &[]);
1545
1546 assert!(
1548 !prompt.contains("### 0c. TASKS"),
1549 "Tasks section should NOT be included when memories disabled"
1550 );
1551 }
1552
1553 #[test]
1554 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1555 let config = RalphConfig::default();
1557 let registry = HatRegistry::new();
1558 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1559 .with_memories_enabled(true);
1560
1561 let prompt = ralph.build_prompt("", &[]);
1562
1563 assert!(
1565 prompt.contains("update scratchpad"),
1566 "Workflow should reference scratchpad when memories enabled"
1567 );
1568 assert!(
1570 prompt.contains("ralph tools task"),
1571 "Workflow should reference tasks CLI when memories enabled"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1577 let yaml = r#"
1579hats:
1580 builder:
1581 name: "Builder"
1582 triggers: ["build.task"]
1583 publishes: ["build.done"]
1584"#;
1585 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1586 let registry = HatRegistry::from_config(&config);
1587 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1588 .with_memories_enabled(true);
1589
1590 let prompt = ralph.build_prompt("", &[]);
1591
1592 assert!(
1594 prompt.contains("scratchpad"),
1595 "Multi-hat workflow should reference scratchpad when memories enabled"
1596 );
1597 assert!(
1599 prompt.contains("ralph tools task add"),
1600 "Multi-hat workflow should reference tasks CLI when memories enabled"
1601 );
1602 }
1603
1604 #[test]
1605 fn test_guardrails_adapt_to_memories_mode() {
1606 let config = RalphConfig::default();
1608 let registry = HatRegistry::new();
1609 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1610 .with_memories_enabled(true);
1611
1612 let prompt = ralph.build_prompt("", &[]);
1613
1614 assert!(
1618 prompt.contains("### GUARDRAILS"),
1619 "Guardrails section should be present"
1620 );
1621 }
1622
1623 #[test]
1624 fn test_guardrails_present_without_memories() {
1625 let config = RalphConfig::default();
1627 let registry = HatRegistry::new();
1628 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1629 let prompt = ralph.build_prompt("", &[]);
1632
1633 assert!(
1634 prompt.contains("### GUARDRAILS"),
1635 "Guardrails section should be present"
1636 );
1637 }
1638
1639 #[test]
1642 fn test_task_closure_verification_in_tasks_section() {
1643 let config = RalphConfig::default();
1646 let registry = HatRegistry::new();
1647 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1648 .with_memories_enabled(true);
1649
1650 let prompt = ralph.build_prompt("", &[]);
1651
1652 assert!(
1654 prompt.contains("### 0c. TASKS"),
1655 "Should include TASKS section when memories enabled"
1656 );
1657 assert!(
1658 prompt.contains("CRITICAL"),
1659 "Should include CRITICAL verification note"
1660 );
1661 assert!(
1662 prompt.contains("tests pass"),
1663 "Should mention tests passing"
1664 );
1665 }
1666
1667 #[test]
1668 fn test_workflow_verify_and_commit_step() {
1669 let config = RalphConfig::default();
1671 let registry = HatRegistry::new();
1672 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1673 .with_memories_enabled(true);
1674
1675 let prompt = ralph.build_prompt("", &[]);
1676
1677 assert!(
1679 prompt.contains("### 4. VERIFY & COMMIT"),
1680 "Should have VERIFY & COMMIT step in workflow"
1681 );
1682 assert!(
1683 prompt.contains("run tests and verify"),
1684 "Should require verification"
1685 );
1686 assert!(
1687 prompt.contains("ralph tools task close"),
1688 "Should reference task close command"
1689 );
1690 }
1691
1692 #[test]
1693 fn test_scratchpad_mode_still_has_commit_step() {
1694 let config = RalphConfig::default();
1696 let registry = HatRegistry::new();
1697 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1698 let prompt = ralph.build_prompt("", &[]);
1701
1702 assert!(
1704 prompt.contains("### 4. COMMIT"),
1705 "Should have COMMIT step in workflow"
1706 );
1707 assert!(
1708 prompt.contains("mark the task `[x]`"),
1709 "Should mark task in scratchpad"
1710 );
1711 assert!(
1713 !prompt.contains("### 0c. TASKS"),
1714 "Scratchpad mode should not have TASKS section"
1715 );
1716 }
1717
1718 #[test]
1721 fn test_objective_section_present_with_set_objective() {
1722 let config = RalphConfig::default();
1724 let registry = HatRegistry::new();
1725 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1726 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
1727
1728 let prompt = ralph.build_prompt("", &[]);
1729
1730 assert!(
1731 prompt.contains("## OBJECTIVE"),
1732 "Should have OBJECTIVE section when objective is set"
1733 );
1734 assert!(
1735 prompt.contains("Implement user authentication with JWT tokens"),
1736 "OBJECTIVE should contain the original user prompt"
1737 );
1738 assert!(
1739 prompt.contains("This is your primary goal"),
1740 "OBJECTIVE should emphasize this is the primary goal"
1741 );
1742 }
1743
1744 #[test]
1745 fn test_objective_reinforced_in_done_section() {
1746 let config = RalphConfig::default();
1749 let registry = HatRegistry::new();
1750 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1751 ralph.set_objective("Fix the login bug in auth module".to_string());
1752
1753 let prompt = ralph.build_prompt("", &[]);
1754
1755 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1757 let after_done = &prompt[done_pos..];
1758
1759 assert!(
1760 after_done.contains("Remember your objective"),
1761 "DONE section should remind about objective"
1762 );
1763 assert!(
1764 after_done.contains("Fix the login bug in auth module"),
1765 "DONE section should restate the objective"
1766 );
1767 }
1768
1769 #[test]
1770 fn test_objective_appears_before_pending_events() {
1771 let config = RalphConfig::default();
1773 let registry = HatRegistry::new();
1774 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1775 ralph.set_objective("Build feature X".to_string());
1776
1777 let context = "Event: task.start - Build feature X";
1778 let prompt = ralph.build_prompt(context, &[]);
1779
1780 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
1781 let events_pos = prompt
1782 .find("## PENDING EVENTS")
1783 .expect("Should have PENDING EVENTS");
1784
1785 assert!(
1786 objective_pos < events_pos,
1787 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
1788 objective_pos,
1789 events_pos
1790 );
1791 }
1792
1793 #[test]
1794 fn test_no_objective_when_not_set() {
1795 let config = RalphConfig::default();
1797 let registry = HatRegistry::new();
1798 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1799
1800 let context = "Event: build.done - Build completed successfully";
1801 let prompt = ralph.build_prompt(context, &[]);
1802
1803 assert!(
1804 !prompt.contains("## OBJECTIVE"),
1805 "Should NOT have OBJECTIVE section when objective not set"
1806 );
1807 }
1808
1809 #[test]
1810 fn test_objective_set_correctly() {
1811 let config = RalphConfig::default();
1813 let registry = HatRegistry::new();
1814 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1815 ralph.set_objective("Review this PR for security issues".to_string());
1816
1817 let prompt = ralph.build_prompt("", &[]);
1818
1819 assert!(
1820 prompt.contains("Review this PR for security issues"),
1821 "Should show the stored objective"
1822 );
1823 }
1824
1825 #[test]
1826 fn test_objective_with_events_context() {
1827 let config = RalphConfig::default();
1829 let registry = HatRegistry::new();
1830 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1831 ralph.set_objective("Implement feature Y".to_string());
1832
1833 let context =
1834 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
1835 let prompt = ralph.build_prompt(context, &[]);
1836
1837 assert!(
1838 prompt.contains("## OBJECTIVE"),
1839 "Should have OBJECTIVE section"
1840 );
1841 assert!(
1842 prompt.contains("Implement feature Y"),
1843 "OBJECTIVE should contain the stored objective"
1844 );
1845 }
1846
1847 #[test]
1848 fn test_done_section_without_objective() {
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 prompt = ralph.build_prompt("", &[]);
1855
1856 assert!(prompt.contains("## DONE"), "Should have DONE section");
1857 assert!(
1858 prompt.contains("LOOP_COMPLETE"),
1859 "DONE should mention completion promise"
1860 );
1861 assert!(
1862 !prompt.contains("Remember your objective"),
1863 "Should NOT have objective reinforcement without objective"
1864 );
1865 }
1866
1867 #[test]
1868 fn test_objective_persists_across_iterations() {
1869 let config = RalphConfig::default();
1872 let registry = HatRegistry::new();
1873 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1874 ralph.set_objective("Build a REST API with authentication".to_string());
1875
1876 let context = "Event: build.done - Build completed";
1878 let prompt = ralph.build_prompt(context, &[]);
1879
1880 assert!(
1881 prompt.contains("## OBJECTIVE"),
1882 "OBJECTIVE should persist even without task.start in context"
1883 );
1884 assert!(
1885 prompt.contains("Build a REST API with authentication"),
1886 "Stored objective should appear in later iterations"
1887 );
1888 }
1889
1890 #[test]
1891 fn test_done_section_suppressed_when_hat_active() {
1892 let yaml = r#"
1894hats:
1895 builder:
1896 name: "Builder"
1897 triggers: ["build.task"]
1898 publishes: ["build.done"]
1899 instructions: "Build the code."
1900"#;
1901 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1902 let registry = HatRegistry::from_config(&config);
1903 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1904 ralph.set_objective("Implement feature X".to_string());
1905
1906 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1907 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
1908
1909 assert!(
1910 !prompt.contains("## DONE"),
1911 "DONE section should be suppressed when a hat is active"
1912 );
1913 assert!(
1914 !prompt.contains("LOOP_COMPLETE"),
1915 "Completion promise should NOT appear when a hat is active"
1916 );
1917 assert!(
1919 prompt.contains("## OBJECTIVE"),
1920 "OBJECTIVE should still appear even when hat is active"
1921 );
1922 assert!(
1923 prompt.contains("Implement feature X"),
1924 "Objective content should be visible to active hat"
1925 );
1926 }
1927
1928 #[test]
1929 fn test_done_section_present_when_coordinating() {
1930 let yaml = r#"
1932hats:
1933 builder:
1934 name: "Builder"
1935 triggers: ["build.task"]
1936 publishes: ["build.done"]
1937"#;
1938 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1939 let registry = HatRegistry::from_config(&config);
1940 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1941 ralph.set_objective("Complete the TDD cycle".to_string());
1942
1943 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
1945
1946 assert!(
1947 prompt.contains("## DONE"),
1948 "DONE section should appear when Ralph is coordinating"
1949 );
1950 assert!(
1951 prompt.contains("LOOP_COMPLETE"),
1952 "Completion promise should appear when coordinating"
1953 );
1954 }
1955
1956 #[test]
1957 fn test_objective_in_done_section_when_coordinating() {
1958 let config = RalphConfig::default();
1960 let registry = HatRegistry::new();
1961 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1962 ralph.set_objective("Deploy the application".to_string());
1963
1964 let prompt = ralph.build_prompt("", &[]);
1965
1966 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1967 let after_done = &prompt[done_pos..];
1968
1969 assert!(
1970 after_done.contains("Remember your objective"),
1971 "DONE section should remind about objective when coordinating"
1972 );
1973 assert!(
1974 after_done.contains("Deploy the application"),
1975 "DONE section should contain the objective text"
1976 );
1977 }
1978
1979 #[test]
1982 fn test_event_publishing_guide_with_receivers() {
1983 let yaml = r#"
1986hats:
1987 builder:
1988 name: "Builder"
1989 description: "Builds and tests code"
1990 triggers: ["build.task"]
1991 publishes: ["build.done", "build.blocked"]
1992 confessor:
1993 name: "Confessor"
1994 description: "Produces a ConfessionReport; rewarded for honesty"
1995 triggers: ["build.done"]
1996 publishes: ["confession.done"]
1997"#;
1998 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1999 let registry = HatRegistry::from_config(&config);
2000 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2001
2002 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2004 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2005
2006 assert!(
2008 prompt.contains("### Event Publishing Guide"),
2009 "Should include Event Publishing Guide section"
2010 );
2011 assert!(
2012 prompt.contains("When you publish:"),
2013 "Guide should explain what happens when publishing"
2014 );
2015 assert!(
2017 prompt.contains("`build.done` → Received by: Confessor"),
2018 "Should show Confessor receives build.done"
2019 );
2020 assert!(
2021 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2022 "Should include receiver's description"
2023 );
2024 assert!(
2026 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2027 "Should show Ralph receives orphan events"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_event_publishing_guide_no_publishes() {
2033 let yaml = r#"
2035hats:
2036 observer:
2037 name: "Observer"
2038 description: "Only observes"
2039 triggers: ["events.*"]
2040"#;
2041 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2042 let registry = HatRegistry::from_config(&config);
2043 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2044
2045 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2046 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2047
2048 assert!(
2050 !prompt.contains("### Event Publishing Guide"),
2051 "Should NOT include Event Publishing Guide when hat has no publishes"
2052 );
2053 }
2054
2055 #[test]
2056 fn test_event_publishing_guide_all_orphan_events() {
2057 let yaml = r#"
2059hats:
2060 solo:
2061 name: "Solo"
2062 triggers: ["solo.start"]
2063 publishes: ["solo.done", "solo.failed"]
2064"#;
2065 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2066 let registry = HatRegistry::from_config(&config);
2067 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2068
2069 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2070 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2071
2072 assert!(
2073 prompt.contains("### Event Publishing Guide"),
2074 "Should include guide even for orphan events"
2075 );
2076 assert!(
2077 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2078 "Orphan solo.done should go to Ralph"
2079 );
2080 assert!(
2081 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2082 "Orphan solo.failed should go to Ralph"
2083 );
2084 }
2085
2086 #[test]
2087 fn test_event_publishing_guide_multiple_receivers() {
2088 let yaml = r#"
2090hats:
2091 broadcaster:
2092 name: "Broadcaster"
2093 triggers: ["broadcast.start"]
2094 publishes: ["signal.sent"]
2095 listener1:
2096 name: "Listener1"
2097 description: "First listener"
2098 triggers: ["signal.sent"]
2099 listener2:
2100 name: "Listener2"
2101 description: "Second listener"
2102 triggers: ["signal.sent"]
2103"#;
2104 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2105 let registry = HatRegistry::from_config(&config);
2106 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2107
2108 let broadcaster = registry
2109 .get(&ralph_proto::HatId::new("broadcaster"))
2110 .unwrap();
2111 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2112
2113 assert!(
2114 prompt.contains("### Event Publishing Guide"),
2115 "Should include guide"
2116 );
2117 assert!(
2119 prompt.contains("Listener1 (First listener)"),
2120 "Should list Listener1 as receiver"
2121 );
2122 assert!(
2123 prompt.contains("Listener2 (Second listener)"),
2124 "Should list Listener2 as receiver"
2125 );
2126 }
2127
2128 #[test]
2129 fn test_event_publishing_guide_excludes_self() {
2130 let yaml = r#"
2132hats:
2133 looper:
2134 name: "Looper"
2135 triggers: ["loop.continue", "loop.start"]
2136 publishes: ["loop.continue"]
2137"#;
2138 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2139 let registry = HatRegistry::from_config(&config);
2140 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2141
2142 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2143 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2144
2145 assert!(
2146 prompt.contains("### Event Publishing Guide"),
2147 "Should include guide"
2148 );
2149 assert!(
2151 prompt.contains("`loop.continue` → Received by: Ralph (coordinates next steps)"),
2152 "Self-subscription should be excluded, falling back to Ralph"
2153 );
2154 }
2155
2156 #[test]
2157 fn test_event_publishing_guide_receiver_without_description() {
2158 let yaml = r#"
2160hats:
2161 sender:
2162 name: "Sender"
2163 triggers: ["send.start"]
2164 publishes: ["message.sent"]
2165 receiver:
2166 name: "NoDescReceiver"
2167 triggers: ["message.sent"]
2168"#;
2169 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2170 let registry = HatRegistry::from_config(&config);
2171 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2172
2173 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2174 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2175
2176 assert!(
2177 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2178 "Should show receiver name without parentheses when no description"
2179 );
2180 assert!(
2182 !prompt.contains("NoDescReceiver ()"),
2183 "Should NOT have empty parentheses for receiver without description"
2184 );
2185 }
2186
2187 #[test]
2190 fn test_constraint_lists_valid_events_when_coordinating() {
2191 let yaml = r#"
2194hats:
2195 test_writer:
2196 name: "Test Writer"
2197 triggers: ["tdd.start"]
2198 publishes: ["test.written"]
2199 implementer:
2200 name: "Implementer"
2201 triggers: ["test.written"]
2202 publishes: ["test.passing"]
2203"#;
2204 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2205 let registry = HatRegistry::from_config(&config);
2206 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2207
2208 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2210
2211 assert!(
2213 prompt.contains("**CONSTRAINT:**"),
2214 "Prompt should include CONSTRAINT when coordinating"
2215 );
2216 assert!(
2217 prompt.contains("tdd.start"),
2218 "CONSTRAINT should list tdd.start as valid event"
2219 );
2220 assert!(
2221 prompt.contains("test.written"),
2222 "CONSTRAINT should list test.written as valid event"
2223 );
2224 assert!(
2225 prompt.contains("Publishing other events will have no effect"),
2226 "CONSTRAINT should warn about invalid events"
2227 );
2228 }
2229
2230 #[test]
2231 fn test_no_constraint_when_hat_is_active() {
2232 let yaml = r#"
2235hats:
2236 builder:
2237 name: "Builder"
2238 triggers: ["build.task"]
2239 publishes: ["build.done"]
2240 instructions: "Build the code."
2241"#;
2242 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2243 let registry = HatRegistry::from_config(&config);
2244 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2245
2246 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2248 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2249
2250 assert!(
2252 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2253 "Active hat should NOT have coordinating CONSTRAINT"
2254 );
2255
2256 assert!(
2258 prompt.contains("### Event Publishing Guide"),
2259 "Active hat should have Event Publishing Guide"
2260 );
2261 }
2262
2263 #[test]
2264 fn test_no_constraint_when_no_hats() {
2265 let config = RalphConfig::default();
2267 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2269
2270 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2271
2272 assert!(
2274 !prompt.contains("**CONSTRAINT:**"),
2275 "Solo mode should NOT have CONSTRAINT"
2276 );
2277 }
2278}