Add language-specific prompt injection for toolchain guidance
- Add language_prompts module that auto-detects programming languages in workspace - Scan for language files with depth limit (2) to inject relevant toolchain prompts - Add prompts/langs/ directory for language-specific markdown files - Include Racket/raco toolchain guidance as first language prompt - Update combine_project_content() to accept language_content parameter - Integrate language detection into main CLI flow and agent mode - Update project memory with new feature documentation
This commit is contained in:
@@ -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()`
|
||||
@@ -193,3 +193,22 @@ Rich few-shot prompting for higher quality memory entries with per-symbol char r
|
||||
- `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
|
||||
|
||||
### 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/<lang>.md` with toolchain guidance
|
||||
2. Add entry to `LANGUAGE_PROMPTS` in `language_prompts.rs` with extensions
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
169
crates/g3-cli/src/language_prompts.rs
Normal file
169
crates/g3-cli/src/language_prompts.rs
Normal file
@@ -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<String> {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -90,12 +90,13 @@ pub fn combine_project_content(
|
||||
agents_content: Option<String>,
|
||||
readme_content: Option<String>,
|
||||
memory_content: Option<String>,
|
||||
language_content: Option<String>,
|
||||
workspace_dir: &Path,
|
||||
) -> Option<String> {
|
||||
// Always include working directory to prevent LLM from hallucinating paths
|
||||
let cwd_info = format!("📂 Working Directory: {}", workspace_dir.display());
|
||||
|
||||
let parts: Vec<String> = [Some(cwd_info), agents_content, readme_content, memory_content]
|
||||
let parts: Vec<String> = [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"));
|
||||
|
||||
30
prompts/langs/racket.md
Normal file
30
prompts/langs/racket.md
Normal file
@@ -0,0 +1,30 @@
|
||||
RACKET LANGUAGE CODE EXPLORATION + RACO TOOLING
|
||||
|
||||
- Core `raco` commands to rely on:
|
||||
- Documentation & discovery:
|
||||
- `raco docs <id>` to open docs for identifiers, modules, or packages.
|
||||
- Compilation & checks:
|
||||
- `raco make <file.rkt>` to force compilation and surface errors early.
|
||||
- Testing:
|
||||
- `raco test <path>` to run `module+ test` blocks and test files.
|
||||
- Packages & dependencies:
|
||||
- `raco pkg show` to inspect installed packages and their locations.
|
||||
- `raco pkg show <pkg>` to inspect package metadata and versions.
|
||||
- Profiling & performance:
|
||||
- `raco profile <file.rkt>` for CPU hot spots.
|
||||
- Debugging & stack traces:
|
||||
- `racket -l errortrace <file.rkt>` (or enabling errortrace) for readable stack traces.
|
||||
|
||||
- Structural analysis tools (use when reasoning about non-trivial codebases):
|
||||
- `raco dependency-graph <path>`:
|
||||
- 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.
|
||||
Reference in New Issue
Block a user