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