Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion specs/ai/ai.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Resolves and executes AI providers for spec generation. Supports CLI-based provi
| `resolve_ai_provider` | `config, cli_provider` | `Result<ResolvedProvider, String>` | Resolve which AI provider to use via 5-level priority chain |
| `resolve_ai_command` | `config, cli_provider` | `Result<String, String>` | Legacy alias — resolves provider and returns CLI command string |
| `generate_spec_with_ai` | `module_name, source_files, root, config, provider` | `Result<String, String>` | Generate a spec file by reading source code and calling the AI provider |
| `regenerate_spec_with_ai` | `module_name, spec_path, requirements_path, root, config, provider` | `Result<String, String>` | Regenerate an existing spec using AI when requirements have drifted; reads source files from the spec's frontmatter |

## Invariants

Expand Down Expand Up @@ -96,7 +97,7 @@ Resolves and executes AI providers for spec generation. Supports CLI-based provi
|--------|-------------|
| generator | `generate_spec_with_ai`, `ResolvedProvider` |
| mcp | `resolve_ai_provider` |
| main | `resolve_ai_provider` |
| main | `resolve_ai_provider`, `regenerate_spec_with_ai` |

## Change Log

Expand Down
17 changes: 10 additions & 7 deletions src/ai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ fn is_binary_available(name: &str) -> bool {
if name.is_empty() {
return false;
}
Command::new("sh")
.args(["-c", &format!("command -v {name}")])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
// Search PATH directly — avoids spawning a shell, which may not inherit
// the full PATH in IDE terminals, build systems, or macOS app launchers.
if let Ok(path_var) = std::env::var("PATH") {
for dir in path_var.split(':') {
if std::path::Path::new(dir).join(name).exists() {
return true;
}
}
}
false
}

/// Build the CLI command string for a provider, optionally using a custom model.
Expand Down
21 changes: 15 additions & 6 deletions src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,14 +628,15 @@ fn generate_spec(
}

/// Generate spec content for a module, using AI if a provider is configured.
/// Returns `(spec_content, ai_was_used)`.
fn generate_module_spec(
module_name: &str,
module_files: &[String],
root: &Path,
specs_dir: &Path,
config: &SpecSyncConfig,
provider: Option<&ResolvedProvider>,
) -> String {
) -> (String, bool) {
if let Some(provider) = provider {
// Make paths relative to root for the AI prompt
let rel_files: Vec<String> = module_files
Expand All @@ -649,7 +650,7 @@ fn generate_module_spec(
.collect();

match ai::generate_spec_with_ai(module_name, &rel_files, root, config, provider) {
Ok(spec) => return spec,
Ok(spec) => return (spec, true),
Err(e) => {
eprintln!(
" {} AI generation failed for {module_name}: {e} — falling back to template",
Expand All @@ -659,7 +660,10 @@ fn generate_module_spec(
}
}

generate_spec(module_name, module_files, root, specs_dir)
(
generate_spec(module_name, module_files, root, specs_dir),
false,
)
}

/// Generate companion files (tasks.md, context.md, requirements.md) alongside a spec file.
Expand Down Expand Up @@ -731,7 +735,7 @@ pub fn generate_specs_for_unspecced_modules(
eprintln!(" Generating {rel} with AI...");
}

let spec_content = generate_module_spec(
let (spec_content, ai_used) = generate_module_spec(
module_name,
&module_files,
root,
Expand All @@ -743,8 +747,13 @@ pub fn generate_specs_for_unspecced_modules(
match fs::write(&spec_file, &spec_content) {
Ok(_) => {
let rel = spec_file.strip_prefix(root).unwrap_or(&spec_file).display();
let from = if provider.is_some() && !ai_used {
" from template"
} else {
""
};
println!(
" {} Generated {rel} ({} files)",
" {} Generated {rel}{from} ({} files)",
"✓".green(),
module_files.len()
);
Expand Down Expand Up @@ -789,7 +798,7 @@ pub fn generate_specs_for_unspecced_modules_paths(
continue;
}

let spec_content = generate_module_spec(
let (spec_content, _ai_used) = generate_module_spec(
module_name,
&module_files,
root,
Expand Down
Loading