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}