handoff-mcp-server 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2 -1
- package/Cargo.toml +2 -1
- package/README.md +112 -32
- package/package.json +2 -1
- package/skills/handoff/SKILL.md +230 -0
- package/skills/handoff-import/SKILL.md +238 -0
- package/skills/handoff-load/SKILL.md +37 -0
- package/skills/handoff-refer/SKILL.md +71 -0
- package/src/mcp/handlers/assignees.rs +254 -0
- package/src/mcp/handlers/auto_schedule.rs +498 -0
- package/src/mcp/handlers/bulk_update.rs +115 -0
- package/src/mcp/handlers/calendar.rs +196 -0
- package/src/mcp/handlers/capacity.rs +331 -0
- package/src/mcp/handlers/config.rs +229 -67
- package/src/mcp/handlers/config_crud.rs +183 -0
- package/src/mcp/handlers/get_session.rs +48 -0
- package/src/mcp/handlers/get_task.rs +1 -0
- package/src/mcp/handlers/import_context.rs +7 -0
- package/src/mcp/handlers/list_sessions.rs +129 -0
- package/src/mcp/handlers/list_tasks.rs +76 -10
- package/src/mcp/handlers/load_context.rs +16 -0
- package/src/mcp/handlers/log_time.rs +70 -0
- package/src/mcp/handlers/metrics.rs +196 -0
- package/src/mcp/handlers/milestones.rs +102 -0
- package/src/mcp/handlers/mod.rs +30 -0
- package/src/mcp/handlers/referrals.rs +20 -1
- package/src/mcp/handlers/update_session.rs +1 -1
- package/src/mcp/handlers/update_task.rs +99 -6
- package/src/mcp/tools.rs +357 -3
- package/src/storage/config.rs +162 -1
- package/src/storage/mod.rs +42 -0
- package/src/storage/referrals.rs +39 -1
- package/src/storage/sessions.rs +95 -22
- package/src/storage/tasks.rs +122 -2
|
@@ -4,7 +4,7 @@ use serde_json::Value;
|
|
|
4
4
|
use super::resolve_project_dir;
|
|
5
5
|
use crate::storage::ensure_handoff_exists;
|
|
6
6
|
use crate::storage::referrals::{
|
|
7
|
-
change_referral_status, is_valid_referral_status, read_referral_summaries,
|
|
7
|
+
change_referral_status, is_valid_referral_status, read_referral_by_id, read_referral_summaries,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
pub fn handle_list(arguments: &Value) -> Result<String> {
|
|
@@ -32,6 +32,25 @@ pub fn handle_list(arguments: &Value) -> Result<String> {
|
|
|
32
32
|
serde_json::to_string_pretty(&result).context("Failed to serialize referrals")
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
pub fn handle_get(arguments: &Value) -> Result<String> {
|
|
36
|
+
let project_dir = resolve_project_dir(arguments)?;
|
|
37
|
+
let handoff = ensure_handoff_exists(&project_dir)?;
|
|
38
|
+
let referrals_dir = handoff.join("referrals");
|
|
39
|
+
|
|
40
|
+
let referral_id = arguments
|
|
41
|
+
.get("referral_id")
|
|
42
|
+
.and_then(|v| v.as_str())
|
|
43
|
+
.ok_or_else(|| anyhow::anyhow!("'referral_id' is required"))?;
|
|
44
|
+
|
|
45
|
+
let (data, status) = read_referral_by_id(&referrals_dir, referral_id)?
|
|
46
|
+
.ok_or_else(|| anyhow::anyhow!("Referral not found: {referral_id}"))?;
|
|
47
|
+
|
|
48
|
+
let mut result = serde_json::to_value(&data).context("Failed to serialize referral")?;
|
|
49
|
+
result["status"] = Value::String(status);
|
|
50
|
+
|
|
51
|
+
serde_json::to_string_pretty(&result).context("Failed to serialize referral")
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
pub fn handle_update(arguments: &Value) -> Result<String> {
|
|
36
55
|
let project_dir = resolve_project_dir(arguments)?;
|
|
37
56
|
let handoff = ensure_handoff_exists(&project_dir)?;
|
|
@@ -179,7 +179,7 @@ fn write_active_session_data(
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
let updated = serde_json::to_string_pretty(data).context("Failed to serialize session")?;
|
|
182
|
-
|
|
182
|
+
crate::storage::atomic_write(entry.path(), updated.as_bytes())
|
|
183
183
|
.with_context(|| format!("Failed to write session: {}", entry.path().display()))?;
|
|
184
184
|
return Ok(());
|
|
185
185
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
1
3
|
use anyhow::{Context, Result};
|
|
2
4
|
use chrono::Utc;
|
|
3
5
|
use serde_json::Value;
|
|
4
6
|
|
|
5
7
|
use super::resolve_project_dir;
|
|
8
|
+
use crate::storage::config::read_config;
|
|
6
9
|
use crate::storage::ensure_handoff_exists;
|
|
7
10
|
use crate::storage::tasks::*;
|
|
8
11
|
|
|
@@ -12,6 +15,10 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
12
15
|
let handoff = ensure_handoff_exists(&project_dir)?;
|
|
13
16
|
let tasks_dir = handoff.join("tasks");
|
|
14
17
|
|
|
18
|
+
let require_estimate_hours = read_config(&handoff.join("config.toml"))
|
|
19
|
+
.map(|c| c.settings.require_estimate_hours)
|
|
20
|
+
.unwrap_or(true);
|
|
21
|
+
|
|
15
22
|
let task_val = arguments
|
|
16
23
|
.get("task")
|
|
17
24
|
.ok_or_else(|| anyhow::anyhow!("'task' parameter is required"))?;
|
|
@@ -25,9 +32,15 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
25
32
|
}
|
|
26
33
|
let task_exists = find_task_dir_by_id(&tasks_dir, existing_id)?.is_some();
|
|
27
34
|
if task_exists {
|
|
28
|
-
return handle_update(&tasks_dir, existing_id, task_val);
|
|
35
|
+
return handle_update(&tasks_dir, existing_id, task_val, require_estimate_hours);
|
|
29
36
|
}
|
|
30
|
-
return handle_upsert_create(
|
|
37
|
+
return handle_upsert_create(
|
|
38
|
+
&tasks_dir,
|
|
39
|
+
existing_id,
|
|
40
|
+
task_val,
|
|
41
|
+
arguments,
|
|
42
|
+
require_estimate_hours,
|
|
43
|
+
);
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
let title = task_val
|
|
@@ -35,7 +48,13 @@ pub fn handle(arguments: &Value) -> Result<String> {
|
|
|
35
48
|
.and_then(|v| v.as_str())
|
|
36
49
|
.ok_or_else(|| anyhow::anyhow!("'task.title' is required for new tasks"))?;
|
|
37
50
|
|
|
38
|
-
handle_create(
|
|
51
|
+
handle_create(
|
|
52
|
+
&tasks_dir,
|
|
53
|
+
title,
|
|
54
|
+
task_val,
|
|
55
|
+
arguments,
|
|
56
|
+
require_estimate_hours,
|
|
57
|
+
)
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
fn handle_create(
|
|
@@ -43,6 +62,7 @@ fn handle_create(
|
|
|
43
62
|
title: &str,
|
|
44
63
|
task_val: &Value,
|
|
45
64
|
arguments: &Value,
|
|
65
|
+
require_estimate_hours: bool,
|
|
46
66
|
) -> Result<String> {
|
|
47
67
|
let parent_id = arguments.get("parent_id").and_then(|v| v.as_str());
|
|
48
68
|
|
|
@@ -103,8 +123,21 @@ fn handle_create(
|
|
|
103
123
|
.get("order")
|
|
104
124
|
.and_then(|v| v.as_u64())
|
|
105
125
|
.map(|v| v as u32),
|
|
126
|
+
assignee: task_val
|
|
127
|
+
.get("assignee")
|
|
128
|
+
.and_then(|v| v.as_str())
|
|
129
|
+
.map(String::from),
|
|
130
|
+
extra: HashMap::new(),
|
|
106
131
|
};
|
|
107
132
|
|
|
133
|
+
// A newly created task is always a leaf (no children yet).
|
|
134
|
+
validate_estimate_required(
|
|
135
|
+
require_estimate_hours,
|
|
136
|
+
status,
|
|
137
|
+
false,
|
|
138
|
+
data.schedule.as_ref(),
|
|
139
|
+
)?;
|
|
140
|
+
|
|
108
141
|
write_task(&task_dir, status, &data)?;
|
|
109
142
|
|
|
110
143
|
Ok(format!("Created task {new_id}: {title} [{status}]"))
|
|
@@ -115,6 +148,7 @@ fn handle_upsert_create(
|
|
|
115
148
|
task_id: &str,
|
|
116
149
|
task_val: &Value,
|
|
117
150
|
arguments: &Value,
|
|
151
|
+
require_estimate_hours: bool,
|
|
118
152
|
) -> Result<String> {
|
|
119
153
|
let title = task_val
|
|
120
154
|
.get("title")
|
|
@@ -181,14 +215,32 @@ fn handle_upsert_create(
|
|
|
181
215
|
.get("order")
|
|
182
216
|
.and_then(|v| v.as_u64())
|
|
183
217
|
.map(|v| v as u32),
|
|
218
|
+
assignee: task_val
|
|
219
|
+
.get("assignee")
|
|
220
|
+
.and_then(|v| v.as_str())
|
|
221
|
+
.map(String::from),
|
|
222
|
+
extra: HashMap::new(),
|
|
184
223
|
};
|
|
185
224
|
|
|
225
|
+
// Upsert-create: a brand-new task is a leaf.
|
|
226
|
+
validate_estimate_required(
|
|
227
|
+
require_estimate_hours,
|
|
228
|
+
status,
|
|
229
|
+
false,
|
|
230
|
+
data.schedule.as_ref(),
|
|
231
|
+
)?;
|
|
232
|
+
|
|
186
233
|
write_task(&task_dir, status, &data)?;
|
|
187
234
|
|
|
188
235
|
Ok(format!("Created task {task_id}: {title} [{status}]"))
|
|
189
236
|
}
|
|
190
237
|
|
|
191
|
-
fn handle_update(
|
|
238
|
+
fn handle_update(
|
|
239
|
+
tasks_dir: &std::path::Path,
|
|
240
|
+
task_id: &str,
|
|
241
|
+
task_val: &Value,
|
|
242
|
+
require_estimate_hours: bool,
|
|
243
|
+
) -> Result<String> {
|
|
192
244
|
let task_dir = find_task_dir_by_id(tasks_dir, task_id)?
|
|
193
245
|
.ok_or_else(|| anyhow::anyhow!("Task not found: {task_id}"))?;
|
|
194
246
|
|
|
@@ -214,8 +266,32 @@ fn handle_update(tasks_dir: &std::path::Path, task_id: &str, task_val: &Value) -
|
|
|
214
266
|
if task_val.get("done_criteria").is_some() {
|
|
215
267
|
data.done_criteria = extract_done_criteria(task_val);
|
|
216
268
|
}
|
|
217
|
-
if task_val.get("schedule")
|
|
218
|
-
|
|
269
|
+
if let Some(sched_val) = task_val.get("schedule") {
|
|
270
|
+
// Field-level merge (not full replacement) so that fields not present in
|
|
271
|
+
// the patch — e.g. actual_hours/remaining_hours accrued by the VSCode timer —
|
|
272
|
+
// are preserved. Mirrors bulk_update_tasks. (referral ref-20260623-232823)
|
|
273
|
+
let schedule = data.schedule.get_or_insert_with(Default::default);
|
|
274
|
+
if let Some(sd) = sched_val.get("start_date").and_then(|v| v.as_str()) {
|
|
275
|
+
schedule.start_date = Some(sd.to_string());
|
|
276
|
+
}
|
|
277
|
+
if let Some(dd) = sched_val.get("due_date").and_then(|v| v.as_str()) {
|
|
278
|
+
schedule.due_date = Some(dd.to_string());
|
|
279
|
+
}
|
|
280
|
+
if let Some(eh) = sched_val.get("estimate_hours").and_then(|v| v.as_f64()) {
|
|
281
|
+
schedule.estimate_hours = Some(eh);
|
|
282
|
+
}
|
|
283
|
+
if let Some(ah) = sched_val.get("actual_hours").and_then(|v| v.as_f64()) {
|
|
284
|
+
schedule.actual_hours = Some(ah);
|
|
285
|
+
}
|
|
286
|
+
if let Some(rh) = sched_val.get("remaining_hours").and_then(|v| v.as_f64()) {
|
|
287
|
+
schedule.remaining_hours = Some(rh);
|
|
288
|
+
}
|
|
289
|
+
if let Some(ms) = sched_val.get("milestone").and_then(|v| v.as_str()) {
|
|
290
|
+
schedule.milestone = Some(ms.to_string());
|
|
291
|
+
}
|
|
292
|
+
if let Some(p) = sched_val.get("pinned").and_then(|v| v.as_bool()) {
|
|
293
|
+
schedule.pinned = Some(p);
|
|
294
|
+
}
|
|
219
295
|
}
|
|
220
296
|
if task_val.get("dependencies").is_some() {
|
|
221
297
|
let new_deps = extract_string_array(task_val, "dependencies");
|
|
@@ -227,6 +303,12 @@ fn handle_update(tasks_dir: &std::path::Path, task_id: &str, task_val: &Value) -
|
|
|
227
303
|
if let Some(order) = task_val.get("order").and_then(|v| v.as_u64()) {
|
|
228
304
|
data.order = Some(order as u32);
|
|
229
305
|
}
|
|
306
|
+
if task_val.get("assignee").is_some() {
|
|
307
|
+
data.assignee = task_val
|
|
308
|
+
.get("assignee")
|
|
309
|
+
.and_then(|v| v.as_str())
|
|
310
|
+
.map(String::from);
|
|
311
|
+
}
|
|
230
312
|
|
|
231
313
|
let new_status = task_val
|
|
232
314
|
.get("status")
|
|
@@ -246,6 +328,15 @@ fn handle_update(tasks_dir: &std::path::Path, task_id: &str, task_val: &Value) -
|
|
|
246
328
|
validate_skipped_transition(&task_dir, &data)?;
|
|
247
329
|
}
|
|
248
330
|
|
|
331
|
+
// Parent tasks (with children) are exempt; only leaf tasks need an estimate.
|
|
332
|
+
let has_children = task_has_children(&task_dir)?;
|
|
333
|
+
validate_estimate_required(
|
|
334
|
+
require_estimate_hours,
|
|
335
|
+
new_status,
|
|
336
|
+
has_children,
|
|
337
|
+
data.schedule.as_ref(),
|
|
338
|
+
)?;
|
|
339
|
+
|
|
249
340
|
data.updated_at = Some(Utc::now().to_rfc3339());
|
|
250
341
|
|
|
251
342
|
if let Some((old_path, _)) = find_task_file(&task_dir)? {
|
|
@@ -335,9 +426,11 @@ fn extract_schedule(val: &Value) -> Option<Schedule> {
|
|
|
335
426
|
.map(String::from),
|
|
336
427
|
estimate_hours: sched.get("estimate_hours").and_then(|v| v.as_f64()),
|
|
337
428
|
actual_hours: sched.get("actual_hours").and_then(|v| v.as_f64()),
|
|
429
|
+
remaining_hours: sched.get("remaining_hours").and_then(|v| v.as_f64()),
|
|
338
430
|
milestone: sched
|
|
339
431
|
.get("milestone")
|
|
340
432
|
.and_then(|v| v.as_str())
|
|
341
433
|
.map(String::from),
|
|
434
|
+
pinned: sched.get("pinned").and_then(|v| v.as_bool()),
|
|
342
435
|
})
|
|
343
436
|
}
|
package/src/mcp/tools.rs
CHANGED
|
@@ -175,7 +175,7 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
175
175
|
},
|
|
176
176
|
ToolDefinition {
|
|
177
177
|
name: "handoff_list_tasks".to_string(),
|
|
178
|
-
description: "List all tasks for the current project with optional
|
|
178
|
+
description: "List all tasks for the current project with optional filters.".to_string(),
|
|
179
179
|
input_schema: json!({
|
|
180
180
|
"type": "object",
|
|
181
181
|
"properties": {
|
|
@@ -185,8 +185,25 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
185
185
|
},
|
|
186
186
|
"status_filter": {
|
|
187
187
|
"type": "string",
|
|
188
|
-
"description": "Filter by status",
|
|
188
|
+
"description": "Filter by status.",
|
|
189
189
|
"enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
|
|
190
|
+
},
|
|
191
|
+
"assignee_filter": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Filter by assignee key."
|
|
194
|
+
},
|
|
195
|
+
"milestone_filter": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "Filter by milestone name."
|
|
198
|
+
},
|
|
199
|
+
"priority_filter": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Filter by priority.",
|
|
202
|
+
"enum": ["low", "medium", "high"]
|
|
203
|
+
},
|
|
204
|
+
"label_filter": {
|
|
205
|
+
"type": "string",
|
|
206
|
+
"description": "Filter by label (task must contain this label)."
|
|
190
207
|
}
|
|
191
208
|
}
|
|
192
209
|
}),
|
|
@@ -286,7 +303,9 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
286
303
|
"due_date": { "type": "string", "description": "YYYY-MM-DD" },
|
|
287
304
|
"estimate_hours": { "type": "number" },
|
|
288
305
|
"actual_hours": { "type": "number" },
|
|
289
|
-
"
|
|
306
|
+
"remaining_hours": { "type": "number", "description": "Hours remaining. Auto-decremented by handoff_log_time." },
|
|
307
|
+
"milestone": { "type": "string" },
|
|
308
|
+
"pinned": { "type": "boolean", "description": "If true, dates are locked and auto-scheduler skips this task." }
|
|
290
309
|
}
|
|
291
310
|
},
|
|
292
311
|
"dependencies": {
|
|
@@ -297,6 +316,10 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
297
316
|
"order": {
|
|
298
317
|
"type": "integer",
|
|
299
318
|
"description": "Display order among siblings. 0-based, lower = higher priority."
|
|
319
|
+
},
|
|
320
|
+
"assignee": {
|
|
321
|
+
"type": "string",
|
|
322
|
+
"description": "Assignee key (matches config.toml [assignees.<key>])."
|
|
300
323
|
}
|
|
301
324
|
},
|
|
302
325
|
},
|
|
@@ -621,6 +644,24 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
621
644
|
}
|
|
622
645
|
}),
|
|
623
646
|
},
|
|
647
|
+
ToolDefinition {
|
|
648
|
+
name: "handoff_get_referral".to_string(),
|
|
649
|
+
description: "Get the full details of a single incoming referral by ID (summary, details, tasks with done_criteria, priority, context, status). Use this instead of reading .handoff/referrals/*.json directly.".to_string(),
|
|
650
|
+
input_schema: json!({
|
|
651
|
+
"type": "object",
|
|
652
|
+
"properties": {
|
|
653
|
+
"project_dir": {
|
|
654
|
+
"type": "string",
|
|
655
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
656
|
+
},
|
|
657
|
+
"referral_id": {
|
|
658
|
+
"type": "string",
|
|
659
|
+
"description": "ID of the referral to retrieve (full id or a unique prefix)."
|
|
660
|
+
}
|
|
661
|
+
},
|
|
662
|
+
"required": ["referral_id"]
|
|
663
|
+
}),
|
|
664
|
+
},
|
|
624
665
|
ToolDefinition {
|
|
625
666
|
name: "handoff_update_referral".to_string(),
|
|
626
667
|
description: "Update the status of an incoming referral (open -> acknowledged -> resolved).".to_string(),
|
|
@@ -703,9 +744,322 @@ pub fn all_tool_definitions() -> Vec<ToolDefinition> {
|
|
|
703
744
|
}
|
|
704
745
|
}),
|
|
705
746
|
},
|
|
747
|
+
ToolDefinition {
|
|
748
|
+
name: "handoff_log_time".to_string(),
|
|
749
|
+
description: "Log hours worked on a task. Adds to actual_hours and deducts from remaining_hours atomically.".to_string(),
|
|
750
|
+
input_schema: json!({
|
|
751
|
+
"type": "object",
|
|
752
|
+
"properties": {
|
|
753
|
+
"project_dir": {
|
|
754
|
+
"type": "string",
|
|
755
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
756
|
+
},
|
|
757
|
+
"task_id": {
|
|
758
|
+
"type": "string",
|
|
759
|
+
"description": "Task ID to log time against."
|
|
760
|
+
},
|
|
761
|
+
"hours": {
|
|
762
|
+
"type": "number",
|
|
763
|
+
"description": "Hours worked (e.g. 0.5 for 30 minutes)."
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
"required": ["task_id", "hours"]
|
|
767
|
+
}),
|
|
768
|
+
},
|
|
769
|
+
ToolDefinition {
|
|
770
|
+
name: "handoff_get_metrics".to_string(),
|
|
771
|
+
description: "Get project metrics: completion %, effort tracking, overdue tasks, budget status, and milestone breakdown.".to_string(),
|
|
772
|
+
input_schema: json!({
|
|
773
|
+
"type": "object",
|
|
774
|
+
"properties": {
|
|
775
|
+
"project_dir": {
|
|
776
|
+
"type": "string",
|
|
777
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
778
|
+
},
|
|
779
|
+
"assignee": {
|
|
780
|
+
"type": "string",
|
|
781
|
+
"description": "Filter metrics to a specific assignee."
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}),
|
|
785
|
+
},
|
|
786
|
+
ToolDefinition {
|
|
787
|
+
name: "handoff_list_sessions".to_string(),
|
|
788
|
+
description: "List all sessions (open, active, paused, closed) with summary info. Use handoff_get_session for full detail.".to_string(),
|
|
789
|
+
input_schema: json!({
|
|
790
|
+
"type": "object",
|
|
791
|
+
"properties": {
|
|
792
|
+
"project_dir": {
|
|
793
|
+
"type": "string",
|
|
794
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
795
|
+
},
|
|
796
|
+
"status_filter": {
|
|
797
|
+
"type": "string",
|
|
798
|
+
"enum": ["open", "active", "paused", "closed"],
|
|
799
|
+
"description": "Filter sessions by status."
|
|
800
|
+
},
|
|
801
|
+
"limit": {
|
|
802
|
+
"type": "integer",
|
|
803
|
+
"description": "Max sessions to return (default 20)."
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}),
|
|
807
|
+
},
|
|
808
|
+
ToolDefinition {
|
|
809
|
+
name: "handoff_list_assignees".to_string(),
|
|
810
|
+
description: "List all team members/assignees from config.toml with their task counts and effort stats.".to_string(),
|
|
811
|
+
input_schema: json!({
|
|
812
|
+
"type": "object",
|
|
813
|
+
"properties": {
|
|
814
|
+
"project_dir": {
|
|
815
|
+
"type": "string",
|
|
816
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}),
|
|
820
|
+
},
|
|
821
|
+
ToolDefinition {
|
|
822
|
+
name: "handoff_bulk_update_tasks".to_string(),
|
|
823
|
+
description: "Update multiple tasks in one call. Useful for applying auto-schedule results or bulk status/assignee changes.".to_string(),
|
|
824
|
+
input_schema: json!({
|
|
825
|
+
"type": "object",
|
|
826
|
+
"properties": {
|
|
827
|
+
"project_dir": {
|
|
828
|
+
"type": "string",
|
|
829
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
830
|
+
},
|
|
831
|
+
"updates": {
|
|
832
|
+
"type": "array",
|
|
833
|
+
"description": "Array of task updates to apply.",
|
|
834
|
+
"items": {
|
|
835
|
+
"type": "object",
|
|
836
|
+
"properties": {
|
|
837
|
+
"task_id": { "type": "string", "description": "Task ID to update." },
|
|
838
|
+
"status": { "type": "string", "enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"] },
|
|
839
|
+
"priority": { "type": "string", "enum": ["low", "medium", "high"] },
|
|
840
|
+
"assignee": { "type": "string" },
|
|
841
|
+
"schedule": {
|
|
842
|
+
"type": "object",
|
|
843
|
+
"properties": {
|
|
844
|
+
"start_date": { "type": "string" },
|
|
845
|
+
"due_date": { "type": "string" },
|
|
846
|
+
"estimate_hours": { "type": "number" },
|
|
847
|
+
"actual_hours": { "type": "number" },
|
|
848
|
+
"remaining_hours": { "type": "number" },
|
|
849
|
+
"milestone": { "type": "string" },
|
|
850
|
+
"pinned": { "type": "boolean" }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
},
|
|
854
|
+
"required": ["task_id"]
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
"required": ["updates"]
|
|
859
|
+
}),
|
|
860
|
+
},
|
|
861
|
+
ToolDefinition {
|
|
862
|
+
name: "handoff_get_session".to_string(),
|
|
863
|
+
description: "Get full detail of a specific session by ID. Returns decisions, checklist, handoff notes, context pointers, etc.".to_string(),
|
|
864
|
+
input_schema: json!({
|
|
865
|
+
"type": "object",
|
|
866
|
+
"properties": {
|
|
867
|
+
"project_dir": {
|
|
868
|
+
"type": "string",
|
|
869
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
870
|
+
},
|
|
871
|
+
"session_id": {
|
|
872
|
+
"type": "string",
|
|
873
|
+
"description": "Session ID to retrieve."
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
"required": ["session_id"]
|
|
877
|
+
}),
|
|
878
|
+
},
|
|
879
|
+
ToolDefinition {
|
|
880
|
+
name: "handoff_get_capacity".to_string(),
|
|
881
|
+
description: "Get work capacity for a date range. Shows available hours per day based on calendar config, and allocated hours from scheduled tasks.".to_string(),
|
|
882
|
+
input_schema: json!({
|
|
883
|
+
"type": "object",
|
|
884
|
+
"properties": {
|
|
885
|
+
"project_dir": {
|
|
886
|
+
"type": "string",
|
|
887
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
888
|
+
},
|
|
889
|
+
"start_date": {
|
|
890
|
+
"type": "string",
|
|
891
|
+
"description": "Start date (YYYY-MM-DD)."
|
|
892
|
+
},
|
|
893
|
+
"end_date": {
|
|
894
|
+
"type": "string",
|
|
895
|
+
"description": "End date (YYYY-MM-DD)."
|
|
896
|
+
},
|
|
897
|
+
"assignee": {
|
|
898
|
+
"type": "string",
|
|
899
|
+
"description": "Filter capacity to a specific assignee's calendar."
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
"required": ["start_date", "end_date"]
|
|
903
|
+
}),
|
|
904
|
+
},
|
|
905
|
+
ToolDefinition {
|
|
906
|
+
name: "handoff_auto_schedule".to_string(),
|
|
907
|
+
description: "Run auto-scheduler to compute optimal task dates based on dependencies, estimates, and calendar capacity. Returns change diff; applies changes unless dry_run=true.".to_string(),
|
|
908
|
+
input_schema: json!({
|
|
909
|
+
"type": "object",
|
|
910
|
+
"properties": {
|
|
911
|
+
"project_dir": {
|
|
912
|
+
"type": "string",
|
|
913
|
+
"description": "Project directory path. Defaults to current working directory."
|
|
914
|
+
},
|
|
915
|
+
"dry_run": {
|
|
916
|
+
"type": "boolean",
|
|
917
|
+
"description": "If true (default), return computed spans without writing. If false, apply changes to task files."
|
|
918
|
+
},
|
|
919
|
+
"assignee_filter": {
|
|
920
|
+
"type": "string",
|
|
921
|
+
"description": "Only schedule tasks assigned to this assignee."
|
|
922
|
+
},
|
|
923
|
+
"start_date": {
|
|
924
|
+
"type": "string",
|
|
925
|
+
"description": "Anchor date YYYY-MM-DD for the earliest task. Defaults to today (UTC)."
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}),
|
|
929
|
+
},
|
|
930
|
+
ToolDefinition {
|
|
931
|
+
name: "handoff_add_assignee".to_string(),
|
|
932
|
+
description: "Add a team member to config.toml [assignees.<key>]. Fails if the key already exists.".to_string(),
|
|
933
|
+
input_schema: assignee_write_schema(true),
|
|
934
|
+
},
|
|
935
|
+
ToolDefinition {
|
|
936
|
+
name: "handoff_update_assignee".to_string(),
|
|
937
|
+
description: "Update an existing [assignees.<key>] entry. Only provided fields change; pass null to clear a field.".to_string(),
|
|
938
|
+
input_schema: assignee_write_schema(false),
|
|
939
|
+
},
|
|
940
|
+
ToolDefinition {
|
|
941
|
+
name: "handoff_remove_assignee".to_string(),
|
|
942
|
+
description: "Remove a team member from config.toml and unassign them from every task.".to_string(),
|
|
943
|
+
input_schema: json!({
|
|
944
|
+
"type": "object",
|
|
945
|
+
"properties": {
|
|
946
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
947
|
+
"key": { "type": "string", "description": "Assignee key to remove." }
|
|
948
|
+
},
|
|
949
|
+
"required": ["key"]
|
|
950
|
+
}),
|
|
951
|
+
},
|
|
952
|
+
ToolDefinition {
|
|
953
|
+
name: "handoff_list_milestones".to_string(),
|
|
954
|
+
description: "List all milestones defined in config.toml [milestones.*].".to_string(),
|
|
955
|
+
input_schema: json!({
|
|
956
|
+
"type": "object",
|
|
957
|
+
"properties": {
|
|
958
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." }
|
|
959
|
+
}
|
|
960
|
+
}),
|
|
961
|
+
},
|
|
962
|
+
ToolDefinition {
|
|
963
|
+
name: "handoff_add_milestone".to_string(),
|
|
964
|
+
description: "Add a milestone to config.toml [milestones.<name>]. Fails if it already exists.".to_string(),
|
|
965
|
+
input_schema: milestone_write_schema(),
|
|
966
|
+
},
|
|
967
|
+
ToolDefinition {
|
|
968
|
+
name: "handoff_update_milestone".to_string(),
|
|
969
|
+
description: "Update an existing [milestones.<name>] entry. Pass null to clear a field.".to_string(),
|
|
970
|
+
input_schema: milestone_write_schema(),
|
|
971
|
+
},
|
|
972
|
+
ToolDefinition {
|
|
973
|
+
name: "handoff_remove_milestone".to_string(),
|
|
974
|
+
description: "Remove a milestone from config.toml.".to_string(),
|
|
975
|
+
input_schema: json!({
|
|
976
|
+
"type": "object",
|
|
977
|
+
"properties": {
|
|
978
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
979
|
+
"name": { "type": "string", "description": "Milestone name to remove." }
|
|
980
|
+
},
|
|
981
|
+
"required": ["name"]
|
|
982
|
+
}),
|
|
983
|
+
},
|
|
984
|
+
ToolDefinition {
|
|
985
|
+
name: "handoff_update_calendar".to_string(),
|
|
986
|
+
description: "Patch the project [calendar] section (work hours, closed days, day_hours, schedule_mode). Only provided fields change.".to_string(),
|
|
987
|
+
input_schema: json!({
|
|
988
|
+
"type": "object",
|
|
989
|
+
"properties": {
|
|
990
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
991
|
+
"work_hours_per_day": { "type": "number", "description": "Default working hours per day." },
|
|
992
|
+
"closed_weekdays": { "type": "array", "description": "Non-working weekdays (0=Sun..6=Sat, or names like \"sat\").", "items": {} },
|
|
993
|
+
"closed_dates": { "type": "array", "description": "Non-working YYYY-MM-DD dates.", "items": { "type": "string" } },
|
|
994
|
+
"open_dates": { "type": "array", "description": "Working YYYY-MM-DD dates that override closed weekdays.", "items": { "type": "string" } },
|
|
995
|
+
"day_hours": { "type": "object", "description": "Per-weekday-name or per-date hour overrides, e.g. {\"fri\": 4, \"2026-07-01\": 0}.", "additionalProperties": { "type": "number" } },
|
|
996
|
+
"schedule_mode": { "type": "string", "description": "\"manual\" or \"auto\"." },
|
|
997
|
+
"overwork_limit_percent": { "type": "number" },
|
|
998
|
+
"max_utilization": { "type": "number" }
|
|
999
|
+
}
|
|
1000
|
+
}),
|
|
1001
|
+
},
|
|
1002
|
+
ToolDefinition {
|
|
1003
|
+
name: "handoff_update_labels".to_string(),
|
|
1004
|
+
description: "Set the project-level label vocabulary (top-level labels array in config.toml).".to_string(),
|
|
1005
|
+
input_schema: json!({
|
|
1006
|
+
"type": "object",
|
|
1007
|
+
"properties": {
|
|
1008
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
1009
|
+
"labels": { "type": "array", "description": "Full replacement list of project labels.", "items": { "type": "string" } }
|
|
1010
|
+
},
|
|
1011
|
+
"required": ["labels"]
|
|
1012
|
+
}),
|
|
1013
|
+
},
|
|
1014
|
+
ToolDefinition {
|
|
1015
|
+
name: "handoff_start_project".to_string(),
|
|
1016
|
+
description: "Set the project started_at date and optionally shift all task dates so the earliest start aligns to it.".to_string(),
|
|
1017
|
+
input_schema: json!({
|
|
1018
|
+
"type": "object",
|
|
1019
|
+
"properties": {
|
|
1020
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
1021
|
+
"start_date": { "type": "string", "description": "Project start date YYYY-MM-DD. Defaults to today (UTC)." },
|
|
1022
|
+
"shift_dates": { "type": "boolean", "description": "If true, shift every task's start/due dates so the earliest start lands on start_date." }
|
|
1023
|
+
}
|
|
1024
|
+
}),
|
|
1025
|
+
},
|
|
706
1026
|
]
|
|
707
1027
|
}
|
|
708
1028
|
|
|
1029
|
+
/// Shared input schema for add/update assignee. `key` is required either way.
|
|
1030
|
+
fn assignee_write_schema(_is_add: bool) -> Value {
|
|
1031
|
+
json!({
|
|
1032
|
+
"type": "object",
|
|
1033
|
+
"properties": {
|
|
1034
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
1035
|
+
"key": { "type": "string", "description": "Stable assignee key (used as [assignees.<key>])." },
|
|
1036
|
+
"display_name": { "type": "string", "description": "Human-readable name." },
|
|
1037
|
+
"color": { "type": "string", "description": "Display color (hex or name)." },
|
|
1038
|
+
"work_hours_per_day": { "type": "number", "description": "This member's daily working hours." },
|
|
1039
|
+
"closed_weekdays": { "type": "array", "description": "Non-working weekdays (0=Sun..6=Sat or names).", "items": {} },
|
|
1040
|
+
"closed_dates": { "type": "array", "description": "Non-working YYYY-MM-DD dates.", "items": { "type": "string" } },
|
|
1041
|
+
"open_dates": { "type": "array", "description": "Working YYYY-MM-DD override dates.", "items": { "type": "string" } },
|
|
1042
|
+
"day_hours": { "type": "object", "description": "Per-weekday/date hour overrides.", "additionalProperties": { "type": "number" } }
|
|
1043
|
+
},
|
|
1044
|
+
"required": ["key"]
|
|
1045
|
+
})
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/// Shared input schema for add/update milestone. `name` is required.
|
|
1049
|
+
fn milestone_write_schema() -> Value {
|
|
1050
|
+
json!({
|
|
1051
|
+
"type": "object",
|
|
1052
|
+
"properties": {
|
|
1053
|
+
"project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
|
|
1054
|
+
"name": { "type": "string", "description": "Milestone name (used as [milestones.<name>])." },
|
|
1055
|
+
"date": { "type": "string", "description": "Target date YYYY-MM-DD." },
|
|
1056
|
+
"color": { "type": "string", "description": "Display color." },
|
|
1057
|
+
"description": { "type": "string", "description": "Free-form description." }
|
|
1058
|
+
},
|
|
1059
|
+
"required": ["name"]
|
|
1060
|
+
})
|
|
1061
|
+
}
|
|
1062
|
+
|
|
709
1063
|
pub fn all_resource_definitions() -> Vec<Value> {
|
|
710
1064
|
vec![
|
|
711
1065
|
json!({
|