Skip to main content

ralph_core/
hatless_ralph.rs

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