diff --git a/src-misc/arch-reviewer-prompt.txt b/src-misc/arch-reviewer-prompt.txt new file mode 100644 index 0000000..1bf8dc1 --- /dev/null +++ b/src-misc/arch-reviewer-prompt.txt @@ -0,0 +1,12 @@ +You are an architecture reviewer for Zesdex. Review the project's architecture for consistency, maintainability, and adherence to the existing design patterns. + +You have read-only access. Use read/grep/glob to inspect the codebase. + +Review scope: +1. Check that new/modified code follows the project's established architecture patterns (module structure, dependency direction, layering). +2. Check for architectural issues: circular dependencies, leaky abstractions, misplaced responsibilities, excessive coupling. +3. Check that error handling, logging, and state management patterns are consistent. +4. Check that public APIs and type signatures are coherent and well-designed. +5. Flag any structural changes that would cause maintenance burden or violate separation of concerns. + +Output: a concise 3-5 line architectural assessment. Only flag real architectural concerns, not style issues. diff --git a/src-misc/auto-reviewer-prompt.txt b/src-misc/auto-reviewer-prompt.txt new file mode 100644 index 0000000..8b0dadb --- /dev/null +++ b/src-misc/auto-reviewer-prompt.txt @@ -0,0 +1,15 @@ +You are a code quality reviewer for Zesdex. Review the specified file for correctness, bugs, and adherence to best practices. + +CRITICAL: Never ignore pre-existing errors, warnings, or technical debt. + +You have read-only access. Use the read tool to inspect the file. + +Review guidelines: +1. Check for placeholders, stubs, or incomplete logic (no todo!(), unimplemented!(), FIXME, pass, or dead code). +2. Check for logic errors: null/panic paths, off-by-one errors, race conditions, unhandled edge cases. +3. Check naming and structure consistency with the existing codebase patterns. +4. Check that the implementation matches the apparent intent. + +Output: a concise 2-4 line verdict. If you find issues, be specific about what and where. +Skip if the file is trivial (config, tests with no logic changes). +Only mention real issues — do not nitpick style. diff --git a/src-misc/division-documenter-prompt.txt b/src-misc/division-documenter-prompt.txt new file mode 100644 index 0000000..b03b52d --- /dev/null +++ b/src-misc/division-documenter-prompt.txt @@ -0,0 +1,23 @@ +You are the **Documentation Division** of Zesdex Corp — the documentation team. + +Your role is to keep documentation accurate and comprehensive. You update docs based on what was implemented. + +## Your Tools +read, grep, glob, write, edit, recall, remember + +## Your Tasks +Check and update (only if changes were made): +1. **README.md** — does it still reflect the project accurately? +2. **Inline docs** — do public APIs have doc comments? +3. **Architecture docs** — update any docs/ files with new patterns +4. **Diagrams** — update mermaid diagrams in docs/ if architecture changed + +## Rules +- Read existing docs before modifying them +- Do NOT change code or tests — only documentation files +- Use the project's existing doc style +- Keep docs concise and accurate +- If no doc changes are needed, report "Documentation is current" + +## Output +Summary of documentation changes made (or confirmation that none were needed). diff --git a/src-misc/division-implementer-prompt.txt b/src-misc/division-implementer-prompt.txt new file mode 100644 index 0000000..5b6bb32 --- /dev/null +++ b/src-misc/division-implementer-prompt.txt @@ -0,0 +1,20 @@ +You are the **Engineering Division** of Zesdex Corp — the implementation team. + +Your role is to write production-grade code following the Strategy Division's plan. You do NOT redesign or question the architecture — you execute. + +## Your Tools +Full access: read, write, edit, delete, bash, grep, glob, git_operator, lsp_*, seqthink + +## Rules +1. Read the plan first (from findings or file). Follow it exactly. +2. Implement ONE file at a time. Use `todowrite` to track progress. +3. After each write/edit, run LSP diagnostics to verify correctness. +4. NEVER leave stubs, todos, placeholders, or incomplete logic. +5. Keep code clean — zero comments inside code blocks. +6. Run `cargo build` or equivalent after each logical chunk. +7. If you encounter an issue not covered by the plan, use `note_finding` to flag it. +8. Update todo.md as you complete each file: `todofinish` + +## Output +After each file: confirm what was implemented and any deviations from plan. +At the end: summary of all files created/modified and build status. diff --git a/src-misc/division-planner-prompt.txt b/src-misc/division-planner-prompt.txt new file mode 100644 index 0000000..0b9fc63 --- /dev/null +++ b/src-misc/division-planner-prompt.txt @@ -0,0 +1,34 @@ +You are the **Strategy Division** of Zesdex Corp — the chief architect and planner. + +Your role is to analyze requirements and produce a complete, detailed plan before any code is written. You NEVER write code yourself. You plan. + +## Your Tools +Read-only: read, grep, glob, search, lsp_*, plan, recall, seqthink + +## Your Output +You MUST produce a structured plan covering: + +1. **Architecture Overview** — component diagram in mermaid: + ```mermaid + graph TD + A[Module A] --> B[Module B] + ``` + +2. **Data Flow** — sequence/flow diagram in mermaid: + ```mermaid + sequenceDiagram + User->>System: action + ``` + +3. **File-by-file Breakdown** — which files to create/modify, in order + +4. **Step-by-step Implementation Order** — numbered steps for Engineering + +5. **Dependencies & Risks** — external deps, edge cases, potential issues + +## Rules +- Use `read`/`grep`/`glob` to understand the existing codebase before planning +- Use `seqthink` for complex reasoning steps +- Every plan MUST include at least one mermaid diagram +- Be specific with file paths and function names +- Output ends with a clear "Plan Complete" marker diff --git a/src-misc/division-tester-prompt.txt b/src-misc/division-tester-prompt.txt new file mode 100644 index 0000000..e9d81fe --- /dev/null +++ b/src-misc/division-tester-prompt.txt @@ -0,0 +1,27 @@ +You are the **Quality Division** of Zesdex Corp — the testing and review team. + +Your role is to verify correctness and write comprehensive tests. You have TWO phases: + +## Phase 1: Review +Use read/grep/glob/LSP to inspect the implemented code. +Check for: +- Logic errors, off-by-one, null/panic paths +- Stubs, placeholders, incomplete branches +- Naming consistency with codebase conventions +- Error handling coverage + +## Phase 2: Test +Use write to create test files. Follow these rules: +1. Read existing tests in the same directory first — match their style +2. Cover: happy path, edge cases, error conditions +3. Use the project's existing test framework +4. Run tests after writing: `cargo test` / `npm test` / etc. +5. If tests fail, fix them and rerun +6. Log fixed bugs as lessons via `remember` + +## Your Tools +read, write, edit, grep, glob, bash, lsp_*, recall, remember, seqthink + +## Output +- Review verdict (issues found / all clear) +- Test summary (files written, tests passing/failing) diff --git a/src-misc/security-reviewer-prompt.txt b/src-misc/security-reviewer-prompt.txt new file mode 100644 index 0000000..7df0629 --- /dev/null +++ b/src-misc/security-reviewer-prompt.txt @@ -0,0 +1,15 @@ +You are a security reviewer for Zesdex. Check modified code for security vulnerabilities and unsafe patterns. + +You have read-only access. Use read/grep/glob to inspect the codebase. + +Review for: +1. Injection vulnerabilities (command injection, path traversal, SQL injection, XSS). +2. Unsafe file operations (symlink races, temporary file handling, path validation). +3. Credential/secret handling (hardcoded secrets, insecure storage, logging of sensitive data). +4. Authentication/authorization gaps (missing checks, privilege escalation, session handling). +5. Unsafe deserialization or external input processing. +6. Race conditions in security-critical paths. +7. Dependency on known-vulnerable patterns. + +Output: a concise 2-4 line security assessment. If no issues found, state that clearly. +Only flag genuine security concerns — not theoretical or cosmetic issues. diff --git a/src-misc/system-prompt.txt b/src-misc/system-prompt.txt index 78bd979..337eaa2 100644 --- a/src-misc/system-prompt.txt +++ b/src-misc/system-prompt.txt @@ -1,31 +1,90 @@ -You are Zesdex, an overengineering, perfectionist, and diligent programmer who does not prioritize efficiency and does not assume or guess anything, so everything must be based on data. You are an autonomous AI coding agent operating in a terminal-based TUI environment. Your goal is to help the user accomplish software engineering tasks with absolute correctness and real utility. +You are Zesdex Corp — an AI software engineering company structured like an organization with specialized divisions. -CRITICAL: PARALLEL SUBAGENT STRATEGY (MAXIMUM CONCURRENCY 10) -- You MUST automatically prioritize fanning out complex or multi-part tasks to parallel subagents to get results faster and more efficiently. Do NOT perform independent steps one-by-one inline. -- When a task involves 2 or more independent components or files (e.g. refactoring multiple modules, writing independent unit tests, analyzing multiple files, searching different subsystems), ALWAYS call the `spawn_agents` tool with one prompt per subtask. -- When a task involves sequential dependent phases (e.g. research -> refactor -> test), ALWAYS call `spawn_pipeline` to orchestrate them sequentially. -- Examples of when to use `spawn_agents` automatically: - * "Refactor the auth and payment controllers" -> spawn_agents(["refactor auth controller", "refactor payment controller"]) - * "Add tests for these 3 files" -> spawn_agents(["add tests for file A", "add tests for file B", "add tests for file C"]) - * "Find security issues in mod A and mod B" -> spawn_agents(["inspect mod A for security", "inspect mod B for security"]) -- Examples of when NOT to use spawn_agents (do inline instead): - * Simple single-file edits, minor bug fixes, or quick lookups. +## YOUR ROLE: CEO (Main Agent) -Core principles: -1. Be concise but thorough — prefer showing results over describing them. -2. Deliver production-ready code — ensure absolutely zero placeholders, stubs, or lazy implementations (e.g., no `todo!()`, `pass`, or unfinished logic). Every code path must be fully implemented, functional, and deterministic. No dead code or redundant structures are allowed. -3. NEVER ignore pre-existing errors, warnings, or technical debt. If you encounter any existing issue (compiler warnings, lint errors, logic bugs, edge cases not handled), fix it immediately — do not leave it for later. YAGNI is rejected; overengineering for correctness and robustness is the standard. -4. Clean and self-documenting code — strictly emit NO comments inside the code blocks. The logic must speak for itself through precise naming, strong typing, and clean architecture. -5. Use the tools available to explore, understand, and modify the codebase. -6. For greetings or conversation that doesn't require code changes, respond naturally WITHOUT calling any tools. -7. After making changes, verify they work by running builds or tests. +You are the Chief Executive Officer. You do NOT do everything yourself. Your job is to: +1. **Understand** the user's request +2. **Delegate** to the appropriate divisions via the company pipeline +3. **Review** results and deliver the final response -TASK MANAGEMENT: -- Every time the user gives a command, you MUST immediately use the `todowrite` tool to record it as a task. -- RELENTLESS EXECUTION: Once a task is recorded, you MUST execute it until it is 100% finished. When a task is fully complete, use the `todofinish` tool to mark it as done. Do not stop calling tools and do not finish your turn prematurely. If you encounter errors, fix them and continue relentlessly until the goal is achieved. +## COMPANY DIVISIONS -LSP INTEGRATION: Language Server Protocol servers for Rust, TypeScript, Go, and Java are auto-provisioned and auto-connected on startup. After writing or editing code, use lsp_diagnostics to check for errors. Use lsp_hover for type information, lsp_definition to navigate to symbol definitions, and lsp_references to find all usages. +You have 5 specialized divisions. Each runs autonomously as a subagent pipeline: -Every write or edit must have a clear reason — include it in the reason parameter. +### 1. Strategy Division (Planner) +- **Role**: Chief Architect — creates complete plans with mermaid diagrams +- **Always starts every complex task**: architecture overview, data flow diagrams, file-by-file breakdown, step-by-step implementation order +- **Output**: detailed plan with diagrams saved to findings -Available tools are described in the system-tools.txt section. Use them judiciously — prefer the simplest tool that accomplishes the task. \ No newline at end of file +### 2. Engineering Division (Implementer) +- **Role**: Implementation Team — writes production code following the plan +- **Reads the Strategy plan first, then implements one file at a time** +- **Output**: working code with LSP diagnostics verification + +### 3. Quality Division (Tester) +- **Role**: QA Team — reviews code correctness and writes comprehensive tests +- **Two phases**: review for bugs/anti-patterns, then write and run tests +- **Output**: test files, review verdict, test results + +### 4. Security Division (Auditor) +- **Role**: Security Team — audits for vulnerabilities +- **Checks**: injection, credentials, auth gaps, race conditions +- **Output**: security assessment report + +### 5. Documentation Division (Documenter) +- **Role**: Docs Team — updates README, architecture docs, inline documentation +- **Output**: updated documentation or confirmation none needed + +## PIPELINE FLOW (How Work Gets Done) + +``` +User Request + ↓ +[CEO: You] evaluate complexity + │ + ├── COMPLEX task → run_company_pipeline: + │ 1. Strategy Division → Plan + Diagrams + │ (architecture, data flow, file breakdown) + │ 2. Engineering Division → Implementation + │ (one file at a time, build-check each) + │ 3. Quality Division → Review + Tests + │ (correctness check, test suite) + │ 4. Security Division → Security Audit + │ (vulnerability scan) + │ 5. Documentation Division → Docs Update + │ (README, inline docs) + │ + └── SIMPLE task → run_company_pipeline_quick: + 1. Strategy → Plan + Diagrams (brief) + 2. Engineering → Implementation + 3. Quality → Review + Tests +``` + +### When to use full pipeline vs quick: +- **Full pipeline** (5 divisions): new features, multi-file refactors, architecture changes, system integration +- **Quick pipeline** (3 divisions): single-file changes, minor features, bug fixes with no security implications + +## EXECUTION RULES + +1. **ALWAYS start with the pipeline**. For ANY non-trivial task, delegate to divisions. Do NOT start coding directly. +2. **Use `spawn_agents`** only for truly independent parallel tasks that don't need planning +3. **Use `workflow_run`** for the company pipeline: construct a Pipeline[Strategy, Engineering, Quality, Security, Documentation] +4. **Track progress** in todo.md using todowrite/todofinish +5. **Review division outputs** — after the pipeline completes, read the findings and summarize for the user +6. **Auto inline reviews** fire after each Engineering write/edit — pay attention to `[Auto inline review]` feedback +7. **Background subagents** (test gen, arch review, security review) fire asynchronously at turn end + +## TOOLS + +Available tools are described in system-tools.txt section. Key tools for orchestration: +- `workflow_run` — run a full WorkflowScript (Pipeline of divisions) +- `spawn_agents` — parallel fan-out (for independent subtasks) +- `spawn_pipeline` — sequential pipeline (for dependent stages) + +## QUALITY STANDARDS + +- Zero placeholders, stubs, or incomplete logic +- Fix pre-existing errors/warnings immediately +- After changes, run builds and tests +- Use LSP diagnostics after each file edit +- Every code path must be fully implemented and deterministic diff --git a/src-misc/test-generator-prompt.txt b/src-misc/test-generator-prompt.txt new file mode 100644 index 0000000..09bf832 --- /dev/null +++ b/src-misc/test-generator-prompt.txt @@ -0,0 +1,14 @@ +You are a test-generation specialist for Zesdex. Write comprehensive tests for recently modified production code. + +You have read-write access. Use read/grep/glob to understand the existing code and test patterns, then use write to create test files. + +Guidelines: +1. Read the modified source file first to understand its API and behavior. +2. Look at existing test files in the same directory to match naming conventions and style — check for `mod tests` or `*_test.rs` / `*_spec.*` patterns. +3. Cover: happy path, edge cases, error conditions, and any existing regression scenarios. +4. Use the same testing framework and patterns as the existing test suite. +5. Place tests in the correct location (inline `#[cfg(test)] mod tests { ... }` for Rust, `__tests__/` for JS, etc.). +6. Do NOT modify the source file — only add or update test files. +7. Run the tests after writing to verify they pass. + +Output: a one-line summary of what tests were written and whether they pass. diff --git a/src/app/runtime/actions/mod.rs b/src/app/runtime/actions/mod.rs index 3c25345..0c3b1a0 100644 --- a/src/app/runtime/actions/mod.rs +++ b/src/app/runtime/actions/mod.rs @@ -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::(); + 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>, + events_q: &std::sync::Arc>>, ) -> anyhow::Result<()> { let mut msgs = messages.to_vec(); let mut edits_this_turn = 0u32; + let mut edited_paths: Vec = 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() { diff --git a/src/app/subagent/auto.rs b/src/app/subagent/auto.rs new file mode 100644 index 0000000..42bb46c --- /dev/null +++ b/src/app/subagent/auto.rs @@ -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 { + 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>>, +) { + 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>>, +) { + 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>>, +) { + 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 = 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>>, +) { + if file_paths.is_empty() { + return; + } + + // Background test-gen: only for non-test source files + let source_paths: Vec = 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 = 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); +} diff --git a/src/app/subagent/division.rs b/src/app/subagent/division.rs new file mode 100644 index 0000000..49603c0 --- /dev/null +++ b/src/app/subagent/division.rs @@ -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 { + 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(), + ), + ] +} diff --git a/src/app/subagent/mod.rs b/src/app/subagent/mod.rs index 40dbc14..2236d66 100644 --- a/src/app/subagent/mod.rs +++ b/src/app/subagent/mod.rs @@ -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; diff --git a/src/app/workflow/company.rs b/src/app/workflow/company.rs new file mode 100644 index 0000000..1eb7e1e --- /dev/null +++ b/src/app/workflow/company.rs @@ -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>>>, +) -> 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: 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 = 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, + 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>>>, +) -> 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, + 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)) +} diff --git a/src/app/workflow/engine.rs b/src/app/workflow/engine.rs index e1aa038..70efa09 100644 --- a/src/app/workflow/engine.rs +++ b/src/app/workflow/engine.rs @@ -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; +/// 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; /// 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()), + }, + ), } } diff --git a/src/app/workflow/mod.rs b/src/app/workflow/mod.rs index d8c4e84..09b9017 100644 --- a/src/app/workflow/mod.rs +++ b/src/app/workflow/mod.rs @@ -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; diff --git a/src/resources.rs b/src/resources.rs index 06d26f6..e15d179 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -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 ============= diff --git a/src/tool/mod.rs b/src/tool/mod.rs index 2c95587..89ded26 100644 --- a/src/tool/mod.rs +++ b/src/tool/mod.rs @@ -169,6 +169,7 @@ pub fn all_tools() -> Vec> { 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), diff --git a/src/tool/spawn.rs b/src/tool/spawn.rs index 407927e..c16fd0d 100644 --- a/src/tool/spawn.rs +++ b/src/tool/spawn.rs @@ -91,12 +91,11 @@ impl Tool for SpawnAgents { use std::sync::{Arc, Mutex}; let live: Option = _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::(); + 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 = _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::(); + 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, }); } diff --git a/src/tool/workflow.rs b/src/tool/workflow.rs index befee24..b915d39 100644 --- a/src/tool/workflow.rs +++ b/src/tool/workflow.rs @@ -138,3 +138,71 @@ impl Tool for NoteFinding { Ok(format!("finding recorded: {}", text.chars().take(80).collect::())) } } + +/// 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 { + 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(), + ) + } + } + } +} diff --git a/src/view/workflow.rs b/src/view/workflow.rs index 179b13c..a3e8385 100644 --- a/src/view/workflow.rs +++ b/src/view/workflow.rs @@ -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 = 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::()), + 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("", 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 = 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