1use crate::config::{CoreConfig, ScratchpadConfig};
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 active_scratchpad: ScratchpadConfig,
33 iteration: u32,
36}
37
38pub struct HatTopology {
40 hats: Vec<HatInfo>,
41}
42
43#[derive(Debug, Clone)]
45pub struct EventReceiver {
46 pub name: String,
47 pub description: String,
48 pub hat_id: HatId,
50 pub concurrency: u32,
52}
53
54pub struct HatInfo {
56 pub name: String,
57 pub description: String,
58 pub subscribes_to: Vec<String>,
59 pub publishes: Vec<String>,
60 pub instructions: String,
61 pub event_receivers: HashMap<String, Vec<EventReceiver>>,
63 pub disallowed_tools: Vec<String>,
65}
66
67impl HatInfo {
68 pub fn event_publishing_guide(&self) -> Option<String> {
72 if self.publishes.is_empty() {
73 return None;
74 }
75
76 let mut guide = String::from(
77 "### Event Publishing Guide\n\n\
78 You MUST publish exactly ONE event when your work is complete.\n\
79 You MUST use `ralph emit \"<topic>\" \"<brief summary>\"` to publish it.\n\
80 Plain-language summaries do NOT publish events.\n\
81 Publishing hands off to the next hat and starts a fresh iteration with clear context.\n\n\
82 When you publish:\n",
83 );
84
85 for pub_event in &self.publishes {
86 let receivers = self.event_receivers.get(pub_event);
87 let receiver_text = match receivers {
88 Some(r) if !r.is_empty() => r
89 .iter()
90 .map(|recv| {
91 if recv.description.is_empty() {
92 recv.name.clone()
93 } else {
94 format!("{} ({})", recv.name, recv.description)
95 }
96 })
97 .collect::<Vec<_>>()
98 .join(", "),
99 _ => "Ralph (coordinates next steps)".to_string(),
100 };
101 guide.push_str(&format!(
102 "- `{}` → Received by: {}\n",
103 pub_event, receiver_text
104 ));
105 }
106
107 Some(guide)
108 }
109
110 pub fn wave_dispatch_section(&self) -> String {
115 let mut wave_topics: Vec<(&str, &str, u32)> = Vec::new();
117 for pub_event in &self.publishes {
118 if let Some(receivers) = self.event_receivers.get(pub_event) {
119 for recv in receivers {
120 if recv.concurrency > 1 {
121 wave_topics.push((pub_event.as_str(), &recv.name, recv.concurrency));
122 }
123 }
124 }
125 }
126
127 if wave_topics.is_empty() {
128 return String::new();
129 }
130
131 let mut section = String::from("### Wave Dispatch (Parallel Execution)\n\n");
132 section.push_str(
133 "Some downstream hats support parallel execution via waves. \
134 Use `ralph wave emit` to dispatch multiple items for concurrent processing.\n\n",
135 );
136
137 section.push_str("| Topic | Activates | Max Concurrent |\n");
138 section.push_str("|-------|-----------|----------------|\n");
139 for (topic, hat_name, concurrency) in &wave_topics {
140 section.push_str(&format!(
141 "| `{}` | {} | {} |\n",
142 topic, hat_name, concurrency
143 ));
144 }
145 section.push('\n');
146
147 if let Some((topic, _, _)) = wave_topics.first() {
149 section.push_str("**Usage:**\n```bash\n");
150 section.push_str(&format!(
151 "ralph wave emit {} --payloads \"item1\" \"item2\" \"item3\"\n",
152 topic
153 ));
154 section.push_str("```\n\n");
155 }
156
157 section
158 }
159}
160
161impl HatTopology {
162 pub fn from_registry(registry: &HatRegistry) -> Self {
164 let hats = registry
165 .all()
166 .map(|hat| {
167 let event_receivers: HashMap<String, Vec<EventReceiver>> = hat
169 .publishes
170 .iter()
171 .map(|pub_topic| {
172 let receivers: Vec<EventReceiver> = registry
173 .subscribers(pub_topic)
174 .into_iter()
175 .map(|h| {
176 let concurrency = registry
177 .get_config(&h.id)
178 .map(|c| c.concurrency)
179 .unwrap_or(1);
180 EventReceiver {
181 name: h.name.clone(),
182 description: h.description.clone(),
183 hat_id: h.id.clone(),
184 concurrency,
185 }
186 })
187 .collect();
188 (pub_topic.as_str().to_string(), receivers)
189 })
190 .collect();
191
192 let disallowed_tools = registry
193 .get_config(&hat.id)
194 .map(|c| c.disallowed_tools.clone())
195 .unwrap_or_default();
196
197 HatInfo {
198 name: hat.name.clone(),
199 description: hat.description.clone(),
200 subscribes_to: hat
201 .subscriptions
202 .iter()
203 .map(|t| t.as_str().to_string())
204 .collect(),
205 publishes: hat
206 .publishes
207 .iter()
208 .map(|t| t.as_str().to_string())
209 .collect(),
210 instructions: hat.instructions.clone(),
211 event_receivers,
212 disallowed_tools,
213 }
214 })
215 .collect();
216
217 Self { hats }
218 }
219}
220
221impl HatlessRalph {
222 pub fn new(
230 completion_promise: impl Into<String>,
231 core: CoreConfig,
232 registry: &HatRegistry,
233 starting_event: Option<String>,
234 ) -> Self {
235 let hat_topology = if registry.is_empty() {
236 None
237 } else {
238 Some(HatTopology::from_registry(registry))
239 };
240
241 let active_scratchpad = core.scratchpad.clone();
242 Self {
243 completion_promise: completion_promise.into(),
244 core,
245 hat_topology,
246 starting_event,
247 memories_enabled: false, objective: None,
249 skill_index: String::new(),
250 robot_guidance: Vec::new(),
251 active_scratchpad,
252 iteration: 0,
253 }
254 }
255
256 pub fn set_active_scratchpad(&mut self, config: ScratchpadConfig) {
261 self.active_scratchpad = config;
262 }
263
264 pub fn active_scratchpad(&self) -> &ScratchpadConfig {
266 &self.active_scratchpad
267 }
268
269 pub fn set_iteration(&mut self, iteration: u32) {
274 self.iteration = iteration;
275 }
276
277 pub fn with_memories_enabled(mut self, enabled: bool) -> Self {
282 self.memories_enabled = enabled;
283 self
284 }
285
286 pub fn with_skill_index(mut self, index: String) -> Self {
291 self.skill_index = index;
292 self
293 }
294
295 pub fn set_objective(&mut self, objective: String) {
301 self.objective = Some(objective);
302 }
303
304 pub fn set_robot_guidance(&mut self, guidance: Vec<String>) {
310 self.robot_guidance = guidance;
311 }
312
313 pub fn clear_robot_guidance(&mut self) {
317 self.robot_guidance.clear();
318 }
319
320 fn collect_robot_guidance(&self) -> String {
325 if self.robot_guidance.is_empty() {
326 return String::new();
327 }
328
329 let mut section = String::from("## ROBOT GUIDANCE\n\n");
330 section.push_str(
331 "This guidance is blocking. You MUST address it before declaring completion. \
332 After addressing it, emit `human.guidance.ack` with a brief summary.\n\n",
333 );
334
335 if self.robot_guidance.len() == 1 {
336 section.push_str(&self.robot_guidance[0]);
337 } else {
338 for (i, guidance) in self.robot_guidance.iter().enumerate() {
339 section.push_str(&format!("{}. {}\n", i + 1, guidance));
340 }
341 }
342
343 section.push_str("\n\n");
344
345 section
346 }
347
348 pub fn build_prompt(&self, context: &str, active_hats: &[&ralph_proto::Hat]) -> String {
356 let mut prompt = self.core_prompt();
357
358 if !self.skill_index.is_empty() {
360 prompt.push_str(&self.skill_index);
361 prompt.push('\n');
362 }
363
364 if let Some(ref obj) = self.objective {
366 prompt.push_str(&self.objective_section(obj));
367 }
368
369 let guidance = self.collect_robot_guidance();
371 if !guidance.is_empty() {
372 prompt.push_str(&guidance);
373 }
374
375 if !context.trim().is_empty() {
377 prompt.push_str("## PENDING EVENTS\n\n");
378 prompt.push_str("You MUST handle these events in this iteration:\n\n");
379 prompt.push_str(context);
380 prompt.push_str("\n\n");
381 }
382
383 let has_custom_workflow = active_hats
386 .iter()
387 .any(|h| !h.instructions.trim().is_empty());
388
389 if !has_custom_workflow {
390 prompt.push_str(&self.workflow_section());
391 }
392
393 if let Some(topology) = &self.hat_topology {
394 prompt.push_str(&self.hats_section(topology, active_hats));
395 }
396
397 prompt.push_str(&self.event_writing_section());
398
399 if active_hats.is_empty() {
402 prompt.push_str(&self.done_section(self.objective.as_deref()));
403 }
404
405 prompt
406 }
407
408 fn objective_section(&self, objective: &str) -> String {
410 format!(
411 r"## OBJECTIVE
412
413**This is your primary goal. All work must advance this objective.**
414
415> {objective}
416
417You MUST keep this objective in mind throughout the iteration.
418You MUST NOT get distracted by workflow mechanics — they serve this goal.
419
420",
421 objective = objective
422 )
423 }
424
425 pub fn should_handle(&self, _topic: &Topic) -> bool {
427 true
428 }
429
430 fn is_fresh_start(&self) -> bool {
436 if self.starting_event.is_none() {
438 return false;
439 }
440
441 if self.iteration > 0 {
443 return false;
444 }
445
446 if self.active_scratchpad.enabled {
449 return !Path::new(&self.active_scratchpad.path).exists();
450 }
451
452 true
454 }
455
456 fn core_prompt(&self) -> String {
457 let guardrails = self
459 .core
460 .guardrails
461 .iter()
462 .enumerate()
463 .map(|(i, g)| {
464 let guardrail = if self.memories_enabled && g.contains("scratchpad is memory") {
466 g.replace(
467 "scratchpad is memory",
468 "save learnings to memories for next time",
469 )
470 } else {
471 g.clone()
472 };
473 format!("{}. {guardrail}", 999 + i)
474 })
475 .collect::<Vec<_>>()
476 .join("\n");
477
478 let mut prompt = if self.memories_enabled {
479 if self.active_scratchpad.enabled {
480 r"
481### 0a. ORIENTATION
482You are Ralph. You are running in a loop. You have fresh context each iteration.
483You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
484
485**First thing every iteration:**
4861. Review your `<scratchpad>` (auto-injected above) for context on your thinking
4872. Review your `<ready-tasks>` (auto-injected above) to see what work exists
4883. If tasks exist, pick one. If not, create them from your plan.
489"
490 .to_string()
491 } else {
492 r"
493### 0a. ORIENTATION
494You are Ralph. You are running in a loop. You have fresh context each iteration.
495You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
496
497**First thing every iteration:**
4981. Review your `<ready-tasks>` (auto-injected above) to see what work exists
4992. If tasks exist, pick one. If not, create them from your plan.
500"
501 .to_string()
502 }
503 } else {
504 r"
505### 0a. ORIENTATION
506You are Ralph. You are running in a loop. You have fresh context each iteration.
507You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
508"
509 .to_string()
510 };
511
512 if self.active_scratchpad.enabled {
514 prompt.push_str(&format!(
515 r"### 0b. SCRATCHPAD
516`{scratchpad}` is your thinking journal for THIS objective.
517Its content is auto-injected in `<scratchpad>` tags at the top of your context each iteration.
518
519**Always append** new entries to the end of the file (most recent = bottom).
520
521**Use for:**
522- Current understanding and reasoning
523- Analysis notes and decisions
524- Plan narrative (the 'why' behind your approach)
525
526**Do NOT use for:**
527- Tracking what tasks exist or their status (use `ralph tools task`)
528- Checklists or todo lists (use `ralph tools task ensure` when a stable key exists, otherwise `ralph tools task add`)
529
530",
531 scratchpad = self.active_scratchpad.path,
532 ));
533 }
534
535 if self.active_scratchpad.enabled {
541 prompt.push_str(&format!(
542 "### STATE MANAGEMENT\n\n\
543**Scratchpad** (`{scratchpad}`) — Your thinking:\n\
544- Current understanding and reasoning\n\
545- Analysis notes, decisions, plan narrative\n\
546- NOT for checklists or status tracking\n\
547\n\
548**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
549- Analysis and temporary notes\n\
550- Read when relevant\n\
551\n\
552**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\
553The 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\
554Do not spend turns on shell or tool-availability diagnosis unless the task is explicitly about the runtime environment.\n\
555Do 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\
556Keep temporary artifacts where later steps can still inspect them, such as a repo-local `logs/` directory or `/var/tmp` when needed.\n\
557\n",
558 scratchpad = self.active_scratchpad.path,
559 ));
560 } else {
561 prompt.push_str(
562 "### STATE MANAGEMENT\n\n\
563**Tasks** (`ralph tools task`) — What needs to be done:\n\
564- Work items, their status, priorities, and dependencies\n\
565- Source of truth for progress across iterations\n\
566- Auto-injected in `<ready-tasks>` tags at the top of your context\n\
567\n\
568**Memories** (`.ralph/agent/memories.md`) — Persistent learning:\n\
569- Codebase patterns and conventions\n\
570- Architectural decisions and rationale\n\
571- Recurring problem solutions\n\
572\n\
573**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
574- Analysis and temporary notes\n\
575- Read when relevant\n\
576\n\
577**Rule:** Work items go in tasks. Learnings go in memories.\n\
578\n",
579 );
580 }
581
582 if let Ok(entries) = std::fs::read_dir(".ralph/agent") {
584 let md_files: Vec<String> = entries
585 .filter_map(|e| e.ok())
586 .filter_map(|e| {
587 let path = e.path();
588 let fname = path.file_name().and_then(|s| s.to_str());
589 if path.extension().and_then(|s| s.to_str()) == Some("md")
590 && fname != Some("memories.md")
591 && fname != Some("scratchpad.md")
592 {
593 path.file_name()
594 .and_then(|s| s.to_str())
595 .map(|s| s.to_string())
596 } else {
597 None
598 }
599 })
600 .collect();
601
602 if !md_files.is_empty() {
603 prompt.push_str("### AVAILABLE CONTEXT FILES\n\n");
604 prompt.push_str(
605 "Context files in `.ralph/agent/` (read if relevant to current work):\n",
606 );
607 for file in md_files {
608 prompt.push_str(&format!("- `.ralph/agent/{}`\n", file));
609 }
610 prompt.push('\n');
611 }
612 }
613
614 prompt.push_str(&format!(
615 r"### GUARDRAILS
616{guardrails}
617
618",
619 guardrails = guardrails,
620 ));
621
622 prompt
623 }
624
625 fn workflow_section(&self) -> String {
626 let scratchpad_enabled = self.active_scratchpad.enabled;
627 let scratchpad = &self.active_scratchpad.path;
628
629 if self.hat_topology.is_some() {
631 if self.is_fresh_start() {
633 return format!(
635 r#"## WORKFLOW
636
637**FAST PATH**: You MUST publish `{}` immediately to start the hat workflow.
638You MUST use `ralph emit "{}" "<brief handoff>"` and stop immediately.
639You MUST NOT plan or analyze — delegate now.
640
641"#,
642 self.starting_event.as_ref().unwrap(),
643 self.starting_event.as_ref().unwrap()
644 );
645 }
646
647 if self.memories_enabled {
649 if scratchpad_enabled {
650 format!(
652 r"## WORKFLOW
653
654### 1. PLAN
655You MUST update `{scratchpad}` with your understanding and plan.
656You MUST check `<ready-tasks>` first.
657You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
658You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
659If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
660
661### 2. DELEGATE
662You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
663Plain-language summaries do NOT hand off work.
664You MUST NOT do implementation work — delegation is your only job.
665
666",
667 )
668 } else {
669 "## WORKFLOW\n\n\
671### 1. PLAN\n\
672You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).\n\
673\n\
674### 2. DELEGATE\n\
675You MUST publish exactly ONE event to hand off to specialized hats.\n\
676You MUST NOT do implementation work — delegation is your only job.\n\
677\n"
678 .to_string()
679 }
680 } else if scratchpad_enabled {
681 format!(
683 r"## WORKFLOW
684
685### 1. PLAN
686You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
687
688### 2. DELEGATE
689You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
690Plain-language summaries do NOT hand off work.
691You MUST NOT do implementation work — delegation is your only job.
692
693",
694 )
695 } else {
696 "## WORKFLOW\n\n\
698### 1. DELEGATE\n\
699You MUST publish exactly ONE event to hand off to specialized hats.\n\
700You MUST NOT do implementation work — delegation is your only job.\n\
701\n"
702 .to_string()
703 }
704 } else {
705 if self.memories_enabled {
707 if scratchpad_enabled {
708 format!(
710 r"## WORKFLOW
711
712### 1. Study the prompt.
713You MUST study, explore, and research what needs to be done.
714
715### 2. PLAN
716You MUST update `{scratchpad}` with your understanding and plan.
717You MUST check `<ready-tasks>` first.
718You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
719You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
720If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
721
722### 3. IMPLEMENT
723You MUST pick exactly ONE task from `<ready-tasks>` to implement.
724You MUST mark it in progress with `ralph tools task start <id>` before implementation.
725
726### 4. VERIFY & COMMIT
727You MUST run tests and verify the implementation works.
728If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
729You SHOULD try at least one realistic failure-path or adversarial input during verification.
730If this turn is likely to take more than a few minutes, you SHOULD send `ralph tools interact progress`.
731You MUST commit after verification passes - one commit per task.
732You SHOULD run `git diff --cached` to review staged changes before committing.
733You MUST close the task with `ralph tools task close <id>` AFTER commit.
734You SHOULD save learnings to memories with `ralph tools memory add`.
735If 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.
736You MUST update scratchpad with what you learned (tasks track what remains).
737
738### 5. EXIT
739You MUST exit after completing ONE task.
740
741",
742 )
743 } else {
744 "## WORKFLOW\n\n\
746### 1. Study the prompt.\n\
747You MUST study, explore, and research what needs to be done.\n\
748\n\
749### 2. PLAN\n\
750You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).\n\
751\n\
752### 3. IMPLEMENT\n\
753You MUST pick exactly ONE task from `<ready-tasks>` to implement.\n\
754\n\
755### 4. VERIFY & COMMIT\n\
756You MUST run tests and verify the implementation works.\n\
757You MUST commit after verification passes - one commit per task.\n\
758You SHOULD run `git diff --cached` to review staged changes before committing.\n\
759You MUST close the task with `ralph tools task close <id>` AFTER commit.\n\
760You SHOULD save learnings to memories with `ralph tools memory add`.\n\
761\n\
762### 5. EXIT\n\
763You MUST exit after completing ONE task.\n\
764\n"
765 .to_string()
766 }
767 } else if scratchpad_enabled {
768 format!(
770 r"## WORKFLOW
771
772### 1. Study the prompt.
773You MUST study, explore, and research what needs to be done.
774You MAY use parallel subagents (up to 10) for searches.
775
776### 2. PLAN
777You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
778
779### 3. IMPLEMENT
780You MUST pick exactly ONE task to implement.
781You MUST NOT use more than 1 subagent for build/tests.
782
783### 4. COMMIT
784If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
785You SHOULD try at least one realistic failure-path or adversarial input during verification.
786You MUST commit after completing each atomic unit of work.
787You MUST capture the why, not just the what.
788You SHOULD run `git diff` before committing to review changes.
789You MUST mark the task `[x]` in scratchpad when complete.
790
791### 5. REPEAT
792You MUST continue until all tasks are `[x]` or `[~]`.
793
794",
795 )
796 } else {
797 "## WORKFLOW\n\n\
799### 1. Study the prompt.\n\
800You MUST study, explore, and research what needs to be done.\n\
801You MAY use parallel subagents (up to 10) for searches.\n\
802\n\
803### 2. IMPLEMENT\n\
804You MUST pick exactly ONE task to implement.\n\
805You MUST NOT use more than 1 subagent for build/tests.\n\
806\n\
807### 3. COMMIT\n\
808You MUST commit after completing each atomic unit of work.\n\
809You MUST capture the why, not just the what.\n\
810You SHOULD run `git diff` before committing to review changes.\n\
811\n\
812### 4. REPEAT\n\
813You MUST continue.\n\
814\n"
815 .to_string()
816 }
817 }
818 }
819
820 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
821 let mut section = String::new();
822
823 if active_hats.is_empty() {
826 section.push_str("## HATS\n\nDelegate via events.\n\n");
828
829 if let Some(ref starting_event) = self.starting_event {
831 section.push_str(&format!(
832 "**After coordination, publish `{}` to start the workflow.**\n\n",
833 starting_event
834 ));
835 }
836
837 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
841 let mut ralph_publishes: Vec<&str> = Vec::new();
842
843 for hat in &topology.hats {
844 for pub_event in &hat.publishes {
845 if !ralph_triggers.contains(&pub_event.as_str()) {
846 ralph_triggers.push(pub_event.as_str());
847 }
848 }
849 for sub_event in &hat.subscribes_to {
850 if !ralph_publishes.contains(&sub_event.as_str()) {
851 ralph_publishes.push(sub_event.as_str());
852 }
853 }
854 }
855
856 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
858 section.push_str("|-----|-------------|----------|-------------|\n");
859
860 section.push_str(&format!(
862 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
863 ralph_triggers.join(", "),
864 ralph_publishes.join(", ")
865 ));
866
867 for hat in &topology.hats {
869 let subscribes = hat.subscribes_to.join(", ");
870 let publishes = hat.publishes.join(", ");
871 section.push_str(&format!(
872 "| {} | {} | {} | {} |\n",
873 hat.name, subscribes, publishes, hat.description
874 ));
875 }
876
877 section.push('\n');
878
879 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
881 section.push('\n');
882
883 if !ralph_publishes.is_empty() {
885 section.push_str(&format!(
886 "**CONSTRAINT:** You MUST only publish events from this list: `{}`\n\
887 Publishing other events will have no effect - no hat will receive them.\n\n",
888 ralph_publishes.join("`, `")
889 ));
890 }
891
892 self.validate_topology_reachability(topology);
894 } else {
895 section.push_str("## ACTIVE HAT\n\n");
897
898 for active_hat in active_hats {
899 let hat_info = topology.hats.iter().find(|h| h.name == active_hat.name);
901
902 if !active_hat.instructions.trim().is_empty() {
903 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
904 section.push_str(&active_hat.instructions);
905 if !active_hat.instructions.ends_with('\n') {
906 section.push('\n');
907 }
908 section.push('\n');
909 }
910
911 if let Some(guide) = hat_info.and_then(|info| info.event_publishing_guide()) {
913 section.push_str(&guide);
914 section.push('\n');
915 }
916
917 if let Some(info) = hat_info {
919 let wave_dispatch = info.wave_dispatch_section();
920 if !wave_dispatch.is_empty() {
921 section.push_str(&wave_dispatch);
922 }
923 }
924
925 if let Some(info) = hat_info
927 && !info.disallowed_tools.is_empty()
928 {
929 section.push_str("### TOOL RESTRICTIONS\n\n");
930 section.push_str("You MUST NOT use these tools in this hat:\n");
931 for tool in &info.disallowed_tools {
932 section.push_str(&format!("- **{}** — blocked for this hat\n", tool));
933 }
934 section.push_str(
935 "\nUsing a restricted tool is a scope violation. \
936 File modifications are audited after each iteration.\n\n",
937 );
938 }
939 }
940 }
941
942 section
943 }
944
945 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
947 let node_ids: std::collections::HashMap<&str, String> = topology
949 .hats
950 .iter()
951 .map(|h| {
952 let id = h
953 .name
954 .chars()
955 .filter(|c| c.is_alphanumeric())
956 .collect::<String>();
957 (h.name.as_str(), id)
958 })
959 .collect();
960
961 let mut diagram = String::from("```mermaid\nflowchart LR\n");
962
963 diagram.push_str(" task.start((task.start)) --> Ralph\n");
965
966 for hat in &topology.hats {
968 let node_id = &node_ids[hat.name.as_str()];
969 for trigger in &hat.subscribes_to {
970 if ralph_publishes.contains(&trigger.as_str()) {
971 if node_id == &hat.name {
972 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
973 } else {
974 diagram.push_str(&format!(
975 " Ralph -->|{}| {}[{}]\n",
976 trigger, node_id, hat.name
977 ));
978 }
979 }
980 }
981 }
982
983 for hat in &topology.hats {
985 let node_id = &node_ids[hat.name.as_str()];
986 for pub_event in &hat.publishes {
987 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
988 }
989 }
990
991 for source_hat in &topology.hats {
993 let source_id = &node_ids[source_hat.name.as_str()];
994 for pub_event in &source_hat.publishes {
995 for target_hat in &topology.hats {
996 if target_hat.name != source_hat.name
997 && target_hat.subscribes_to.contains(pub_event)
998 {
999 let target_id = &node_ids[target_hat.name.as_str()];
1000 diagram.push_str(&format!(
1001 " {} -->|{}| {}\n",
1002 source_id, pub_event, target_id
1003 ));
1004 }
1005 }
1006 }
1007 }
1008
1009 diagram.push_str("```\n");
1010 diagram
1011 }
1012
1013 fn validate_topology_reachability(&self, topology: &HatTopology) {
1016 use std::collections::HashSet;
1017 use tracing::warn;
1018
1019 let mut reachable_events: HashSet<&str> = HashSet::new();
1021 reachable_events.insert("task.start");
1022
1023 for hat in &topology.hats {
1025 for trigger in &hat.subscribes_to {
1026 reachable_events.insert(trigger.as_str());
1027 }
1028 }
1029
1030 for hat in &topology.hats {
1032 for pub_event in &hat.publishes {
1033 reachable_events.insert(pub_event.as_str());
1034 }
1035 }
1036
1037 for hat in &topology.hats {
1039 let hat_reachable = hat
1040 .subscribes_to
1041 .iter()
1042 .any(|t| reachable_events.contains(t.as_str()));
1043 if !hat_reachable {
1044 warn!(
1045 hat = %hat.name,
1046 triggers = ?hat.subscribes_to,
1047 "Hat has triggers that are never published - it may be unreachable"
1048 );
1049 }
1050 }
1051 }
1052
1053 fn event_writing_section(&self) -> String {
1054 let detailed_output_hint = if self.active_scratchpad.enabled {
1055 format!(
1056 "\nYou SHOULD write detailed output to `{}` and emit only a brief event.\n",
1057 self.active_scratchpad.path
1058 )
1059 } else {
1060 String::new()
1061 };
1062
1063 format!(
1064 r#"## EVENT WRITING
1065
1066Events are routing signals, not data transport. You SHOULD keep payloads brief.
1067
1068You MUST use `ralph emit` to write events (handles JSON escaping correctly):
1069```bash
1070ralph emit "build.done" "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass"
1071ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
1072```
1073
1074You MUST NOT use echo/cat to write events because shell escaping breaks JSON.
1075{detailed_output_hint}
1076**Constraints:**
1077- You MUST stop working after publishing an event because a new iteration will start with fresh context
1078- You MUST NOT continue with additional work after publishing because the next iteration handles it with the appropriate hat persona
1079"#,
1080 detailed_output_hint = detailed_output_hint
1081 )
1082 }
1083
1084 fn done_section(&self, objective: Option<&str>) -> String {
1085 let mut section = if self.hat_topology.is_some() {
1086 format!(
1087 r"## DONE
1088
1089You MUST emit the completion event `{}` via `ralph emit` when the objective is complete and all tasks are done.
1090Stdout text does NOT end the loop in coordinated mode.
1091",
1092 self.completion_promise
1093 )
1094 } else {
1095 format!(
1096 r"## DONE
1097
1098You MUST output the literal completion promise `{}` as the final non-empty line when the objective is complete and all tasks are done.
1099You MUST NOT substitute a prose summary for `{}`.
1100You MUST NOT print any text after `{}`.
1101",
1102 self.completion_promise, self.completion_promise, self.completion_promise
1103 )
1104 };
1105
1106 if self.memories_enabled {
1108 section.push_str(
1109 r"
1110**Before declaring completion:**
11111. Run `ralph tools task list` to check for any remaining non-terminal tasks
11122. If any tasks are still open or in progress, close, fail, or reopen them first
11133. Only declare completion when YOUR tasks for this objective are all terminal
1114
1115Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
1116
1117You MUST NOT declare completion while tasks remain open.
1118",
1119 );
1120 }
1121
1122 if let Some(obj) = objective {
1124 section.push_str(&format!(
1125 r"
1126**Remember your objective:**
1127> {}
1128
1129You MUST NOT declare completion until this objective is fully satisfied.
1130",
1131 obj
1132 ));
1133 }
1134
1135 section
1136 }
1137}
1138
1139#[cfg(test)]
1140mod tests {
1141 use super::*;
1142 use crate::config::RalphConfig;
1143
1144 #[test]
1145 fn test_prompt_without_hats() {
1146 let config = RalphConfig::default();
1147 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1149
1150 let prompt = ralph.build_prompt("", &[]);
1151
1152 assert!(prompt.contains(
1154 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1155 ));
1156
1157 assert!(prompt.contains("### 0a. ORIENTATION"));
1159 assert!(prompt.contains("MUST complete only one atomic task"));
1160
1161 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1163 assert!(prompt.contains("auto-injected"));
1164 assert!(prompt.contains("**Always append**"));
1165
1166 assert!(prompt.contains("## WORKFLOW"));
1168 assert!(prompt.contains("### 1. Study the prompt"));
1169 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
1170 assert!(prompt.contains("### 2. PLAN"));
1171 assert!(prompt.contains("### 3. IMPLEMENT"));
1172 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
1173 assert!(prompt.contains("### 4. COMMIT"));
1174 assert!(prompt.contains("You MUST capture the why"));
1175 assert!(prompt.contains("### 5. REPEAT"));
1176
1177 assert!(!prompt.contains("## HATS"));
1179
1180 assert!(prompt.contains("## EVENT WRITING"));
1182 assert!(prompt.contains("You MUST use `ralph emit`"));
1183 assert!(prompt.contains("You MUST NOT use echo/cat"));
1184 assert!(prompt.contains("LOOP_COMPLETE"));
1185 }
1186
1187 #[test]
1188 fn test_prompt_with_hats() {
1189 let yaml = r#"
1191hats:
1192 planner:
1193 name: "Planner"
1194 triggers: ["planning.start", "build.done", "build.blocked"]
1195 publishes: ["build.task"]
1196 builder:
1197 name: "Builder"
1198 triggers: ["build.task"]
1199 publishes: ["build.done", "build.blocked"]
1200"#;
1201 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1202 let registry = HatRegistry::from_config(&config);
1203 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1205
1206 let prompt = ralph.build_prompt("", &[]);
1207
1208 assert!(prompt.contains(
1210 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1211 ));
1212
1213 assert!(prompt.contains("### 0a. ORIENTATION"));
1215 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1216
1217 assert!(prompt.contains("## WORKFLOW"));
1219 assert!(prompt.contains("### 1. PLAN"));
1220 assert!(
1221 prompt.contains("### 2. DELEGATE"),
1222 "Multi-hat mode should have DELEGATE step"
1223 );
1224 assert!(
1225 !prompt.contains("### 3. IMPLEMENT"),
1226 "Multi-hat mode should NOT tell Ralph to implement"
1227 );
1228 assert!(
1229 prompt.contains("You MUST stop working after publishing"),
1230 "Should explicitly tell Ralph to stop after publishing event"
1231 );
1232
1233 assert!(prompt.contains("## HATS"));
1235 assert!(prompt.contains("Delegate via events"));
1236 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
1237
1238 assert!(prompt.contains("## EVENT WRITING"));
1240 assert!(prompt.contains("LOOP_COMPLETE"));
1241 }
1242
1243 #[test]
1244 fn test_should_handle_always_true() {
1245 let config = RalphConfig::default();
1246 let registry = HatRegistry::new();
1247 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1248
1249 assert!(ralph.should_handle(&Topic::new("any.topic")));
1250 assert!(ralph.should_handle(&Topic::new("build.task")));
1251 assert!(ralph.should_handle(&Topic::new("unknown.event")));
1252 }
1253
1254 #[test]
1255 fn test_rfc2119_patterns_present() {
1256 let config = RalphConfig::default();
1257 let registry = HatRegistry::new();
1258 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1259
1260 let prompt = ralph.build_prompt("", &[]);
1261
1262 assert!(
1264 prompt.contains("You MUST study"),
1265 "Should use RFC2119 MUST with 'study' verb"
1266 );
1267 assert!(
1268 prompt.contains("You MUST complete only one atomic task"),
1269 "Should have RFC2119 MUST complete atomic task constraint"
1270 );
1271 assert!(
1272 prompt.contains("You MAY use parallel subagents"),
1273 "Should mention parallel subagents with MAY"
1274 );
1275 assert!(
1276 prompt.contains("You MUST NOT use more than 1 subagent"),
1277 "Should limit to 1 subagent for builds with MUST NOT"
1278 );
1279 assert!(
1280 prompt.contains("You MUST capture the why"),
1281 "Should emphasize 'why' in commits with MUST"
1282 );
1283
1284 assert!(
1286 prompt.contains("### GUARDRAILS"),
1287 "Should have guardrails section"
1288 );
1289 assert!(
1290 prompt.contains("999."),
1291 "Guardrails should use high numbers"
1292 );
1293 }
1294
1295 #[test]
1296 fn test_scratchpad_format_documented() {
1297 let config = RalphConfig::default();
1298 let registry = HatRegistry::new();
1299 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1300
1301 let prompt = ralph.build_prompt("", &[]);
1302
1303 assert!(prompt.contains("auto-injected"));
1305 assert!(prompt.contains("**Always append**"));
1306 }
1307
1308 #[test]
1309 fn test_starting_event_in_prompt() {
1310 let yaml = r#"
1312hats:
1313 tdd_writer:
1314 name: "TDD Writer"
1315 triggers: ["tdd.start"]
1316 publishes: ["test.written"]
1317"#;
1318 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1319 let registry = HatRegistry::from_config(&config);
1320 let ralph = HatlessRalph::new(
1321 "LOOP_COMPLETE",
1322 config.core.clone(),
1323 ®istry,
1324 Some("tdd.start".to_string()),
1325 );
1326
1327 let prompt = ralph.build_prompt("", &[]);
1328
1329 assert!(
1331 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1332 "Prompt should include starting_event delegation instruction"
1333 );
1334 }
1335
1336 #[test]
1337 fn test_no_starting_event_instruction_when_none() {
1338 let yaml = r#"
1340hats:
1341 some_hat:
1342 name: "Some Hat"
1343 triggers: ["some.event"]
1344"#;
1345 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1346 let registry = HatRegistry::from_config(&config);
1347 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1348
1349 let prompt = ralph.build_prompt("", &[]);
1350
1351 assert!(
1353 !prompt.contains("After coordination, publish"),
1354 "Prompt should NOT include starting_event delegation when None"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_hat_instructions_propagated_to_prompt() {
1360 let yaml = r#"
1363hats:
1364 tdd_writer:
1365 name: "TDD Writer"
1366 triggers: ["tdd.start"]
1367 publishes: ["test.written"]
1368 instructions: |
1369 You are a Test-Driven Development specialist.
1370 Always write failing tests before implementation.
1371 Focus on edge cases and error handling.
1372"#;
1373 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1374 let registry = HatRegistry::from_config(&config);
1375 let ralph = HatlessRalph::new(
1376 "LOOP_COMPLETE",
1377 config.core.clone(),
1378 ®istry,
1379 Some("tdd.start".to_string()),
1380 );
1381
1382 let tdd_writer = registry
1384 .get(&ralph_proto::HatId::new("tdd_writer"))
1385 .unwrap();
1386 let prompt = ralph.build_prompt("", &[tdd_writer]);
1387
1388 assert!(
1390 prompt.contains("### TDD Writer Instructions"),
1391 "Prompt should include hat instructions section header"
1392 );
1393 assert!(
1394 prompt.contains("Test-Driven Development specialist"),
1395 "Prompt should include actual instructions content"
1396 );
1397 assert!(
1398 prompt.contains("Always write failing tests"),
1399 "Prompt should include full instructions"
1400 );
1401 }
1402
1403 #[test]
1404 fn test_empty_instructions_not_rendered() {
1405 let yaml = r#"
1407hats:
1408 builder:
1409 name: "Builder"
1410 triggers: ["build.task"]
1411 publishes: ["build.done"]
1412"#;
1413 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1414 let registry = HatRegistry::from_config(&config);
1415 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1416
1417 let prompt = ralph.build_prompt("", &[]);
1418
1419 assert!(
1421 !prompt.contains("### Builder Instructions"),
1422 "Prompt should NOT include instructions section for hat with empty instructions"
1423 );
1424 }
1425
1426 #[test]
1427 fn test_multiple_hats_with_instructions() {
1428 let yaml = r#"
1430hats:
1431 planner:
1432 name: "Planner"
1433 triggers: ["planning.start"]
1434 publishes: ["build.task"]
1435 instructions: "Plan carefully before implementation."
1436 builder:
1437 name: "Builder"
1438 triggers: ["build.task"]
1439 publishes: ["build.done"]
1440 instructions: "Focus on clean, testable code."
1441"#;
1442 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1443 let registry = HatRegistry::from_config(&config);
1444 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1445
1446 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1448 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1449 let prompt = ralph.build_prompt("", &[planner, builder]);
1450
1451 assert!(
1453 prompt.contains("### Planner Instructions"),
1454 "Prompt should include Planner instructions section"
1455 );
1456 assert!(
1457 prompt.contains("Plan carefully before implementation"),
1458 "Prompt should include Planner instructions content"
1459 );
1460 assert!(
1461 prompt.contains("### Builder Instructions"),
1462 "Prompt should include Builder instructions section"
1463 );
1464 assert!(
1465 prompt.contains("Focus on clean, testable code"),
1466 "Prompt should include Builder instructions content"
1467 );
1468 }
1469
1470 #[test]
1471 fn test_fast_path_with_starting_event() {
1472 let yaml = r#"
1475core:
1476 scratchpad: "/nonexistent/path/scratchpad.md"
1477hats:
1478 tdd_writer:
1479 name: "TDD Writer"
1480 triggers: ["tdd.start"]
1481 publishes: ["test.written"]
1482"#;
1483 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1484 let registry = HatRegistry::from_config(&config);
1485 let ralph = HatlessRalph::new(
1486 "LOOP_COMPLETE",
1487 config.core.clone(),
1488 ®istry,
1489 Some("tdd.start".to_string()),
1490 );
1491
1492 let prompt = ralph.build_prompt("", &[]);
1493
1494 assert!(
1496 prompt.contains("FAST PATH"),
1497 "Prompt should indicate fast path when starting_event set and no scratchpad"
1498 );
1499 assert!(
1500 prompt.contains("You MUST publish `tdd.start` immediately"),
1501 "Prompt should instruct immediate event publishing with MUST"
1502 );
1503 assert!(
1504 prompt.contains("ralph emit \"tdd.start\""),
1505 "Fast path should require explicit event emission"
1506 );
1507 assert!(
1508 !prompt.contains("### 1. PLAN"),
1509 "Fast path should skip PLAN step"
1510 );
1511 }
1512
1513 #[test]
1514 fn test_events_context_included_in_prompt() {
1515 let config = RalphConfig::default();
1519 let registry = HatRegistry::new();
1520 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1521
1522 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1523[build.done] Build completed successfully";
1524
1525 let prompt = ralph.build_prompt(events_context, &[]);
1526
1527 assert!(
1528 prompt.contains("## PENDING EVENTS"),
1529 "Prompt should contain PENDING EVENTS section"
1530 );
1531 assert!(
1532 prompt.contains("Review this code for security vulnerabilities"),
1533 "Prompt should contain the user's task"
1534 );
1535 assert!(
1536 prompt.contains("Build completed successfully"),
1537 "Prompt should contain all events from context"
1538 );
1539 }
1540
1541 #[test]
1542 fn test_empty_context_no_pending_events_section() {
1543 let config = RalphConfig::default();
1547 let registry = HatRegistry::new();
1548 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1549
1550 let prompt = ralph.build_prompt("", &[]);
1551
1552 assert!(
1553 !prompt.contains("## PENDING EVENTS"),
1554 "Empty context should not produce PENDING EVENTS section"
1555 );
1556 }
1557
1558 #[test]
1559 fn test_whitespace_only_context_no_pending_events_section() {
1560 let config = RalphConfig::default();
1564 let registry = HatRegistry::new();
1565 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1566
1567 let prompt = ralph.build_prompt(" \n\t ", &[]);
1568
1569 assert!(
1570 !prompt.contains("## PENDING EVENTS"),
1571 "Whitespace-only context should not produce PENDING EVENTS section"
1572 );
1573 }
1574
1575 #[test]
1576 fn test_events_section_before_workflow() {
1577 let config = RalphConfig::default();
1581 let registry = HatRegistry::new();
1582 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1583
1584 let events_context = "[task.start] Implement feature X";
1585 let prompt = ralph.build_prompt(events_context, &[]);
1586
1587 let events_pos = prompt
1588 .find("## PENDING EVENTS")
1589 .expect("Should have PENDING EVENTS");
1590 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1591
1592 assert!(
1593 events_pos < workflow_pos,
1594 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1595 events_pos,
1596 workflow_pos
1597 );
1598 }
1599
1600 #[test]
1603 fn test_only_active_hat_instructions_included() {
1604 let yaml = r#"
1606hats:
1607 security_reviewer:
1608 name: "Security Reviewer"
1609 triggers: ["review.security"]
1610 instructions: "Review code for security vulnerabilities."
1611 architecture_reviewer:
1612 name: "Architecture Reviewer"
1613 triggers: ["review.architecture"]
1614 instructions: "Review system design and architecture."
1615 correctness_reviewer:
1616 name: "Correctness Reviewer"
1617 triggers: ["review.correctness"]
1618 instructions: "Review logic and correctness."
1619"#;
1620 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1621 let registry = HatRegistry::from_config(&config);
1622 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1623
1624 let security_hat = registry
1626 .get(&ralph_proto::HatId::new("security_reviewer"))
1627 .unwrap();
1628 let active_hats = vec![security_hat];
1629
1630 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1631
1632 assert!(
1634 prompt.contains("### Security Reviewer Instructions"),
1635 "Should include Security Reviewer instructions section"
1636 );
1637 assert!(
1638 prompt.contains("Review code for security vulnerabilities"),
1639 "Should include Security Reviewer instructions content"
1640 );
1641
1642 assert!(
1644 !prompt.contains("### Architecture Reviewer Instructions"),
1645 "Should NOT include Architecture Reviewer instructions"
1646 );
1647 assert!(
1648 !prompt.contains("Review system design and architecture"),
1649 "Should NOT include Architecture Reviewer instructions content"
1650 );
1651 assert!(
1652 !prompt.contains("### Correctness Reviewer Instructions"),
1653 "Should NOT include Correctness Reviewer instructions"
1654 );
1655 }
1656
1657 #[test]
1658 fn test_multiple_active_hats_all_included() {
1659 let yaml = r#"
1661hats:
1662 security_reviewer:
1663 name: "Security Reviewer"
1664 triggers: ["review.security"]
1665 instructions: "Review code for security vulnerabilities."
1666 architecture_reviewer:
1667 name: "Architecture Reviewer"
1668 triggers: ["review.architecture"]
1669 instructions: "Review system design and architecture."
1670 correctness_reviewer:
1671 name: "Correctness Reviewer"
1672 triggers: ["review.correctness"]
1673 instructions: "Review logic and correctness."
1674"#;
1675 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1676 let registry = HatRegistry::from_config(&config);
1677 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1678
1679 let security_hat = registry
1681 .get(&ralph_proto::HatId::new("security_reviewer"))
1682 .unwrap();
1683 let arch_hat = registry
1684 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1685 .unwrap();
1686 let active_hats = vec![security_hat, arch_hat];
1687
1688 let prompt = ralph.build_prompt("Events", &active_hats);
1689
1690 assert!(
1692 prompt.contains("### Security Reviewer Instructions"),
1693 "Should include Security Reviewer instructions"
1694 );
1695 assert!(
1696 prompt.contains("Review code for security vulnerabilities"),
1697 "Should include Security Reviewer content"
1698 );
1699 assert!(
1700 prompt.contains("### Architecture Reviewer Instructions"),
1701 "Should include Architecture Reviewer instructions"
1702 );
1703 assert!(
1704 prompt.contains("Review system design and architecture"),
1705 "Should include Architecture Reviewer content"
1706 );
1707
1708 assert!(
1710 !prompt.contains("### Correctness Reviewer Instructions"),
1711 "Should NOT include Correctness Reviewer instructions"
1712 );
1713 }
1714
1715 #[test]
1716 fn test_no_active_hats_no_instructions() {
1717 let yaml = r#"
1719hats:
1720 security_reviewer:
1721 name: "Security Reviewer"
1722 triggers: ["review.security"]
1723 instructions: "Review code for security vulnerabilities."
1724"#;
1725 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1726 let registry = HatRegistry::from_config(&config);
1727 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1728
1729 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1731
1732 let prompt = ralph.build_prompt("Events", &active_hats);
1733
1734 assert!(
1736 !prompt.contains("### Security Reviewer Instructions"),
1737 "Should NOT include instructions when no active hats"
1738 );
1739 assert!(
1740 !prompt.contains("Review code for security vulnerabilities"),
1741 "Should NOT include instructions content when no active hats"
1742 );
1743
1744 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1746 assert!(
1747 prompt.contains("| Hat | Triggers On | Publishes |"),
1748 "Should still have topology table"
1749 );
1750 }
1751
1752 #[test]
1753 fn test_topology_table_only_when_ralph_coordinating() {
1754 let yaml = r#"
1757hats:
1758 security_reviewer:
1759 name: "Security Reviewer"
1760 triggers: ["review.security"]
1761 instructions: "Security instructions."
1762 architecture_reviewer:
1763 name: "Architecture Reviewer"
1764 triggers: ["review.architecture"]
1765 instructions: "Architecture instructions."
1766"#;
1767 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1768 let registry = HatRegistry::from_config(&config);
1769 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1770
1771 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1773
1774 assert!(
1775 prompt_coordinating.contains("## HATS"),
1776 "Should have HATS section when coordinating"
1777 );
1778 assert!(
1779 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1780 "Should have topology table when coordinating"
1781 );
1782 assert!(
1783 prompt_coordinating.contains("```mermaid"),
1784 "Should have Mermaid diagram when coordinating"
1785 );
1786
1787 let security_hat = registry
1789 .get(&ralph_proto::HatId::new("security_reviewer"))
1790 .unwrap();
1791 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1792
1793 assert!(
1794 prompt_active.contains("## ACTIVE HAT"),
1795 "Should have ACTIVE HAT section when hat is active"
1796 );
1797 assert!(
1798 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1799 "Should NOT have topology table when hat is active"
1800 );
1801 assert!(
1802 !prompt_active.contains("```mermaid"),
1803 "Should NOT have Mermaid diagram when hat is active"
1804 );
1805 assert!(
1806 prompt_active.contains("### Security Reviewer Instructions"),
1807 "Should still have the active hat's instructions"
1808 );
1809 }
1810
1811 #[test]
1814 fn test_scratchpad_always_included() {
1815 let config = RalphConfig::default();
1817 let registry = HatRegistry::new();
1818 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1819
1820 let prompt = ralph.build_prompt("", &[]);
1821
1822 assert!(
1823 prompt.contains("### 0b. SCRATCHPAD"),
1824 "Scratchpad section should be included"
1825 );
1826 assert!(
1827 prompt.contains("`.ralph/agent/scratchpad.md`"),
1828 "Scratchpad path should be referenced"
1829 );
1830 assert!(
1831 prompt.contains("auto-injected"),
1832 "Auto-injection should be documented"
1833 );
1834 }
1835
1836 #[test]
1837 fn test_scratchpad_included_with_memories_enabled() {
1838 let config = RalphConfig::default();
1840 let registry = HatRegistry::new();
1841 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1842 .with_memories_enabled(true);
1843
1844 let prompt = ralph.build_prompt("", &[]);
1845
1846 assert!(
1848 prompt.contains("### 0b. SCRATCHPAD"),
1849 "Scratchpad section should be included even with memories enabled"
1850 );
1851 assert!(
1852 prompt.contains("**Always append**"),
1853 "Append instruction should be documented"
1854 );
1855
1856 assert!(
1858 !prompt.contains("### 0c. TASKS"),
1859 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1860 );
1861 }
1862
1863 #[test]
1864 fn test_no_tasks_section_in_core_prompt() {
1865 let config = RalphConfig::default();
1867 let registry = HatRegistry::new();
1868 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1869
1870 let prompt = ralph.build_prompt("", &[]);
1871
1872 assert!(
1874 !prompt.contains("### 0c. TASKS"),
1875 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1876 );
1877 }
1878
1879 #[test]
1880 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1881 let config = RalphConfig::default();
1883 let registry = HatRegistry::new();
1884 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1885 .with_memories_enabled(true);
1886
1887 let prompt = ralph.build_prompt("", &[]);
1888
1889 assert!(
1891 prompt.contains("update scratchpad"),
1892 "Workflow should reference scratchpad when memories enabled"
1893 );
1894 assert!(
1896 prompt.contains("ralph tools task"),
1897 "Workflow should reference tasks CLI when memories enabled"
1898 );
1899 }
1900
1901 #[test]
1902 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1903 let yaml = r#"
1905hats:
1906 builder:
1907 name: "Builder"
1908 triggers: ["build.task"]
1909 publishes: ["build.done"]
1910"#;
1911 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1912 let registry = HatRegistry::from_config(&config);
1913 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1914 .with_memories_enabled(true);
1915
1916 let prompt = ralph.build_prompt("", &[]);
1917
1918 assert!(
1920 prompt.contains("scratchpad"),
1921 "Multi-hat workflow should reference scratchpad when memories enabled"
1922 );
1923 assert!(
1925 prompt.contains("ralph tools task ensure"),
1926 "Multi-hat workflow should reference tasks CLI when memories enabled"
1927 );
1928 }
1929
1930 #[test]
1931 fn test_guardrails_adapt_to_memories_mode() {
1932 let config = RalphConfig::default();
1934 let registry = HatRegistry::new();
1935 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1936 .with_memories_enabled(true);
1937
1938 let prompt = ralph.build_prompt("", &[]);
1939
1940 assert!(
1944 prompt.contains("### GUARDRAILS"),
1945 "Guardrails section should be present"
1946 );
1947 }
1948
1949 #[test]
1950 fn test_guardrails_present_without_memories() {
1951 let config = RalphConfig::default();
1953 let registry = HatRegistry::new();
1954 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1955 let prompt = ralph.build_prompt("", &[]);
1958
1959 assert!(
1960 prompt.contains("### GUARDRAILS"),
1961 "Guardrails section should be present"
1962 );
1963 }
1964
1965 #[test]
1968 fn test_task_closure_verification_in_done_section() {
1969 let config = RalphConfig::default();
1972 let registry = HatRegistry::new();
1973 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1974 .with_memories_enabled(true);
1975
1976 let prompt = ralph.build_prompt("", &[]);
1977
1978 assert!(
1981 prompt.contains("ralph tools task list"),
1982 "Should reference task list command in DONE section"
1983 );
1984 assert!(
1985 prompt.contains("MUST NOT declare completion while tasks remain open"),
1986 "Should require tasks closed before completion"
1987 );
1988 }
1989
1990 #[test]
1991 fn test_workflow_verify_and_commit_step() {
1992 let config = RalphConfig::default();
1994 let registry = HatRegistry::new();
1995 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1996 .with_memories_enabled(true);
1997
1998 let prompt = ralph.build_prompt("", &[]);
1999
2000 assert!(
2002 prompt.contains("### 4. VERIFY & COMMIT"),
2003 "Should have VERIFY & COMMIT step in workflow"
2004 );
2005 assert!(
2006 prompt.contains("run tests and verify"),
2007 "Should require verification"
2008 );
2009 assert!(
2010 prompt.contains("ralph tools task start"),
2011 "Should reference task start command"
2012 );
2013 assert!(
2014 prompt.contains("ralph tools task close"),
2015 "Should reference task close command"
2016 );
2017 }
2018
2019 #[test]
2020 fn test_scratchpad_mode_still_has_commit_step() {
2021 let config = RalphConfig::default();
2023 let registry = HatRegistry::new();
2024 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2025 let prompt = ralph.build_prompt("", &[]);
2028
2029 assert!(
2031 prompt.contains("### 4. COMMIT"),
2032 "Should have COMMIT step in workflow"
2033 );
2034 assert!(
2035 prompt.contains("mark the task `[x]`"),
2036 "Should mark task in scratchpad"
2037 );
2038 assert!(
2040 !prompt.contains("### 0c. TASKS"),
2041 "Scratchpad mode should not have TASKS section"
2042 );
2043 }
2044
2045 #[test]
2048 fn test_objective_section_present_with_set_objective() {
2049 let config = RalphConfig::default();
2051 let registry = HatRegistry::new();
2052 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2053 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
2054
2055 let prompt = ralph.build_prompt("", &[]);
2056
2057 assert!(
2058 prompt.contains("## OBJECTIVE"),
2059 "Should have OBJECTIVE section when objective is set"
2060 );
2061 assert!(
2062 prompt.contains("Implement user authentication with JWT tokens"),
2063 "OBJECTIVE should contain the original user prompt"
2064 );
2065 assert!(
2066 prompt.contains("This is your primary goal"),
2067 "OBJECTIVE should emphasize this is the primary goal"
2068 );
2069 }
2070
2071 #[test]
2072 fn test_objective_reinforced_in_done_section() {
2073 let config = RalphConfig::default();
2076 let registry = HatRegistry::new();
2077 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2078 ralph.set_objective("Fix the login bug in auth module".to_string());
2079
2080 let prompt = ralph.build_prompt("", &[]);
2081
2082 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2084 let after_done = &prompt[done_pos..];
2085
2086 assert!(
2087 after_done.contains("Remember your objective"),
2088 "DONE section should remind about objective"
2089 );
2090 assert!(
2091 after_done.contains("Fix the login bug in auth module"),
2092 "DONE section should restate the objective"
2093 );
2094 }
2095
2096 #[test]
2097 fn test_objective_appears_before_pending_events() {
2098 let config = RalphConfig::default();
2100 let registry = HatRegistry::new();
2101 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2102 ralph.set_objective("Build feature X".to_string());
2103
2104 let context = "Event: task.start - Build feature X";
2105 let prompt = ralph.build_prompt(context, &[]);
2106
2107 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2108 let events_pos = prompt
2109 .find("## PENDING EVENTS")
2110 .expect("Should have PENDING EVENTS");
2111
2112 assert!(
2113 objective_pos < events_pos,
2114 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
2115 objective_pos,
2116 events_pos
2117 );
2118 }
2119
2120 #[test]
2121 fn test_no_objective_when_not_set() {
2122 let config = RalphConfig::default();
2124 let registry = HatRegistry::new();
2125 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2126
2127 let context = "Event: build.done - Build completed successfully";
2128 let prompt = ralph.build_prompt(context, &[]);
2129
2130 assert!(
2131 !prompt.contains("## OBJECTIVE"),
2132 "Should NOT have OBJECTIVE section when objective not set"
2133 );
2134 }
2135
2136 #[test]
2137 fn test_objective_set_correctly() {
2138 let config = RalphConfig::default();
2140 let registry = HatRegistry::new();
2141 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2142 ralph.set_objective("Review this PR for security issues".to_string());
2143
2144 let prompt = ralph.build_prompt("", &[]);
2145
2146 assert!(
2147 prompt.contains("Review this PR for security issues"),
2148 "Should show the stored objective"
2149 );
2150 }
2151
2152 #[test]
2153 fn test_objective_with_events_context() {
2154 let config = RalphConfig::default();
2156 let registry = HatRegistry::new();
2157 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2158 ralph.set_objective("Implement feature Y".to_string());
2159
2160 let context =
2161 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
2162 let prompt = ralph.build_prompt(context, &[]);
2163
2164 assert!(
2165 prompt.contains("## OBJECTIVE"),
2166 "Should have OBJECTIVE section"
2167 );
2168 assert!(
2169 prompt.contains("Implement feature Y"),
2170 "OBJECTIVE should contain the stored objective"
2171 );
2172 }
2173
2174 #[test]
2175 fn test_done_section_without_objective() {
2176 let config = RalphConfig::default();
2178 let registry = HatRegistry::new();
2179 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2180
2181 let prompt = ralph.build_prompt("", &[]);
2182
2183 assert!(prompt.contains("## DONE"), "Should have DONE section");
2184 assert!(
2185 prompt.contains("LOOP_COMPLETE"),
2186 "DONE should mention completion event"
2187 );
2188 assert!(
2189 prompt.contains("final non-empty line"),
2190 "Solo DONE section should require literal terminal output"
2191 );
2192 assert!(
2193 !prompt.contains("Remember your objective"),
2194 "Should NOT have objective reinforcement without objective"
2195 );
2196 }
2197
2198 #[test]
2199 fn test_objective_persists_across_iterations() {
2200 let config = RalphConfig::default();
2203 let registry = HatRegistry::new();
2204 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2205 ralph.set_objective("Build a REST API with authentication".to_string());
2206
2207 let context = "Event: build.done - Build completed";
2209 let prompt = ralph.build_prompt(context, &[]);
2210
2211 assert!(
2212 prompt.contains("## OBJECTIVE"),
2213 "OBJECTIVE should persist even without task.start in context"
2214 );
2215 assert!(
2216 prompt.contains("Build a REST API with authentication"),
2217 "Stored objective should appear in later iterations"
2218 );
2219 }
2220
2221 #[test]
2222 fn test_done_section_suppressed_when_hat_active() {
2223 let yaml = r#"
2225hats:
2226 builder:
2227 name: "Builder"
2228 triggers: ["build.task"]
2229 publishes: ["build.done"]
2230 instructions: "Build the code."
2231"#;
2232 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2233 let registry = HatRegistry::from_config(&config);
2234 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2235 ralph.set_objective("Implement feature X".to_string());
2236
2237 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2238 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
2239
2240 assert!(
2241 !prompt.contains("## DONE"),
2242 "DONE section should be suppressed when a hat is active"
2243 );
2244 assert!(
2245 !prompt.contains("LOOP_COMPLETE"),
2246 "Completion promise should NOT appear when a hat is active"
2247 );
2248 assert!(
2250 prompt.contains("## OBJECTIVE"),
2251 "OBJECTIVE should still appear even when hat is active"
2252 );
2253 assert!(
2254 prompt.contains("Implement feature X"),
2255 "Objective content should be visible to active hat"
2256 );
2257 }
2258
2259 #[test]
2260 fn test_done_section_present_when_coordinating() {
2261 let yaml = r#"
2263hats:
2264 builder:
2265 name: "Builder"
2266 triggers: ["build.task"]
2267 publishes: ["build.done"]
2268"#;
2269 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2270 let registry = HatRegistry::from_config(&config);
2271 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2272 ralph.set_objective("Complete the TDD cycle".to_string());
2273
2274 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
2276
2277 assert!(
2278 prompt.contains("## DONE"),
2279 "DONE section should appear when Ralph is coordinating"
2280 );
2281 assert!(
2282 prompt.contains("LOOP_COMPLETE"),
2283 "Completion promise should appear when coordinating"
2284 );
2285 assert!(
2286 prompt.contains("via `ralph emit`"),
2287 "Coordinating DONE section should require explicit event emission"
2288 );
2289 }
2290
2291 #[test]
2292 fn test_objective_in_done_section_when_coordinating() {
2293 let config = RalphConfig::default();
2295 let registry = HatRegistry::new();
2296 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2297 ralph.set_objective("Deploy the application".to_string());
2298
2299 let prompt = ralph.build_prompt("", &[]);
2300
2301 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2302 let after_done = &prompt[done_pos..];
2303
2304 assert!(
2305 after_done.contains("Remember your objective"),
2306 "DONE section should remind about objective when coordinating"
2307 );
2308 assert!(
2309 after_done.contains("Deploy the application"),
2310 "DONE section should contain the objective text"
2311 );
2312 }
2313
2314 #[test]
2317 fn test_event_publishing_guide_with_receivers() {
2318 let yaml = r#"
2321hats:
2322 builder:
2323 name: "Builder"
2324 description: "Builds and tests code"
2325 triggers: ["build.task"]
2326 publishes: ["build.done", "build.blocked"]
2327 confessor:
2328 name: "Confessor"
2329 description: "Produces a ConfessionReport; rewarded for honesty"
2330 triggers: ["build.done"]
2331 publishes: ["confession.done"]
2332"#;
2333 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2334 let registry = HatRegistry::from_config(&config);
2335 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2336
2337 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2339 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2340
2341 assert!(
2343 prompt.contains("### Event Publishing Guide"),
2344 "Should include Event Publishing Guide section"
2345 );
2346 assert!(
2347 prompt.contains("When you publish:"),
2348 "Guide should explain what happens when publishing"
2349 );
2350 assert!(
2351 prompt.contains("You MUST use `ralph emit"),
2352 "Guide should require explicit event emission"
2353 );
2354 assert!(
2356 prompt.contains("`build.done` → Received by: Confessor"),
2357 "Should show Confessor receives build.done"
2358 );
2359 assert!(
2360 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2361 "Should include receiver's description"
2362 );
2363 assert!(
2365 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2366 "Should show Ralph receives orphan events"
2367 );
2368 }
2369
2370 #[test]
2371 fn test_event_publishing_guide_no_publishes() {
2372 let yaml = r#"
2374hats:
2375 observer:
2376 name: "Observer"
2377 description: "Only observes"
2378 triggers: ["events.*"]
2379"#;
2380 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2381 let registry = HatRegistry::from_config(&config);
2382 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2383
2384 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2385 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2386
2387 assert!(
2389 !prompt.contains("### Event Publishing Guide"),
2390 "Should NOT include Event Publishing Guide when hat has no publishes"
2391 );
2392 }
2393
2394 #[test]
2395 fn test_event_publishing_guide_all_orphan_events() {
2396 let yaml = r#"
2398hats:
2399 solo:
2400 name: "Solo"
2401 triggers: ["solo.start"]
2402 publishes: ["solo.done", "solo.failed"]
2403"#;
2404 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2405 let registry = HatRegistry::from_config(&config);
2406 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2407
2408 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2409 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2410
2411 assert!(
2412 prompt.contains("### Event Publishing Guide"),
2413 "Should include guide even for orphan events"
2414 );
2415 assert!(
2416 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2417 "Orphan solo.done should go to Ralph"
2418 );
2419 assert!(
2420 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2421 "Orphan solo.failed should go to Ralph"
2422 );
2423 }
2424
2425 #[test]
2426 fn test_event_publishing_guide_multiple_receivers() {
2427 let yaml = r#"
2429hats:
2430 broadcaster:
2431 name: "Broadcaster"
2432 triggers: ["broadcast.start"]
2433 publishes: ["signal.sent"]
2434 listener1:
2435 name: "Listener1"
2436 description: "First listener"
2437 triggers: ["signal.sent"]
2438 listener2:
2439 name: "Listener2"
2440 description: "Second listener"
2441 triggers: ["signal.sent"]
2442"#;
2443 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2444 let registry = HatRegistry::from_config(&config);
2445 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2446
2447 let broadcaster = registry
2448 .get(&ralph_proto::HatId::new("broadcaster"))
2449 .unwrap();
2450 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2451
2452 assert!(
2453 prompt.contains("### Event Publishing Guide"),
2454 "Should include guide"
2455 );
2456 assert!(
2458 prompt.contains("Listener1 (First listener)"),
2459 "Should list Listener1 as receiver"
2460 );
2461 assert!(
2462 prompt.contains("Listener2 (Second listener)"),
2463 "Should list Listener2 as receiver"
2464 );
2465 }
2466
2467 #[test]
2468 fn test_event_publishing_guide_includes_self() {
2469 let yaml = r#"
2471hats:
2472 looper:
2473 name: "Looper"
2474 triggers: ["loop.continue", "loop.start"]
2475 publishes: ["loop.continue"]
2476"#;
2477 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2478 let registry = HatRegistry::from_config(&config);
2479 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2480
2481 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2482 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2483
2484 assert!(
2485 prompt.contains("### Event Publishing Guide"),
2486 "Should include guide"
2487 );
2488 assert!(
2490 prompt.contains("`loop.continue` → Received by: Looper"),
2491 "Self-loop event should show the hat itself as receiver"
2492 );
2493 }
2494
2495 #[test]
2496 fn test_event_publishing_guide_self_loop_shows_self_as_receiver() {
2497 let yaml = r#"
2500hats:
2501 processor:
2502 name: "Processor"
2503 description: "Processes work with retry"
2504 triggers: ["start", "process.retry"]
2505 publishes: ["process.done", "process.retry"]
2506 validator:
2507 name: "Validator"
2508 triggers: ["process.done"]
2509 publishes: ["validate.pass"]
2510"#;
2511 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2512 let registry = HatRegistry::from_config(&config);
2513 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2514
2515 let processor = registry.get(&ralph_proto::HatId::new("processor")).unwrap();
2516 let prompt = ralph.build_prompt("[start] Go", &[processor]);
2517
2518 assert!(
2520 prompt.contains("`process.retry` → Received by: Processor"),
2521 "Self-loop event should show the hat itself as receiver, not Ralph. Got:\n{}",
2522 prompt
2523 .lines()
2524 .filter(|l| l.contains("process.retry"))
2525 .collect::<Vec<_>>()
2526 .join("\n")
2527 );
2528 assert!(
2530 prompt.contains("`process.done` → Received by: Validator"),
2531 "Non-self event should still show correct receiver"
2532 );
2533 }
2534
2535 #[test]
2536 fn test_event_publishing_guide_receiver_without_description() {
2537 let yaml = r#"
2539hats:
2540 sender:
2541 name: "Sender"
2542 triggers: ["send.start"]
2543 publishes: ["message.sent"]
2544 receiver:
2545 name: "NoDescReceiver"
2546 triggers: ["message.sent"]
2547"#;
2548 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2549 let registry = HatRegistry::from_config(&config);
2550 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2551
2552 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2553 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2554
2555 assert!(
2556 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2557 "Should show receiver name without parentheses when no description"
2558 );
2559 assert!(
2561 !prompt.contains("NoDescReceiver ()"),
2562 "Should NOT have empty parentheses for receiver without description"
2563 );
2564 }
2565
2566 #[test]
2569 fn test_constraint_lists_valid_events_when_coordinating() {
2570 let yaml = r#"
2573hats:
2574 test_writer:
2575 name: "Test Writer"
2576 triggers: ["tdd.start"]
2577 publishes: ["test.written"]
2578 implementer:
2579 name: "Implementer"
2580 triggers: ["test.written"]
2581 publishes: ["test.passing"]
2582"#;
2583 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2584 let registry = HatRegistry::from_config(&config);
2585 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2586
2587 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2589
2590 assert!(
2592 prompt.contains("**CONSTRAINT:**"),
2593 "Prompt should include CONSTRAINT when coordinating"
2594 );
2595 assert!(
2596 prompt.contains("tdd.start"),
2597 "CONSTRAINT should list tdd.start as valid event"
2598 );
2599 assert!(
2600 prompt.contains("test.written"),
2601 "CONSTRAINT should list test.written as valid event"
2602 );
2603 assert!(
2604 prompt.contains("Publishing other events will have no effect"),
2605 "CONSTRAINT should warn about invalid events"
2606 );
2607 }
2608
2609 #[test]
2610 fn test_no_constraint_when_hat_is_active() {
2611 let yaml = r#"
2614hats:
2615 builder:
2616 name: "Builder"
2617 triggers: ["build.task"]
2618 publishes: ["build.done"]
2619 instructions: "Build the code."
2620"#;
2621 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2622 let registry = HatRegistry::from_config(&config);
2623 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2624
2625 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2627 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2628
2629 assert!(
2631 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2632 "Active hat should NOT have coordinating CONSTRAINT"
2633 );
2634
2635 assert!(
2637 prompt.contains("### Event Publishing Guide"),
2638 "Active hat should have Event Publishing Guide"
2639 );
2640 }
2641
2642 #[test]
2643 fn test_no_constraint_when_no_hats() {
2644 let config = RalphConfig::default();
2646 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2648
2649 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2650
2651 assert!(
2653 !prompt.contains("**CONSTRAINT:**"),
2654 "Solo mode should NOT have CONSTRAINT"
2655 );
2656 }
2657
2658 #[test]
2661 fn test_single_guidance_injection() {
2662 let config = RalphConfig::default();
2664 let registry = HatRegistry::new();
2665 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2666 ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2667
2668 let prompt = ralph.build_prompt("", &[]);
2669
2670 assert!(
2671 prompt.contains("## ROBOT GUIDANCE"),
2672 "Should include ROBOT GUIDANCE section"
2673 );
2674 assert!(
2675 prompt.contains("Focus on error handling first"),
2676 "Should contain the guidance message"
2677 );
2678 assert!(
2680 !prompt.contains("1. Focus on error handling first"),
2681 "Single guidance should not be numbered"
2682 );
2683 }
2684
2685 #[test]
2686 fn test_multiple_guidance_squashing() {
2687 let config = RalphConfig::default();
2689 let registry = HatRegistry::new();
2690 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2691 ralph.set_robot_guidance(vec![
2692 "Focus on error handling".to_string(),
2693 "Use the existing retry pattern".to_string(),
2694 "Check edge cases for empty input".to_string(),
2695 ]);
2696
2697 let prompt = ralph.build_prompt("", &[]);
2698
2699 assert!(
2700 prompt.contains("## ROBOT GUIDANCE"),
2701 "Should include ROBOT GUIDANCE section"
2702 );
2703 assert!(
2704 prompt.contains("1. Focus on error handling"),
2705 "First guidance should be numbered 1"
2706 );
2707 assert!(
2708 prompt.contains("2. Use the existing retry pattern"),
2709 "Second guidance should be numbered 2"
2710 );
2711 assert!(
2712 prompt.contains("3. Check edge cases for empty input"),
2713 "Third guidance should be numbered 3"
2714 );
2715 }
2716
2717 #[test]
2718 fn test_guidance_appears_in_prompt_before_events() {
2719 let config = RalphConfig::default();
2721 let registry = HatRegistry::new();
2722 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2723 ralph.set_objective("Build feature X".to_string());
2724 ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2725
2726 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2727
2728 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2729 let guidance_pos = prompt
2730 .find("## ROBOT GUIDANCE")
2731 .expect("Should have ROBOT GUIDANCE");
2732 let events_pos = prompt
2733 .find("## PENDING EVENTS")
2734 .expect("Should have PENDING EVENTS");
2735
2736 assert!(
2737 objective_pos < guidance_pos,
2738 "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2739 objective_pos,
2740 guidance_pos
2741 );
2742 assert!(
2743 guidance_pos < events_pos,
2744 "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2745 guidance_pos,
2746 events_pos
2747 );
2748 }
2749
2750 #[test]
2751 fn test_guidance_cleared_after_injection() {
2752 let config = RalphConfig::default();
2754 let registry = HatRegistry::new();
2755 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2756 ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2757
2758 let prompt1 = ralph.build_prompt("", &[]);
2760 assert!(
2761 prompt1.contains("## ROBOT GUIDANCE"),
2762 "First prompt should have guidance"
2763 );
2764
2765 ralph.clear_robot_guidance();
2767
2768 let prompt2 = ralph.build_prompt("", &[]);
2770 assert!(
2771 !prompt2.contains("## ROBOT GUIDANCE"),
2772 "After clearing, prompt should not have guidance"
2773 );
2774 }
2775
2776 #[test]
2777 fn test_no_injection_when_no_guidance() {
2778 let config = RalphConfig::default();
2780 let registry = HatRegistry::new();
2781 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2782
2783 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2784
2785 assert!(
2786 !prompt.contains("## ROBOT GUIDANCE"),
2787 "Should NOT include ROBOT GUIDANCE when no guidance set"
2788 );
2789 }
2790
2791 #[test]
2795 fn test_scratchpad_disabled_suppresses_all_sections() {
2796 use crate::config::ScratchpadConfig;
2797
2798 let config = RalphConfig::default();
2799 let registry = HatRegistry::new();
2800 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2801 .with_memories_enabled(true);
2802 ralph.set_active_scratchpad(ScratchpadConfig {
2803 enabled: false,
2804 path: ".ralph/agent/scratchpad.md".to_string(),
2805 });
2806
2807 let prompt = ralph.build_prompt("", &[]);
2808
2809 assert!(
2811 !prompt.contains("Review your `<scratchpad>`"),
2812 "Disabled scratchpad: ORIENTATION should not reference scratchpad"
2813 );
2814 assert!(
2816 !prompt.contains("### 0b. SCRATCHPAD"),
2817 "Disabled scratchpad: SCRATCHPAD section should be suppressed"
2818 );
2819 assert!(
2821 !prompt.contains("**Scratchpad**"),
2822 "Disabled scratchpad: STATE MANAGEMENT should not reference Scratchpad"
2823 );
2824 assert!(
2825 !prompt.contains("Thinking goes in scratchpad"),
2826 "Disabled scratchpad: Rule should not reference scratchpad"
2827 );
2828 assert!(
2830 !prompt.contains("update scratchpad"),
2831 "Disabled scratchpad: WORKFLOW should not reference scratchpad"
2832 );
2833 assert!(
2835 !prompt.contains("write detailed output to"),
2836 "Disabled scratchpad: EVENT WRITING should not reference scratchpad"
2837 );
2838 }
2839
2840 #[test]
2842 fn test_scratchpad_custom_path_in_prompt() {
2843 use crate::config::ScratchpadConfig;
2844
2845 let config = RalphConfig::default();
2846 let registry = HatRegistry::new();
2847 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2848 .with_memories_enabled(true);
2849 ralph.set_active_scratchpad(ScratchpadConfig {
2850 enabled: true,
2851 path: ".ralph/agent/planner.md".to_string(),
2852 });
2853
2854 let prompt = ralph.build_prompt("", &[]);
2855
2856 assert!(
2858 prompt.contains("`.ralph/agent/planner.md` is your thinking journal"),
2859 "Custom path should appear in SCRATCHPAD section"
2860 );
2861 assert!(
2863 prompt.contains("**Scratchpad** (`.ralph/agent/planner.md`)"),
2864 "STATE MANAGEMENT should reference custom path"
2865 );
2866 assert!(
2868 prompt.contains("`.ralph/agent/planner.md`") && prompt.contains("PLAN"),
2869 "WORKFLOW should reference custom path"
2870 );
2871 assert!(
2873 prompt.contains("write detailed output to `.ralph/agent/planner.md`"),
2874 "EVENT WRITING should use custom scratchpad path"
2875 );
2876 }
2877
2878 #[test]
2880 fn test_scratchpad_inherits_global_path() {
2881 let config = RalphConfig::default();
2882 let registry = HatRegistry::new();
2883 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2884
2885 let prompt = ralph.build_prompt("", &[]);
2886
2887 assert!(
2888 prompt.contains("`.ralph/agent/scratchpad.md`"),
2889 "Default global path should be used"
2890 );
2891 assert!(
2892 prompt.contains("### 0b. SCRATCHPAD"),
2893 "SCRATCHPAD section should be present"
2894 );
2895 }
2896
2897 #[test]
2899 fn test_disabled_scratchpad_is_fresh_start() {
2900 use crate::config::ScratchpadConfig;
2901
2902 let yaml = r#"
2903hats:
2904 tdd_writer:
2905 name: "TDD Writer"
2906 triggers: ["tdd.start"]
2907 publishes: ["test.written"]
2908"#;
2909 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2910 let registry = HatRegistry::from_config(&config);
2911 let mut ralph = HatlessRalph::new(
2912 "LOOP_COMPLETE",
2913 config.core.clone(),
2914 ®istry,
2915 Some("tdd.start".to_string()),
2916 );
2917 ralph.set_active_scratchpad(ScratchpadConfig {
2918 enabled: false,
2919 path: ".ralph/agent/scratchpad.md".to_string(),
2920 });
2921
2922 ralph.set_iteration(0);
2924 let prompt = ralph.build_prompt("", &[]);
2925 assert!(
2926 prompt.contains("FAST PATH"),
2927 "First iteration with disabled scratchpad should trigger fast path"
2928 );
2929
2930 ralph.set_iteration(1);
2932 let prompt = ralph.build_prompt("", &[]);
2933 assert!(
2934 !prompt.contains("FAST PATH"),
2935 "Second iteration should NOT trigger fast path even with disabled scratchpad"
2936 );
2937 }
2938
2939 #[test]
2941 fn test_multiple_hats_different_scratchpad_prompts() {
2942 use crate::config::ScratchpadConfig;
2943
2944 let config = RalphConfig::default();
2945 let registry = HatRegistry::new();
2946
2947 let mut ralph_planner =
2949 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2950 .with_memories_enabled(true);
2951 ralph_planner.set_active_scratchpad(ScratchpadConfig {
2952 enabled: true,
2953 path: ".ralph/agent/planner.md".to_string(),
2954 });
2955 let planner_prompt = ralph_planner.build_prompt("", &[]);
2956 assert!(
2957 planner_prompt.contains("`.ralph/agent/planner.md`"),
2958 "Planner should use custom path"
2959 );
2960
2961 let mut ralph_builder =
2963 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2964 .with_memories_enabled(true);
2965 ralph_builder.set_active_scratchpad(config.core.scratchpad.clone());
2966 let builder_prompt = ralph_builder.build_prompt("", &[]);
2967 assert!(
2968 builder_prompt.contains("`.ralph/agent/scratchpad.md`"),
2969 "Builder should use global path"
2970 );
2971
2972 let mut ralph_validator =
2974 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2975 .with_memories_enabled(true);
2976 ralph_validator.set_active_scratchpad(ScratchpadConfig {
2977 enabled: false,
2978 path: ".ralph/agent/scratchpad.md".to_string(),
2979 });
2980 let validator_prompt = ralph_validator.build_prompt("", &[]);
2981 assert!(
2982 !validator_prompt.contains("### 0b. SCRATCHPAD"),
2983 "Validator should have no scratchpad sections"
2984 );
2985 }
2986}