From 6a4be9ddd77f6bdb6edccc342edd19044e2d085e Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 28 Oct 2025 17:55:12 +1100 Subject: [PATCH] gitignore awareness --- README.md | 6 +- crates/g3-core/src/gitignore_prompt_tests.rs | 76 ++++++++++++++++++++ crates/g3-core/src/lib.rs | 32 ++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 crates/g3-core/src/gitignore_prompt_tests.rs diff --git a/README.md b/README.md index c3b09aa..fb8a800 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ 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** @@ -97,7 +98,10 @@ These commands give you fine-grained control over context management, allowing y - **Final Output**: Formatted result presentation ### Provider Flexibility -- Support for multiple LLM providers through a unified interface + +### Smart Project Awareness + +- Automatically detects and respects `.gitignore` when present - Hot-swappable providers without code changes - Provider-specific optimizations and feature support - Local model support for offline operation diff --git a/crates/g3-core/src/gitignore_prompt_tests.rs b/crates/g3-core/src/gitignore_prompt_tests.rs new file mode 100644 index 0000000..79bf574 --- /dev/null +++ b/crates/g3-core/src/gitignore_prompt_tests.rs @@ -0,0 +1,76 @@ +#[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::::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::::get_gitignore_prompt_snippet(); + + // Restore .gitignore + if has_gitignore { + std::fs::rename(".gitignore.backup", ".gitignore").unwrap(); + } + + assert_eq!(snippet, ""); + } +} diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 969e854..facb184 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -18,6 +18,9 @@ mod tilde_expansion_tests; #[cfg(test)] mod error_handling_test; + +#[cfg(test)] +mod gitignore_prompt_tests; use anyhow::Result; use g3_computer_control::WebDriverController; use g3_config::Config; @@ -1031,6 +1034,28 @@ impl Agent { Ok(context_length) } + /// Check if .gitignore exists and return a prompt snippet about respecting it + fn get_gitignore_prompt_snippet() -> String { + // Check if .gitignore exists in the current directory + if std::path::Path::new(".gitignore").exists() { + // Try to read it to show some examples of what's ignored + if let Ok(gitignore_content) = std::fs::read_to_string(".gitignore") { + // Extract non-comment, non-empty lines as examples + let patterns: Vec<&str> = gitignore_content + .lines() + .filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#')) + .take(5) // Show up to 5 patterns + .collect(); + + if !patterns.is_empty() { + return format!("\n\nIMPORTANT: This project has a .gitignore file. When using ripgrep or other file operations, be aware that the following patterns are ignored: {}. Ripgrep automatically respects .gitignore by default.", patterns.join(", ")); + } + } + return "\n\nIMPORTANT: This project has a .gitignore file. When using ripgrep or other file operations, ripgrep automatically respects .gitignore by default.".to_string(); + } + String::new() + } + pub fn get_provider_info(&self) -> Result<(String, String)> { let provider = self.providers.get(None)?; Ok((provider.name().to_string(), provider.model().to_string())) @@ -1135,7 +1160,9 @@ impl Agent { // Only add system message if this is the first interaction (empty conversation history) if self.context_window.conversation_history.is_empty() { let provider = self.providers.get(None)?; - let system_prompt = if provider.has_native_tool_calling() { + let gitignore_snippet = Self::get_gitignore_prompt_snippet(); + + let mut system_prompt = if provider.has_native_tool_calling() { // For native tool calling providers, use a more explicit system prompt "You are G3, an AI programming agent of the same skill level as a seasoned engineer at a major technology company. You analyze given tasks and write code to achieve goals. @@ -1267,6 +1294,9 @@ Template: ".to_string() }; + + // Append gitignore snippet if present + system_prompt.push_str(&gitignore_snippet); if show_prompt { self.ui_writer.print_system_prompt(&system_prompt);