Skip to main content

task_graph_mcp/tools/
claiming.rs

1//! Task claiming tools.
2//!
3//! The `claim` tool is a convenience wrapper around `update` that transitions
4//! a task to the first timed state. For releasing tasks, use `update` with
5//! a non-timed state (ownership clears automatically).
6
7use super::{get_bool, get_string, make_tool_with_prompts};
8use crate::config::{AutoAdvanceConfig, DependenciesConfig, PhasesConfig, Prompts, StatesConfig};
9use crate::db::Database;
10use crate::error::ToolError;
11use crate::prompts::PromptContext;
12use anyhow::Result;
13use rmcp::model::Tool;
14use serde_json::{Value, json};
15
16pub fn get_tools(prompts: &Prompts, _states_config: &StatesConfig) -> Vec<Tool> {
17    vec![make_tool_with_prompts(
18        "claim",
19        "Commit to working on a task (like adding to a changelist). Fails if: already claimed, deps unsatisfied, or worker lacks required tags. Sets status to timed (working) status.",
20        json!({
21            "worker_id": {
22                "type": "string",
23                "description": "Worker ID claiming the task"
24            },
25            "task": {
26                "type": "string",
27                "description": "Task ID to claim"
28            },
29            "force": {
30                "type": "boolean",
31                "description": "Force claim even if owned by another agent (default: false)"
32            }
33        }),
34        vec!["worker_id", "task"],
35        prompts,
36    )]
37}
38
39pub fn claim(
40    db: &Database,
41    states_config: &StatesConfig,
42    phases_config: &PhasesConfig,
43    deps_config: &DependenciesConfig,
44    auto_advance: &AutoAdvanceConfig,
45    workflows: &crate::config::workflows::WorkflowsConfig,
46    args: Value,
47) -> Result<Value> {
48    let worker_id =
49        get_string(&args, "worker_id").ok_or_else(|| ToolError::missing_field("worker_id"))?;
50    let task_id = get_string(&args, "task").ok_or_else(|| ToolError::missing_field("task"))?;
51    let force = get_bool(&args, "force").unwrap_or(false);
52
53    // Find the first timed state to use for claiming
54    let claim_status = states_config
55        .definitions
56        .iter()
57        .find(|(_, def)| def.timed)
58        .map(|(name, _)| name.clone())
59        .unwrap_or_else(|| "working".to_string());
60
61    // Use unified update which handles claiming when transitioning to timed state
62    // Claim transitions TO a blocking state, so unblocked/auto_advanced will be empty
63    let (task, _unblocked, _auto_advanced) = db.update_task_unified(
64        &task_id,
65        &worker_id,
66        None,               // assignee (not assigning to another agent)
67        None,               // title
68        None,               // description
69        Some(claim_status), // status - first timed state
70        None,               // phase
71        None,               // priority
72        None,               // points
73        None,               // tags
74        None,               // needed_tags
75        None,               // wanted_tags
76        None,               // time_estimate_ms
77        None,               // reason
78        force,
79        states_config,
80        deps_config,
81        auto_advance,
82    )?;
83
84    // Get transition prompts for claiming (with template expansion)
85    let transition_prompt_list: Vec<String> = {
86        match db.update_worker_state(&worker_id, Some(&task.status), task.phase.as_deref()) {
87            Ok((old_status, old_phase)) => {
88                // Create context for template expansion
89                let ctx = PromptContext::new(
90                    &task.status,
91                    task.phase.as_deref(),
92                    states_config,
93                    phases_config,
94                );
95                crate::prompts::get_transition_prompts_with_context(
96                    old_status.as_deref().unwrap_or(""),
97                    old_phase.as_deref(),
98                    &task.status,
99                    task.phase.as_deref(),
100                    workflows,
101                    &ctx,
102                )
103            }
104            Err(_) => vec![],
105        }
106    };
107
108    let mut response = json!({
109        "success": true,
110        "task": {
111            "id": &task.id,
112            "title": task.title,
113            "status": task.status,
114            "worker_id": task.worker_id,
115            "claimed_at": task.claimed_at
116        }
117    });
118
119    // Add prompts if any
120    if !transition_prompt_list.is_empty() {
121        if let Value::Object(ref mut map) = response {
122            map.insert("prompts".to_string(), json!(transition_prompt_list));
123        }
124    }
125
126    Ok(response)
127}