diff --git a/README.md b/README.md index d40387c..346ddd1 100644 --- a/README.md +++ b/README.md @@ -322,6 +322,41 @@ G3 automatically saves session logs for each interaction in the `.g3/sessions/` The `.g3/` directory is created automatically on first use and is excluded from version control. +## Agent Mode + +Agent mode runs specialized AI agents with custom prompts tailored for specific tasks. Each agent has a distinct personality and focus area. + +### Built-in Agents + +g3 comes with several embedded agents that work out of the box: + +| Agent | Focus | +|-------|-------| +| **carmack** | Code readability and craft - simplifies, refactors, improves naming | +| **hopper** | Testing and quality - writes tests, finds edge cases | +| **euler** | Architecture and dependencies - analyzes structure, finds coupling | +| **lamport** | Concurrency and correctness - reviews async code, finds race conditions | +| **fowler** | Refactoring patterns - applies design patterns, reduces duplication | +| **breaker** | Adversarial testing - finds bugs, creates minimal repros | +| **scout** | Research - investigates APIs, libraries, approaches | + +### Usage + +```bash +# List all available agents +g3 --list-agents + +# Run an agent on the current project +g3 --agent carmack + +# Run an agent with a specific task +g3 --agent hopper "add tests for the parser module" +``` + +### Custom Agents + +Create custom agents by adding markdown files to `agents/.md` in your workspace. Workspace agents override embedded agents with the same name, allowing per-project customization. + ## Studio - Multi-Agent Workspace Manager Studio is a companion tool for managing multiple g3 agent sessions using git worktrees. Each session runs in an isolated worktree with its own branch, allowing multiple agents to work on the same codebase without conflicts. diff --git a/crates/g3-cli/src/agent_mode.rs b/crates/g3-cli/src/agent_mode.rs index 1bb9a39..b35edc5 100644 --- a/crates/g3-cli/src/agent_mode.rs +++ b/crates/g3-cli/src/agent_mode.rs @@ -9,6 +9,7 @@ use g3_core::Agent; use crate::project_files::{combine_project_content, read_agents_config, read_project_memory, read_project_readme}; use crate::simple_output::SimpleOutput; +use crate::embedded_agents::load_agent_prompt; use crate::ui_writer_impl::ConsoleUiWriter; /// Run agent mode - loads a specialized agent prompt and executes a single task. @@ -71,53 +72,17 @@ pub async fn run_agent_mode( output.print(""); } - // Load agent prompt from agents/.md - let agent_prompt_path = workspace_dir - .join("agents") - .join(format!("{}.md", agent_name)); + // Load agent prompt: workspace agents/.md first, then embedded fallback + let (agent_prompt, from_disk) = load_agent_prompt(agent_name, &workspace_dir).ok_or_else(|| { + anyhow::anyhow!( + "Agent '{}' not found.\nAvailable embedded agents: breaker, carmack, euler, fowler, hopper, lamport, scout\nOr create agents/{}.md in your workspace.", + agent_name, + agent_name + ) + })?; - // Also check in the g3 installation directory - let agent_prompt = if agent_prompt_path.exists() { - std::fs::read_to_string(&agent_prompt_path).map_err(|e| { - anyhow::anyhow!( - "Failed to read agent prompt from {:?}: {}", - agent_prompt_path, - e - ) - })? - } else { - // Try to find agents/ relative to the executable or in common locations - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - let possible_paths = [ - exe_dir - .as_ref() - .map(|d| d.join("agents").join(format!("{}.md", agent_name))), - Some(PathBuf::from(format!("agents/{}.md", agent_name))), - ]; - - let mut found_prompt = None; - for path_opt in possible_paths.iter().flatten() { - if path_opt.exists() { - found_prompt = Some(std::fs::read_to_string(path_opt).map_err(|e| { - anyhow::anyhow!("Failed to read agent prompt from {:?}: {}", path_opt, e) - })?); - break; - } - } - - found_prompt.ok_or_else(|| { - anyhow::anyhow!( - "Agent prompt not found: agents/{}.md\nSearched in: {:?} and current directory", - agent_name, - agent_prompt_path - ) - })? - }; - - output.print(&format!(">> agent mode | {}", agent_name)); + let source = if from_disk { "workspace" } else { "embedded" }; + output.print(&format!(">> agent mode | {} ({})", agent_name, source)); // Format workspace path, replacing home dir with ~ let workspace_display = { let path_str = workspace_dir.display().to_string(); diff --git a/crates/g3-cli/src/cli_args.rs b/crates/g3-cli/src/cli_args.rs index 6e6dd55..8e5b4ee 100644 --- a/crates/g3-cli/src/cli_args.rs +++ b/crates/g3-cli/src/cli_args.rs @@ -99,6 +99,10 @@ pub struct Cli { #[arg(long, value_name = "NAME", conflicts_with_all = ["autonomous", "auto", "chat", "planning"])] pub agent: Option, + /// List all available agents (embedded and workspace) + #[arg(long)] + pub list_agents: bool, + /// Skip session resumption and force a new session (for agent mode) #[arg(long)] pub new_session: bool, diff --git a/crates/g3-cli/src/embedded_agents.rs b/crates/g3-cli/src/embedded_agents.rs new file mode 100644 index 0000000..cef71b2 --- /dev/null +++ b/crates/g3-cli/src/embedded_agents.rs @@ -0,0 +1,115 @@ +//! Embedded agent prompts - compiled into the binary for portability. +//! +//! Agent prompts are embedded at compile time using `include_str!`. +//! This allows g3 to run on any repository without needing the agents/ directory. +//! +//! Priority order for loading agent prompts: +//! 1. Workspace `agents/.md` (allows per-project customization) +//! 2. Embedded prompts (fallback, always available) + +use std::collections::HashMap; +use std::path::Path; + +/// Embedded agent prompts, keyed by agent name. +static EMBEDDED_AGENTS: &[(&str, &str)] = &[ + ("breaker", include_str!("../../../agents/breaker.md")), + ("carmack", include_str!("../../../agents/carmack.md")), + ("euler", include_str!("../../../agents/euler.md")), + ("fowler", include_str!("../../../agents/fowler.md")), + ("hopper", include_str!("../../../agents/hopper.md")), + ("lamport", include_str!("../../../agents/lamport.md")), + ("scout", include_str!("../../../agents/scout.md")), +]; + +/// Get an embedded agent prompt by name. +pub fn get_embedded_agent(name: &str) -> Option<&'static str> { + EMBEDDED_AGENTS + .iter() + .find(|(n, _)| *n == name) + .map(|(_, content)| *content) +} + +/// Get all available embedded agent names. +pub fn list_embedded_agents() -> Vec<&'static str> { + EMBEDDED_AGENTS.iter().map(|(name, _)| *name).collect() +} + +/// Load an agent prompt, checking workspace first, then falling back to embedded. +/// +/// Returns the prompt content and a boolean indicating if it was loaded from disk (true) +/// or embedded (false). +pub fn load_agent_prompt(name: &str, workspace_dir: &Path) -> Option<(String, bool)> { + // First, try workspace agents/.md + let workspace_path = workspace_dir.join("agents").join(format!("{}.md", name)); + if workspace_path.exists() { + if let Ok(content) = std::fs::read_to_string(&workspace_path) { + return Some((content, true)); + } + } + + // Fall back to embedded prompt + get_embedded_agent(name).map(|content| (content.to_string(), false)) +} + +/// Get a map of all available agents (both embedded and from workspace). +pub fn get_available_agents(workspace_dir: &Path) -> HashMap { + let mut agents = HashMap::new(); + + // Add all embedded agents + for name in list_embedded_agents() { + agents.insert(name.to_string(), false); // false = embedded + } + + // Check for workspace agents (these override embedded) + let agents_dir = workspace_dir.join("agents"); + if agents_dir.is_dir() { + if let Ok(entries) = std::fs::read_dir(&agents_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().map_or(false, |ext| ext == "md") { + if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { + agents.insert(stem.to_string(), true); // true = from disk + } + } + } + } + } + + agents +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_embedded_agents_exist() { + // Verify all expected agents are embedded + let expected = ["breaker", "carmack", "euler", "fowler", "hopper", "lamport", "scout"]; + for name in expected { + assert!( + get_embedded_agent(name).is_some(), + "Agent '{}' should be embedded", + name + ); + } + } + + #[test] + fn test_list_embedded_agents() { + let agents = list_embedded_agents(); + assert!(agents.len() >= 7, "Should have at least 7 embedded agents"); + assert!(agents.contains(&"carmack")); + assert!(agents.contains(&"hopper")); + } + + #[test] + fn test_embedded_agent_content() { + // Verify the content looks reasonable + let carmack = get_embedded_agent("carmack").unwrap(); + assert!(carmack.contains("Carmack"), "Carmack prompt should mention Carmack"); + + let hopper = get_embedded_agent("hopper").unwrap(); + assert!(hopper.contains("Hopper"), "Hopper prompt should mention Hopper"); + } +} diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 070d6bd..bca0c81 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -4,6 +4,7 @@ pub mod filter_json; pub mod metrics; pub mod project_files; pub mod streaming_markdown; +pub mod embedded_agents; mod accumulative; mod agent_mode; @@ -44,6 +45,22 @@ pub async fn run() -> Result<()> { std::process::exit(1); } + // Check if --list-agents was requested + if cli.list_agents { + let workspace_dir = cli.workspace.clone().unwrap_or_else(|| std::env::current_dir().unwrap_or_default()); + let agents = embedded_agents::get_available_agents(&workspace_dir); + println!("Available agents:"); + let mut names: Vec<_> = agents.keys().collect(); + names.sort(); + for name in names { + let source = if agents[name] { "workspace" } else { "embedded" }; + println!(" {} ({})", name, source); + } + println!("\nUse: g3 --agent [task]"); + println!("Workspace agents override embedded agents with the same name."); + return Ok(()); + } + // Check if planning mode is enabled if cli.planning { let codepath = cli.codepath.clone();