Add subagent prompts and implement company workflow orchestration
- Introduced prompts for various subagent roles: architecture reviewer, code quality reviewer, documentation maintainer, implementation team, testing team, and security reviewer. - Implemented the auto-subagent orchestration in `auto.rs` to manage inline and background reviews. - Created a division structure in `division.rs` to define roles and responsibilities for each subagent. - Developed a company workflow orchestrator in `company.rs` to run the complete division pipeline, consolidating findings and generating executive summaries. - Added logic to determine whether to run a full or quick pipeline based on request complexity.
This commit is contained in:
@@ -380,6 +380,43 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
} else if kind == "connectivity" {
|
||||
state.misc.api_connected = message == "connected";
|
||||
} else if kind == "pipeline" {
|
||||
// Clear old workflow agents when a new pipeline starts.
|
||||
if message.contains("started") {
|
||||
state.workflow_engine.agents.clear();
|
||||
state.workflow_engine.findings.clear();
|
||||
}
|
||||
state.push_toast(Toast {
|
||||
kind: ToastKind::Info,
|
||||
message: message.clone(),
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 12000,
|
||||
});
|
||||
state.dirty = true;
|
||||
} else if kind == "bg-test-gen" {
|
||||
state.push_toast(Toast {
|
||||
kind: ToastKind::Info,
|
||||
message: message.clone(),
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 8000,
|
||||
});
|
||||
state.dirty = true;
|
||||
} else if kind == "bg-arch-review" {
|
||||
state.push_toast(Toast {
|
||||
kind: ToastKind::Info,
|
||||
message: message.clone(),
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 10000,
|
||||
});
|
||||
state.dirty = true;
|
||||
} else if kind == "bg-security-review" {
|
||||
state.push_toast(Toast {
|
||||
kind: ToastKind::Info,
|
||||
message: message.clone(),
|
||||
created_at: chrono::Utc::now().timestamp_millis(),
|
||||
lifetime_ms: 10000,
|
||||
});
|
||||
state.dirty = true;
|
||||
} else if kind == "workflow_done" {
|
||||
state.push_toast(Toast {
|
||||
kind: ToastKind::Success,
|
||||
@@ -608,12 +645,11 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
|
||||
// Build a live-state callback that pushes WorkflowAgentUpdate events
|
||||
// into the turn_events queue so the TUI panel updates in real time.
|
||||
let live: LiveStateFn = Arc::new(move |agent_id: String, status: AgentStatus| {
|
||||
let name = agent_id.chars().take(30).collect::<String>();
|
||||
let live: LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status: AgentStatus| {
|
||||
if let Ok(mut q) = turn_events_live.lock() {
|
||||
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
|
||||
agent_id: agent_id.clone(),
|
||||
agent_name: name,
|
||||
agent_name,
|
||||
status,
|
||||
});
|
||||
}
|
||||
@@ -899,6 +935,11 @@ const MAX_TURN_STEPS: usize = 10000;
|
||||
/// exhausted (e.g. slow LLM responses, stuck tool calls).
|
||||
const MAX_TURN_TIMEOUT_MS: u64 = 300_000;
|
||||
|
||||
/// Maximum number of auto inline reviews spawned per single agent turn.
|
||||
/// After N edits, the inline review is skipped to keep the turn fast;
|
||||
/// background subagents still fire at the end of the turn.
|
||||
const MAX_AUTO_REVIEWS_PER_TURN: usize = 2;
|
||||
|
||||
/// Execute one full agent turn: stream the conversation to the LLM,
|
||||
/// handle tool calls, and loop until the LLM produces a non-tool response
|
||||
/// or runs out of unfinished todo items.
|
||||
@@ -924,10 +965,12 @@ const MAX_TURN_TIMEOUT_MS: u64 = 300_000;
|
||||
fn run_agent_turn(
|
||||
tc: TurnCtx,
|
||||
messages: &[ChatMessage],
|
||||
events_q: &std::sync::Mutex<VecDeque<TurnEvent>>,
|
||||
events_q: &std::sync::Arc<std::sync::Mutex<VecDeque<TurnEvent>>>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut msgs = messages.to_vec();
|
||||
let mut edits_this_turn = 0u32;
|
||||
let mut edited_paths: Vec<String> = Vec::new();
|
||||
let mut inline_reviews_count: usize = 0;
|
||||
let mut prev_shaped = false;
|
||||
let turn_start_ms = std::time::Instant::now();
|
||||
|
||||
@@ -949,6 +992,82 @@ fn run_agent_turn(
|
||||
msgs.insert(0, sys);
|
||||
}
|
||||
|
||||
// ── AUTO CEO PIPELINE ──
|
||||
// Before the main agent starts working, check if the request is complex
|
||||
// enough to warrant the full company pipeline. If so, delegate to the
|
||||
// divisions (Strategy → Engineering → Quality → Security → Documentation)
|
||||
// and inject the results before the main agent even starts.
|
||||
//
|
||||
// This only triggers on the first turn of a session (few user messages)
|
||||
// to avoid re-planning mid-conversation.
|
||||
let user_msg_count = msgs.iter()
|
||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.count();
|
||||
if user_msg_count <= 2 {
|
||||
let user_request = msgs.iter()
|
||||
.rev()
|
||||
.filter(|m| matches!(m.role, crate::dto::chat::message::Role::User))
|
||||
.next()
|
||||
.and_then(|m| m.content.as_deref())
|
||||
.unwrap_or("");
|
||||
|
||||
if !user_request.is_empty()
|
||||
&& crate::app::workflow::company::is_complex_request(user_request)
|
||||
{
|
||||
tracing::info!(
|
||||
"[ceo] complex request detected — delegating to company pipeline"
|
||||
);
|
||||
|
||||
// Notify TUI that pipeline is starting
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: "Company pipeline started: Strategy → Engineering → Quality → Security → Documentation".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// Run the full company pipeline (blocks this thread — OK since
|
||||
// run_agent_turn already runs on a dedicated thread).
|
||||
match crate::app::workflow::company::run_company_pipeline(
|
||||
user_request,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.workspace_roots,
|
||||
Some(events_q),
|
||||
) {
|
||||
Ok(summary) => {
|
||||
tracing::info!("[ceo] company pipeline completed successfully");
|
||||
let pipeline_msg = ChatMessage::system(format!(
|
||||
"=== Company Pipeline — Executive Summary ===\n\
|
||||
The divisions have completed their work.\n\
|
||||
Review the results below as CEO, then deliver to the user.\n\n\
|
||||
{}",
|
||||
summary,
|
||||
));
|
||||
archive_message(&tc.db, &tc.session_id, &pipeline_msg);
|
||||
msgs.push(pipeline_msg);
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "pipeline".to_string(),
|
||||
message: "Company pipeline complete. CEO reviewing results...".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[ceo] company pipeline failed: {}", e);
|
||||
let fail_msg = ChatMessage::system(format!(
|
||||
"[Pipeline Note] The company pipeline encountered issues: {}.\n\
|
||||
Proceeding with direct execution as fallback.",
|
||||
e,
|
||||
));
|
||||
msgs.push(fail_msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("[ceo] request not complex — handling directly");
|
||||
}
|
||||
}
|
||||
|
||||
let mut turn_step = 0usize;
|
||||
let mut todo_retry_count = 0usize;
|
||||
const MAX_TODO_RETRIES: usize = 5;
|
||||
@@ -1145,8 +1264,61 @@ fn run_agent_turn(
|
||||
|
||||
if is_edit {
|
||||
edits_this_turn += 1;
|
||||
|
||||
// ── Auto-subagent orchestration ──
|
||||
// Extract path from tool args for auto-review and
|
||||
// background subagent tracking.
|
||||
let edit_path = args.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
if let Some(ref p) = edit_path {
|
||||
edited_paths.push(p.clone());
|
||||
|
||||
// Inline quick-review: spawn a lightweight read-only
|
||||
// subagent that reviews the written file and feeds
|
||||
// its verdict back into the LLM conversation so the
|
||||
// agent can fix issues immediately in the same turn.
|
||||
if inline_reviews_count < MAX_AUTO_REVIEWS_PER_TURN
|
||||
&& crate::app::subagent::auto::is_reviewable_path(p)
|
||||
{
|
||||
inline_reviews_count += 1;
|
||||
let review_start = std::time::Instant::now();
|
||||
match crate::app::subagent::auto::spawn_quick_review(
|
||||
p,
|
||||
&tc.edit_log_session_dir,
|
||||
&tc.workspace_roots,
|
||||
) {
|
||||
Ok(verdict) => {
|
||||
let elapsed = review_start.elapsed().as_millis();
|
||||
let review_msg = ChatMessage::tool_result(
|
||||
format!("auto-review-{}", inline_reviews_count),
|
||||
format!(
|
||||
"[Auto inline review: {} ({}ms)]\n{}",
|
||||
p,
|
||||
elapsed,
|
||||
verdict.trim(),
|
||||
),
|
||||
);
|
||||
archive_message(&tc.db, &tc.session_id, &review_msg);
|
||||
msgs.push(review_msg);
|
||||
tracing::info!(
|
||||
"[auto-review] inline review for '{}' completed in {}ms: {}",
|
||||
p, elapsed,
|
||||
verdict.lines().next().unwrap_or(&verdict).trim(),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"[auto-review] inline review failed for '{}': {}",
|
||||
p, e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let tool_path = args.get("path").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
|
||||
{
|
||||
@@ -1221,6 +1393,29 @@ fn run_agent_turn(
|
||||
message: edits_this_turn.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Background auto-subagents ──
|
||||
// After a turn with edits, spawn deeper-analysis subagents in the
|
||||
// background (test generation, architecture review, security review).
|
||||
// These run asynchronously on OS threads and report via SystemNote
|
||||
// events, so they do not block the main agent or TUI.
|
||||
//
|
||||
// Only spawn background agents if we actually accumulated paths
|
||||
// (safety check — should always be true when edits_this_turn > 0).
|
||||
if !edited_paths.is_empty() {
|
||||
let bg_paths = edited_paths.clone();
|
||||
let bg_session_dir = tc.edit_log_session_dir.clone();
|
||||
let bg_workspaces = tc.workspace_roots.clone();
|
||||
let bg_events = events_q.clone();
|
||||
std::thread::spawn(move || {
|
||||
crate::app::subagent::auto::spawn_all_background(
|
||||
&bg_paths,
|
||||
&bg_session_dir,
|
||||
&bg_workspaces,
|
||||
&bg_events,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(mut q) = events_q.lock() {
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Auto-subagent orchestration: the main agent automatically delegates
|
||||
//! review, test-generation, architecture-review, and security-review tasks
|
||||
//! to subagents without requiring explicit tool calls from the LLM.
|
||||
//!
|
||||
//! Two modes:
|
||||
//! - **Inline** (`spawn_quick_review`): runs synchronously within the turn
|
||||
//! after each write/edit tool call. Results are fed back into the LLM
|
||||
//! conversation so the agent can act on feedback immediately.
|
||||
//! - **Background** (`spawn_background_*`): runs asynchronously on a
|
||||
//! dedicated OS thread at the end of a turn. Reports results via
|
||||
//! `TurnEvent::SystemNote`, consumed by the TUI on the next Tick.
|
||||
//!
|
||||
//! Why inline vs background:
|
||||
//! - Inline reviews give the agent an immediate feedback loop ("I just
|
||||
//! wrote this file, let me check if it's correct before continuing").
|
||||
//! - Background reviews catch broader concerns (missing tests, architectural
|
||||
//! drift, security issues) without blocking the main agent's flow.
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::VecDeque;
|
||||
use crate::app::state::runtime::TurnEvent;
|
||||
use crate::app::subagent::context::build_subagent_context;
|
||||
use crate::app::subagent::engine::run_subagent;
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
use crate::app::subagent::event::SubagentEvent;
|
||||
|
||||
/// File extensions that should not trigger auto-review (config, lock, data).
|
||||
const SKIP_REVIEW_EXTENSIONS: &[&str] = &[
|
||||
".lock", ".md", ".txt", ".json", ".toml", ".yaml", ".yml",
|
||||
".svg", ".png", ".jpg", ".ico", ".woff", ".woff2",
|
||||
];
|
||||
|
||||
/// File names that should not trigger auto-review.
|
||||
const SKIP_REVIEW_FILES: &[&str] = &[
|
||||
"Cargo.lock", "yarn.lock", "package-lock.json",
|
||||
".gitignore", ".env", ".env.example",
|
||||
];
|
||||
|
||||
/// Maximum LLM steps for a quick-review subagent. Keeps reviews fast.
|
||||
const QUICK_REVIEW_MAX_STEPS: usize = 2;
|
||||
|
||||
/// Maximum LLM steps for background subagents (test gen, arch, security).
|
||||
const BG_SUBAGENT_MAX_STEPS: usize = 8;
|
||||
|
||||
/// ─── Helpers ───
|
||||
|
||||
/// Check whether a file path is worth auto-reviewing (not config/lock/data).
|
||||
pub fn is_reviewable_path(path: &str) -> bool {
|
||||
let lower = path.to_lowercase();
|
||||
if SKIP_REVIEW_FILES.iter().any(|f| lower.ends_with(f)) {
|
||||
return false;
|
||||
}
|
||||
if SKIP_REVIEW_EXTENSIONS.iter().any(|e| lower.ends_with(e)) {
|
||||
return false;
|
||||
}
|
||||
// Skip paths that are clearly generated or vendored
|
||||
if lower.contains("/target/") || lower.contains("/node_modules/")
|
||||
|| lower.contains("/.git/") || lower.contains("/vendor/")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Determine whether a file change looks like it modifies production logic
|
||||
/// (vs. tests, config, or documentation) — used to decide if a test-gen
|
||||
/// or security-review background subagent should fire.
|
||||
fn is_production_code(path: &str) -> bool {
|
||||
let lower = path.to_lowercase();
|
||||
// Skip test files — they don't need test-gen from another agent
|
||||
if lower.contains("test") || lower.contains("spec") || lower.contains("_test.") {
|
||||
return false;
|
||||
}
|
||||
// Only source files
|
||||
lower.ends_with(".rs") || lower.ends_with(".ts") || lower.ends_with(".tsx")
|
||||
|| lower.ends_with(".js") || lower.ends_with(".jsx") || lower.ends_with(".go")
|
||||
|| lower.ends_with(".py") || lower.ends_with(".java") || lower.ends_with(".kt")
|
||||
|| lower.ends_with(".swift") || lower.ends_with(".c") || lower.ends_with(".cpp")
|
||||
|| lower.ends_with(".h") || lower.ends_with(".hpp")
|
||||
}
|
||||
|
||||
/// ─── Inline Quick Review (synchronous, feeds back to LLM) ───
|
||||
|
||||
/// Spawn a lightweight inline code review subagent for the given file.
|
||||
///
|
||||
/// The subagent reads the file (read-only), checks for common issues,
|
||||
/// and returns a concise text verdict. This runs synchronously so the
|
||||
/// main agent's `run_agent_turn` can inject the result back into the
|
||||
/// LLM conversation for immediate action.
|
||||
///
|
||||
/// Returns `Ok(verdict)` if the review completed, or an error if the
|
||||
/// subagent could not be spawned or failed internally. Callers should
|
||||
/// log and swallow errors gracefully — a failed inline review should
|
||||
/// never interrupt the main agent's flow.
|
||||
pub fn spawn_quick_review(
|
||||
file_path: &str,
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
) -> anyhow::Result<String> {
|
||||
let prompt = format!(
|
||||
"{}\n\nFile to review: {}",
|
||||
crate::resources::AUTO_REVIEWER_PROMPT,
|
||||
file_path,
|
||||
);
|
||||
|
||||
let def = AgentDefinition::new(
|
||||
"quick-reviewer".to_string(),
|
||||
"reviewer".to_string(),
|
||||
)
|
||||
.with_system_prompt(prompt)
|
||||
.with_max_steps(QUICK_REVIEW_MAX_STEPS);
|
||||
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = session_dir.to_path_buf();
|
||||
ctx.workspaces = workspaces.to_vec();
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { _tool, .. } => {
|
||||
tracing::debug!("[auto-review] tool call: {}", _tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { _tool, .. } => {
|
||||
tracing::debug!("[auto-review] tool result: {}", _tool);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[auto-review] completed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let verdict = run_subagent(ctx, tx)?;
|
||||
tracing::info!(
|
||||
"[auto-review] quick review for '{}': {}",
|
||||
file_path,
|
||||
verdict.lines().next().unwrap_or(&verdict),
|
||||
);
|
||||
Ok(verdict)
|
||||
}
|
||||
|
||||
/// ─── Background Subagent Spawners (async, report via SystemNote) ───
|
||||
|
||||
/// Spawn a background subagent that generates tests for modified files.
|
||||
///
|
||||
/// Uses the test-generator prompt and has read-write access so it can
|
||||
/// create test files. Runs in a separate OS thread and reports completion
|
||||
/// via `TurnEvent::SystemNote { kind: "bg-test-gen" }`.
|
||||
pub fn spawn_background_test_gen(
|
||||
file_paths: &[String],
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) {
|
||||
if file_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let paths = file_paths.to_vec();
|
||||
let sd = session_dir.to_path_buf();
|
||||
let ws = workspaces.to_vec();
|
||||
let events = turn_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
tracing::info!(
|
||||
"[bg-test-gen] spawning for {} file(s): {:?}",
|
||||
paths.len(),
|
||||
paths,
|
||||
);
|
||||
|
||||
let file_list = paths.join("\n");
|
||||
let prompt = format!(
|
||||
"{}\n\nModified files that need tests:\n{}",
|
||||
crate::resources::TEST_GENERATOR_PROMPT,
|
||||
file_list,
|
||||
);
|
||||
|
||||
let def = AgentDefinition::new(
|
||||
"test-generator".to_string(),
|
||||
"coder".to_string(), // needs write access
|
||||
)
|
||||
.with_system_prompt(prompt)
|
||||
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
|
||||
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { _tool, .. } => {
|
||||
tracing::debug!("[bg-test-gen] tool: {}", _tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { _tool, .. } => {
|
||||
tracing::debug!("[bg-test-gen] result: {}", _tool);
|
||||
}
|
||||
SubagentEvent::StepCompleted { _step, .. } => {
|
||||
tracing::trace!("[bg-test-gen] step {} done", _step);
|
||||
}
|
||||
SubagentEvent::StepFailed { _step, _error } => {
|
||||
tracing::warn!("[bg-test-gen] step {} failed: {}", _step, _error);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-test-gen] completed");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(ctx, tx);
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Auto test-gen: {}", first)
|
||||
}
|
||||
Err(e) => format!("Auto test-gen failed: {}", e),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "bg-test-gen".to_string(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawn a background architecture-review subagent.
|
||||
///
|
||||
/// Inspects the modified files for architectural consistency (layering,
|
||||
/// coupling, module boundaries). Reports via
|
||||
/// `TurnEvent::SystemNote { kind: "bg-arch-review" }`.
|
||||
pub fn spawn_background_arch_review(
|
||||
file_paths: &[String],
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) {
|
||||
if file_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let paths = file_paths.to_vec();
|
||||
let sd = session_dir.to_path_buf();
|
||||
let ws = workspaces.to_vec();
|
||||
let events = turn_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let file_list = paths.join("\n");
|
||||
let prompt = format!(
|
||||
"{}\n\nModified files for architecture review:\n{}",
|
||||
crate::resources::ARCH_REVIEWER_PROMPT,
|
||||
file_list,
|
||||
);
|
||||
|
||||
let def = AgentDefinition::new(
|
||||
"arch-reviewer".to_string(),
|
||||
"reviewer".to_string(),
|
||||
)
|
||||
.with_system_prompt(prompt)
|
||||
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
|
||||
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { _tool, .. } => {
|
||||
tracing::debug!("[bg-arch] tool: {}", _tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { _tool, .. } => {
|
||||
tracing::debug!("[bg-arch] result: {}", _tool);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-arch] completed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(ctx, tx);
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Architecture review: {}", first)
|
||||
}
|
||||
Err(e) => format!("Architecture review failed: {}", e),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "bg-arch-review".to_string(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawn a background security-review subagent.
|
||||
///
|
||||
/// Checks modified files for security vulnerabilities. Reports via
|
||||
/// `TurnEvent::SystemNote { kind: "bg-security-review" }`.
|
||||
pub fn spawn_background_security_review(
|
||||
file_paths: &[String],
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) {
|
||||
if file_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only review production code files for security — test files and
|
||||
// config files are out of scope for security review.
|
||||
let prod_paths: Vec<String> = file_paths
|
||||
.iter()
|
||||
.filter(|p| is_production_code(p))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if prod_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let paths = prod_paths;
|
||||
let sd = session_dir.to_path_buf();
|
||||
let ws = workspaces.to_vec();
|
||||
let events = turn_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let file_list = paths.join("\n");
|
||||
let prompt = format!(
|
||||
"{}\n\nModified files for security review:\n{}",
|
||||
crate::resources::SECURITY_REVIEWER_PROMPT,
|
||||
file_list,
|
||||
);
|
||||
|
||||
let def = AgentDefinition::new(
|
||||
"security-reviewer".to_string(),
|
||||
"reviewer".to_string(),
|
||||
)
|
||||
.with_system_prompt(prompt)
|
||||
.with_max_steps(BG_SUBAGENT_MAX_STEPS);
|
||||
|
||||
let mut ctx = build_subagent_context(def);
|
||||
ctx.session_dir = sd;
|
||||
ctx.workspaces = ws;
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::channel(32);
|
||||
let _drain = std::thread::spawn(move || {
|
||||
while let Some(event) = rx.blocking_recv() {
|
||||
match &event {
|
||||
SubagentEvent::ToolCall { _tool, .. } => {
|
||||
tracing::debug!("[bg-security] tool: {}", _tool);
|
||||
}
|
||||
SubagentEvent::ToolResult { _tool, .. } => {
|
||||
tracing::debug!("[bg-security] result: {}", _tool);
|
||||
}
|
||||
SubagentEvent::Completed { .. } => {
|
||||
tracing::debug!("[bg-security] completed");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = run_subagent(ctx, tx);
|
||||
let message = match &result {
|
||||
Ok(output) => {
|
||||
let first = output.lines().next().unwrap_or(output);
|
||||
format!("Security review: {}", first)
|
||||
}
|
||||
Err(e) => format!("Security review failed: {}", e),
|
||||
};
|
||||
|
||||
if let Ok(mut q) = events.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "bg-security-review".to_string(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Convenience: spawn all applicable background subagents for a set of edited
|
||||
/// file paths. Called once at the end of a main agent turn.
|
||||
///
|
||||
/// Flow: always spawns arch-review and security-review if there are
|
||||
/// reviewable production files → spawns test-gen only if there are source
|
||||
/// files that aren't already tests.
|
||||
pub fn spawn_all_background(
|
||||
file_paths: &[String],
|
||||
session_dir: &Path,
|
||||
workspaces: &[std::path::PathBuf],
|
||||
turn_events: &Arc<Mutex<VecDeque<TurnEvent>>>,
|
||||
) {
|
||||
if file_paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Background test-gen: only for non-test source files
|
||||
let source_paths: Vec<String> = file_paths
|
||||
.iter()
|
||||
.filter(|p| is_production_code(p))
|
||||
.cloned()
|
||||
.collect();
|
||||
spawn_background_test_gen(&source_paths, session_dir, workspaces, turn_events);
|
||||
|
||||
// Background arch review: for all files that are reviewable
|
||||
let reviewable: Vec<String> = file_paths
|
||||
.iter()
|
||||
.filter(|p| is_reviewable_path(p))
|
||||
.cloned()
|
||||
.collect();
|
||||
spawn_background_arch_review(&reviewable, session_dir, workspaces, turn_events);
|
||||
|
||||
// Background security review: only production source files
|
||||
spawn_background_security_review(&source_paths, session_dir, workspaces, turn_events);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
//! Company-style agent divisions: specialized subagent roles that form an
|
||||
//! organizational hierarchy like a company.
|
||||
//!
|
||||
//! ```text
|
||||
//! CEO (Main Agent)
|
||||
//! ├── Strategy Division (planner) — architecture, diagrams, plan
|
||||
//! ├── Engineering Division (coder) — implementation
|
||||
//! ├── Quality Division (tester) — review, test
|
||||
//! ├── Security Division (auditor) — security audit
|
||||
//! └── Documentation Division (doc) — documentation
|
||||
//! ```
|
||||
//!
|
||||
//! Each division has a specific role, tools, and system prompt tailored to
|
||||
//! its function. The main agent (CEO) delegates work to divisions via
|
||||
//! the company pipeline workflow.
|
||||
|
||||
use crate::app::subagent::spawn::AgentDefinition;
|
||||
|
||||
/// Division roles — used as both the `role` field in AgentDefinition
|
||||
/// and as the key for pipeline routing.
|
||||
pub mod roles {
|
||||
/// Strategy Division: plans architecture, creates diagrams, breaks down work.
|
||||
pub const STRATEGY: &str = "planner";
|
||||
/// Engineering Division: implements code per the plan.
|
||||
pub const ENGINEERING: &str = "coder";
|
||||
/// Quality Division: reviews implementation, writes tests.
|
||||
pub const QUALITY: &str = "tester";
|
||||
/// Security Division: audits for vulnerabilities.
|
||||
pub const SECURITY: &str = "auditor";
|
||||
/// Documentation Division: updates docs, README, inline documentation.
|
||||
pub const DOCUMENTATION: &str = "documenter";
|
||||
}
|
||||
|
||||
/// ─── Division Agent Definitions ───
|
||||
|
||||
/// Build the Strategy Division agent — chief architect and planner.
|
||||
///
|
||||
/// Tools: read-only (read, grep, glob, search, lsp, plan, seqthink, recall)
|
||||
/// Role: never writes code; produces detailed plans with mermaid diagrams.
|
||||
pub fn strategy_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"strategy-division".to_string(),
|
||||
roles::STRATEGY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_PLANNER_PROMPT.to_string())
|
||||
.with_max_steps(15)
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"search".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"plan".to_string(),
|
||||
"recall".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Engineering Division agent — implements code per the plan.
|
||||
///
|
||||
/// Tools: full access (all write/edit/bash/git/LSP tools)
|
||||
/// Role: executes the strategy plan, one file at a time.
|
||||
pub fn engineering_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"engineering-division".to_string(),
|
||||
roles::ENGINEERING.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_IMPLEMENTER_PROMPT.to_string())
|
||||
.with_max_steps(50)
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"delete".to_string(),
|
||||
"bash".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"git_operator".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
"lsp_completion".to_string(),
|
||||
"lsp_disconnect".to_string(),
|
||||
"todowrite".to_string(),
|
||||
"todofinish".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Quality Division agent — reviews code and writes tests.
|
||||
///
|
||||
/// Tools: read, write, grep, glob, bash (for running tests), LSP, memory
|
||||
/// Role: verifies correctness and creates/runs tests.
|
||||
pub fn quality_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"quality-division".to_string(),
|
||||
roles::QUALITY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_TESTER_PROMPT.to_string())
|
||||
.with_max_steps(30)
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"bash".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Security Division agent — security auditor.
|
||||
///
|
||||
/// Tools: read-only + search + memory
|
||||
/// Role: audits implementation for vulnerabilities.
|
||||
pub fn security_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"security-division".to_string(),
|
||||
roles::SECURITY.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::SECURITY_REVIEWER_PROMPT.to_string())
|
||||
.with_max_steps(15)
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"search".to_string(),
|
||||
"seqthink".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
"lsp_connect".to_string(),
|
||||
"lsp_diagnostics".to_string(),
|
||||
"lsp_hover".to_string(),
|
||||
"lsp_definition".to_string(),
|
||||
"lsp_references".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// Build the Documentation Division agent — documentation maintainer.
|
||||
///
|
||||
/// Tools: read, grep, glob, write, edit, memory
|
||||
/// Role: updates README, inline docs, architecture docs.
|
||||
pub fn documentation_division() -> AgentDefinition {
|
||||
AgentDefinition::new(
|
||||
"documentation-division".to_string(),
|
||||
roles::DOCUMENTATION.to_string(),
|
||||
)
|
||||
.with_system_prompt(crate::resources::DIVISION_DOCUMENTER_PROMPT.to_string())
|
||||
.with_max_steps(15)
|
||||
.with_allowed_tools(vec![
|
||||
"read".to_string(),
|
||||
"write".to_string(),
|
||||
"edit".to_string(),
|
||||
"grep".to_string(),
|
||||
"glob".to_string(),
|
||||
"recall".to_string(),
|
||||
"remember".to_string(),
|
||||
])
|
||||
}
|
||||
|
||||
/// ─── Division Registry ───
|
||||
|
||||
/// A named division with its agent definition and display metadata.
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub struct Division {
|
||||
/// Display name for the division (e.g. "Strategy", "Engineering").
|
||||
pub name: &'static str,
|
||||
/// Role tag used for pipeline routing (matches `roles::*` constants).
|
||||
pub role: &'static str,
|
||||
/// One-line description of what this division does.
|
||||
pub description: &'static str,
|
||||
/// Agent definition with tools, prompt, and step budget.
|
||||
pub agent_def: AgentDefinition,
|
||||
}
|
||||
|
||||
impl Division {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
role: &'static str,
|
||||
description: &'static str,
|
||||
agent_def: AgentDefinition,
|
||||
) -> Self {
|
||||
Division { name, role, description, agent_def }
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all company divisions as an ordered list matching the pipeline flow:
|
||||
/// Strategy → Engineering → Quality → Security → Documentation.
|
||||
pub fn all_divisions() -> Vec<Division> {
|
||||
vec![
|
||||
Division::new(
|
||||
"Strategy",
|
||||
roles::STRATEGY,
|
||||
"Architecture planning with diagrams and step-by-step breakdown",
|
||||
strategy_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Engineering",
|
||||
roles::ENGINEERING,
|
||||
"Code implementation following the plan",
|
||||
engineering_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Quality",
|
||||
roles::QUALITY,
|
||||
"Code review and comprehensive testing",
|
||||
quality_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Security",
|
||||
roles::SECURITY,
|
||||
"Security vulnerability audit",
|
||||
security_division(),
|
||||
),
|
||||
Division::new(
|
||||
"Documentation",
|
||||
roles::DOCUMENTATION,
|
||||
"Documentation updates and maintenance",
|
||||
documentation_division(),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Subagent management: spawning, context building, engine loop, and
|
||||
//! progress events.
|
||||
|
||||
pub mod auto;
|
||||
pub mod context;
|
||||
pub mod division;
|
||||
pub mod engine;
|
||||
pub mod event;
|
||||
pub mod spawn;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
//! 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::sync::{Arc, Mutex};
|
||||
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<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
) -> anyhow::Result<String> {
|
||||
let divisions = division::all_divisions();
|
||||
let mut pipeline_scripts: Vec<ScriptPrimitive> = 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: format!(
|
||||
"Company Pipeline (full): Strategy → Engineering → Quality → Security → Documentation",
|
||||
),
|
||||
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<LiveStateFn> = 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::<String>();
|
||||
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<String, String> = HashMap::new();
|
||||
let live_ref = live.as_ref();
|
||||
|
||||
// Create a per-pipeline findings scope so divisions can pass data
|
||||
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let results = execute_primitive(
|
||||
&wf.script,
|
||||
&args,
|
||||
1,
|
||||
true,
|
||||
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<Mutex<std::collections::VecDeque<crate::app::state::runtime::TurnEvent>>>>,
|
||||
) -> anyhow::Result<String> {
|
||||
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<ScriptPrimitive> = 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<LiveStateFn> = 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::<String>();
|
||||
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<String, String> = HashMap::new();
|
||||
let findings: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let results = execute_primitive(
|
||||
&wf.script, &args, 1, true,
|
||||
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 consolidated executive summary from pipeline results.
|
||||
fn build_executive_summary(
|
||||
request: &str,
|
||||
results: &[String],
|
||||
findings: &[String],
|
||||
divisions: &[division::Division],
|
||||
) -> String {
|
||||
let mut summary = String::new();
|
||||
summary.push_str(&format!("# Company Pipeline — Executive Summary\n\n"));
|
||||
summary.push_str(&format!("**Request**: {}\n\n", request));
|
||||
summary.push_str("## Division Results\n\n");
|
||||
|
||||
for (i, div) in divisions.iter().enumerate() {
|
||||
let result_summary = results.get(i)
|
||||
.map(|r| {
|
||||
let first_line = r.lines().next().unwrap_or(r);
|
||||
if first_line.len() > 120 {
|
||||
format!("{}...", &first_line[..117])
|
||||
} else {
|
||||
first_line.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| "No output".to_string());
|
||||
|
||||
summary.push_str(&format!("### {} Division\n", div.name));
|
||||
summary.push_str(&format!("- Role: {}\n", div.description));
|
||||
summary.push_str(&format!("- Result: {}\n\n", result_summary));
|
||||
}
|
||||
|
||||
if !findings.is_empty() {
|
||||
summary.push_str("## Cross-Division Findings\n\n");
|
||||
for (i, f) in findings.iter().enumerate() {
|
||||
summary.push_str(&format!("{}. {}\n", i + 1, f));
|
||||
}
|
||||
summary.push_str("\n");
|
||||
}
|
||||
|
||||
summary.push_str("---\n");
|
||||
summary.push_str(&format!(
|
||||
"Pipeline completed: {} division(s) executed.\n",
|
||||
divisions.len(),
|
||||
));
|
||||
|
||||
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.
|
||||
pub fn is_complex_request(request: &str) -> bool {
|
||||
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",
|
||||
];
|
||||
let lower = request.to_lowercase();
|
||||
complexity_keywords.iter().any(|k| lower.contains(k))
|
||||
}
|
||||
+41
-22
@@ -67,9 +67,14 @@ impl WorkflowEngine {
|
||||
/// Shared live state used by `run_workflow_tracked` to push real-time
|
||||
/// agent status updates into the TUI's `WorkflowEngine`.
|
||||
///
|
||||
/// The closure receives `(agent_id, new_status)` and should update the
|
||||
/// corresponding agent in `AppStateRest::workflow_engine`.
|
||||
pub type LiveStateFn = Arc<dyn Fn(String, AgentStatus) + Send + Sync>;
|
||||
/// The closure receives `(agent_id, agent_name, new_status)`:
|
||||
/// - `agent_id`: unique identifier (UUID) for upserting the agent.
|
||||
/// - `agent_name`: human-readable display name for the TUI panel.
|
||||
/// - `status`: the agent's lifecycle state and timing.
|
||||
///
|
||||
/// Callers should use `agent_id` as the stable key and `agent_name` for
|
||||
/// display purposes (e.g. the division name in the company pipeline).
|
||||
pub type LiveStateFn = Arc<dyn Fn(String, String, AgentStatus) + Send + Sync>;
|
||||
|
||||
/// Spawn a single synchronous subagent with the given prompt, passing it
|
||||
/// any findings from earlier sibling agents. Updates live state before and
|
||||
@@ -107,14 +112,20 @@ fn spawn_single_agent(
|
||||
|
||||
let started_at = chrono::Utc::now().timestamp_millis();
|
||||
|
||||
// Notify UI: this agent is now running
|
||||
// Notify UI: this agent is now running.
|
||||
// Pass both the unique agent_id (UUID for stable key) and agent_name
|
||||
// (human-readable display name, e.g. division name).
|
||||
if let Some(f) = live {
|
||||
f(agent_id.to_string(), AgentStatus {
|
||||
state: AgentState::Running,
|
||||
started_at: Some(started_at),
|
||||
completed_at: None,
|
||||
error: None,
|
||||
});
|
||||
f(
|
||||
agent_id.to_string(),
|
||||
agent_name.to_string(),
|
||||
AgentStatus {
|
||||
state: AgentState::Running,
|
||||
started_at: Some(started_at),
|
||||
completed_at: None,
|
||||
error: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let def = AgentDefinition::new(agent_name.to_string(), "coder".to_string())
|
||||
@@ -207,18 +218,26 @@ fn spawn_single_agent(
|
||||
// Notify UI: agent completed or failed
|
||||
if let Some(f) = live {
|
||||
match &result {
|
||||
Ok(_) => f(agent_id.to_string(), AgentStatus {
|
||||
state: AgentState::Completed,
|
||||
started_at: Some(started_at),
|
||||
completed_at: Some(completed_at),
|
||||
error: None,
|
||||
}),
|
||||
Err(e) => f(agent_id.to_string(), AgentStatus {
|
||||
state: AgentState::Failed,
|
||||
started_at: Some(started_at),
|
||||
completed_at: Some(completed_at),
|
||||
error: Some(e.to_string()),
|
||||
}),
|
||||
Ok(_) => f(
|
||||
agent_id.to_string(),
|
||||
agent_name.to_string(),
|
||||
AgentStatus {
|
||||
state: AgentState::Completed,
|
||||
started_at: Some(started_at),
|
||||
completed_at: Some(completed_at),
|
||||
error: None,
|
||||
},
|
||||
),
|
||||
Err(e) => f(
|
||||
agent_id.to_string(),
|
||||
agent_name.to_string(),
|
||||
AgentStatus {
|
||||
state: AgentState::Failed,
|
||||
started_at: Some(started_at),
|
||||
completed_at: Some(completed_at),
|
||||
error: Some(e.to_string()),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Workflow orchestration: a script interpreter that runs pipeline/parallel
|
||||
//! primitives across multiple subagent instances.
|
||||
|
||||
pub mod company;
|
||||
pub mod engine;
|
||||
pub mod script;
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
pub const SYSTEM_PROMPT: &str = include_str!("../src-misc/system-prompt.txt");
|
||||
pub const SYSTEM_TOOLS: &str = include_str!("../src-misc/system-tools.txt");
|
||||
|
||||
/// Prompt templates for auto-subagent types (inline quick review,
|
||||
/// test generation, architecture review, security review).
|
||||
pub const AUTO_REVIEWER_PROMPT: &str = include_str!("../src-misc/auto-reviewer-prompt.txt");
|
||||
pub const TEST_GENERATOR_PROMPT: &str = include_str!("../src-misc/test-generator-prompt.txt");
|
||||
pub const ARCH_REVIEWER_PROMPT: &str = include_str!("../src-misc/arch-reviewer-prompt.txt");
|
||||
pub const SECURITY_REVIEWER_PROMPT: &str = include_str!("../src-misc/security-reviewer-prompt.txt");
|
||||
|
||||
/// Division-specific prompts for the company-style agent architecture.
|
||||
pub const DIVISION_PLANNER_PROMPT: &str = include_str!("../src-misc/division-planner-prompt.txt");
|
||||
pub const DIVISION_IMPLEMENTER_PROMPT: &str = include_str!("../src-misc/division-implementer-prompt.txt");
|
||||
pub const DIVISION_TESTER_PROMPT: &str = include_str!("../src-misc/division-tester-prompt.txt");
|
||||
pub const DIVISION_DOCUMENTER_PROMPT: &str = include_str!("../src-misc/division-documenter-prompt.txt");
|
||||
|
||||
pub const HELP_TEXT: &str = "
|
||||
ZESDEX - Help
|
||||
=============
|
||||
|
||||
@@ -169,6 +169,7 @@ pub fn all_tools() -> Vec<Box<dyn Tool>> {
|
||||
Box::new(super::tool::plan::PlanReady),
|
||||
Box::new(super::tool::workflow::WorkflowRun),
|
||||
Box::new(super::tool::workflow::NoteFinding),
|
||||
Box::new(super::tool::workflow::CompanyPipeline),
|
||||
Box::new(super::tool::spawn::SpawnAgents),
|
||||
Box::new(super::tool::spawn::SpawnPipeline),
|
||||
Box::new(super::tool::memory::remember::Remember),
|
||||
|
||||
+4
-6
@@ -91,12 +91,11 @@ impl Tool for SpawnAgents {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
|
||||
let turn_events = turn_events.clone();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, status| {
|
||||
let name = agent_id.chars().take(30).collect::<String>();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| {
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
|
||||
agent_id,
|
||||
agent_name: name,
|
||||
agent_name,
|
||||
status,
|
||||
});
|
||||
}
|
||||
@@ -182,12 +181,11 @@ impl Tool for SpawnPipeline {
|
||||
use std::sync::{Arc, Mutex};
|
||||
let live: Option<crate::app::workflow::engine::LiveStateFn> = _ctx.turn_events.as_ref().map(|turn_events| {
|
||||
let turn_events = turn_events.clone();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, status| {
|
||||
let name = agent_id.chars().take(30).collect::<String>();
|
||||
let f: crate::app::workflow::engine::LiveStateFn = Arc::new(move |agent_id: String, agent_name: String, status| {
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
q.push_back(crate::app::state::runtime::TurnEvent::WorkflowAgentUpdate {
|
||||
agent_id,
|
||||
agent_name: name,
|
||||
agent_name,
|
||||
status,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,3 +138,71 @@ impl Tool for NoteFinding {
|
||||
Ok(format!("finding recorded: {}", text.chars().take(80).collect::<String>()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool that delegates work to the company-style division pipeline.
|
||||
///
|
||||
/// The main agent (CEO) calls this tool to pass a user request through the
|
||||
/// full company organization: Strategy → Engineering → Quality → Security
|
||||
/// → Documentation. Returns an executive summary.
|
||||
///
|
||||
/// Use this for any complex or multi-step task. For simple tasks, handle
|
||||
/// inline or use the quick variant.
|
||||
pub struct CompanyPipeline;
|
||||
|
||||
impl Tool for CompanyPipeline {
|
||||
fn name(&self) -> &'static str {
|
||||
"company_pipeline"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Delegate a task to the full company division pipeline: Strategy (plan+diagrams) → Engineering (implement) → Quality (review+test) → Security (audit) → Documentation (docs). Use this for ALL non-trivial tasks instead of doing them yourself. The pipeline returns an executive summary."
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request": {
|
||||
"type": "string",
|
||||
"description": "The task description to delegate to the company pipeline"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["full", "quick"],
|
||||
"description": "Pipeline mode: 'full' (5 divisions) for complex tasks, 'quick' (3 divisions: Strategy→Engineering→Quality) for simpler tasks",
|
||||
"default": "full"
|
||||
}
|
||||
},
|
||||
"required": ["request"]
|
||||
})
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
|
||||
let request = args.get("request")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow!("missing required argument: request"))?;
|
||||
|
||||
let mode = args.get("mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("full");
|
||||
|
||||
match mode {
|
||||
"quick" => {
|
||||
crate::app::workflow::company::run_company_pipeline_quick(
|
||||
request,
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
_ctx.turn_events.as_ref(),
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
crate::app::workflow::company::run_company_pipeline(
|
||||
request,
|
||||
&_ctx.session_dir,
|
||||
&_ctx.workspaces,
|
||||
_ctx.turn_events.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-28
@@ -4,9 +4,8 @@
|
||||
//! rich panel showing agent statuses, findings count, session counters,
|
||||
//! and usage hints.
|
||||
//!
|
||||
//! Why: the panel is useful even without a running session (shows engine
|
||||
//! state and instructions), and only shows non-zero counters to keep it
|
||||
//! compact.
|
||||
//! Division-aware: when the workflow is a company pipeline, shows the
|
||||
//! division pipeline header with visual arrows between stages.
|
||||
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Style, Modifier};
|
||||
@@ -16,21 +15,45 @@ use ratatui::Frame;
|
||||
use super::theme::Theme;
|
||||
use crate::app::workflow::engine::AgentState;
|
||||
|
||||
/// Icons for division states in the company pipeline.
|
||||
fn div_icon(state: AgentState) -> &'static str {
|
||||
match state {
|
||||
AgentState::Idle => "○",
|
||||
AgentState::Running => "▶",
|
||||
AgentState::Completed => "✓",
|
||||
AgentState::Failed => "✗",
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if the current workflow looks like a company pipeline by
|
||||
/// checking agent names for division keywords.
|
||||
fn is_company_pipeline(agents: &[crate::app::workflow::engine::WorkflowAgent]) -> bool {
|
||||
if agents.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Company pipeline agents have names like "Strategy", "Engineering", etc.
|
||||
let division_keywords = ["Strategy", "Engineering", "Quality", "Security", "Documentation"];
|
||||
agents.iter().any(|a| {
|
||||
division_keywords.iter().any(|k| a.name.contains(k))
|
||||
})
|
||||
}
|
||||
|
||||
/// Render the workflow status panel.
|
||||
///
|
||||
/// Flow: build header lines (title + usage hints) → if agents exist,
|
||||
/// list each with its lifecycle state colour-coded → show findings
|
||||
/// count and session counters → fall back to an instruction paragraph
|
||||
/// when no agents have been spawned yet.
|
||||
///
|
||||
/// Return: nothing; draws directly into `frame` at `area`.
|
||||
pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
|
||||
use ratatui::layout::{Constraint, Direction, Layout};
|
||||
|
||||
let is_company = is_company_pipeline(&state.workflow_engine.agents);
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Theme::PRIMARY))
|
||||
.title(Span::styled(" ⚙ Workflow ", Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD)));
|
||||
.title({
|
||||
if is_company {
|
||||
Span::styled(" 🏢 Company Pipeline ", Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD))
|
||||
} else {
|
||||
Span::styled(" ⚙ Workflow ", Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
});
|
||||
|
||||
let inner = block.inner(area);
|
||||
frame.render_widget(block, area);
|
||||
@@ -44,14 +67,53 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
|
||||
])
|
||||
.split(inner);
|
||||
|
||||
// ── Header: usage hints ─────────────────────────────────────────────────
|
||||
let hint_lines = vec![
|
||||
Line::from(vec![
|
||||
// ── Header ─────────────────────────────────────────────────────────
|
||||
let mut header_lines = vec![];
|
||||
|
||||
if is_company {
|
||||
// Show the division pipeline header with visual arrows
|
||||
let agents = &state.workflow_engine.agents;
|
||||
let mut pipeline_spans: Vec<Span> = Vec::new();
|
||||
for (i, agent) in agents.iter().enumerate() {
|
||||
if i > 0 {
|
||||
pipeline_spans.push(Span::styled(" → ", Style::default().fg(Theme::DIM)));
|
||||
}
|
||||
let icon = div_icon(agent.status.state);
|
||||
let (color, modif) = match agent.status.state {
|
||||
AgentState::Idle => (Theme::DIM, Modifier::empty()),
|
||||
AgentState::Running => (Theme::WARNING, Modifier::BOLD),
|
||||
AgentState::Completed => (Theme::SUCCESS, Modifier::BOLD),
|
||||
AgentState::Failed => (Theme::ERROR, Modifier::BOLD),
|
||||
};
|
||||
pipeline_spans.push(Span::styled(
|
||||
format!("{} {} ", icon, agent.name.chars().take(12).collect::<String>()),
|
||||
Style::default().fg(color).add_modifier(modif),
|
||||
));
|
||||
}
|
||||
header_lines.push(Line::from(pipeline_spans));
|
||||
header_lines.push(Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().fg(Theme::DIM)),
|
||||
if state.turn_in_flight() {
|
||||
Span::styled("● Pipeline Running", Style::default().fg(Theme::WARNING).add_modifier(Modifier::BOLD))
|
||||
} else {
|
||||
Span::styled("● Pipeline Complete", Style::default().fg(Theme::SUCCESS))
|
||||
},
|
||||
Span::raw(" "),
|
||||
Span::styled(
|
||||
format!("Divisions: {} Findings: {}",
|
||||
state.workflow_engine.agents.len(),
|
||||
state.workflow_engine.findings.len(),
|
||||
),
|
||||
Style::default().fg(Theme::DIM),
|
||||
),
|
||||
]));
|
||||
} else {
|
||||
header_lines.push(Line::from(vec![
|
||||
Span::styled("/workflow run ", Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD)),
|
||||
Span::styled("<prompt>", Style::default().fg(Theme::DIM)),
|
||||
Span::styled(" · Esc to close", Style::default().fg(Theme::DIM)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
]));
|
||||
header_lines.push(Line::from(vec![
|
||||
Span::styled("Status: ", Style::default().fg(Theme::DIM)),
|
||||
if state.turn_in_flight() {
|
||||
Span::styled("● Running", Style::default().fg(Theme::WARNING).add_modifier(Modifier::BOLD))
|
||||
@@ -66,25 +128,23 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
|
||||
),
|
||||
Style::default().fg(Theme::DIM),
|
||||
),
|
||||
]),
|
||||
];
|
||||
let header = Paragraph::new(hint_lines);
|
||||
]));
|
||||
}
|
||||
let header = Paragraph::new(header_lines);
|
||||
frame.render_widget(header, chunks[0]);
|
||||
|
||||
// ── Body: agent list or placeholder ─────────────────────────────────────
|
||||
// ── Body: agent/division list ──────────────────────────────────────
|
||||
if state.workflow_engine.agents.is_empty() {
|
||||
// No agents yet — show session counters and a welcome message
|
||||
let session_lines = build_session_lines(state);
|
||||
let placeholder = Paragraph::new(session_lines).wrap(Wrap { trim: false });
|
||||
frame.render_widget(placeholder, chunks[1]);
|
||||
} else {
|
||||
// Build agent status list
|
||||
let items: Vec<ListItem> = state.workflow_engine.agents.iter().map(|agent| {
|
||||
let (state_str, state_color) = match agent.status.state {
|
||||
AgentState::Idle => ("Idle", Theme::DIM),
|
||||
AgentState::Running => ("Running…", Theme::WARNING),
|
||||
AgentState::Completed => ("Done ✓", Theme::SUCCESS),
|
||||
AgentState::Failed => ("Failed ✗", Theme::ERROR),
|
||||
AgentState::Idle => ("○ Idle", Theme::DIM),
|
||||
AgentState::Running => ("▶ Running…", Theme::WARNING),
|
||||
AgentState::Completed => ("✓ Done", Theme::SUCCESS),
|
||||
AgentState::Failed => ("✗ Failed", Theme::ERROR),
|
||||
};
|
||||
let duration_str = match (agent.status.started_at, agent.status.completed_at) {
|
||||
(Some(s), Some(e)) => format!(" {}ms", e.saturating_sub(s)),
|
||||
@@ -93,7 +153,7 @@ pub fn draw_workflow_panel(frame: &mut Frame, area: Rect, state: &crate::app::st
|
||||
};
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::styled(
|
||||
format!(" {:8} ", state_str),
|
||||
format!(" {:12} ", state_str),
|
||||
Style::default().fg(state_color).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::styled(
|
||||
@@ -159,7 +219,7 @@ fn build_session_lines(state: &crate::app::state::rest::AppStateRest) -> Vec<Lin
|
||||
|
||||
lines.push(Line::from(Span::raw("")));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Run a workflow to see agents here.",
|
||||
" Complex tasks auto-delegate to the company pipeline.",
|
||||
Style::default().fg(Theme::DIM).add_modifier(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user