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
|
# 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
|
### Remember Tool Wiring
|
||||||
- `crates/g3-core/src/tools/memory.rs` [0..5000] - `execute_remember()`, `get_memory_path()`, `merge_memory()`
|
- `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
|
- `send_auto_memory_reminder()` [47800..48800] - MEMORY CHECKPOINT prompt with few-shot examples
|
||||||
- `crates/g3-core/src/prompts.rs`
|
- `crates/g3-core/src/prompts.rs`
|
||||||
- Memory Format section [3800..4500] - system prompt template and examples
|
- 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,
|
agents_content_opt,
|
||||||
readme_content_opt,
|
readme_content_opt,
|
||||||
memory_content_opt,
|
memory_content_opt,
|
||||||
|
crate::language_prompts::get_language_prompts_for_workspace(&workspace_dir),
|
||||||
&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 project_files;
|
||||||
pub mod streaming_markdown;
|
pub mod streaming_markdown;
|
||||||
pub mod embedded_agents;
|
pub mod embedded_agents;
|
||||||
|
pub mod language_prompts;
|
||||||
|
|
||||||
mod accumulative;
|
mod accumulative;
|
||||||
mod agent_mode;
|
mod agent_mode;
|
||||||
@@ -98,6 +99,7 @@ pub async fn run() -> Result<()> {
|
|||||||
let agents_content = read_agents_config(&workspace_dir);
|
let agents_content = read_agents_config(&workspace_dir);
|
||||||
let readme_content = read_project_readme(&workspace_dir);
|
let readme_content = read_project_readme(&workspace_dir);
|
||||||
let memory_content = read_project_memory(&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
|
// Create project model
|
||||||
let project = create_project(&cli, &workspace_dir)?;
|
let project = create_project(&cli, &workspace_dir)?;
|
||||||
@@ -110,7 +112,7 @@ pub async fn run() -> Result<()> {
|
|||||||
let config = load_config_with_cli_overrides(&cli)?;
|
let config = load_config_with_cli_overrides(&cli)?;
|
||||||
|
|
||||||
// Combine AGENTS.md, README, and memory content
|
// 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
|
run_console_mode(cli, config, project, combined_content, workspace_dir).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,12 +90,13 @@ pub fn combine_project_content(
|
|||||||
agents_content: Option<String>,
|
agents_content: Option<String>,
|
||||||
readme_content: Option<String>,
|
readme_content: Option<String>,
|
||||||
memory_content: Option<String>,
|
memory_content: Option<String>,
|
||||||
|
language_content: Option<String>,
|
||||||
workspace_dir: &Path,
|
workspace_dir: &Path,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
// Always include working directory to prevent LLM from hallucinating paths
|
// Always include working directory to prevent LLM from hallucinating paths
|
||||||
let cwd_info = format!("📂 Working Directory: {}", workspace_dir.display());
|
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()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
@@ -222,6 +223,7 @@ mod tests {
|
|||||||
Some("agents".to_string()),
|
Some("agents".to_string()),
|
||||||
Some("readme".to_string()),
|
Some("readme".to_string()),
|
||||||
Some("memory".to_string()),
|
Some("memory".to_string()),
|
||||||
|
Some("language".to_string()),
|
||||||
&workspace,
|
&workspace,
|
||||||
);
|
);
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
@@ -230,12 +232,13 @@ mod tests {
|
|||||||
assert!(content.contains("agents"));
|
assert!(content.contains("agents"));
|
||||||
assert!(content.contains("readme"));
|
assert!(content.contains("readme"));
|
||||||
assert!(content.contains("memory"));
|
assert!(content.contains("memory"));
|
||||||
|
assert!(content.contains("language"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_combine_project_content_partial() {
|
fn test_combine_project_content_partial() {
|
||||||
let workspace = std::path::PathBuf::from("/test/workspace");
|
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());
|
assert!(result.is_some());
|
||||||
let content = result.unwrap();
|
let content = result.unwrap();
|
||||||
assert!(content.contains("📂 Working Directory: /test/workspace"));
|
assert!(content.contains("📂 Working Directory: /test/workspace"));
|
||||||
@@ -245,7 +248,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_combine_project_content_all_none() {
|
fn test_combine_project_content_all_none() {
|
||||||
let workspace = std::path::PathBuf::from("/test/workspace");
|
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
|
// Now always returns Some because we always include the working directory
|
||||||
assert!(result.is_some());
|
assert!(result.is_some());
|
||||||
assert!(result.unwrap().contains("📂 Working Directory: /test/workspace"));
|
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