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}