ralph/commands/prd/
workflow.rs1use std::path::PathBuf;
16
17use anyhow::{Context, Result, bail};
18
19use crate::contracts::{TaskPriority, TaskStatus};
20use crate::{config, queue, timeutil};
21
22use super::generate::{generate_multi_tasks, generate_single_task};
23use super::parse::parse_prd;
24
25pub struct CreateOptions {
26 pub path: PathBuf,
27 pub multi: bool,
28 pub dry_run: bool,
29 pub priority: Option<TaskPriority>,
30 pub tags: Vec<String>,
31 pub draft: bool,
32}
33
34pub fn create_from_prd(
35 resolved: &config::Resolved,
36 opts: &CreateOptions,
37 force: bool,
38) -> Result<()> {
39 if !opts.path.exists() {
40 bail!(
41 "PRD file not found: {}. Check the path and try again.",
42 opts.path.display()
43 );
44 }
45
46 let content = std::fs::read_to_string(&opts.path)
47 .with_context(|| format!("Failed to read PRD file: {}", opts.path.display()))?;
48 if content.trim().is_empty() {
49 bail!("PRD file is empty: {}", opts.path.display());
50 }
51
52 let parsed = parse_prd(&content);
53 if parsed.title.is_empty() {
54 bail!(
55 "Could not extract title from PRD: {}. Ensure the file has a # Heading at the start.",
56 opts.path.display()
57 );
58 }
59
60 let _queue_lock = if !opts.dry_run {
61 Some(queue::acquire_queue_lock(
62 &resolved.repo_root,
63 "prd create",
64 force,
65 )?)
66 } else {
67 None
68 };
69
70 let mut queue_file = queue::load_queue(&resolved.queue_path)?;
71 let done_file = queue::load_queue_or_default(&resolved.done_path)?;
72 let done_ref = if done_file.tasks.is_empty() && !resolved.done_path.exists() {
73 None
74 } else {
75 Some(&done_file)
76 };
77
78 let insert_index = queue::suggest_new_task_insert_index(&queue_file);
79 let now = timeutil::now_utc_rfc3339()?;
80 let priority = opts.priority.unwrap_or(TaskPriority::Medium);
81 let status = if opts.draft {
82 TaskStatus::Draft
83 } else {
84 TaskStatus::Todo
85 };
86 let max_depth = resolved.config.queue.max_dependency_depth.unwrap_or(10);
87
88 let tasks = if opts.multi {
89 generate_multi_tasks(
90 &parsed,
91 &now,
92 priority,
93 status,
94 &opts.tags,
95 &queue_file,
96 done_ref,
97 &resolved.id_prefix,
98 resolved.id_width,
99 max_depth,
100 )?
101 } else {
102 vec![generate_single_task(
103 &parsed,
104 &now,
105 priority,
106 status,
107 &opts.tags,
108 &queue_file,
109 done_ref,
110 &resolved.id_prefix,
111 resolved.id_width,
112 max_depth,
113 )?]
114 };
115
116 if tasks.is_empty() {
117 bail!(
118 "No tasks generated from PRD: {}. Check the file format.",
119 opts.path.display()
120 );
121 }
122
123 if opts.dry_run {
124 print_preview(&tasks);
125 return Ok(());
126 }
127
128 let new_task_ids: Vec<String> = tasks.iter().map(|task| task.id.clone()).collect();
129 for task in tasks {
130 queue_file.tasks.insert(insert_index, task);
131 }
132 queue::save_queue(&resolved.queue_path, &queue_file)?;
133
134 println!("Created {} task(s) from PRD:", new_task_ids.len());
135 for id in &new_task_ids {
136 println!(" {}", id);
137 }
138 Ok(())
139}
140
141fn print_preview(tasks: &[crate::contracts::Task]) {
142 println!("Dry run - would create {} task(s):", tasks.len());
143 for task in tasks {
144 println!("\n ID: {}", task.id);
145 println!(" Title: {}", task.title);
146 println!(" Priority: {}", task.priority);
147 println!(" Status: {}", task.status);
148 if !task.tags.is_empty() {
149 println!(" Tags: {}", task.tags.join(", "));
150 }
151 if let Some(request) = &task.request {
152 println!(" Request: {}", request.lines().next().unwrap_or(request));
153 }
154 }
155}