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` - String 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"
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 output {} when the objective is complete and all tasks are done.
837",
838            self.completion_promise
839        );
840
841        // Add task verification when memories/tasks mode is enabled
842        if self.memories_enabled {
843            section.push_str(
844                r"
845**Before declaring completion:**
8461. Run `ralph tools task ready` to check for open tasks
8472. If any tasks are open, complete them first
8483. Only output LOOP_COMPLETE when YOUR tasks are all closed
849
850Tasks from other parallel loops are filtered out automatically. You only need to verify tasks YOU created for THIS objective are complete.
851
852You MUST NOT output LOOP_COMPLETE while tasks remain open.
853",
854            );
855        }
856
857        // Reinforce the objective at the end to bookend the prompt
858        if let Some(obj) = objective {
859            section.push_str(&format!(
860                r"
861**Remember your objective:**
862> {}
863
864You MUST NOT declare completion until this objective is fully satisfied.
865",
866                obj
867            ));
868        }
869
870        section
871    }
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use crate::config::RalphConfig;
878
879    #[test]
880    fn test_prompt_without_hats() {
881        let config = RalphConfig::default();
882        let registry = HatRegistry::new(); // Empty registry
883        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
884
885        let prompt = ralph.build_prompt("", &[]);
886
887        // Identity with RFC2119 style
888        assert!(prompt.contains(
889            "You are Ralph. You are running in a loop. You have fresh context each iteration."
890        ));
891
892        // Numbered orientation phases (RFC2119)
893        assert!(prompt.contains("### 0a. ORIENTATION"));
894        assert!(prompt.contains("MUST complete only one atomic task"));
895
896        // Scratchpad section with auto-inject and append instructions
897        assert!(prompt.contains("### 0b. SCRATCHPAD"));
898        assert!(prompt.contains("auto-injected"));
899        assert!(prompt.contains("**Always append**"));
900
901        // Workflow with numbered steps (solo mode) using RFC2119
902        assert!(prompt.contains("## WORKFLOW"));
903        assert!(prompt.contains("### 1. Study the prompt"));
904        assert!(prompt.contains("You MAY use parallel subagents (up to 10)"));
905        assert!(prompt.contains("### 2. PLAN"));
906        assert!(prompt.contains("### 3. IMPLEMENT"));
907        assert!(prompt.contains("You MUST NOT use more than 1 subagent for build/tests"));
908        assert!(prompt.contains("### 4. COMMIT"));
909        assert!(prompt.contains("You MUST capture the why"));
910        assert!(prompt.contains("### 5. REPEAT"));
911
912        // Should NOT have hats section when no hats
913        assert!(!prompt.contains("## HATS"));
914
915        // Event writing and completion using RFC2119
916        assert!(prompt.contains("## EVENT WRITING"));
917        assert!(prompt.contains("You MUST use `ralph emit`"));
918        assert!(prompt.contains("You MUST NOT use echo/cat"));
919        assert!(prompt.contains("LOOP_COMPLETE"));
920    }
921
922    #[test]
923    fn test_prompt_with_hats() {
924        // Test multi-hat mode WITHOUT starting_event (no fast path)
925        let yaml = r#"
926hats:
927  planner:
928    name: "Planner"
929    triggers: ["planning.start", "build.done", "build.blocked"]
930    publishes: ["build.task"]
931  builder:
932    name: "Builder"
933    triggers: ["build.task"]
934    publishes: ["build.done", "build.blocked"]
935"#;
936        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
937        let registry = HatRegistry::from_config(&config);
938        // Note: No starting_event - tests normal multi-hat workflow (not fast path)
939        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
940
941        let prompt = ralph.build_prompt("", &[]);
942
943        // Identity with RFC2119 style
944        assert!(prompt.contains(
945            "You are Ralph. You are running in a loop. You have fresh context each iteration."
946        ));
947
948        // Orientation phases
949        assert!(prompt.contains("### 0a. ORIENTATION"));
950        assert!(prompt.contains("### 0b. SCRATCHPAD"));
951
952        // Multi-hat workflow: PLAN + DELEGATE, not IMPLEMENT (RFC2119)
953        assert!(prompt.contains("## WORKFLOW"));
954        assert!(prompt.contains("### 1. PLAN"));
955        assert!(
956            prompt.contains("### 2. DELEGATE"),
957            "Multi-hat mode should have DELEGATE step"
958        );
959        assert!(
960            !prompt.contains("### 3. IMPLEMENT"),
961            "Multi-hat mode should NOT tell Ralph to implement"
962        );
963        assert!(
964            prompt.contains("You MUST stop working after publishing"),
965            "Should explicitly tell Ralph to stop after publishing event"
966        );
967
968        // Hats section when hats are defined
969        assert!(prompt.contains("## HATS"));
970        assert!(prompt.contains("Delegate via events"));
971        assert!(prompt.contains("| Hat | Triggers On | Publishes |"));
972
973        // Event writing and completion
974        assert!(prompt.contains("## EVENT WRITING"));
975        assert!(prompt.contains("LOOP_COMPLETE"));
976    }
977
978    #[test]
979    fn test_should_handle_always_true() {
980        let config = RalphConfig::default();
981        let registry = HatRegistry::new();
982        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
983
984        assert!(ralph.should_handle(&Topic::new("any.topic")));
985        assert!(ralph.should_handle(&Topic::new("build.task")));
986        assert!(ralph.should_handle(&Topic::new("unknown.event")));
987    }
988
989    #[test]
990    fn test_rfc2119_patterns_present() {
991        let config = RalphConfig::default();
992        let registry = HatRegistry::new();
993        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
994
995        let prompt = ralph.build_prompt("", &[]);
996
997        // Key RFC2119 language patterns
998        assert!(
999            prompt.contains("You MUST study"),
1000            "Should use RFC2119 MUST with 'study' verb"
1001        );
1002        assert!(
1003            prompt.contains("You MUST complete only one atomic task"),
1004            "Should have RFC2119 MUST complete atomic task constraint"
1005        );
1006        assert!(
1007            prompt.contains("You MAY use parallel subagents"),
1008            "Should mention parallel subagents with MAY"
1009        );
1010        assert!(
1011            prompt.contains("You MUST NOT use more than 1 subagent"),
1012            "Should limit to 1 subagent for builds with MUST NOT"
1013        );
1014        assert!(
1015            prompt.contains("You MUST capture the why"),
1016            "Should emphasize 'why' in commits with MUST"
1017        );
1018
1019        // Numbered guardrails (999+)
1020        assert!(
1021            prompt.contains("### GUARDRAILS"),
1022            "Should have guardrails section"
1023        );
1024        assert!(
1025            prompt.contains("999."),
1026            "Guardrails should use high numbers"
1027        );
1028    }
1029
1030    #[test]
1031    fn test_scratchpad_format_documented() {
1032        let config = RalphConfig::default();
1033        let registry = HatRegistry::new();
1034        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1035
1036        let prompt = ralph.build_prompt("", &[]);
1037
1038        // Auto-injection and append instructions are documented
1039        assert!(prompt.contains("auto-injected"));
1040        assert!(prompt.contains("**Always append**"));
1041    }
1042
1043    #[test]
1044    fn test_starting_event_in_prompt() {
1045        // When starting_event is configured, prompt should include delegation instruction
1046        let yaml = r#"
1047hats:
1048  tdd_writer:
1049    name: "TDD Writer"
1050    triggers: ["tdd.start"]
1051    publishes: ["test.written"]
1052"#;
1053        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1054        let registry = HatRegistry::from_config(&config);
1055        let ralph = HatlessRalph::new(
1056            "LOOP_COMPLETE",
1057            config.core.clone(),
1058            &registry,
1059            Some("tdd.start".to_string()),
1060        );
1061
1062        let prompt = ralph.build_prompt("", &[]);
1063
1064        // Should include delegation instruction
1065        assert!(
1066            prompt.contains("After coordination, publish `tdd.start` to start the workflow"),
1067            "Prompt should include starting_event delegation instruction"
1068        );
1069    }
1070
1071    #[test]
1072    fn test_no_starting_event_instruction_when_none() {
1073        // When starting_event is None, no delegation instruction should appear
1074        let yaml = r#"
1075hats:
1076  some_hat:
1077    name: "Some Hat"
1078    triggers: ["some.event"]
1079"#;
1080        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1081        let registry = HatRegistry::from_config(&config);
1082        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1083
1084        let prompt = ralph.build_prompt("", &[]);
1085
1086        // Should NOT include delegation instruction
1087        assert!(
1088            !prompt.contains("After coordination, publish"),
1089            "Prompt should NOT include starting_event delegation when None"
1090        );
1091    }
1092
1093    #[test]
1094    fn test_hat_instructions_propagated_to_prompt() {
1095        // When a hat has instructions defined in config,
1096        // those instructions should appear in the generated prompt
1097        let yaml = r#"
1098hats:
1099  tdd_writer:
1100    name: "TDD Writer"
1101    triggers: ["tdd.start"]
1102    publishes: ["test.written"]
1103    instructions: |
1104      You are a Test-Driven Development specialist.
1105      Always write failing tests before implementation.
1106      Focus on edge cases and error handling.
1107"#;
1108        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1109        let registry = HatRegistry::from_config(&config);
1110        let ralph = HatlessRalph::new(
1111            "LOOP_COMPLETE",
1112            config.core.clone(),
1113            &registry,
1114            Some("tdd.start".to_string()),
1115        );
1116
1117        // Get the tdd_writer hat as active to see its instructions
1118        let tdd_writer = registry
1119            .get(&ralph_proto::HatId::new("tdd_writer"))
1120            .unwrap();
1121        let prompt = ralph.build_prompt("", &[tdd_writer]);
1122
1123        // Instructions should appear in the prompt
1124        assert!(
1125            prompt.contains("### TDD Writer Instructions"),
1126            "Prompt should include hat instructions section header"
1127        );
1128        assert!(
1129            prompt.contains("Test-Driven Development specialist"),
1130            "Prompt should include actual instructions content"
1131        );
1132        assert!(
1133            prompt.contains("Always write failing tests"),
1134            "Prompt should include full instructions"
1135        );
1136    }
1137
1138    #[test]
1139    fn test_empty_instructions_not_rendered() {
1140        // When a hat has empty/no instructions, no instructions section should appear
1141        let yaml = r#"
1142hats:
1143  builder:
1144    name: "Builder"
1145    triggers: ["build.task"]
1146    publishes: ["build.done"]
1147"#;
1148        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1149        let registry = HatRegistry::from_config(&config);
1150        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1151
1152        let prompt = ralph.build_prompt("", &[]);
1153
1154        // No instructions section should appear for hats without instructions
1155        assert!(
1156            !prompt.contains("### Builder Instructions"),
1157            "Prompt should NOT include instructions section for hat with empty instructions"
1158        );
1159    }
1160
1161    #[test]
1162    fn test_multiple_hats_with_instructions() {
1163        // When multiple hats have instructions, each should have its own section
1164        let yaml = r#"
1165hats:
1166  planner:
1167    name: "Planner"
1168    triggers: ["planning.start"]
1169    publishes: ["build.task"]
1170    instructions: "Plan carefully before implementation."
1171  builder:
1172    name: "Builder"
1173    triggers: ["build.task"]
1174    publishes: ["build.done"]
1175    instructions: "Focus on clean, testable code."
1176"#;
1177        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1178        let registry = HatRegistry::from_config(&config);
1179        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1180
1181        // Get both hats as active to see their instructions
1182        let planner = registry.get(&ralph_proto::HatId::new("planner")).unwrap();
1183        let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1184        let prompt = ralph.build_prompt("", &[planner, builder]);
1185
1186        // Both hats' instructions should appear
1187        assert!(
1188            prompt.contains("### Planner Instructions"),
1189            "Prompt should include Planner instructions section"
1190        );
1191        assert!(
1192            prompt.contains("Plan carefully before implementation"),
1193            "Prompt should include Planner instructions content"
1194        );
1195        assert!(
1196            prompt.contains("### Builder Instructions"),
1197            "Prompt should include Builder instructions section"
1198        );
1199        assert!(
1200            prompt.contains("Focus on clean, testable code"),
1201            "Prompt should include Builder instructions content"
1202        );
1203    }
1204
1205    #[test]
1206    fn test_fast_path_with_starting_event() {
1207        // When starting_event is configured AND scratchpad doesn't exist,
1208        // should use fast path (skip PLAN step)
1209        let yaml = r#"
1210core:
1211  scratchpad: "/nonexistent/path/scratchpad.md"
1212hats:
1213  tdd_writer:
1214    name: "TDD Writer"
1215    triggers: ["tdd.start"]
1216    publishes: ["test.written"]
1217"#;
1218        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1219        let registry = HatRegistry::from_config(&config);
1220        let ralph = HatlessRalph::new(
1221            "LOOP_COMPLETE",
1222            config.core.clone(),
1223            &registry,
1224            Some("tdd.start".to_string()),
1225        );
1226
1227        let prompt = ralph.build_prompt("", &[]);
1228
1229        // Should use fast path - immediate delegation with RFC2119
1230        assert!(
1231            prompt.contains("FAST PATH"),
1232            "Prompt should indicate fast path when starting_event set and no scratchpad"
1233        );
1234        assert!(
1235            prompt.contains("You MUST publish `tdd.start` immediately"),
1236            "Prompt should instruct immediate event publishing with MUST"
1237        );
1238        assert!(
1239            !prompt.contains("### 1. PLAN"),
1240            "Fast path should skip PLAN step"
1241        );
1242    }
1243
1244    #[test]
1245    fn test_events_context_included_in_prompt() {
1246        // Given a non-empty events context
1247        // When build_prompt(context) is called
1248        // Then the prompt contains ## PENDING EVENTS section with the context
1249        let config = RalphConfig::default();
1250        let registry = HatRegistry::new();
1251        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1252
1253        let events_context = r"[task.start] User's task: Review this code for security vulnerabilities
1254[build.done] Build completed successfully";
1255
1256        let prompt = ralph.build_prompt(events_context, &[]);
1257
1258        assert!(
1259            prompt.contains("## PENDING EVENTS"),
1260            "Prompt should contain PENDING EVENTS section"
1261        );
1262        assert!(
1263            prompt.contains("Review this code for security vulnerabilities"),
1264            "Prompt should contain the user's task"
1265        );
1266        assert!(
1267            prompt.contains("Build completed successfully"),
1268            "Prompt should contain all events from context"
1269        );
1270    }
1271
1272    #[test]
1273    fn test_empty_context_no_pending_events_section() {
1274        // Given an empty events context
1275        // When build_prompt("") is called
1276        // Then no PENDING EVENTS section appears
1277        let config = RalphConfig::default();
1278        let registry = HatRegistry::new();
1279        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1280
1281        let prompt = ralph.build_prompt("", &[]);
1282
1283        assert!(
1284            !prompt.contains("## PENDING EVENTS"),
1285            "Empty context should not produce PENDING EVENTS section"
1286        );
1287    }
1288
1289    #[test]
1290    fn test_whitespace_only_context_no_pending_events_section() {
1291        // Given a whitespace-only events context
1292        // When build_prompt is called
1293        // Then no PENDING EVENTS section appears
1294        let config = RalphConfig::default();
1295        let registry = HatRegistry::new();
1296        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1297
1298        let prompt = ralph.build_prompt("   \n\t  ", &[]);
1299
1300        assert!(
1301            !prompt.contains("## PENDING EVENTS"),
1302            "Whitespace-only context should not produce PENDING EVENTS section"
1303        );
1304    }
1305
1306    #[test]
1307    fn test_events_section_before_workflow() {
1308        // Given events context with a task
1309        // When prompt is built
1310        // Then ## PENDING EVENTS appears BEFORE ## WORKFLOW
1311        let config = RalphConfig::default();
1312        let registry = HatRegistry::new();
1313        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1314
1315        let events_context = "[task.start] Implement feature X";
1316        let prompt = ralph.build_prompt(events_context, &[]);
1317
1318        let events_pos = prompt
1319            .find("## PENDING EVENTS")
1320            .expect("Should have PENDING EVENTS");
1321        let workflow_pos = prompt.find("## WORKFLOW").expect("Should have WORKFLOW");
1322
1323        assert!(
1324            events_pos < workflow_pos,
1325            "PENDING EVENTS ({}) should come before WORKFLOW ({})",
1326            events_pos,
1327            workflow_pos
1328        );
1329    }
1330
1331    // === Phase 3: Filtered Hat Instructions Tests ===
1332
1333    #[test]
1334    fn test_only_active_hat_instructions_included() {
1335        // Scenario 4 from plan.md: Only active hat instructions included in prompt
1336        let yaml = r#"
1337hats:
1338  security_reviewer:
1339    name: "Security Reviewer"
1340    triggers: ["review.security"]
1341    instructions: "Review code for security vulnerabilities."
1342  architecture_reviewer:
1343    name: "Architecture Reviewer"
1344    triggers: ["review.architecture"]
1345    instructions: "Review system design and architecture."
1346  correctness_reviewer:
1347    name: "Correctness Reviewer"
1348    triggers: ["review.correctness"]
1349    instructions: "Review logic and correctness."
1350"#;
1351        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1352        let registry = HatRegistry::from_config(&config);
1353        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1354
1355        // Get active hats - only security_reviewer is active
1356        let security_hat = registry
1357            .get(&ralph_proto::HatId::new("security_reviewer"))
1358            .unwrap();
1359        let active_hats = vec![security_hat];
1360
1361        let prompt = ralph.build_prompt("Event: review.security - Check auth", &active_hats);
1362
1363        // Should contain ONLY security_reviewer instructions
1364        assert!(
1365            prompt.contains("### Security Reviewer Instructions"),
1366            "Should include Security Reviewer instructions section"
1367        );
1368        assert!(
1369            prompt.contains("Review code for security vulnerabilities"),
1370            "Should include Security Reviewer instructions content"
1371        );
1372
1373        // Should NOT contain other hats' instructions
1374        assert!(
1375            !prompt.contains("### Architecture Reviewer Instructions"),
1376            "Should NOT include Architecture Reviewer instructions"
1377        );
1378        assert!(
1379            !prompt.contains("Review system design and architecture"),
1380            "Should NOT include Architecture Reviewer instructions content"
1381        );
1382        assert!(
1383            !prompt.contains("### Correctness Reviewer Instructions"),
1384            "Should NOT include Correctness Reviewer instructions"
1385        );
1386    }
1387
1388    #[test]
1389    fn test_multiple_active_hats_all_included() {
1390        // Scenario 6 from plan.md: Multiple active hats includes all instructions
1391        let yaml = r#"
1392hats:
1393  security_reviewer:
1394    name: "Security Reviewer"
1395    triggers: ["review.security"]
1396    instructions: "Review code for security vulnerabilities."
1397  architecture_reviewer:
1398    name: "Architecture Reviewer"
1399    triggers: ["review.architecture"]
1400    instructions: "Review system design and architecture."
1401  correctness_reviewer:
1402    name: "Correctness Reviewer"
1403    triggers: ["review.correctness"]
1404    instructions: "Review logic and correctness."
1405"#;
1406        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1407        let registry = HatRegistry::from_config(&config);
1408        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1409
1410        // Get active hats - both security_reviewer and architecture_reviewer are active
1411        let security_hat = registry
1412            .get(&ralph_proto::HatId::new("security_reviewer"))
1413            .unwrap();
1414        let arch_hat = registry
1415            .get(&ralph_proto::HatId::new("architecture_reviewer"))
1416            .unwrap();
1417        let active_hats = vec![security_hat, arch_hat];
1418
1419        let prompt = ralph.build_prompt("Events", &active_hats);
1420
1421        // Should contain BOTH active hats' instructions
1422        assert!(
1423            prompt.contains("### Security Reviewer Instructions"),
1424            "Should include Security Reviewer instructions"
1425        );
1426        assert!(
1427            prompt.contains("Review code for security vulnerabilities"),
1428            "Should include Security Reviewer content"
1429        );
1430        assert!(
1431            prompt.contains("### Architecture Reviewer Instructions"),
1432            "Should include Architecture Reviewer instructions"
1433        );
1434        assert!(
1435            prompt.contains("Review system design and architecture"),
1436            "Should include Architecture Reviewer content"
1437        );
1438
1439        // Should NOT contain inactive hat's instructions
1440        assert!(
1441            !prompt.contains("### Correctness Reviewer Instructions"),
1442            "Should NOT include Correctness Reviewer instructions"
1443        );
1444    }
1445
1446    #[test]
1447    fn test_no_active_hats_no_instructions() {
1448        // No active hats = no instructions section (but topology table still present)
1449        let yaml = r#"
1450hats:
1451  security_reviewer:
1452    name: "Security Reviewer"
1453    triggers: ["review.security"]
1454    instructions: "Review code for security vulnerabilities."
1455"#;
1456        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1457        let registry = HatRegistry::from_config(&config);
1458        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1459
1460        // No active hats
1461        let active_hats: Vec<&ralph_proto::Hat> = vec![];
1462
1463        let prompt = ralph.build_prompt("Events", &active_hats);
1464
1465        // Should NOT contain any instructions
1466        assert!(
1467            !prompt.contains("### Security Reviewer Instructions"),
1468            "Should NOT include instructions when no active hats"
1469        );
1470        assert!(
1471            !prompt.contains("Review code for security vulnerabilities"),
1472            "Should NOT include instructions content when no active hats"
1473        );
1474
1475        // But topology table should still be present
1476        assert!(prompt.contains("## HATS"), "Should still have HATS section");
1477        assert!(
1478            prompt.contains("| Hat | Triggers On | Publishes |"),
1479            "Should still have topology table"
1480        );
1481    }
1482
1483    #[test]
1484    fn test_topology_table_only_when_ralph_coordinating() {
1485        // Topology table + Mermaid shown only when Ralph is coordinating (no active hats)
1486        // When a hat is active, skip the table to reduce token usage
1487        let yaml = r#"
1488hats:
1489  security_reviewer:
1490    name: "Security Reviewer"
1491    triggers: ["review.security"]
1492    instructions: "Security instructions."
1493  architecture_reviewer:
1494    name: "Architecture Reviewer"
1495    triggers: ["review.architecture"]
1496    instructions: "Architecture instructions."
1497"#;
1498        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1499        let registry = HatRegistry::from_config(&config);
1500        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1501
1502        // Test 1: No active hats (Ralph coordinating) - should show table + Mermaid
1503        let prompt_coordinating = ralph.build_prompt("Events", &[]);
1504
1505        assert!(
1506            prompt_coordinating.contains("## HATS"),
1507            "Should have HATS section when coordinating"
1508        );
1509        assert!(
1510            prompt_coordinating.contains("| Hat | Triggers On | Publishes |"),
1511            "Should have topology table when coordinating"
1512        );
1513        assert!(
1514            prompt_coordinating.contains("```mermaid"),
1515            "Should have Mermaid diagram when coordinating"
1516        );
1517
1518        // Test 2: Active hat - should NOT show table/Mermaid, just instructions
1519        let security_hat = registry
1520            .get(&ralph_proto::HatId::new("security_reviewer"))
1521            .unwrap();
1522        let prompt_active = ralph.build_prompt("Events", &[security_hat]);
1523
1524        assert!(
1525            prompt_active.contains("## ACTIVE HAT"),
1526            "Should have ACTIVE HAT section when hat is active"
1527        );
1528        assert!(
1529            !prompt_active.contains("| Hat | Triggers On | Publishes |"),
1530            "Should NOT have topology table when hat is active"
1531        );
1532        assert!(
1533            !prompt_active.contains("```mermaid"),
1534            "Should NOT have Mermaid diagram when hat is active"
1535        );
1536        assert!(
1537            prompt_active.contains("### Security Reviewer Instructions"),
1538            "Should still have the active hat's instructions"
1539        );
1540    }
1541
1542    // === Memories/Scratchpad Exclusivity Tests ===
1543
1544    #[test]
1545    fn test_scratchpad_always_included() {
1546        // Scratchpad section should always be included (regardless of memories mode)
1547        let config = RalphConfig::default();
1548        let registry = HatRegistry::new();
1549        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1550
1551        let prompt = ralph.build_prompt("", &[]);
1552
1553        assert!(
1554            prompt.contains("### 0b. SCRATCHPAD"),
1555            "Scratchpad section should be included"
1556        );
1557        assert!(
1558            prompt.contains("`.ralph/agent/scratchpad.md`"),
1559            "Scratchpad path should be referenced"
1560        );
1561        assert!(
1562            prompt.contains("auto-injected"),
1563            "Auto-injection should be documented"
1564        );
1565    }
1566
1567    #[test]
1568    fn test_scratchpad_included_with_memories_enabled() {
1569        // When memories are enabled, scratchpad should STILL be included (not excluded)
1570        let config = RalphConfig::default();
1571        let registry = HatRegistry::new();
1572        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1573            .with_memories_enabled(true);
1574
1575        let prompt = ralph.build_prompt("", &[]);
1576
1577        // Scratchpad should still be present
1578        assert!(
1579            prompt.contains("### 0b. SCRATCHPAD"),
1580            "Scratchpad section should be included even with memories enabled"
1581        );
1582        assert!(
1583            prompt.contains("**Always append**"),
1584            "Append instruction should be documented"
1585        );
1586
1587        // Tasks section is now injected via the skills pipeline (not in core_prompt)
1588        assert!(
1589            !prompt.contains("### 0c. TASKS"),
1590            "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1591        );
1592    }
1593
1594    #[test]
1595    fn test_no_tasks_section_in_core_prompt() {
1596        // Tasks section is now in the skills pipeline, not core_prompt
1597        let config = RalphConfig::default();
1598        let registry = HatRegistry::new();
1599        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1600
1601        let prompt = ralph.build_prompt("", &[]);
1602
1603        // core_prompt no longer contains the tasks section (injected via skills)
1604        assert!(
1605            !prompt.contains("### 0c. TASKS"),
1606            "Tasks section should NOT be in core_prompt — injected via skills pipeline"
1607        );
1608    }
1609
1610    #[test]
1611    fn test_workflow_references_both_scratchpad_and_tasks_with_memories() {
1612        // When memories enabled, workflow should reference BOTH scratchpad AND tasks CLI
1613        let config = RalphConfig::default();
1614        let registry = HatRegistry::new();
1615        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1616            .with_memories_enabled(true);
1617
1618        let prompt = ralph.build_prompt("", &[]);
1619
1620        // Workflow should mention scratchpad
1621        assert!(
1622            prompt.contains("update scratchpad"),
1623            "Workflow should reference scratchpad when memories enabled"
1624        );
1625        // Workflow should also mention tasks CLI
1626        assert!(
1627            prompt.contains("ralph tools task"),
1628            "Workflow should reference tasks CLI when memories enabled"
1629        );
1630    }
1631
1632    #[test]
1633    fn test_multi_hat_mode_workflow_with_memories_enabled() {
1634        // Multi-hat mode should reference scratchpad AND tasks CLI when memories enabled
1635        let yaml = r#"
1636hats:
1637  builder:
1638    name: "Builder"
1639    triggers: ["build.task"]
1640    publishes: ["build.done"]
1641"#;
1642        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1643        let registry = HatRegistry::from_config(&config);
1644        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1645            .with_memories_enabled(true);
1646
1647        let prompt = ralph.build_prompt("", &[]);
1648
1649        // Multi-hat workflow should mention scratchpad
1650        assert!(
1651            prompt.contains("scratchpad"),
1652            "Multi-hat workflow should reference scratchpad when memories enabled"
1653        );
1654        // And tasks CLI
1655        assert!(
1656            prompt.contains("ralph tools task add"),
1657            "Multi-hat workflow should reference tasks CLI when memories enabled"
1658        );
1659    }
1660
1661    #[test]
1662    fn test_guardrails_adapt_to_memories_mode() {
1663        // When memories enabled, guardrails should encourage saving to memories
1664        let config = RalphConfig::default();
1665        let registry = HatRegistry::new();
1666        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1667            .with_memories_enabled(true);
1668
1669        let prompt = ralph.build_prompt("", &[]);
1670
1671        // With memories enabled + include_scratchpad still true (default),
1672        // the guardrail transformation doesn't apply
1673        // Just verify the prompt generates correctly
1674        assert!(
1675            prompt.contains("### GUARDRAILS"),
1676            "Guardrails section should be present"
1677        );
1678    }
1679
1680    #[test]
1681    fn test_guardrails_present_without_memories() {
1682        // Without memories, guardrails should still be present
1683        let config = RalphConfig::default();
1684        let registry = HatRegistry::new();
1685        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1686        // memories_enabled defaults to false
1687
1688        let prompt = ralph.build_prompt("", &[]);
1689
1690        assert!(
1691            prompt.contains("### GUARDRAILS"),
1692            "Guardrails section should be present"
1693        );
1694    }
1695
1696    // === Task Completion Verification Tests ===
1697
1698    #[test]
1699    fn test_task_closure_verification_in_done_section() {
1700        // When memories/tasks mode is enabled, the DONE section should include
1701        // task verification requirements
1702        let config = RalphConfig::default();
1703        let registry = HatRegistry::new();
1704        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1705            .with_memories_enabled(true);
1706
1707        let prompt = ralph.build_prompt("", &[]);
1708
1709        // The tasks CLI instructions are now injected via the skills pipeline,
1710        // but the DONE section still requires task verification before completion
1711        assert!(
1712            prompt.contains("ralph tools task ready"),
1713            "Should reference task ready command in DONE section"
1714        );
1715        assert!(
1716            prompt.contains("MUST NOT output LOOP_COMPLETE while tasks remain open"),
1717            "Should require tasks closed before completion"
1718        );
1719    }
1720
1721    #[test]
1722    fn test_workflow_verify_and_commit_step() {
1723        // Solo mode with memories should have VERIFY & COMMIT step
1724        let config = RalphConfig::default();
1725        let registry = HatRegistry::new();
1726        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None)
1727            .with_memories_enabled(true);
1728
1729        let prompt = ralph.build_prompt("", &[]);
1730
1731        // Should have VERIFY & COMMIT step
1732        assert!(
1733            prompt.contains("### 4. VERIFY & COMMIT"),
1734            "Should have VERIFY & COMMIT step in workflow"
1735        );
1736        assert!(
1737            prompt.contains("run tests and verify"),
1738            "Should require verification"
1739        );
1740        assert!(
1741            prompt.contains("ralph tools task close"),
1742            "Should reference task close command"
1743        );
1744    }
1745
1746    #[test]
1747    fn test_scratchpad_mode_still_has_commit_step() {
1748        // Scratchpad-only mode (no memories) should have COMMIT step
1749        let config = RalphConfig::default();
1750        let registry = HatRegistry::new();
1751        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1752        // memories_enabled defaults to false
1753
1754        let prompt = ralph.build_prompt("", &[]);
1755
1756        // Scratchpad mode uses different format - COMMIT step without task CLI
1757        assert!(
1758            prompt.contains("### 4. COMMIT"),
1759            "Should have COMMIT step in workflow"
1760        );
1761        assert!(
1762            prompt.contains("mark the task `[x]`"),
1763            "Should mark task in scratchpad"
1764        );
1765        // Scratchpad mode doesn't have the TASKS section
1766        assert!(
1767            !prompt.contains("### 0c. TASKS"),
1768            "Scratchpad mode should not have TASKS section"
1769        );
1770    }
1771
1772    // === Objective Section Tests ===
1773
1774    #[test]
1775    fn test_objective_section_present_with_set_objective() {
1776        // When objective is set via set_objective(), OBJECTIVE section should appear
1777        let config = RalphConfig::default();
1778        let registry = HatRegistry::new();
1779        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1780        ralph.set_objective("Implement user authentication with JWT tokens".to_string());
1781
1782        let prompt = ralph.build_prompt("", &[]);
1783
1784        assert!(
1785            prompt.contains("## OBJECTIVE"),
1786            "Should have OBJECTIVE section when objective is set"
1787        );
1788        assert!(
1789            prompt.contains("Implement user authentication with JWT tokens"),
1790            "OBJECTIVE should contain the original user prompt"
1791        );
1792        assert!(
1793            prompt.contains("This is your primary goal"),
1794            "OBJECTIVE should emphasize this is the primary goal"
1795        );
1796    }
1797
1798    #[test]
1799    fn test_objective_reinforced_in_done_section() {
1800        // The objective should be restated in the DONE section (bookend pattern)
1801        // when Ralph is coordinating (no active hats)
1802        let config = RalphConfig::default();
1803        let registry = HatRegistry::new();
1804        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1805        ralph.set_objective("Fix the login bug in auth module".to_string());
1806
1807        let prompt = ralph.build_prompt("", &[]);
1808
1809        // Check DONE section contains objective reinforcement
1810        let done_pos = prompt.find("## DONE").expect("Should have DONE section");
1811        let after_done = &prompt[done_pos..];
1812
1813        assert!(
1814            after_done.contains("Remember your objective"),
1815            "DONE section should remind about objective"
1816        );
1817        assert!(
1818            after_done.contains("Fix the login bug in auth module"),
1819            "DONE section should restate the objective"
1820        );
1821    }
1822
1823    #[test]
1824    fn test_objective_appears_before_pending_events() {
1825        // OBJECTIVE should appear BEFORE PENDING EVENTS for prominence
1826        let config = RalphConfig::default();
1827        let registry = HatRegistry::new();
1828        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1829        ralph.set_objective("Build feature X".to_string());
1830
1831        let context = "Event: task.start - Build feature X";
1832        let prompt = ralph.build_prompt(context, &[]);
1833
1834        let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
1835        let events_pos = prompt
1836            .find("## PENDING EVENTS")
1837            .expect("Should have PENDING EVENTS");
1838
1839        assert!(
1840            objective_pos < events_pos,
1841            "OBJECTIVE ({}) should appear before PENDING EVENTS ({})",
1842            objective_pos,
1843            events_pos
1844        );
1845    }
1846
1847    #[test]
1848    fn test_no_objective_when_not_set() {
1849        // When no objective has been set, no OBJECTIVE section should appear
1850        let config = RalphConfig::default();
1851        let registry = HatRegistry::new();
1852        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1853
1854        let context = "Event: build.done - Build completed successfully";
1855        let prompt = ralph.build_prompt(context, &[]);
1856
1857        assert!(
1858            !prompt.contains("## OBJECTIVE"),
1859            "Should NOT have OBJECTIVE section when objective not set"
1860        );
1861    }
1862
1863    #[test]
1864    fn test_objective_set_correctly() {
1865        // Test that set_objective stores the objective and it appears in prompt
1866        let config = RalphConfig::default();
1867        let registry = HatRegistry::new();
1868        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1869        ralph.set_objective("Review this PR for security issues".to_string());
1870
1871        let prompt = ralph.build_prompt("", &[]);
1872
1873        assert!(
1874            prompt.contains("Review this PR for security issues"),
1875            "Should show the stored objective"
1876        );
1877    }
1878
1879    #[test]
1880    fn test_objective_with_events_context() {
1881        // Objective should appear even when context has other events (not task.start)
1882        let config = RalphConfig::default();
1883        let registry = HatRegistry::new();
1884        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1885        ralph.set_objective("Implement feature Y".to_string());
1886
1887        let context =
1888            "Event: build.done - Previous build succeeded\nEvent: test.passed - All tests green";
1889        let prompt = ralph.build_prompt(context, &[]);
1890
1891        assert!(
1892            prompt.contains("## OBJECTIVE"),
1893            "Should have OBJECTIVE section"
1894        );
1895        assert!(
1896            prompt.contains("Implement feature Y"),
1897            "OBJECTIVE should contain the stored objective"
1898        );
1899    }
1900
1901    #[test]
1902    fn test_done_section_without_objective() {
1903        // When no objective, DONE section should still work but without reinforcement
1904        let config = RalphConfig::default();
1905        let registry = HatRegistry::new();
1906        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1907
1908        let prompt = ralph.build_prompt("", &[]);
1909
1910        assert!(prompt.contains("## DONE"), "Should have DONE section");
1911        assert!(
1912            prompt.contains("LOOP_COMPLETE"),
1913            "DONE should mention completion promise"
1914        );
1915        assert!(
1916            !prompt.contains("Remember your objective"),
1917            "Should NOT have objective reinforcement without objective"
1918        );
1919    }
1920
1921    #[test]
1922    fn test_objective_persists_across_iterations() {
1923        // Objective is present in prompt even when context has no task.start event
1924        // (simulating iteration 2+ where the start event has been consumed)
1925        let config = RalphConfig::default();
1926        let registry = HatRegistry::new();
1927        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1928        ralph.set_objective("Build a REST API with authentication".to_string());
1929
1930        // Simulate iteration 2: only non-start events present
1931        let context = "Event: build.done - Build completed";
1932        let prompt = ralph.build_prompt(context, &[]);
1933
1934        assert!(
1935            prompt.contains("## OBJECTIVE"),
1936            "OBJECTIVE should persist even without task.start in context"
1937        );
1938        assert!(
1939            prompt.contains("Build a REST API with authentication"),
1940            "Stored objective should appear in later iterations"
1941        );
1942    }
1943
1944    #[test]
1945    fn test_done_section_suppressed_when_hat_active() {
1946        // When active_hats is non-empty, prompt does NOT contain ## DONE
1947        let yaml = r#"
1948hats:
1949  builder:
1950    name: "Builder"
1951    triggers: ["build.task"]
1952    publishes: ["build.done"]
1953    instructions: "Build the code."
1954"#;
1955        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1956        let registry = HatRegistry::from_config(&config);
1957        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1958        ralph.set_objective("Implement feature X".to_string());
1959
1960        let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
1961        let prompt = ralph.build_prompt("Event: build.task - Do the build", &[builder]);
1962
1963        assert!(
1964            !prompt.contains("## DONE"),
1965            "DONE section should be suppressed when a hat is active"
1966        );
1967        assert!(
1968            !prompt.contains("LOOP_COMPLETE"),
1969            "Completion promise should NOT appear when a hat is active"
1970        );
1971        // But objective should still be visible
1972        assert!(
1973            prompt.contains("## OBJECTIVE"),
1974            "OBJECTIVE should still appear even when hat is active"
1975        );
1976        assert!(
1977            prompt.contains("Implement feature X"),
1978            "Objective content should be visible to active hat"
1979        );
1980    }
1981
1982    #[test]
1983    fn test_done_section_present_when_coordinating() {
1984        // When active_hats is empty, prompt contains ## DONE with objective reinforcement
1985        let yaml = r#"
1986hats:
1987  builder:
1988    name: "Builder"
1989    triggers: ["build.task"]
1990    publishes: ["build.done"]
1991"#;
1992        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
1993        let registry = HatRegistry::from_config(&config);
1994        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
1995        ralph.set_objective("Complete the TDD cycle".to_string());
1996
1997        // No active hats - Ralph is coordinating
1998        let prompt = ralph.build_prompt("Event: build.done - Build finished", &[]);
1999
2000        assert!(
2001            prompt.contains("## DONE"),
2002            "DONE section should appear when Ralph is coordinating"
2003        );
2004        assert!(
2005            prompt.contains("LOOP_COMPLETE"),
2006            "Completion promise should appear when coordinating"
2007        );
2008    }
2009
2010    #[test]
2011    fn test_objective_in_done_section_when_coordinating() {
2012        // DONE section includes "Remember your objective" when Ralph is coordinating
2013        let config = RalphConfig::default();
2014        let registry = HatRegistry::new();
2015        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2016        ralph.set_objective("Deploy the application".to_string());
2017
2018        let prompt = ralph.build_prompt("", &[]);
2019
2020        let done_pos = prompt.find("## DONE").expect("Should have DONE section");
2021        let after_done = &prompt[done_pos..];
2022
2023        assert!(
2024            after_done.contains("Remember your objective"),
2025            "DONE section should remind about objective when coordinating"
2026        );
2027        assert!(
2028            after_done.contains("Deploy the application"),
2029            "DONE section should contain the objective text"
2030        );
2031    }
2032
2033    // === Event Publishing Guide Tests ===
2034
2035    #[test]
2036    fn test_event_publishing_guide_with_receivers() {
2037        // When a hat publishes events and other hats receive them,
2038        // the guide should show who receives each event
2039        let yaml = r#"
2040hats:
2041  builder:
2042    name: "Builder"
2043    description: "Builds and tests code"
2044    triggers: ["build.task"]
2045    publishes: ["build.done", "build.blocked"]
2046  confessor:
2047    name: "Confessor"
2048    description: "Produces a ConfessionReport; rewarded for honesty"
2049    triggers: ["build.done"]
2050    publishes: ["confession.done"]
2051"#;
2052        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2053        let registry = HatRegistry::from_config(&config);
2054        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2055
2056        // Get the builder hat as active
2057        let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2058        let prompt = ralph.build_prompt("[build.task] Build the feature", &[builder]);
2059
2060        // Should include Event Publishing Guide
2061        assert!(
2062            prompt.contains("### Event Publishing Guide"),
2063            "Should include Event Publishing Guide section"
2064        );
2065        assert!(
2066            prompt.contains("When you publish:"),
2067            "Guide should explain what happens when publishing"
2068        );
2069        // build.done has a receiver (Confessor)
2070        assert!(
2071            prompt.contains("`build.done` → Received by: Confessor"),
2072            "Should show Confessor receives build.done"
2073        );
2074        assert!(
2075            prompt.contains("Produces a ConfessionReport; rewarded for honesty"),
2076            "Should include receiver's description"
2077        );
2078        // build.blocked has no receiver, so falls back to Ralph
2079        assert!(
2080            prompt.contains("`build.blocked` → Received by: Ralph (coordinates next steps)"),
2081            "Should show Ralph receives orphan events"
2082        );
2083    }
2084
2085    #[test]
2086    fn test_event_publishing_guide_no_publishes() {
2087        // When a hat doesn't publish any events, no guide should appear
2088        let yaml = r#"
2089hats:
2090  observer:
2091    name: "Observer"
2092    description: "Only observes"
2093    triggers: ["events.*"]
2094"#;
2095        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2096        let registry = HatRegistry::from_config(&config);
2097        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2098
2099        let observer = registry.get(&ralph_proto::HatId::new("observer")).unwrap();
2100        let prompt = ralph.build_prompt("[events.start] Start", &[observer]);
2101
2102        // Should NOT include Event Publishing Guide
2103        assert!(
2104            !prompt.contains("### Event Publishing Guide"),
2105            "Should NOT include Event Publishing Guide when hat has no publishes"
2106        );
2107    }
2108
2109    #[test]
2110    fn test_event_publishing_guide_all_orphan_events() {
2111        // When all published events have no receivers, all should show Ralph
2112        let yaml = r#"
2113hats:
2114  solo:
2115    name: "Solo"
2116    triggers: ["solo.start"]
2117    publishes: ["solo.done", "solo.failed"]
2118"#;
2119        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2120        let registry = HatRegistry::from_config(&config);
2121        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2122
2123        let solo = registry.get(&ralph_proto::HatId::new("solo")).unwrap();
2124        let prompt = ralph.build_prompt("[solo.start] Go", &[solo]);
2125
2126        assert!(
2127            prompt.contains("### Event Publishing Guide"),
2128            "Should include guide even for orphan events"
2129        );
2130        assert!(
2131            prompt.contains("`solo.done` → Received by: Ralph (coordinates next steps)"),
2132            "Orphan solo.done should go to Ralph"
2133        );
2134        assert!(
2135            prompt.contains("`solo.failed` → Received by: Ralph (coordinates next steps)"),
2136            "Orphan solo.failed should go to Ralph"
2137        );
2138    }
2139
2140    #[test]
2141    fn test_event_publishing_guide_multiple_receivers() {
2142        // When an event has multiple receivers, all should be listed
2143        let yaml = r#"
2144hats:
2145  broadcaster:
2146    name: "Broadcaster"
2147    triggers: ["broadcast.start"]
2148    publishes: ["signal.sent"]
2149  listener1:
2150    name: "Listener1"
2151    description: "First listener"
2152    triggers: ["signal.sent"]
2153  listener2:
2154    name: "Listener2"
2155    description: "Second listener"
2156    triggers: ["signal.sent"]
2157"#;
2158        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2159        let registry = HatRegistry::from_config(&config);
2160        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2161
2162        let broadcaster = registry
2163            .get(&ralph_proto::HatId::new("broadcaster"))
2164            .unwrap();
2165        let prompt = ralph.build_prompt("[broadcast.start] Go", &[broadcaster]);
2166
2167        assert!(
2168            prompt.contains("### Event Publishing Guide"),
2169            "Should include guide"
2170        );
2171        // Both listeners should be mentioned (order may vary due to HashMap iteration)
2172        assert!(
2173            prompt.contains("Listener1 (First listener)"),
2174            "Should list Listener1 as receiver"
2175        );
2176        assert!(
2177            prompt.contains("Listener2 (Second listener)"),
2178            "Should list Listener2 as receiver"
2179        );
2180    }
2181
2182    #[test]
2183    fn test_event_publishing_guide_excludes_self() {
2184        // If a hat subscribes to its own event, it should NOT be listed as receiver
2185        let yaml = r#"
2186hats:
2187  looper:
2188    name: "Looper"
2189    triggers: ["loop.continue", "loop.start"]
2190    publishes: ["loop.continue"]
2191"#;
2192        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2193        let registry = HatRegistry::from_config(&config);
2194        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2195
2196        let looper = registry.get(&ralph_proto::HatId::new("looper")).unwrap();
2197        let prompt = ralph.build_prompt("[loop.start] Start", &[looper]);
2198
2199        assert!(
2200            prompt.contains("### Event Publishing Guide"),
2201            "Should include guide"
2202        );
2203        // Self-reference should be excluded, so should fall back to Ralph
2204        assert!(
2205            prompt.contains("`loop.continue` → Received by: Ralph (coordinates next steps)"),
2206            "Self-subscription should be excluded, falling back to Ralph"
2207        );
2208    }
2209
2210    #[test]
2211    fn test_event_publishing_guide_receiver_without_description() {
2212        // When a receiver has no description, just show the name
2213        let yaml = r#"
2214hats:
2215  sender:
2216    name: "Sender"
2217    triggers: ["send.start"]
2218    publishes: ["message.sent"]
2219  receiver:
2220    name: "NoDescReceiver"
2221    triggers: ["message.sent"]
2222"#;
2223        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2224        let registry = HatRegistry::from_config(&config);
2225        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2226
2227        let sender = registry.get(&ralph_proto::HatId::new("sender")).unwrap();
2228        let prompt = ralph.build_prompt("[send.start] Go", &[sender]);
2229
2230        assert!(
2231            prompt.contains("`message.sent` → Received by: NoDescReceiver"),
2232            "Should show receiver name without parentheses when no description"
2233        );
2234        // Should NOT have empty parentheses
2235        assert!(
2236            !prompt.contains("NoDescReceiver ()"),
2237            "Should NOT have empty parentheses for receiver without description"
2238        );
2239    }
2240
2241    // === Event Publishing Constraint Tests ===
2242
2243    #[test]
2244    fn test_constraint_lists_valid_events_when_coordinating() {
2245        // When Ralph is coordinating (no active hats), the prompt should include
2246        // a CONSTRAINT listing valid events to publish
2247        let yaml = r#"
2248hats:
2249  test_writer:
2250    name: "Test Writer"
2251    triggers: ["tdd.start"]
2252    publishes: ["test.written"]
2253  implementer:
2254    name: "Implementer"
2255    triggers: ["test.written"]
2256    publishes: ["test.passing"]
2257"#;
2258        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2259        let registry = HatRegistry::from_config(&config);
2260        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2261
2262        // No active hats - Ralph is coordinating
2263        let prompt = ralph.build_prompt("[task.start] Do TDD for feature X", &[]);
2264
2265        // Should contain CONSTRAINT with valid events
2266        assert!(
2267            prompt.contains("**CONSTRAINT:**"),
2268            "Prompt should include CONSTRAINT when coordinating"
2269        );
2270        assert!(
2271            prompt.contains("tdd.start"),
2272            "CONSTRAINT should list tdd.start as valid event"
2273        );
2274        assert!(
2275            prompt.contains("test.written"),
2276            "CONSTRAINT should list test.written as valid event"
2277        );
2278        assert!(
2279            prompt.contains("Publishing other events will have no effect"),
2280            "CONSTRAINT should warn about invalid events"
2281        );
2282    }
2283
2284    #[test]
2285    fn test_no_constraint_when_hat_is_active() {
2286        // When a hat is active, the CONSTRAINT should NOT appear
2287        // (the active hat has its own Event Publishing Guide)
2288        let yaml = r#"
2289hats:
2290  builder:
2291    name: "Builder"
2292    triggers: ["build.task"]
2293    publishes: ["build.done"]
2294    instructions: "Build the code."
2295"#;
2296        let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
2297        let registry = HatRegistry::from_config(&config);
2298        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2299
2300        // Builder hat is active
2301        let builder = registry.get(&ralph_proto::HatId::new("builder")).unwrap();
2302        let prompt = ralph.build_prompt("[build.task] Build feature X", &[builder]);
2303
2304        // Should NOT contain the coordinating CONSTRAINT
2305        assert!(
2306            !prompt.contains("**CONSTRAINT:** You MUST only publish events from this list"),
2307            "Active hat should NOT have coordinating CONSTRAINT"
2308        );
2309
2310        // Should have Event Publishing Guide instead
2311        assert!(
2312            prompt.contains("### Event Publishing Guide"),
2313            "Active hat should have Event Publishing Guide"
2314        );
2315    }
2316
2317    #[test]
2318    fn test_no_constraint_when_no_hats() {
2319        // When there are no hats (solo mode), no CONSTRAINT should appear
2320        let config = RalphConfig::default();
2321        let registry = HatRegistry::new(); // Empty registry
2322        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2323
2324        let prompt = ralph.build_prompt("[task.start] Do something", &[]);
2325
2326        // Should NOT contain CONSTRAINT (no hats to coordinate)
2327        assert!(
2328            !prompt.contains("**CONSTRAINT:**"),
2329            "Solo mode should NOT have CONSTRAINT"
2330        );
2331    }
2332
2333    // === Human Guidance Injection Tests ===
2334
2335    #[test]
2336    fn test_single_guidance_injection() {
2337        // Single human.guidance message should be injected as-is (no numbered list)
2338        let config = RalphConfig::default();
2339        let registry = HatRegistry::new();
2340        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2341        ralph.set_robot_guidance(vec!["Focus on error handling first".to_string()]);
2342
2343        let prompt = ralph.build_prompt("", &[]);
2344
2345        assert!(
2346            prompt.contains("## ROBOT GUIDANCE"),
2347            "Should include ROBOT GUIDANCE section"
2348        );
2349        assert!(
2350            prompt.contains("Focus on error handling first"),
2351            "Should contain the guidance message"
2352        );
2353        // Single message should NOT be numbered
2354        assert!(
2355            !prompt.contains("1. Focus on error handling first"),
2356            "Single guidance should not be numbered"
2357        );
2358    }
2359
2360    #[test]
2361    fn test_multiple_guidance_squashing() {
2362        // Multiple human.guidance messages should be squashed into a numbered list
2363        let config = RalphConfig::default();
2364        let registry = HatRegistry::new();
2365        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2366        ralph.set_robot_guidance(vec![
2367            "Focus on error handling".to_string(),
2368            "Use the existing retry pattern".to_string(),
2369            "Check edge cases for empty input".to_string(),
2370        ]);
2371
2372        let prompt = ralph.build_prompt("", &[]);
2373
2374        assert!(
2375            prompt.contains("## ROBOT GUIDANCE"),
2376            "Should include ROBOT GUIDANCE section"
2377        );
2378        assert!(
2379            prompt.contains("1. Focus on error handling"),
2380            "First guidance should be numbered 1"
2381        );
2382        assert!(
2383            prompt.contains("2. Use the existing retry pattern"),
2384            "Second guidance should be numbered 2"
2385        );
2386        assert!(
2387            prompt.contains("3. Check edge cases for empty input"),
2388            "Third guidance should be numbered 3"
2389        );
2390    }
2391
2392    #[test]
2393    fn test_guidance_appears_in_prompt_before_events() {
2394        // ROBOT GUIDANCE should appear after OBJECTIVE but before PENDING EVENTS
2395        let config = RalphConfig::default();
2396        let registry = HatRegistry::new();
2397        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2398        ralph.set_objective("Build feature X".to_string());
2399        ralph.set_robot_guidance(vec!["Use the new API".to_string()]);
2400
2401        let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2402
2403        let objective_pos = prompt.find("## OBJECTIVE").expect("Should have OBJECTIVE");
2404        let guidance_pos = prompt
2405            .find("## ROBOT GUIDANCE")
2406            .expect("Should have ROBOT GUIDANCE");
2407        let events_pos = prompt
2408            .find("## PENDING EVENTS")
2409            .expect("Should have PENDING EVENTS");
2410
2411        assert!(
2412            objective_pos < guidance_pos,
2413            "OBJECTIVE ({}) should come before ROBOT GUIDANCE ({})",
2414            objective_pos,
2415            guidance_pos
2416        );
2417        assert!(
2418            guidance_pos < events_pos,
2419            "ROBOT GUIDANCE ({}) should come before PENDING EVENTS ({})",
2420            guidance_pos,
2421            events_pos
2422        );
2423    }
2424
2425    #[test]
2426    fn test_guidance_cleared_after_injection() {
2427        // After build_prompt consumes guidance, clear_robot_guidance should leave it empty
2428        let config = RalphConfig::default();
2429        let registry = HatRegistry::new();
2430        let mut ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2431        ralph.set_robot_guidance(vec!["First guidance".to_string()]);
2432
2433        // First prompt should include guidance
2434        let prompt1 = ralph.build_prompt("", &[]);
2435        assert!(
2436            prompt1.contains("## ROBOT GUIDANCE"),
2437            "First prompt should have guidance"
2438        );
2439
2440        // Clear guidance (as EventLoop would)
2441        ralph.clear_robot_guidance();
2442
2443        // Second prompt should NOT include guidance
2444        let prompt2 = ralph.build_prompt("", &[]);
2445        assert!(
2446            !prompt2.contains("## ROBOT GUIDANCE"),
2447            "After clearing, prompt should not have guidance"
2448        );
2449    }
2450
2451    #[test]
2452    fn test_no_injection_when_no_guidance() {
2453        // When no guidance events, prompt should not have ROBOT GUIDANCE section
2454        let config = RalphConfig::default();
2455        let registry = HatRegistry::new();
2456        let ralph = HatlessRalph::new("LOOP_COMPLETE", config.core.clone(), &registry, None);
2457
2458        let prompt = ralph.build_prompt("Event: build.task - Do the work", &[]);
2459
2460        assert!(
2461            !prompt.contains("## ROBOT GUIDANCE"),
2462            "Should NOT include ROBOT GUIDANCE when no guidance set"
2463        );
2464    }
2465}