task_graph_mcp/tools/
claiming.rs1use super::{get_bool, get_string, make_tool_with_prompts};
8use crate::config::{AppConfig, 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 config: &AppConfig,
42 workflows: &crate::config::workflows::WorkflowsConfig,
43 args: Value,
44) -> Result<Value> {
45 let states_config = &config.states;
46 let phases_config = &config.phases;
47 let deps_config = &config.deps;
48 let auto_advance = &config.auto_advance;
49 let worker_id =
50 get_string(&args, "worker_id").ok_or_else(|| ToolError::missing_field("worker_id"))?;
51 let task_id = get_string(&args, "task").ok_or_else(|| ToolError::missing_field("task"))?;
52 let force = get_bool(&args, "force").unwrap_or(false);
53
54 let claim_status = states_config
56 .definitions
57 .iter()
58 .find(|(_, def)| def.timed)
59 .map(|(name, _)| name.clone())
60 .unwrap_or_else(|| "working".to_string());
61
62 let (task, _unblocked, _auto_advanced) = match db.update_task_unified(
65 &task_id,
66 &worker_id,
67 None, None, None, Some(claim_status), None, None, None, None, None, None, None, None, force,
80 states_config,
81 deps_config,
82 auto_advance,
83 ) {
84 Ok(result) => result,
85 Err(e) => {
86 let err_msg = e.to_string();
88 if err_msg.contains("unsatisfied dependencies") {
89 let blockers = db
91 .get_start_blockers(&task_id, deps_config)
92 .unwrap_or_default();
93 if !blockers.is_empty() {
94 return Err(ToolError::deps_not_satisfied(&blockers).into());
95 }
96 }
97 return Err(e);
98 }
99 };
100
101 let worker_info = db.get_worker(&worker_id).ok().flatten();
103 let worker_role = worker_info
104 .as_ref()
105 .map(|w| workflows.match_role(&w.tags))
106 .unwrap_or(None);
107
108 let mut transition_prompt_list: Vec<String> = {
110 match db.update_worker_state(&worker_id, Some(&task.status), task.phase.as_deref()) {
111 Ok((old_status, old_phase)) => {
112 let mut ctx = PromptContext::new(
114 &task.status,
115 task.phase.as_deref(),
116 states_config,
117 phases_config,
118 )
119 .with_task(&task.id, &task.title, task.priority, &task.tags);
120
121 if let Some(ref worker) = worker_info {
123 ctx = ctx.with_agent(&worker_id, worker_role.as_deref(), &worker.tags);
124 }
125
126 crate::prompts::get_transition_prompts_with_context(
127 old_status.as_deref().unwrap_or(""),
128 old_phase.as_deref(),
129 &task.status,
130 task.phase.as_deref(),
131 workflows,
132 &ctx,
133 )
134 }
135 Err(_) => vec![],
136 }
137 };
138
139 let mut response = json!({
140 "success": true,
141 "task": {
142 "id": &task.id,
143 "title": task.title,
144 "status": task.status,
145 "worker_id": task.worker_id,
146 "claimed_at": task.claimed_at
147 }
148 });
149
150 if let Some(ref role_name) = worker_role {
153 if let Some(claiming_prompt) = workflows.get_role_prompt(role_name, "claiming") {
154 transition_prompt_list.push(claiming_prompt.to_string());
155 }
156 if let Some(reporting_prompt) = workflows.get_role_prompt(role_name, "reporting") {
159 transition_prompt_list.push(reporting_prompt.to_string());
160 }
161 }
162
163 if !transition_prompt_list.is_empty()
165 && let Value::Object(ref mut map) = response
166 {
167 map.insert("prompts".to_string(), json!(transition_prompt_list));
168 }
169
170 Ok(response)
171}