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