Fix --project flag not working in agent mode
- Add CommonFlags struct to group flags that apply across all modes - Refactor run_agent_mode() to accept CommonFlags instead of individual params - Add project loading logic for agent chat mode - Add integration tests for --project with agent mode This refactor prevents future bugs where new flags work in one mode but are forgotten in another.
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
//! Agent mode for G3 CLI - runs specialized agents with custom prompts.
|
//! Agent mode for G3 CLI - runs specialized agents with custom prompts.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::path::PathBuf;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use g3_core::ui_writer::UiWriter;
|
use g3_core::ui_writer::UiWriter;
|
||||||
@@ -15,21 +14,17 @@ use crate::embedded_agents::load_agent_prompt;
|
|||||||
use crate::ui_writer_impl::ConsoleUiWriter;
|
use crate::ui_writer_impl::ConsoleUiWriter;
|
||||||
use crate::interactive::run_interactive;
|
use crate::interactive::run_interactive;
|
||||||
use crate::template::process_template;
|
use crate::template::process_template;
|
||||||
|
use crate::project::{Project, load_and_validate_project};
|
||||||
|
use crate::cli_args::CommonFlags;
|
||||||
|
|
||||||
/// Run agent mode - loads a specialized agent prompt and executes a single task.
|
/// Run agent mode - loads a specialized agent prompt and executes a single task.
|
||||||
|
///
|
||||||
|
/// Uses `CommonFlags` for flags that apply across all modes, ensuring consistency.
|
||||||
pub async fn run_agent_mode(
|
pub async fn run_agent_mode(
|
||||||
agent_name: &str,
|
agent_name: &str,
|
||||||
workspace: Option<PathBuf>,
|
|
||||||
config_path: Option<&str>,
|
|
||||||
_quiet: bool,
|
|
||||||
new_session: bool,
|
|
||||||
task: Option<String>,
|
task: Option<String>,
|
||||||
chrome_headless: bool,
|
|
||||||
safari: bool,
|
|
||||||
chat: bool,
|
chat: bool,
|
||||||
include_prompt_path: Option<PathBuf>,
|
flags: CommonFlags,
|
||||||
no_auto_memory: bool,
|
|
||||||
acd_enabled: bool,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use g3_core::find_incomplete_agent_session;
|
use g3_core::find_incomplete_agent_session;
|
||||||
use g3_core::get_agent_system_prompt;
|
use g3_core::get_agent_system_prompt;
|
||||||
@@ -40,7 +35,7 @@ pub async fn run_agent_mode(
|
|||||||
let output = SimpleOutput::new();
|
let output = SimpleOutput::new();
|
||||||
|
|
||||||
// Determine workspace directory (current dir if not specified)
|
// Determine workspace directory (current dir if not specified)
|
||||||
let workspace_dir = workspace.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
let workspace_dir = flags.workspace.clone().unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||||||
|
|
||||||
// Change to the workspace directory first so session scanning works correctly
|
// Change to the workspace directory first so session scanning works correctly
|
||||||
std::env::set_current_dir(&workspace_dir)?;
|
std::env::set_current_dir(&workspace_dir)?;
|
||||||
@@ -49,7 +44,7 @@ pub async fn run_agent_mode(
|
|||||||
// Skip session resume entirely when in chat mode (--agent --chat)
|
// Skip session resume entirely when in chat mode (--agent --chat)
|
||||||
let resuming_session = if chat {
|
let resuming_session = if chat {
|
||||||
None // Chat mode always starts fresh
|
None // Chat mode always starts fresh
|
||||||
} else if new_session {
|
} else if flags.new_session {
|
||||||
if !chat {
|
if !chat {
|
||||||
output.print("\n🆕 Starting new session (--new-session flag set)");
|
output.print("\n🆕 Starting new session (--new-session flag set)");
|
||||||
output.print("");
|
output.print("");
|
||||||
@@ -97,16 +92,16 @@ pub async fn run_agent_mode(
|
|||||||
print_workspace_path(&workspace_dir);
|
print_workspace_path(&workspace_dir);
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
let mut config = g3_config::Config::load(config_path)?;
|
let mut config = g3_config::Config::load(flags.config.as_deref())?;
|
||||||
|
|
||||||
// Apply chrome-headless flag override
|
// Apply chrome-headless flag override
|
||||||
if chrome_headless {
|
if flags.chrome_headless {
|
||||||
config.webdriver.enabled = true;
|
config.webdriver.enabled = true;
|
||||||
config.webdriver.browser = g3_config::WebDriverBrowser::ChromeHeadless;
|
config.webdriver.browser = g3_config::WebDriverBrowser::ChromeHeadless;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply safari flag override
|
// Apply safari flag override
|
||||||
if safari {
|
if flags.safari {
|
||||||
config.webdriver.enabled = true;
|
config.webdriver.enabled = true;
|
||||||
config.webdriver.browser = g3_config::WebDriverBrowser::Safari;
|
config.webdriver.browser = g3_config::WebDriverBrowser::Safari;
|
||||||
}
|
}
|
||||||
@@ -120,10 +115,10 @@ pub async fn run_agent_mode(
|
|||||||
let memory_content_opt = read_workspace_memory(&workspace_dir);
|
let memory_content_opt = read_workspace_memory(&workspace_dir);
|
||||||
|
|
||||||
// Read include prompt early so we can show it in the status line
|
// Read include prompt early so we can show it in the status line
|
||||||
let include_prompt = read_include_prompt(include_prompt_path.as_deref());
|
let include_prompt = read_include_prompt(flags.include_prompt.as_deref());
|
||||||
|
|
||||||
// Build and print status line showing what was loaded
|
// Build and print status line showing what was loaded
|
||||||
let include_filename = include_prompt_path.as_ref()
|
let include_filename = flags.include_prompt.as_ref()
|
||||||
.filter(|_| include_prompt.is_some())
|
.filter(|_| include_prompt.is_some())
|
||||||
.and_then(|p| p.file_name())
|
.and_then(|p| p.file_name())
|
||||||
.map(|s| s.to_string_lossy().to_string());
|
.map(|s| s.to_string_lossy().to_string());
|
||||||
@@ -181,10 +176,10 @@ pub async fn run_agent_mode(
|
|||||||
|
|
||||||
// Auto-memory is enabled by default in agent mode (unless --no-auto-memory is set)
|
// Auto-memory is enabled by default in agent mode (unless --no-auto-memory is set)
|
||||||
// This prompts the LLM to save discoveries to workspace memory after each turn
|
// This prompts the LLM to save discoveries to workspace memory after each turn
|
||||||
agent.set_auto_memory(!no_auto_memory);
|
agent.set_auto_memory(!flags.no_auto_memory);
|
||||||
|
|
||||||
// Enable ACD (Aggressive Context Dehydration) if requested
|
// Enable ACD (Aggressive Context Dehydration) if requested
|
||||||
if acd_enabled {
|
if flags.acd {
|
||||||
agent.set_acd_enabled(true);
|
agent.set_acd_enabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,15 +239,43 @@ pub async fn run_agent_mode(
|
|||||||
|
|
||||||
// If chat mode is enabled, run interactive loop instead of single task
|
// If chat mode is enabled, run interactive loop instead of single task
|
||||||
if chat {
|
if chat {
|
||||||
|
// Load project if --project flag was specified
|
||||||
|
let initial_project: Option<Project> = if let Some(ref proj_path) = flags.project {
|
||||||
|
match load_and_validate_project(&proj_path.to_string_lossy(), &workspace_dir) {
|
||||||
|
Ok(cli_project) => {
|
||||||
|
// Set project content in agent's system message
|
||||||
|
if agent.set_project_content(Some(cli_project.content.clone())) {
|
||||||
|
// Set project path on UI writer for path shortening
|
||||||
|
let project_name = cli_project.path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("project")
|
||||||
|
.to_string();
|
||||||
|
agent.ui_writer().set_project_path(cli_project.path.clone(), project_name);
|
||||||
|
Some(cli_project)
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: Failed to set project content in agent context.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading project: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
return run_interactive(
|
return run_interactive(
|
||||||
agent,
|
agent,
|
||||||
false, // show_prompt
|
false, // show_prompt
|
||||||
false, // show_code
|
false, // show_code
|
||||||
combined_content,
|
combined_content,
|
||||||
&workspace_dir,
|
&workspace_dir,
|
||||||
new_session,
|
flags.new_session,
|
||||||
Some(agent_name), // agent name for prompt (e.g., "butler>")
|
Some(agent_name), // agent name for prompt (e.g., "butler>")
|
||||||
None, // initial_project (not supported in agent mode yet)
|
initial_project,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,35 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Flags that apply across all execution modes (interactive, agent, autonomous).
|
||||||
|
///
|
||||||
|
/// When adding a new flag that should work in all modes, add it here instead of
|
||||||
|
/// passing individual parameters to mode functions. This prevents bugs where a
|
||||||
|
/// flag works in one mode but is forgotten in another.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct CommonFlags {
|
||||||
|
/// Workspace directory
|
||||||
|
pub workspace: Option<PathBuf>,
|
||||||
|
/// Configuration file path
|
||||||
|
pub config: Option<String>,
|
||||||
|
/// Skip session resumption and force a new session
|
||||||
|
pub new_session: bool,
|
||||||
|
/// Suppress output/logging
|
||||||
|
pub quiet: bool,
|
||||||
|
/// Use Chrome in headless mode for WebDriver
|
||||||
|
pub chrome_headless: bool,
|
||||||
|
/// Use Safari for WebDriver
|
||||||
|
pub safari: bool,
|
||||||
|
/// Include additional prompt content from a file
|
||||||
|
pub include_prompt: Option<PathBuf>,
|
||||||
|
/// Disable automatic memory update reminder
|
||||||
|
pub no_auto_memory: bool,
|
||||||
|
/// Enable aggressive context dehydration
|
||||||
|
pub acd: bool,
|
||||||
|
/// Load a project from the given path at startup
|
||||||
|
pub project: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser, Clone)]
|
#[derive(Parser, Clone)]
|
||||||
#[command(name = "g3")]
|
#[command(name = "g3")]
|
||||||
#[command(about = "A modular, composable AI coding agent")]
|
#[command(about = "A modular, composable AI coding agent")]
|
||||||
@@ -127,3 +156,22 @@ pub struct Cli {
|
|||||||
#[arg(long, value_name = "PATH")]
|
#[arg(long, value_name = "PATH")]
|
||||||
pub project: Option<PathBuf>,
|
pub project: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
/// Extract common flags that apply across all execution modes.
|
||||||
|
/// This ensures flags like --project, --acd, --include-prompt work consistently.
|
||||||
|
pub fn common_flags(&self) -> CommonFlags {
|
||||||
|
CommonFlags {
|
||||||
|
workspace: self.workspace.clone(),
|
||||||
|
config: self.config.clone(),
|
||||||
|
new_session: self.new_session,
|
||||||
|
quiet: self.quiet,
|
||||||
|
chrome_headless: self.chrome_headless,
|
||||||
|
safari: self.safari,
|
||||||
|
include_prompt: self.include_prompt.clone(),
|
||||||
|
no_auto_memory: self.no_auto_memory,
|
||||||
|
acd: self.acd,
|
||||||
|
project: self.project.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,17 +90,9 @@ pub async fn run() -> Result<()> {
|
|||||||
if let Some(agent_name) = &cli.agent {
|
if let Some(agent_name) = &cli.agent {
|
||||||
return run_agent_mode(
|
return run_agent_mode(
|
||||||
agent_name,
|
agent_name,
|
||||||
cli.workspace.clone(),
|
|
||||||
cli.config.as_deref(),
|
|
||||||
cli.quiet,
|
|
||||||
cli.new_session,
|
|
||||||
cli.task.clone(),
|
cli.task.clone(),
|
||||||
cli.chrome_headless,
|
|
||||||
cli.safari,
|
|
||||||
cli.chat,
|
cli.chat,
|
||||||
cli.include_prompt.clone(),
|
cli.common_flags(),
|
||||||
cli.no_auto_memory,
|
|
||||||
cli.acd,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,3 +305,33 @@ fn test_no_auto_memory_in_help_output() {
|
|||||||
"Help output should mention --no-auto-memory flag"
|
"Help output should mention --no-auto-memory flag"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Test: Project option is accepted (including with agent mode)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_project_option_accepted() {
|
||||||
|
let output = Command::new(get_g3_binary())
|
||||||
|
.args(["--project", "/tmp/myproject", "--help"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to execute g3 with project option");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"--project option should be recognized"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_project_option_with_agent_mode_accepted() {
|
||||||
|
let output = Command::new(get_g3_binary())
|
||||||
|
.args(["--agent", "butler", "--chat", "--project", "/tmp/myproject", "--help"])
|
||||||
|
.output()
|
||||||
|
.expect("Failed to execute g3 with agent and project options");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"--project option should work with --agent --chat"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user