1use super::{get_bool, get_i32, get_i64, get_string, get_string_array, make_tool_with_prompts};
4use crate::config::{AttachmentsConfig, AutoAdvanceConfig, DependenciesConfig, Prompts, StatesConfig, UnknownKeyBehavior};
5use crate::db::Database;
6use crate::error::ToolError;
7use crate::format::{format_scan_result_markdown, format_task_markdown, format_tasks_markdown, markdown_to_json, OutputFormat};
8use crate::types::{parse_priority, ScanResult, TaskTreeInput};
9use anyhow::Result;
10use rmcp::model::Tool;
11use serde_json::{json, Value};
12
13pub fn get_tools(prompts: &Prompts, states_config: &StatesConfig) -> Vec<Tool> {
14 let state_names: Vec<&str> = states_config.state_names();
16 let state_enum: Vec<Value> = state_names.iter().map(|s| json!(s)).collect();
17
18 vec![
19 make_tool_with_prompts(
20 "create",
21 "Create a new task. Use parent for subtasks. Use the link system (block tool) for dependencies.",
22 json!({
23 "id": {
24 "type": "string",
25 "description": "Custom task ID (optional, UUID7 generated if not provided)"
26 },
27 "description": {
28 "type": "string",
29 "description": "Task description (required)"
30 },
31 "parent": {
32 "type": "string",
33 "description": "Parent task ID for nesting"
34 },
35 "priority": {
36 "type": "integer",
37 "description": "Task priority 0-10 (higher = more important, default 5)"
38 },
39 "points": {
40 "type": "integer",
41 "description": "Story points / complexity estimate"
42 },
43 "time_estimate_ms": {
44 "type": "integer",
45 "description": "Estimated duration in milliseconds"
46 },
47 "tags": {
48 "type": "array",
49 "items": { "type": "string" },
50 "description": "Categorization/discovery tags (what the task IS, for querying)"
51 }
52 }),
53 vec!["description"],
54 prompts,
55 ),
56 make_tool_with_prompts(
57 "create_tree",
58 "Create a task tree from nested structure. child_type (default 'contains') links parent→children, sibling_type ('follows' or null) links siblings. Use 'ref' in nodes to include existing tasks.",
59 json!({
60 "tree": {
61 "type": "object",
62 "description": "Nested tree structure with title, children[], etc. Use 'ref' to reference existing tasks.",
63 "properties": {
64 "ref": { "type": "string", "description": "Reference to an existing task ID (other fields ignored when set)" },
65 "id": { "type": "string", "description": "Custom task ID (optional, UUID7 generated if not provided)" },
66 "title": { "type": "string", "description": "Task title (required for new tasks)" },
67 "description": { "type": "string", "description": "Task description" },
68 "priority": { "type": "integer", "description": "Task priority 0-10 (default 5)" },
69 "points": { "type": "integer", "description": "Story points / complexity estimate" },
70 "time_estimate_ms": { "type": "integer", "description": "Estimated duration in milliseconds" },
71 "tags": { "type": "array", "items": { "type": "string" }, "description": "Categorization/discovery tags" },
72 "needed_tags": { "type": "array", "items": { "type": "string" }, "description": "Tags agent must have ALL of to claim (AND)" },
73 "wanted_tags": { "type": "array", "items": { "type": "string" }, "description": "Tags agent must have AT LEAST ONE of to claim (OR)" },
74 "children": { "type": "array", "description": "Child nodes (same structure, recursive)" }
75 }
76 },
77 "parent": {
78 "type": "string",
79 "description": "Optional parent task ID for the tree root"
80 },
81 "child_type": {
82 "type": "string",
83 "description": "Dependency type from parent to children (default: 'contains'). Set to null for no parent-child deps."
84 },
85 "sibling_type": {
86 "type": "string",
87 "description": "Dependency type between consecutive siblings (default: null/parallel). Use 'follows' for sequential."
88 }
89 }),
90 vec!["tree"],
91 prompts,
92 ),
93 make_tool_with_prompts(
94 "get",
95 "Get a single task by ID. Returns detailed task with attachment metadata list and counts by type.",
96 json!({
97 "task": {
98 "type": "string",
99 "description": "Task ID"
100 }
101 }),
102 vec!["task"],
103 prompts,
104 ),
105 make_tool_with_prompts(
106 "list_tasks",
107 "Query tasks with flexible filters.",
108 json!({
109 "status": {
110 "oneOf": [
111 { "type": "string", "enum": state_enum },
112 { "type": "array", "items": { "type": "string" } }
113 ],
114 "description": "Filter by status (single or array)"
115 },
116 "ready": {
117 "type": "boolean",
118 "description": "Filter for claimable tasks: in initial status, unclaimed, all start-blocking deps satisfied. When combined with 'agent', also filters by agent's tag qualifications."
119 },
120 "blocked": {
121 "type": "boolean",
122 "description": "Filter for blocked tasks: have unsatisfied start-blocking dependencies"
123 },
124 "claimed": {
125 "type": "boolean",
126 "description": "Filter for claimed tasks: currently owned by any agent (owner_agent IS NOT NULL)"
127 },
128 "owner": {
129 "type": "string",
130 "description": "Filter by owner agent ID (tasks currently claimed by this specific agent)"
131 },
132 "parent": {
133 "type": "string",
134 "description": "Filter by parent task ID (use 'null' for root tasks)"
135 },
136 "agent": {
137 "type": "string",
138 "description": "Agent ID for filtering. With ready=true, filters tasks the agent is qualified to claim based on agent_tags_all/agent_tags_any requirements."
139 },
140 "tags_any": {
141 "type": "array",
142 "items": { "type": "string" },
143 "description": "Filter tasks that have ANY of these tags (OR)"
144 },
145 "tags_all": {
146 "type": "array",
147 "items": { "type": "string" },
148 "description": "Filter tasks that have ALL of these tags (AND)"
149 },
150 "sort_by": {
151 "type": "string",
152 "enum": ["priority", "created_at", "updated_at"],
153 "description": "Field to sort by (default: created_at for general queries, priority then created_at for ready queries)"
154 },
155 "sort_order": {
156 "type": "string",
157 "enum": ["asc", "desc"],
158 "description": "Sort order: 'asc' for ascending, 'desc' for descending (default: desc for created_at/updated_at, priority always high-to-low)"
159 },
160 "limit": {
161 "type": "integer",
162 "description": "Maximum number of tasks to return"
163 }
164 }),
165 vec![],
166 prompts,
167 ),
168 make_tool_with_prompts(
169 "update",
170 "Update a task's properties. Status changes handle ownership automatically: transitioning to a timed status (e.g., in_progress) claims the task, transitioning to non-timed releases it, transitioning to terminal (e.g., completed) completes it. For push coordination: use assignee to assign a task to another agent (sets owner and transitions to 'assigned' status). Only the owner can update a claimed task unless force=true.",
171 json!({
172 "worker_id": {
173 "type": "string",
174 "description": "Worker ID making the update"
175 },
176 "task": {
177 "type": "string",
178 "description": "Task ID"
179 },
180 "assignee": {
181 "type": "string",
182 "description": "Agent ID to assign the task to (push coordination). Sets owner_agent to assignee and transitions to 'assigned' status. The assignee can then claim (transition to in_progress) when ready."
183 },
184 "status": {
185 "type": "string",
186 "enum": state_enum,
187 "description": "New status"
188 },
189 "title": {
190 "type": "string",
191 "description": "New title"
192 },
193 "description": {
194 "type": "string",
195 "description": "New description"
196 },
197 "priority": {
198 "type": "integer",
199 "description": "New priority 0-10 (higher = more important)"
200 },
201 "points": {
202 "type": "integer",
203 "description": "New points estimate"
204 },
205 "tags": {
206 "type": "array",
207 "items": { "type": "string" },
208 "description": "New categorization/discovery tags"
209 },
210 "needed_tags": {
211 "type": "array",
212 "items": { "type": "string" },
213 "description": "Tags agent must have ALL of to claim (AND)"
214 },
215 "wanted_tags": {
216 "type": "array",
217 "items": { "type": "string" },
218 "description": "Tags agent must have AT LEAST ONE of to claim (OR)"
219 },
220 "time_estimate_ms": {
221 "type": "integer",
222 "description": "Estimated duration in milliseconds"
223 },
224 "reason": {
225 "type": "string",
226 "description": "Reason for the update (stored in audit trail for state transitions)"
227 },
228 "force": {
229 "type": "boolean",
230 "description": "Force ownership changes even if owned by another worker (default: false)"
231 },
232 "attachments": {
233 "type": "array",
234 "description": "List of attachments to add to the task (e.g., commit hashes, changelists, notes)",
235 "items": {
236 "type": "object",
237 "properties": {
238 "name": {
239 "type": "string",
240 "description": "Attachment name/key (e.g., 'commit', 'changelist', 'note')"
241 },
242 "content": {
243 "type": "string",
244 "description": "Attachment content (text)"
245 },
246 "mime": {
247 "type": "string",
248 "description": "MIME type (uses configured default if omitted)"
249 },
250 "mode": {
251 "type": "string",
252 "enum": ["append", "replace"],
253 "description": "How to handle existing attachment with same name (uses configured default if omitted)"
254 }
255 },
256 "required": ["name", "content"]
257 }
258 }
259 }),
260 vec!["worker_id", "task"],
261 prompts,
262 ),
263 make_tool_with_prompts(
264 "delete",
265 "Delete a task. Soft deletes by default (sets deleted_at), use obliterate=true to permanently remove. Rejects if task is claimed by another worker unless force=true.",
266 json!({
267 "worker_id": {
268 "type": "string",
269 "description": "Worker ID attempting to delete"
270 },
271 "task": {
272 "type": "string",
273 "description": "Task ID"
274 },
275 "cascade": {
276 "type": "boolean",
277 "description": "Whether to delete children (default: false)"
278 },
279 "reason": {
280 "type": "string",
281 "description": "Optional reason for deletion"
282 },
283 "obliterate": {
284 "type": "boolean",
285 "description": "If true, permanently deletes the task from the database. If false (default), soft deletes by setting deleted_at timestamp."
286 },
287 "force": {
288 "type": "boolean",
289 "description": "Force deletion even if claimed by another worker (default: false)"
290 }
291 }),
292 vec!["worker_id", "task"],
293 prompts,
294 ),
295 make_tool_with_prompts(
296 "scan",
297 "Scan the task graph from a starting task in multiple directions. Returns related tasks organized by direction: before (predecessors via blocks/follows), after (successors), above (ancestors via contains), below (descendants). Each direction has depth control: 0=none, N=levels, -1=all.",
298 json!({
299 "task": {
300 "type": "string",
301 "description": "Task ID to scan from"
302 },
303 "before": {
304 "type": "integer",
305 "description": "Depth for predecessors (tasks that block this one): 0=none, N=levels, -1=all (default: 0)"
306 },
307 "after": {
308 "type": "integer",
309 "description": "Depth for successors (tasks this one blocks): 0=none, N=levels, -1=all (default: 0)"
310 },
311 "above": {
312 "type": "integer",
313 "description": "Depth for ancestors (parent chain): 0=none, N=levels, -1=all (default: 0)"
314 },
315 "below": {
316 "type": "integer",
317 "description": "Depth for descendants (children tree): 0=none, N=levels, -1=all (default: 0)"
318 },
319 "format": {
320 "type": "string",
321 "enum": ["json", "markdown"],
322 "description": "Output format (default: json)"
323 }
324 }),
325 vec!["task"],
326 prompts,
327 ),
328 ]
329}
330
331pub fn create(db: &Database, states_config: &StatesConfig, args: Value) -> Result<Value> {
332 let id = get_string(&args, "id");
333 let description = get_string(&args, "description")
334 .ok_or_else(|| ToolError::missing_field("description"))?;
335 let parent_id = get_string(&args, "parent");
336 let priority = get_i32(&args, "priority")
338 .or_else(|| get_string(&args, "priority").map(|s| parse_priority(&s)));
339 let points = get_i32(&args, "points");
340 let time_estimate_ms = get_i64(&args, "time_estimate_ms");
341 let tags = get_string_array(&args, "tags");
342 let needed_tags = get_string_array(&args, "needed_tags");
343 let wanted_tags = get_string_array(&args, "wanted_tags");
344
345 let task = db.create_task(
346 id,
347 description,
348 parent_id,
349 priority,
350 points,
351 time_estimate_ms,
352 needed_tags,
353 wanted_tags,
354 tags,
355 states_config,
356 )?;
357
358 Ok(json!({
359 "id": &task.id,
360 "description": task.description,
361 "status": task.status,
362 "priority": task.priority,
363 "created_at": task.created_at
364 }))
365}
366
367pub fn create_tree(db: &Database, states_config: &StatesConfig, args: Value) -> Result<Value> {
368 let tree: TaskTreeInput = serde_json::from_value(
369 args.get("tree")
370 .cloned()
371 .ok_or_else(|| ToolError::missing_field("tree"))?,
372 )?;
373 let parent_id = get_string(&args, "parent");
374 let child_type = get_string(&args, "child_type");
375 let sibling_type = get_string(&args, "sibling_type");
376
377 let (root_id, all_ids) = db.create_task_tree(tree, parent_id, child_type, sibling_type, states_config)?;
378
379 let root_task = db.get_task(&root_id)?
381 .ok_or_else(|| ToolError::new(crate::error::ErrorCode::TaskNotFound, "Root task not found after creation"))?;
382
383 Ok(json!({
384 "root": {
385 "id": root_task.id,
386 "title": root_task.title,
387 "description": root_task.description,
388 "status": root_task.status,
389 "priority": root_task.priority,
390 "created_at": root_task.created_at
391 },
392 "all_ids": all_ids,
393 "count": all_ids.len()
394 }))
395}
396
397pub fn get(db: &Database, default_format: OutputFormat, args: Value) -> Result<Value> {
398 let task_id = get_string(&args, "task")
399 .ok_or_else(|| ToolError::missing_field("task"))?;
400 let format = get_string(&args, "format")
401 .and_then(|s| OutputFormat::from_str(&s))
402 .unwrap_or(default_format);
403
404 let task = db.get_task(&task_id)?
405 .ok_or_else(|| ToolError::new(crate::error::ErrorCode::TaskNotFound, "Task not found"))?;
406
407 let blocked_by = db.get_blockers(&task_id)?;
408
409 let attachments = db.get_attachments(&task_id)?;
411
412 let mut attachment_counts: std::collections::HashMap<String, i32> = std::collections::HashMap::new();
414 for att in &attachments {
415 *attachment_counts.entry(att.mime_type.clone()).or_insert(0) += 1;
416 }
417
418 match format {
419 OutputFormat::Markdown => {
420 let mut md = format_task_markdown(&task, &blocked_by);
421
422 if !attachments.is_empty() {
424 md.push_str("\n### Attachments\n");
425 for att in &attachments {
426 let file_indicator = if att.file_path.is_some() { " (file)" } else { "" };
427 md.push_str(&format!("- **{}** [{}]{}\n", att.name, att.mime_type, file_indicator));
428 }
429
430 md.push_str("\n**Counts by type:**\n");
432 for (mime_type, count) in &attachment_counts {
433 md.push_str(&format!("- {}: {}\n", mime_type, count));
434 }
435 }
436
437 Ok(markdown_to_json(md))
438 }
439 OutputFormat::Json => {
440 let mut task_json = serde_json::to_value(&task)?;
441 if let Some(obj) = task_json.as_object_mut() {
442 obj.insert("blocked_by".to_string(), json!(blocked_by));
443 obj.insert("attachments".to_string(), serde_json::to_value(&attachments)?);
444 obj.insert("attachment_counts".to_string(), serde_json::to_value(&attachment_counts)?);
445 }
446 Ok(task_json)
447 }
448 }
449}
450
451pub fn list_tasks(
452 db: &Database,
453 states_config: &StatesConfig,
454 deps_config: &DependenciesConfig,
455 default_format: OutputFormat,
456 args: Value,
457) -> Result<Value> {
458 let format = get_string(&args, "format")
459 .and_then(|s| OutputFormat::from_str(&s))
460 .unwrap_or(default_format);
461
462 let ready = get_bool(&args, "ready").unwrap_or(false);
463 let blocked = get_bool(&args, "blocked").unwrap_or(false);
464 let claimed = get_bool(&args, "claimed").unwrap_or(false);
465 let limit = get_i32(&args, "limit");
466
467 let tags_any = get_string_array(&args, "tags_any");
469 let tags_all = get_string_array(&args, "tags_all");
470
471 let agent_id = get_string(&args, "agent");
473
474 let sort_by = get_string(&args, "sort_by");
476 let sort_order = get_string(&args, "sort_order");
477
478 let mut tasks = if ready {
480 db.get_ready_tasks(agent_id.as_deref(), states_config, deps_config, sort_by.as_deref(), sort_order.as_deref())?
483 } else if blocked {
484 db.get_blocked_tasks(states_config, deps_config, sort_by.as_deref(), sort_order.as_deref())?
486 } else if claimed {
487 db.get_claimed_tasks(None)?
489 } else {
490 let status_vec: Option<Vec<String>> = if let Some(status_val) = args.get("status") {
493 if let Some(s) = status_val.as_str() {
494 Some(vec![s.to_string()])
495 } else if let Some(arr) = status_val.as_array() {
496 Some(arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
497 } else {
498 None
499 }
500 } else {
501 None
502 };
503 let owner = get_string(&args, "owner");
504 let parent_id_str = get_string(&args, "parent");
505 let parent_id: Option<Option<&str>> = match &parent_id_str {
506 Some(pid_str) if pid_str == "null" => Some(None), Some(pid_str) => Some(Some(pid_str.as_str())),
508 None => None,
509 };
510
511 let has_tag_filters = tags_any.is_some() || tags_all.is_some() || agent_id.is_some();
513
514 if has_tag_filters {
515 let qualified_agent_tags = if let Some(aid) = &agent_id {
518 Some(db.get_agent_tags(aid)?)
519 } else {
520 None
521 };
522
523 db.list_tasks_with_tag_filters(
524 status_vec,
525 owner.as_deref(),
526 parent_id,
527 tags_any,
528 tags_all,
529 qualified_agent_tags,
530 limit,
531 sort_by.as_deref(),
532 sort_order.as_deref(),
533 )?
534 } else {
535 let status = status_vec.as_ref().and_then(|v| v.first().map(|s| s.as_str()));
537 db.list_tasks(status, owner.as_deref(), parent_id, limit, sort_by.as_deref(), sort_order.as_deref())?
538 }
539 };
540
541 if let Some(l) = limit {
543 tasks.truncate(l as usize);
544 }
545
546 let tasks_with_blockers: Vec<_> = tasks
548 .into_iter()
549 .map(|task| {
550 let blockers = db.get_blockers(&task.id).unwrap_or_default();
551 (task, blockers)
552 })
553 .collect();
554
555 match format {
556 OutputFormat::Markdown => Ok(markdown_to_json(format_tasks_markdown(
557 &tasks_with_blockers,
558 states_config,
559 ))),
560 OutputFormat::Json => Ok(json!({
561 "tasks": tasks_with_blockers.iter().map(|(task, blockers)| {
562 let mut task_json = serde_json::to_value(task).unwrap();
563 if let Some(obj) = task_json.as_object_mut() {
564 obj.insert("blocked_by".to_string(), json!(blockers));
565 }
566 task_json
567 }).collect::<Vec<_>>()
568 })),
569 }
570}
571
572pub fn update(
573 db: &Database,
574 attachments_config: &AttachmentsConfig,
575 states_config: &StatesConfig,
576 deps_config: &DependenciesConfig,
577 auto_advance: &AutoAdvanceConfig,
578 args: Value,
579) -> Result<Value> {
580 let worker_id = get_string(&args, "worker_id")
581 .ok_or_else(|| ToolError::missing_field("worker_id"))?;
582 let task_id = get_string(&args, "task")
583 .ok_or_else(|| ToolError::missing_field("task"))?;
584 let assignee = get_string(&args, "assignee");
585 let title = get_string(&args, "title");
586 let description = if args.get("description").is_some() {
587 Some(get_string(&args, "description"))
588 } else {
589 None
590 };
591 let status = get_string(&args, "status");
592 let priority = get_i32(&args, "priority")
594 .or_else(|| get_string(&args, "priority").map(|s| parse_priority(&s)));
595 let points = if args.get("points").is_some() {
596 Some(get_i32(&args, "points"))
597 } else {
598 None
599 };
600 let tags = if args.get("tags").is_some() {
601 Some(get_string_array(&args, "tags").unwrap_or_default())
602 } else {
603 None
604 };
605 let needed_tags = if args.get("needed_tags").is_some() {
606 Some(get_string_array(&args, "needed_tags").unwrap_or_default())
607 } else {
608 None
609 };
610 let wanted_tags = if args.get("wanted_tags").is_some() {
611 Some(get_string_array(&args, "wanted_tags").unwrap_or_default())
612 } else {
613 None
614 };
615 let time_estimate_ms = get_i64(&args, "time_estimate_ms");
616 let reason = get_string(&args, "reason");
617 let force = get_bool(&args, "force").unwrap_or(false);
618
619 let mut attachment_results: Vec<Value> = Vec::new();
621 let mut attachment_warnings: Vec<String> = Vec::new();
622
623 if let Some(attachments_arr) = args.get("attachments").and_then(|v| v.as_array()) {
624 for att_value in attachments_arr {
625 let name = att_value.get("name").and_then(|v| v.as_str());
626 let content = att_value.get("content").and_then(|v| v.as_str());
627 let mime_override = att_value.get("mime").and_then(|v| v.as_str());
628 let mode_override = att_value.get("mode").and_then(|v| v.as_str());
629
630 let name = match name {
631 Some(n) => n,
632 None => {
633 attachment_warnings.push("Skipped attachment: missing 'name' field".to_string());
634 continue;
635 }
636 };
637
638 let content = match content {
639 Some(c) => c,
640 None => {
641 attachment_warnings.push(format!(
642 "Skipped attachment '{}': missing 'content' field",
643 name
644 ));
645 continue;
646 }
647 };
648
649 if !attachments_config.is_known_key(name) {
651 match attachments_config.unknown_key {
652 UnknownKeyBehavior::Reject => {
653 attachment_warnings.push(format!(
654 "Rejected attachment '{}': unknown key (configure in attachments.definitions or set unknown_key to 'allow')",
655 name
656 ));
657 continue;
658 }
659 UnknownKeyBehavior::Warn => {
660 attachment_warnings.push(format!("Unknown attachment key '{}'", name));
661 }
662 UnknownKeyBehavior::Allow => {}
663 }
664 }
665
666 let mime_type = mime_override
668 .map(String::from)
669 .unwrap_or_else(|| attachments_config.get_mime_default(name).to_string());
670 let mode = mode_override.unwrap_or_else(|| attachments_config.get_mode_default(name));
671
672 if mode != "append" && mode != "replace" {
674 attachment_warnings.push(format!(
675 "Skipped attachment '{}': mode must be 'append' or 'replace'",
676 name
677 ));
678 continue;
679 }
680
681 if mode == "replace" {
683 let _ = db.delete_attachment_by_name(&task_id, name);
684 }
685
686 match db.add_attachment(&task_id, name.to_string(), content.to_string(), Some(mime_type.clone()), None) {
688 Ok(order_index) => {
689 attachment_results.push(json!({
690 "name": name,
691 "order_index": order_index,
692 "mime_type": mime_type
693 }));
694 }
695 Err(e) => {
696 attachment_warnings.push(format!(
697 "Failed to add attachment '{}': {}",
698 name, e
699 ));
700 }
701 }
702 }
703 }
704
705 let (task, unblocked, auto_advanced) = db.update_task_unified(
707 &task_id,
708 &worker_id,
709 assignee.as_deref(),
710 title,
711 description,
712 status,
713 priority,
714 points,
715 tags,
716 needed_tags,
717 wanted_tags,
718 time_estimate_ms,
719 reason,
720 force,
721 states_config,
722 deps_config,
723 auto_advance,
724 )?;
725
726 let mut response = serde_json::to_value(&task)?;
728 if let Value::Object(ref mut map) = response {
729 if !unblocked.is_empty() {
731 map.insert("unblocked".to_string(), json!(unblocked));
732 }
733 if !auto_advanced.is_empty() {
735 map.insert("auto_advanced".to_string(), json!(auto_advanced));
736 }
737 if !attachment_results.is_empty() {
739 map.insert("attachments_added".to_string(), json!(attachment_results));
740 }
741 if !attachment_warnings.is_empty() {
743 map.insert("attachment_warnings".to_string(), json!(attachment_warnings));
744 }
745 }
746
747 Ok(response)
748}
749
750pub fn delete(db: &Database, args: Value) -> Result<Value> {
751 let worker_id = get_string(&args, "worker_id")
752 .ok_or_else(|| ToolError::missing_field("worker_id"))?;
753 let task_id = get_string(&args, "task")
754 .ok_or_else(|| ToolError::missing_field("task"))?;
755 let cascade = get_bool(&args, "cascade").unwrap_or(false);
756 let reason = get_string(&args, "reason");
757 let obliterate = get_bool(&args, "obliterate").unwrap_or(false);
758 let force = get_bool(&args, "force").unwrap_or(false);
759
760 db.delete_task(&task_id, &worker_id, cascade, reason, obliterate, force)?;
761
762 Ok(json!({
763 "success": true,
764 "soft_deleted": !obliterate
765 }))
766}
767
768pub fn scan(db: &Database, default_format: OutputFormat, args: Value) -> Result<Value> {
769 let task_id = get_string(&args, "task")
770 .ok_or_else(|| ToolError::missing_field("task"))?;
771 let format = get_string(&args, "format")
772 .and_then(|s| OutputFormat::from_str(&s))
773 .unwrap_or(default_format);
774
775 let before_depth = get_i32(&args, "before").unwrap_or(0);
777 let after_depth = get_i32(&args, "after").unwrap_or(0);
778 let above_depth = get_i32(&args, "above").unwrap_or(0);
779 let below_depth = get_i32(&args, "below").unwrap_or(0);
780
781 let root_task = db.get_task(&task_id)?
783 .ok_or_else(|| ToolError::new(crate::error::ErrorCode::TaskNotFound, "Task not found"))?;
784
785 let before = db.get_predecessors(&task_id, before_depth)?;
787 let after = db.get_successors(&task_id, after_depth)?;
788 let above = db.get_ancestors(&task_id, above_depth)?;
789 let below = db.get_descendants(&task_id, below_depth)?;
790
791 let result = ScanResult {
792 root: root_task,
793 before,
794 after,
795 above,
796 below,
797 };
798
799 match format {
800 OutputFormat::Markdown => Ok(markdown_to_json(format_scan_result_markdown(&result))),
801 OutputFormat::Json => Ok(serde_json::to_value(&result)?),
802 }
803}