1use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11use crate::models::phase::Phase;
12use crate::models::task::{Task, TaskStatus};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ClaudeTask {
20 pub id: String,
22
23 pub subject: String,
25
26 #[serde(default)]
28 pub description: String,
29
30 pub status: String,
32
33 #[serde(default, rename = "blockedBy")]
35 pub blocked_by: Vec<String>,
36
37 #[serde(default)]
39 pub blocks: Vec<String>,
40
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub owner: Option<String>,
44
45 #[serde(default)]
47 pub metadata: serde_json::Value,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ClaudeTaskList {
53 pub tasks: Vec<ClaudeTask>,
55}
56
57impl ClaudeTask {
58 pub fn from_scud_task(task: &Task, tag: &str) -> Self {
67 let status = match task.status {
68 TaskStatus::Pending => "pending",
69 TaskStatus::InProgress => "in_progress",
70 TaskStatus::Done => "completed",
71 TaskStatus::Blocked | TaskStatus::Deferred => "pending",
73 TaskStatus::Failed | TaskStatus::Cancelled => "completed",
74 TaskStatus::Review => "in_progress",
75 TaskStatus::Expanded => "completed",
76 };
77
78 ClaudeTask {
79 id: format!("{}:{}", tag, task.id),
80 subject: task.title.clone(),
81 description: task.description.clone(),
82 status: status.to_string(),
83 blocked_by: task
84 .dependencies
85 .iter()
86 .map(|d: &String| {
87 if d.contains(':') {
89 d.clone()
90 } else {
91 format!("{}:{}", tag, d)
92 }
93 })
94 .collect(),
95 blocks: vec![], owner: task.assigned_to.clone(),
97 metadata: serde_json::json!({
98 "scud_tag": tag,
99 "scud_status": format!("{:?}", task.status),
100 "complexity": task.complexity,
101 "priority": format!("{:?}", task.priority),
102 "agent_type": task.agent_type,
103 }),
104 }
105 }
106}
107
108pub fn claude_tasks_dir() -> PathBuf {
112 dirs::home_dir()
113 .unwrap_or_else(|| PathBuf::from("."))
114 .join(".claude")
115 .join("tasks")
116}
117
118pub fn task_list_id(tag: &str) -> String {
129 format!("scud-{}", tag)
130}
131
132pub fn sync_phase(phase: &Phase, tag: &str) -> Result<PathBuf> {
155 let tasks_dir = claude_tasks_dir();
156 std::fs::create_dir_all(&tasks_dir)?;
157
158 let list_id = task_list_id(tag);
159 let task_file = tasks_dir.join(format!("{}.json", list_id));
160
161 let mut blocks_map: HashMap<String, Vec<String>> = HashMap::new();
163
164 for task in phase.tasks.iter() {
165 let task_full_id = format!("{}:{}", tag, task.id);
166 for dep in task.dependencies.iter() {
167 let dep_full_id: String = if dep.contains(':') {
169 dep.clone()
170 } else {
171 format!("{}:{}", tag, dep)
172 };
173 blocks_map
174 .entry(dep_full_id)
175 .or_default()
176 .push(task_full_id.clone());
177 }
178 }
179
180 let claude_tasks: Vec<ClaudeTask> = phase
182 .tasks
183 .iter()
184 .filter(|t: &&Task| !t.is_expanded()) .map(|t: &Task| {
186 let mut ct = ClaudeTask::from_scud_task(t, tag);
187 let full_id = format!("{}:{}", tag, t.id);
188 ct.blocks = blocks_map.get(&full_id).cloned().unwrap_or_default();
189 ct
190 })
191 .collect();
192
193 let task_list = ClaudeTaskList {
194 tasks: claude_tasks,
195 };
196 let json = serde_json::to_string_pretty(&task_list)?;
197 std::fs::write(&task_file, json)?;
198
199 Ok(task_file)
200}
201
202pub fn sync_phases(phases: &HashMap<String, Phase>) -> Result<Vec<PathBuf>> {
210 phases
211 .iter()
212 .map(|(tag, phase)| sync_phase(phase, tag))
213 .collect()
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::models::task::Priority;
220
221 #[test]
222 fn test_task_list_id() {
223 assert_eq!(task_list_id("auth"), "scud-auth");
224 assert_eq!(task_list_id("my-feature"), "scud-my-feature");
225 }
226
227 #[test]
228 fn test_claude_task_from_scud_task() {
229 let mut task = Task::new(
230 "1".to_string(),
231 "Implement login".to_string(),
232 "Add login functionality".to_string(),
233 );
234 task.complexity = 5;
235 task.priority = Priority::High;
236 task.dependencies = vec!["setup".to_string()];
237
238 let claude_task = ClaudeTask::from_scud_task(&task, "auth");
239
240 assert_eq!(claude_task.id, "auth:1");
241 assert_eq!(claude_task.subject, "Implement login");
242 assert_eq!(claude_task.status, "pending");
243 assert_eq!(claude_task.blocked_by, vec!["auth:setup"]);
244 }
245
246 #[test]
247 fn test_status_mapping() {
248 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
249
250 task.status = TaskStatus::Pending;
252 assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "pending");
253
254 task.status = TaskStatus::InProgress;
256 assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "in_progress");
257
258 task.status = TaskStatus::Done;
260 assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "completed");
261
262 task.status = TaskStatus::Review;
264 assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "in_progress");
265
266 task.status = TaskStatus::Failed;
268 assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "completed");
269 }
270
271 #[test]
272 fn test_cross_tag_dependencies() {
273 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
274 task.dependencies = vec!["other:setup".to_string(), "local".to_string()];
275
276 let claude_task = ClaudeTask::from_scud_task(&task, "auth");
277
278 assert!(claude_task.blocked_by.contains(&"other:setup".to_string()));
280 assert!(claude_task.blocked_by.contains(&"auth:local".to_string()));
281 }
282
283 #[test]
284 fn test_sync_phase() {
285 use tempfile::TempDir;
286
287 let tmp = TempDir::new().unwrap();
289 let original_home = std::env::var("HOME").ok();
290
291 let mut phase = Phase::new("test".to_string());
294
295 let task1 = Task::new(
296 "1".to_string(),
297 "First".to_string(),
298 "First task".to_string(),
299 );
300 let mut task2 = Task::new(
301 "2".to_string(),
302 "Second".to_string(),
303 "Second task".to_string(),
304 );
305 task2.dependencies = vec!["1".to_string()];
306
307 phase.add_task(task1);
308 phase.add_task(task2);
309
310 let claude_tasks: Vec<ClaudeTask> = phase
312 .tasks
313 .iter()
314 .map(|t| ClaudeTask::from_scud_task(t, "test"))
315 .collect();
316
317 assert_eq!(claude_tasks.len(), 2);
318 assert_eq!(claude_tasks[0].id, "test:1");
319 assert_eq!(claude_tasks[1].id, "test:2");
320 assert_eq!(claude_tasks[1].blocked_by, vec!["test:1"]);
321 }
322}