Compare commits

..

15 Commits

Author SHA1 Message Date
Jochen
a1ad94ed75 Added comment & example for native flow
detailed examples for using code_search tool for native tool use.
2025-11-02 21:02:43 +11:00
Jochen
982c0bbfb3 amend instructions for tool use 2025-11-01 15:52:08 +11:00
Jochen
ad9ba5e5d8 added ast-grep use
g3 tool use of ast-grep command with batching for faster code exploration.
2025-11-01 14:59:55 +11:00
Dhanji Prasanna
f89bbfc89a fix final_output bug 2025-10-31 14:48:36 +11:00
Dhanji Prasanna
11eb01e04d add back progress bar to cli 2025-10-31 14:28:35 +11:00
Dhanji Prasanna
bdaacfd051 fix for duplicate messages at end 2025-10-31 13:34:36 +11:00
Dhanji R. Prasanna
92ae776510 Merge pull request #14 from dhanji/micn/always-coach-player
always coach player
2025-10-31 12:52:17 +11:00
Michael Neale
c42e0bce54 use --auto flag 2025-10-31 11:42:42 +11:00
Michael Neale
b529d7f814 add ability to use slash commands, and also go back to chat in context of player auto mode 2025-10-29 18:09:13 +11:00
Michael Neale
9752e81489 cleanup 2025-10-29 14:53:10 +11:00
Michael Neale
63c2aff7ba clearer 2025-10-29 14:47:25 +11:00
Michael Neale
aa4a0267ea can interrupt now 2025-10-29 13:29:03 +11:00
Michael Neale
6cfa1e225c can cancell acc mode 2025-10-29 13:13:41 +11:00
Michael Neale
f53cd8e8f3 requirements always 2025-10-29 13:09:15 +11:00
Michael Neale
45bffc40da coach player always when starting 2025-10-29 13:04:16 +11:00
10 changed files with 2101 additions and 933 deletions

32
Cargo.lock generated
View File

@@ -990,7 +990,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users 0.5.2",
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1062,7 +1062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1391,6 +1391,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"serde_yaml",
"shellexpand",
"thiserror 1.0.69",
"tokio",
@@ -2333,7 +2334,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -2904,7 +2905,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.11.0",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3078,6 +3079,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -3292,7 +3306,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.2",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -3667,6 +3681,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "url"
version = "2.5.7"
@@ -3935,7 +3955,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.61.2",
]
[[package]]

View File

