1use crate::config::CoreConfig;
6use crate::hat_registry::HatRegistry;
7use ralph_proto::{HatId, 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 pub hat_id: HatId,
44 pub concurrency: u32,
46}
47
48pub struct HatInfo {
50 pub name: String,
51 pub description: String,
52 pub subscribes_to: Vec<String>,
53 pub publishes: Vec<String>,
54 pub instructions: String,
55 pub event_receivers: HashMap<String, Vec<EventReceiver>>,
57 pub disallowed_tools: Vec<String>,
59}
60
61impl HatInfo {
62 pub fn event_publishing_guide(&self) -> Option<String> {
66 if self.publishes.is_empty() {
67 return None;
68 }
69
70 let mut guide = String::from(
71 "### Event Publishing Guide\n\n\
72 You MUST publish exactly ONE event when your work is complete.\n\
73 You MUST use `ralph emit \"<topic>\" \"<brief summary>\"` to publish it.\n\
74 Plain-language summaries do NOT publish events.\n\
75 Publishing hands off to the next hat and starts a fresh iteration with clear context.\n\n\
76 When you publish:\n",
77 );
78
79 for pub_event in &self.publishes {
80 let receivers = self.event_receivers.get(pub_event);
81 let receiver_text = match receivers {
82 Some(r) if !r.is_empty() => r
83 .iter()
84 .map(|recv| {
85 if recv.description.is_empty() {
86 recv.name.clone()
87 } else {
88 format!("{} ({})", recv.name, recv.description)
89 }
90 })
91 .collect::<Vec<_>>()
92 .join(", "),
93 _ => "Ralph (coordinates next steps)".to_string(),
94 };
95 guide.push_str(&format!(
96 "- `{}` → Received by: {}\n",
97 pub_event, receiver_text
98 ));
99 }
100
101 Some(guide)
102 }
103
104 pub fn wave_dispatch_section(&self) -> String {
109 let mut wave_topics: Vec<(&str, &str, u32)> = Vec::new();
111 for pub_event in &self.publishes {
112 if let Some(receivers) = self.event_receivers.get(pub_event) {
113 for recv in receivers {
114 if recv.concurrency > 1 {
115 wave_topics.push((pub_event.as_str(), &recv.name, recv.concurrency));
116 }
117 }
118 }
119 }
120
121 if wave_topics.is_empty() {
122 return String::new();
123 }
124
125 let mut section = String::from("### Wave Dispatch (Parallel Execution)\n\n");
126 section.push_str(
127 "Some downstream hats support parallel execution via waves. \
128 Use `ralph wave emit` to dispatch multiple items for concurrent processing.\n\n",
129 );
130
131 section.push_str("| Topic | Activates | Max Concurrent |\n");
132 section.push_str("|-------|-----------|----------------|\n");
133 for (topic, hat_name, concurrency) in &wave_topics {
134 section.push_str(&format!(
135 "| `{}` | {} | {} |\n",
136 topic, hat_name, concurrency
137 ));
138 }
139 section.push('\n');
140
141 if let Some((topic, _, _)) = wave_topics.first() {
143 section.push_str("**Usage:**\n```bash\n");
144 section.push_str(&format!(
145 "ralph wave emit {} --payloads \"item1\" \"item2\" \"item3\"\n",
146 topic
147 ));
148 section.push_str("```\n\n");
149 }
150
151 section
152 }
153}
154
155impl HatTopology {
156 pub fn from_registry(registry: &HatRegistry) -> Self {
158 let hats = registry
159 .all()
160 .map(|hat| {
161 let event_receivers: HashMap<String, Vec<EventReceiver>> = hat
163 .publishes
164 .iter()
165 .map(|pub_topic| {
166 let receivers: Vec<EventReceiver> = registry
167 .subscribers(pub_topic)
168 .into_iter()
169 .map(|h| {
170 let concurrency = registry
171 .get_config(&h.id)
172 .map(|c| c.concurrency)
173 .unwrap_or(1);
174 EventReceiver {
175 name: h.name.clone(),
176 description: h.description.clone(),
177 hat_id: h.id.clone(),
178 concurrency,
179 }
180 })
181 .collect();
182 (pub_topic.as_str().to_string(), receivers)
183 })
184 .collect();
185
186 let disallowed_tools = registry
187 .get_config(&hat.id)
188 .map(|c| c.disallowed_tools.clone())
189 .unwrap_or_default();
190
191 HatInfo {
192 name: hat.name.clone(),
193 description: hat.description.clone(),
194 subscribes_to: hat
195 .subscriptions
196 .iter()
197 .map(|t| t.as_str().to_string())
198 .collect(),
199 publishes: hat
200 .publishes
201 .iter()
202 .map(|t| t.as_str().to_string())
203 .collect(),
204 instructions: hat.instructions.clone(),
205 event_receivers,
206 disallowed_tools,
207 }
208 })
209 .collect();
210
211 Self { hats }
212 }
213}
214
215impl HatlessRalph {
216 pub fn new(
224 completion_promise: impl Into<String>,
225 core: CoreConfig,
226 registry: &HatRegistry,
227 starting_event: Option<String>,
228 ) -> Self {
229 let hat_topology = if registry.is_empty() {
230 None
231 } else {
232 Some(HatTopology::from_registry(registry))
233 };
234
235 Self {
236 completion_promise: completion_promise.into(),
237 core,
238 hat_topology,
239 starting_event,
240 memories_enabled: false, objective: None,
242 skill_index: String::new(),
243 robot_guidance: Vec::new(),
244 }
245 }
246
247 pub fn with_memories_enabled(mut self, enabled: bool) -> Self {
252 self.memories_enabled = enabled;
253 self
254 }
255
256 pub fn with_skill_index(mut self, index: String) -> Self {
261 self.skill_index = index;
262 self
263 }
264
265 pub fn set_objective(&mut self, objective: String) {
271 self.objective = Some(objective);
272 }
273
274 pub fn set_robot_guidance(&mut self, guidance: Vec<String>) {
280 self.robot_guidance = guidance;
281 }
282
283 pub fn clear_robot_guidance(&mut self) {
287 self.robot_guidance.clear();
288 }
289
290 fn collect_robot_guidance(&self) -> String {
295 if self.robot_guidance.is_empty() {
296 return String::new();
297 }
298
299 let mut section = String::from("## ROBOT GUIDANCE\n\n");
300
301 if self.robot_guidance.len() == 1 {
302 section.push_str(&self.robot_guidance[0]);
303 } else {
304 for (i, guidance) in self.robot_guidance.iter().enumerate() {
305 section.push_str(&format!("{}. {}\n", i + 1, guidance));
306 }
307 }
308
309 section.push_str("\n\n");
310
311 section
312 }
313
314 pub fn build_prompt(&self, context: &str, active_hats: &[&ralph_proto::Hat]) -> String {
322 let mut prompt = self.core_prompt();
323
324 if !self.skill_index.is_empty() {
326 prompt.push_str(&self.skill_index);
327 prompt.push('\n');
328 }
329
330 if let Some(ref obj) = self.objective {
332 prompt.push_str(&self.objective_section(obj));
333 }
334
335 let guidance = self.collect_robot_guidance();
337 if !guidance.is_empty() {
338 prompt.push_str(&guidance);
339 }
340
341 if !context.trim().is_empty() {
343 prompt.push_str("## PENDING EVENTS\n\n");
344 prompt.push_str("You MUST handle these events in this iteration:\n\n");
345 prompt.push_str(context);
346 prompt.push_str("\n\n");
347 }
348
349 let has_custom_workflow = active_hats
352 .iter()
353 .any(|h| !h.instructions.trim().is_empty());
354
355 if !has_custom_workflow {
356 prompt.push_str(&self.workflow_section());
357 }
358
359 if let Some(topology) = &self.hat_topology {
360 prompt.push_str(&self.hats_section(topology, active_hats));
361 }
362
363 prompt.push_str(&self.event_writing_section());
364
365 if active_hats.is_empty() {
368 prompt.push_str(&self.done_section(self.objective.as_deref()));
369 }
370
371 prompt
372 }
373
374 fn objective_section(&self, objective: &str) -> String {
376 format!(
377 r"## OBJECTIVE
378
379**This is your primary goal. All work must advance this objective.**
380
381> {objective}
382
383You MUST keep this objective in mind throughout the iteration.
384You MUST NOT get distracted by workflow mechanics — they serve this goal.
385
386",
387 objective = objective
388 )
389 }
390
391 pub fn should_handle(&self, _topic: &Topic) -> bool {
393 true
394 }
395
396 fn is_fresh_start(&self) -> bool {
401 if self.starting_event.is_none() {
403 return false;
404 }
405
406 let path = Path::new(&self.core.scratchpad);
408 !path.exists()
409 }
410
411 fn core_prompt(&self) -> String {
412 let guardrails = self
414 .core
415 .guardrails
416 .iter()
417 .enumerate()
418 .map(|(i, g)| {
419 let guardrail = if self.memories_enabled && g.contains("scratchpad is memory") {
421 g.replace(
422 "scratchpad is memory",
423 "save learnings to memories for next time",
424 )
425 } else {
426 g.clone()
427 };
428 format!("{}. {guardrail}", 999 + i)
429 })
430 .collect::<Vec<_>>()
431 .join("\n");
432
433 let mut prompt = if self.memories_enabled {
434 r"
435### 0a. ORIENTATION
436You are Ralph. You are running in a loop. You have fresh context each iteration.
437You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
438
439**First thing every iteration:**
4401. Review your `<scratchpad>` (auto-injected above) for context on your thinking
4412. Review your `<ready-tasks>` (auto-injected above) to see what work exists
4423. If tasks exist, pick one. If not, create them from your plan.
443"
444 } else {
445 r"
446### 0a. ORIENTATION
447You are Ralph. You are running in a loop. You have fresh context each iteration.
448You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
449"
450 }
451 .to_string();
452
453 prompt.push_str(&format!(
455 r"### 0b. SCRATCHPAD
456`{scratchpad}` is your thinking journal for THIS objective.
457Its content is auto-injected in `<scratchpad>` tags at the top of your context each iteration.
458
459**Always append** new entries to the end of the file (most recent = bottom).
460
461**Use for:**
462- Current understanding and reasoning
463- Analysis notes and decisions
464- Plan narrative (the 'why' behind your approach)
465
466**Do NOT use for:**
467- Tracking what tasks exist or their status (use `ralph tools task`)
468- Checklists or todo lists (use `ralph tools task ensure` when a stable key exists, otherwise `ralph tools task add`)
469
470",
471 scratchpad = self.core.scratchpad,
472 ));
473
474 prompt.push_str(&format!(
480 "### STATE MANAGEMENT\n\n\
481**Scratchpad** (`{scratchpad}`) — Your thinking:\n\
482- Current understanding and reasoning\n\
483- Analysis notes, decisions, plan narrative\n\
484- NOT for checklists or status tracking\n\
485\n\
486**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
487- Analysis and temporary notes\n\
488- Read when relevant\n\
489\n\
490**Tool reliability rule:** Assume the workflow commands are available when the loop is already running and use the task-specific command you actually need.\n\
491The loop sets `$RALPH_BIN` to the current Ralph executable. Prefer `$RALPH_BIN emit ...` and `$RALPH_BIN tools ...` when you need a direct command form.\n\
492Do not spend turns on shell or tool-availability diagnosis unless the task is explicitly about the runtime environment.\n\
493Do NOT infer failure from empty or terse stdout alone. Verify the intended side effect in the task/event state or in the files and artifacts the command should have changed.\n\
494Keep temporary artifacts where later steps can still inspect them, such as a repo-local `logs/` directory or `/var/tmp` when needed.\n\
495\n",
496 scratchpad = self.core.scratchpad,
497 ));
498
499 if let Ok(entries) = std::fs::read_dir(".ralph/agent") {
501 let md_files: Vec<String> = entries
502 .filter_map(|e| e.ok())
503 .filter_map(|e| {
504 let path = e.path();
505 let fname = path.file_name().and_then(|s| s.to_str());
506 if path.extension().and_then(|s| s.to_str()) == Some("md")
507 && fname != Some("memories.md")
508 && fname != Some("scratchpad.md")
509 {
510 path.file_name()
511 .and_then(|s| s.to_str())
512 .map(|s| s.to_string())
513 } else {
514 None
515 }
516 })
517 .collect();
518
519 if !md_files.is_empty() {
520 prompt.push_str("### AVAILABLE CONTEXT FILES\n\n");
521 prompt.push_str(
522 "Context files in `.ralph/agent/` (read if relevant to current work):\n",
523 );
524 for file in md_files {
525 prompt.push_str(&format!("- `.ralph/agent/{}`\n", file));
526 }
527 prompt.push('\n');
528 }
529 }
530
531 prompt.push_str(&format!(
532 r"### GUARDRAILS
533{guardrails}
534
535",
536 guardrails = guardrails,
537 ));
538
539 prompt
540 }
541
542 fn workflow_section(&self) -> String {
543 if self.hat_topology.is_some() {
545 if self.is_fresh_start() {
547 return format!(
549 r#"## WORKFLOW
550
551**FAST PATH**: You MUST publish `{}` immediately to start the hat workflow.
552You MUST use `ralph emit "{}" "<brief handoff>"` and stop immediately.
553You MUST NOT plan or analyze — delegate now.
554
555"#,
556 self.starting_event.as_ref().unwrap(),
557 self.starting_event.as_ref().unwrap()
558 );
559 }
560
561 if self.memories_enabled {
563 format!(
565 r"## WORKFLOW
566
567### 1. PLAN
568You MUST update `{scratchpad}` with your understanding and plan.
569You MUST check `<ready-tasks>` first.
570You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
571You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
572If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
573
574### 2. DELEGATE
575You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
576Plain-language summaries do NOT hand off work.
577You MUST NOT do implementation work — delegation is your only job.
578
579",
580 scratchpad = self.core.scratchpad
581 )
582 } else {
583 format!(
585 r"## WORKFLOW
586
587### 1. PLAN
588You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
589
590### 2. DELEGATE
591You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
592Plain-language summaries do NOT hand off work.
593You MUST NOT do implementation work — delegation is your only job.
594
595",
596 scratchpad = self.core.scratchpad
597 )
598 }
599 } else {
600 if self.memories_enabled {
602 format!(
604 r"## WORKFLOW
605
606### 1. Study the prompt.
607You MUST study, explore, and research what needs to be done.
608
609### 2. PLAN
610You MUST update `{scratchpad}` with your understanding and plan.
611You MUST check `<ready-tasks>` first.
612You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
613You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
614If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
615
616### 3. IMPLEMENT
617You MUST pick exactly ONE task from `<ready-tasks>` to implement.
618You MUST mark it in progress with `ralph tools task start <id>` before implementation.
619
620### 4. VERIFY & COMMIT
621You MUST run tests and verify the implementation works.
622If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
623You SHOULD try at least one realistic failure-path or adversarial input during verification.
624If this turn is likely to take more than a few minutes, you SHOULD send `ralph tools interact progress`.
625You MUST commit after verification passes - one commit per task.
626You SHOULD run `git diff --cached` to review staged changes before committing.
627You MUST close the task with `ralph tools task close <id>` AFTER commit.
628You SHOULD save learnings to memories with `ralph tools memory add`.
629If a command fails, a dependency is missing, or work becomes blocked and you cannot resolve it in this iteration, you MUST record a `fix` memory and `ralph tools task fail <id>` or `ralph tools task reopen <id>` before stopping.
630You MUST update scratchpad with what you learned (tasks track what remains).
631
632### 5. EXIT
633You MUST exit after completing ONE task.
634
635",
636 scratchpad = self.core.scratchpad
637 )
638 } else {
639 format!(
641 r"## WORKFLOW
642
643### 1. Study the prompt.
644You MUST study, explore, and research what needs to be done.
645You MAY use parallel subagents (up to 10) for searches.
646
647### 2. PLAN
648You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
649
650### 3. IMPLEMENT
651You MUST pick exactly ONE task to implement.
652You MUST NOT use more than 1 subagent for build/tests.
653
654### 4. COMMIT
655If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
656You SHOULD try at least one realistic failure-path or adversarial input during verification.
657You MUST commit after completing each atomic unit of work.
658You MUST capture the why, not just the what.
659You SHOULD run `git diff` before committing to review changes.
660You MUST mark the task `[x]` in scratchpad when complete.
661
662### 5. REPEAT
663You MUST continue until all tasks are `[x]` or `[~]`.
664
665",
666 scratchpad = self.core.scratchpad
667 )
668 }
669 }
670 }
671
672 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
673 let mut section = String::new();
674
675 if active_hats.is_empty() {
678 section.push_str("## HATS\n\nDelegate via events.\n\n");
680
681 if let Some(ref starting_event) = self.starting_event {
683 section.push_str(&format!(
684 "**After coordination, publish `{}` to start the workflow.**\n\n",
685 starting_event
686 ));
687 }
688
689 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
693 let mut ralph_publishes: Vec<&str> = Vec::new();
694
695 for hat in &topology.hats {
696 for pub_event in &hat.publishes {
697 if !ralph_triggers.contains(&pub_event.as_str()) {
698 ralph_triggers.push(pub_event.as_str());
699 }
700 }
701 for sub_event in &hat.subscribes_to {
702 if !ralph_publishes.contains(&sub_event.as_str()) {
703 ralph_publishes.push(sub_event.as_str());
704 }
705 }
706 }
707
708 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
710 section.push_str("|-----|-------------|----------|-------------|\n");
711
712 section.push_str(&format!(
714 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
715 ralph_triggers.join(", "),
716 ralph_publishes.join(", ")
717 ));
718
719 for hat in &topology.hats {
721 let subscribes = hat.subscribes_to.join(", ");
722 let publishes = hat.publishes.join(", ");
723 section.push_str(&format!(
724 "| {} | {} | {} | {} |\n",
725 hat.name, subscribes, publishes, hat.description
726 ));
727 }
728
729 section.push('\n');
730
731 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
733 section.push('\n');
734
735 if !ralph_publishes.is_empty() {
737 section.push_str(&format!(
738 "**CONSTRAINT:** You MUST only publish events from this list: `{}`\n\
739 Publishing other events will have no effect - no hat will receive them.\n\n",
740 ralph_publishes.join("`, `")
741 ));
742 }
743
744 self.validate_topology_reachability(topology);
746 } else {
747 section.push_str("## ACTIVE HAT\n\n");
749
750 for active_hat in active_hats {
751 let hat_info = topology.hats.iter().find(|h| h.name == active_hat.name);
753
754 if !active_hat.instructions.trim().is_empty() {
755 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
756 section.push_str(&active_hat.instructions);
757 if !active_hat.instructions.ends_with('\n') {
758 section.push('\n');
759 }
760 section.push('\n');
761 }
762
763 if let Some(guide) = hat_info.and_then(|info| info.event_publishing_guide()) {
765 section.push_str(&guide);
766 section.push('\n');
767 }
768
769 if let Some(info) = hat_info {
771 let wave_dispatch = info.wave_dispatch_section();
772 if !wave_dispatch.is_empty() {
773 section.push_str(&wave_dispatch);
774 }
775 }
776
777 if let Some(info) = hat_info
779 && !info.disallowed_tools.is_empty()
780 {
781 section.push_str("### TOOL RESTRICTIONS\n\n");
782 section.push_str("You MUST NOT use these tools in this hat:\n");
783 for tool in &info.disallowed_tools {
784 section.push_str(&format!("- **{}** — blocked for this hat\n", tool));
785 }
786 section.push_str(
787 "\nUsing a restricted tool is a scope violation. \
788 File modifications are audited after each iteration.\n\n",
789 );
790 }
791 }
792 }
793
794 section
795 }
796
797 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
799 let node_ids: std::collections::HashMap<&str, String> = topology
801 .hats
802 .iter()
803 .map(|h| {
804 let id = h
805 .name
806 .chars()
807 .filter(|c| c.is_alphanumeric())
808 .collect::<String>();
809 (h.name.as_str(), id)
810 })
811 .collect();
812
813 let mut diagram = String::from("```mermaid\nflowchart LR\n");
814
815 diagram.push_str(" task.start((task.start)) --> Ralph\n");
817
818 for hat in &topology.hats {
820 let node_id = &node_ids[hat.name.as_str()];
821 for trigger in &hat.subscribes_to {
822 if ralph_publishes.contains(&trigger.as_str()) {
823 if node_id == &hat.name {
824 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
825 } else {
826 diagram.push_str(&format!(
827 " Ralph -->|{}| {}[{}]\n",
828 trigger, node_id, hat.name
829 ));
830 }
831 }
832 }
833 }
834
835 for hat in &topology.hats {
837 let node_id = &node_ids[hat.name.as_str()];
838 for pub_event in &hat.publishes {
839 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
840 }
841 }
842
843 for source_hat in &topology.hats {
845 let source_id = &node_ids[source_hat.name.as_str()];
846 for pub_event in &source_hat.publishes {
847 for target_hat in &topology.hats {
848 if target_hat.name != source_hat.name
849 && target_hat.subscribes_to.contains(pub_event)
850 {
851 let target_id = &node_ids[target_hat.name.as_str()];
852 diagram.push_str(&format!(
853 " {} -->|{}| {}\n",
854 source_id, pub_event, target_id
855 ));
856 }
857 }
858 }
859 }
860
861 diagram.push_str("```\n");
862 diagram
863 }
864
865 fn validate_topology_reachability(&self, topology: &HatTopology) {
868 use std::collections::HashSet;
869 use tracing::warn;
870
871 let mut reachable_events: HashSet<&str> = HashSet::new();
873 reachable_events.insert("task.start");
874
875 for hat in &topology.hats {
877 for trigger in &hat.subscribes_to {
878 reachable_events.insert(trigger.as_str());
879 }
880 }
881
882 for hat in &topology.hats {
884 for pub_event in &hat.publishes {
885 reachable_events.insert(pub_event.as_str());
886 }
887 }
888
889 for hat in &topology.hats {
891 let hat_reachable = hat
892 .subscribes_to
893 .iter()
894 .any(|t| reachable_events.contains(t.as_str()));
895 if !hat_reachable {
896 warn!(
897 hat = %hat.name,
898 triggers = ?hat.subscribes_to,
899 "Hat has triggers that are never published - it may be unreachable"
900 );
901 }
902 }
903 }
904
905 fn event_writing_section(&self) -> String {
906 let detailed_output_hint = format!(
908 "You SHOULD write detailed output to `{}` and emit only a brief event.",
909 self.core.scratchpad
910 );
911
912 format!(
913 r#"## EVENT WRITING
914
915Events are routing signals, not data transport. You SHOULD keep payloads brief.
916
917You MUST use `ralph emit` to write events (handles JSON escaping correctly):
918```bash
919ralph emit "build.done" "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass"
920ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
921```
922
923You MUST NOT use echo/cat to write events because shell escaping breaks JSON.
924
925{detailed_output_hint}
926
927**Constraints:**
928- You MUST stop working after publishing an event because a new iteration will start with fresh context
929- You MUST NOT continue with additional work after publishing because the next iteration handles it with the appropriate hat persona
930"#,
931 detailed_output_hint = detailed_output_hint
932 )
933 }
934
935 fn done_section(&self, objective: Option<&str>) -> String {
936 let mut section = if self.hat_topology.is_some() {
937 format!(
938 r"## DONE
939
940You MUST emit the completion event `{}` via `ralph emit` when the objective is complete and all tasks are done.
941Stdout text does NOT end the loop in coordinated mode.
942",
943 self.completion_promise
944 )
945 } else {
946 format!(
947 r"## DONE
948
949You MUST output the literal completion promise `{}` as the final non-empty line when the objective is complete and all tasks are done.
950You MUST NOT substitute a prose summary for `{}`.
951You MUST NOT print any text after `{}`.
952",
953 self.completion_promise, self.completion_promise, self.completion_promise
954 )
955 };
956
957 if self.memories_enabled {
959 section.push_str(
960 r"
961**Before declaring completion:**
9621. Run `ralph tools task list` to check for any remaining non-terminal tasks
9632. If any tasks are still open or in progress, close, fail, or reopen them first
9643. Only declare completion when YOUR tasks for this objective are all terminal
965
966Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
967
968You MUST NOT declare completion while tasks remain open.
969",
970 );
971 }
972
973 if let Some(obj) = objective {
975 section.push_str(&format!(
976 r"
977**Remember your objective:**
978> {}
979
980You MUST NOT declare completion until this objective is fully satisfied.
981",
982 obj
983 ));
984 }
985
986 section
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993 use crate::config::RalphConfig;
994
995 #[test]
996 fn test_prompt_without_hats() {
997 let config = RalphConfig::default();
998 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1000
1001 let prompt = ralph.build_prompt("", &[]);
1002
1003 assert!(prompt.contains(
1005 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1006 ));
1007
1008 assert!(prompt.contains("### 0a. ORIENTATION"));
1010 assert!(prompt.contains("MUST complete only one atomic task"));
1011
1012 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1014 assert!(prompt.contains("auto-injected"));
1015 assert!(prompt.contains("**Always append**"));
1016
1017 assert!(prompt.contains("## WORKFLOW"));
1019 assert!(prompt.contains("### 1. Study the prompt"));
1020 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
1021 assert!(prompt.contains("### 2. PLAN"));
1022 assert!(prompt.contains("### 3. IMPLEMENT"));
1023 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
1024 assert!(prompt.contains("### 4. COMMIT"));
1025 assert!(prompt.contains("You MUST capture the why"));
1026 assert!(prompt.contains("### 5. REPEAT"));
1027
1028 assert!(!prompt.contains("## HATS"));
1030
1031 assert!(prompt.contains("## EVENT WRITING"));
1033 assert!(prompt.contains("You MUST use `ralph emit`"));
1034 assert!(prompt.contains("You MUST NOT use echo/cat"));
1035 assert!(prompt.contains("LOOP_COMPLETE"));
1036 }
1037
1038 #[test]
1039 fn test_prompt_with_hats() {
1040 let yaml = r#"
1042hats:
1043 planner:
1044 name: "Planner"
1045 triggers: ["planning.start", "build.done", "build.blocked"]
1046 publishes: ["build.task"]
1047 builder:
1048 name: "Builder"
1049 triggers: ["build.task"]
1050 publishes: ["build.done", "build.blocked"]
1051"#;
1052 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1053 let registry = HatRegistry::from_config(&config);
1054 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1056
1057 let prompt = ralph.build_prompt("", &[]);
1058
1059 assert!(prompt.contains(
1061 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1062 ));
1063
1064 assert!(prompt.contains("### 0a. ORIENTATION"));
1066 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1067
1068 assert!(prompt.contains("## WORKFLOW"));
1070 assert!(prompt.contains("### 1. PLAN"));
1071 assert!(
1072 prompt.contains("### 2. DELEGATE"),
1073 "Multi-hat mode should have DELEGATE step"
1074 );
1075 assert!(
1076 !prompt.contains("### 3. IMPLEMENT"),
1077 "Multi-hat mode should NOT tell Ralph to implement"
1078 );
1079 assert!(
1080 prompt.contains("You MUST stop working after publishing"),
1081 "Should explicitly tell Ralph to stop after publishing event"
1082 );
1083
1084 assert!(prompt.contains("## HATS"));
1086 assert!(prompt.contains("Delegate via events"));
1087 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
1088
1089 assert!(prompt.contains("## EVENT WRITING"));
1091 assert!(prompt.contains("LOOP_COMPLETE"));
1092 }
1093
1094 #[test]
1095 fn test_should_handle_always_true() {
1096 let config = RalphConfig::default();
1097 let registry = HatRegistry::new();
1098 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1099
1100 assert!(ralph.should_handle(&Topic::new("any.topic")));
1101 assert!(ralph.should_handle(&Topic::new("build.task")));
1102 assert!(ralph.should_handle(&Topic::new("unknown.event")));
1103 }
1104
1105 #[test]
1106 fn test_rfc2119_patterns_present() {
1107 let config = RalphConfig::default();
1108 let registry = HatRegistry::new();
1109 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1110
1111 let prompt = ralph.build_prompt("", &[]);
1112
1113 assert!(
1115 prompt.contains("You MUST study"),
1116 "Should use RFC2119 MUST with 'study' verb"
1117 );
1118 assert!(
1119 prompt.contains("You MUST complete only one atomic task"),
1120 "Should have RFC2119 MUST complete atomic task constraint"
1121 );
1122 assert!(
1123 prompt.contains("You MAY use parallel subagents"),
1124 "Should mention parallel subagents with MAY"
1125 );
1126 assert!(
1127 prompt.contains("You MUST NOT use more than 1 subagent"),
1128 "Should limit to 1 subagent for builds with MUST NOT"
1129 );
1130 assert!(
1131 prompt.contains("You MUST capture the why"),
1132 "Should emphasize 'why' in commits with MUST"
1133 );
1134
1135 assert!(
1137 prompt.contains("### GUARDRAILS"),
1138 "Should have guardrails section"
1139 );
1140 assert!(
1141 prompt.contains("999."),
1142 "Guardrails should use high numbers"
1143 );
1144 }
1145
1146 #[test]
1147 fn test_scratchpad_format_documented() {
1148 let config = RalphConfig::default();
1149 let registry = HatRegistry::new();
1150 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1151
1152 let prompt = ralph.build_prompt("", &[]);
1153
1154 assert!(prompt.contains("auto-injected"));
1156 assert!(prompt.contains("**Always append**"));
1157 }
1158
1159 #[test]
1160 fn test_starting_event_in_prompt() {
1161 let yaml = r#"
1163hats:
1164 tdd_writer:
1165 name: "TDD Writer"
1166 triggers: ["tdd.start"]
1167 publishes: ["test.written"]
1168"#;
1169 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1170 let registry = HatRegistry::from_config(&config);
1171 let ralph = HatlessRalph::new(
1172 "LOOP_COMPLETE",
1173 config.core.clone(),
1174 ®istry,
1175 Some("tdd.start".to_string()),
1176 );
1177
1178 let prompt = ralph.build_prompt("", &[]);
1179
1180 assert!(
1182 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1183 "Prompt should include starting_event delegation instruction"
1184 );
1185 }
1186
1187 #[test]
1188 fn test_no_starting_event_instruction_when_none() {
1189 let yaml = r#"
1191hats:
1192 some_hat:
1193 name: "Some Hat"
1194 triggers: ["some.event"]
1195"#;
1196 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1197 let registry = HatRegistry::from_config(&config);
1198 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1199
1200 let prompt = ralph.build_prompt("", &[]);
1201
1202 assert!(
1204 !prompt.contains("After coordination, publish"),
1205 "Prompt should NOT include starting_event delegation when None"
1206 );
1207 }
1208
1209 #[test]
1210 fn test_hat_instructions_propagated_to_prompt() {
1211 let yaml = r#"
1214hats:
1215 tdd_writer:
1216 name: "TDD Writer"
1217 triggers: ["tdd.start"]
1218 publishes: ["test.written"]
1219 instructions: |
1220 You are a Test-Driven Development specialist.
1221 Always write failing tests before implementation.
1222 Focus on edge cases and error handling.
1223"#;
1224 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1225 let registry = HatRegistry::from_config(&config);
1226 let ralph = HatlessRalph::new(
1227 "LOOP_COMPLETE",
1228 config.core.clone(),
1229 ®istry,
1230 Some("tdd.start".to_string()),
1231 );
1232
1233 let tdd_writer = registry
1235 .get(&ralph_proto::HatId::new("tdd_writer"))
1236 .unwrap();
1237 let prompt = ralph.build_prompt("", &[tdd_writer]);
1238
1239 assert!(
1241 prompt.contains("### TDD Writer Instructions"),
1242 "Prompt should include hat instructions section header"
1243 );
1244 assert!(
1245 prompt.contains("Test-Driven Development specialist"),
1246 "Prompt should include actual instructions content"
1247 );
1248 assert!(
1249 prompt.contains("Always write failing tests"),
1250 "Prompt should include full instructions"
1251 );
1252 }
1253
1254 #[test]
1255 fn test_empty_instructions_not_rendered() {
1256 let yaml = r#"
1258hats:
1259 builder:
1260 name: "Builder"
1261 triggers: ["build.task"]
1262 publishes: ["build.done"]
1263"#;
1264 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1265 let registry = HatRegistry::from_config(&config);
1266 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1267
1268 let prompt = ralph.build_prompt("", &[]);
1269
1270 assert!(
1272 !prompt.contains("### Builder Instructions"),
1273 "Prompt should NOT include instructions section for hat with empty instructions"
1274 );
1275 }
1276
1277 #[test]
1278 fn test_multiple_hats_with_instructions() {
1279 let yaml = r#"
1281hats:
1282 planner:
1283 name: "Planner"
1284 triggers: ["planning.start"]
1285 publishes: ["build.task"]
1286 instructions: "Plan carefully before implementation."
1287 builder:
1288 name: "Builder"
1289 triggers: ["build.task"]
1290 publishes: ["build.done"]
1291 instructions: "Focus on clean, testable code."
1292"#;
1293 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1294 let registry = HatRegistry::from_config(&config);
1295 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1296
1297 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1299 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1300 let prompt = ralph.build_prompt("", &[planner, builder]);
1301
1302 assert!(
1304 prompt.contains("### Planner Instructions"),
1305 "Prompt should include Planner instructions section"
1306 );
1307 assert!(
1308 prompt.contains("Plan carefully before implementation"),
1309 "Prompt should include Planner instructions content"
1310 );
1311 assert!(
1312 prompt.contains("### Builder Instructions"),
1313 "Prompt should include Builder instructions section"
1314 );
1315 assert!(
1316 prompt.contains("Focus on clean, testable code"),
1317 "Prompt should include Builder instructions content"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_fast_path_with_starting_event() {
1323 let yaml = r#"
1326core:
1327 scratchpad: "/nonexistent/path/scratchpad.md"
1328hats:
1329 tdd_writer:
1330 name: "TDD Writer"
1331 triggers: ["tdd.start"]
1332 publishes: ["test.written"]
1333"#;
1334 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1335 let registry = HatRegistry::from_config(&config);
1336 let ralph = HatlessRalph::new(
1337 "LOOP_COMPLETE",
1338 config.core.clone(),
1339 ®istry,
1340 Some("tdd.start".to_string()),
1341 );
1342
1343 let prompt = ralph.build_prompt("", &[]);
1344
1345 assert!(
1347 prompt.contains("FAST PATH"),
1348 "Prompt should indicate fast path when starting_event set and no scratchpad"
1349 );
1350 assert!(
1351 prompt.contains("You MUST publish `tdd.start` immediately"),
1352 "Prompt should instruct immediate event publishing with MUST"
1353 );
1354 assert!(
1355 prompt.contains("ralph emit \"tdd.start\""),
1356 "Fast path should require explicit event emission"
1357 );
1358 assert!(
1359 !prompt.contains("### 1. PLAN"),
1360 "Fast path should skip PLAN step"
1361 );
1362 }
1363
1364 #[test]
1365 fn test_events_context_included_in_prompt() {
1366 let config = RalphConfig::default();
1370 let registry = HatRegistry::new();
1371 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1372
1373 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1374[build.done] Build completed successfully";
1375
1376 let prompt = ralph.build_prompt(events_context, &[]);
1377
1378 assert!(
1379 prompt.contains("## PENDING EVENTS"),
1380 "Prompt should contain PENDING EVENTS section"
1381 );
1382 assert!(
1383 prompt.contains("Review this code for security vulnerabilities"),
1384 "Prompt should contain the user's task"
1385 );
1386 assert!(
1387 prompt.contains("Build completed successfully"),
1388 "Prompt should contain all events from context"
1389 );
1390 }
1391
1392 #[test]
1393 fn test_empty_context_no_pending_events_section() {
1394 let config = RalphConfig::default();
1398 let registry = HatRegistry::new();
1399 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1400
1401 let prompt = ralph.build_prompt("", &[]);
1402
1403 assert!(
1404 !prompt.contains("## PENDING EVENTS"),
1405 "Empty context should not produce PENDING EVENTS section"
1406 );
1407 }
1408
1409 #[test]
1410 fn test_whitespace_only_context_no_pending_events_section() {
1411 let config = RalphConfig::default();
1415 let registry = HatRegistry::new();
1416 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1417
1418 let prompt = ralph.build_prompt(" \n\t ", &[]);
1419
1420 assert!(
1421 !prompt.contains("## PENDING EVENTS"),
1422 "Whitespace-only context should not produce PENDING EVENTS section"
1423 );
1424 }
1425
1426 #[test]
1427 fn test_events_section_before_workflow() {
1428 let config = RalphConfig::default();
1432 let registry = HatRegistry::new();
1433 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1434
1435 let events_context = "[task.start] Implement feature X";
1436 let prompt = ralph.build_prompt(events_context, &[]);
1437
1438 let events_pos = prompt
1439 .find("## PENDING EVENTS")
1440 .expect("Should have PENDING EVENTS");
1441 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1442
1443 assert!(
1444 events_pos < workflow_pos,
1445 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1446 events_pos,
1447 workflow_pos
1448 );
1449 }
1450
1451 #[test]
1454 fn test_only_active_hat_instructions_included() {
1455 let yaml = r#"
1457hats:
1458 security_reviewer:
1459 name: "Security Reviewer"
1460 triggers: ["review.security"]
1461 instructions: "Review code for security vulnerabilities."
1462 architecture_reviewer:
1463 name: "Architecture Reviewer"
1464 triggers: ["review.architecture"]
1465 instructions: "Review system design and architecture."
1466 correctness_reviewer:
1467 name: "Correctness Reviewer"
1468 triggers: ["review.correctness"]
1469 instructions: "Review logic and correctness."
1470"#;
1471 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1472 let registry = HatRegistry::from_config(&config);
1473 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1474
1475 let security_hat = registry
1477 .get(&ralph_proto::HatId::new("security_reviewer"))
1478 .unwrap();
1479 let active_hats = vec![security_hat];
1480
1481 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1482
1483 assert!(
1485 prompt.contains("### Security Reviewer Instructions"),
1486 "Should include Security Reviewer instructions section"
1487 );
1488 assert!(
1489 prompt.contains("Review code for security vulnerabilities"),
1490 "Should include Security Reviewer instructions content"
1491 );
1492
1493 assert!(
1495 !prompt.contains("### Architecture Reviewer Instructions"),
1496 "Should NOT include Architecture Reviewer instructions"
1497 );
1498 assert!(
1499 !prompt.contains("Review system design and architecture"),
1500 "Should NOT include Architecture Reviewer instructions content"
1501 );
1502 assert!(
1503 !prompt.contains("### Correctness Reviewer Instructions"),
1504 "Should NOT include Correctness Reviewer instructions"
1505 );
1506 }
1507
1508 #[test]
1509 fn test_multiple_active_hats_all_included() {
1510 let yaml = r#"
1512hats:
1513 security_reviewer:
1514 name: "Security Reviewer"
1515 triggers: ["review.security"]
1516 instructions: "Review code for security vulnerabilities."
1517 architecture_reviewer:
1518 name: "Architecture Reviewer"
1519 triggers: ["review.architecture"]
1520 instructions: "Review system design and architecture."
1521 correctness_reviewer:
1522 name: "Correctness Reviewer"
1523 triggers: ["review.correctness"]
1524 instructions: "Review logic and correctness."
1525"#;
1526 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1527 let registry = HatRegistry::from_config(&config);
1528 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1529
1530 let security_hat = registry
1532 .get(&ralph_proto::HatId::new("security_reviewer"))
1533 .unwrap();
1534 let arch_hat = registry
1535 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1536 .unwrap();
1537 let active_hats = vec![security_hat, arch_hat];
1538
1539 let prompt = ralph.build_prompt("Events", &active_hats);
1540
1541 assert!(
1543 prompt.contains("### Security Reviewer Instructions"),
1544 "Should include Security Reviewer instructions"
1545 );
1546 assert!(
1547 prompt.contains("Review code for security vulnerabilities"),
1548 "Should include Security Reviewer content"
1549 );
1550 assert!(
1551 prompt.contains("### Architecture Reviewer Instructions"),
1552 "Should include Architecture Reviewer instructions"
1553 );
1554 assert!(
1555 prompt.contains("Review system design and architecture"),
1556 "Should include Architecture Reviewer content"
1557 );
1558
1559 assert!(
1561 !prompt.contains("### Correctness Reviewer Instructions"),
1562 "Should NOT include Correctness Reviewer instructions"
1563 );
1564 }
1565
1566 #[test]
1567 fn test_no_active_hats_no_instructions() {
1568 let yaml = r#"
1570hats:
1571 security_reviewer:
1572 name: "Security Reviewer"
1573 triggers: ["review.security"]
1574 instructions: "Review code for security vulnerabilities."
1575"#;
1576 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1577 let registry = HatRegistry::from_config(&config);
1578 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1579
1580 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1582
1583 let prompt = ralph.build_prompt("Events", &active_hats);
1584
1585 assert!(
1587 !prompt.contains("### Security Reviewer Instructions"),
1588 "Should NOT include instructions when no active hats"
1589 );
1590 assert!(
1591 !prompt.contains("Review code for security vulnerabilities"),
1592 "Should NOT include instructions content when no active hats"
1593 );
1594
1595 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1597 assert!(
1598 prompt.contains("| Hat | Triggers On | Publishes |"),
1599 "Should still have topology table"
1600 );
1601 }
1602
1603 #[test]
1604 fn test_topology_table_only_when_ralph_coordinating() {
1605 let yaml = r#"
1608hats:
1609 security_reviewer:
1610 name: "Security Reviewer"
1611 triggers: ["review.security"]
1612 instructions: "Security instructions."
1613 architecture_reviewer:
1614 name: "Architecture Reviewer"
1615 triggers: ["review.architecture"]
1616 instructions: "Architecture instructions."
1617"#;
1618 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1619 let registry = HatRegistry::from_config(&config);
1620 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1621
1622 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1624
1625 assert!(
1626 prompt_coordinating.contains("## HATS"),
1627 "Should have HATS section when coordinating"
1628 );
1629 assert!(
1630 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1631 "Should have topology table when coordinating"
1632 );
1633 assert!(
1634 prompt_coordinating.contains("```mermaid"),
1635 "Should have Mermaid diagram when coordinating"
1636 );
1637
1638 let security_hat = registry
1640 .get(&ralph_proto::HatId::new("security_reviewer"))
1641 .unwrap();
1642 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1643
1644 assert!(
1645 prompt_active.contains("## ACTIVE HAT"),
1646 "Should have ACTIVE HAT section when hat is active"
1647 );
1648 assert!(
1649 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1650 "Should NOT have topology table when hat is active"
1651 );
1652 assert!(
1653 !prompt_active.contains("```mermaid"),
1654 "Should NOT have Mermaid diagram when hat is active"
1655 );
1656 assert!(
1657 prompt_active.contains("### Security Reviewer Instructions"),
1658 "Should still have the active hat's instructions"
1659 );
1660 }
1661
1662 #[test]
1665 fn test_scratchpad_always_included() {
1666 let config = RalphConfig::default();
1668 let registry = HatRegistry::new();
1669 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1670
1671 let prompt = ralph.build_prompt("", &[]);
1672
1673 assert!(
1674 prompt.contains("### 0b. SCRATCHPAD"),
1675 "Scratchpad section should be included"
1676 );
1677 assert!(
1678 prompt.contains("`.ralph/agent/scratchpad.md`"),
1679 "Scratchpad path should be referenced"
1680 );
1681 assert!(
1682 prompt.contains("auto-injected"),
1683 "Auto-injection should be documented"
1684 );
1685 }
1686
1687 #[test]
1688 fn test_scratchpad_included_with_memories_enabled() {
1689 let config = RalphConfig::default();
1691 let registry = HatRegistry::new();
1692 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1693 .with_memories_enabled(true);
1694
1695 let prompt = ralph.build_prompt("", &[]);
1696
1697 assert!(
1699 prompt.contains("### 0b. SCRATCHPAD"),
1700 "Scratchpad section should be included even with memories enabled"
1701 );
1702 assert!(
1703 prompt.contains("**Always append**"),
1704 "Append instruction should be documented"
1705 );
1706
1707 assert!(
1709 !prompt.contains("### 0c. TASKS"),
1710 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1711 );
1712 }
1713
1714 #[test]
1715 fn test_no_tasks_section_in_core_prompt() {
1716 let config = RalphConfig::default();
1718 let registry = HatRegistry::new();
1719 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1720
1721 let prompt = ralph.build_prompt("", &[]);
1722
1723 assert!(
1725 !prompt.contains("### 0c. TASKS"),
1726 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1727 );
1728 }
1729
1730 #[test]
1731 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1732 let config = RalphConfig::default();
1734 let registry = HatRegistry::new();
1735 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1736 .with_memories_enabled(true);
1737
1738 let prompt = ralph.build_prompt("", &[]);
1739
1740 assert!(
1742 prompt.contains("update scratchpad"),
1743 "Workflow should reference scratchpad when memories enabled"
1744 );
1745 assert!(
1747 prompt.contains("ralph tools task"),
1748 "Workflow should reference tasks CLI when memories enabled"
1749 );
1750 }
1751
1752 #[test]
1753 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1754 let yaml = r#"
1756hats:
1757 builder:
1758 name: "Builder"
1759 triggers: ["build.task"]
1760 publishes: ["build.done"]
1761"#;
1762 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1763 let registry = HatRegistry::from_config(&config);
1764 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1765 .with_memories_enabled(true);
1766
1767 let prompt = ralph.build_prompt("", &[]);
1768
1769 assert!(
1771 prompt.contains("scratchpad"),
1772 "Multi-hat workflow should reference scratchpad when memories enabled"
1773 );
1774 assert!(
1776 prompt.contains("ralph tools task ensure"),
1777 "Multi-hat workflow should reference tasks CLI when memories enabled"
1778 );
1779 }
1780
1781 #[test]
1782 fn test_guardrails_adapt_to_memories_mode() {
1783 let config = RalphConfig::default();
1785 let registry = HatRegistry::new();
1786 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1787 .with_memories_enabled(true);
1788
1789 let prompt = ralph.build_prompt("", &[]);
1790
1791 assert!(
1795 prompt.contains("### GUARDRAILS"),
1796 "Guardrails section should be present"
1797 );
1798 }
1799
1800 #[test]
1801 fn test_guardrails_present_without_memories() {
1802 let config = RalphConfig::default();
1804 let registry = HatRegistry::new();
1805 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1806 let prompt = ralph.build_prompt("", &[]);
1809
1810 assert!(
1811 prompt.contains("### GUARDRAILS"),
1812 "Guardrails section should be present"
1813 );
1814 }
1815
1816 #[test]
1819 fn test_task_closure_verification_in_done_section() {
1820 let config = RalphConfig::default();
1823 let registry = HatRegistry::new();
1824 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1825 .with_memories_enabled(true);
1826
1827 let prompt = ralph.build_prompt("", &[]);
1828
1829 assert!(
1832 prompt.contains("ralph tools task list"),
1833 "Should reference task list command in DONE section"
1834 );
1835 assert!(
1836 prompt.contains("MUST NOT declare completion while tasks remain open"),
1837 "Should require tasks closed before completion"
1838 );
1839 }
1840
1841 #[test]
1842 fn test_workflow_verify_and_commit_step() {
1843 let config = RalphConfig::default();
1845 let registry = HatRegistry::new();
1846 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1847 .with_memories_enabled(true);
1848
1849 let prompt = ralph.build_prompt("", &[]);
1850
1851 assert!(
1853 prompt.contains("### 4. VERIFY & COMMIT"),
1854 "Should have VERIFY & COMMIT step in workflow"
1855 );
1856 assert!(
1857 prompt.contains("run tests and verify"),
1858 "Should require verification"
1859 );
1860 assert!(
1861 prompt.contains("ralph tools task start"),
1862 "Should reference task start command"
1863 );
1864 assert!(
1865 prompt.contains("ralph tools task close"),
1866 "Should reference task close command"
1867 );
1868 }
1869
1870 #[test]
1871 fn test_scratchpad_mode_still_has_commit_step() {
1872 let config = RalphConfig::default();
1874 let registry = HatRegistry::new();
1875 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1876 let prompt = ralph.build_prompt("", &[]);
1879
1880 assert!(
1882 prompt.contains("### 4. COMMIT"),
1883 "Should have COMMIT step in workflow"
1884 );
1885 assert!(
1886 prompt.contains("mark the task `[x]`"),
1887 "Should mark task in scratchpad"
1888 );
1889 assert!(
1891 !prompt.contains("### 0c. TASKS"),
1892 "Scratchpad mode should not have TASKS section"
1893 );
1894 }
1895
1896 #[test]
1899 fn test_objective_section_present_with_set_objective() {
1900 let config = RalphConfig::default();
1902 let registry = HatRegistry::new();
1903 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1904 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
1905
1906 let prompt = ralph.build_prompt("", &[]);
1907
1908 assert!(
1909 prompt.contains("## OBJECTIVE"),
1910 "Should have OBJECTIVE section when objective is set"
1911 );
1912 assert!(
1913 prompt.contains("Implement user authentication with JWT tokens"),
1914 "OBJECTIVE should contain the original user prompt"
1915 );
1916 assert!(
1917 prompt.contains("This is your primary goal"),
1918 "OBJECTIVE should emphasize this is the primary goal"
1919 );
1920 }
1921
1922 #[test]
1923 fn test_objective_reinforced_in_done_section() {
1924 let config = RalphConfig::default();
1927 let registry = HatRegistry::new();
1928 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1929 ralph.set_objective("Fix the login bug in auth module".to_string());
1930
1931 let prompt = ralph.build_prompt("", &[]);
1932
1933 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1935 let after_done = &prompt[done_pos..];
1936
1937 assert!(
1938 after_done.contains("Remember your objective"),
1939 "DONE section should remind about objective"
1940 );
1941 assert!(
1942 after_done.contains("Fix the login bug in auth module"),
1943 "DONE section should restate the objective"
1944 );
1945 }
1946
1947 #[test]
1948 fn test_objective_appears_before_pending_events() {
1949 let config = RalphConfig::default();
1951 let registry = HatRegistry::new();
1952 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1953 ralph.set_objective("Build feature X".to_string());
1954
1955 let context = "Event: task.start - Build feature X";
1956 let prompt = ralph.build_prompt(context, &[]);
1957
1958 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
1959 let events_pos = prompt
1960 .find("## PENDING EVENTS")
1961 .expect("Should have PENDING EVENTS");
1962
1963 assert!(
1964 objective_pos < events_pos,
1965 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
1966 objective_pos,
1967 events_pos
1968 );
1969 }
1970
1971 #[test]
1972 fn test_no_objective_when_not_set() {
1973 let config = RalphConfig::default();
1975 let registry = HatRegistry::new();
1976 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1977
1978 let context = "Event: build.done - Build completed successfully";
1979 let prompt = ralph.build_prompt(context, &[]);
1980
1981 assert!(
1982 !prompt.contains("## OBJECTIVE"),
1983 "Should NOT have OBJECTIVE section when objective not set"
1984 );
1985 }
1986
1987 #[test]
1988 fn test_objective_set_correctly() {
1989 let config = RalphConfig::default();
1991 let registry = HatRegistry::new();
1992 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1993 ralph.set_objective("Review this PR for security issues".to_string());
1994
1995 let prompt = ralph.build_prompt("", &[]);
1996
1997 assert!(
1998 prompt.contains("Review this PR for security issues"),
1999 "Should show the stored objective"
2000 );
2001 }
2002
2003 #[test]
2004 fn test_objective_with_events_context() {
2005 let config = RalphConfig::default();
2007 let registry = HatRegistry::new();
2008 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2009 ralph.set_objective("Implement feature Y".to_string());
2010
2011 let context =
2012 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
2013 let prompt = ralph.build_prompt(context, &[]);
2014
2015 assert!(
2016 prompt.contains("## OBJECTIVE"),
2017 "Should have OBJECTIVE section"
2018 );
2019 assert!(
2020 prompt.contains("Implement feature Y"),
2021 "OBJECTIVE should contain the stored objective"
2022 );
2023 }
2024
2025 #[test]
2026 fn test_done_section_without_objective() {
2027 let config = RalphConfig::default();
2029 let registry = HatRegistry::new();
2030 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2031
2032 let prompt = ralph.build_prompt("", &[]);
2033
2034 assert!(prompt.contains("## DONE"), "Should have DONE section");
2035 assert!(
2036 prompt.contains("LOOP_COMPLETE"),
2037 "DONE should mention completion event"
2038 );
2039 assert!(
2040 prompt.contains("final non-empty line"),
2041 "Solo DONE section should require literal terminal output"
2042 );
2043 assert!(
2044 !prompt.contains("Remember your objective"),
2045 "Should NOT have objective reinforcement without objective"
2046 );
2047 }
2048
2049 #[test]
2050 fn test_objective_persists_across_iterations() {
2051 let config = RalphConfig::default();
2054 let registry = HatRegistry::new();
2055 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2056 ralph.set_objective("Build a REST API with authentication".to_string());
2057
2058 let context = "Event: build.done - Build completed";
2060 let prompt = ralph.build_prompt(context, &[]);
2061
2062 assert!(
2063 prompt.contains("## OBJECTIVE"),
2064 "OBJECTIVE should persist even without task.start in context"
2065 );
2066 assert!(
2067 prompt.contains("Build a REST API with authentication"),
2068 "Stored objective should appear in later iterations"
2069 );
2070 }
2071
2072 #[test]
2073 fn test_done_section_suppressed_when_hat_active() {
2074 let yaml = r#"
2076hats:
2077 builder:
2078 name: "Builder"
2079 triggers: ["build.task"]
2080 publishes: ["build.done"]
2081 instructions: "Build the code."
2082"#;
2083 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2084 let registry = HatRegistry::from_config(&config);
2085 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2086 ralph.set_objective("Implement feature X".to_string());
2087
2088 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2089 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
2090
2091 assert!(
2092 !prompt.contains("## DONE"),
2093 "DONE section should be suppressed when a hat is active"
2094 );
2095 assert!(
2096 !prompt.contains("LOOP_COMPLETE"),
2097 "Completion promise should NOT appear when a hat is active"
2098 );
2099 assert!(
2101 prompt.contains("## OBJECTIVE"),
2102 "OBJECTIVE should still appear even when hat is active"
2103 );
2104 assert!(
2105 prompt.contains("Implement feature X"),
2106 "Objective content should be visible to active hat"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_done_section_present_when_coordinating() {
2112 let yaml = r#"
2114hats:
2115 builder:
2116 name: "Builder"
2117 triggers: ["build.task"]
2118 publishes: ["build.done"]
2119"#;
2120 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2121 let registry = HatRegistry::from_config(&config);
2122 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2123 ralph.set_objective("Complete the TDD cycle".to_string());
2124
2125 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
2127
2128 assert!(
2129 prompt.contains("## DONE"),
2130 "DONE section should appear when Ralph is coordinating"
2131 );
2132 assert!(
2133 prompt.contains("LOOP_COMPLETE"),
2134 "Completion promise should appear when coordinating"
2135 );
2136 assert!(
2137 prompt.contains("via `ralph emit`"),
2138 "Coordinating DONE section should require explicit event emission"
2139 );
2140 }
2141
2142 #[test]
2143 fn test_objective_in_done_section_when_coordinating() {
2144 let config = RalphConfig::default();
2146 let registry = HatRegistry::new();
2147 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2148 ralph.set_objective("Deploy the application".to_string());
2149
2150 let prompt = ralph.build_prompt("", &[]);
2151
2152 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2153 let after_done = &prompt[done_pos..];
2154
2155 assert!(
2156 after_done.contains("Remember your objective"),
2157 "DONE section should remind about objective when coordinating"
2158 );
2159 assert!(
2160 after_done.contains("Deploy the application"),
2161 "DONE section should contain the objective text"
2162 );
2163 }
2164
2165 #[test]
2168 fn test_event_publishing_guide_with_receivers() {
2169 let yaml = r#"
2172hats:
2173 builder:
2174 name: "Builder"
2175 description: "Builds and tests code"
2176 triggers: ["build.task"]
2177 publishes: ["build.done", "build.blocked"]
2178 confessor:
2179 name: "Confessor"
2180 description: "Produces a ConfessionReport; rewarded for honesty"
2181 triggers: ["build.done"]
2182 publishes: ["confession.done"]
2183"#;
2184 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2185 let registry = HatRegistry::from_config(&config);
2186 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2187
2188 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2190 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2191
2192 assert!(
2194 prompt.contains("### Event Publishing Guide"),
2195 "Should include Event Publishing Guide section"
2196 );
2197 assert!(
2198 prompt.contains("When you publish:"),
2199 "Guide should explain what happens when publishing"
2200 );
2201 assert!(
2202 prompt.contains("You MUST use `ralph emit"),
2203 "Guide should require explicit event emission"
2204 );
2205 assert!(
2207 prompt.contains("`build.done` → Received by: Confessor"),
2208 "Should show Confessor receives build.done"
2209 );
2210 assert!(
2211 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2212 "Should include receiver's description"
2213 );
2214 assert!(
2216 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2217 "Should show Ralph receives orphan events"
2218 );
2219 }
2220
2221 #[test]
2222 fn test_event_publishing_guide_no_publishes() {
2223 let yaml = r#"
2225hats:
2226 observer:
2227 name: "Observer"
2228 description: "Only observes"
2229 triggers: ["events.*"]
2230"#;
2231 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2232 let registry = HatRegistry::from_config(&config);
2233 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2234
2235 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2236 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2237
2238 assert!(
2240 !prompt.contains("### Event Publishing Guide"),
2241 "Should NOT include Event Publishing Guide when hat has no publishes"
2242 );
2243 }
2244
2245 #[test]
2246 fn test_event_publishing_guide_all_orphan_events() {
2247 let yaml = r#"
2249hats:
2250 solo:
2251 name: "Solo"
2252 triggers: ["solo.start"]
2253 publishes: ["solo.done", "solo.failed"]
2254"#;
2255 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2256 let registry = HatRegistry::from_config(&config);
2257 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2258
2259 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2260 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2261
2262 assert!(
2263 prompt.contains("### Event Publishing Guide"),
2264 "Should include guide even for orphan events"
2265 );
2266 assert!(
2267 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2268 "Orphan solo.done should go to Ralph"
2269 );
2270 assert!(
2271 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2272 "Orphan solo.failed should go to Ralph"
2273 );
2274 }
2275
2276 #[test]
2277 fn test_event_publishing_guide_multiple_receivers() {
2278 let yaml = r#"
2280hats:
2281 broadcaster:
2282 name: "Broadcaster"
2283 triggers: ["broadcast.start"]
2284 publishes: ["signal.sent"]
2285 listener1:
2286 name: "Listener1"
2287 description: "First listener"
2288 triggers: ["signal.sent"]
2289 listener2:
2290 name: "Listener2"
2291 description: "Second listener"
2292 triggers: ["signal.sent"]
2293"#;
2294 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2295 let registry = HatRegistry::from_config(&config);
2296 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2297
2298 let broadcaster = registry
2299 .get(&ralph_proto::HatId::new("broadcaster"))
2300 .unwrap();
2301 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2302
2303 assert!(
2304 prompt.contains("### Event Publishing Guide"),
2305 "Should include guide"
2306 );
2307 assert!(
2309 prompt.contains("Listener1 (First listener)"),
2310 "Should list Listener1 as receiver"
2311 );
2312 assert!(
2313 prompt.contains("Listener2 (Second listener)"),
2314 "Should list Listener2 as receiver"
2315 );
2316 }
2317
2318 #[test]
2319 fn test_event_publishing_guide_includes_self() {
2320 let yaml = r#"
2322hats:
2323 looper:
2324 name: "Looper"
2325 triggers: ["loop.continue", "loop.start"]
2326 publishes: ["loop.continue"]
2327"#;
2328 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2329 let registry = HatRegistry::from_config(&config);
2330 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2331
2332 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2333 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2334
2335 assert!(
2336 prompt.contains("### Event Publishing Guide"),
2337 "Should include guide"
2338 );
2339 assert!(
2341 prompt.contains("`loop.continue` → Received by: Looper"),
2342 "Self-loop event should show the hat itself as receiver"
2343 );
2344 }
2345
2346 #[test]
2347 fn test_event_publishing_guide_self_loop_shows_self_as_receiver() {
2348 let yaml = r#"
2351hats:
2352 processor:
2353 name: "Processor"
2354 description: "Processes work with retry"
2355 triggers: ["start", "process.retry"]
2356 publishes: ["process.done", "process.retry"]
2357 validator:
2358 name: "Validator"
2359 triggers: ["process.done"]
2360 publishes: ["validate.pass"]
2361"#;
2362 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2363 let registry = HatRegistry::from_config(&config);
2364 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2365
2366 let processor = registry.get(&ralph_proto::HatId::new("processor")).unwrap();
2367 let prompt = ralph.build_prompt("[start] Go", &[processor]);
2368
2369 assert!(
2371 prompt.contains("`process.retry` → Received by: Processor"),
2372 "Self-loop event should show the hat itself as receiver, not Ralph. Got:\n{}",
2373 prompt
2374 .lines()
2375 .filter(|l| l.contains("process.retry"))
2376 .collect::<Vec<_>>()
2377 .join("\n")
2378 );
2379 assert!(
2381 prompt.contains("`process.done` → Received by: Validator"),
2382 "Non-self event should still show correct receiver"
2383 );
2384 }
2385
2386 #[test]
2387 fn test_event_publishing_guide_receiver_without_description() {
2388 let yaml = r#"
2390hats:
2391 sender:
2392 name: "Sender"
2393 triggers: ["send.start"]
2394 publishes: ["message.sent"]
2395 receiver:
2396 name: "NoDescReceiver"
2397 triggers: ["message.sent"]
2398"#;
2399 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2400 let registry = HatRegistry::from_config(&config);
2401 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2402
2403 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2404 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2405
2406 assert!(
2407 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2408 "Should show receiver name without parentheses when no description"
2409 );
2410 assert!(
2412 !prompt.contains("NoDescReceiver ()"),
2413 "Should NOT have empty parentheses for receiver without description"
2414 );
2415 }
2416
2417 #[test]
2420 fn test_constraint_lists_valid_events_when_coordinating() {
2421 let yaml = r#"
2424hats:
2425 test_writer:
2426 name: "Test Writer"
2427 triggers: ["tdd.start"]
2428 publishes: ["test.written"]
2429 implementer:
2430 name: "Implementer"
2431 triggers: ["test.written"]
2432 publishes: ["test.passing"]
2433"#;
2434 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2435 let registry = HatRegistry::from_config(&config);
2436 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2437
2438 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2440
2441 assert!(
2443 prompt.contains("**CONSTRAINT:**"),
2444 "Prompt should include CONSTRAINT when coordinating"
2445 );
2446 assert!(
2447 prompt.contains("tdd.start"),
2448 "CONSTRAINT should list tdd.start as valid event"
2449 );
2450 assert!(
2451 prompt.contains("test.written"),
2452 "CONSTRAINT should list test.written as valid event"
2453 );
2454 assert!(
2455 prompt.contains("Publishing other events will have no effect"),
2456 "CONSTRAINT should warn about invalid events"
2457 );
2458 }
2459
2460 #[test]
2461 fn test_no_constraint_when_hat_is_active() {
2462 let yaml = r#"
2465hats:
2466 builder:
2467 name: "Builder"
2468 triggers: ["build.task"]
2469 publishes: ["build.done"]
2470 instructions: "Build the code."
2471"#;
2472 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2473 let registry = HatRegistry::from_config(&config);
2474 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2475
2476 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2478 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2479
2480 assert!(
2482 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2483 "Active hat should NOT have coordinating CONSTRAINT"
2484 );
2485
2486 assert!(
2488 prompt.contains("### Event Publishing Guide"),
2489 "Active hat should have Event Publishing Guide"
2490 );
2491 }
2492
2493 #[test]
2494 fn test_no_constraint_when_no_hats() {
2495 let config = RalphConfig::default();
2497 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2499
2500 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2501
2502 assert!(
2504 !prompt.contains("**CONSTRAINT:**"),
2505 "Solo mode should NOT have CONSTRAINT"
2506 );
2507 }
2508
2509 #[test]
2512 fn test_single_guidance_injection() {
2513 let config = RalphConfig::default();
2515 let registry = HatRegistry::new();
2516 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2517 ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2518
2519 let prompt = ralph.build_prompt("", &[]);
2520
2521 assert!(
2522 prompt.contains("## ROBOT GUIDANCE"),
2523 "Should include ROBOT GUIDANCE section"
2524 );
2525 assert!(
2526 prompt.contains("Focus on error handling first"),
2527 "Should contain the guidance message"
2528 );
2529 assert!(
2531 !prompt.contains("1. Focus on error handling first"),
2532 "Single guidance should not be numbered"
2533 );
2534 }
2535
2536 #[test]
2537 fn test_multiple_guidance_squashing() {
2538 let config = RalphConfig::default();
2540 let registry = HatRegistry::new();
2541 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2542 ralph.set_robot_guidance(vec![
2543 "Focus on error handling".to_string(),
2544 "Use the existing retry pattern".to_string(),
2545 "Check edge cases for empty input".to_string(),
2546 ]);
2547
2548 let prompt = ralph.build_prompt("", &[]);
2549
2550 assert!(
2551 prompt.contains("## ROBOT GUIDANCE"),
2552 "Should include ROBOT GUIDANCE section"
2553 );
2554 assert!(
2555 prompt.contains("1. Focus on error handling"),
2556 "First guidance should be numbered 1"
2557 );
2558 assert!(
2559 prompt.contains("2. Use the existing retry pattern"),
2560 "Second guidance should be numbered 2"
2561 );
2562 assert!(
2563 prompt.contains("3. Check edge cases for empty input"),
2564 "Third guidance should be numbered 3"
2565 );
2566 }
2567
2568 #[test]
2569 fn test_guidance_appears_in_prompt_before_events() {
2570 let config = RalphConfig::default();
2572 let registry = HatRegistry::new();
2573 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2574 ralph.set_objective("Build feature X".to_string());
2575 ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2576
2577 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2578
2579 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2580 let guidance_pos = prompt
2581 .find("## ROBOT GUIDANCE")
2582 .expect("Should have ROBOT GUIDANCE");
2583 let events_pos = prompt
2584 .find("## PENDING EVENTS")
2585 .expect("Should have PENDING EVENTS");
2586
2587 assert!(
2588 objective_pos < guidance_pos,
2589 "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2590 objective_pos,
2591 guidance_pos
2592 );
2593 assert!(
2594 guidance_pos < events_pos,
2595 "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2596 guidance_pos,
2597 events_pos
2598 );
2599 }
2600
2601 #[test]
2602 fn test_guidance_cleared_after_injection() {
2603 let config = RalphConfig::default();
2605 let registry = HatRegistry::new();
2606 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2607 ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2608
2609 let prompt1 = ralph.build_prompt("", &[]);
2611 assert!(
2612 prompt1.contains("## ROBOT GUIDANCE"),
2613 "First prompt should have guidance"
2614 );
2615
2616 ralph.clear_robot_guidance();
2618
2619 let prompt2 = ralph.build_prompt("", &[]);
2621 assert!(
2622 !prompt2.contains("## ROBOT GUIDANCE"),
2623 "After clearing, prompt should not have guidance"
2624 );
2625 }
2626
2627 #[test]
2628 fn test_no_injection_when_no_guidance() {
2629 let config = RalphConfig::default();
2631 let registry = HatRegistry::new();
2632 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2633
2634 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2635
2636 assert!(
2637 !prompt.contains("## ROBOT GUIDANCE"),
2638 "Should NOT include ROBOT GUIDANCE when no guidance set"
2639 );
2640 }
2641}