diff --git a/specs/ai/ai.spec.md b/specs/ai/ai.spec.md index ee6c6f2..db47628 100644 --- a/specs/ai/ai.spec.md +++ b/specs/ai/ai.spec.md @@ -30,6 +30,7 @@ Resolves and executes AI providers for spec generation. Supports CLI-based provi | `resolve_ai_provider` | `config, cli_provider` | `Result` | Resolve which AI provider to use via 5-level priority chain | | `resolve_ai_command` | `config, cli_provider` | `Result` | Legacy alias — resolves provider and returns CLI command string | | `generate_spec_with_ai` | `module_name, source_files, root, config, provider` | `Result` | 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` | Regenerate an existing spec using AI when requirements have drifted; reads source files from the spec's frontmatter | ## Invariants @@ -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 diff --git a/src/ai.rs b/src/ai.rs index c22dd2f..50749fa 100644 --- a/src/ai.rs +++ b/src/ai.rs @@ -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. diff --git a/src/generator.rs b/src/generator.rs index 2465f3c..4a81d37 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -628,6 +628,7 @@ 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], @@ -635,7 +636,7 @@ fn generate_module_spec( 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 = module_files @@ -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", @@ -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. @@ -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, @@ -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() ); @@ -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,