diff --git a/.claude/agents/alignment-expert.md b/.claude/agents/alignment-expert.md index 603fcf6..d67dc83 100644 --- a/.claude/agents/alignment-expert.md +++ b/.claude/agents/alignment-expert.md @@ -1,7 +1,7 @@ --- name: alignment-expert description: Expert agent for alignment dialogues. Produces focused perspectives with inline markers. Use when orchestrating multi-expert alignment dialogues via blue_dialogue_create. -tools: Read, Grep, Glob +tools: Read, Grep, Glob, Write model: sonnet --- diff --git a/crates/blue-mcp/src/handlers/dialogue.rs b/crates/blue-mcp/src/handlers/dialogue.rs index 0c793f9..efe5830 100644 --- a/crates/blue-mcp/src/handlers/dialogue.rs +++ b/crates/blue-mcp/src/handlers/dialogue.rs @@ -346,7 +346,8 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result Result Result Value { let agent_list: Vec = agents .iter() @@ -909,6 +918,7 @@ pub fn build_judge_protocol( "emoji": a.emoji, "tier": a.tier, "relevance": a.relevance, + "name_lowercase": a.name.to_lowercase(), }) }) .collect(); @@ -953,7 +963,14 @@ OUTPUT LIMIT — THIS IS MANDATORY: - If the topic needs more depth, save it for the next round - Aim for under 2000 characters total - DO NOT write essays, literature reviews, or exhaustive analyses -- Be pointed and specific, not comprehensive{source_read_instructions}"## +- Be pointed and specific, not comprehensive + +WRITE YOUR OUTPUT — THIS IS MANDATORY: +Use the Write tool to write your COMPLETE response to: + {{{{OUTPUT_FILE}}}} + +Write your full perspective to this file. This is your primary output mechanism. +After writing the file, you may stop.{source_read_instructions}"## ); let instructions = format!( @@ -961,6 +978,9 @@ OUTPUT LIMIT — THIS IS MANDATORY: === HOW TO SPAWN EXPERT SUBAGENTS === +BEFORE spawning each round, create the round directory: + Use Bash: mkdir -p {output_dir}/round-N + Spawn ALL {agent_count} experts in a SINGLE message with {agent_count} Task tool calls. Multiple Task calls in one message run as parallel subagents. @@ -968,28 +988,32 @@ Each Task call: - subagent_type: "alignment-expert" - description: "🧁 Muffin expert deliberation" - max_turns: 10 -- prompt: the AGENT PROMPT TEMPLATE with {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}} substituted +- prompt: the AGENT PROMPT TEMPLATE with {{{{NAME}}}}, {{{{EMOJI}}}}, {{{{ROLE}}}}, {{{{OUTPUT_FILE}}}} substituted + - {{{{OUTPUT_FILE}}}} → {output_dir}/round-N/AGENT_NAME_LOWERCASE.md - run_in_background: true -All {agent_count} results return when complete. Read each agent's output from the results. +All {agent_count} results return when complete. === ROUND WORKFLOW === -1. SPAWN: One message, {agent_count} Task calls (parallel subagents) -2. READ: Each subagent returns its output directly -3. SCORE: ALIGNMENT = Wisdom + Consistency + Truth + Relationships (UNBOUNDED) +1. MKDIR: Create round directory via Bash: mkdir -p {output_dir}/round-N +2. SPAWN: One message, {agent_count} Task calls (parallel subagents) +3. READ: After all agents complete, read each file with Read tool + If a file is missing, fall back to blue_extract_dialogue(task_id=TASK_ID) +4. SCORE: ALIGNMENT = Wisdom + Consistency + Truth + Relationships (UNBOUNDED) - Score ONLY AFTER reading output — NEVER pre-fill scores -4. UPDATE {dialogue_file}: +5. UPDATE {dialogue_file}: - Agent responses under the correct Round section - Scoreboard with scores from this round - Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker) - Tensions Tracker (one row per [TENSION Tn:] marker) -5. CONVERGE: If velocity approaches 0 OR all tensions resolved → declare convergence +6. CONVERGE: If velocity approaches 0 OR all tensions resolved → declare convergence Otherwise, start next round with updated prompt including prior perspectives Maximum 5 rounds (safety valve) -6. SAVE via blue_dialogue_save +7. SAVE via blue_dialogue_save AGENTS: {agent_names} +OUTPUT DIR: {output_dir} FORMAT RULES — MANDATORY: - ALWAYS prefix agent names with their emoji (🧁 Muffin) not bare name (Muffin) @@ -1001,6 +1025,7 @@ FORMAT RULES — MANDATORY: IMPORTANT: Each agent has NO memory of other agents. They see only the topic and their role."##, agent_count = agents.len(), dialogue_file = dialogue_file, + output_dir = output_dir, agent_names = agents .iter() .map(|a| format!("{} {} ({})", a.emoji, a.name, a.role)) @@ -1015,6 +1040,7 @@ IMPORTANT: Each agent has NO memory of other agents. They see only the topic and "dialogue_file": dialogue_file, "model": model, "sources": sources, + "output_dir": output_dir, "convergence": { "max_rounds": 5, "velocity_threshold": 0.1, @@ -1152,6 +1178,7 @@ mod tests { "/tmp/test.dialogue.md", "sonnet", &["/tmp/source.rs".to_string()], + "/tmp/blue-dialogue/system-design", ); // Must have instructions @@ -1161,6 +1188,10 @@ mod tests { assert!(instructions.contains("ALIGNMENT")); assert!(instructions.contains("Wisdom")); assert!(instructions.contains("convergence")); + // RFC 0029: file-based output instructions + assert!(instructions.contains("/tmp/blue-dialogue/system-design")); + assert!(instructions.contains("mkdir")); + assert!(instructions.contains("Read tool")); // Must have agent prompt template with Read tool reference let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap(); @@ -1169,11 +1200,15 @@ mod tests { assert!(template.contains("PERSPECTIVE")); assert!(template.contains("TENSION")); assert!(template.contains("Read tool")); + // RFC 0029: WRITE YOUR OUTPUT section + assert!(template.contains("WRITE YOUR OUTPUT")); + assert!(template.contains("{{OUTPUT_FILE}}")); - // Must have agents array + // Must have agents array with name_lowercase let agents_arr = protocol.get("agents").unwrap().as_array().unwrap(); assert_eq!(agents_arr.len(), 3); assert_eq!(agents_arr[0]["name"], "Muffin"); + assert_eq!(agents_arr[0]["name_lowercase"], "muffin"); // Must have model assert_eq!(protocol["model"], "sonnet"); @@ -1183,6 +1218,9 @@ mod tests { assert_eq!(sources.len(), 1); assert_eq!(sources[0], "/tmp/source.rs"); + // Must have output_dir + assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/system-design"); + // Must have convergence params assert_eq!(protocol["convergence"]["max_rounds"], 5); assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap()); @@ -1196,10 +1234,44 @@ mod tests { "/tmp/test.dialogue.md", "haiku", &[], + "/tmp/blue-dialogue/quick-topic", ); // Template should NOT contain grounding instructions when no sources let template = protocol.get("agent_prompt_template").unwrap().as_str().unwrap(); assert!(!template.contains("GROUNDING")); } + + #[test] + fn test_build_judge_protocol_output_paths() { + let agents = assign_pastry_agents(4, "api design"); + let protocol = build_judge_protocol( + &agents, + "/tmp/test.dialogue.md", + "sonnet", + &[], + "/tmp/blue-dialogue/api-design", + ); + + // output_dir in JSON + assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/api-design"); + + // All agents have name_lowercase + let agents_arr = protocol["agents"].as_array().unwrap(); + assert_eq!(agents_arr[0]["name_lowercase"], "muffin"); + assert_eq!(agents_arr[1]["name_lowercase"], "cupcake"); + assert_eq!(agents_arr[2]["name_lowercase"], "scone"); + assert_eq!(agents_arr[3]["name_lowercase"], "eclair"); + + // WRITE YOUR OUTPUT in template + let template = protocol["agent_prompt_template"].as_str().unwrap(); + assert!(template.contains("WRITE YOUR OUTPUT")); + assert!(template.contains("{{OUTPUT_FILE}}")); + assert!(template.contains("Write tool")); + + // output_dir referenced in instructions + let instructions = protocol["instructions"].as_str().unwrap(); + assert!(instructions.contains("/tmp/blue-dialogue/api-design")); + assert!(instructions.contains("OUTPUT DIR:")); + } }