diff --git a/analysis/memory.md b/analysis/memory.md index 14f3166..c28e79a 100644 --- a/analysis/memory.md +++ b/analysis/memory.md @@ -1,5 +1,5 @@ # Project Memory -> Updated: 2026-01-13T16:15:37Z | Size: 11.8k chars +> Updated: 2026-01-14T15:11:36Z | Size: 12.0k chars ### Remember Tool Wiring - `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()` @@ -192,4 +192,23 @@ Rich few-shot prompting for higher quality memory entries with per-symbol char r - `crates/g3-core/src/lib.rs` - `send_auto_memory_reminder()` [47800..48800] - MEMORY CHECKPOINT prompt with few-shot examples - `crates/g3-core/src/prompts.rs` - - Memory Format section [3800..4500] - system prompt template and examples \ No newline at end of file + - Memory Format section [3800..4500] - system prompt template and examples + +### Language-Specific Prompt Injection +Auto-detects programming languages in workspace and injects toolchain guidance. + +- `crates/g3-cli/src/language_prompts.rs` + - `LANGUAGE_PROMPTS` [12..19] - static array of (lang_name, extensions, prompt_content) + - `detect_languages()` [22..32] - scans workspace for language files + - `get_language_prompts_for_workspace()` [88..108] - returns formatted prompt for detected languages + - `scan_directory_for_extensions()` [42..77] - recursive scan with depth limit (2), skips hidden/vendor dirs + +- `prompts/langs/` - directory for language prompt markdown files + - `racket.md` - Racket/raco toolchain guidance (compilation, testing, analysis, profiling) + +- `crates/g3-cli/src/project_files.rs` + - `combine_project_content()` [89..106] - now accepts `language_content` parameter + +To add a new language: +1. Create `prompts/langs/.md` with toolchain guidance +2. Add entry to `LANGUAGE_PROMPTS` in `language_prompts.rs` with extensions \ No newline at end of file diff --git a/crates/g3-cli/src/agent_mode.rs b/crates/g3-cli/src/agent_mode.rs index b35edc5..9a0a903 100644 --- a/crates/g3-cli/src/agent_mode.rs +++ b/crates/g3-cli/src/agent_mode.rs @@ -146,6 +146,7 @@ pub async fn run_agent_mode( agents_content_opt, readme_content_opt, memory_content_opt, + crate::language_prompts::get_language_prompts_for_workspace(&workspace_dir), &workspace_dir, ); diff --git a/crates/g3-cli/src/language_prompts.rs b/crates/g3-cli/src/language_prompts.rs new file mode 100644 index 0000000..6baf3c3 --- /dev/null +++ b/crates/g3-cli/src/language_prompts.rs @@ -0,0 +1,169 @@ +//! Language-specific prompt injection. +//! +//! Detects programming languages in the workspace and injects relevant +//! toolchain guidance into the system prompt. +//! +//! Language prompts are embedded at compile time from `prompts/langs/*.md`. + +use std::path::Path; + +/// Embedded language prompts, keyed by language name. +/// The key should match common file extensions or language identifiers. +static LANGUAGE_PROMPTS: &[(&str, &[&str], &str)] = &[ + // (language_name, file_extensions, prompt_content) + ( + "racket", + &[".rkt", ".rktl", ".rktd", ".scrbl"], + include_str!("../../../prompts/langs/racket.md"), + ), +]; + +/// Detect languages present in the workspace by scanning for file extensions. +/// Returns a list of detected language names. +pub fn detect_languages(workspace_dir: &Path) -> Vec<&'static str> { + let mut detected = Vec::new(); + + for (lang_name, extensions, _) in LANGUAGE_PROMPTS { + if has_files_with_extensions(workspace_dir, extensions) { + detected.push(*lang_name); + } + } + + detected +} + +/// Check if the workspace contains files with any of the given extensions. +/// Scans up to a reasonable depth to avoid slow startup on large repos. +fn has_files_with_extensions(workspace_dir: &Path, extensions: &[&str]) -> bool { + // Quick check: scan top-level and one level deep + // This avoids slow startup on large repos while catching most projects + scan_directory_for_extensions(workspace_dir, extensions, 2) +} + +/// Recursively scan a directory for files with given extensions, up to max_depth. +fn scan_directory_for_extensions(dir: &Path, extensions: &[&str], max_depth: usize) -> bool { + if max_depth == 0 { + return false; + } + + let entries = match std::fs::read_dir(dir) { + Ok(entries) => entries, + Err(_) => return false, + }; + + for entry in entries.flatten() { + let path = entry.path(); + + // Skip hidden directories and common non-source directories + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if name.starts_with('.') || name == "node_modules" || name == "target" || name == "vendor" { + continue; + } + } + + if path.is_file() { + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + for ext in extensions { + if name.ends_with(ext) { + return true; + } + } + } + } else if path.is_dir() { + if scan_directory_for_extensions(&path, extensions, max_depth - 1) { + return true; + } + } + } + + false +} + +/// Get the prompt content for a specific language. +pub fn get_language_prompt(lang: &str) -> Option<&'static str> { + LANGUAGE_PROMPTS + .iter() + .find(|(name, _, _)| *name == lang) + .map(|(_, _, content)| *content) +} + +/// Get all language prompts for detected languages in the workspace. +/// Returns formatted content ready for injection into the system prompt. +pub fn get_language_prompts_for_workspace(workspace_dir: &Path) -> Option { + let detected = detect_languages(workspace_dir); + + if detected.is_empty() { + return None; + } + + let mut prompts = Vec::new(); + for lang in detected { + if let Some(content) = get_language_prompt(lang) { + prompts.push(content); + } + } + + if prompts.is_empty() { + return None; + } + + Some(format!( + "πŸ”§ Language-Specific Guidance:\n\n{}", + prompts.join("\n\n---\n\n") + )) +} + +/// List all available language prompts. +pub fn list_available_languages() -> Vec<&'static str> { + LANGUAGE_PROMPTS.iter().map(|(name, _, _)| *name).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_racket_prompt_embedded() { + let prompt = get_language_prompt("racket"); + assert!(prompt.is_some()); + assert!(prompt.unwrap().contains("raco")); + } + + #[test] + fn test_list_available_languages() { + let langs = list_available_languages(); + assert!(langs.contains(&"racket")); + } + + #[test] + fn test_detect_racket_files() { + let temp_dir = TempDir::new().unwrap(); + let rkt_file = temp_dir.path().join("main.rkt"); + fs::write(&rkt_file, "#lang racket\n").unwrap(); + + let detected = detect_languages(temp_dir.path()); + assert!(detected.contains(&"racket")); + } + + #[test] + fn test_no_detection_empty_dir() { + let temp_dir = TempDir::new().unwrap(); + let detected = detect_languages(temp_dir.path()); + assert!(detected.is_empty()); + } + + #[test] + fn test_get_prompts_for_workspace() { + let temp_dir = TempDir::new().unwrap(); + let rkt_file = temp_dir.path().join("main.rkt"); + fs::write(&rkt_file, "#lang racket\n").unwrap(); + + let prompts = get_language_prompts_for_workspace(temp_dir.path()); + assert!(prompts.is_some()); + let content = prompts.unwrap(); + assert!(content.contains("πŸ”§ Language-Specific Guidance")); + assert!(content.contains("raco")); + } +} diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index bca0c81..5daa11d 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -5,6 +5,7 @@ pub mod metrics; pub mod project_files; pub mod streaming_markdown; pub mod embedded_agents; +pub mod language_prompts; mod accumulative; mod agent_mode; @@ -98,6 +99,7 @@ pub async fn run() -> Result<()> { let agents_content = read_agents_config(&workspace_dir); let readme_content = read_project_readme(&workspace_dir); let memory_content = read_project_memory(&workspace_dir); + let language_content = language_prompts::get_language_prompts_for_workspace(&workspace_dir); // Create project model let project = create_project(&cli, &workspace_dir)?; @@ -110,7 +112,7 @@ pub async fn run() -> Result<()> { let config = load_config_with_cli_overrides(&cli)?; // Combine AGENTS.md, README, and memory content - let combined_content = combine_project_content(agents_content, readme_content, memory_content, &workspace_dir); + let combined_content = combine_project_content(agents_content, readme_content, memory_content, language_content, &workspace_dir); run_console_mode(cli, config, project, combined_content, workspace_dir).await } diff --git a/crates/g3-cli/src/project_files.rs b/crates/g3-cli/src/project_files.rs index 363669b..c0f0fd5 100644 --- a/crates/g3-cli/src/project_files.rs +++ b/crates/g3-cli/src/project_files.rs @@ -90,12 +90,13 @@ pub fn combine_project_content( agents_content: Option, readme_content: Option, memory_content: Option, + language_content: Option, workspace_dir: &Path, ) -> Option { // Always include working directory to prevent LLM from hallucinating paths let cwd_info = format!("πŸ“‚ Working Directory: {}", workspace_dir.display()); - let parts: Vec = [Some(cwd_info), agents_content, readme_content, memory_content] + let parts: Vec = [Some(cwd_info), agents_content, readme_content, memory_content, language_content] .into_iter() .flatten() .collect(); @@ -222,6 +223,7 @@ mod tests { Some("agents".to_string()), Some("readme".to_string()), Some("memory".to_string()), + Some("language".to_string()), &workspace, ); assert!(result.is_some()); @@ -230,12 +232,13 @@ mod tests { assert!(content.contains("agents")); assert!(content.contains("readme")); assert!(content.contains("memory")); + assert!(content.contains("language")); } #[test] fn test_combine_project_content_partial() { let workspace = std::path::PathBuf::from("/test/workspace"); - let result = combine_project_content(None, Some("readme".to_string()), None, &workspace); + let result = combine_project_content(None, Some("readme".to_string()), None, None, &workspace); assert!(result.is_some()); let content = result.unwrap(); assert!(content.contains("πŸ“‚ Working Directory: /test/workspace")); @@ -245,7 +248,7 @@ mod tests { #[test] fn test_combine_project_content_all_none() { let workspace = std::path::PathBuf::from("/test/workspace"); - let result = combine_project_content(None, None, None, &workspace); + let result = combine_project_content(None, None, None, None, &workspace); // Now always returns Some because we always include the working directory assert!(result.is_some()); assert!(result.unwrap().contains("πŸ“‚ Working Directory: /test/workspace")); diff --git a/prompts/langs/racket.md b/prompts/langs/racket.md new file mode 100644 index 0000000..d8e3ad8 --- /dev/null +++ b/prompts/langs/racket.md @@ -0,0 +1,30 @@ +RACKET LANGUAGE CODE EXPLORATION + RACO TOOLING + +- Core `raco` commands to rely on: + - Documentation & discovery: + - `raco docs ` to open docs for identifiers, modules, or packages. + - Compilation & checks: + - `raco make ` to force compilation and surface errors early. + - Testing: + - `raco test ` to run `module+ test` blocks and test files. + - Packages & dependencies: + - `raco pkg show` to inspect installed packages and their locations. + - `raco pkg show ` to inspect package metadata and versions. + - Profiling & performance: + - `raco profile ` for CPU hot spots. + - Debugging & stack traces: + - `racket -l errortrace ` (or enabling errortrace) for readable stack traces. + +- Structural analysis tools (use when reasoning about non-trivial codebases): + - `raco dependency-graph `: + - Use to visualize or reason about module dependencies and layering. + - Identify cycles, high fan-in β€œcore” modules, and accidental coupling. + - `raco modgraph`: + - Use for quick textual inspection of module graphs when visualization isn’t needed. + - Treat dependency graphs as architectural signals, not just diagrams. + +- `racket -e`-driven exploration: + - Use the one-shot script execution to: + - `require` modules incrementally and inspect exports. + - Probe functions with small concrete examples. + - Validate assumptions about data shapes and return values.