1use crate::errors::LitError;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub enum TaskStatus {
13 Pending,
14 Accepted,
15 InProgress,
16 Completed,
17 Failed,
18 Rejected,
19}
20
21impl std::fmt::Display for TaskStatus {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 TaskStatus::Pending => write!(f, "pending"),
25 TaskStatus::Accepted => write!(f, "accepted"),
26 TaskStatus::InProgress => write!(f, "in-progress"),
27 TaskStatus::Completed => write!(f, "completed"),
28 TaskStatus::Failed => write!(f, "failed"),
29 TaskStatus::Rejected => write!(f, "rejected"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub enum TaskPriority {
36 Low,
37 Medium,
38 High,
39 Critical,
40}
41
42impl std::fmt::Display for TaskPriority {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 TaskPriority::Low => write!(f, "low"),
46 TaskPriority::Medium => write!(f, "medium"),
47 TaskPriority::High => write!(f, "high"),
48 TaskPriority::Critical => write!(f, "critical"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DelegatedTask {
56 pub id: String,
58 pub delegator: String,
60 pub delegatee: String,
62 pub title: String,
64 pub description: String,
66 pub priority: TaskPriority,
68 pub status: TaskStatus,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub ucan_proof: Option<String>,
73 #[serde(default)]
75 pub scope: Vec<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub deadline: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub result: Option<String>,
82 pub history: Vec<TaskHistoryEntry>,
84 pub created: String,
85 pub updated: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct TaskHistoryEntry {
90 pub status: TaskStatus,
91 pub timestamp: String,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub message: Option<String>,
94}
95
96fn tasks_dir(repo_root: &Path) -> std::path::PathBuf {
97 repo_root.join(".lit").join("delegations")
98}
99
100#[allow(clippy::too_many_arguments)]
102pub fn create_task(
103 repo_root: &Path,
104 delegator: &str,
105 delegatee: &str,
106 title: &str,
107 description: &str,
108 priority: TaskPriority,
109 scope: Vec<String>,
110 deadline: Option<String>,
111 ucan_proof: Option<String>,
112) -> Result<DelegatedTask, LitError> {
113 let dir = tasks_dir(repo_root);
114 fs::create_dir_all(&dir)
115 .map_err(|e| LitError::io(format!("Failed to create delegations dir: {}", e)))?;
116
117 let now = chrono::Utc::now().to_rfc3339();
118 let id = format!("task-{}", chrono::Utc::now().timestamp_millis());
119 let task = DelegatedTask {
120 id: id.clone(),
121 delegator: delegator.to_string(),
122 delegatee: delegatee.to_string(),
123 title: title.to_string(),
124 description: description.to_string(),
125 priority,
126 status: TaskStatus::Pending,
127 ucan_proof,
128 scope,
129 deadline,
130 result: None,
131 history: vec![TaskHistoryEntry {
132 status: TaskStatus::Pending,
133 timestamp: now.clone(),
134 message: Some("Task created".to_string()),
135 }],
136 created: now.clone(),
137 updated: now,
138 };
139
140 let path = dir.join(format!("{}.json", id));
141 let json = serde_json::to_string_pretty(&task)
142 .map_err(|e| LitError::general(format!("Serialize: {}", e)))?;
143 fs::write(&path, json).map_err(|e| LitError::io(format!("Write: {}", e)))?;
144 Ok(task)
145}
146
147pub fn update_task_status(
149 repo_root: &Path,
150 task_id: &str,
151 new_status: TaskStatus,
152 message: Option<String>,
153) -> Result<DelegatedTask, LitError> {
154 let mut task = get_task(repo_root, task_id)?;
155 let now = chrono::Utc::now().to_rfc3339();
156
157 task.history.push(TaskHistoryEntry {
158 status: new_status.clone(),
159 timestamp: now.clone(),
160 message,
161 });
162 task.status = new_status;
163 task.updated = now;
164
165 save_task(repo_root, &task)?;
166 Ok(task)
167}
168
169pub fn complete_task(
171 repo_root: &Path,
172 task_id: &str,
173 result: &str,
174) -> Result<DelegatedTask, LitError> {
175 let mut task = get_task(repo_root, task_id)?;
176 let now = chrono::Utc::now().to_rfc3339();
177
178 task.result = Some(result.to_string());
179 task.history.push(TaskHistoryEntry {
180 status: TaskStatus::Completed,
181 timestamp: now.clone(),
182 message: Some(result.to_string()),
183 });
184 task.status = TaskStatus::Completed;
185 task.updated = now;
186
187 save_task(repo_root, &task)?;
188 Ok(task)
189}
190
191pub fn get_task(repo_root: &Path, task_id: &str) -> Result<DelegatedTask, LitError> {
193 let path = tasks_dir(repo_root).join(format!("{}.json", task_id));
194 if !path.exists() {
195 return Err(LitError::general(format!("Task not found: {}", task_id)));
196 }
197 let json = fs::read_to_string(&path).map_err(|e| LitError::io(format!("IO: {}", e)))?;
198 serde_json::from_str(&json).map_err(|e| LitError::general(format!("Parse: {}", e)))
199}
200
201pub fn list_tasks(
203 repo_root: &Path,
204 agent_did: Option<&str>,
205 status: Option<TaskStatus>,
206) -> Result<Vec<DelegatedTask>, LitError> {
207 let dir = tasks_dir(repo_root);
208 if !dir.exists() {
209 return Ok(Vec::new());
210 }
211
212 let mut tasks = Vec::new();
213 for entry in fs::read_dir(&dir).map_err(|e| LitError::io(format!("IO: {}", e)))? {
214 let entry = entry.map_err(|e| LitError::io(format!("IO: {}", e)))?;
215 if entry.path().extension().is_some_and(|e| e == "json") {
216 if let Ok(json) = fs::read_to_string(entry.path()) {
217 if let Ok(task) = serde_json::from_str::<DelegatedTask>(&json) {
218 let agent_match =
219 agent_did.is_none_or(|did| task.delegator == did || task.delegatee == did);
220 let status_match = status.as_ref().is_none_or(|s| task.status == *s);
221 if agent_match && status_match {
222 tasks.push(task);
223 }
224 }
225 }
226 }
227 }
228
229 tasks.sort_by(|a, b| b.created.cmp(&a.created));
230 Ok(tasks)
231}
232
233fn save_task(repo_root: &Path, task: &DelegatedTask) -> Result<(), LitError> {
234 let path = tasks_dir(repo_root).join(format!("{}.json", task.id));
235 let json = serde_json::to_string_pretty(task)
236 .map_err(|e| LitError::general(format!("Serialize: {}", e)))?;
237 fs::write(&path, json).map_err(|e| LitError::io(format!("Write: {}", e)))?;
238 Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use std::path::PathBuf;
245 use std::sync::atomic::{AtomicU32, Ordering};
246
247 static COUNTER: AtomicU32 = AtomicU32::new(0);
248
249 fn tmp_dir() -> PathBuf {
250 let n = COUNTER.fetch_add(1, Ordering::SeqCst);
251 let dir =
252 std::env::temp_dir().join(format!("lit_delegate_test_{}_{}", std::process::id(), n));
253 fs::create_dir_all(&dir).unwrap();
254 dir
255 }
256
257 #[test]
258 fn test_create_and_list_tasks() {
259 let dir = tmp_dir();
260 let task = create_task(
261 &dir,
262 "did:lit:manager",
263 "did:lit:agent1",
264 "Fix merge bug",
265 "The merge function panics on empty branches",
266 TaskPriority::High,
267 vec!["src/commands/merge.rs".into()],
268 None,
269 None,
270 )
271 .unwrap();
272
273 assert_eq!(task.status, TaskStatus::Pending);
274 assert_eq!(task.delegatee, "did:lit:agent1");
275
276 let tasks = list_tasks(&dir, Some("did:lit:agent1"), None).unwrap();
277 assert_eq!(tasks.len(), 1);
278
279 let _ = fs::remove_dir_all(&dir);
280 }
281
282 #[test]
283 fn test_task_lifecycle() {
284 let dir = tmp_dir();
285 let task = create_task(
286 &dir,
287 "did:lit:a",
288 "did:lit:b",
289 "Task",
290 "Description",
291 TaskPriority::Medium,
292 vec![],
293 None,
294 None,
295 )
296 .unwrap();
297
298 let task =
299 update_task_status(&dir, &task.id, TaskStatus::Accepted, Some("On it".into())).unwrap();
300 assert_eq!(task.status, TaskStatus::Accepted);
301
302 let task = complete_task(&dir, &task.id, "Fixed in commit abc123").unwrap();
303 assert_eq!(task.status, TaskStatus::Completed);
304 assert_eq!(task.history.len(), 3);
305
306 let _ = fs::remove_dir_all(&dir);
307 }
308}