From 00e29139c53c5fed4c13b0493297dd9da984460c Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 13 Jul 2026 13:31:20 +0700 Subject: [PATCH] feat: remove pipeline command and refactor workflow execution to use custom specialists --- src/app/runtime/actions/mod.rs | 171 +++++++++++++++++++-------------- src/app/runtime/commands.rs | 3 - src/app/state/misc.rs | 13 --- src/app/subagent/engine.rs | 10 +- src/app/workflow/company.rs | 165 +++++++++++++++++-------------- src/app/workflow/engine.rs | 46 ++++++++- src/controller/command.rs | 13 --- src/tool/workflow.rs | 34 ++++++- 8 files changed, 277 insertions(+), 178 deletions(-) diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 5de1d8a..02195de 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -84,10 +84,6 @@ pub enum Action { RunWorkflow { script: String, }, - /// User-initiated pipeline via `/pipeline full|quick|skip`. - RunPipeline { - mode: String, - }, } /// Apply an `Action` to the application state. @@ -529,9 +525,6 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { } } if turn_finished { - // Consume pipeline override after each turn so it doesn't - // persist across multiple submissions. - state.misc.pipeline_override = None; maybe_trigger_review(state); } if turn_finished || state.dirty { @@ -593,30 +586,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) { state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {name}"))); state.dirty = true; } - Action::RunPipeline { mode } => { - match mode.as_str() { - "full" => { - state.misc.pipeline_override = Some("full".to_string()); - state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: full (5 divisions) — next request will run Strategy→Engineering→Quality→Security→Documentation".to_string())); - } - "quick" => { - state.misc.pipeline_override = Some("quick".to_string()); - state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: quick (3 divisions) — next request will run Strategy→Engineering→Quality".to_string())); - } - "skip" => { - state.misc.pipeline_override = Some("skip".to_string()); - state.push_toast(Toast::new(ToastKind::Info, "Pipeline mode: skip — next request will NOT run the company pipeline".to_string())); - } - "status" => { - let current = state.misc.pipeline_override.as_deref().unwrap_or("auto"); - state.push_toast(Toast::new(ToastKind::Info, format!("Pipeline mode: {current} (use /pipeline full|quick|skip to change)"))); - } - _ => { - state.push_toast(Toast::new(ToastKind::Error, format!("Unknown pipeline mode: {mode} (use: full, quick, skip)"))); - } - } - state.dirty = true; - } + Action::RunWorkflow { script } => { // Open the Workflow overlay so the user can see progress. state.misc.overlay = Overlay::Workflow; @@ -775,7 +745,6 @@ fn spawn_turn(state: &AppStateRest) { }) = true; let events_q = turn_events.clone(); - let pipeline_mode = state.misc.pipeline_override.clone(); std::thread::spawn(move || { let db = crate::model::msglog::open_or_create(&edit_session_dir) @@ -795,7 +764,6 @@ fn spawn_turn(state: &AppStateRest) { temperature, max_tokens, abort_flag, - pipeline_mode, }; let result = run_agent_turn(&tc, &messages, &events_q); if let Err(e) = result { @@ -824,9 +792,6 @@ struct TurnCtx { temperature: f32, max_tokens: Option, abort_flag: std::sync::Arc, - /// Pipeline override: None=auto, Some("full"), Some("quick"), Some("skip"). - /// Set by the `/pipeline` slash command. Consumed once per turn. - pipeline_mode: Option, } /// Build an ASCII tree of the workspace directory structure for the @@ -1017,11 +982,6 @@ fn run_agent_turn( // ── AUTO CEO PIPELINE ── // Before the main agent starts working, check if the pipeline should run. - // The pipeline mode is determined by: - // 1. User override: `/pipeline full|quick|skip` (consumed once) - // 2. Auto-detect: `is_complex_request()` heuristics - // - // This only triggers on the first turn of a session to avoid re-planning. let user_msg_count = msgs.iter() .filter(|m| matches!(m.role, crate::dto::chat::message::Role::User)) .count(); @@ -1034,14 +994,7 @@ fn run_agent_turn( if user_request.is_empty() { false } else { - match tc.pipeline_mode.as_deref() { - Some("skip") => { - tracing::debug!("[ceo] pipeline skipped via /pipeline skip"); - false - } - Some("full" | "quick") => true, - _ => crate::app::workflow::company::is_complex_request(user_request), - } + crate::app::workflow::company::is_complex_request(user_request) } } else { false @@ -1053,10 +1006,10 @@ fn run_agent_turn( .and_then(|m| m.content.as_deref()) .unwrap_or(""); - let use_full = tc.pipeline_mode.as_deref() != Some("quick"); - let mode_label = if use_full { "full" } else { "quick" }; + let use_full = true; + let mode_label = "full"; tracing::info!( - "[ceo] pipeline triggered (mode={}) — delegating to company pipeline", + "[ceo] pipeline triggered (mode={}) — dynamically generating planning workflow via LLM", mode_label ); @@ -1064,31 +1017,109 @@ fn run_agent_turn( q.push_back(TurnEvent::SystemNote { kind: "pipeline".to_string(), message: format!( - "Company pipeline started ({}): {} → Engineering → Quality{}", + "CEO is planning workflow (mode={})...", mode_label, - "Strategy", - if use_full { " → Security → Documentation" } else { "" }, ), }); } let pipeline_abort = Some(tc.abort_flag.clone()); - let pipeline_result = if use_full { - crate::app::workflow::company::run_company_pipeline( - user_request, - &tc.edit_log_session_dir, - &tc.workspace_roots, - Some(events_q), - &pipeline_abort, - ) + + // Ask LLM to dynamically generate the workflow specialists plan + let required_divisions = if use_full { + "all 5 divisions (Strategy, Engineering, Quality, Security, Documentation)" } else { - crate::app::workflow::company::run_company_pipeline_quick( - user_request, - &tc.edit_log_session_dir, - &tc.workspace_roots, - Some(events_q), - &pipeline_abort, - ) + "the 3 quick divisions (Strategy, Engineering, Quality)" + }; + let example_json = if use_full { + r#"{ + "Strategy": [ ["Architect", "Analyze component tree..."] ], + "Engineering": [ ["Developer", "Implement core algorithms..."] ], + "Quality": [ ["Tester", "Write unit tests..."] ], + "Security": [ ["Auditor", "Review dependencies..."] ], + "Documentation": [ ["Writer", "Document API endpoints..."] ] + }"# + } else { + r#"{ + "Strategy": [ ["Architect", "Analyze component tree..."] ], + "Engineering": [ ["Developer", "Implement core algorithms..."] ], + "Quality": [ ["Tester", "Write unit tests..."] ] + }"# + }; + + let system_msg = ChatMessage::system( + "You are a professional software architect and workflow planner. \ + Generate a tailored, structured multi-agent workflow specialists plan for the requested task. \ + Do not explain. Return ONLY raw JSON matching the requested structure." + ); + let user_msg = ChatMessage::user(format!( + "Design a structured multi-agent workflow plan for the following task:\n\n\ + \"{}\"\n\n\ + You must output a JSON object representing the 'specialists' configuration for {}.\n\ + Each division must have a list of custom specialists defined by a pair of [label, focus_description].\n\n\ + Return ONLY a JSON object with this exact structure, with no markdown codeblocks and no explanation:\n\ + {}", + user_request, required_divisions, example_json + )); + + let planner_result = tc.client.chat_with_tools_non_streaming(&[system_msg, user_msg], None); + let pipeline_result = match planner_result { + Ok((reply, _)) => { + let reply_text = reply.content.as_deref().unwrap_or("").trim(); + let clean_json = if reply_text.starts_with("```") { + let mut lines = reply_text.lines(); + lines.next(); + let mut content = lines.collect::>(); + if content.last().map(|s| s.trim() == "```").unwrap_or(false) { + content.pop(); + } + content.join("\n") + } else { + reply_text.to_string() + }; + + match serde_json::from_str::>>(&clean_json) { + Ok(custom_specialists) => { + let spec_desc = custom_specialists.iter() + .map(|(k, v)| format!("{}: {} agents", k, v.len())) + .collect::>() + .join(", "); + + if let Ok(mut q) = events_q.lock() { + q.push_back(TurnEvent::SystemNote { + kind: "pipeline".to_string(), + message: format!( + "CEO planned: Strategy → Engineering → Quality{}. (Config: {}) Running specialists...", + if use_full { " → Security → Documentation" } else { "" }, + spec_desc + ), + }); + } + + if use_full { + crate::app::workflow::company::run_company_pipeline( + user_request, + &tc.edit_log_session_dir, + &tc.workspace_roots, + Some(events_q), + &pipeline_abort, + custom_specialists, + ) + } else { + crate::app::workflow::company::run_company_pipeline_quick( + user_request, + &tc.edit_log_session_dir, + &tc.workspace_roots, + Some(events_q), + &pipeline_abort, + custom_specialists, + ) + } + } + Err(e) => Err(anyhow::anyhow!("Failed to parse LLM planning JSON: {}. Cleaned JSON was: {}", e, clean_json)), + } + } + Err(e) => Err(anyhow::anyhow!("Failed to query LLM for planning workflow: {}", e)), }; match pipeline_result { diff --git a/src/app/runtime/commands.rs b/src/app/runtime/commands.rs index c1c1a24..24b346e 100644 --- a/src/app/runtime/commands.rs +++ b/src/app/runtime/commands.rs @@ -69,9 +69,6 @@ pub fn apply_command(command: Command) -> Vec { Command::WorkflowRun { script } => { vec![Action::RunWorkflow { script }] } - Command::Pipeline { mode } => { - vec![Action::RunPipeline { mode }] - } Command::Unknown(cmd) => { vec![Action::SystemNote { kind: "error".to_string(), diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index 37600b8..35e1f9e 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -90,10 +90,6 @@ const COMMANDS: &[&str] = &[ "/model add", "/workflow", "/workflow run", - "/pipeline", - "/pipeline full", - "/pipeline quick", - "/pipeline skip", "/compact", ]; @@ -293,14 +289,6 @@ pub struct MiscState { pub api_context_length: Option, pub tick_count: u64, pub todo_content: String, - /// Pipeline mode override set by `/pipeline` command. - /// - `None`: auto-detect (default) - /// - `Some("full")`: force full pipeline - /// - `Some("quick")`: force quick pipeline - /// - `Some("skip")`: skip pipeline, handle directly - /// - /// Consumed on the next agent turn. - pub pipeline_override: Option, } impl MiscState { @@ -319,7 +307,6 @@ impl MiscState { api_context_length: None, tick_count: 0, todo_content: String::new(), - pipeline_override: None, } } diff --git a/src/app/subagent/engine.rs b/src/app/subagent/engine.rs index d91a6bb..0e1226a 100644 --- a/src/app/subagent/engine.rs +++ b/src/app/subagent/engine.rs @@ -28,10 +28,16 @@ use super::event::SubagentEvent; fn build_subagent_tools(allowed_tools: &[String]) -> (Vec>, Vec) { let all = all_tools(); let filtered: Vec> = if allowed_tools.is_empty() { - all + all.into_iter() + .filter(|t| t.name() != "company_pipeline" && t.name() != "workflow_run") + .collect() } else { all.into_iter() - .filter(|t| allowed_tools.contains(&t.name().to_string())) + .filter(|t| { + allowed_tools.contains(&t.name().to_string()) + && t.name() != "company_pipeline" + && t.name() != "workflow_run" + }) .collect() }; let defs = tool_defs(&filtered); diff --git a/src/app/workflow/company.rs b/src/app/workflow/company.rs index eb60951..68d3ec2 100644 --- a/src/app/workflow/company.rs +++ b/src/app/workflow/company.rs @@ -34,59 +34,28 @@ use crate::app::workflow::script::{ScriptPrimitive, ScriptOptions, WorkflowScrip use crate::app::workflow::engine::{execute_primitive, LiveStateFn, AgentStatus}; use crate::app::subagent::division; -/// Construct the 4 specialized agents for a 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(""); - let specializations = match div.name { - "Strategy" => vec![ - ("Architectural Analysis", "Focus on component tree, file layout, and module structure."), - ("Data Flow Planning", "Focus on sequence of calls, interface definitions, and APIs."), - ("Task Breakdown", "Focus on step-by-step TODO lists and implementation order."), - ("Risk Evaluation", "Focus on edge cases, compatibility, and system constraints."), - ], - "Engineering" => vec![ - ("Core Logic", "Focus on core algorithms, mathematical processing, and backend logic."), - ("Interface & Endpoints", "Focus on implementing routes, IPC handlers, and struct mappings."), - ("Error Handling & Logs", "Focus on implementing robust error handling, try/catch, tracing, and Result wrappers."), - ("Utility & Helpers", "Focus on filesystem helpers, input sanitization, and parsing utilities."), - ], - "Quality" => vec![ - ("Code Reviewer", "Focus on checking coding style, naming standards, and coding conventions."), - ("Unit Testing", "Focus on writing and running unit tests for individual functions and modules."), - ("Integration Testing", "Focus on writing and running integration tests for system interactions and state flows."), - ("Performance Analyst", "Focus on efficiency check, bottleneck analysis, and time complexity."), - ], - "Security" => vec![ - ("Dependency Auditor", "Focus on auditing cargo lock and checking dependencies for vulnerabilities."), - ("Input Sanitizer", "Focus on auditing input validation, injection prevention, path traversal, and shell safety."), - ("Access Control", "Focus on auditing authorization, filesystem access permissions, and API scopes."), - ("Secrets Auditor", "Focus on auditing secrets leakage, credentials safety, and log auditing."), - ], - "Documentation" => vec![ - ("README & Setup", "Focus on updating README, installation guides, usage examples, and high-level setup."), - ("API Reference", "Focus on updating API reference, parameter details, and traits/functions documentation."), - ("Changelog & Architecture", "Focus on updating CHANGELOG and describing system architecture/diagrams."), - ("Inline Comments", "Focus on adding explanatory inline comments and documentation comments inside source files."), - ], - _ => vec![ - ("Specialist 1", "Focus on general tasks and responsibilities of this division."), - ("Specialist 2", "Focus on code review and validation."), - ("Specialist 3", "Focus on error handling and reporting."), - ("Specialist 4", "Focus on documentation and testing."), - ], - }; - - specializations - .into_iter() + 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: {{findings}}", + "[{}: {}]\n\n{}\n\n{}\n\nUser request: {}\n\nFindings from previous divisions:\n{{{{findings}}}}", div.name, label, focus, @@ -99,11 +68,16 @@ fn make_division_specialists( } /// 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); + let specialists = make_division_specialists(div, user_request, specs); ScriptPrimitive::Phase { name: div.name.to_string(), script: Box::new(ScriptPrimitive::Parallel(specialists)), @@ -113,10 +87,7 @@ fn make_division_phase( /// 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: -/// 1. **Strategy** — create plan with diagrams (4 parallel subagents) -/// 2. **Engineering** — implement code per the plan (4 parallel subagents) -/// 3. **Quality** || **Security** || **Documentation** (in parallel, up to 10 concurrent subagents total) +/// sequentially, followed by Quality, Security, and Documentation in parallel. /// /// Each division receives findings from all previous divisions, enabling /// context to flow through the pipeline. @@ -128,15 +99,27 @@ pub fn run_company_pipeline( workspaces: &[std::path::PathBuf], turn_events: Option<&Arc>>>, abort_flag: &Option>, + custom_specialists: HashMap>, ) -> anyhow::Result { let divisions = division::all_divisions(); - let strategy_phase = make_division_phase(&divisions[0], user_request); - let engineering_phase = make_division_phase(&divisions[1], user_request); + 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 quality_phase = make_division_phase(&divisions[2], user_request); - let security_phase = make_division_phase(&divisions[3], user_request); - let documentation_phase = make_division_phase(&divisions[4], user_request); + 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, @@ -155,14 +138,12 @@ pub fn run_company_pipeline( description: "Company Pipeline: Strategy → Engineering → (Quality || Security || Documentation)".to_string(), script: pipeline_primitive, options: ScriptOptions { - max_concurrency: 10, // Max concurrent agents in execution - continue_on_error: true, // one division failing shouldn't block the rest + max_concurrency: 10, + continue_on_error: true, timeout_ms: None, }, }; - // Build a live callback for TUI updates if turn_events is available. - // Uses agent_name (division + specialist 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| { @@ -179,9 +160,6 @@ pub fn run_company_pipeline( }); 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( @@ -190,19 +168,18 @@ pub fn run_company_pipeline( wf.options.max_concurrency, true, abort_flag, - live_ref, + live.as_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)) + Ok(build_executive_summary(user_request, &results, &all_findings, &divisions, &custom_specialists)) } /// Run a quick company pipeline that skips non-essential divisions @@ -215,14 +192,21 @@ pub fn run_company_pipeline_quick( workspaces: &[std::path::PathBuf], turn_events: Option<&Arc>>>, abort_flag: &Option>, + custom_specialists: HashMap>, ) -> anyhow::Result { let divisions = division::all_divisions(); - // Only use first 3 divisions for quick pipeline: Strategy, Engineering, Quality let quick_divisions = &divisions[..3]; - let strategy_phase = make_division_phase(&quick_divisions[0], user_request); - let engineering_phase = make_division_phase(&quick_divisions[1], user_request); - let quality_phase = make_division_phase(&quick_divisions[2], user_request); + 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, @@ -268,27 +252,37 @@ pub fn run_company_pipeline_quick( .map(|f| f.clone()) .unwrap_or_default(); - Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions)) + Ok(build_executive_summary(user_request, &results, &all_findings, quick_divisions, &custom_specialists)) } /// 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. +/// 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(); - for (i, div) in divisions.iter().enumerate() { + let mut start_index = 0; + for div in divisions { + let count = custom_specialists.get(div.name) + .map(Vec::len) + .unwrap_or(0); + let mut division_verdicts = Vec::new(); - for offset in 0..4 { - if let Some(r) = results.get(4 * i + offset) { + 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); @@ -302,6 +296,7 @@ fn build_executive_summary( }; writeln!(summary, " {}: {}", div.name, verdict).unwrap(); + start_index += count; } if !findings.is_empty() { @@ -325,6 +320,7 @@ fn build_executive_summary( /// - 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 @@ -385,4 +381,25 @@ mod tests { 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"); + } + } } diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index 7393824..7c86fc8 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -134,7 +134,35 @@ fn spawn_single_agent( ); } - let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string()); + let mut role = "coder".to_string(); + let mut allowed_tools = None; + + if agent_name.contains("Strategy") { + let div_def = crate::app::subagent::division::strategy_division(); + role = div_def.role; + allowed_tools = div_def.allowed_tools; + } else if agent_name.contains("Engineering") { + let div_def = crate::app::subagent::division::engineering_division(); + role = div_def.role; + allowed_tools = div_def.allowed_tools; + } else if agent_name.contains("Quality") { + let div_def = crate::app::subagent::division::quality_division(); + role = div_def.role; + allowed_tools = div_def.allowed_tools; + } else if agent_name.contains("Security") { + let div_def = crate::app::subagent::division::security_division(); + role = div_def.role; + allowed_tools = div_def.allowed_tools; + } else if agent_name.contains("Documentation") { + let div_def = crate::app::subagent::division::documentation_division(); + role = div_def.role; + allowed_tools = div_def.allowed_tools; + } + + let mut def = AgentDefinition::new(agent_name.to_string(), role); + if let Some(tools) = allowed_tools { + def = def.with_allowed_tools(tools); + } let mut ctx = build_subagent_context(&def); ctx.session_dir = session_dir.to_path_buf(); ctx.workspaces = workspaces.to_vec(); @@ -344,8 +372,22 @@ pub fn execute_primitive( ) -> anyhow::Result> { match primitive { ScriptPrimitive::Agent(prompt) => { - let resolved = resolve_template(prompt, args); + let mut resolved_args = args.clone(); let findings_snapshot = findings.lock().map(|f| f.clone()).unwrap_or_default(); + if !resolved_args.contains_key("findings") { + let formatted_findings = if findings_snapshot.is_empty() { + "None".to_string() + } else { + findings_snapshot + .iter() + .enumerate() + .map(|(i, f)| format!("{}. {}", i + 1, f)) + .collect::>() + .join("\n") + }; + resolved_args.insert("findings".to_string(), formatted_findings); + } + let resolved = resolve_template(prompt, &resolved_args); let agent_id = uuid::Uuid::new_v4().to_string(); let agent_name = resolved.chars().take(40).collect::(); match spawn_single_agent(&agent_id, &agent_name, &resolved, &findings_snapshot, findings, abort_flag, live, session_dir, workspaces, timeout_ms) { diff --git a/src/controller/command.rs b/src/controller/command.rs index bf32d38..c5c5ad7 100644 --- a/src/controller/command.rs +++ b/src/controller/command.rs @@ -22,10 +22,6 @@ pub enum Command { WorkflowRun { script: String, }, - /// /pipeline full|quick|skip - Pipeline { - mode: String, - }, Unknown(String), } @@ -79,15 +75,6 @@ pub fn parse_command(text: &str) -> Command { "/workflow" => Command::WorkflowRun { script: arg1.to_string(), }, - "/pipeline" if arg1.is_empty() => Command::Pipeline { - mode: "status".to_string(), - }, - "/pipeline" if arg1 == "full" || arg1 == "quick" || arg1 == "skip" => { - Command::Pipeline { - mode: arg1.to_string(), - } - } - "/pipeline" => Command::Unknown(format!("/pipeline {arg1} (use: full|quick|skip)")), _ => Command::Unknown(cmd.to_string()), } } diff --git a/src/tool/workflow.rs b/src/tool/workflow.rs index a67606b..6f8bba0 100644 --- a/src/tool/workflow.rs +++ b/src/tool/workflow.rs @@ -171,9 +171,22 @@ impl Tool for CompanyPipeline { "enum": ["full", "quick"], "description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks", "default": "full" + }, + "specialists": { + "type": "object", + "description": "Mapping from division name (Strategy, Engineering, Quality, Security, Documentation) to list of custom specialists. Each specialist is defined by a pair of [label, focus_description]. This parameter is mandatory. The CEO/main agent must fully define the specialized roles and focuses for every division in the pipeline to run.", + "additionalProperties": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "string" }, + "minItems": 2, + "maxItems": 2 + } + } } }, - "required": ["request"] + "required": ["request", "specialists"] }) } @@ -186,6 +199,23 @@ impl Tool for CompanyPipeline { .and_then(|v| v.as_str()) .unwrap_or("full"); + let custom_specialists: std::collections::HashMap> = args.get("specialists") + .and_then(|v| v.as_object()) + .map(|obj| { + obj.iter().map(|(k, v)| { + let specs = v.as_array().map(|arr| { + arr.iter().filter_map(|item| { + let pair = item.as_array()?; + let label = pair.get(0)?.as_str()?.to_string(); + let focus = pair.get(1)?.as_str()?.to_string(); + Some((label, focus)) + }).collect() + }).unwrap_or_default(); + (k.clone(), specs) + }).collect() + }) + .ok_or_else(|| anyhow!("missing required argument: specialists"))?; + let no_abort: Option> = None; match mode { "quick" => { @@ -195,6 +225,7 @@ impl Tool for CompanyPipeline { &ctx.workspaces, ctx.turn_events.as_ref(), &no_abort, + custom_specialists, ) } _ => { @@ -204,6 +235,7 @@ impl Tool for CompanyPipeline { &ctx.workspaces, ctx.turn_events.as_ref(), &no_abort, + custom_specialists, ) } }