1use super::{get_bool, get_string, get_string_or_array, make_tool_with_prompts};
8use crate::config::{AppConfig, Prompts, StatesConfig};
9use crate::db::Database;
10use crate::error::ToolError;
11use crate::prompts::{AttributedPrompt, 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 "files": {
34 "oneOf": [
35 { "type": "string" },
36 { "type": "array", "items": { "type": "string" } }
37 ],
38 "description": "File paths to mark as being worked on (auto-marks with task ID for cleanup on completion)"
39 }
40 }),
41 vec!["worker_id", "task"],
42 prompts,
43 )]
44}
45
46pub fn claim(
47 db: &Database,
48 config: &AppConfig,
49 workflows: &crate::config::workflows::WorkflowsConfig,
50 args: Value,
51) -> Result<Value> {
52 let states_config_owned: StatesConfig = workflows.into();
54 let states_config = &states_config_owned;
55 let phases_config = &config.phases;
56 let deps_config = &config.deps;
57 let auto_advance = &config.auto_advance;
58 let worker_id =
59 get_string(&args, "worker_id").ok_or_else(|| ToolError::missing_field("worker_id"))?;
60 let task_id = get_string(&args, "task").ok_or_else(|| ToolError::missing_field("task"))?;
61 let force = get_bool(&args, "force").unwrap_or(false);
62
63 let pre_claim_status = db.get_task(&task_id)?.map(|t| (t.status, t.phase));
68
69 let claim_status = states_config
71 .definitions
72 .iter()
73 .find(|(_, def)| def.timed)
74 .map(|(name, _)| name.clone())
75 .unwrap_or_else(|| "working".to_string());
76
77 let (task, _unblocked, _auto_advanced, _auto_completed) = match db.update_task_unified(
80 &task_id,
81 &worker_id,
82 None, None, None, Some(claim_status), None, None, None, None, None, None, None, None, force,
95 states_config,
96 deps_config,
97 auto_advance,
98 ) {
99 Ok(result) => result,
100 Err(e) => {
101 let err_msg = e.to_string();
103 if err_msg.contains("unsatisfied dependencies") {
104 let blockers = db
106 .get_start_blockers(&task_id, deps_config)
107 .unwrap_or_default();
108 if !blockers.is_empty() {
109 return Err(ToolError::deps_not_satisfied(&blockers).into());
110 }
111 }
112 return Err(e);
113 }
114 };
115
116 let files_marked: Vec<String> = if let Some(file_paths) = get_string_or_array(&args, "files") {
118 let mut marked = Vec::new();
119 for path in file_paths {
120 let normalized = super::files::normalize_file_path(&path);
121 let _warning = db.lock_file(
123 normalized.clone(),
124 &worker_id,
125 None, Some(task_id.clone()), )?;
128 marked.push(normalized);
129 }
130 marked
131 } else {
132 Vec::new()
133 };
134
135 let worker_info = db.get_worker(&worker_id).ok().flatten();
137 let worker_role = worker_info
138 .as_ref()
139 .map(|w| workflows.match_role(&w.tags))
140 .unwrap_or(None);
141
142 let mut transition_prompt_list: Vec<AttributedPrompt> = {
150 let _ = db.update_worker_state(
152 &worker_id,
153 Some(&task.status),
154 task.phase.as_deref(),
155 Some(&task.id),
156 );
157
158 let (from_status, from_phase) = match &pre_claim_status {
160 Some((status, phase)) => (status.as_str(), phase.as_deref()),
161 None => ("", None),
162 };
163
164 let mut ctx = PromptContext::new(
166 &task.status,
167 task.phase.as_deref(),
168 states_config,
169 phases_config,
170 )
171 .with_task(&task.id, &task.title, task.priority, &task.tags);
172
173 let task_level_str: Option<String> = task
175 .tags
176 .iter()
177 .find(|t| t.starts_with("level:"))
178 .map(|t| t.strip_prefix("level:").unwrap_or(t).to_string());
179 let child_count = db.get_children_ids(&task.id).ok().map(|ids| ids.len());
180 let task_level_ref = task_level_str.as_deref();
181 ctx = ctx.with_level(task_level_ref, child_count);
182
183 if let Some(ref worker) = worker_info {
185 ctx = ctx.with_agent(&worker_id, worker_role.as_deref(), &worker.tags);
186 }
187
188 crate::prompts::get_transition_prompts_attributed(
189 from_status,
190 from_phase,
191 &task.status,
192 task.phase.as_deref(),
193 workflows,
194 &ctx,
195 )
196 };
197
198 let mut response = json!({
199 "success": true,
200 "task": {
201 "id": &task.id,
202 "title": task.title,
203 "status": task.status,
204 "worker_id": task.worker_id,
205 "claimed_at": task.claimed_at.map(crate::types::ms_to_iso)
206 }
207 });
208
209 if let Some((pre_status, pre_phase)) = &pre_claim_status
212 && let Value::Object(ref mut map) = response
213 {
214 map.insert("pre_claim_status".to_string(), json!(pre_status));
215 if let Some(phase) = pre_phase {
216 map.insert("pre_claim_phase".to_string(), json!(phase));
217 }
218 }
219
220 if let Some(ref role_name) = worker_role {
223 if let Some(claiming_prompt) = workflows.get_role_prompt(role_name, "claiming") {
224 transition_prompt_list.push(AttributedPrompt {
225 text: claiming_prompt.to_string(),
226 source: format!("role:{}", role_name),
227 });
228 }
229 if let Some(reporting_prompt) = workflows.get_role_prompt(role_name, "reporting") {
232 transition_prompt_list.push(AttributedPrompt {
233 text: reporting_prompt.to_string(),
234 source: format!("role:{}", role_name),
235 });
236 }
237 }
238
239 let file_contention = db.find_file_contention(&task.id, &worker_id);
243
244 if let Value::Object(ref mut map) = response {
245 if !files_marked.is_empty() {
247 map.insert("files_marked".to_string(), json!(files_marked));
248 }
249
250 if !transition_prompt_list.is_empty() {
252 let prompt_objects: Vec<Value> = transition_prompt_list
253 .iter()
254 .map(|p| json!({"text": p.text, "source": p.source}))
255 .collect();
256 map.insert("prompts".to_string(), json!(prompt_objects));
257 }
258
259 let advisory_hints = super::advisories::relevant_advisory_topics(
261 workflows,
262 &task.tags,
263 task.phase.as_deref(),
264 worker_role.as_deref(),
265 );
266 if !advisory_hints.is_empty() {
267 map.insert("advisory_hints".to_string(), json!(advisory_hints));
268 }
269
270 if let Ok(ref contentions) = file_contention
272 && !contentions.is_empty()
273 {
274 let contention_entries: Vec<Value> = contentions
275 .iter()
276 .map(|(file_path, other_task_id, other_worker_id)| {
277 json!({
278 "file": file_path,
279 "other_task": other_task_id,
280 "other_worker": other_worker_id
281 })
282 })
283 .collect();
284 map.insert("file_contention".to_string(), json!(contention_entries));
285 }
286 }
287
288 Ok(response)
289}