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 { tasks: claude_tasks };
194 let json = serde_json::to_string_pretty(&task_list)?;
195 std::fs::write(&task_file, json)?;
196
197 Ok(task_file)
198}
199
200pub fn sync_phases(phases: &HashMap<String, Phase>) -> Result<Vec<PathBuf>> {
208 phases
209 .iter()
210 .map(|(tag, phase)| sync_phase(phase, tag))
211 .collect()
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::models::task::Priority;
218
219 #[test]
220 fn test_task_list_id() {
221 assert_eq!(task_list_id("auth"), "scud-auth");
222 assert_eq!(task_list_id("my-feature"), "scud-my-feature");
223 }
224
225 #[test]
226 fn test_claude_task_from_scud_task() {
227 let mut task = Task::new(
228 "1".to_string(),
229 "Implement login".to_string(),
230 "Add login functionality".to_string(),
231 );
232 task.complexity = 5;
233 task.priority = Priority::High;
234 task.dependencies = vec!["setup".to_string()];
235
236 let claude_task = ClaudeTask::from_scud_task(&task, "auth");
237
238 assert_eq!(claude_task.id, "auth:1");
239 assert_eq!(claude_task.subject, "Implement login");
240 assert_eq!(claude_task.status, "pending");
241 assert_eq!(claude_task.blocked_by, vec!["auth:setup"]);
242 }
243
244 #[test]
245 fn test_status_mapping() {
246 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
247
248 task.status = TaskStatus::Pending;
250 assert_eq!(
251 ClaudeTask::from_scud_task(&task, "t").status,
252 "pending"
253 );
254
255 task.status = TaskStatus::InProgress;
257 assert_eq!(
258 ClaudeTask::from_scud_task(&task, "t").status,
259 "in_progress"
260 );
261
262 task.status = TaskStatus::Done;
264 assert_eq!(
265 ClaudeTask::from_scud_task(&task, "t").status,
266 "completed"
267 );
268
269 task.status = TaskStatus::Review;
271 assert_eq!(
272 ClaudeTask::from_scud_task(&task, "t").status,
273 "in_progress"
274 );
275
276 task.status = TaskStatus::Failed;
278 assert_eq!(
279 ClaudeTask::from_scud_task(&task, "t").status,
280 "completed"
281 );
282 }
283
284 #[test]
285 fn test_cross_tag_dependencies() {
286 let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
287 task.dependencies = vec!["other:setup".to_string(), "local".to_string()];
288
289 let claude_task = ClaudeTask::from_scud_task(&task, "auth");
290
291 assert!(claude_task.blocked_by.contains(&"other:setup".to_string()));
293 assert!(claude_task.blocked_by.contains(&"auth:local".to_string()));
294 }
295
296 #[test]
297 fn test_sync_phase() {
298 use tempfile::TempDir;
299
300 let tmp = TempDir::new().unwrap();
302 let original_home = std::env::var("HOME").ok();
303
304 let mut phase = Phase::new("test".to_string());
307
308 let task1 = Task::new("1".to_string(), "First".to_string(), "First task".to_string());
309 let mut task2 = Task::new(
310 "2".to_string(),
311 "Second".to_string(),
312 "Second task".to_string(),
313 );
314 task2.dependencies = vec!["1".to_string()];
315
316 phase.add_task(task1);
317 phase.add_task(task2);
318
319 let claude_tasks: Vec<ClaudeTask> = phase
321 .tasks
322 .iter()
323 .map(|t| ClaudeTask::from_scud_task(t, "test"))
324 .collect();
325
326 assert_eq!(claude_tasks.len(), 2);
327 assert_eq!(claude_tasks[0].id, "test:1");
328 assert_eq!(claude_tasks[1].id, "test:2");
329 assert_eq!(claude_tasks[1].blocked_by, vec!["test:1"]);
330 }
331}