//! Company-style workflow orchestrator: runs the complete division pipeline //! (Strategy → Engineering → Quality → Security → Documentation) 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 │ //! │ Engineering Division — implement per plan │ //! │ Quality Division — review + write tests │ //! │ Security Division — vulnerability audit │ //! │ Documentation Div — update docs │ //! └──────────────────────────────────────────────────┘ //! │ returns consolidated summary //! ▼ //! CEO Main Agent delivers to user //! ``` use std::collections::HashMap; use std::fmt::Write; use std::sync::{Arc, Mutex, atomic::AtomicBool}; use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus}; use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScript}; use crate::app::subagent::division; /// Run the full company-style pipeline for a given user request. /// /// This orchestrates all five divisions in sequence: /// 1. **Strategy** — create plan with diagrams /// 2. **Engineering** — implement code /// 3. **Quality** — review + write tests /// 4. **Security** — audit /// 5. **Documentation** — update docs /// /// Each division receives findings from all previous divisions, enabling /// context to flow through the pipeline. /// /// Returns a consolidated executive summary string. pub fn run_company_pipeline( user_request: &str, session_dir: &std::path::Path, workspaces: &[std::path::PathBuf], turn_events: Option<&Arc>>>, abort_flag: &Option>, ) -> anyhow::Result { let divisions = division::all_divisions(); let mut pipeline_scripts: Vec = Vec::with_capacity(divisions.len()); for div in &divisions { let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or(""); // Prepend [Division Name] so the first 40 chars of the prompt // become the agent_name in spawn_single_agent, making the TUI // panel show division names instead of UUID fragments. let prompt = format!( "[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}", div.name, div_prompt, user_request, ); pipeline_scripts.push(ScriptPrimitive::Agent(prompt)); } let wf = WorkflowScript { name: "company-pipeline".to_string(), description: "Company Pipeline (full): Strategy → Engineering → Quality → Security → Documentation".to_string(), script: ScriptPrimitive::Pipeline(pipeline_scripts), options: ScriptOptions { max_concurrency: 1, // sequential by design continue_on_error: true, // one division failing shouldn't block the rest timeout_ms: None, }, }; // Build a live callback for TUI updates if turn_events is available. // Uses agent_name (division name) for the display label in the panel. 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 live_ref = live.as_ref(); // Create a per-pipeline findings scope so divisions can pass data let findings: Arc>> = Arc::new(Mutex::new(Vec::new())); let results = execute_primitive( &wf.script, &args, 1, true, abort_flag, live_ref, session_dir, workspaces, &findings, None, )?; // Collect all findings for the executive summary let all_findings = findings.lock() .map(|f| f.clone()) .unwrap_or_default(); Ok(build_executive_summary(user_request, &results, &all_findings, &divisions)) } /// 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. 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>, ) -> anyhow::Result { let divisions = division::all_divisions(); // Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality let quick_divisions = &divisions[..3]; let mut pipeline_scripts: Vec = Vec::with_capacity(quick_divisions.len()); for div in quick_divisions { let div_prompt = div.agent_def.system_prompt.as_deref().unwrap_or(""); let prompt = format!( "[{}]\n\n{}\n\nUser request: {}\n\nFindings from previous divisions: {{findings}}", div.name, div_prompt, user_request, ); pipeline_scripts.push(ScriptPrimitive::Agent(prompt)); } let wf = WorkflowScript { name: "company-pipeline-quick".to_string(), description: "Company Pipeline (quick): Strategy → Engineering → Quality".to_string(), script: ScriptPrimitive::Pipeline(pipeline_scripts), options: ScriptOptions { max_concurrency: 1, 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, 1, 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)) } /// Build a compressed executive summary from pipeline results. /// /// Keeps output brief to save context window space — just division verdicts /// and key findings, not full outputs. Full results are accessible to the /// CEO via the notes/findings that were archived during execution. fn build_executive_summary( request: &str, results: &[String], findings: &[String], divisions: &[division::Division], ) -> String { let mut summary = String::new(); writeln!(summary, "Pipeline for: {request}").unwrap(); for (i, div) in divisions.iter().enumerate() { let verdict = results.get(i).map_or_else(|| "—".to_string(), |r| { r.lines().next().unwrap_or(r) .chars().take(100).collect::() }); writeln!(summary, " {}: {}", div.name, verdict).unwrap(); } 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. 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)) }