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
331 if self.robot_guidance.len() == 1 {
332 section.push_str(&self.robot_guidance[0]);
333 } else {
334 for (i, guidance) in self.robot_guidance.iter().enumerate() {
335 section.push_str(&format!("{}. {}\n", i + 1, guidance));
336 }
337 }
338
339 section.push_str("\n\n");
340
341 section
342 }
343
344 pub fn build_prompt(&self, context: &str, active_hats: &[&ralph_proto::Hat]) -> String {
352 let mut prompt = self.core_prompt();
353
354 if !self.skill_index.is_empty() {
356 prompt.push_str(&self.skill_index);
357 prompt.push('\n');
358 }
359
360 if let Some(ref obj) = self.objective {
362 prompt.push_str(&self.objective_section(obj));
363 }
364
365 let guidance = self.collect_robot_guidance();
367 if !guidance.is_empty() {
368 prompt.push_str(&guidance);
369 }
370
371 if !context.trim().is_empty() {
373 prompt.push_str("## PENDING EVENTS\n\n");
374 prompt.push_str("You MUST handle these events in this iteration:\n\n");
375 prompt.push_str(context);
376 prompt.push_str("\n\n");
377 }
378
379 let has_custom_workflow = active_hats
382 .iter()
383 .any(|h| !h.instructions.trim().is_empty());
384
385 if !has_custom_workflow {
386 prompt.push_str(&self.workflow_section());
387 }
388
389 if let Some(topology) = &self.hat_topology {
390 prompt.push_str(&self.hats_section(topology, active_hats));
391 }
392
393 prompt.push_str(&self.event_writing_section());
394
395 if active_hats.is_empty() {
398 prompt.push_str(&self.done_section(self.objective.as_deref()));
399 }
400
401 prompt
402 }
403
404 fn objective_section(&self, objective: &str) -> String {
406 format!(
407 r"## OBJECTIVE
408
409**This is your primary goal. All work must advance this objective.**
410
411> {objective}
412
413You MUST keep this objective in mind throughout the iteration.
414You MUST NOT get distracted by workflow mechanics — they serve this goal.
415
416",
417 objective = objective
418 )
419 }
420
421 pub fn should_handle(&self, _topic: &Topic) -> bool {
423 true
424 }
425
426 fn is_fresh_start(&self) -> bool {
432 if self.starting_event.is_none() {
434 return false;
435 }
436
437 if self.iteration > 0 {
439 return false;
440 }
441
442 if self.active_scratchpad.enabled {
445 return !Path::new(&self.active_scratchpad.path).exists();
446 }
447
448 true
450 }
451
452 fn core_prompt(&self) -> String {
453 let guardrails = self
455 .core
456 .guardrails
457 .iter()
458 .enumerate()
459 .map(|(i, g)| {
460 let guardrail = if self.memories_enabled && g.contains("scratchpad is memory") {
462 g.replace(
463 "scratchpad is memory",
464 "save learnings to memories for next time",
465 )
466 } else {
467 g.clone()
468 };
469 format!("{}. {guardrail}", 999 + i)
470 })
471 .collect::<Vec<_>>()
472 .join("\n");
473
474 let mut prompt = if self.memories_enabled {
475 if self.active_scratchpad.enabled {
476 r"
477### 0a. ORIENTATION
478You are Ralph. You are running in a loop. You have fresh context each iteration.
479You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
480
481**First thing every iteration:**
4821. Review your `<scratchpad>` (auto-injected above) for context on your thinking
4832. Review your `<ready-tasks>` (auto-injected above) to see what work exists
4843. If tasks exist, pick one. If not, create them from your plan.
485"
486 .to_string()
487 } else {
488 r"
489### 0a. ORIENTATION
490You are Ralph. You are running in a loop. You have fresh context each iteration.
491You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
492
493**First thing every iteration:**
4941. Review your `<ready-tasks>` (auto-injected above) to see what work exists
4952. If tasks exist, pick one. If not, create them from your plan.
496"
497 .to_string()
498 }
499 } else {
500 r"
501### 0a. ORIENTATION
502You are Ralph. You are running in a loop. You have fresh context each iteration.
503You MUST complete only one atomic task for the overall objective. Leave work for future iterations.
504"
505 .to_string()
506 };
507
508 if self.active_scratchpad.enabled {
510 prompt.push_str(&format!(
511 r"### 0b. SCRATCHPAD
512`{scratchpad}` is your thinking journal for THIS objective.
513Its content is auto-injected in `<scratchpad>` tags at the top of your context each iteration.
514
515**Always append** new entries to the end of the file (most recent = bottom).
516
517**Use for:**
518- Current understanding and reasoning
519- Analysis notes and decisions
520- Plan narrative (the 'why' behind your approach)
521
522**Do NOT use for:**
523- Tracking what tasks exist or their status (use `ralph tools task`)
524- Checklists or todo lists (use `ralph tools task ensure` when a stable key exists, otherwise `ralph tools task add`)
525
526",
527 scratchpad = self.active_scratchpad.path,
528 ));
529 }
530
531 if self.active_scratchpad.enabled {
537 prompt.push_str(&format!(
538 "### STATE MANAGEMENT\n\n\
539**Scratchpad** (`{scratchpad}`) — Your thinking:\n\
540- Current understanding and reasoning\n\
541- Analysis notes, decisions, plan narrative\n\
542- NOT for checklists or status tracking\n\
543\n\
544**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
545- Analysis and temporary notes\n\
546- Read when relevant\n\
547\n\
548**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\
549The 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\
550Do not spend turns on shell or tool-availability diagnosis unless the task is explicitly about the runtime environment.\n\
551Do 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\
552Keep temporary artifacts where later steps can still inspect them, such as a repo-local `logs/` directory or `/var/tmp` when needed.\n\
553\n",
554 scratchpad = self.active_scratchpad.path,
555 ));
556 } else {
557 prompt.push_str(
558 "### STATE MANAGEMENT\n\n\
559**Tasks** (`ralph tools task`) — What needs to be done:\n\
560- Work items, their status, priorities, and dependencies\n\
561- Source of truth for progress across iterations\n\
562- Auto-injected in `<ready-tasks>` tags at the top of your context\n\
563\n\
564**Memories** (`.ralph/agent/memories.md`) — Persistent learning:\n\
565- Codebase patterns and conventions\n\
566- Architectural decisions and rationale\n\
567- Recurring problem solutions\n\
568\n\
569**Context Files** (`.ralph/agent/*.md`) — Research artifacts:\n\
570- Analysis and temporary notes\n\
571- Read when relevant\n\
572\n\
573**Rule:** Work items go in tasks. Learnings go in memories.\n\
574\n",
575 );
576 }
577
578 if let Ok(entries) = std::fs::read_dir(".ralph/agent") {
580 let md_files: Vec<String> = entries
581 .filter_map(|e| e.ok())
582 .filter_map(|e| {
583 let path = e.path();
584 let fname = path.file_name().and_then(|s| s.to_str());
585 if path.extension().and_then(|s| s.to_str()) == Some("md")
586 && fname != Some("memories.md")
587 && fname != Some("scratchpad.md")
588 {
589 path.file_name()
590 .and_then(|s| s.to_str())
591 .map(|s| s.to_string())
592 } else {
593 None
594 }
595 })
596 .collect();
597
598 if !md_files.is_empty() {
599 prompt.push_str("### AVAILABLE CONTEXT FILES\n\n");
600 prompt.push_str(
601 "Context files in `.ralph/agent/` (read if relevant to current work):\n",
602 );
603 for file in md_files {
604 prompt.push_str(&format!("- `.ralph/agent/{}`\n", file));
605 }
606 prompt.push('\n');
607 }
608 }
609
610 prompt.push_str(&format!(
611 r"### GUARDRAILS
612{guardrails}
613
614",
615 guardrails = guardrails,
616 ));
617
618 prompt
619 }
620
621 fn workflow_section(&self) -> String {
622 let scratchpad_enabled = self.active_scratchpad.enabled;
623 let scratchpad = &self.active_scratchpad.path;
624
625 if self.hat_topology.is_some() {
627 if self.is_fresh_start() {
629 return format!(
631 r#"## WORKFLOW
632
633**FAST PATH**: You MUST publish `{}` immediately to start the hat workflow.
634You MUST use `ralph emit "{}" "<brief handoff>"` and stop immediately.
635You MUST NOT plan or analyze — delegate now.
636
637"#,
638 self.starting_event.as_ref().unwrap(),
639 self.starting_event.as_ref().unwrap()
640 );
641 }
642
643 if self.memories_enabled {
645 if scratchpad_enabled {
646 format!(
648 r"## WORKFLOW
649
650### 1. PLAN
651You MUST update `{scratchpad}` with your understanding and plan.
652You MUST check `<ready-tasks>` first.
653You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
654You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
655If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
656
657### 2. DELEGATE
658You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
659Plain-language summaries do NOT hand off work.
660You MUST NOT do implementation work — delegation is your only job.
661
662",
663 )
664 } else {
665 "## WORKFLOW\n\n\
667### 1. PLAN\n\
668You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).\n\
669\n\
670### 2. DELEGATE\n\
671You MUST publish exactly ONE event to hand off to specialized hats.\n\
672You MUST NOT do implementation work — delegation is your only job.\n\
673\n"
674 .to_string()
675 }
676 } else if scratchpad_enabled {
677 format!(
679 r"## WORKFLOW
680
681### 1. PLAN
682You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
683
684### 2. DELEGATE
685You MUST emit exactly ONE next event via `ralph emit` to hand off to specialized hats.
686Plain-language summaries do NOT hand off work.
687You MUST NOT do implementation work — delegation is your only job.
688
689",
690 )
691 } else {
692 "## WORKFLOW\n\n\
694### 1. DELEGATE\n\
695You MUST publish exactly ONE event to hand off to specialized hats.\n\
696You MUST NOT do implementation work — delegation is your only job.\n\
697\n"
698 .to_string()
699 }
700 } else {
701 if self.memories_enabled {
703 if scratchpad_enabled {
704 format!(
706 r"## WORKFLOW
707
708### 1. Study the prompt.
709You MUST study, explore, and research what needs to be done.
710
711### 2. PLAN
712You MUST update `{scratchpad}` with your understanding and plan.
713You MUST check `<ready-tasks>` first.
714You MUST represent work items with runtime tasks using `ralph tools task ensure` when you can derive a stable key, otherwise `ralph tools task add`.
715You SHOULD search memories with `ralph tools memory search` before acting in unfamiliar areas.
716If confidence is 80 or below on a consequential decision, you MUST document it in `.ralph/agent/decisions.md`.
717
718### 3. IMPLEMENT
719You MUST pick exactly ONE task from `<ready-tasks>` to implement.
720You MUST mark it in progress with `ralph tools task start <id>` before implementation.
721
722### 4. VERIFY & COMMIT
723You MUST run tests and verify the implementation works.
724If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
725You SHOULD try at least one realistic failure-path or adversarial input during verification.
726If this turn is likely to take more than a few minutes, you SHOULD send `ralph tools interact progress`.
727You MUST commit after verification passes - one commit per task.
728You SHOULD run `git diff --cached` to review staged changes before committing.
729You MUST close the task with `ralph tools task close <id>` AFTER commit.
730You SHOULD save learnings to memories with `ralph tools memory add`.
731If 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.
732You MUST update scratchpad with what you learned (tasks track what remains).
733
734### 5. EXIT
735You MUST exit after completing ONE task.
736
737",
738 )
739 } else {
740 "## WORKFLOW\n\n\
742### 1. Study the prompt.\n\
743You MUST study, explore, and research what needs to be done.\n\
744\n\
745### 2. PLAN\n\
746You MUST create tasks with `ralph tools task add` for each work item (check `<ready-tasks>` first to avoid duplicates).\n\
747\n\
748### 3. IMPLEMENT\n\
749You MUST pick exactly ONE task from `<ready-tasks>` to implement.\n\
750\n\
751### 4. VERIFY & COMMIT\n\
752You MUST run tests and verify the implementation works.\n\
753You MUST commit after verification passes - one commit per task.\n\
754You SHOULD run `git diff --cached` to review staged changes before committing.\n\
755You MUST close the task with `ralph tools task close <id>` AFTER commit.\n\
756You SHOULD save learnings to memories with `ralph tools memory add`.\n\
757\n\
758### 5. EXIT\n\
759You MUST exit after completing ONE task.\n\
760\n"
761 .to_string()
762 }
763 } else if scratchpad_enabled {
764 format!(
766 r"## WORKFLOW
767
768### 1. Study the prompt.
769You MUST study, explore, and research what needs to be done.
770You MAY use parallel subagents (up to 10) for searches.
771
772### 2. PLAN
773You MUST update `{scratchpad}` with prioritized tasks to complete the objective end-to-end.
774
775### 3. IMPLEMENT
776You MUST pick exactly ONE task to implement.
777You MUST NOT use more than 1 subagent for build/tests.
778
779### 4. COMMIT
780If the target is runnable or user-facing, you MUST exercise it with the strongest available harness (Playwright, tmux, real CLI/API) before committing.
781You SHOULD try at least one realistic failure-path or adversarial input during verification.
782You MUST commit after completing each atomic unit of work.
783You MUST capture the why, not just the what.
784You SHOULD run `git diff` before committing to review changes.
785You MUST mark the task `[x]` in scratchpad when complete.
786
787### 5. REPEAT
788You MUST continue until all tasks are `[x]` or `[~]`.
789
790",
791 )
792 } else {
793 "## WORKFLOW\n\n\
795### 1. Study the prompt.\n\
796You MUST study, explore, and research what needs to be done.\n\
797You MAY use parallel subagents (up to 10) for searches.\n\
798\n\
799### 2. IMPLEMENT\n\
800You MUST pick exactly ONE task to implement.\n\
801You MUST NOT use more than 1 subagent for build/tests.\n\
802\n\
803### 3. COMMIT\n\
804You MUST commit after completing each atomic unit of work.\n\
805You MUST capture the why, not just the what.\n\
806You SHOULD run `git diff` before committing to review changes.\n\
807\n\
808### 4. REPEAT\n\
809You MUST continue.\n\
810\n"
811 .to_string()
812 }
813 }
814 }
815
816 fn hats_section(&self, topology: &HatTopology, active_hats: &[&ralph_proto::Hat]) -> String {
817 let mut section = String::new();
818
819 if active_hats.is_empty() {
822 section.push_str("## HATS\n\nDelegate via events.\n\n");
824
825 if let Some(ref starting_event) = self.starting_event {
827 section.push_str(&format!(
828 "**After coordination, publish `{}` to start the workflow.**\n\n",
829 starting_event
830 ));
831 }
832
833 let mut ralph_triggers: Vec<&str> = vec!["task.start"];
837 let mut ralph_publishes: Vec<&str> = Vec::new();
838
839 for hat in &topology.hats {
840 for pub_event in &hat.publishes {
841 if !ralph_triggers.contains(&pub_event.as_str()) {
842 ralph_triggers.push(pub_event.as_str());
843 }
844 }
845 for sub_event in &hat.subscribes_to {
846 if !ralph_publishes.contains(&sub_event.as_str()) {
847 ralph_publishes.push(sub_event.as_str());
848 }
849 }
850 }
851
852 section.push_str("| Hat | Triggers On | Publishes | Description |\n");
854 section.push_str("|-----|-------------|----------|-------------|\n");
855
856 section.push_str(&format!(
858 "| Ralph | {} | {} | Coordinates workflow, delegates to specialized hats |\n",
859 ralph_triggers.join(", "),
860 ralph_publishes.join(", ")
861 ));
862
863 for hat in &topology.hats {
865 let subscribes = hat.subscribes_to.join(", ");
866 let publishes = hat.publishes.join(", ");
867 section.push_str(&format!(
868 "| {} | {} | {} | {} |\n",
869 hat.name, subscribes, publishes, hat.description
870 ));
871 }
872
873 section.push('\n');
874
875 section.push_str(&self.generate_mermaid_diagram(topology, &ralph_publishes));
877 section.push('\n');
878
879 if !ralph_publishes.is_empty() {
881 section.push_str(&format!(
882 "**CONSTRAINT:** You MUST only publish events from this list: `{}`\n\
883 Publishing other events will have no effect - no hat will receive them.\n\n",
884 ralph_publishes.join("`, `")
885 ));
886 }
887
888 self.validate_topology_reachability(topology);
890 } else {
891 section.push_str("## ACTIVE HAT\n\n");
893
894 for active_hat in active_hats {
895 let hat_info = topology.hats.iter().find(|h| h.name == active_hat.name);
897
898 if !active_hat.instructions.trim().is_empty() {
899 section.push_str(&format!("### {} Instructions\n\n", active_hat.name));
900 section.push_str(&active_hat.instructions);
901 if !active_hat.instructions.ends_with('\n') {
902 section.push('\n');
903 }
904 section.push('\n');
905 }
906
907 if let Some(guide) = hat_info.and_then(|info| info.event_publishing_guide()) {
909 section.push_str(&guide);
910 section.push('\n');
911 }
912
913 if let Some(info) = hat_info {
915 let wave_dispatch = info.wave_dispatch_section();
916 if !wave_dispatch.is_empty() {
917 section.push_str(&wave_dispatch);
918 }
919 }
920
921 if let Some(info) = hat_info
923 && !info.disallowed_tools.is_empty()
924 {
925 section.push_str("### TOOL RESTRICTIONS\n\n");
926 section.push_str("You MUST NOT use these tools in this hat:\n");
927 for tool in &info.disallowed_tools {
928 section.push_str(&format!("- **{}** — blocked for this hat\n", tool));
929 }
930 section.push_str(
931 "\nUsing a restricted tool is a scope violation. \
932 File modifications are audited after each iteration.\n\n",
933 );
934 }
935 }
936 }
937
938 section
939 }
940
941 fn generate_mermaid_diagram(&self, topology: &HatTopology, ralph_publishes: &[&str]) -> String {
943 let node_ids: std::collections::HashMap<&str, String> = topology
945 .hats
946 .iter()
947 .map(|h| {
948 let id = h
949 .name
950 .chars()
951 .filter(|c| c.is_alphanumeric())
952 .collect::<String>();
953 (h.name.as_str(), id)
954 })
955 .collect();
956
957 let mut diagram = String::from("```mermaid\nflowchart LR\n");
958
959 diagram.push_str(" task.start((task.start)) --> Ralph\n");
961
962 for hat in &topology.hats {
964 let node_id = &node_ids[hat.name.as_str()];
965 for trigger in &hat.subscribes_to {
966 if ralph_publishes.contains(&trigger.as_str()) {
967 if node_id == &hat.name {
968 diagram.push_str(&format!(" Ralph -->|{}| {}\n", trigger, hat.name));
969 } else {
970 diagram.push_str(&format!(
971 " Ralph -->|{}| {}[{}]\n",
972 trigger, node_id, hat.name
973 ));
974 }
975 }
976 }
977 }
978
979 for hat in &topology.hats {
981 let node_id = &node_ids[hat.name.as_str()];
982 for pub_event in &hat.publishes {
983 diagram.push_str(&format!(" {} -->|{}| Ralph\n", node_id, pub_event));
984 }
985 }
986
987 for source_hat in &topology.hats {
989 let source_id = &node_ids[source_hat.name.as_str()];
990 for pub_event in &source_hat.publishes {
991 for target_hat in &topology.hats {
992 if target_hat.name != source_hat.name
993 && target_hat.subscribes_to.contains(pub_event)
994 {
995 let target_id = &node_ids[target_hat.name.as_str()];
996 diagram.push_str(&format!(
997 " {} -->|{}| {}\n",
998 source_id, pub_event, target_id
999 ));
1000 }
1001 }
1002 }
1003 }
1004
1005 diagram.push_str("```\n");
1006 diagram
1007 }
1008
1009 fn validate_topology_reachability(&self, topology: &HatTopology) {
1012 use std::collections::HashSet;
1013 use tracing::warn;
1014
1015 let mut reachable_events: HashSet<&str> = HashSet::new();
1017 reachable_events.insert("task.start");
1018
1019 for hat in &topology.hats {
1021 for trigger in &hat.subscribes_to {
1022 reachable_events.insert(trigger.as_str());
1023 }
1024 }
1025
1026 for hat in &topology.hats {
1028 for pub_event in &hat.publishes {
1029 reachable_events.insert(pub_event.as_str());
1030 }
1031 }
1032
1033 for hat in &topology.hats {
1035 let hat_reachable = hat
1036 .subscribes_to
1037 .iter()
1038 .any(|t| reachable_events.contains(t.as_str()));
1039 if !hat_reachable {
1040 warn!(
1041 hat = %hat.name,
1042 triggers = ?hat.subscribes_to,
1043 "Hat has triggers that are never published - it may be unreachable"
1044 );
1045 }
1046 }
1047 }
1048
1049 fn event_writing_section(&self) -> String {
1050 let detailed_output_hint = if self.active_scratchpad.enabled {
1051 format!(
1052 "\nYou SHOULD write detailed output to `{}` and emit only a brief event.\n",
1053 self.active_scratchpad.path
1054 )
1055 } else {
1056 String::new()
1057 };
1058
1059 format!(
1060 r#"## EVENT WRITING
1061
1062Events are routing signals, not data transport. You SHOULD keep payloads brief.
1063
1064You MUST use `ralph emit` to write events (handles JSON escaping correctly):
1065```bash
1066ralph emit "build.done" "tests: pass, lint: pass, typecheck: pass, audit: pass, coverage: pass"
1067ralph emit "review.done" --json '{{"status": "approved", "issues": 0}}'
1068```
1069
1070You MUST NOT use echo/cat to write events because shell escaping breaks JSON.
1071{detailed_output_hint}
1072**Constraints:**
1073- You MUST stop working after publishing an event because a new iteration will start with fresh context
1074- You MUST NOT continue with additional work after publishing because the next iteration handles it with the appropriate hat persona
1075"#,
1076 detailed_output_hint = detailed_output_hint
1077 )
1078 }
1079
1080 fn done_section(&self, objective: Option<&str>) -> String {
1081 let mut section = if self.hat_topology.is_some() {
1082 format!(
1083 r"## DONE
1084
1085You MUST emit the completion event `{}` via `ralph emit` when the objective is complete and all tasks are done.
1086Stdout text does NOT end the loop in coordinated mode.
1087",
1088 self.completion_promise
1089 )
1090 } else {
1091 format!(
1092 r"## DONE
1093
1094You MUST output the literal completion promise `{}` as the final non-empty line when the objective is complete and all tasks are done.
1095You MUST NOT substitute a prose summary for `{}`.
1096You MUST NOT print any text after `{}`.
1097",
1098 self.completion_promise, self.completion_promise, self.completion_promise
1099 )
1100 };
1101
1102 if self.memories_enabled {
1104 section.push_str(
1105 r"
1106**Before declaring completion:**
11071. Run `ralph tools task list` to check for any remaining non-terminal tasks
11082. If any tasks are still open or in progress, close, fail, or reopen them first
11093. Only declare completion when YOUR tasks for this objective are all terminal
1110
1111Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
1112
1113You MUST NOT declare completion while tasks remain open.
1114",
1115 );
1116 }
1117
1118 if let Some(obj) = objective {
1120 section.push_str(&format!(
1121 r"
1122**Remember your objective:**
1123> {}
1124
1125You MUST NOT declare completion until this objective is fully satisfied.
1126",
1127 obj
1128 ));
1129 }
1130
1131 section
1132 }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137 use super::*;
1138 use crate::config::RalphConfig;
1139
1140 #[test]
1141 fn test_prompt_without_hats() {
1142 let config = RalphConfig::default();
1143 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1145
1146 let prompt = ralph.build_prompt("", &[]);
1147
1148 assert!(prompt.contains(
1150 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1151 ));
1152
1153 assert!(prompt.contains("### 0a. ORIENTATION"));
1155 assert!(prompt.contains("MUST complete only one atomic task"));
1156
1157 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1159 assert!(prompt.contains("auto-injected"));
1160 assert!(prompt.contains("**Always append**"));
1161
1162 assert!(prompt.contains("## WORKFLOW"));
1164 assert!(prompt.contains("### 1. Study the prompt"));
1165 assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
1166 assert!(prompt.contains("### 2. PLAN"));
1167 assert!(prompt.contains("### 3. IMPLEMENT"));
1168 assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
1169 assert!(prompt.contains("### 4. COMMIT"));
1170 assert!(prompt.contains("You MUST capture the why"));
1171 assert!(prompt.contains("### 5. REPEAT"));
1172
1173 assert!(!prompt.contains("## HATS"));
1175
1176 assert!(prompt.contains("## EVENT WRITING"));
1178 assert!(prompt.contains("You MUST use `ralph emit`"));
1179 assert!(prompt.contains("You MUST NOT use echo/cat"));
1180 assert!(prompt.contains("LOOP_COMPLETE"));
1181 }
1182
1183 #[test]
1184 fn test_prompt_with_hats() {
1185 let yaml = r#"
1187hats:
1188 planner:
1189 name: "Planner"
1190 triggers: ["planning.start", "build.done", "build.blocked"]
1191 publishes: ["build.task"]
1192 builder:
1193 name: "Builder"
1194 triggers: ["build.task"]
1195 publishes: ["build.done", "build.blocked"]
1196"#;
1197 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1198 let registry = HatRegistry::from_config(&config);
1199 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1201
1202 let prompt = ralph.build_prompt("", &[]);
1203
1204 assert!(prompt.contains(
1206 "You are Ralph. You are running in a loop. You have fresh context each iteration."
1207 ));
1208
1209 assert!(prompt.contains("### 0a. ORIENTATION"));
1211 assert!(prompt.contains("### 0b. SCRATCHPAD"));
1212
1213 assert!(prompt.contains("## WORKFLOW"));
1215 assert!(prompt.contains("### 1. PLAN"));
1216 assert!(
1217 prompt.contains("### 2. DELEGATE"),
1218 "Multi-hat mode should have DELEGATE step"
1219 );
1220 assert!(
1221 !prompt.contains("### 3. IMPLEMENT"),
1222 "Multi-hat mode should NOT tell Ralph to implement"
1223 );
1224 assert!(
1225 prompt.contains("You MUST stop working after publishing"),
1226 "Should explicitly tell Ralph to stop after publishing event"
1227 );
1228
1229 assert!(prompt.contains("## HATS"));
1231 assert!(prompt.contains("Delegate via events"));
1232 assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
1233
1234 assert!(prompt.contains("## EVENT WRITING"));
1236 assert!(prompt.contains("LOOP_COMPLETE"));
1237 }
1238
1239 #[test]
1240 fn test_should_handle_always_true() {
1241 let config = RalphConfig::default();
1242 let registry = HatRegistry::new();
1243 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1244
1245 assert!(ralph.should_handle(&Topic::new("any.topic")));
1246 assert!(ralph.should_handle(&Topic::new("build.task")));
1247 assert!(ralph.should_handle(&Topic::new("unknown.event")));
1248 }
1249
1250 #[test]
1251 fn test_rfc2119_patterns_present() {
1252 let config = RalphConfig::default();
1253 let registry = HatRegistry::new();
1254 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1255
1256 let prompt = ralph.build_prompt("", &[]);
1257
1258 assert!(
1260 prompt.contains("You MUST study"),
1261 "Should use RFC2119 MUST with 'study' verb"
1262 );
1263 assert!(
1264 prompt.contains("You MUST complete only one atomic task"),
1265 "Should have RFC2119 MUST complete atomic task constraint"
1266 );
1267 assert!(
1268 prompt.contains("You MAY use parallel subagents"),
1269 "Should mention parallel subagents with MAY"
1270 );
1271 assert!(
1272 prompt.contains("You MUST NOT use more than 1 subagent"),
1273 "Should limit to 1 subagent for builds with MUST NOT"
1274 );
1275 assert!(
1276 prompt.contains("You MUST capture the why"),
1277 "Should emphasize 'why' in commits with MUST"
1278 );
1279
1280 assert!(
1282 prompt.contains("### GUARDRAILS"),
1283 "Should have guardrails section"
1284 );
1285 assert!(
1286 prompt.contains("999."),
1287 "Guardrails should use high numbers"
1288 );
1289 }
1290
1291 #[test]
1292 fn test_scratchpad_format_documented() {
1293 let config = RalphConfig::default();
1294 let registry = HatRegistry::new();
1295 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1296
1297 let prompt = ralph.build_prompt("", &[]);
1298
1299 assert!(prompt.contains("auto-injected"));
1301 assert!(prompt.contains("**Always append**"));
1302 }
1303
1304 #[test]
1305 fn test_starting_event_in_prompt() {
1306 let yaml = r#"
1308hats:
1309 tdd_writer:
1310 name: "TDD Writer"
1311 triggers: ["tdd.start"]
1312 publishes: ["test.written"]
1313"#;
1314 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1315 let registry = HatRegistry::from_config(&config);
1316 let ralph = HatlessRalph::new(
1317 "LOOP_COMPLETE",
1318 config.core.clone(),
1319 ®istry,
1320 Some("tdd.start".to_string()),
1321 );
1322
1323 let prompt = ralph.build_prompt("", &[]);
1324
1325 assert!(
1327 prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1328 "Prompt should include starting_event delegation instruction"
1329 );
1330 }
1331
1332 #[test]
1333 fn test_no_starting_event_instruction_when_none() {
1334 let yaml = r#"
1336hats:
1337 some_hat:
1338 name: "Some Hat"
1339 triggers: ["some.event"]
1340"#;
1341 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1342 let registry = HatRegistry::from_config(&config);
1343 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1344
1345 let prompt = ralph.build_prompt("", &[]);
1346
1347 assert!(
1349 !prompt.contains("After coordination, publish"),
1350 "Prompt should NOT include starting_event delegation when None"
1351 );
1352 }
1353
1354 #[test]
1355 fn test_hat_instructions_propagated_to_prompt() {
1356 let yaml = r#"
1359hats:
1360 tdd_writer:
1361 name: "TDD Writer"
1362 triggers: ["tdd.start"]
1363 publishes: ["test.written"]
1364 instructions: |
1365 You are a Test-Driven Development specialist.
1366 Always write failing tests before implementation.
1367 Focus on edge cases and error handling.
1368"#;
1369 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1370 let registry = HatRegistry::from_config(&config);
1371 let ralph = HatlessRalph::new(
1372 "LOOP_COMPLETE",
1373 config.core.clone(),
1374 ®istry,
1375 Some("tdd.start".to_string()),
1376 );
1377
1378 let tdd_writer = registry
1380 .get(&ralph_proto::HatId::new("tdd_writer"))
1381 .unwrap();
1382 let prompt = ralph.build_prompt("", &[tdd_writer]);
1383
1384 assert!(
1386 prompt.contains("### TDD Writer Instructions"),
1387 "Prompt should include hat instructions section header"
1388 );
1389 assert!(
1390 prompt.contains("Test-Driven Development specialist"),
1391 "Prompt should include actual instructions content"
1392 );
1393 assert!(
1394 prompt.contains("Always write failing tests"),
1395 "Prompt should include full instructions"
1396 );
1397 }
1398
1399 #[test]
1400 fn test_empty_instructions_not_rendered() {
1401 let yaml = r#"
1403hats:
1404 builder:
1405 name: "Builder"
1406 triggers: ["build.task"]
1407 publishes: ["build.done"]
1408"#;
1409 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1410 let registry = HatRegistry::from_config(&config);
1411 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1412
1413 let prompt = ralph.build_prompt("", &[]);
1414
1415 assert!(
1417 !prompt.contains("### Builder Instructions"),
1418 "Prompt should NOT include instructions section for hat with empty instructions"
1419 );
1420 }
1421
1422 #[test]
1423 fn test_multiple_hats_with_instructions() {
1424 let yaml = r#"
1426hats:
1427 planner:
1428 name: "Planner"
1429 triggers: ["planning.start"]
1430 publishes: ["build.task"]
1431 instructions: "Plan carefully before implementation."
1432 builder:
1433 name: "Builder"
1434 triggers: ["build.task"]
1435 publishes: ["build.done"]
1436 instructions: "Focus on clean, testable code."
1437"#;
1438 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1439 let registry = HatRegistry::from_config(&config);
1440 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1441
1442 let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1444 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1445 let prompt = ralph.build_prompt("", &[planner, builder]);
1446
1447 assert!(
1449 prompt.contains("### Planner Instructions"),
1450 "Prompt should include Planner instructions section"
1451 );
1452 assert!(
1453 prompt.contains("Plan carefully before implementation"),
1454 "Prompt should include Planner instructions content"
1455 );
1456 assert!(
1457 prompt.contains("### Builder Instructions"),
1458 "Prompt should include Builder instructions section"
1459 );
1460 assert!(
1461 prompt.contains("Focus on clean, testable code"),
1462 "Prompt should include Builder instructions content"
1463 );
1464 }
1465
1466 #[test]
1467 fn test_fast_path_with_starting_event() {
1468 let yaml = r#"
1471core:
1472 scratchpad: "/nonexistent/path/scratchpad.md"
1473hats:
1474 tdd_writer:
1475 name: "TDD Writer"
1476 triggers: ["tdd.start"]
1477 publishes: ["test.written"]
1478"#;
1479 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1480 let registry = HatRegistry::from_config(&config);
1481 let ralph = HatlessRalph::new(
1482 "LOOP_COMPLETE",
1483 config.core.clone(),
1484 ®istry,
1485 Some("tdd.start".to_string()),
1486 );
1487
1488 let prompt = ralph.build_prompt("", &[]);
1489
1490 assert!(
1492 prompt.contains("FAST PATH"),
1493 "Prompt should indicate fast path when starting_event set and no scratchpad"
1494 );
1495 assert!(
1496 prompt.contains("You MUST publish `tdd.start` immediately"),
1497 "Prompt should instruct immediate event publishing with MUST"
1498 );
1499 assert!(
1500 prompt.contains("ralph emit \"tdd.start\""),
1501 "Fast path should require explicit event emission"
1502 );
1503 assert!(
1504 !prompt.contains("### 1. PLAN"),
1505 "Fast path should skip PLAN step"
1506 );
1507 }
1508
1509 #[test]
1510 fn test_events_context_included_in_prompt() {
1511 let config = RalphConfig::default();
1515 let registry = HatRegistry::new();
1516 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1517
1518 let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1519[build.done] Build completed successfully";
1520
1521 let prompt = ralph.build_prompt(events_context, &[]);
1522
1523 assert!(
1524 prompt.contains("## PENDING EVENTS"),
1525 "Prompt should contain PENDING EVENTS section"
1526 );
1527 assert!(
1528 prompt.contains("Review this code for security vulnerabilities"),
1529 "Prompt should contain the user's task"
1530 );
1531 assert!(
1532 prompt.contains("Build completed successfully"),
1533 "Prompt should contain all events from context"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_empty_context_no_pending_events_section() {
1539 let config = RalphConfig::default();
1543 let registry = HatRegistry::new();
1544 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1545
1546 let prompt = ralph.build_prompt("", &[]);
1547
1548 assert!(
1549 !prompt.contains("## PENDING EVENTS"),
1550 "Empty context should not produce PENDING EVENTS section"
1551 );
1552 }
1553
1554 #[test]
1555 fn test_whitespace_only_context_no_pending_events_section() {
1556 let config = RalphConfig::default();
1560 let registry = HatRegistry::new();
1561 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1562
1563 let prompt = ralph.build_prompt(" \n\t ", &[]);
1564
1565 assert!(
1566 !prompt.contains("## PENDING EVENTS"),
1567 "Whitespace-only context should not produce PENDING EVENTS section"
1568 );
1569 }
1570
1571 #[test]
1572 fn test_events_section_before_workflow() {
1573 let config = RalphConfig::default();
1577 let registry = HatRegistry::new();
1578 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1579
1580 let events_context = "[task.start] Implement feature X";
1581 let prompt = ralph.build_prompt(events_context, &[]);
1582
1583 let events_pos = prompt
1584 .find("## PENDING EVENTS")
1585 .expect("Should have PENDING EVENTS");
1586 let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1587
1588 assert!(
1589 events_pos < workflow_pos,
1590 "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1591 events_pos,
1592 workflow_pos
1593 );
1594 }
1595
1596 #[test]
1599 fn test_only_active_hat_instructions_included() {
1600 let yaml = r#"
1602hats:
1603 security_reviewer:
1604 name: "Security Reviewer"
1605 triggers: ["review.security"]
1606 instructions: "Review code for security vulnerabilities."
1607 architecture_reviewer:
1608 name: "Architecture Reviewer"
1609 triggers: ["review.architecture"]
1610 instructions: "Review system design and architecture."
1611 correctness_reviewer:
1612 name: "Correctness Reviewer"
1613 triggers: ["review.correctness"]
1614 instructions: "Review logic and correctness."
1615"#;
1616 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1617 let registry = HatRegistry::from_config(&config);
1618 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1619
1620 let security_hat = registry
1622 .get(&ralph_proto::HatId::new("security_reviewer"))
1623 .unwrap();
1624 let active_hats = vec![security_hat];
1625
1626 let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1627
1628 assert!(
1630 prompt.contains("### Security Reviewer Instructions"),
1631 "Should include Security Reviewer instructions section"
1632 );
1633 assert!(
1634 prompt.contains("Review code for security vulnerabilities"),
1635 "Should include Security Reviewer instructions content"
1636 );
1637
1638 assert!(
1640 !prompt.contains("### Architecture Reviewer Instructions"),
1641 "Should NOT include Architecture Reviewer instructions"
1642 );
1643 assert!(
1644 !prompt.contains("Review system design and architecture"),
1645 "Should NOT include Architecture Reviewer instructions content"
1646 );
1647 assert!(
1648 !prompt.contains("### Correctness Reviewer Instructions"),
1649 "Should NOT include Correctness Reviewer instructions"
1650 );
1651 }
1652
1653 #[test]
1654 fn test_multiple_active_hats_all_included() {
1655 let yaml = r#"
1657hats:
1658 security_reviewer:
1659 name: "Security Reviewer"
1660 triggers: ["review.security"]
1661 instructions: "Review code for security vulnerabilities."
1662 architecture_reviewer:
1663 name: "Architecture Reviewer"
1664 triggers: ["review.architecture"]
1665 instructions: "Review system design and architecture."
1666 correctness_reviewer:
1667 name: "Correctness Reviewer"
1668 triggers: ["review.correctness"]
1669 instructions: "Review logic and correctness."
1670"#;
1671 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1672 let registry = HatRegistry::from_config(&config);
1673 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1674
1675 let security_hat = registry
1677 .get(&ralph_proto::HatId::new("security_reviewer"))
1678 .unwrap();
1679 let arch_hat = registry
1680 .get(&ralph_proto::HatId::new("architecture_reviewer"))
1681 .unwrap();
1682 let active_hats = vec![security_hat, arch_hat];
1683
1684 let prompt = ralph.build_prompt("Events", &active_hats);
1685
1686 assert!(
1688 prompt.contains("### Security Reviewer Instructions"),
1689 "Should include Security Reviewer instructions"
1690 );
1691 assert!(
1692 prompt.contains("Review code for security vulnerabilities"),
1693 "Should include Security Reviewer content"
1694 );
1695 assert!(
1696 prompt.contains("### Architecture Reviewer Instructions"),
1697 "Should include Architecture Reviewer instructions"
1698 );
1699 assert!(
1700 prompt.contains("Review system design and architecture"),
1701 "Should include Architecture Reviewer content"
1702 );
1703
1704 assert!(
1706 !prompt.contains("### Correctness Reviewer Instructions"),
1707 "Should NOT include Correctness Reviewer instructions"
1708 );
1709 }
1710
1711 #[test]
1712 fn test_no_active_hats_no_instructions() {
1713 let yaml = r#"
1715hats:
1716 security_reviewer:
1717 name: "Security Reviewer"
1718 triggers: ["review.security"]
1719 instructions: "Review code for security vulnerabilities."
1720"#;
1721 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1722 let registry = HatRegistry::from_config(&config);
1723 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1724
1725 let active_hats: Vec<&ralph_proto::Hat> = vec![];
1727
1728 let prompt = ralph.build_prompt("Events", &active_hats);
1729
1730 assert!(
1732 !prompt.contains("### Security Reviewer Instructions"),
1733 "Should NOT include instructions when no active hats"
1734 );
1735 assert!(
1736 !prompt.contains("Review code for security vulnerabilities"),
1737 "Should NOT include instructions content when no active hats"
1738 );
1739
1740 assert!(prompt.contains("## HATS"), "Should still have HATS section");
1742 assert!(
1743 prompt.contains("| Hat | Triggers On | Publishes |"),
1744 "Should still have topology table"
1745 );
1746 }
1747
1748 #[test]
1749 fn test_topology_table_only_when_ralph_coordinating() {
1750 let yaml = r#"
1753hats:
1754 security_reviewer:
1755 name: "Security Reviewer"
1756 triggers: ["review.security"]
1757 instructions: "Security instructions."
1758 architecture_reviewer:
1759 name: "Architecture Reviewer"
1760 triggers: ["review.architecture"]
1761 instructions: "Architecture instructions."
1762"#;
1763 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1764 let registry = HatRegistry::from_config(&config);
1765 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1766
1767 let prompt_coordinating = ralph.build_prompt("Events", &[]);
1769
1770 assert!(
1771 prompt_coordinating.contains("## HATS"),
1772 "Should have HATS section when coordinating"
1773 );
1774 assert!(
1775 prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1776 "Should have topology table when coordinating"
1777 );
1778 assert!(
1779 prompt_coordinating.contains("```mermaid"),
1780 "Should have Mermaid diagram when coordinating"
1781 );
1782
1783 let security_hat = registry
1785 .get(&ralph_proto::HatId::new("security_reviewer"))
1786 .unwrap();
1787 let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1788
1789 assert!(
1790 prompt_active.contains("## ACTIVE HAT"),
1791 "Should have ACTIVE HAT section when hat is active"
1792 );
1793 assert!(
1794 !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1795 "Should NOT have topology table when hat is active"
1796 );
1797 assert!(
1798 !prompt_active.contains("```mermaid"),
1799 "Should NOT have Mermaid diagram when hat is active"
1800 );
1801 assert!(
1802 prompt_active.contains("### Security Reviewer Instructions"),
1803 "Should still have the active hat's instructions"
1804 );
1805 }
1806
1807 #[test]
1810 fn test_scratchpad_always_included() {
1811 let config = RalphConfig::default();
1813 let registry = HatRegistry::new();
1814 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1815
1816 let prompt = ralph.build_prompt("", &[]);
1817
1818 assert!(
1819 prompt.contains("### 0b. SCRATCHPAD"),
1820 "Scratchpad section should be included"
1821 );
1822 assert!(
1823 prompt.contains("`.ralph/agent/scratchpad.md`"),
1824 "Scratchpad path should be referenced"
1825 );
1826 assert!(
1827 prompt.contains("auto-injected"),
1828 "Auto-injection should be documented"
1829 );
1830 }
1831
1832 #[test]
1833 fn test_scratchpad_included_with_memories_enabled() {
1834 let config = RalphConfig::default();
1836 let registry = HatRegistry::new();
1837 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1838 .with_memories_enabled(true);
1839
1840 let prompt = ralph.build_prompt("", &[]);
1841
1842 assert!(
1844 prompt.contains("### 0b. SCRATCHPAD"),
1845 "Scratchpad section should be included even with memories enabled"
1846 );
1847 assert!(
1848 prompt.contains("**Always append**"),
1849 "Append instruction should be documented"
1850 );
1851
1852 assert!(
1854 !prompt.contains("### 0c. TASKS"),
1855 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1856 );
1857 }
1858
1859 #[test]
1860 fn test_no_tasks_section_in_core_prompt() {
1861 let config = RalphConfig::default();
1863 let registry = HatRegistry::new();
1864 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1865
1866 let prompt = ralph.build_prompt("", &[]);
1867
1868 assert!(
1870 !prompt.contains("### 0c. TASKS"),
1871 "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1872 );
1873 }
1874
1875 #[test]
1876 fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1877 let config = RalphConfig::default();
1879 let registry = HatRegistry::new();
1880 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1881 .with_memories_enabled(true);
1882
1883 let prompt = ralph.build_prompt("", &[]);
1884
1885 assert!(
1887 prompt.contains("update scratchpad"),
1888 "Workflow should reference scratchpad when memories enabled"
1889 );
1890 assert!(
1892 prompt.contains("ralph tools task"),
1893 "Workflow should reference tasks CLI when memories enabled"
1894 );
1895 }
1896
1897 #[test]
1898 fn test_multi_hat_mode_workflow_with_memories_enabled() {
1899 let yaml = r#"
1901hats:
1902 builder:
1903 name: "Builder"
1904 triggers: ["build.task"]
1905 publishes: ["build.done"]
1906"#;
1907 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1908 let registry = HatRegistry::from_config(&config);
1909 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1910 .with_memories_enabled(true);
1911
1912 let prompt = ralph.build_prompt("", &[]);
1913
1914 assert!(
1916 prompt.contains("scratchpad"),
1917 "Multi-hat workflow should reference scratchpad when memories enabled"
1918 );
1919 assert!(
1921 prompt.contains("ralph tools task ensure"),
1922 "Multi-hat workflow should reference tasks CLI when memories enabled"
1923 );
1924 }
1925
1926 #[test]
1927 fn test_guardrails_adapt_to_memories_mode() {
1928 let config = RalphConfig::default();
1930 let registry = HatRegistry::new();
1931 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1932 .with_memories_enabled(true);
1933
1934 let prompt = ralph.build_prompt("", &[]);
1935
1936 assert!(
1940 prompt.contains("### GUARDRAILS"),
1941 "Guardrails section should be present"
1942 );
1943 }
1944
1945 #[test]
1946 fn test_guardrails_present_without_memories() {
1947 let config = RalphConfig::default();
1949 let registry = HatRegistry::new();
1950 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
1951 let prompt = ralph.build_prompt("", &[]);
1954
1955 assert!(
1956 prompt.contains("### GUARDRAILS"),
1957 "Guardrails section should be present"
1958 );
1959 }
1960
1961 #[test]
1964 fn test_task_closure_verification_in_done_section() {
1965 let config = RalphConfig::default();
1968 let registry = HatRegistry::new();
1969 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1970 .with_memories_enabled(true);
1971
1972 let prompt = ralph.build_prompt("", &[]);
1973
1974 assert!(
1977 prompt.contains("ralph tools task list"),
1978 "Should reference task list command in DONE section"
1979 );
1980 assert!(
1981 prompt.contains("MUST NOT declare completion while tasks remain open"),
1982 "Should require tasks closed before completion"
1983 );
1984 }
1985
1986 #[test]
1987 fn test_workflow_verify_and_commit_step() {
1988 let config = RalphConfig::default();
1990 let registry = HatRegistry::new();
1991 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
1992 .with_memories_enabled(true);
1993
1994 let prompt = ralph.build_prompt("", &[]);
1995
1996 assert!(
1998 prompt.contains("### 4. VERIFY & COMMIT"),
1999 "Should have VERIFY & COMMIT step in workflow"
2000 );
2001 assert!(
2002 prompt.contains("run tests and verify"),
2003 "Should require verification"
2004 );
2005 assert!(
2006 prompt.contains("ralph tools task start"),
2007 "Should reference task start command"
2008 );
2009 assert!(
2010 prompt.contains("ralph tools task close"),
2011 "Should reference task close command"
2012 );
2013 }
2014
2015 #[test]
2016 fn test_scratchpad_mode_still_has_commit_step() {
2017 let config = RalphConfig::default();
2019 let registry = HatRegistry::new();
2020 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2021 let prompt = ralph.build_prompt("", &[]);
2024
2025 assert!(
2027 prompt.contains("### 4. COMMIT"),
2028 "Should have COMMIT step in workflow"
2029 );
2030 assert!(
2031 prompt.contains("mark the task `[x]`"),
2032 "Should mark task in scratchpad"
2033 );
2034 assert!(
2036 !prompt.contains("### 0c. TASKS"),
2037 "Scratchpad mode should not have TASKS section"
2038 );
2039 }
2040
2041 #[test]
2044 fn test_objective_section_present_with_set_objective() {
2045 let config = RalphConfig::default();
2047 let registry = HatRegistry::new();
2048 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2049 ralph.set_objective("Implement user authentication with JWT tokens".to_string());
2050
2051 let prompt = ralph.build_prompt("", &[]);
2052
2053 assert!(
2054 prompt.contains("## OBJECTIVE"),
2055 "Should have OBJECTIVE section when objective is set"
2056 );
2057 assert!(
2058 prompt.contains("Implement user authentication with JWT tokens"),
2059 "OBJECTIVE should contain the original user prompt"
2060 );
2061 assert!(
2062 prompt.contains("This is your primary goal"),
2063 "OBJECTIVE should emphasize this is the primary goal"
2064 );
2065 }
2066
2067 #[test]
2068 fn test_objective_reinforced_in_done_section() {
2069 let config = RalphConfig::default();
2072 let registry = HatRegistry::new();
2073 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2074 ralph.set_objective("Fix the login bug in auth module".to_string());
2075
2076 let prompt = ralph.build_prompt("", &[]);
2077
2078 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2080 let after_done = &prompt[done_pos..];
2081
2082 assert!(
2083 after_done.contains("Remember your objective"),
2084 "DONE section should remind about objective"
2085 );
2086 assert!(
2087 after_done.contains("Fix the login bug in auth module"),
2088 "DONE section should restate the objective"
2089 );
2090 }
2091
2092 #[test]
2093 fn test_objective_appears_before_pending_events() {
2094 let config = RalphConfig::default();
2096 let registry = HatRegistry::new();
2097 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2098 ralph.set_objective("Build feature X".to_string());
2099
2100 let context = "Event: task.start - Build feature X";
2101 let prompt = ralph.build_prompt(context, &[]);
2102
2103 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2104 let events_pos = prompt
2105 .find("## PENDING EVENTS")
2106 .expect("Should have PENDING EVENTS");
2107
2108 assert!(
2109 objective_pos < events_pos,
2110 "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
2111 objective_pos,
2112 events_pos
2113 );
2114 }
2115
2116 #[test]
2117 fn test_no_objective_when_not_set() {
2118 let config = RalphConfig::default();
2120 let registry = HatRegistry::new();
2121 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2122
2123 let context = "Event: build.done - Build completed successfully";
2124 let prompt = ralph.build_prompt(context, &[]);
2125
2126 assert!(
2127 !prompt.contains("## OBJECTIVE"),
2128 "Should NOT have OBJECTIVE section when objective not set"
2129 );
2130 }
2131
2132 #[test]
2133 fn test_objective_set_correctly() {
2134 let config = RalphConfig::default();
2136 let registry = HatRegistry::new();
2137 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2138 ralph.set_objective("Review this PR for security issues".to_string());
2139
2140 let prompt = ralph.build_prompt("", &[]);
2141
2142 assert!(
2143 prompt.contains("Review this PR for security issues"),
2144 "Should show the stored objective"
2145 );
2146 }
2147
2148 #[test]
2149 fn test_objective_with_events_context() {
2150 let config = RalphConfig::default();
2152 let registry = HatRegistry::new();
2153 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2154 ralph.set_objective("Implement feature Y".to_string());
2155
2156 let context =
2157 "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
2158 let prompt = ralph.build_prompt(context, &[]);
2159
2160 assert!(
2161 prompt.contains("## OBJECTIVE"),
2162 "Should have OBJECTIVE section"
2163 );
2164 assert!(
2165 prompt.contains("Implement feature Y"),
2166 "OBJECTIVE should contain the stored objective"
2167 );
2168 }
2169
2170 #[test]
2171 fn test_done_section_without_objective() {
2172 let config = RalphConfig::default();
2174 let registry = HatRegistry::new();
2175 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2176
2177 let prompt = ralph.build_prompt("", &[]);
2178
2179 assert!(prompt.contains("## DONE"), "Should have DONE section");
2180 assert!(
2181 prompt.contains("LOOP_COMPLETE"),
2182 "DONE should mention completion event"
2183 );
2184 assert!(
2185 prompt.contains("final non-empty line"),
2186 "Solo DONE section should require literal terminal output"
2187 );
2188 assert!(
2189 !prompt.contains("Remember your objective"),
2190 "Should NOT have objective reinforcement without objective"
2191 );
2192 }
2193
2194 #[test]
2195 fn test_objective_persists_across_iterations() {
2196 let config = RalphConfig::default();
2199 let registry = HatRegistry::new();
2200 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2201 ralph.set_objective("Build a REST API with authentication".to_string());
2202
2203 let context = "Event: build.done - Build completed";
2205 let prompt = ralph.build_prompt(context, &[]);
2206
2207 assert!(
2208 prompt.contains("## OBJECTIVE"),
2209 "OBJECTIVE should persist even without task.start in context"
2210 );
2211 assert!(
2212 prompt.contains("Build a REST API with authentication"),
2213 "Stored objective should appear in later iterations"
2214 );
2215 }
2216
2217 #[test]
2218 fn test_done_section_suppressed_when_hat_active() {
2219 let yaml = r#"
2221hats:
2222 builder:
2223 name: "Builder"
2224 triggers: ["build.task"]
2225 publishes: ["build.done"]
2226 instructions: "Build the code."
2227"#;
2228 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2229 let registry = HatRegistry::from_config(&config);
2230 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2231 ralph.set_objective("Implement feature X".to_string());
2232
2233 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2234 let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
2235
2236 assert!(
2237 !prompt.contains("## DONE"),
2238 "DONE section should be suppressed when a hat is active"
2239 );
2240 assert!(
2241 !prompt.contains("LOOP_COMPLETE"),
2242 "Completion promise should NOT appear when a hat is active"
2243 );
2244 assert!(
2246 prompt.contains("## OBJECTIVE"),
2247 "OBJECTIVE should still appear even when hat is active"
2248 );
2249 assert!(
2250 prompt.contains("Implement feature X"),
2251 "Objective content should be visible to active hat"
2252 );
2253 }
2254
2255 #[test]
2256 fn test_done_section_present_when_coordinating() {
2257 let yaml = r#"
2259hats:
2260 builder:
2261 name: "Builder"
2262 triggers: ["build.task"]
2263 publishes: ["build.done"]
2264"#;
2265 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2266 let registry = HatRegistry::from_config(&config);
2267 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2268 ralph.set_objective("Complete the TDD cycle".to_string());
2269
2270 let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
2272
2273 assert!(
2274 prompt.contains("## DONE"),
2275 "DONE section should appear when Ralph is coordinating"
2276 );
2277 assert!(
2278 prompt.contains("LOOP_COMPLETE"),
2279 "Completion promise should appear when coordinating"
2280 );
2281 assert!(
2282 prompt.contains("via `ralph emit`"),
2283 "Coordinating DONE section should require explicit event emission"
2284 );
2285 }
2286
2287 #[test]
2288 fn test_objective_in_done_section_when_coordinating() {
2289 let config = RalphConfig::default();
2291 let registry = HatRegistry::new();
2292 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2293 ralph.set_objective("Deploy the application".to_string());
2294
2295 let prompt = ralph.build_prompt("", &[]);
2296
2297 let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2298 let after_done = &prompt[done_pos..];
2299
2300 assert!(
2301 after_done.contains("Remember your objective"),
2302 "DONE section should remind about objective when coordinating"
2303 );
2304 assert!(
2305 after_done.contains("Deploy the application"),
2306 "DONE section should contain the objective text"
2307 );
2308 }
2309
2310 #[test]
2313 fn test_event_publishing_guide_with_receivers() {
2314 let yaml = r#"
2317hats:
2318 builder:
2319 name: "Builder"
2320 description: "Builds and tests code"
2321 triggers: ["build.task"]
2322 publishes: ["build.done", "build.blocked"]
2323 confessor:
2324 name: "Confessor"
2325 description: "Produces a ConfessionReport; rewarded for honesty"
2326 triggers: ["build.done"]
2327 publishes: ["confession.done"]
2328"#;
2329 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2330 let registry = HatRegistry::from_config(&config);
2331 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2332
2333 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2335 let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2336
2337 assert!(
2339 prompt.contains("### Event Publishing Guide"),
2340 "Should include Event Publishing Guide section"
2341 );
2342 assert!(
2343 prompt.contains("When you publish:"),
2344 "Guide should explain what happens when publishing"
2345 );
2346 assert!(
2347 prompt.contains("You MUST use `ralph emit"),
2348 "Guide should require explicit event emission"
2349 );
2350 assert!(
2352 prompt.contains("`build.done` → Received by: Confessor"),
2353 "Should show Confessor receives build.done"
2354 );
2355 assert!(
2356 prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2357 "Should include receiver's description"
2358 );
2359 assert!(
2361 prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2362 "Should show Ralph receives orphan events"
2363 );
2364 }
2365
2366 #[test]
2367 fn test_event_publishing_guide_no_publishes() {
2368 let yaml = r#"
2370hats:
2371 observer:
2372 name: "Observer"
2373 description: "Only observes"
2374 triggers: ["events.*"]
2375"#;
2376 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2377 let registry = HatRegistry::from_config(&config);
2378 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2379
2380 let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2381 let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2382
2383 assert!(
2385 !prompt.contains("### Event Publishing Guide"),
2386 "Should NOT include Event Publishing Guide when hat has no publishes"
2387 );
2388 }
2389
2390 #[test]
2391 fn test_event_publishing_guide_all_orphan_events() {
2392 let yaml = r#"
2394hats:
2395 solo:
2396 name: "Solo"
2397 triggers: ["solo.start"]
2398 publishes: ["solo.done", "solo.failed"]
2399"#;
2400 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2401 let registry = HatRegistry::from_config(&config);
2402 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2403
2404 let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2405 let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2406
2407 assert!(
2408 prompt.contains("### Event Publishing Guide"),
2409 "Should include guide even for orphan events"
2410 );
2411 assert!(
2412 prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2413 "Orphan solo.done should go to Ralph"
2414 );
2415 assert!(
2416 prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2417 "Orphan solo.failed should go to Ralph"
2418 );
2419 }
2420
2421 #[test]
2422 fn test_event_publishing_guide_multiple_receivers() {
2423 let yaml = r#"
2425hats:
2426 broadcaster:
2427 name: "Broadcaster"
2428 triggers: ["broadcast.start"]
2429 publishes: ["signal.sent"]
2430 listener1:
2431 name: "Listener1"
2432 description: "First listener"
2433 triggers: ["signal.sent"]
2434 listener2:
2435 name: "Listener2"
2436 description: "Second listener"
2437 triggers: ["signal.sent"]
2438"#;
2439 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2440 let registry = HatRegistry::from_config(&config);
2441 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2442
2443 let broadcaster = registry
2444 .get(&ralph_proto::HatId::new("broadcaster"))
2445 .unwrap();
2446 let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2447
2448 assert!(
2449 prompt.contains("### Event Publishing Guide"),
2450 "Should include guide"
2451 );
2452 assert!(
2454 prompt.contains("Listener1 (First listener)"),
2455 "Should list Listener1 as receiver"
2456 );
2457 assert!(
2458 prompt.contains("Listener2 (Second listener)"),
2459 "Should list Listener2 as receiver"
2460 );
2461 }
2462
2463 #[test]
2464 fn test_event_publishing_guide_includes_self() {
2465 let yaml = r#"
2467hats:
2468 looper:
2469 name: "Looper"
2470 triggers: ["loop.continue", "loop.start"]
2471 publishes: ["loop.continue"]
2472"#;
2473 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2474 let registry = HatRegistry::from_config(&config);
2475 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2476
2477 let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2478 let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2479
2480 assert!(
2481 prompt.contains("### Event Publishing Guide"),
2482 "Should include guide"
2483 );
2484 assert!(
2486 prompt.contains("`loop.continue` → Received by: Looper"),
2487 "Self-loop event should show the hat itself as receiver"
2488 );
2489 }
2490
2491 #[test]
2492 fn test_event_publishing_guide_self_loop_shows_self_as_receiver() {
2493 let yaml = r#"
2496hats:
2497 processor:
2498 name: "Processor"
2499 description: "Processes work with retry"
2500 triggers: ["start", "process.retry"]
2501 publishes: ["process.done", "process.retry"]
2502 validator:
2503 name: "Validator"
2504 triggers: ["process.done"]
2505 publishes: ["validate.pass"]
2506"#;
2507 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2508 let registry = HatRegistry::from_config(&config);
2509 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2510
2511 let processor = registry.get(&ralph_proto::HatId::new("processor")).unwrap();
2512 let prompt = ralph.build_prompt("[start] Go", &[processor]);
2513
2514 assert!(
2516 prompt.contains("`process.retry` → Received by: Processor"),
2517 "Self-loop event should show the hat itself as receiver, not Ralph. Got:\n{}",
2518 prompt
2519 .lines()
2520 .filter(|l| l.contains("process.retry"))
2521 .collect::<Vec<_>>()
2522 .join("\n")
2523 );
2524 assert!(
2526 prompt.contains("`process.done` → Received by: Validator"),
2527 "Non-self event should still show correct receiver"
2528 );
2529 }
2530
2531 #[test]
2532 fn test_event_publishing_guide_receiver_without_description() {
2533 let yaml = r#"
2535hats:
2536 sender:
2537 name: "Sender"
2538 triggers: ["send.start"]
2539 publishes: ["message.sent"]
2540 receiver:
2541 name: "NoDescReceiver"
2542 triggers: ["message.sent"]
2543"#;
2544 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2545 let registry = HatRegistry::from_config(&config);
2546 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2547
2548 let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2549 let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2550
2551 assert!(
2552 prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2553 "Should show receiver name without parentheses when no description"
2554 );
2555 assert!(
2557 !prompt.contains("NoDescReceiver ()"),
2558 "Should NOT have empty parentheses for receiver without description"
2559 );
2560 }
2561
2562 #[test]
2565 fn test_constraint_lists_valid_events_when_coordinating() {
2566 let yaml = r#"
2569hats:
2570 test_writer:
2571 name: "Test Writer"
2572 triggers: ["tdd.start"]
2573 publishes: ["test.written"]
2574 implementer:
2575 name: "Implementer"
2576 triggers: ["test.written"]
2577 publishes: ["test.passing"]
2578"#;
2579 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2580 let registry = HatRegistry::from_config(&config);
2581 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2582
2583 let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2585
2586 assert!(
2588 prompt.contains("**CONSTRAINT:**"),
2589 "Prompt should include CONSTRAINT when coordinating"
2590 );
2591 assert!(
2592 prompt.contains("tdd.start"),
2593 "CONSTRAINT should list tdd.start as valid event"
2594 );
2595 assert!(
2596 prompt.contains("test.written"),
2597 "CONSTRAINT should list test.written as valid event"
2598 );
2599 assert!(
2600 prompt.contains("Publishing other events will have no effect"),
2601 "CONSTRAINT should warn about invalid events"
2602 );
2603 }
2604
2605 #[test]
2606 fn test_no_constraint_when_hat_is_active() {
2607 let yaml = r#"
2610hats:
2611 builder:
2612 name: "Builder"
2613 triggers: ["build.task"]
2614 publishes: ["build.done"]
2615 instructions: "Build the code."
2616"#;
2617 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2618 let registry = HatRegistry::from_config(&config);
2619 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2620
2621 let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2623 let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2624
2625 assert!(
2627 !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2628 "Active hat should NOT have coordinating CONSTRAINT"
2629 );
2630
2631 assert!(
2633 prompt.contains("### Event Publishing Guide"),
2634 "Active hat should have Event Publishing Guide"
2635 );
2636 }
2637
2638 #[test]
2639 fn test_no_constraint_when_no_hats() {
2640 let config = RalphConfig::default();
2642 let registry = HatRegistry::new(); let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2644
2645 let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2646
2647 assert!(
2649 !prompt.contains("**CONSTRAINT:**"),
2650 "Solo mode should NOT have CONSTRAINT"
2651 );
2652 }
2653
2654 #[test]
2657 fn test_single_guidance_injection() {
2658 let config = RalphConfig::default();
2660 let registry = HatRegistry::new();
2661 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2662 ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2663
2664 let prompt = ralph.build_prompt("", &[]);
2665
2666 assert!(
2667 prompt.contains("## ROBOT GUIDANCE"),
2668 "Should include ROBOT GUIDANCE section"
2669 );
2670 assert!(
2671 prompt.contains("Focus on error handling first"),
2672 "Should contain the guidance message"
2673 );
2674 assert!(
2676 !prompt.contains("1. Focus on error handling first"),
2677 "Single guidance should not be numbered"
2678 );
2679 }
2680
2681 #[test]
2682 fn test_multiple_guidance_squashing() {
2683 let config = RalphConfig::default();
2685 let registry = HatRegistry::new();
2686 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2687 ralph.set_robot_guidance(vec![
2688 "Focus on error handling".to_string(),
2689 "Use the existing retry pattern".to_string(),
2690 "Check edge cases for empty input".to_string(),
2691 ]);
2692
2693 let prompt = ralph.build_prompt("", &[]);
2694
2695 assert!(
2696 prompt.contains("## ROBOT GUIDANCE"),
2697 "Should include ROBOT GUIDANCE section"
2698 );
2699 assert!(
2700 prompt.contains("1. Focus on error handling"),
2701 "First guidance should be numbered 1"
2702 );
2703 assert!(
2704 prompt.contains("2. Use the existing retry pattern"),
2705 "Second guidance should be numbered 2"
2706 );
2707 assert!(
2708 prompt.contains("3. Check edge cases for empty input"),
2709 "Third guidance should be numbered 3"
2710 );
2711 }
2712
2713 #[test]
2714 fn test_guidance_appears_in_prompt_before_events() {
2715 let config = RalphConfig::default();
2717 let registry = HatRegistry::new();
2718 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2719 ralph.set_objective("Build feature X".to_string());
2720 ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2721
2722 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2723
2724 let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2725 let guidance_pos = prompt
2726 .find("## ROBOT GUIDANCE")
2727 .expect("Should have ROBOT GUIDANCE");
2728 let events_pos = prompt
2729 .find("## PENDING EVENTS")
2730 .expect("Should have PENDING EVENTS");
2731
2732 assert!(
2733 objective_pos < guidance_pos,
2734 "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2735 objective_pos,
2736 guidance_pos
2737 );
2738 assert!(
2739 guidance_pos < events_pos,
2740 "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2741 guidance_pos,
2742 events_pos
2743 );
2744 }
2745
2746 #[test]
2747 fn test_guidance_cleared_after_injection() {
2748 let config = RalphConfig::default();
2750 let registry = HatRegistry::new();
2751 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2752 ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2753
2754 let prompt1 = ralph.build_prompt("", &[]);
2756 assert!(
2757 prompt1.contains("## ROBOT GUIDANCE"),
2758 "First prompt should have guidance"
2759 );
2760
2761 ralph.clear_robot_guidance();
2763
2764 let prompt2 = ralph.build_prompt("", &[]);
2766 assert!(
2767 !prompt2.contains("## ROBOT GUIDANCE"),
2768 "After clearing, prompt should not have guidance"
2769 );
2770 }
2771
2772 #[test]
2773 fn test_no_injection_when_no_guidance() {
2774 let config = RalphConfig::default();
2776 let registry = HatRegistry::new();
2777 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2778
2779 let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2780
2781 assert!(
2782 !prompt.contains("## ROBOT GUIDANCE"),
2783 "Should NOT include ROBOT GUIDANCE when no guidance set"
2784 );
2785 }
2786
2787 #[test]
2791 fn test_scratchpad_disabled_suppresses_all_sections() {
2792 use crate::config::ScratchpadConfig;
2793
2794 let config = RalphConfig::default();
2795 let registry = HatRegistry::new();
2796 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2797 .with_memories_enabled(true);
2798 ralph.set_active_scratchpad(ScratchpadConfig {
2799 enabled: false,
2800 path: ".ralph/agent/scratchpad.md".to_string(),
2801 });
2802
2803 let prompt = ralph.build_prompt("", &[]);
2804
2805 assert!(
2807 !prompt.contains("Review your `<scratchpad>`"),
2808 "Disabled scratchpad: ORIENTATION should not reference scratchpad"
2809 );
2810 assert!(
2812 !prompt.contains("### 0b. SCRATCHPAD"),
2813 "Disabled scratchpad: SCRATCHPAD section should be suppressed"
2814 );
2815 assert!(
2817 !prompt.contains("**Scratchpad**"),
2818 "Disabled scratchpad: STATE MANAGEMENT should not reference Scratchpad"
2819 );
2820 assert!(
2821 !prompt.contains("Thinking goes in scratchpad"),
2822 "Disabled scratchpad: Rule should not reference scratchpad"
2823 );
2824 assert!(
2826 !prompt.contains("update scratchpad"),
2827 "Disabled scratchpad: WORKFLOW should not reference scratchpad"
2828 );
2829 assert!(
2831 !prompt.contains("write detailed output to"),
2832 "Disabled scratchpad: EVENT WRITING should not reference scratchpad"
2833 );
2834 }
2835
2836 #[test]
2838 fn test_scratchpad_custom_path_in_prompt() {
2839 use crate::config::ScratchpadConfig;
2840
2841 let config = RalphConfig::default();
2842 let registry = HatRegistry::new();
2843 let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2844 .with_memories_enabled(true);
2845 ralph.set_active_scratchpad(ScratchpadConfig {
2846 enabled: true,
2847 path: ".ralph/agent/planner.md".to_string(),
2848 });
2849
2850 let prompt = ralph.build_prompt("", &[]);
2851
2852 assert!(
2854 prompt.contains("`.ralph/agent/planner.md` is your thinking journal"),
2855 "Custom path should appear in SCRATCHPAD section"
2856 );
2857 assert!(
2859 prompt.contains("**Scratchpad** (`.ralph/agent/planner.md`)"),
2860 "STATE MANAGEMENT should reference custom path"
2861 );
2862 assert!(
2864 prompt.contains("`.ralph/agent/planner.md`") && prompt.contains("PLAN"),
2865 "WORKFLOW should reference custom path"
2866 );
2867 assert!(
2869 prompt.contains("write detailed output to `.ralph/agent/planner.md`"),
2870 "EVENT WRITING should use custom scratchpad path"
2871 );
2872 }
2873
2874 #[test]
2876 fn test_scratchpad_inherits_global_path() {
2877 let config = RalphConfig::default();
2878 let registry = HatRegistry::new();
2879 let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None);
2880
2881 let prompt = ralph.build_prompt("", &[]);
2882
2883 assert!(
2884 prompt.contains("`.ralph/agent/scratchpad.md`"),
2885 "Default global path should be used"
2886 );
2887 assert!(
2888 prompt.contains("### 0b. SCRATCHPAD"),
2889 "SCRATCHPAD section should be present"
2890 );
2891 }
2892
2893 #[test]
2895 fn test_disabled_scratchpad_is_fresh_start() {
2896 use crate::config::ScratchpadConfig;
2897
2898 let yaml = r#"
2899hats:
2900 tdd_writer:
2901 name: "TDD Writer"
2902 triggers: ["tdd.start"]
2903 publishes: ["test.written"]
2904"#;
2905 let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2906 let registry = HatRegistry::from_config(&config);
2907 let mut ralph = HatlessRalph::new(
2908 "LOOP_COMPLETE",
2909 config.core.clone(),
2910 ®istry,
2911 Some("tdd.start".to_string()),
2912 );
2913 ralph.set_active_scratchpad(ScratchpadConfig {
2914 enabled: false,
2915 path: ".ralph/agent/scratchpad.md".to_string(),
2916 });
2917
2918 ralph.set_iteration(0);
2920 let prompt = ralph.build_prompt("", &[]);
2921 assert!(
2922 prompt.contains("FAST PATH"),
2923 "First iteration with disabled scratchpad should trigger fast path"
2924 );
2925
2926 ralph.set_iteration(1);
2928 let prompt = ralph.build_prompt("", &[]);
2929 assert!(
2930 !prompt.contains("FAST PATH"),
2931 "Second iteration should NOT trigger fast path even with disabled scratchpad"
2932 );
2933 }
2934
2935 #[test]
2937 fn test_multiple_hats_different_scratchpad_prompts() {
2938 use crate::config::ScratchpadConfig;
2939
2940 let config = RalphConfig::default();
2941 let registry = HatRegistry::new();
2942
2943 let mut ralph_planner =
2945 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2946 .with_memories_enabled(true);
2947 ralph_planner.set_active_scratchpad(ScratchpadConfig {
2948 enabled: true,
2949 path: ".ralph/agent/planner.md".to_string(),
2950 });
2951 let planner_prompt = ralph_planner.build_prompt("", &[]);
2952 assert!(
2953 planner_prompt.contains("`.ralph/agent/planner.md`"),
2954 "Planner should use custom path"
2955 );
2956
2957 let mut ralph_builder =
2959 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2960 .with_memories_enabled(true);
2961 ralph_builder.set_active_scratchpad(config.core.scratchpad.clone());
2962 let builder_prompt = ralph_builder.build_prompt("", &[]);
2963 assert!(
2964 builder_prompt.contains("`.ralph/agent/scratchpad.md`"),
2965 "Builder should use global path"
2966 );
2967
2968 let mut ralph_validator =
2970 HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), ®istry, None)
2971 .with_memories_enabled(true);
2972 ralph_validator.set_active_scratchpad(ScratchpadConfig {
2973 enabled: false,
2974 path: ".ralph/agent/scratchpad.md".to_string(),
2975 });
2976 let validator_prompt = ralph_validator.build_prompt("", &[]);
2977 assert!(
2978 !validator_prompt.contains("### 0b. SCRATCHPAD"),
2979 "Validator should have no scratchpad sections"
2980 );
2981 }
2982}