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!(
184        "{} Parsing PRD into tasks...",
185        "Phase 1:".yellow().bold()
186    );
187
188    if dry_run {
189        println!(
190            "  {} Would parse {} into tag '{}'",
191            "→".cyan(),
192            file.display(),
193            tag
194        );
195        println!(
196            "  {} Would create ~{} tasks (append: {})",
197            "→".cyan(),
198            num_tasks,
199            append
200        );
201    } else {
202        ai::parse_prd::run(
203            project_root.clone(),
204            file,
205            tag,
206            num_tasks,
207            append,
208            no_guidance,
209            id_format,
210            model,
211        )
212        .await?;
213    }
214
215    if verbose {
216        println!("  {} Parse phase completed", "✓".green());
217    }
218    println!();
219
220    // ═══════════════════════════════════════════════════════════════════════
221    // Phase 2: Expand complex tasks into subtasks
222    // ═══════════════════════════════════════════════════════════════════════
223    if no_expand {
224        println!(
225            "{} Skipping expansion {}",
226            "Phase 2:".yellow().bold(),
227            "(--no-expand)".dimmed()
228        );
229    } else {
230        println!(
231            "{} Expanding complex tasks into subtasks...",
232            "Phase 2:".yellow().bold()
233        );
234
235        if dry_run {
236            println!(
237                "  {} Would expand tasks with complexity >= 5 in tag '{}'",
238                "→".cyan(),
239                tag
240            );
241        } else {
242            ai::expand::run(
243                project_root.clone(),
244                None,      // task_id - expand all
245                false,     // all_tags - only current tag
246                Some(tag), // tag
247                no_guidance,
248                model,
249            )
250            .await?;
251        }
252
253        if verbose {
254            println!("  {} Expand phase completed", "✓".green());
255        }
256    }
257    println!();
258
259    // ═══════════════════════════════════════════════════════════════════════
260    // Phase 3: Validate dependencies
261    // ═══════════════════════════════════════════════════════════════════════
262    if no_check_deps {
263        println!(
264            "{} Skipping dependency validation {}",
265            "Phase 3:".yellow().bold(),
266            "(--no-check-deps)".dimmed()
267        );
268    } else {
269        println!(
270            "{} Validating task dependencies...",
271            "Phase 3:".yellow().bold()
272        );
273
274        if dry_run {
275            println!(
276                "  {} Would validate dependencies in tag '{}'",
277                "→".cyan(),
278                tag
279            );
280        } else {
281            // Run check-deps without PRD validation (just structural checks)
282            // Use a separate result to avoid early exit on dep issues
283            let check_result = check_deps::run(
284                project_root.clone(),
285                Some(tag), // tag
286                false,     // all_tags
287                None,      // prd_file - no PRD validation in generate
288                false,     // fix
289                model,
290            )
291            .await;
292
293            // Log but don't fail the pipeline on dep issues
294            if let Err(e) = check_result {
295                println!(
296                    "  {} Dependency check encountered issues: {}",
297                    "⚠".yellow(),
298                    e
299                );
300                println!(
301                    "  {} Run '{}' to see details",
302                    "ℹ".blue(),
303                    "scud check-deps".green()
304                );
305            }
306        }
307
308        if verbose {
309            println!("  {} Check-deps phase completed", "✓".green());
310        }
311    }
312    println!();
313
314    // ═══════════════════════════════════════════════════════════════════════
315    // Summary
316    // ═══════════════════════════════════════════════════════════════════════
317    println!("{}", "━".repeat(50).green());
318    println!("{}", "✅ Generate pipeline complete!".green().bold());
319    println!("{}", "━".repeat(50).green());
320    println!();
321
322    if dry_run {
323        println!("{}", "Dry run - no changes were made.".yellow());
324        println!("Run without --dry-run to execute the pipeline.");
325    } else {
326        println!("{}", "Next steps:".blue());
327        println!("  1. Review tasks: scud list --tag {}", tag);
328        println!("  2. View execution waves: scud waves --tag {}", tag);
329        println!("  3. Start working: scud next --tag {}", tag);
330    }
331    println!();
332
333    Ok(())
334}