use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::models::phase::Phase;
use crate::models::task::{Task, TaskStatus};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeTask {
pub id: String,
pub subject: String,
#[serde(default)]
pub description: String,
pub status: String,
#[serde(default, rename = "blockedBy")]
pub blocked_by: Vec<String>,
#[serde(default)]
pub blocks: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(default)]
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClaudeTaskList {
pub tasks: Vec<ClaudeTask>,
}
impl ClaudeTask {
pub fn from_scud_task(task: &Task, tag: &str) -> Self {
let status = match task.status {
TaskStatus::Pending => "pending",
TaskStatus::InProgress => "in_progress",
TaskStatus::Done => "completed",
TaskStatus::Blocked | TaskStatus::Deferred => "pending",
TaskStatus::Failed | TaskStatus::Cancelled => "completed",
TaskStatus::Review => "in_progress",
TaskStatus::Expanded => "completed",
};
ClaudeTask {
id: format!("{}:{}", tag, task.id),
subject: task.title.clone(),
description: task.description.clone(),
status: status.to_string(),
blocked_by: task
.dependencies
.iter()
.map(|d: &String| {
if d.contains(':') {
d.clone()
} else {
format!("{}:{}", tag, d)
}
})
.collect(),
blocks: vec![], owner: task.assigned_to.clone(),
metadata: serde_json::json!({
"scud_tag": tag,
"scud_status": format!("{:?}", task.status),
"complexity": task.complexity,
"priority": format!("{:?}", task.priority),
"agent_type": task.agent_type,
}),
}
}
}
pub fn claude_tasks_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude")
.join("tasks")
}
pub fn task_list_id(tag: &str) -> String {
format!("scud-{}", tag)
}
pub fn sync_phase(phase: &Phase, tag: &str) -> Result<PathBuf> {
let tasks_dir = claude_tasks_dir();
std::fs::create_dir_all(&tasks_dir)?;
let list_id = task_list_id(tag);
let task_file = tasks_dir.join(format!("{}.json", list_id));
let mut blocks_map: HashMap<String, Vec<String>> = HashMap::new();
for task in phase.tasks.iter() {
let task_full_id = format!("{}:{}", tag, task.id);
for dep in task.dependencies.iter() {
let dep_full_id: String = if dep.contains(':') {
dep.clone()
} else {
format!("{}:{}", tag, dep)
};
blocks_map
.entry(dep_full_id)
.or_default()
.push(task_full_id.clone());
}
}
let claude_tasks: Vec<ClaudeTask> = phase
.tasks
.iter()
.filter(|t: &&Task| !t.is_expanded()) .map(|t: &Task| {
let mut ct = ClaudeTask::from_scud_task(t, tag);
let full_id = format!("{}:{}", tag, t.id);
ct.blocks = blocks_map.get(&full_id).cloned().unwrap_or_default();
ct
})
.collect();
let task_list = ClaudeTaskList {
tasks: claude_tasks,
};
let json = serde_json::to_string_pretty(&task_list)?;
std::fs::write(&task_file, json)?;
Ok(task_file)
}
pub fn sync_phases(phases: &HashMap<String, Phase>) -> Result<Vec<PathBuf>> {
phases
.iter()
.map(|(tag, phase)| sync_phase(phase, tag))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::task::Priority;
#[test]
fn test_task_list_id() {
assert_eq!(task_list_id("auth"), "scud-auth");
assert_eq!(task_list_id("my-feature"), "scud-my-feature");
}
#[test]
fn test_claude_task_from_scud_task() {
let mut task = Task::new(
"1".to_string(),
"Implement login".to_string(),
"Add login functionality".to_string(),
);
task.complexity = 5;
task.priority = Priority::High;
task.dependencies = vec!["setup".to_string()];
let claude_task = ClaudeTask::from_scud_task(&task, "auth");
assert_eq!(claude_task.id, "auth:1");
assert_eq!(claude_task.subject, "Implement login");
assert_eq!(claude_task.status, "pending");
assert_eq!(claude_task.blocked_by, vec!["auth:setup"]);
}
#[test]
fn test_status_mapping() {
let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
task.status = TaskStatus::Pending;
assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "pending");
task.status = TaskStatus::InProgress;
assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "in_progress");
task.status = TaskStatus::Done;
assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "completed");
task.status = TaskStatus::Review;
assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "in_progress");
task.status = TaskStatus::Failed;
assert_eq!(ClaudeTask::from_scud_task(&task, "t").status, "completed");
}
#[test]
fn test_cross_tag_dependencies() {
let mut task = Task::new("1".to_string(), "Test".to_string(), "Desc".to_string());
task.dependencies = vec!["other:setup".to_string(), "local".to_string()];
let claude_task = ClaudeTask::from_scud_task(&task, "auth");
assert!(claude_task.blocked_by.contains(&"other:setup".to_string()));
assert!(claude_task.blocked_by.contains(&"auth:local".to_string()));
}
#[test]
fn test_sync_phase() {
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let original_home = std::env::var("HOME").ok();
let mut phase = Phase::new("test".to_string());
let task1 = Task::new(
"1".to_string(),
"First".to_string(),
"First task".to_string(),
);
let mut task2 = Task::new(
"2".to_string(),
"Second".to_string(),
"Second task".to_string(),
);
task2.dependencies = vec!["1".to_string()];
phase.add_task(task1);
phase.add_task(task2);
let claude_tasks: Vec<ClaudeTask> = phase
.tasks
.iter()
.map(|t| ClaudeTask::from_scud_task(t, "test"))
.collect();
assert_eq!(claude_tasks.len(), 2);
assert_eq!(claude_tasks[0].id, "test:1");
assert_eq!(claude_tasks[1].id, "test:2");
assert_eq!(claude_tasks[1].blocked_by, vec!["test:1"]);
}
}