//! Company-style workflow orchestrator: runs the complete division pipeline //! (Strategy → Engineering → [Quality || Security || Documentation] in parallel) //! with findings flowing between stages, then returns a consolidated executive //! summary to the CEO (main agent). //! //! Flow: //! ``` //! CEO Main Agent //! │ delegates to run_company_pipeline(request) //! ▼ //! ┌──────────────────────────────────────────────────┐ //! │ Strategy Division — plan + mermaid diagrams │ (runs sequentially first) //! └─────────────────────────┬────────────────────────┘ //! ▼ //! ┌──────────────────────────────────────────────────┐ //! │ Engineering Division — implement per plan │ (runs sequentially second) //! └─────────────────────────┬────────────────────────┘ //! ▼ //! ┌────────────┼────────────┐ //! ▼ ▼ ▼ //! ┌───────────┐┌───────────┐┌───────────┐ //! │ Quality ││ Security ││ Docs │ (run concurrently in parallel) //! └───────────┘└───────────┘└───────────┘ //! │ │ │ //! └────────────┼────────────┘ //! ▼ //! CEO Main Agent delivers consolidated summary to user //! ``` use std::collections::HashMap; use std::fmt::Write; use std::sync::{Arc, Mutex, atomic::AtomicBool}; use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript}; use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus}; use crate::app::subagent::division; /// Construct the specialized agents for a division. /// /// Flow: map division name to its specialization pool. /// /// Return: a `Vec` containing the specialist agents. fn make_division_specialists( div: &division::Division, user_request: &str, specs: &[(String, String)], ) -> Vec { let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or(""); specs .iter() .map(|(label, focus)| { // Prepend [Division Name: Specialist Label] so the first 40 chars // of the prompt become the agent_name in spawn_single_agent. // We use quadruple curly braces `{{{{findings}}}}` so that Rust's `format!` formats it // into `{{findings}}` in the output string, which `resolve_template` then recognizes // and replaces. let prompt = format!( "[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions:\n{{{{findings}}}}", div.name, label, focus, div_prompt, user_request, ); ScriptPrimitive::Agent(prompt) }) .collect() } /// Construct a named Phase wrapper containing a Parallel block of division specialists. /// /// Flow: construct division specialists → wrap in a `Parallel` primitive wrapper. /// /// Return: a `ScriptPrimitive::Phase` wrapper. fn make_division_phase( div: &division::Division, user_request: &str, specs: &[(String, String)], ) -> ScriptPrimitive { let specialists = make_division_specialists(div, user_request, specs); ScriptPrimitive::Phase { name: div.name.to_string(), script: Box::new(ScriptPrimitive::Parallel(specialists)), } } /// Run the full company-style pipeline for a given user request. /// /// This orchestrates all five divisions, running Strategy and Engineering /// sequentially, followed by Quality, Security, and Documentation in parallel. /// /// Each division receives findings from all previous divisions, enabling /// context to flow through the pipeline. /// /// Returns a consolidated executive summary string. #[allow(clippy::ref_option)] pub fn run_company_pipeline( user_request: &str, session_dir: &std::path::Path, workspaces: &[std::path::PathBuf], turn_events: Option<&Arc>>>, abort_flag: &Option>, custom_specialists: &HashMap>, ) -> anyhow::Result { let divisions = division::all_divisions(); let strategy_specs = custom_specialists.get("Strategy") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?; let engineering_specs = custom_specialists.get("Engineering") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?; let quality_specs = custom_specialists.get("Quality") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?; let security_specs = custom_specialists.get("Security") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Security"))?; let documentation_specs = custom_specialists.get("Documentation") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Documentation"))?; let strategy_phase = make_division_phase(&divisions[0], user_request, strategy_specs); let engineering_phase = make_division_phase(&divisions[1], user_request, engineering_specs); let quality_phase = make_division_phase(&divisions[2], user_request, quality_specs); let security_phase = make_division_phase(&divisions[3], user_request, security_specs); let documentation_phase = make_division_phase(&divisions[4], user_request, documentation_specs); let parallel_divisions = ScriptPrimitive::Parallel(vec![ quality_phase, security_phase, documentation_phase, ]); let pipeline_primitive = ScriptPrimitive::Pipeline(vec![ strategy_phase, engineering_phase, parallel_divisions, ]); let wf = WorkflowScript { name: "company-pipeline".to_string(), description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(), script: pipeline_primitive, options: ScriptOptions { max_concurrency: 10, continue_on_error: true, timeout_ms: None, }, }; let live: Option = turn_events.map(|events| { let events = events.clone(); let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| { let display_name = agent_name.chars().take(30).collect::(); if let Ok(mut q) = events.lock() { q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate { agent_id: display_name.clone(), agent_name: display_name, status, }); } }); f }); let args: HashMap = HashMap::new(); let findings: Arc>> = Arc::new(Mutex::new(Vec::new())); let results = execute_primitive( &wf.script, &args, wf.options.max_concurrency, true, abort_flag, live.as_ref(), session_dir, workspaces, &findings, None, )?; let all_findings = findings.lock() .map(|f| f.clone()) .unwrap_or_default(); Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, custom_specialists)) } /// Run a quick company pipeline that skips non-essential divisions /// for simple tasks. Flow: Strategy → Engineering → Quality. /// /// This is for smaller tasks where security audit and full docs are overkill. #[allow(clippy::ref_option)] pub fn run_company_pipeline_quick( user_request: &str, session_dir: &std::path::Path, workspaces: &[std::path::PathBuf], turn_events: Option<&Arc>>>, abort_flag: &Option>, custom_specialists: &HashMap>, ) -> anyhow::Result { let divisions = division::all_divisions(); let quick_divisions = &divisions[..3]; let strategy_specs = custom_specialists.get("Strategy") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Strategy"))?; let engineering_specs = custom_specialists.get("Engineering") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Engineering"))?; let quality_specs = custom_specialists.get("Quality") .ok_or_else(|| anyhow::anyhow!("missing required division configuration: Quality"))?; let strategy_phase = make_division_phase(&quick_divisions[0], user_request, strategy_specs); let engineering_phase = make_division_phase(&quick_divisions[1], user_request, engineering_specs); let quality_phase = make_division_phase(&quick_divisions[2], user_request, quality_specs); let pipeline_primitive = ScriptPrimitive::Pipeline(vec![ strategy_phase, engineering_phase, quality_phase, ]); let wf = WorkflowScript { name: "company-pipeline-quick".to_string(), description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(), script: pipeline_primitive, options: ScriptOptions { max_concurrency: 10, continue_on_error: true, timeout_ms: None, }, }; let live: Option = turn_events.map(|events| { let events = events.clone(); let f: LiveStateFn = Arc::new(move |_agent_id: String, agent_name: String, status: AgentStatus| { let display_name = agent_name.chars().take(30).collect::(); if let Ok(mut q) = events.lock() { q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate { agent_id: display_name.clone(), agent_name: display_name, status, }); } }); f }); let args: HashMap = HashMap::new(); let findings: Arc>> = Arc::new(Mutex::new(Vec::new())); let results = execute_primitive( &wf.script, &args, wf.options.max_concurrency, true, abort_flag, live.as_ref(), session_dir, workspaces, &findings, None, )?; let all_findings = findings.lock() .map(|f| f.clone()) .unwrap_or_default(); Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, custom_specialists)) } /// Build a compressed executive summary from pipeline results. /// /// Flow: print user request header → for each division, fetch its specialist verdicts /// → join with pipes → append findings count. /// /// Why: keeps output brief to save context window space. Full results are accessible /// to the CEO via findings. /// /// Return: a formatted executive summary string. fn build_executive_summary( request: &str, results: &[String], findings: &[String], divisions: &[division::Division], custom_specialists: &HashMap>, ) -> String { let mut summary = String::new(); writeln!(summary, "Pipeline for: {request}").unwrap(); let mut start_index = 0; for div in divisions { let count = custom_specialists.get(div.name) .map_or(0, Vec::len); let mut division_verdicts = Vec::new(); for offset in 0..count { if let Some(r) = results.get(start_index + offset) { let first_line = r.lines().next().unwrap_or(r); let trimmed = first_line.chars().take(40).collect::(); division_verdicts.push(trimmed); } } let verdict = if division_verdicts.is_empty() { "—".to_string() } else { division_verdicts.join(" | ") }; writeln!(summary, " {}: {}", div.name, verdict).unwrap(); start_index += count; } if !findings.is_empty() { writeln!(summary, " Notes: {} cross-division finding(s)", findings.len()).unwrap(); } summary } /// Determine whether a request is complex enough for the full pipeline /// or can use the quick version. /// /// Simple = single file, minor fix, quick lookup, config change. /// Complex = new feature, multi-file refactor, architecture change. /// /// Used by the auto-CEO pipeline trigger in `run_agent_turn` to decide /// whether to delegate to the full company pipeline or handle directly. /// /// Heuristics: /// - Very short requests (< 10 chars) are never complex. /// - Negative keywords (simple/trivial/typo/quick) skip the pipeline. /// - Positive keywords (refactor/api/implement/architecture) trigger it. /// - Multi-line or multi-sentence requests are more likely complex. #[allow(dead_code)] pub fn is_complex_request(request: &str) -> bool { let trimmed = request.trim(); // Very short requests are never complex if trimmed.len() < 10 { return false; } // Single-line simple update patterns let lower = trimmed.to_lowercase(); let negative_keywords = [ "simple", "trivial", "typo", "just a", "only a", "minor", "quick", "tiny", "small fix", "rename", "nitpick", "cosmetic", "formatting", "spelling", "grammar", "bump", "version bump", "update comment", ]; if negative_keywords.iter().any(|k| lower.contains(k)) { return false; } // Multi-line/multi-sentence → likely complex let sentences = trimmed.split(['.', '!', '?']) .filter(|s| !s.trim().is_empty()) .count(); if sentences >= 3 { return true; } // Positive complexity keywords let complexity_keywords = [ "refactor", "redesign", "architecture", "feature", "implement", "migrate", "restructure", "rewrite", "new module", "new component", "scaffold", "multi", "multiple files", "api", "endpoint", "integration", "system", "workflow", "pipeline", "database", "authentication", "authorization", "full stack", ]; complexity_keywords.iter().any(|k| lower.contains(k)) } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_complex_request_too_short() { assert!(!is_complex_request("abc")); } #[test] fn test_is_complex_request_simple_keywords() { assert!(!is_complex_request("just a simple update to the readme")); assert!(!is_complex_request("minor typo fix in main.rs")); } #[test] fn test_is_complex_request_multi_sentence() { assert!(is_complex_request("This is sentence one. This is sentence two. This is sentence three.")); } #[test] fn test_is_complex_request_complex_keywords() { assert!(is_complex_request("implement user authentication endpoint")); assert!(is_complex_request("refactor the whole engine module")); } #[test] fn test_make_division_specialists_custom() { let divisions = division::all_divisions(); let div = &divisions[0]; let mut custom = HashMap::new(); custom.insert( "Strategy".to_string(), vec![ ("Custom Label".to_string(), "Custom Focus Description".to_string()) ] ); let specs = make_division_specialists(div, "Test Request", custom.get("Strategy").unwrap()); assert_eq!(specs.len(), 1); if let ScriptPrimitive::Agent(prompt) = &specs[0] { assert!(prompt.contains("Custom Label")); assert!(prompt.contains("Custom Focus Description")); } else { panic!("Expected ScriptPrimitive::Agent"); } } }