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