@@ -14,7 +14,6 @@ The heart of the agent system, containing:
- **Context Window Management**: Intelligent tracking of token usage with context thinning (50-80%) and auto-summarization at 80% capacity
- **Tool System**: Built-in tools for file operations, shell commands, computer control, TODO management, and structured output
- **Streaming Response Parser**: Real-time parsing of LLM responses with tool call detection and execution
- **Smart Project Awareness**: Automatically detects and respects `.gitignore` patterns, informing the agent about ignored files
- **Task Execution**: Support for single and iterative task execution with automatic retry logic
#### **g3-providers**
@@ -98,10 +97,7 @@ These commands give you fine-grained control over context management, allowing y
- **Final Output**: Formatted result presentation
### Provider Flexibility
### Smart Project Awareness
- Automatically detects and respects `.gitignore` when present
- Support for multiple LLM providers through a unified interface
- Hot-swappable providers without code changes
- Provider-specific optimizations and feature support
- Local model support for offline operation
@@ -136,6 +132,40 @@ G3 is designed for:
## Getting Started
### Default Mode: Accumulative Autonomous
The default interactive mode now uses **accumulative autonomous mode**, which combines the best of interactive and autonomous workflows:
```bash
# Simply run g3 in any directory
g3
# You'll be prompted to describe what you want to build
# Each input you provide:
# 1. Gets added to accumulated requirements
# 2. Automatically triggers autonomous mode (coach-player loop)
# 3. Implements your requirements iteratively
# Example session:
requirement> create a simple web server in Python with Flask
# ... autonomous mode runs and implements it ...
requirement> add a /health endpoint that returns JSON
# ... autonomous mode runs again with both requirements ...
```
### Other Modes
```bash
# Single-shot mode (one task, then exit)
g3 "implement a function to calculate fibonacci numbers"
# Traditional autonomous mode (reads requirements.md)
g3 --autonomous
# Traditional chat mode (simple interactive chat without autonomous runs)
g3 --chat
```
```bash
# Build the project
cargo build --release

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use crossterm::style::{Color, SetForegroundColor, ResetColor};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
@@ -174,7 +175,7 @@ mod machine_ui_writer;
use machine_ui_writer::MachineUiWriter;
use ui_writer_impl::ConsoleUiWriter;
#[derive(Parser)]
#[derive(Parser, Clone)]
#[command(name = "g3")]
#[command(about = "A modular, composable AI coding agent")]
#[command(version)]
@@ -214,9 +215,9 @@ pub struct Cli {
#[arg(long, value_name = "TEXT")]
pub requirements: Option<String>,
/// Interactive mode: prompt for requirements and save to requirements.md before starting autonomous mode
/// Enable accumulative autonomous mode (default is chat mode)
#[arg(long)]
pub interactive_requirements: bool,
pub auto: bool,
/// Enable machine-friendly output mode with JSON markers and stats
#[arg(long)]
@@ -309,112 +310,6 @@ pub async fn run() -> Result<()> {
// Create project model
let project = if cli.autonomous {
// Handle interactive requirements mode with AI enhancement
if cli.interactive_requirements {
println!("\n📝 Interactive Requirements Mode");
println!("================================\n");
println!("Describe what you want to build (can be brief):");
println!("Press Ctrl+D (Unix) or Ctrl+Z (Windows) when done.\n");
use std::io::{self, Read, Write};
let mut requirements_input = String::new();
io::stdin().read_to_string(&mut requirements_input)?;
if requirements_input.trim().is_empty() {
anyhow::bail!("No requirements provided. Exiting.");
}
println!("\n🤖 Enhancing your requirements with AI...\n");
// Create a temporary agent to enhance the requirements
let temp_config = Config::load_with_overrides(
cli.config.as_deref(),
cli.provider.clone(),
cli.model.clone(),
)?;
let ui_writer = ConsoleUiWriter::new();
let mut temp_agent = Agent::new_with_readme_and_quiet(
temp_config,
ui_writer,
None,
true, // quiet mode
).await?;
// Craft the enhancement prompt
let enhancement_prompt = format!(
r#"You are a requirements analyst. Take this brief user input and expand it into a structured requirements document.
USER INPUT:
{}
Create a professional requirements document with:
1. A clear project title (# heading)
2. An overview section explaining what will be built
3. Organized requirements (functional, technical, quality)
4. Acceptance criteria
5. Any technical constraints or preferences mentioned
Format as proper markdown. Be specific and actionable. If the user's input is vague, make reasonable assumptions but keep it focused on what they described.
Output ONLY the markdown content, no explanations or meta-commentary."#,
requirements_input.trim()
);
// Execute enhancement task
let result = temp_agent
.execute_task_with_timing(&enhancement_prompt, None, false, false, false, false)
.await?;
let enhanced_requirements = result.response.trim().to_string();
// Show the enhanced requirements
println!("\n📋 Enhanced Requirements Document:");
println!("{}\n", "=".repeat(60));
println!("{}", enhanced_requirements);
println!("{}\n", "=".repeat(60));
// Ask for confirmation
println!("\n❓ Is this requirements document acceptable?");
println!(" [y] Yes, proceed with autonomous mode");
println!(" [e] Edit and save manually");
println!(" [n] No, cancel\n");
print!("Your choice (y/e/n): ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim().to_lowercase();
let requirements_path = workspace_dir.join("requirements.md");
match choice.as_str() {
"y" | "yes" => {
// Save enhanced requirements
std::fs::write(&requirements_path, &enhanced_requirements)?;
println!("\n✅ Requirements saved to: {}", requirements_path.display());
println!("🚀 Starting autonomous mode...\n");
}
"e" | "edit" => {
// Save enhanced requirements for manual editing
std::fs::write(&requirements_path, &enhanced_requirements)?;
println!("\n✅ Requirements saved to: {}", requirements_path.display());
println!("📝 Please edit the file and run: g3 --autonomous");
println!(" Exiting for now.\n");
return Ok(());
}
"n" | "no" => {
println!("\n❌ Cancelled. No files were saved.\n");
return Ok(());
}
_ => {
println!("\n❌ Invalid choice. Cancelled.\n");
return Ok(());
}
}
}
if let Some(requirements_text) = &cli.requirements {
// Use requirements text override
Project::new_autonomous_with_requirements(workspace_dir.clone(), requirements_text.clone())?
@@ -482,6 +377,7 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
// Execute task, autonomous mode, or start interactive mode based on machine mode
if cli.machine {
// Machine mode - use MachineUiWriter
let ui_writer = MachineUiWriter::new();
let agent = if cli.autonomous {
@@ -505,6 +401,20 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
run_with_machine_mode(agent, cli, project).await?;
} else {
// Normal mode - use ConsoleUiWriter
// DEFAULT: Chat mode for interactive sessions
// It runs when:
// 1. No task is provided (not single-shot)
// 2. Not in autonomous mode
// 3. Not explicitly enabled with --auto flag
let use_accumulative = cli.task.is_none() && !cli.autonomous && cli.auto;
if use_accumulative {
// Run accumulative mode and return early
run_accumulative_mode(workspace_dir.clone(), cli.clone(), combined_content.clone()).await?;
return Ok(());
}
let ui_writer = ConsoleUiWriter::new();
let agent = if cli.autonomous {
@@ -531,6 +441,273 @@ Output ONLY the markdown content, no explanations or meta-commentary."#,
Ok(())
}
/// Accumulative autonomous mode: accumulates requirements from user input
/// and runs autonomous mode after each input
async fn run_accumulative_mode(
workspace_dir: PathBuf,
cli: Cli,
combined_content: Option<String>,
) -> Result<()> {
let output = SimpleOutput::new();
output.print("");
output.print("🪿 G3 AI Coding Agent - Autonomous Mode");
output.print(" >> describe what you want, I'll build it iteratively");
output.print("");
output.print(&format!("📁 Workspace: {}", workspace_dir.display()));
output.print("");
output.print("💡 Each input you provide will be added to requirements");
output.print(" and I'll automatically work on implementing them. You can");
output.print(" interrupt at any time (Ctrl+C) to add clarifications or more requirements.");
output.print("");
output.print(" Type '/help' for commands, 'exit' or 'quit' to stop, Ctrl+D to finish");
output.print("");
// Initialize rustyline editor with history
let mut rl = DefaultEditor::new()?;
let history_file = dirs::home_dir().map(|mut path| {
path.push(".g3_accumulative_history");
path
});
if let Some(ref history_path) = history_file {
let _ = rl.load_history(history_path);
}
// Accumulated requirements stored in memory
let mut accumulated_requirements = Vec::new();
let mut turn_number = 0;
loop {
output.print(&format!("\n{}", "=".repeat(60)));
if accumulated_requirements.is_empty() {
output.print("📝 What would you like me to build? (describe your requirements)");
} else {
output.print(&format!("📝 Turn {} - What's next? (add more requirements or refinements)", turn_number + 1));
}
output.print(&format!("{}", "=".repeat(60)));
let readline = rl.readline("requirement> ");
match readline {
Ok(line) => {
let input = line.trim().to_string();
if input.is_empty() {
continue;
}
if input == "exit" || input == "quit" {
output.print("\n👋 Goodbye!");
break;
}
// Check for slash commands
if input.starts_with('/') {
match input.as_str() {
"/help" => {
output.print("");
output.print("📖 Available Commands:");
output.print(" /requirements - Show all accumulated requirements");
output.print(" /chat - Switch to interactive chat mode");
output.print(" /help - Show this help message");
output.print(" exit/quit - Exit the session");
output.print("");
continue;
}
"/requirements" => {
output.print("");
if accumulated_requirements.is_empty() {
output.print("📋 No requirements accumulated yet");
} else {
output.print("📋 Accumulated Requirements:");
output.print("");
for req in &accumulated_requirements {
output.print(&format!(" {}", req));
}
}
output.print("");
continue;
}
"/chat" => {
output.print("");
output.print("🔄 Switching to interactive chat mode...");
output.print("");
// Build context message with accumulated requirements
let requirements_context = if accumulated_requirements.is_empty() {
None
} else {
Some(format!(
"📋 Context from Accumulative Mode:\n\n\
We were working on these requirements. There may be unstaged or in-progress changes or recent changes to this branch. This is for your information.\n\n\
Requirements:\n{}\n",
accumulated_requirements.join("\n")
))
};
// Combine with existing content (README/AGENTS.md)
let chat_combined_content = match (requirements_context, combined_content.clone()) {
(Some(req_ctx), Some(existing)) => Some(format!("{}\n\n{}", req_ctx, existing)),
(Some(req_ctx), None) => Some(req_ctx),
(None, existing) => existing,
};
// Load configuration
let mut config = Config::load_with_overrides(
cli.config.as_deref(),
cli.provider.clone(),
cli.model.clone(),
)?;
// Apply macax flag override
if cli.macax {
config.macax.enabled = true;
}
// Apply webdriver flag override
if cli.webdriver {
config.webdriver.enabled = true;
}
// Create agent for interactive mode with requirements context
let ui_writer = ConsoleUiWriter::new();
let agent = Agent::new_with_readme_and_quiet(
config,
ui_writer,
chat_combined_content.clone(),
cli.quiet,
)
.await?;
// Run interactive mode
run_interactive(agent, cli.show_prompt, cli.show_code, chat_combined_content).await?;
// After returning from interactive mode, exit
output.print("\n👋 Goodbye!");
break;
}
_ => {
output.print(&format!("❌ Unknown command: {}. Type /help for available commands.", input));
continue;
}
}
}
// Add to history
rl.add_history_entry(&input)?;
// Add this requirement to accumulated list
turn_number += 1;
accumulated_requirements.push(format!("{}. {}", turn_number, input));
// Build the complete requirements document
let requirements_doc = format!(
"# Project Requirements\n\n\
## Current Instructions and Requirements:\n\n\
{}\n\n\
## Latest Requirement (Turn {}):\n\n\
{}",
accumulated_requirements.join("\n"),
turn_number,
input
);
output.print("");
output.print(&format!("📋 Current instructions and requirements (Turn {}):", turn_number));
output.print(&format!(" {}", input));
output.print("");
output.print("🚀 Starting autonomous implementation...");
output.print("");
// Create a project with the accumulated requirements
let project = Project::new_autonomous_with_requirements(
workspace_dir.clone(),
requirements_doc.clone()
)?;
// Ensure workspace exists and enter it
project.ensure_workspace_exists()?;
project.enter_workspace()?;
// Load configuration with CLI overrides
let mut config = Config::load_with_overrides(
cli.config.as_deref(),
cli.provider.clone(),
cli.model.clone(),
)?;
// Apply macax flag override
if cli.macax {
config.macax.enabled = true;
}
// Apply webdriver flag override
if cli.webdriver {
config.webdriver.enabled = true;
}
// Create agent for this autonomous run
let ui_writer = ConsoleUiWriter::new();
let agent = Agent::new_autonomous_with_readme_and_quiet(
config.clone(),
ui_writer,
combined_content.clone(),
cli.quiet,
)
.await?;
// Run autonomous mode with the accumulated requirements
let autonomous_result = tokio::select! {
result = run_autonomous(
agent,
project,
cli.show_prompt,
cli.show_code,
cli.max_turns,
cli.quiet,
) => result,
_ = tokio::signal::ctrl_c() => {
output.print("\n⚠️ Autonomous run cancelled by user (Ctrl+C)");
Ok(())
}
};
match autonomous_result
{
Ok(_) => {
output.print("");
output.print("✅ Autonomous run completed");
}
Err(e) => {
output.print("");
output.print(&format!("❌ Autonomous run failed: {}", e));
output.print(" You can provide more requirements to continue.");
}
}
}
Err(ReadlineError::Interrupted) => {
output.print("\n👋 Interrupted. Goodbye!");
break;
}
Err(ReadlineError::Eof) => {
output.print("\n👋 Goodbye!");
break;
}
Err(err) => {
error!("Error: {:?}", err);
break;
}
}
}
// Save history before exiting
if let Some(ref history_path) = history_file {
let _ = rl.save_history(history_path);
}
Ok(())
}
// Simplified machine mode version of autonomous mode
async fn run_autonomous_machine(
mut agent: Agent<MachineUiWriter>,
@@ -1303,10 +1480,32 @@ fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput,
}
}
fn display_context_progress<W: UiWriter>(agent: &Agent<W>, output: &SimpleOutput) {
fn display_context_progress<W: UiWriter>(agent: &Agent<W>, _output: &SimpleOutput) {
let context = agent.get_context_window();
output.print(&format!("Context: {}/{} tokens ({:.1}%)",
context.used_tokens, context.total_tokens, context.percentage_used()));
let percentage = context.percentage_used();
// Create 10 dots representing context fullness
let total_dots: usize = 10;
let filled_dots = ((percentage / 100.0) * total_dots as f32).round() as usize;
let empty_dots = total_dots.saturating_sub(filled_dots);
let filled_str = "".repeat(filled_dots);
let empty_str = "".repeat(empty_dots);
// Determine color based on percentage
let color = if percentage < 40.0 {
Color::Green
} else if percentage < 60.0 {
Color::Yellow
} else if percentage < 80.0 {
Color::Rgb { r: 255, g: 165, b: 0 } // Orange
} else {
Color::Red
};
// Print with colored dots (using print! directly to handle color codes)
print!("Context: {}{}{}{} {:.0}% ({}/{} tokens)\n",
SetForegroundColor(color), filled_str, empty_str, ResetColor, percentage, context.used_tokens, context.total_tokens);
}
/// Set up the workspace directory for autonomous mode

View File

@@ -71,18 +71,20 @@ impl SimpleOutput {
}
pub fn print_context(&self, used: u32, total: u32, percentage: f32) {
let bar_width: usize = 10;
let filled_width = ((percentage / 100.0) * bar_width as f32) as usize;
let empty_width = bar_width.saturating_sub(filled_width);
let total_dots = 10;
let filled_dots = ((percentage / 100.0) * total_dots as f32) as usize;
let empty_dots = total_dots.saturating_sub(filled_dots);
let filled_chars = "".repeat(filled_width);
let empty_chars = "".repeat(empty_width);
let filled_str = "".repeat(filled_dots);
let empty_str = "".repeat(empty_dots);
// Determine color based on percentage
let color = if percentage < 60.0 {
let color = if percentage < 40.0 {
crossterm::style::Color::Green
} else if percentage < 80.0 {
} else if percentage < 60.0 {
crossterm::style::Color::Yellow
} else if percentage < 80.0 {
crossterm::style::Color::Rgb { r: 255, g: 165, b: 0 } // Orange
} else {
crossterm::style::Color::Red
};
@@ -90,9 +92,9 @@ impl SimpleOutput {
// Print with colored progress bar
print!("Context: ");
print!("{}", SetForegroundColor(color));
print!("{}{}", filled_chars, empty_chars);
print!("{}{}", filled_str, empty_str);
print!("{}", ResetColor);
println!(" {:.1}% | {}/{} tokens", percentage, used, total);
println!(" {:.0}% ({}/{} tokens)", percentage, used, total);
}
pub fn print_context_thinning(&self, message: &str) {

View File

@@ -25,3 +25,4 @@ chrono = { version = "0.4", features = ["serde"] }
rand = "0.8"
regex = "1.0"
shellexpand = "3.1"
serde_yaml = "0.9"

View File

@@ -0,0 +1,787 @@
//! Code search functionality using ast-grep for syntax-aware semantic searches
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Command;
use tokio::sync::Semaphore;
use tracing::{debug, error, info, warn};
/// Maximum number of searches allowed per request
const MAX_SEARCHES: usize = 20;
/// Default timeout for individual searches in seconds
const DEFAULT_TIMEOUT_SECS: u64 = 60;
/// Default maximum concurrency
const DEFAULT_MAX_CONCURRENCY: usize = 4;
/// Default maximum matches per search
const DEFAULT_MAX_MATCHES: usize = 500;
/// Search specification for a single ast-grep search
#[derive(Debug, Clone, Deserialize)]
pub struct SearchSpec {
pub name: String,
pub mode: SearchMode,
// Pattern mode fields
pub pattern: Option<String>,
pub language: Option<String>,
// YAML mode fields
pub rule_yaml: Option<String>,
// Common fields
pub paths: Option<Vec<String>>,
pub globs: Option<Vec<String>>,
pub json_style: Option<JsonStyle>,
pub context: Option<u32>,
pub threads: Option<u32>,
pub include_metadata: Option<bool>,
pub no_ignore: Option<Vec<NoIgnoreType>>,
pub severity: Option<HashMap<String, SeverityLevel>>,
pub timeout_secs: Option<u64>,
}
/// Search mode: pattern or yaml
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SearchMode {
Pattern,
Yaml,
}
/// JSON output style
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum JsonStyle {
Pretty,
Stream,
Compact,
}
impl Default for JsonStyle {
fn default() -> Self {
JsonStyle::Stream
}
}
/// No-ignore types
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum NoIgnoreType {
Hidden,
Dot,
Exclude,
Global,
Parent,
Vcs,
}
/// Severity levels for YAML rules
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum SeverityLevel {
Error,
Warning,
Info,
Hint,
Off,
}
/// Request structure for code search
#[derive(Debug, Deserialize)]
pub struct CodeSearchRequest {
pub searches: Vec<SearchSpec>,
pub max_concurrency: Option<usize>,
pub max_matches_per_search: Option<usize>,
}
/// Result of a single search
#[derive(Debug, Serialize)]
pub struct SearchResult {
pub name: String,
pub mode: String,
pub status: String,
pub cmd: Vec<String>,
pub match_count: Option<usize>,
pub truncated: Option<bool>,
pub matches: Option<Vec<Value>>,
pub stderr: Option<String>,
pub exit_code: Option<i32>,
pub duration_ms: u64,
}
/// Summary of all searches
#[derive(Debug, Serialize)]
pub struct SearchSummary {
pub completed: usize,
pub total: usize,
pub total_matches: usize,
pub duration_ms: u64,
}
/// Complete response structure
#[derive(Debug, Serialize)]
pub struct CodeSearchResponse {
pub summary: SearchSummary,
pub searches: Vec<SearchResult>,
}
/// YAML rule structure for validation
#[derive(Debug, Deserialize)]
struct YamlRule {
pub id: String,
pub language: String,
pub rule: Value,
}
/// Execute a batch of code searches using ast-grep
pub async fn execute_code_search(request: CodeSearchRequest) -> Result<CodeSearchResponse> {
let start_time = Instant::now();
// Validate request
if request.searches.is_empty() {
return Err(anyhow!("No searches specified"));
}
if request.searches.len() > MAX_SEARCHES {
return Err(anyhow!(
"Too many searches: {} (max: {})",
request.searches.len(),
MAX_SEARCHES
));
}
// Check if ast-grep is available
check_ast_grep_available().await?;
let max_concurrency = request.max_concurrency.unwrap_or(DEFAULT_MAX_CONCURRENCY);
let max_matches = request.max_matches_per_search.unwrap_or(DEFAULT_MAX_MATCHES);
// Create semaphore for concurrency control
let semaphore = std::sync::Arc::new(Semaphore::new(max_concurrency));
// Execute searches concurrently
let mut tasks = Vec::new();
for search in request.searches {
let sem = semaphore.clone();
let task = tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
execute_single_search(search, max_matches).await
});
tasks.push(task);
}
// Wait for all searches to complete
let mut results = Vec::new();
let mut total_matches = 0;
let mut completed = 0;
for task in tasks {
match task.await {
Ok(result) => {
if result.status == "ok" {
completed += 1;
if let Some(count) = result.match_count {
total_matches += count;
}
}
results.push(result);
}
Err(e) => {
error!("Task join error: {}", e);
// Create an error result
results.push(SearchResult {
name: "unknown".to_string(),
mode: "unknown".to_string(),
status: "error".to_string(),
cmd: vec![],
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("Task execution error: {}", e)),
exit_code: None,
duration_ms: 0,
});
}
}
}
let total_duration = start_time.elapsed();
Ok(CodeSearchResponse {
summary: SearchSummary {
completed,
total: results.len(),
total_matches,
duration_ms: total_duration.as_millis() as u64,
},
searches: results,
})
}
/// Execute a single search
async fn execute_single_search(search: SearchSpec, max_matches: usize) -> SearchResult {
let start_time = Instant::now();
let timeout_secs = search.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
// Validate the search specification
if let Err(e) = validate_search_spec(&search) {
return SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "error".to_string(),
cmd: vec![],
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("Validation error: {}", e)),
exit_code: None,
duration_ms: start_time.elapsed().as_millis() as u64,
};
}
// Build command
let cmd_args = match build_ast_grep_command(&search) {
Ok(args) => args,
Err(e) => {
return SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "error".to_string(),
cmd: vec![],
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("Command build error: {}", e)),
exit_code: None,
duration_ms: start_time.elapsed().as_millis() as u64,
};
}
};
debug!("Executing ast-grep command: {:?}", cmd_args);
// Execute with timeout
let timeout_duration = Duration::from_secs(timeout_secs);
match tokio::time::timeout(timeout_duration, run_ast_grep_command(&cmd_args)).await {
Ok(Ok((stdout, stderr, exit_code))) => {
let duration_ms = start_time.elapsed().as_millis() as u64;
if exit_code == 0 {
// Parse JSON output
match parse_ast_grep_output(&stdout, max_matches) {
Ok((matches, truncated)) => {
SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "ok".to_string(),
cmd: cmd_args,
match_count: Some(matches.len()),
truncated: Some(truncated),
matches: Some(matches),
stderr: if stderr.is_empty() { None } else { Some(stderr) },
exit_code: None,
duration_ms,
}
}
Err(e) => {
SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "error".to_string(),
cmd: cmd_args,
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("JSON parse error: {}\nRaw output: {}", e, stdout)),
exit_code: Some(exit_code),
duration_ms,
}
}
}
} else {
SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "error".to_string(),
cmd: cmd_args,
match_count: None,
truncated: None,
matches: None,
stderr: Some(stderr),
exit_code: Some(exit_code),
duration_ms,
}
}
}
Ok(Err(e)) => {
SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "error".to_string(),
cmd: cmd_args,
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("Execution error: {}", e)),
exit_code: None,
duration_ms: start_time.elapsed().as_millis() as u64,
}
}
Err(_) => {
SearchResult {
name: search.name,
mode: format!("{:?}", search.mode).to_lowercase(),
status: "timeout".to_string(),
cmd: cmd_args,
match_count: None,
truncated: None,
matches: None,
stderr: Some(format!("Search timed out after {} seconds", timeout_secs)),
exit_code: None,
duration_ms: start_time.elapsed().as_millis() as u64,
}
}
}
}
/// Validate a search specification
fn validate_search_spec(search: &SearchSpec) -> Result<()> {
match search.mode {
SearchMode::Pattern => {
if search.pattern.is_none() || search.pattern.as_ref().unwrap().is_empty() {
return Err(anyhow!("Pattern mode requires non-empty 'pattern' field"));
}
}
SearchMode::Yaml => {
let rule_yaml = search.rule_yaml.as_ref()
.ok_or_else(|| anyhow!("YAML mode requires 'rule_yaml' field"))?;
if rule_yaml.is_empty() {
return Err(anyhow!("YAML mode requires non-empty 'rule_yaml' field"));
}
// Parse and validate YAML structure
let parsed: YamlRule = serde_yaml::from_str(rule_yaml)
.map_err(|e| anyhow!("Invalid YAML rule: {}", e))?;
if parsed.id.is_empty() {
return Err(anyhow!("YAML rule must have non-empty 'id' field"));
}
if parsed.language.is_empty() {
return Err(anyhow!("YAML rule must have non-empty 'language' field"));
}
// Validate language is supported (basic check)
validate_language(&parsed.language)?;
}
}
// Validate context range
if let Some(context) = search.context {
if context > 20 {
return Err(anyhow!("Context lines cannot exceed 20"));
}
}
Ok(())
}
/// Validate that a language is supported by ast-grep
fn validate_language(language: &str) -> Result<()> {
let supported_languages = [
"rust", "javascript", "typescript", "python", "java", "c", "cpp", "csharp",
"go", "html", "css", "json", "yaml", "xml", "bash", "kotlin", "swift",
"php", "ruby", "scala", "dart", "lua", "r", "sql", "dockerfile",
"Rust", "JavaScript", "TypeScript", "Python", "Java", "C", "Cpp", "CSharp",
"Go", "Html", "Css", "Json", "Yaml", "Xml", "Bash", "Kotlin", "Swift",
"Php", "Ruby", "Scala", "Dart", "Lua", "R", "Sql", "Dockerfile"
];
if !supported_languages.contains(&language) {
warn!("Language '{}' may not be supported by ast-grep", language);
}
Ok(())
}
/// Build ast-grep command arguments
fn build_ast_grep_command(search: &SearchSpec) -> Result<Vec<String>> {
let mut args = vec!["ast-grep".to_string()];
match search.mode {
SearchMode::Pattern => {
args.push("run".to_string());
// Add pattern
args.push("-p".to_string());
args.push(search.pattern.as_ref().unwrap().clone());
// Add language if specified
if let Some(ref lang) = search.language {
args.push("-l".to_string());
args.push(lang.clone());
}
}
SearchMode::Yaml => {
args.push("scan".to_string());
// Add inline rules
args.push("--inline-rules".to_string());
args.push(search.rule_yaml.as_ref().unwrap().clone());
// Add include-metadata if requested
if search.include_metadata.unwrap_or(false) {
args.push("--include-metadata".to_string());
}
// Add severity overrides
if let Some(ref severity_map) = search.severity {
for (rule_id, severity) in severity_map {
match severity {
SeverityLevel::Error => {
args.push("--error".to_string());
args.push(rule_id.clone());
}
SeverityLevel::Warning => {
args.push("--warning".to_string());
args.push(rule_id.clone());
}
SeverityLevel::Info => {
args.push("--info".to_string());
args.push(rule_id.clone());
}
SeverityLevel::Hint => {
args.push("--hint".to_string());
args.push(rule_id.clone());
}
SeverityLevel::Off => {
args.push("--off".to_string());
args.push(rule_id.clone());
}
}
}
}
}
}
// Add common arguments
// Add globs if specified
if let Some(ref globs) = search.globs {
if !globs.is_empty() {
args.push("--globs".to_string());
args.push(globs.join(","));
}
}
// Add context
if let Some(context) = search.context {
args.push("-C".to_string());
args.push(context.to_string());
}
// Add threads
if let Some(threads) = search.threads {
args.push("-j".to_string());
args.push(threads.to_string());
}
// Add JSON output style
let json_style = search.json_style.as_ref().unwrap_or(&JsonStyle::Stream);
let json_arg = match json_style {
JsonStyle::Pretty => "--json=pretty",
JsonStyle::Stream => "--json=stream",
JsonStyle::Compact => "--json=compact",
};
args.push(json_arg.to_string());
// Add no-ignore options
if let Some(ref no_ignore_list) = search.no_ignore {
for no_ignore_type in no_ignore_list {
let flag = match no_ignore_type {
NoIgnoreType::Hidden => "--no-ignore=hidden",
NoIgnoreType::Dot => "--no-ignore=dot",
NoIgnoreType::Exclude => "--no-ignore=exclude",
NoIgnoreType::Global => "--no-ignore=global",
NoIgnoreType::Parent => "--no-ignore=parent",
NoIgnoreType::Vcs => "--no-ignore=vcs",
};
args.push(flag.to_string());
}
}
// Add paths (default to current directory if none specified)
if let Some(ref paths) = search.paths {
if !paths.is_empty() {
args.extend(paths.clone());
} else {
args.push(".".to_string());
}
} else {
args.push(".".to_string());
}
Ok(args)
}
/// Run ast-grep command and capture output
async fn run_ast_grep_command(args: &[String]) -> Result<(String, String, i32)> {
let mut cmd = Command::new(&args[0]);
cmd.args(&args[1..]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
debug!("Running command: {:?}", args);
let mut child = cmd.spawn()
.map_err(|e| anyhow!("Failed to spawn ast-grep process: {}", e))?;
let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap();
let stdout_reader = BufReader::new(stdout);
let stderr_reader = BufReader::new(stderr);
let stdout_task = tokio::spawn(async move {
let mut lines = stdout_reader.lines();
let mut output = String::new();
while let Ok(Some(line)) = lines.next_line().await {
if !output.is_empty() {
output.push('\n');
}
output.push_str(&line);
}
output
});
let stderr_task = tokio::spawn(async move {
let mut lines = stderr_reader.lines();
let mut output = String::new();
while let Ok(Some(line)) = lines.next_line().await {
if !output.is_empty() {
output.push('\n');
}
output.push_str(&line);
}
output
});
let status = child.wait().await
.map_err(|e| anyhow!("Failed to wait for ast-grep process: {}", e))?;
let stdout_output = stdout_task.await
.map_err(|e| anyhow!("Failed to read stdout: {}", e))?;
let stderr_output = stderr_task.await
.map_err(|e| anyhow!("Failed to read stderr: {}", e))?;
let exit_code = status.code().unwrap_or(-1);
Ok((stdout_output, stderr_output, exit_code))
}
/// Parse ast-grep JSON output
fn parse_ast_grep_output(output: &str, max_matches: usize) -> Result<(Vec<Value>, bool)> {
if output.trim().is_empty() {
return Ok((vec![], false));
}
let mut matches = Vec::new();
let mut truncated = false;
// Handle stream format (line-delimited JSON)
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<Value>(line) {
Ok(match_obj) => {
if matches.len() >= max_matches {
truncated = true;
break;
}
matches.push(match_obj);
}
Err(e) => {
debug!("Failed to parse JSON line '{}': {}", line, e);
// Try to parse the entire output as a single JSON array
match serde_json::from_str::<Vec<Value>>(output) {
Ok(array_matches) => {
let take_count = array_matches.len().min(max_matches);
let total_count = array_matches.len();
matches = array_matches.into_iter().take(take_count).collect();
truncated = take_count < total_count;
break;
}
Err(e2) => {
return Err(anyhow!(
"Failed to parse ast-grep output as line-delimited JSON or JSON array. Line error: {}, Array error: {}",
e, e2
));
}
}
}
}
}
Ok((matches, truncated))
}
/// Check if ast-grep is available and provide installation hints if not
async fn check_ast_grep_available() -> Result<()> {
match Command::new("ast-grep")
.arg("--version")
.output()
.await
{
Ok(output) => {
if output.status.success() {
let version = String::from_utf8_lossy(&output.stdout);
info!("Found ast-grep: {}", version.trim());
Ok(())
} else {
Err(anyhow!("ast-grep command failed: {}", String::from_utf8_lossy(&output.stderr)))
}
}
Err(_) => {
Err(anyhow!(
"ast-grep not found. Please install it using one of these methods:\n\n\
• Homebrew (macOS): brew install ast-grep\n\
• MacPorts (macOS): sudo port install ast-grep\n\
• Nix: nix-env -iA nixpkgs.ast-grep\n\
• Cargo: cargo install ast-grep\n\
• npm: npm install -g @ast-grep/cli\n\
• pip: pip install ast-grep\n\n\
For more installation options, visit: https://ast-grep.github.io/guide/quick-start.html"
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_pattern_search() {
let search = SearchSpec {
name: "test".to_string(),
mode: SearchMode::Pattern,
pattern: Some("fn $NAME() {}".to_string()),
language: Some("rust".to_string()),
rule_yaml: None,
paths: None,
globs: None,
json_style: None,
context: None,
threads: None,
include_metadata: None,
no_ignore: None,
severity: None,
timeout_secs: None,
};
assert!(validate_search_spec(&search).is_ok());
}
#[test]
fn test_validate_yaml_search() {
let yaml_rule = r#"
id: test-rule
language: Rust
rule:
pattern: "fn $NAME() {}"
"#;
let search = SearchSpec {
name: "test".to_string(),
mode: SearchMode::Yaml,
pattern: None,
language: None,
rule_yaml: Some(yaml_rule.to_string()),
paths: None,
globs: None,
json_style: None,
context: None,
threads: None,
include_metadata: None,
no_ignore: None,
severity: None,
timeout_secs: None,
};
assert!(validate_search_spec(&search).is_ok());
}
#[test]
fn test_build_pattern_command() {
let search = SearchSpec {
name: "test".to_string(),
mode: SearchMode::Pattern,
pattern: Some("fn $NAME() {}".to_string()),
language: Some("rust".to_string()),
rule_yaml: None,
paths: Some(vec!["src/".to_string()]),
globs: None,
json_style: Some(JsonStyle::Stream),
context: Some(2),
threads: Some(4),
include_metadata: None,
no_ignore: None,
severity: None,
timeout_secs: None,
};
let cmd = build_ast_grep_command(&search).unwrap();
assert_eq!(cmd[0], "ast-grep");
assert_eq!(cmd[1], "run");
assert!(cmd.contains(&"-p".to_string()));
assert!(cmd.contains(&"fn $NAME() {}".to_string()));
assert!(cmd.contains(&"-l".to_string()));
assert!(cmd.contains(&"rust".to_string()));
assert!(cmd.contains(&"--json=stream".to_string()));
assert!(cmd.contains(&"-C".to_string()));
assert!(cmd.contains(&"2".to_string()));
assert!(cmd.contains(&"-j".to_string()));
assert!(cmd.contains(&"4".to_string()));
assert!(cmd.contains(&"src/".to_string()));
}
#[test]
fn test_parse_stream_json() {
let output = r#"{"file":"test.rs","text":"fn hello() {}"}
{"file":"test2.rs","text":"fn world() {}"}"#;
let (matches, truncated) = parse_ast_grep_output(output, 10).unwrap();
assert_eq!(matches.len(), 2);
assert!(!truncated);
assert_eq!(matches[0]["file"], "test.rs");
assert_eq!(matches[1]["file"], "test2.rs");
}
#[test]
fn test_parse_truncated_output() {
let output = r#"{"file":"test1.rs","text":"fn a() {}"}
{"file":"test2.rs","text":"fn b() {}"}
{"file":"test3.rs","text":"fn c() {}"}"#;
let (matches, truncated) = parse_ast_grep_output(output, 2).unwrap();
assert_eq!(matches.len(), 2);
assert!(truncated);
}
}

View File

@@ -1,76 +0,0 @@
#[cfg(test)]
mod gitignore_prompt_tests {
use crate::Agent;
use crate::ui_writer::UiWriter;
// Mock UI writer for testing
struct MockUiWriter;
impl UiWriter for MockUiWriter {
fn print_agent_prompt(&self) {}
fn print_agent_response(&self, _text: &str) {}
fn print(&self, _message: &str) {}
fn print_inline(&self, _message: &str) {}
fn print_tool_output_line(&self, _line: &str) {}
fn print_system_prompt(&self, _text: &str) {}
fn print_tool_header(&self, _tool_name: &str) {}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
fn print_tool_output_header(&self) {}
fn update_tool_output_line(&self, _line: &str) {}
fn print_tool_output_summary(&self, _total_lines: usize) {}
fn print_tool_timing(&self, _duration: &str) {}
fn print_context_status(&self, _message: &str) {}
fn print_context_thinning(&self, _message: &str) {}
fn println(&self, _text: &str) {}
fn flush(&self) {}
fn notify_sse_received(&self) {}
fn wants_full_output(&self) -> bool { false }
}
#[test]
fn test_gitignore_prompt_snippet_with_file() {
// Create a temporary .gitignore file
let test_gitignore = "# Test comment\ntarget/\n*.log\n\n# Another comment\nlogs/\n";
std::fs::write(".gitignore.test", test_gitignore).unwrap();
// Temporarily rename actual .gitignore if it exists
let has_real_gitignore = std::path::Path::new(".gitignore").exists();
if has_real_gitignore {
std::fs::rename(".gitignore", ".gitignore.backup").unwrap();
}
// Rename test file to .gitignore
std::fs::rename(".gitignore.test", ".gitignore").unwrap();
let snippet = Agent::<MockUiWriter>::get_gitignore_prompt_snippet();
// Restore original .gitignore
std::fs::remove_file(".gitignore").unwrap();
if has_real_gitignore {
std::fs::rename(".gitignore.backup", ".gitignore").unwrap();
}
assert!(snippet.contains("IMPORTANT"));
assert!(snippet.contains(".gitignore"));
assert!(snippet.contains("target/"));
assert!(snippet.contains("*.log"));
}
#[test]
fn test_gitignore_prompt_snippet_without_file() {
// Temporarily rename .gitignore if it exists
let has_gitignore = std::path::Path::new(".gitignore").exists();
if has_gitignore {
std::fs::rename(".gitignore", ".gitignore.backup").unwrap();
}
let snippet = Agent::<MockUiWriter>::get_gitignore_prompt_snippet();
// Restore .gitignore
if has_gitignore {
std::fs::rename(".gitignore.backup", ".gitignore").unwrap();
}
assert_eq!(snippet, "");
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -276,6 +276,7 @@ impl AnthropicProvider {
let mut partial_tool_json = String::new(); // Accumulate partial JSON for tool calls
let mut accumulated_usage: Option<Usage> = None;
let mut byte_buffer = Vec::new(); // Buffer for incomplete UTF-8 sequences
let mut message_stopped = false; // Track if we've received message_stop
while let Some(chunk_result) = stream.next().await {
match chunk_result {
@@ -316,6 +317,12 @@ impl AnthropicProvider {
continue;
}
// If we've already sent the final chunk, skip processing more events
if message_stopped {
debug!("Skipping event after message_stop: {}", line);
continue;
}
// Parse Server-Sent Events format
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" {
@@ -451,6 +458,7 @@ impl AnthropicProvider {
}
"message_stop" => {
debug!("Received message stop event");
message_stopped = true;
let final_chunk = CompletionChunk {
content: String::new(),
finished: true,
@@ -460,7 +468,8 @@ impl AnthropicProvider {
if tx.send(Ok(final_chunk)).await.is_err() {
debug!("Receiver dropped, stopping stream");
}
return accumulated_usage;
// Don't return here - let the stream naturally exhaust
// This prevents dropping the sender prematurely
}
"error" => {
if let Some(error) = event.error {
@@ -468,7 +477,7 @@ impl AnthropicProvider {
let _ = tx
.send(Err(anyhow!("Anthropic API error: {:?}", error)))
.await;
return accumulated_usage;
break; // Break to let stream exhaust naturally
}
}
_ => {
@@ -487,7 +496,10 @@ impl AnthropicProvider {
Err(e) => {
error!("Stream error: {}", e);
let _ = tx.send(Err(anyhow!("Stream error: {}", e))).await;
return accumulated_usage;
// Don't return here either - let the stream exhaust naturally
// The error has been sent to the receiver, so it will handle it
// Breaking here ensures we clean up properly
break;
}
}
}

389
docs/ACCUMULATIVE_MODE.md Normal file
View File

@@ -0,0 +1,389 @@
# Accumulative Autonomous Mode
## Overview
Accumulative Autonomous Mode is the **new default interactive mode** for G3. It combines the ease of interactive chat with the power of autonomous implementation, allowing you to build projects iteratively by describing what you want, one requirement at a time.
## How It Works
### The Flow
1. **Start G3** in any directory (no arguments needed)
2. **Describe** what you want to build
3. **G3 automatically**:
- Adds your input to accumulated requirements
- Runs autonomous mode (coach-player feedback loop)
- Implements your requirements with quality checks
4. **Continue** adding more requirements or refinements
5. **Repeat** until your project is complete
### Example Session
```bash
$ cd ~/projects/my-new-app
$ g3
🪿 G3 AI Coding Agent - Accumulative Mode
>> describe what you want, I'll build it iteratively
📁 Workspace: /Users/you/projects/my-new-app
💡 Each input you provide will be added to requirements
and I'll automatically work on implementing them.
Type 'exit' or 'quit' to stop, Ctrl+D to finish
============================================================
📝 What would you like me to build? (describe your requirements)
============================================================
requirement> create a simple web server in Python with Flask that serves a homepage
📋 Current instructions and requirements (Turn 1):
create a simple web server in Python with Flask that serves a homepage
🚀 Starting autonomous implementation...
🤖 G3 AI Coding Agent - Autonomous Mode
📁 Using workspace: /Users/you/projects/my-new-app
📋 Requirements loaded from --requirements flag
🔄 Starting coach-player feedback loop...
📂 No existing implementation files detected
🎯 Starting with player implementation
=== TURN 1/5 - PLAYER MODE ===
🎯 Starting player implementation...
📋 Player starting initial implementation (no prior coach feedback)
[Player creates files, writes code...]
=== TURN 1/5 - COACH MODE ===
🎓 Starting coach review...
🎓 Coach review completed
Coach feedback:
The Flask server is implemented correctly with a homepage route.
The code follows best practices and meets the requirements.
IMPLEMENTATION_APPROVED
=== SESSION COMPLETED - IMPLEMENTATION APPROVED ===
✅ Coach approved the implementation!
============================================================
📊 AUTONOMOUS MODE SESSION REPORT
============================================================
⏱️ Total Duration: 12.34s
🔄 Turns Taken: 1/5
📝 Final Status: ✅ APPROVED
...
============================================================
✅ Autonomous run completed
============================================================
📝 Turn 2 - What's next? (add more requirements or refinements)
============================================================
requirement> add a /api/users endpoint that returns a list of users as JSON
📋 Current instructions and requirements (Turn 2):
add a /api/users endpoint that returns a list of users as JSON
🚀 Starting autonomous implementation...
[Autonomous mode runs again with BOTH requirements...]
============================================================
📝 Turn 3 - What's next? (add more requirements or refinements)
============================================================
requirement> exit
👋 Goodbye!
```
## Key Features
### 1. Requirement Accumulation
Each input you provide is:
- **Numbered sequentially** (1, 2, 3, ...)
- **Stored in memory** for the session
- **Included in all subsequent runs**
This means the agent always has the full context of what you've asked for.
### 2. Automatic Requirements Document
G3 automatically generates a structured requirements document:
```markdown
# Project Requirements
## Current Instructions and Requirements:
1. create a simple web server in Python with Flask that serves a homepage
2. add a /api/users endpoint that returns a list of users as JSON
3. add error handling for 404 and 500 errors
## Latest Requirement (Turn 3):
add error handling for 404 and 500 errors
```
This document is passed to autonomous mode, ensuring the agent:
- Knows all previous requirements
- Focuses on the latest addition
- Maintains consistency across iterations
### 3. Full Autonomous Quality
Each requirement triggers a complete autonomous run with:
- **Coach-Player Feedback Loop**: Quality assurance built-in
- **Multiple Turns**: Up to 5 iterations per requirement (configurable with `--max-turns`)
- **Compilation Checks**: Ensures code actually works
- **Testing**: Coach can run tests to verify functionality
### 4. Error Recovery
If an autonomous run fails:
- You're notified of the error
- You can provide additional requirements to fix issues
- The session continues (doesn't crash)
### 5. Workspace Management
- Uses **current directory** as workspace
- All files created in current directory
- No need to specify workspace path
- Works with existing projects or empty directories
## Command-Line Options
### Default (Accumulative Mode)
```bash
g3
```
Starts accumulative autonomous mode in the current directory.
### With Options
```bash
# Use a specific workspace
g3 --workspace ~/projects/my-app
# Limit autonomous turns per requirement
g3 --max-turns 3
# Enable macOS Accessibility tools
g3 --macax
# Enable WebDriver browser automation
g3 --webdriver
# Use a specific provider/model
g3 --provider anthropic --model claude-3-5-sonnet-20241022
# Show prompts and code during execution
g3 --show-prompt --show-code
# Disable log files
g3 --quiet
```
### Disable Accumulative Mode
To use the traditional chat mode (without automatic autonomous runs):
```bash
g3 --chat
# Alternative: legacy flag also works
g3 --accumulative
```
This gives you the old behavior where you chat with the agent without automatic autonomous runs.
## Use Cases
### 1. Rapid Prototyping
```bash
requirement> create a REST API for a todo app
requirement> add SQLite database storage
requirement> add authentication with JWT
requirement> add rate limiting
```
### 2. Iterative Refinement
```bash
requirement> create a data visualization dashboard
requirement> make the charts interactive
requirement> add dark mode support
requirement> optimize for mobile devices
```
### 3. Bug Fixing
```bash
requirement> fix the login form validation
requirement> handle edge case when username is empty
requirement> add better error messages
```
### 4. Feature Addition
```bash
requirement> add export to CSV functionality
requirement> add email notifications
requirement> add admin dashboard
```
## Tips and Best Practices
### 1. Start Simple
Begin with a basic requirement, let it be implemented, then add complexity:
```bash
✅ Good:
requirement> create a basic Flask web server
requirement> add a homepage with a form
requirement> add form validation
❌ Too Complex:
requirement> create a full-stack web app with authentication, database, API, and frontend
```
### 2. Be Specific
The more specific you are, the better the results:
```bash
✅ Good:
requirement> add a /api/users endpoint that returns JSON with id, name, and email fields
❌ Vague:
requirement> add users
```
### 3. One Thing at a Time
Focus each requirement on a single feature or fix:
```bash
✅ Good:
requirement> add error handling for database connections
requirement> add logging for all API requests
❌ Multiple Things:
requirement> add error handling and logging and monitoring and alerts
```
### 4. Review Between Turns
After each autonomous run completes:
- Check the generated files
- Test the functionality
- Decide what to add or fix next
### 5. Use Exit Commands
When done:
- Type `exit` or `quit`
- Press `Ctrl+D` (EOF)
- Press `Ctrl+C` to cancel current input
## Comparison with Other Modes
| Feature | Accumulative (Default) | Traditional Interactive | Autonomous | Single-Shot |
|---------|----------------------|------------------------|------------|-------------|
| **Command** | `g3` | `g3 --accumulative` | `g3 --autonomous` | `g3 "task"` |
| **Input Style** | Iterative prompts | Chat messages | requirements.md file | Command-line arg |
| **Auto-Autonomous** | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
| **Coach-Player Loop** | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
| **Accumulates Requirements** | ✅ Yes | ❌ No | ❌ No | ❌ No |
| **Multiple Iterations** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No |
| **Best For** | Iterative development | Quick questions | Pre-planned projects | One-off tasks |
## Technical Details
### Requirements Storage
- Stored in memory (not persisted to disk)
- Numbered sequentially starting from 1
- Formatted as markdown list
- Passed to autonomous mode as `--requirements` override
### History
- Saved to `~/.g3_accumulative_history`
- Separate from traditional interactive history
- Persists across sessions
- Uses rustyline for readline support
### Workspace
- Defaults to current directory
- Can be overridden with `--workspace`
- All files created in workspace
- Logs saved to `workspace/logs/`
### Autonomous Execution
- Full coach-player feedback loop
- Configurable max turns (default: 5)
- Respects all CLI flags (--macax, --webdriver, etc.)
- Error handling allows continuation
## Troubleshooting
### "No requirements provided"
This shouldn't happen in accumulative mode, but if it does:
- Check that you entered a requirement
- Ensure the requirement isn't empty
- Try restarting G3
### "Autonomous run failed"
If an autonomous run fails:
- Read the error message
- Provide a new requirement to fix the issue
- Or type `exit` and investigate manually
### "Context window full"
If you hit token limits:
- The agent will auto-summarize
- Or you can start a new session
- Consider using `--max-turns` to limit iterations
### "Coach never approves"
If the coach keeps rejecting:
- Check the coach feedback for specific issues
- Provide more specific requirements
- Consider increasing `--max-turns`
## Future Enhancements
Planned improvements:
1. **Persistence**: Save accumulated requirements to disk
2. **Editing**: Edit or remove previous requirements
3. **Branching**: Try different approaches
4. **Templates**: Pre-defined requirement sets
5. **Review**: Show all accumulated requirements
6. **Export**: Save to requirements.md
7. **Undo**: Remove last requirement
8. **Replay**: Re-run with same requirements
## Feedback
This is a new feature! Please provide feedback:
- What works well?
- What's confusing?
- What features would you like?
- Any bugs or issues?
Open an issue on GitHub or contribute improvements!