scud/commands/
generate.rs

1//! Generate command - combines parse, expand, and check-deps into a single pipeline.
2
3use anyhow::Result;
4use colored::Colorize;
5use std::path::{Path, PathBuf};
6
7use crate::commands::{ai, check_deps};
8
9/// Options for the task generation pipeline.
10///
11/// This struct configures the multi-phase task generation process:
12/// 1. **Parse**: Convert a PRD document into initial tasks
13/// 2. **Expand**: Break down complex tasks into subtasks
14/// 3. **Check Dependencies**: Validate and fix task dependencies
15///
16/// # Example
17///
18/// ```no_run
19/// use scud::commands::generate::{generate, GenerateOptions};
20/// use std::path::PathBuf;
21///
22/// #[tokio::main]
23/// async fn main() -> anyhow::Result<()> {
24///     let options = GenerateOptions::new(
25///         PathBuf::from("docs/prd.md"),
26///         "my-feature".to_string(),
27///     );
28///
29///     generate(options).await?;
30///     Ok(())
31/// }
32/// ```
33#[derive(Debug, Clone)]
34pub struct GenerateOptions {
35    /// Project root directory (None for current directory)
36    pub project_root: Option<PathBuf>,
37    /// Path to the PRD/spec document to parse
38    pub file: PathBuf,
39    /// Tag name for generated tasks
40    pub tag: String,
41    /// Number of tasks to generate (default: 10)
42    pub num_tasks: u32,
43    /// Skip task expansion phase
44    pub no_expand: bool,
45    /// Skip dependency validation phase
46    pub no_check_deps: bool,
47    /// Append tasks to existing tag instead of replacing
48    pub append: bool,
49    /// Skip loading guidance from .scud/guidance/
50    pub no_guidance: bool,
51    /// Task ID format: "sequential" (default) or "uuid"
52    pub id_format: String,
53    /// Model to use for AI operations (overrides config)
54    pub model: Option<String>,
55    /// Show what would be done without making changes
56    pub dry_run: bool,
57    /// Verbose output showing each phase's details
58    pub verbose: bool,
59}
60
61impl GenerateOptions {
62    /// Create new options with required fields and sensible defaults.
63    ///
64    /// # Arguments
65    ///
66    /// * `file` - Path to the PRD/spec document
67    /// * `tag` - Tag name for the generated tasks
68    pub fn new(file: PathBuf, tag: String) -> Self {
69        Self {
70            project_root: None,
71            file,
72            tag,
73            num_tasks: 10,
74            no_expand: false,
75            no_check_deps: false,
76            append: false,
77            no_guidance: false,
78            id_format: "sequential".to_string(),
79            model: None,
80            dry_run: false,
81            verbose: false,
82        }
83    }
84}
85
86impl Default for GenerateOptions {
87    fn default() -> Self {
88        Self {
89            project_root: None,
90            file: PathBuf::new(),
91            tag: String::new(),
92            num_tasks: 10,
93            no_expand: false,
94            no_check_deps: false,
95            append: false,
96            no_guidance: false,
97            id_format: "sequential".to_string(),
98            model: None,
99            dry_run: false,
100            verbose: false,
101        }
102    }
103}
104
105/// Run the task generation pipeline with the given options.
106///
107/// This is the main entry point for programmatic task generation.
108/// It orchestrates the parse → expand → check-deps pipeline.
109///
110/// # Example
111///
112/// ```no_run
113/// use scud::commands::generate::{generate, GenerateOptions};
114/// use std::path::PathBuf;
115///
116/// #[tokio::main]
117/// async fn main() -> anyhow::Result<()> {
118///     let mut options = GenerateOptions::new(
119///         PathBuf::from("requirements.md"),
120///         "api".to_string(),
121///     );
122///     options.num_tasks = 15;
123///     options.verbose = true;
124///
125///     generate(options).await?;
126///     Ok(())
127/// }
128/// ```
129pub async fn generate(options: GenerateOptions) -> Result<()> {
130    run(
131        options.project_root,
132        &options.file,
133        &options.tag,
134        options.num_tasks,
135        options.no_expand,
136        options.no_check_deps,
137        options.append,
138        options.no_guidance,
139        &options.id_format,
140        options.model.as_deref(),
141        options.dry_run,
142        options.verbose,
143    )
144    .await
145}
146
147/// Run the generate pipeline: parse PRD → expand tasks → validate dependencies
148///
149/// This is the internal implementation used by the CLI. For programmatic usage,
150/// prefer the [`generate`] function with [`GenerateOptions`].
151#[allow(clippy::too_many_arguments)]
152pub async fn run(
153    project_root: Option<PathBuf>,
154    file: &Path,
155    tag: &str,
156    num_tasks: u32,
157    no_expand: bool,
158    no_check_deps: bool,
159    append: bool,
160    no_guidance: bool,
161    id_format: &str,
162    model: Option<&str>,
163    dry_run: bool,
164    verbose: bool,
165) -> Result<()> {
166    println!("{}", "━".repeat(50).blue());
167    println!(
168        "{} {}",
169        "Generate Pipeline".blue().bold(),
170        format!("(tag: {})", tag).cyan()
171    );
172    println!("{}", "━".repeat(50).blue());
173    println!();
174
175    if dry_run {
176        println!("{} Dry run mode - no changes will be made", "ℹ".blue());
177        println!();
178    }
179
180    // ═══════════════════════════════════════════════════════════════════════
181    // Phase 1: Parse PRD into tasks
182    // ═══════════════════════════════════════════════════════════════════════
183    println!("{} Parsing PRD into tasks...", "Phase 1:".yellow().bold());
184
185    if dry_run {
186        println!(
187            "  {} Would parse {} into tag '{}'",
188            "→".cyan(),
189            file.display(),
190            tag
191        );
192        println!(
193            "  {} Would create ~{} tasks (append: {})",
194            "→".cyan(),
195            num_tasks,
196            append
197        );
198    } else {
199        ai::parse_prd::run(
200            project_root.clone(),
201            file,
202            tag,
203            num_tasks,
204            append,
205            no_guidance,
206            id_format,
207            model,
208        )
209        .await?;
210    }
211
212    if verbose {
213        println!("  {} Parse phase completed", "✓".green());
214    }
215    println!();
216
217    // ═══════════════════════════════════════════════════════════════════════
218    // Phase 2: Expand complex tasks into subtasks
219    // ═══════════════════════════════════════════════════════════════════════
220    if no_expand {
221        println!(
222            "{} Skipping expansion {}",
223            "Phase 2:".yellow().bold(),
224            "(--no-expand)".dimmed()
225        );
226    } else {
227        println!(
228            "{} Expanding complex tasks into subtasks...",
229            "Phase 2:".yellow().bold()
230        );
231
232        if dry_run {
233            println!(
234                "  {} Would expand tasks with complexity >= 5 in tag '{}'",
235                "→".cyan(),
236                tag
237            );
238        } else {
239            ai::expand::run(
240                project_root.clone(),
241                None,      // task_id - expand all
242                false,     // all_tags - only current tag
243                Some(tag), // tag
244                no_guidance,
245                model,
246            )
247            .await?;
248        }
249
250        if verbose {
251            println!("  {} Expand phase completed", "✓".green());
252        }
253    }
254    println!();
255
256    // ═══════════════════════════════════════════════════════════════════════
257    // Phase 3: Validate dependencies
258    // ═══════════════════════════════════════════════════════════════════════
259    if no_check_deps {
260        println!(
261            "{} Skipping dependency validation {}",
262            "Phase 3:".yellow().bold(),
263            "(--no-check-deps)".dimmed()
264        );
265    } else {
266        println!(
267            "{} Validating task dependencies...",
268            "Phase 3:".yellow().bold()
269        );
270
271        if dry_run {
272            println!(
273                "  {} Would validate dependencies in tag '{}' against PRD",
274                "→".cyan(),
275                tag
276            );
277            println!(
278                "  {} Would auto-fix issues including agent type assignments",
279                "→".cyan()
280            );
281        } else {
282            // Run check-deps with PRD validation and fix mode enabled
283            // This validates against the PRD and auto-fixes issues including agent assignments
284            let check_result = check_deps::run(
285                project_root.clone(),
286                Some(tag),  // tag
287                false,      // all_tags
288                Some(file), // prd_file - validate against PRD
289                true,       // fix - auto-fix issues
290                model,
291            )
292            .await;
293
294            // Log but don't fail the pipeline on dep issues
295            if let Err(e) = check_result {
296                println!(
297                    "  {} Dependency check encountered issues: {}",
298                    "⚠".yellow(),
299                    e
300                );
301                println!(
302                    "  {} Run '{}' to see details",
303                    "ℹ".blue(),
304                    "scud check-deps".green()
305                );
306            }
307        }
308
309        if verbose {
310            println!("  {} Check-deps phase completed", "✓".green());
311        }
312    }
313    println!();
314
315    // ═══════════════════════════════════════════════════════════════════════
316    // Summary
317    // ═══════════════════════════════════════════════════════════════════════
318    println!("{}", "━".repeat(50).green());
319    println!("{}", "✅ Generate pipeline complete!".green().bold());
320    println!("{}", "━".repeat(50).green());
321    println!();
322
323    if dry_run {
324        println!("{}", "Dry run - no changes were made.".yellow());
325        println!("Run without --dry-run to execute the pipeline.");
326    } else {
327        println!("{}", "Next steps:".blue());
328        println!("  1. Review tasks: scud list --tag {}", tag);
329        println!("  2. View execution waves: scud waves --tag {}", tag);
330        println!("  3. Start working: scud next --tag {}", tag);
331    }
332    println!();
333
334    Ok(())
335}