handoff-mcp-server 0.26.0 → 0.28.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.
Files changed (72) hide show
  1. package/README.md +31 -4
  2. package/bin/handoff-mcp.js +56 -13
  3. package/bin/resolve-binary.js +122 -0
  4. package/package.json +14 -9
  5. package/Cargo.lock +0 -686
  6. package/Cargo.toml +0 -30
  7. package/scripts/cargo-env.sh +0 -29
  8. package/scripts/handoff-memory-hook.py +0 -208
  9. package/scripts/install-local.sh +0 -109
  10. package/scripts/postinstall.js +0 -50
  11. package/scripts/sync-plugin-skills.sh +0 -35
  12. package/scripts/sync-plugin-version.sh +0 -85
  13. package/scripts/sync-workflow-inline.sh +0 -138
  14. package/src/cli.rs +0 -551
  15. package/src/context/injection.rs +0 -276
  16. package/src/context/mod.rs +0 -129
  17. package/src/lib.rs +0 -5
  18. package/src/main.rs +0 -157
  19. package/src/mcp/handlers/assignees.rs +0 -254
  20. package/src/mcp/handlers/auto_schedule.rs +0 -489
  21. package/src/mcp/handlers/bulk_update.rs +0 -155
  22. package/src/mcp/handlers/calendar.rs +0 -196
  23. package/src/mcp/handlers/capacity.rs +0 -318
  24. package/src/mcp/handlers/check_criterion.rs +0 -70
  25. package/src/mcp/handlers/config.rs +0 -402
  26. package/src/mcp/handlers/config_crud.rs +0 -183
  27. package/src/mcp/handlers/dashboard.rs +0 -214
  28. package/src/mcp/handlers/docs.rs +0 -2288
  29. package/src/mcp/handlers/docs_query.rs +0 -1335
  30. package/src/mcp/handlers/fork_session.rs +0 -91
  31. package/src/mcp/handlers/get_session.rs +0 -48
  32. package/src/mcp/handlers/get_task.rs +0 -53
  33. package/src/mcp/handlers/import_context.rs +0 -470
  34. package/src/mcp/handlers/init.rs +0 -28
  35. package/src/mcp/handlers/list_sessions.rs +0 -187
  36. package/src/mcp/handlers/list_tasks.rs +0 -308
  37. package/src/mcp/handlers/load_context.rs +0 -361
  38. package/src/mcp/handlers/log_time.rs +0 -67
  39. package/src/mcp/handlers/memory.rs +0 -961
  40. package/src/mcp/handlers/merge_sessions.rs +0 -103
  41. package/src/mcp/handlers/metrics.rs +0 -196
  42. package/src/mcp/handlers/milestones.rs +0 -102
  43. package/src/mcp/handlers/mod.rs +0 -140
  44. package/src/mcp/handlers/refer.rs +0 -307
  45. package/src/mcp/handlers/referrals.rs +0 -74
  46. package/src/mcp/handlers/save_context.rs +0 -354
  47. package/src/mcp/handlers/task_checklist.rs +0 -507
  48. package/src/mcp/handlers/timer.rs +0 -529
  49. package/src/mcp/handlers/update_session.rs +0 -197
  50. package/src/mcp/handlers/update_task.rs +0 -452
  51. package/src/mcp/mod.rs +0 -6
  52. package/src/mcp/protocol.rs +0 -41
  53. package/src/mcp/resources.rs +0 -57
  54. package/src/mcp/router.rs +0 -154
  55. package/src/mcp/tools.rs +0 -1522
  56. package/src/mcp/types.rs +0 -108
  57. package/src/setup.rs +0 -1212
  58. package/src/storage/config.rs +0 -578
  59. package/src/storage/docs/frontmatter.rs +0 -509
  60. package/src/storage/docs/mod.rs +0 -835
  61. package/src/storage/docs/model.rs +0 -708
  62. package/src/storage/docs/reassemble.rs +0 -167
  63. package/src/storage/docs/split.rs +0 -377
  64. package/src/storage/git.rs +0 -47
  65. package/src/storage/memory/injected.rs +0 -340
  66. package/src/storage/memory/mod.rs +0 -236
  67. package/src/storage/memory/model.rs +0 -127
  68. package/src/storage/mod.rs +0 -96
  69. package/src/storage/referrals.rs +0 -248
  70. package/src/storage/sessions.rs +0 -859
  71. package/src/storage/tasks.rs +0 -957
  72. package/templates/claude-md-section.md +0 -12
package/src/mcp/tools.rs DELETED
@@ -1,1522 +0,0 @@
1
- use serde_json::{json, Value};
2
-
3
- use super::types::ToolDefinition;
4
-
5
- pub fn all_tool_definitions() -> Vec<ToolDefinition> {
6
- vec![
7
- ToolDefinition {
8
- name: "handoff_init".to_string(),
9
- description: "Initialize handoff tracking for a new project. Creates .handoff/ directory structure.".to_string(),
10
- input_schema: json!({
11
- "type": "object",
12
- "properties": {
13
- "project_dir": {
14
- "type": "string",
15
- "description": "Project directory path. Defaults to current working directory."
16
- },
17
- "project_name": {
18
- "type": "string",
19
- "description": "Project name"
20
- },
21
- "description": {
22
- "type": "string",
23
- "description": "Project description"
24
- }
25
- },
26
- "required": ["project_name"]
27
- }),
28
- },
29
- ToolDefinition {
30
- name: "handoff_load_context".to_string(),
31
- description: "Load handoff context for the current project. Call at session start to resume work. Can also resume a paused session by ID.".to_string(),
32
- input_schema: json!({
33
- "type": "object",
34
- "properties": {
35
- "project_dir": {
36
- "type": "string",
37
- "description": "Project directory path. Defaults to current working directory."
38
- },
39
- "session_id": {
40
- "type": "string",
41
- "description": "Session ID to activate and load. Searches open sessions first, then paused sessions. If omitted, activates all open sessions and returns the latest."
42
- }
43
- }
44
- }),
45
- },
46
- ToolDefinition {
47
- name: "handoff_save_context".to_string(),
48
- description: "Save current session state for the next session. Call at session end.".to_string(),
49
- input_schema: json!({
50
- "type": "object",
51
- "properties": {
52
- "project_dir": {
53
- "type": "string",
54
- "description": "Project directory path. Defaults to current working directory."
55
- },
56
- "summary": {
57
- "type": "string",
58
- "description": "One-line summary of this session"
59
- },
60
- "session_status": {
61
- "type": "string",
62
- "description": "Session status after save. 'closed' (default) = close the active session as history. 'active' = keep or create an active session (use at session start to establish a persistent session that survives interruptions).",
63
- "enum": ["closed", "active"],
64
- "default": "closed"
65
- },
66
- "close_session_id": {
67
- "type": "string",
68
- "description": "Session ID to close. If omitted (and no pause options set), active sessions are closed."
69
- },
70
- "pause_session_id": {
71
- "type": "string",
72
- "description": "Session ID to pause instead of close. The paused session can be resumed later via load_context with the same session_id. Use this when switching to different work temporarily."
73
- },
74
- "pause_active": {
75
- "type": "boolean",
76
- "description": "If true, pause all active sessions instead of closing them. Cannot be combined with close_session_id."
77
- },
78
- "pause_only": {
79
- "type": "boolean",
80
- "description": "If true, only pause sessions (via pause_session_id or pause_active) without creating a new session. Useful for session switching. When true, summary is optional."
81
- },
82
- "decisions": {
83
- "type": "array",
84
- "description": "Decisions made during this session",
85
- "items": {
86
- "type": "object",
87
- "properties": {
88
- "decision": { "type": "string", "description": "What was decided" },
89
- "reason": { "type": "string", "description": "Why this decision was made" },
90
- "confidence": {
91
- "type": "string",
92
- "description": "confirmed = verified by testing/evidence; estimated = reasoned but not verified; unverified = hypothesis needing validation",
93
- "enum": ["confirmed", "estimated", "unverified"]
94
- }
95
- },
96
- "required": ["decision"]
97
- }
98
- },
99
- "blockers": {
100
- "type": "array",
101
- "description": "Issues preventing progress. The next session should address these before starting new work.",
102
- "items": { "type": "string" }
103
- },
104
- "checklist": {
105
- "type": "array",
106
- "description": "Verification items for the next session or user. Mark completed items as checked:true before saving.",
107
- "items": {
108
- "type": "object",
109
- "properties": {
110
- "item": { "type": "string", "description": "What to verify or confirm" },
111
- "checked": { "type": "boolean", "description": "true if already verified, false if pending" },
112
- "owner": {
113
- "type": "string",
114
- "description": "user = requires human action; ai = the next AI session should handle this",
115
- "enum": ["user", "ai"]
116
- }
117
- },
118
- "required": ["item"]
119
- }
120
- },
121
- "handoff_notes": {
122
- "type": "array",
123
- "description": "Notes for the next session. Include at least one 'suggestion' with a concrete next action.",
124
- "items": {
125
- "type": "object",
126
- "properties": {
127
- "note": { "type": "string", "description": "The note content. For suggestions: state what is ALREADY DONE, then describe the concrete next action." },
128
- "category": {
129
- "type": "string",
130
- "description": "caution = risks/rules the next session must respect; context = background info for decisions; suggestion = concrete next action the next session should execute first (at least one required)",
131
- "enum": ["caution", "context", "suggestion"]
132
- }
133
- },
134
- "required": ["note"]
135
- }
136
- },
137
- "references": {
138
- "type": "array",
139
- "description": "Links to related docs, issues, MRs, or external resources for reference (not active work files — use context_pointers for those).",
140
- "items": {
141
- "type": "object",
142
- "properties": {
143
- "label": { "type": "string", "description": "Human-readable label for this reference" },
144
- "uri": { "type": "string", "description": "Path, URL, or identifier" },
145
- "type": {
146
- "type": "string",
147
- "description": "file = project file; issue = issue tracker; mr = merge/pull request; wiki = wiki page; doc = design document; url = external URL",
148
- "enum": ["file", "issue", "mr", "wiki", "doc", "url"]
149
- },
150
- "notes": { "type": "string", "description": "Additional context (e.g. 'see section 3 for root cause analysis')" }
151
- },
152
- "required": ["label", "uri"]
153
- }
154
- },
155
- "context_pointers": {
156
- "type": "array",
157
- "description": "Files the next session should open first to resume work. Point to files that NEED WORK, not completed files. For completed files, use a 'context' handoff_note instead.",
158
- "items": {
159
- "type": "object",
160
- "properties": {
161
- "path": { "type": "string", "description": "File path relative to project root" },
162
- "reason": { "type": "string", "description": "Why the next session should read this (e.g. 'resume implementation here', 'needs review')" },
163
- "lines": { "type": "string", "description": "Line range to focus on (e.g. '42-78')" }
164
- },
165
- "required": ["path"]
166
- }
167
- },
168
- "environment": {
169
- "type": "object",
170
- "description": "Free-form environment state"
171
- },
172
- "session_id": {
173
- "type": "string",
174
- "description": "Target active session ID. When multiple active sessions exist, specifies which to update/close. If omitted, uses the latest active session. Lower priority than close_session_id / pause_session_id."
175
- },
176
- "timeline": {
177
- "type": "string",
178
- "description": "Session timeline/group label (e.g. 'feature-x', 'hotfix-y')."
179
- },
180
- "label": {
181
- "type": "string",
182
- "description": "Short human-readable session label for switching UI (e.g. 'WT2作業', 'API設計')."
183
- },
184
- "related_task_ids": {
185
- "type": "array",
186
- "description": "Task IDs this session is primarily working on.",
187
- "items": { "type": "string" }
188
- }
189
- },
190
- "required": ["summary"]
191
- }),
192
- },
193
- ToolDefinition {
194
- name: "handoff_list_tasks".to_string(),
195
- description: "List all tasks for the current project with optional filters.".to_string(),
196
- input_schema: json!({
197
- "type": "object",
198
- "properties": {
199
- "project_dir": {
200
- "type": "string",
201
- "description": "Project directory path. Defaults to current working directory."
202
- },
203
- "status_filter": {
204
- "type": "string",
205
- "description": "Filter by status.",
206
- "enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
207
- },
208
- "assignee_filter": {
209
- "type": "string",
210
- "description": "Filter by assignee key."
211
- },
212
- "milestone_filter": {
213
- "type": "string",
214
- "description": "Filter by milestone name."
215
- },
216
- "priority_filter": {
217
- "type": "string",
218
- "description": "Filter by priority.",
219
- "enum": ["low", "medium", "high"]
220
- },
221
- "label_filter": {
222
- "type": "string",
223
- "description": "Filter by label (task must contain this label)."
224
- },
225
- "include_children": {
226
- "type": "boolean",
227
- "description": "If true, recursively scan project_dir for child .handoff/ projects and include their tasks. Each task gets project_name, project_dir, and task_ref fields (task_ref is a composite '{project_name}-{hash}:{id}' identifier unique across projects). The original 'id' field is left unchanged so it stays usable with handoff_get_task/handoff_update_task/dependencies when paired with the task's own project_dir. Default: false."
228
- }
229
- }
230
- }),
231
- },
232
- ToolDefinition {
233
- name: "handoff_get_task".to_string(),
234
- description: "Get full task details (notes, done_criteria, labels, links) by task ID. Use when list_tasks summary is not enough.".to_string(),
235
- input_schema: json!({
236
- "type": "object",
237
- "properties": {
238
- "project_dir": {
239
- "type": "string",
240
- "description": "Project directory path. Defaults to current working directory."
241
- },
242
- "task_id": {
243
- "type": "string",
244
- "description": "Task ID to retrieve (e.g. 't1', 't1.2')."
245
- }
246
- },
247
- "required": ["task_id"]
248
- }),
249
- },
250
- ToolDefinition {
251
- name: "handoff_check_criterion".to_string(),
252
- description: "Toggle a single done_criteria item by index. No need to resend the entire criteria list.".to_string(),
253
- input_schema: json!({
254
- "type": "object",
255
- "properties": {
256
- "project_dir": {
257
- "type": "string",
258
- "description": "Project directory path. Defaults to current working directory."
259
- },
260
- "task_id": {
261
- "type": "string",
262
- "description": "Task ID containing the criterion."
263
- },
264
- "criterion_index": {
265
- "type": "integer",
266
- "description": "0-based index of the done_criteria item to toggle."
267
- },
268
- "checked": {
269
- "type": "boolean",
270
- "description": "true to mark as checked, false to uncheck."
271
- }
272
- },
273
- "required": ["task_id", "criterion_index", "checked"]
274
- }),
275
- },
276
- ToolDefinition {
277
- name: "handoff_update_task".to_string(),
278
- description: "Add, update, or move a task. Manages the tasks/ directory structure. Include task.schedule.estimate_hours (raw human-effort hours, > 0) when moving a leaf task to in_progress/review/done; it is rejected without one unless the task is a parent, todo, blocked, or skipped.".to_string(),
279
- input_schema: json!({
280
- "type": "object",
281
- "properties": {
282
- "project_dir": {
283
- "type": "string",
284
- "description": "Project directory path. Defaults to current working directory."
285
- },
286
- "task": {
287
- "type": "object",
288
- "description": "The task to add or update. schedule.estimate_hours is REQUIRED when a leaf task is in status in_progress/review/done. Omit it for parent tasks (any task with children) or status todo/blocked/skipped.",
289
- "properties": {
290
- "id": { "type": "string", "description": "Task ID. Omit for auto-generated ID. If provided and task exists, updates it. If provided and task does not exist, creates a new task with that ID (upsert)." },
291
- "title": { "type": "string", "description": "Required for new tasks. Optional when updating (id present)." },
292
- "status": {
293
- "type": "string",
294
- "enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
295
- },
296
- "notes": { "type": "string" },
297
- "notes_append": { "type": "string", "description": "Append text to existing notes with a timestamp heading. If both notes and notes_append are provided, notes (replace) takes precedence." },
298
- "priority": {
299
- "type": "string",
300
- "enum": ["low", "medium", "high"]
301
- },
302
- "labels": {
303
- "type": "array",
304
- "items": { "type": "string" }
305
- },
306
- "links": {
307
- "type": "array",
308
- "items": { "type": "string" }
309
- },
310
- "done_criteria": {
311
- "type": "array",
312
- "items": {
313
- "type": "object",
314
- "properties": {
315
- "item": { "type": "string" },
316
- "checked": { "type": "boolean" }
317
- },
318
- "required": ["item"]
319
- }
320
- },
321
- "schedule": {
322
- "type": "object",
323
- "description": "Schedule and effort tracking. Supply estimate_hours when a leaf task enters in_progress/review/done.",
324
- "properties": {
325
- "start_date": { "type": "string", "description": "YYYY-MM-DD" },
326
- "due_date": { "type": "string", "description": "YYYY-MM-DD" },
327
- "estimate_hours": { "type": "number", "description": "REQUIRED for leaf tasks in status in_progress/review/done; the call is rejected without it. Omit for parent tasks (any task with children) or status todo/blocked/skipped. Raw human-effort hours, > 0 — do not pre-multiply by settings.ai_estimate_multiplier, which is applied at aggregation time." },
328
- "actual_hours": { "type": "number", "description": "Hours actually spent. Prefer handoff_log_time, which adds to this and decrements remaining_hours atomically." },
329
- "remaining_hours": { "type": "number", "description": "Hours remaining. Auto-decremented by handoff_log_time." },
330
- "milestone": { "type": "string" },
331
- "pinned": { "type": "boolean", "description": "If true, dates are locked and auto-scheduler skips this task." }
332
- }
333
- },
334
- "dependencies": {
335
- "type": "array",
336
- "description": "Task IDs this task depends on. Circular dependencies are rejected.",
337
- "items": { "type": "string" }
338
- },
339
- "order": {
340
- "type": "integer",
341
- "description": "Display order among siblings. 0-based, lower = higher priority."
342
- },
343
- "assignee": {
344
- "type": "string",
345
- "description": "Assignee key (matches config.toml [assignees.<key>])."
346
- }
347
- },
348
- },
349
- "parent_id": {
350
- "type": "string",
351
- "description": "Parent task ID for placement. Omit for auto-placement."
352
- },
353
- "move_to": {
354
- "type": "string",
355
- "description": "Move existing task subtree to a new parent."
356
- }
357
- },
358
- "required": ["task"]
359
- }),
360
- },
361
- ToolDefinition {
362
- name: "handoff_get_config".to_string(),
363
- description: "Read the project's handoff configuration.".to_string(),
364
- input_schema: json!({
365
- "type": "object",
366
- "properties": {
367
- "project_dir": {
368
- "type": "string",
369
- "description": "Project directory path. Defaults to current working directory."
370
- }
371
- }
372
- }),
373
- },
374
- ToolDefinition {
375
- name: "handoff_update_config".to_string(),
376
- description: "Update the project's handoff configuration.".to_string(),
377
- input_schema: json!({
378
- "type": "object",
379
- "properties": {
380
- "project_dir": {
381
- "type": "string",
382
- "description": "Project directory path. Defaults to current working directory."
383
- },
384
- "updates": {
385
- "type": "object",
386
- "description": "Key-value pairs to update (dot-notation keys like 'settings.history_limit')"
387
- }
388
- },
389
- "required": ["updates"]
390
- }),
391
- },
392
- ToolDefinition {
393
- name: "handoff_dashboard".to_string(),
394
- description: "Show handoff status across all projects in configured scan directories.".to_string(),
395
- input_schema: json!({
396
- "type": "object",
397
- "properties": {
398
- "scan_dirs": {
399
- "type": "array",
400
- "items": { "type": "string" },
401
- "description": "Directories to scan. Defaults to config's dashboard.scan_dirs."
402
- },
403
- "max_depth": {
404
- "type": "integer",
405
- "description": "Maximum directory depth for recursive scanning. Defaults to config's dashboard.max_depth (5)."
406
- },
407
- "exclude_patterns": {
408
- "type": "array",
409
- "items": { "type": "string" },
410
- "description": "Directory names to skip during recursive scanning (exact match). Defaults to config's dashboard.exclude_patterns."
411
- },
412
- "include_completed": {
413
- "type": "boolean",
414
- "description": "Include completed tasks in summary"
415
- }
416
- }
417
- }),
418
- },
419
- ToolDefinition {
420
- name: "handoff_import_context".to_string(),
421
- description: "Import existing handoff documents into .handoff/ management. AI reads the source material, structures it, and submits everything in one call. Supports nested task hierarchies via children field.".to_string(),
422
- input_schema: json!({
423
- "type": "object",
424
- "properties": {
425
- "project_dir": {
426
- "type": "string",
427
- "description": "Project directory path. Defaults to current working directory."
428
- },
429
- "source": {
430
- "type": "object",
431
- "description": "Metadata about the original document being imported",
432
- "properties": {
433
- "description": {
434
- "type": "string",
435
- "description": "What is being imported (e.g. 'Migration from tmp/260601-sprint-handoff.md')"
436
- },
437
- "format": {
438
- "type": "string",
439
- "enum": ["markdown", "json", "text", "other"],
440
- "description": "Format of the source material. Defaults to 'other'."
441
- }
442
- },
443
- "required": ["description"]
444
- },
445
- "tasks": {
446
- "type": "array",
447
- "description": "Tasks to import. Supports nested hierarchies via children field.",
448
- "items": {
449
- "$ref": "#/$defs/importTask"
450
- }
451
- },
452
- "session": {
453
- "type": "object",
454
- "description": "Session context to save. Same fields as handoff_save_context.",
455
- "properties": {
456
- "summary": { "type": "string", "description": "One-line summary (required)" },
457
- "decisions": {
458
- "type": "array",
459
- "description": "Decisions made during this session",
460
- "items": {
461
- "type": "object",
462
- "properties": {
463
- "decision": { "type": "string", "description": "What was decided" },
464
- "reason": { "type": "string", "description": "Why this decision was made" },
465
- "confidence": {
466
- "type": "string",
467
- "description": "confirmed = verified; estimated = reasoned but not verified; unverified = hypothesis",
468
- "enum": ["confirmed", "estimated", "unverified"]
469
- }
470
- },
471
- "required": ["decision"]
472
- }
473
- },
474
- "blockers": {
475
- "type": "array",
476
- "description": "Issues preventing progress",
477
- "items": { "type": "string" }
478
- },
479
- "checklist": {
480
- "type": "array",
481
- "description": "Verification items for the next session or user",
482
- "items": {
483
- "type": "object",
484
- "properties": {
485
- "item": { "type": "string", "description": "What to verify" },
486
- "checked": { "type": "boolean", "description": "true if verified, false if pending" },
487
- "owner": { "type": "string", "description": "user = human action; ai = next AI session", "enum": ["user", "ai"] }
488
- },
489
- "required": ["item"]
490
- }
491
- },
492
- "handoff_notes": {
493
- "type": "array",
494
- "description": "Notes for the next session. Include at least one 'suggestion' with a concrete next action.",
495
- "items": {
496
- "type": "object",
497
- "properties": {
498
- "note": { "type": "string", "description": "The note content. For suggestions: state what is done, then the next action." },
499
- "category": { "type": "string", "description": "caution = risks/rules; context = background; suggestion = concrete next action (at least one required)", "enum": ["caution", "context", "suggestion"] }
500
- },
501
- "required": ["note"]
502
- }
503
- },
504
- "references": {
505
- "type": "array",
506
- "description": "Links to related docs, issues, MRs (not active work files)",
507
- "items": {
508
- "type": "object",
509
- "properties": {
510
- "label": { "type": "string", "description": "Human-readable label" },
511
- "uri": { "type": "string", "description": "Path, URL, or identifier" },
512
- "type": { "type": "string", "description": "file/issue/mr/wiki/doc/url", "enum": ["file", "issue", "mr", "wiki", "doc", "url"] },
513
- "notes": { "type": "string", "description": "Additional context" }
514
- },
515
- "required": ["label", "uri"]
516
- }
517
- },
518
- "context_pointers": {
519
- "type": "array",
520
- "description": "Files the next session should open first to resume work (not completed files)",
521
- "items": {
522
- "type": "object",
523
- "properties": {
524
- "path": { "type": "string", "description": "File path relative to project root" },
525
- "reason": { "type": "string", "description": "Why to read this file" },
526
- "lines": { "type": "string", "description": "Line range (e.g. '42-78')" }
527
- },
528
- "required": ["path"]
529
- }
530
- },
531
- "environment": {
532
- "type": "object",
533
- "description": "Free-form environment state"
534
- }
535
- },
536
- "required": ["summary"]
537
- },
538
- "raw_notes": {
539
- "type": "string",
540
- "description": "Free-form text that couldn't be structured. Saved as a handoff_note with category 'context'."
541
- },
542
- "skip_session_close": {
543
- "type": "boolean",
544
- "description": "If true, do not close active sessions before creating the import session. Default false."
545
- }
546
- },
547
- "required": ["source"],
548
- "$defs": {
549
- "importTask": {
550
- "type": "object",
551
- "properties": {
552
- "title": { "type": "string" },
553
- "status": {
554
- "type": "string",
555
- "enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"]
556
- },
557
- "notes": { "type": "string" },
558
- "priority": {
559
- "type": "string",
560
- "enum": ["low", "medium", "high"]
561
- },
562
- "labels": {
563
- "type": "array",
564
- "items": { "type": "string" }
565
- },
566
- "links": {
567
- "type": "array",
568
- "items": { "type": "string" }
569
- },
570
- "done_criteria": {
571
- "type": "array",
572
- "items": {
573
- "type": "object",
574
- "properties": {
575
- "item": { "type": "string" },
576
- "checked": { "type": "boolean" }
577
- },
578
- "required": ["item"]
579
- }
580
- },
581
- "children": {
582
- "type": "array",
583
- "description": "Nested child tasks. Recursively supports the same structure.",
584
- "items": {
585
- "$ref": "#/$defs/importTask"
586
- }
587
- }
588
- },
589
- "required": ["title"]
590
- }
591
- }
592
- }),
593
- },
594
- ToolDefinition {
595
- name: "handoff_refer".to_string(),
596
- description: "Send a cross-project referral (improvement request, bug report, work request) to another project's .handoff/. The target project sees it on load_context.".to_string(),
597
- input_schema: json!({
598
- "type": "object",
599
- "properties": {
600
- "project_dir": {
601
- "type": "string",
602
- "description": "Source project directory (sender). Defaults to current working directory."
603
- },
604
- "target_project": {
605
- "type": "string",
606
- "description": "Target project name (resolved via scan_dirs). Use this OR target_project_dir."
607
- },
608
- "target_project_dir": {
609
- "type": "string",
610
- "description": "Target project directory path (absolute). Takes precedence over target_project."
611
- },
612
- "referral_type": {
613
- "type": "string",
614
- "enum": ["improvement", "bug", "request", "info"],
615
- "description": "Type of referral. Defaults to 'request'."
616
- },
617
- "summary": {
618
- "type": "string",
619
- "description": "One-line summary of the referral."
620
- },
621
- "details": {
622
- "type": "string",
623
- "description": "Detailed description of the referral."
624
- },
625
- "priority": {
626
- "type": "string",
627
- "enum": ["low", "medium", "high"],
628
- "description": "Priority of the referral."
629
- },
630
- "tasks": {
631
- "type": "array",
632
- "description": "Suggested tasks for the target project.",
633
- "items": {
634
- "type": "object",
635
- "properties": {
636
- "title": { "type": "string" },
637
- "priority": { "type": "string", "enum": ["low", "medium", "high"] },
638
- "done_criteria": {
639
- "type": "array",
640
- "items": {
641
- "type": "object",
642
- "properties": {
643
- "item": { "type": "string" },
644
- "checked": { "type": "boolean" }
645
- },
646
- "required": ["item"]
647
- }
648
- }
649
- },
650
- "required": ["title"]
651
- }
652
- },
653
- "context": {
654
- "type": "object",
655
- "description": "Additional context (branch, commit, references)."
656
- }
657
- },
658
- "required": ["summary"]
659
- }),
660
- },
661
- ToolDefinition {
662
- name: "handoff_list_referrals".to_string(),
663
- description: "List incoming referrals from other projects with optional status filter.".to_string(),
664
- input_schema: json!({
665
- "type": "object",
666
- "properties": {
667
- "project_dir": {
668
- "type": "string",
669
- "description": "Project directory path. Defaults to current working directory."
670
- },
671
- "status_filter": {
672
- "type": "string",
673
- "enum": ["open", "acknowledged", "resolved"],
674
- "description": "Filter by referral status."
675
- }
676
- }
677
- }),
678
- },
679
- ToolDefinition {
680
- name: "handoff_get_referral".to_string(),
681
- 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(),
682
- input_schema: json!({
683
- "type": "object",
684
- "properties": {
685
- "project_dir": {
686
- "type": "string",
687
- "description": "Project directory path. Defaults to current working directory."
688
- },
689
- "referral_id": {
690
- "type": "string",
691
- "description": "ID of the referral to retrieve (full id or a unique prefix)."
692
- }
693
- },
694
- "required": ["referral_id"]
695
- }),
696
- },
697
- ToolDefinition {
698
- name: "handoff_update_referral".to_string(),
699
- description: "Update the status of an incoming referral (open -> acknowledged -> resolved).".to_string(),
700
- input_schema: json!({
701
- "type": "object",
702
- "properties": {
703
- "project_dir": {
704
- "type": "string",
705
- "description": "Project directory path. Defaults to current working directory."
706
- },
707
- "referral_id": {
708
- "type": "string",
709
- "description": "ID of the referral to update."
710
- },
711
- "status": {
712
- "type": "string",
713
- "enum": ["open", "acknowledged", "resolved"],
714
- "description": "New status for the referral."
715
- }
716
- },
717
- "required": ["referral_id", "status"]
718
- }),
719
- },
720
- ToolDefinition {
721
- name: "handoff_update_session".to_string(),
722
- description: "Incrementally update the active session. Toggle checklist items, add decisions, notes, or context pointers without resending everything. Use during work for progressive updates.".to_string(),
723
- input_schema: json!({
724
- "type": "object",
725
- "properties": {
726
- "project_dir": {
727
- "type": "string",
728
- "description": "Project directory path. Defaults to current working directory."
729
- },
730
- "session_id": {
731
- "type": "string",
732
- "description": "Target active session ID. When multiple active sessions exist, specifies which to update. If omitted and multiple exist, uses the latest."
733
- },
734
- "checklist_index": {
735
- "type": "integer",
736
- "description": "0-based index of a checklist item to toggle."
737
- },
738
- "checklist_checked": {
739
- "type": "boolean",
740
- "description": "Set the checklist item to checked (true) or unchecked (false). Defaults to true."
741
- },
742
- "add_checklist_item": {
743
- "type": "string",
744
- "description": "Text of a new checklist item to add (unchecked)."
745
- },
746
- "checklist_owner": {
747
- "type": "string",
748
- "description": "Owner for the new checklist item: 'user' or 'ai'. Defaults to 'ai'.",
749
- "enum": ["user", "ai"]
750
- },
751
- "add_decision": {
752
- "type": "object",
753
- "description": "A decision to append to the session.",
754
- "properties": {
755
- "decision": { "type": "string" },
756
- "reason": { "type": "string" },
757
- "confidence": { "type": "string", "enum": ["confirmed", "estimated", "unverified"] }
758
- },
759
- "required": ["decision"]
760
- },
761
- "add_handoff_note": {
762
- "type": "object",
763
- "description": "A handoff note to append to the session.",
764
- "properties": {
765
- "note": { "type": "string" },
766
- "category": { "type": "string", "enum": ["caution", "context", "suggestion"] }
767
- },
768
- "required": ["note"]
769
- },
770
- "add_context_pointer": {
771
- "type": "object",
772
- "description": "A context pointer to append to the session.",
773
- "properties": {
774
- "path": { "type": "string" },
775
- "reason": { "type": "string" },
776
- "lines": { "type": "string" }
777
- },
778
- "required": ["path"]
779
- }
780
- }
781
- }),
782
- },
783
- ToolDefinition {
784
- name: "handoff_log_time".to_string(),
785
- description: "Log hours worked on a task. Adds to actual_hours and deducts from remaining_hours atomically.".to_string(),
786
- input_schema: json!({
787
- "type": "object",
788
- "properties": {
789
- "project_dir": {
790
- "type": "string",
791
- "description": "Project directory path. Defaults to current working directory."
792
- },
793
- "task_id": {
794
- "type": "string",
795
- "description": "Task ID to log time against."
796
- },
797
- "hours": {
798
- "type": "number",
799
- "description": "Hours worked (e.g. 0.5 for 30 minutes)."
800
- }
801
- },
802
- "required": ["task_id", "hours"]
803
- }),
804
- },
805
- ToolDefinition {
806
- name: "handoff_get_metrics".to_string(),
807
- description: "Get project metrics: completion %, effort tracking, overdue tasks, budget status, and milestone breakdown.".to_string(),
808
- input_schema: json!({
809
- "type": "object",
810
- "properties": {
811
- "project_dir": {
812
- "type": "string",
813
- "description": "Project directory path. Defaults to current working directory."
814
- },
815
- "assignee": {
816
- "type": "string",
817
- "description": "Filter metrics to a specific assignee."
818
- }
819
- }
820
- }),
821
- },
822
- ToolDefinition {
823
- name: "handoff_list_sessions".to_string(),
824
- description: "List all sessions (open, active, paused, closed) with summary info. Use handoff_get_session for full detail.".to_string(),
825
- input_schema: json!({
826
- "type": "object",
827
- "properties": {
828
- "project_dir": {
829
- "type": "string",
830
- "description": "Project directory path. Defaults to current working directory."
831
- },
832
- "status_filter": {
833
- "type": "string",
834
- "enum": ["open", "active", "paused", "closed"],
835
- "description": "Filter sessions by status."
836
- },
837
- "timeline": {
838
- "type": "string",
839
- "description": "Filter sessions by timeline label."
840
- },
841
- "limit": {
842
- "type": "integer",
843
- "description": "Max sessions to return (default 20)."
844
- },
845
- "include_children": {
846
- "type": "boolean",
847
- "description": "If true, include a 'children' array on each session showing its forked child sessions."
848
- }
849
- }
850
- }),
851
- },
852
- ToolDefinition {
853
- name: "handoff_list_assignees".to_string(),
854
- description: "List all team members/assignees from config.toml with their task counts and effort stats.".to_string(),
855
- input_schema: json!({
856
- "type": "object",
857
- "properties": {
858
- "project_dir": {
859
- "type": "string",
860
- "description": "Project directory path. Defaults to current working directory."
861
- }
862
- }
863
- }),
864
- },
865
- ToolDefinition {
866
- name: "handoff_bulk_update_tasks".to_string(),
867
- description: "Update multiple tasks in one call. Useful for applying auto-schedule results or bulk status/assignee changes. Enforces the same estimate rule as handoff_update_task: a leaf task in status in_progress/review/done must carry schedule.estimate_hours (> 0). Offending updates are rejected individually and reported in errors[]; the rest still apply.".to_string(),
868
- input_schema: json!({
869
- "type": "object",
870
- "properties": {
871
- "project_dir": {
872
- "type": "string",
873
- "description": "Project directory path. Defaults to current working directory."
874
- },
875
- "updates": {
876
- "type": "array",
877
- "description": "Array of task updates to apply. Each is validated on its own: if an update would leave a leaf task in status in_progress/review/done without schedule.estimate_hours, that update is rejected and listed in errors[] while the others still apply. Supply estimate_hours in the same update to move an estimateless task out of blocked/skipped or todo.",
878
- "items": {
879
- "type": "object",
880
- "properties": {
881
- "task_id": { "type": "string", "description": "Task ID to update." },
882
- "status": { "type": "string", "enum": ["todo", "in_progress", "review", "done", "blocked", "skipped"], "description": "Moving a leaf task into in_progress/review/done requires schedule.estimate_hours to be present or supplied in the same update. Parent tasks (any task with children) and the statuses todo/blocked/skipped are exempt." },
883
- "priority": { "type": "string", "enum": ["low", "medium", "high"] },
884
- "assignee": { "type": "string" },
885
- "notes": { "type": "string", "description": "Replace task notes." },
886
- "notes_append": { "type": "string", "description": "Append text to existing notes with a timestamp heading. If both notes and notes_append are provided, notes (replace) takes precedence." },
887
- "schedule": {
888
- "type": "object",
889
- "description": "Schedule fields to merge. Omitted fields are preserved, not cleared.",
890
- "properties": {
891
- "start_date": { "type": "string", "description": "YYYY-MM-DD" },
892
- "due_date": { "type": "string", "description": "YYYY-MM-DD" },
893
- "estimate_hours": { "type": "number", "description": "REQUIRED for a leaf task in status in_progress/review/done; the update is rejected without it. Omit for parent tasks (any task with children) or status todo/blocked/skipped. Raw human-effort hours, > 0 — do not pre-multiply by settings.ai_estimate_multiplier, which is applied at aggregation time." },
894
- "actual_hours": { "type": "number", "description": "Hours actually spent. Prefer handoff_log_time, which adds to this and decrements remaining_hours atomically." },
895
- "remaining_hours": { "type": "number", "description": "Hours remaining. Auto-decremented by handoff_log_time." },
896
- "milestone": { "type": "string" },
897
- "pinned": { "type": "boolean", "description": "If true, dates are locked and auto-scheduler skips this task." }
898
- }
899
- }
900
- },
901
- "required": ["task_id"]
902
- }
903
- }
904
- },
905
- "required": ["updates"]
906
- }),
907
- },
908
- ToolDefinition {
909
- name: "handoff_get_session".to_string(),
910
- description: "Get full detail of a specific session by ID. Returns decisions, checklist, handoff notes, context pointers, etc.".to_string(),
911
- input_schema: json!({
912
- "type": "object",
913
- "properties": {
914
- "project_dir": {
915
- "type": "string",
916
- "description": "Project directory path. Defaults to current working directory."
917
- },
918
- "session_id": {
919
- "type": "string",
920
- "description": "Session ID to retrieve."
921
- }
922
- },
923
- "required": ["session_id"]
924
- }),
925
- },
926
- ToolDefinition {
927
- name: "handoff_get_capacity".to_string(),
928
- 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(),
929
- input_schema: json!({
930
- "type": "object",
931
- "properties": {
932
- "project_dir": {
933
- "type": "string",
934
- "description": "Project directory path. Defaults to current working directory."
935
- },
936
- "start_date": {
937
- "type": "string",
938
- "description": "Start date (YYYY-MM-DD)."
939
- },
940
- "end_date": {
941
- "type": "string",
942
- "description": "End date (YYYY-MM-DD)."
943
- },
944
- "assignee": {
945
- "type": "string",
946
- "description": "Filter capacity to a specific assignee's calendar."
947
- }
948
- },
949
- "required": ["start_date", "end_date"]
950
- }),
951
- },
952
- ToolDefinition {
953
- name: "handoff_auto_schedule".to_string(),
954
- 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(),
955
- input_schema: json!({
956
- "type": "object",
957
- "properties": {
958
- "project_dir": {
959
- "type": "string",
960
- "description": "Project directory path. Defaults to current working directory."
961
- },
962
- "dry_run": {
963
- "type": "boolean",
964
- "description": "If true (default), return computed spans without writing. If false, apply changes to task files."
965
- },
966
- "assignee_filter": {
967
- "type": "string",
968
- "description": "Only schedule tasks assigned to this assignee."
969
- },
970
- "start_date": {
971
- "type": "string",
972
- "description": "Anchor date YYYY-MM-DD for the earliest task. Defaults to today (UTC)."
973
- }
974
- }
975
- }),
976
- },
977
- ToolDefinition {
978
- name: "handoff_add_assignee".to_string(),
979
- description: "Add a team member to config.toml [assignees.<key>]. Fails if the key already exists.".to_string(),
980
- input_schema: assignee_write_schema(true),
981
- },
982
- ToolDefinition {
983
- name: "handoff_update_assignee".to_string(),
984
- description: "Update an existing [assignees.<key>] entry. Only provided fields change; pass null to clear a field.".to_string(),
985
- input_schema: assignee_write_schema(false),
986
- },
987
- ToolDefinition {
988
- name: "handoff_remove_assignee".to_string(),
989
- description: "Remove a team member from config.toml and unassign them from every task.".to_string(),
990
- input_schema: json!({
991
- "type": "object",
992
- "properties": {
993
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
994
- "key": { "type": "string", "description": "Assignee key to remove." }
995
- },
996
- "required": ["key"]
997
- }),
998
- },
999
- ToolDefinition {
1000
- name: "handoff_list_milestones".to_string(),
1001
- description: "List all milestones defined in config.toml [milestones.*].".to_string(),
1002
- input_schema: json!({
1003
- "type": "object",
1004
- "properties": {
1005
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." }
1006
- }
1007
- }),
1008
- },
1009
- ToolDefinition {
1010
- name: "handoff_add_milestone".to_string(),
1011
- description: "Add a milestone to config.toml [milestones.<name>]. Fails if it already exists.".to_string(),
1012
- input_schema: milestone_write_schema(),
1013
- },
1014
- ToolDefinition {
1015
- name: "handoff_update_milestone".to_string(),
1016
- description: "Update an existing [milestones.<name>] entry. Pass null to clear a field.".to_string(),
1017
- input_schema: milestone_write_schema(),
1018
- },
1019
- ToolDefinition {
1020
- name: "handoff_remove_milestone".to_string(),
1021
- description: "Remove a milestone from config.toml.".to_string(),
1022
- input_schema: json!({
1023
- "type": "object",
1024
- "properties": {
1025
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1026
- "name": { "type": "string", "description": "Milestone name to remove." }
1027
- },
1028
- "required": ["name"]
1029
- }),
1030
- },
1031
- ToolDefinition {
1032
- name: "handoff_update_calendar".to_string(),
1033
- description: "Patch the project [calendar] section (work hours, closed days, day_hours, schedule_mode). Only provided fields change.".to_string(),
1034
- input_schema: json!({
1035
- "type": "object",
1036
- "properties": {
1037
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1038
- "work_hours_per_day": { "type": "number", "description": "Default working hours per day." },
1039
- "closed_weekdays": { "type": "array", "description": "Non-working weekdays (0=Sun..6=Sat, or names like \"sat\").", "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 dates that override closed weekdays.", "items": { "type": "string" } },
1042
- "day_hours": { "type": "object", "description": "Per-weekday-name or per-date hour overrides, e.g. {\"fri\": 4, \"2026-07-01\": 0}.", "additionalProperties": { "type": "number" } },
1043
- "schedule_mode": { "type": "string", "description": "\"manual\" or \"auto\"." },
1044
- "overwork_limit_percent": { "type": "number" },
1045
- "max_utilization": { "type": "number" }
1046
- }
1047
- }),
1048
- },
1049
- ToolDefinition {
1050
- name: "handoff_update_labels".to_string(),
1051
- description: "Set the project-level label vocabulary (top-level labels array in config.toml).".to_string(),
1052
- input_schema: json!({
1053
- "type": "object",
1054
- "properties": {
1055
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1056
- "labels": { "type": "array", "description": "Full replacement list of project labels.", "items": { "type": "string" } }
1057
- },
1058
- "required": ["labels"]
1059
- }),
1060
- },
1061
- ToolDefinition {
1062
- name: "handoff_start_project".to_string(),
1063
- description: "Set the project started_at date and optionally shift all task dates so the earliest start aligns to it.".to_string(),
1064
- input_schema: json!({
1065
- "type": "object",
1066
- "properties": {
1067
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1068
- "start_date": { "type": "string", "description": "Project start date YYYY-MM-DD. Defaults to today (UTC)." },
1069
- "shift_dates": { "type": "boolean", "description": "If true, shift every task's start/due dates so the earliest start lands on start_date." }
1070
- }
1071
- }),
1072
- },
1073
- ToolDefinition {
1074
- name: "handoff_memory_save".to_string(),
1075
- description: "Save a long-lived project memory (lesson/rule/convention/gotcha) that future sessions should respect. Detects exact and near-duplicate memories: an exact match is reported (not rewritten), a near-duplicate is returned as a 'conflict' with both bodies for you to merge (call again with merge_into=<id> and absorb_ids=[…]) or save separately with force=true. Returns a JSON string.".to_string(),
1076
- input_schema: json!({
1077
- "type": "object",
1078
- "properties": {
1079
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1080
- "text": { "type": "string", "description": "The memory body (any language). Required, non-empty." },
1081
- "kind": { "type": "string", "description": "Memory kind.", "enum": ["lesson", "rule", "convention", "gotcha"], "default": "lesson" },
1082
- "tags": { "type": "array", "items": { "type": "string" }, "description": "Optional tags; also indexed for similarity." },
1083
- "keywords": { "type": "array", "items": { "type": "string" }, "description": "Subject keywords — nouns, technical terms, proper nouns that identify what this memory is about. These are weighted higher than body text in BM25 relevance scoring. Distinct from tags (classification labels)." },
1084
- "scope_paths": { "type": "array", "items": { "type": "string" }, "description": "Path prefixes this memory applies to (e.g. 'src/storage/'). Boosts relevance when a query touches a matching file." },
1085
- "merge_into": { "type": "string", "description": "Commit an AI merge: overwrite this memory id with `text` and absorb `absorb_ids`." },
1086
- "absorb_ids": { "type": "array", "items": { "type": "string" }, "description": "Memory ids to delete and record as superseded when merging." },
1087
- "force": { "type": "boolean", "description": "Save even if a near-duplicate exists (skip the conflict response).", "default": false }
1088
- },
1089
- "required": ["text"]
1090
- }),
1091
- },
1092
- ToolDefinition {
1093
- name: "handoff_memory_query".to_string(),
1094
- description: "Return the project memories most relevant to the given text/file (BM25 + scope-path boosting). Intended for automatic injection via hooks, but callable directly. Returns a JSON string {\"memories\":[{id,text,kind,score}],\"injected_count\"}.".to_string(),
1095
- input_schema: json!({
1096
- "type": "object",
1097
- "properties": {
1098
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1099
- "session_id": { "type": "string", "description": "Hook session id. When given, memories already injected this session (same content hash) are filtered out; an edited memory is re-injected." },
1100
- "text": { "type": "string", "description": "The current prompt or context text to match against." },
1101
- "tool_name": { "type": "string", "description": "Name of the tool about to run (e.g. 'Edit'); added to the query." },
1102
- "file_paths": { "type": "array", "items": { "type": "string" }, "description": "File paths in play; basenames are added to the query and scope_paths are matched against these." },
1103
- "limit": { "type": "integer", "description": "Maximum memories to return.", "default": 5 },
1104
- "mark_injected": { "type": "boolean", "description": "Record returned memories in the session sidecar and bump their hit_count/last_referenced_at. Requires session_id.", "default": true }
1105
- },
1106
- "required": ["text"]
1107
- }),
1108
- },
1109
- ToolDefinition {
1110
- name: "handoff_memory_delete".to_string(),
1111
- description: "Delete a project memory by id (full id or unique prefix). Use for AI-driven cleanup of stale memories. Returns a JSON string {\"status\":\"deleted\",\"id\"}.".to_string(),
1112
- input_schema: json!({
1113
- "type": "object",
1114
- "properties": {
1115
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1116
- "id": { "type": "string", "description": "Memory id to delete (full id or unique prefix)." }
1117
- },
1118
- "required": ["id"]
1119
- }),
1120
- },
1121
- ToolDefinition {
1122
- name: "handoff_memory_cleanup".to_string(),
1123
- description: "Housekeep the project memory store (intended for SessionStart). Silently merges exact duplicates (lossless), then returns recommendations the AI should act on: near-duplicate clusters (merge with memory_save merge_into=…) and stale memories (consider memory_delete). Also garbage-collects old per-session injection sidecars. Returns a JSON string {\"auto_merged_exact\":n,\"cleanup_recommendations\":{\"similar_clusters\":[…],\"stale\":[…]},\"injected_sidecars_removed\":k}.".to_string(),
1124
- input_schema: json!({
1125
- "type": "object",
1126
- "properties": {
1127
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1128
- "apply_exact_merges": { "type": "boolean", "description": "Auto-merge exact-duplicate memories (same content hash). Lossless and safe.", "default": true },
1129
- "stale_days": { "type": "integer", "description": "Flag memories not referenced for this many days as stale recommendations.", "default": 60 }
1130
- }
1131
- }),
1132
- },
1133
- // ---- Session fork/merge tools ----
1134
- ToolDefinition {
1135
- name: "handoff_fork_session".to_string(),
1136
- description: "Fork a new session from an existing one. Inherits decisions, context_pointers, references, and handoff_notes by default. The forked session becomes active with parent_session_id set.".to_string(),
1137
- input_schema: json!({
1138
- "type": "object",
1139
- "properties": {
1140
- "project_dir": {
1141
- "type": "string",
1142
- "description": "Project directory path. Defaults to current working directory."
1143
- },
1144
- "source_session_id": {
1145
- "type": "string",
1146
- "description": "Session ID to fork from (active, paused, or closed)."
1147
- },
1148
- "summary": {
1149
- "type": "string",
1150
- "description": "Summary for the new forked session."
1151
- },
1152
- "label": {
1153
- "type": "string",
1154
- "description": "Short human-readable label for the forked session."
1155
- },
1156
- "timeline": {
1157
- "type": "string",
1158
- "description": "Timeline label. Defaults to the source session's timeline."
1159
- },
1160
- "inherit": {
1161
- "type": "array",
1162
- "description": "Fields to inherit from the source. Default: [\"decisions\", \"context_pointers\", \"references\", \"handoff_notes\", \"environment\"]. Available: decisions, context_pointers, references, handoff_notes, environment, blockers, checklist.",
1163
- "items": { "type": "string" }
1164
- },
1165
- "related_task_ids": {
1166
- "type": "array",
1167
- "description": "Task IDs the forked session will work on.",
1168
- "items": { "type": "string" }
1169
- }
1170
- },
1171
- "required": ["source_session_id", "summary"]
1172
- }),
1173
- },
1174
- ToolDefinition {
1175
- name: "handoff_merge_sessions".to_string(),
1176
- description: "Merge multiple sessions into one. Combines decisions, notes, references, and context_pointers. Detects duplicate decisions as conflicts. Source sessions (except the target) are closed by default.".to_string(),
1177
- input_schema: json!({
1178
- "type": "object",
1179
- "properties": {
1180
- "project_dir": {
1181
- "type": "string",
1182
- "description": "Project directory path. Defaults to current working directory."
1183
- },
1184
- "source_session_ids": {
1185
- "type": "array",
1186
- "description": "Session IDs to merge (must include at least 2).",
1187
- "items": { "type": "string" }
1188
- },
1189
- "target_session_id": {
1190
- "type": "string",
1191
- "description": "Which source session becomes the merge target (must be one of source_session_ids)."
1192
- },
1193
- "close_sources": {
1194
- "type": "boolean",
1195
- "description": "Close non-target source sessions after merge. Default: true."
1196
- }
1197
- },
1198
- "required": ["source_session_ids", "target_session_id"]
1199
- }),
1200
- },
1201
- // ---- Timer coordination tools ----
1202
- ToolDefinition {
1203
- name: "handoff_timer_start".to_string(),
1204
- description: "Start a timer for a task. If VSCode extension is running (authority alive), delegates to the extension via a request file. Otherwise starts an MCP fallback timer.".to_string(),
1205
- input_schema: json!({
1206
- "type": "object",
1207
- "properties": {
1208
- "task_id": { "type": "string", "description": "Task ID to start timing (e.g. 't1', 't1.2')." },
1209
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." }
1210
- },
1211
- "required": ["task_id"]
1212
- }),
1213
- },
1214
- ToolDefinition {
1215
- name: "handoff_timer_stop".to_string(),
1216
- description: "Stop the timer for a task. If VSCode extension is the authority, delegates the stop command. If MCP is the authority (fallback), stops the internal timer and adds elapsed time to the task's actual_hours (with optimistic locking).".to_string(),
1217
- input_schema: json!({
1218
- "type": "object",
1219
- "properties": {
1220
- "task_id": { "type": "string", "description": "Task ID to stop timing." },
1221
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." }
1222
- },
1223
- "required": ["task_id"]
1224
- }),
1225
- },
1226
- ToolDefinition {
1227
- name: "handoff_timer_get_time".to_string(),
1228
- description: "Get the current timer state for a task. Returns elapsed time, timer state (tracking/paused/stopped), authority info, and projected total hours. Reads from .handoff/timer/state.json.".to_string(),
1229
- input_schema: json!({
1230
- "type": "object",
1231
- "properties": {
1232
- "task_id": { "type": "string", "description": "Task ID to query timer for." },
1233
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." }
1234
- },
1235
- "required": ["task_id"]
1236
- }),
1237
- },
1238
- // ---- Document management tools (P1-6a, v5 rearchitecture: wiki/130-document-management.md §3.1) ----
1239
- ToolDefinition {
1240
- name: "handoff_doc_save".to_string(),
1241
- description: "Save a complete document. The body MUST be a whole, human-readable Markdown document starting with a level-1 heading (e.g. `# Authentication Spec`) — group related content into ONE document (for example, all ADRs belong in a single `# Architecture Decision Records` document with each ADR as an `## ADR-001: ...` section, NOT as separate documents). MCP handles section-level indexing internally, so do not pre-split content yourself. To append a new section to an existing document (e.g. adding ADR-003) without rewriting the whole body, pass `append_body` with just the new section instead of `body` — `body` and `append_body` are mutually exclusive, and `append_body` requires an existing `doc_id` (there is nothing to append to when creating a new document). The body (or, for append_body, the resulting combined body) is stored verbatim at _doc.<slug>.md and split in-memory into a `sections` byte-offset index (no per-section files), syncing the bidirectional task<->doc link when task_ids is given. Omit doc_id to create a new document (slug is then required and must be unique); pass an existing doc_id to update it (slug is taken from the existing document — it cannot be renamed via doc_save). Returns a JSON string {doc_id,slug,title,doc_type,section_count,content_hash,warnings:[…]} — warnings lists any task_ids that could not be resolved, and includes a soft notice when the saved body does not start with a level-1 heading (the save is never rejected for this).".to_string(),
1242
- input_schema: json!({
1243
- "type": "object",
1244
- "properties": {
1245
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1246
- "doc_id": { "type": "string", "description": "Existing document id to update. Omit to create a new document. Required when append_body is given." },
1247
- "slug": { "type": "string", "description": "Human-readable file-naming slug ([a-z0-9-], max 60 chars), used to name _doc.<slug>.json/.md. Required when creating; ignored on update (the existing document's slug is kept)." },
1248
- "title": { "type": "string", "description": "Document title. Required when creating; optional on update or append (defaults to the existing title)." },
1249
- "body": { "type": "string", "description": "Full Markdown document, starting with a level-1 heading. Mutually exclusive with append_body; one of the two is required." },
1250
- "append_body": { "type": "string", "description": "New section(s) to append to an existing document's body (e.g. `## ADR-003: ...`). Joined onto the existing body with `separator` before the usual split/save. Requires doc_id. Mutually exclusive with body; one of the two is required. Use the same line-ending style as the existing document." },
1251
- "separator": { "type": "string", "description": "append_body only: text inserted between the existing body and append_body.", "default": "\n\n" },
1252
- "doc_type": { "type": "string", "description": "Document type.", "enum": ["spec", "design", "adr", "guide", "note"], "default": "note" },
1253
- "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags for filtering/search." },
1254
- "scope_paths": { "type": "array", "items": { "type": "string" }, "description": "Path prefixes this document applies to; boosts relevance in doc_list(query=...) when a file path matches." },
1255
- "parent_id": { "type": "string", "description": "Parent document id (family tree)." },
1256
- "task_ids": { "type": "array", "items": { "type": "string" }, "description": "Task ids to link bidirectionally. On update, ids removed from this list are unlinked; ids added are linked." },
1257
- "related": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "rel": { "type": "string", "enum": ["supersedes", "references", "implements", "extends", "conflicts"] } }, "required": ["id", "rel"] }, "description": "Sibling/relative relationships to other documents." },
1258
- "split_level": { "type": "integer", "description": "ATX heading level at/above which the body is split into sections.", "default": 2 },
1259
- "auto_inject": { "type": "string", "description": "Auto-injection control.", "enum": ["auto", "full", "outline", "none"], "default": "auto" }
1260
- }
1261
- }),
1262
- },
1263
- ToolDefinition {
1264
- name: "handoff_doc_update_section".to_string(),
1265
- description: "Replace a single section of a document by its seq number, without re-sending the whole document body. new_content is the replacement Markdown (including the section's own heading line); an empty string deletes the section. Sections are computed on-demand from the current body (same as doc_get/doc_save), so seq must match the document's current section numbering. expected_hash is an optional optimistic lock: when given, it must match the section's current content_hash or the call errors with the actual current hash (for retry) and makes no change. Updates the document's updated_at and content_hash, and — if a verification matrix item exists at this fragment_seq — surfaces a warning that it is now stale (its content_hash_at_verify no longer matches). Returns a JSON string {doc_id,seq,heading,content_hash,updated_at,section_count,warnings?:[…]}.".to_string(),
1266
- input_schema: json!({
1267
- "type": "object",
1268
- "properties": {
1269
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1270
- "doc_id": { "type": "string", "description": "Document id or slug to update." },
1271
- "seq": { "type": "integer", "description": "Section sequence number to replace (0 = preamble)." },
1272
- "new_content": { "type": "string", "description": "Replacement Markdown text for this section, including its own heading line. An empty string deletes the section." },
1273
- "expected_hash": { "type": "string", "description": "Optimistic lock: the section's expected current content_hash. If it does not match, the update is rejected with the actual current hash." }
1274
- },
1275
- "required": ["doc_id", "seq", "new_content"]
1276
- }),
1277
- },
1278
- ToolDefinition {
1279
- name: "handoff_doc_get".to_string(),
1280
- description: "Read a document by doc_id or slug. format='full' returns the original Markdown body (read directly from _doc.<slug>.md) plus metadata; 'meta' returns metadata only (no body, cheap for graph traversal); 'section' returns one section's body (byte-sliced from the document body, requires seq). Returns a JSON string.".to_string(),
1281
- input_schema: json!({
1282
- "type": "object",
1283
- "properties": {
1284
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1285
- "doc_id": { "type": "string", "description": "Document id or slug to read." },
1286
- "format": { "type": "string", "description": "Read mode.", "enum": ["full", "meta", "section"], "default": "full" },
1287
- "seq": { "type": "integer", "description": "Section sequence number. Required when format='section'." }
1288
- },
1289
- "required": ["doc_id"]
1290
- }),
1291
- },
1292
- ToolDefinition {
1293
- name: "handoff_doc_list".to_string(),
1294
- description: "List/search documents. Filters (doc_type, tags [AND — every tag must be present], task_id) are applied first; an optional query BM25-ranks the survivors by title + tags + body text. include_body includes each matching document's full body, read from _doc.<slug>.md (default false — metadata only). Returns a JSON string {documents:[…]}.".to_string(),
1295
- input_schema: json!({
1296
- "type": "object",
1297
- "properties": {
1298
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1299
- "doc_type": { "type": "string", "description": "Filter by document type." },
1300
- "tags": { "type": "array", "items": { "type": "string" }, "description": "Filter: document must have every listed tag (AND)." },
1301
- "task_id": { "type": "string", "description": "Filter: only documents linked to this task." },
1302
- "include_body": { "type": "boolean", "description": "Include each document's full body.", "default": false },
1303
- "query": { "type": "string", "description": "BM25 text search over title + tags + body." }
1304
- }
1305
- }),
1306
- },
1307
- ToolDefinition {
1308
- name: "handoff_doc_delete".to_string(),
1309
- description: "Delete a document (by doc_id or slug) and its body file. Unlinks the document from any linked tasks' task_links, removes it from its parent's children list, and clears parent_id on any of its own children (orphaning them — delete does not cascade to descendants). Returns a JSON string {deleted,doc_id,section_count,warnings:[…]}.".to_string(),
1310
- input_schema: json!({
1311
- "type": "object",
1312
- "properties": {
1313
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1314
- "doc_id": { "type": "string", "description": "Document id or slug to delete." }
1315
- },
1316
- "required": ["doc_id"]
1317
- }),
1318
- },
1319
- ToolDefinition {
1320
- name: "handoff_doc_reassemble".to_string(),
1321
- description: "Read a document's (by doc_id or slug) original Markdown body directly from _doc.<slug>.md, restoring BOM/frontmatter, and detect drift (the body's current content hash no longer matches its recorded content_hash — e.g. edited directly outside doc_save). Optionally writes the body to output_path. Returns a JSON string {doc_id,body,drifted,output_path?}.".to_string(),
1322
- input_schema: json!({
1323
- "type": "object",
1324
- "properties": {
1325
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1326
- "doc_id": { "type": "string", "description": "Document id or slug to reassemble." },
1327
- "output_path": { "type": "string", "description": "Optional filesystem path to write the reassembled body to." }
1328
- },
1329
- "required": ["doc_id"]
1330
- }),
1331
- },
1332
- ToolDefinition {
1333
- name: "handoff_doc_tree".to_string(),
1334
- description: "Traverse a document's family tree (parent/children) starting from doc_id (id or slug), up to depth levels of descendants, plus the immediate parent (if any). include_related additionally attaches the document's related (semantic) links. Returns a JSON string tree {id,title,doc_type,parent,children:[…],related:[…]}.".to_string(),
1335
- input_schema: json!({
1336
- "type": "object",
1337
- "properties": {
1338
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1339
- "doc_id": { "type": "string", "description": "Root document id or slug to traverse from." },
1340
- "depth": { "type": "integer", "description": "How many levels of children to descend.", "default": 2 },
1341
- "include_related": { "type": "boolean", "description": "Also include the root document's `related` entries.", "default": false }
1342
- },
1343
- "required": ["doc_id"]
1344
- }),
1345
- },
1346
- ToolDefinition {
1347
- name: "handoff_doc_verify".to_string(),
1348
- description: "Operate on a document's verification matrix (wiki/140-verification-matrix.md): generate (create a matrix from the document's current sections, error if one already exists), check (mark fragment_seq — or its sub_item_index — verified, recording verified_at/reviewer/notes/content_hash_at_verify — fragment_seq may be a single section seq or an array of seqs to verify in one call), check_all (verify every section and sub_item in the matrix in one call, error if no matrix exists yet), skip (mark fragment_seq — or its sub_item_index — skipped), sync (re-sync the matrix with the document's current sections — adds new sections as pending, removes deleted ones, preserves existing item status), set_refs (update impl_refs/test_refs for fragment_seq), add_item (v2, spec §7.2: with fragment_seq given, append a SubItem — description required — to that section's sub_items; with fragment_seq omitted, append a new freeform top-level item not tied to any section — label required), or suggest_refs (t124.6: read-only — scans the document's scope_paths for source/test files whose fn/struct/impl/mod definitions or test functions fuzzy-match each verification item's heading, and returns candidate impl_refs/test_refs per item for the caller to review and apply via set_refs; errors if no matrix exists yet). Overall verification_status is recomputed after every mutation: 'pending' if all items pending, 'verified' if all verified/skipped, else 'in_review'. Returns a JSON string {doc_id,verification_status,checked,skipped,pending,total,stale} for mutating actions, or {doc_id,suggestions:[{fragment_seq,heading,suggested_impl_refs,suggested_test_refs}]} for suggest_refs.".to_string(),
1349
- input_schema: json!({
1350
- "type": "object",
1351
- "properties": {
1352
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1353
- "doc_id": { "type": "string", "description": "Document id or slug to operate on." },
1354
- "action": { "type": "string", "description": "Verification matrix action.", "enum": ["generate", "check", "check_all", "skip", "sync", "set_refs", "add_item", "suggest_refs"] },
1355
- "skip_seqs": { "type": "array", "items": { "type": "integer" }, "description": "generate only: section seqs to create as 'skipped' instead of 'pending'." },
1356
- "fragment_seq": { "description": "skip/set_refs: the section seq (VerificationItem.fragment_seq) to operate on. check: a single section seq, or an array of section seqs to verify in one call. add_item: the section seq to attach a new sub_item to; omit to add a freeform top-level item instead.", "oneOf": [ { "type": "integer" }, { "type": "array", "items": { "type": "integer" } } ] },
1357
- "sub_item_index": { "type": "integer", "description": "check/skip: the 0-based SubItem.index within fragment_seq's sub_items to operate on, instead of the parent item itself." },
1358
- "description": { "type": "string", "description": "add_item (fragment_seq given): the new sub_item's description. Required in this form." },
1359
- "label": { "type": "string", "description": "add_item (fragment_seq omitted): the new freeform top-level item's label. Required in this form." },
1360
- "category": { "type": "string", "description": "add_item: item/sub_item category (free-extensible, e.g. 'requirement', 'visual', 'regression', 'manual'). Defaults to 'requirement' for sub_items, 'visual' for freeform items." },
1361
- "reviewer": { "type": "string", "description": "check/check_all: who verified it.", "enum": ["ai", "user"] },
1362
- "notes": { "type": "string", "description": "check/check_all: optional free-text note." },
1363
- "impl_refs": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "lines": { "type": "string" }, "label": { "type": "string" } }, "required": ["path"] }, "description": "set_refs: implementation code references to attach to fragment_seq." },
1364
- "test_refs": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "lines": { "type": "string" }, "label": { "type": "string" } }, "required": ["path"] }, "description": "set_refs: test code references to attach to fragment_seq." }
1365
- },
1366
- "required": ["doc_id", "action"]
1367
- }),
1368
- },
1369
- ToolDefinition {
1370
- name: "handoff_doc_verify_status".to_string(),
1371
- description: "Get a document's verification matrix status: overall verification_status, progress counts (checked/skipped/pending/total/stale/percentage — v2: counts leaf items, i.e. sub_items and freeform items, spec §7.4), and (when include_items=true) every item with a computed stale flag (its content_hash_at_verify no longer matches the section's current content_hash — spec §3.5). Errors if the document has no verification matrix yet (use handoff_doc_verify(action='generate') first). Returns a JSON string {doc_id,title,verification_status,progress:{…},items?:[…]} by default, or (format='checklist') a Markdown checklist rendering (spec §7.3) instead.".to_string(),
1372
- input_schema: json!({
1373
- "type": "object",
1374
- "properties": {
1375
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1376
- "doc_id": { "type": "string", "description": "Document id or slug to read verification status for." },
1377
- "include_items": { "type": "boolean", "description": "Include the full per-item list (with stale detection).", "default": false },
1378
- "format": { "type": "string", "description": "Output format: 'json' (default, structured payload) or 'checklist' (Markdown checklist rendering with headings, sub_item checkboxes, refs, and categories).", "enum": ["json", "checklist"], "default": "json" }
1379
- },
1380
- "required": ["doc_id"]
1381
- }),
1382
- },
1383
- ToolDefinition {
1384
- name: "handoff_doc_graph".to_string(),
1385
- description: "Build a graph of every document in the project: nodes (one per document, with id/slug/title/doc_type/tags/task_ids/section_count/updated_at, plus verification_progress {total,verified} when include_verification=true and a matrix exists), edges (explicit parent_id -> type='parent_child'/direction='down', explicit related[] -> type=<rel>/direction='forward', and — when include_implicit=true — implicit shared_task edges for documents sharing task_ids and shared_scope edges for documents sharing scope_paths), and layers (doc ids grouped by doc_type). Intended for graph-visualization UIs. Returns a JSON string {nodes:[…],edges:[…],layers:{…}}.".to_string(),
1386
- input_schema: json!({
1387
- "type": "object",
1388
- "properties": {
1389
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1390
- "include_implicit": { "type": "boolean", "description": "Also emit shared_task/shared_scope implicit edges.", "default": true },
1391
- "include_verification": { "type": "boolean", "description": "Attach verification_progress {total,verified} to each node that has a verification matrix.", "default": false }
1392
- }
1393
- }),
1394
- },
1395
- ToolDefinition {
1396
- name: "handoff_doc_trace".to_string(),
1397
- description: "Trace a document's family-tree lineage from doc_id (id or slug): direction='up' walks the child->parent chain to the root; 'down' walks parent->children (DFS); 'both' (default) merges the up chain, the target doc, and the down chain into one ordered chain (root to leaf). related (implements/references/etc.) documents encountered along the chain are appended as detour entries. Multi-child forks encountered in the down direction are additionally reported in branches[] (one entry per fork, {fork_from,docs:[…]}). Cycle-safe: a visited set skips any document already seen in the traversal. Returns a JSON string {chain:[{id,title,doc_type,rel}…],branches:[{fork_from,docs:[…]}…]}.".to_string(),
1398
- input_schema: json!({
1399
- "type": "object",
1400
- "properties": {
1401
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1402
- "doc_id": { "type": "string", "description": "Document id or slug to trace from." },
1403
- "direction": { "type": "string", "description": "Traversal direction.", "enum": ["up", "down", "both"], "default": "both" }
1404
- },
1405
- "required": ["doc_id"]
1406
- }),
1407
- },
1408
- ToolDefinition {
1409
- name: "handoff_doc_query".to_string(),
1410
- description: "Inject document sections relevant to the current prompt/file/task (hook-driven context injection, mirrors memory_query at section granularity). Ranks by BM25 relevance + scope_paths match + task_id affinity, then stages each result as 'full' (whole section body, when its token estimate is within the inline threshold) or 'outline' (heading + sibling table of contents only, for larger sections — fetch the body via doc_get(format='section')). With session_id, already-injected sections (same content_hash) are skipped this session; mark_injected (default true) records survivors. suppress_doc_ids excludes given documents from this call's results; combined with suppress_until_changed=true (requires session_id), the suppression is recorded in the session's injected sidecar and persists across future calls until that document's content_hash changes. Returns a JSON string {documents:[…],injected_count}.".to_string(),
1411
- input_schema: json!({
1412
- "type": "object",
1413
- "properties": {
1414
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1415
- "text": { "type": "string", "description": "Prompt/query text to rank sections against." },
1416
- "file_paths": { "type": "array", "items": { "type": "string" }, "description": "File paths in play; boosts documents whose scope_paths match." },
1417
- "task_id": { "type": "string", "description": "Boost sections belonging to documents linked to this task (highest-weight ranking signal)." },
1418
- "session_id": { "type": "string", "description": "Session id for per-session diff injection (skips sections already injected at their current content_hash)." },
1419
- "limit": { "type": "integer", "description": "Max number of sections to return.", "default": 5 },
1420
- "mark_injected": { "type": "boolean", "description": "Record returned sections in the session's injected sidecar.", "default": true },
1421
- "suppress_doc_ids": { "type": "array", "items": { "type": "string" }, "description": "Document ids to exclude entirely from this call's results." },
1422
- "suppress_until_changed": { "type": "boolean", "description": "With suppress_doc_ids and session_id: persist the suppression in the session's injected sidecar so those documents stay excluded from future doc_query calls until their content_hash changes.", "default": false }
1423
- }
1424
- }),
1425
- },
1426
- ToolDefinition {
1427
- name: "handoff_doc_analyze".to_string(),
1428
- description: "Read-only scan of a Markdown file or directory (never writes). Auto-detects doc_type (keyword scan), tags (frontmatter + heading tokens), scope_paths (code/inline file paths), and a suggested_slug (derived from title) per file; extracts and classifies Markdown links (internal/external/broken); proposes a parent/children tree from directory structure (skip with flatten=true). Returns a JSON conditioning report {files_scanned,auto_resolved:[…],needs_review:[…],proposed_tree:{…}} for AI review before handoff_doc_import.".to_string(),
1429
- input_schema: json!({
1430
- "type": "object",
1431
- "properties": {
1432
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1433
- "path": { "type": "string", "description": "File or directory path (relative to project_dir) to scan." },
1434
- "recursive": { "type": "boolean", "description": "Recurse into subdirectories when path is a directory.", "default": true },
1435
- "flatten": { "type": "boolean", "description": "Skip parent/children tree inference; every file is a standalone document.", "default": false }
1436
- },
1437
- "required": ["path"]
1438
- }),
1439
- },
1440
- ToolDefinition {
1441
- name: "handoff_doc_import".to_string(),
1442
- description: "Bulk-import pre-existing Markdown files. Each file becomes ONE document in .handoff/docs/ — the file's content is stored as-is, including its h1 heading. Do NOT split a single source file into multiple documents; MCP indexes sections internally. Writes an analyzed payload (from handoff_doc_analyze, with the AI's overrides applied) as new documents. Each analyzed.auto_resolved entry must carry its file's full Markdown 'body' (doc_import writes from the payload, it does not re-read the filesystem). Each document's slug is taken from its override's 'slug' if given, else its suggested_slug, disambiguated with a numeric suffix on collision. Persists every file as a document, applies proposed_tree parent/children relationships, links task_ids to every imported document (bidirectionally), and invalidates the doc corpus cache. Returns a JSON string {imported_count,documents:[{doc_id,slug,title,section_count}],warnings:[…]}.".to_string(),
1443
- input_schema: json!({
1444
- "type": "object",
1445
- "properties": {
1446
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1447
- "analyzed": { "type": "object", "description": "The handoff_doc_analyze report, with each auto_resolved entry additionally carrying its file's 'body'." },
1448
- "overrides": { "type": "array", "items": { "type": "object", "properties": { "file": { "type": "string" }, "slug": { "type": "string" }, "title": { "type": "string" }, "doc_type": { "type": "string" }, "tags": { "type": "array", "items": { "type": "string" } }, "scope_paths": { "type": "array", "items": { "type": "string" } } }, "required": ["file"] }, "description": "Per-file AI overrides applied on top of analyzed.auto_resolved before writing." },
1449
- "task_ids": { "type": "array", "items": { "type": "string" }, "description": "Link every imported document to these tasks (bidirectionally)." }
1450
- },
1451
- "required": ["analyzed"]
1452
- }),
1453
- },
1454
- ToolDefinition {
1455
- name: "handoff_task_checklist".to_string(),
1456
- description: "action=\"view\" (default): pure-view aggregation of a task's done_criteria and its linked documents' verification matrices. No new data is written — reads task_links (link_type=\"doc\") and each linked document's verification matrix, computed fresh on every call. Returns {task_id,title,no_linked_docs:true} as a fast-path response when the task has no linked documents. Otherwise returns {task_id,title,no_linked_docs:false,done_criteria:{items:[…],progress:{…}},verification_coverage:{documents:[{doc_id,slug,title,doc_type,items:[{fragment_seq,heading,status,stale,visual_state,impl_refs,test_refs}],progress:{…}}],overall:{…}},combined_readiness:{done_criteria_met,verification_complete,ready,blockers:[{type:\"criteria\"|\"verification\",…}]},suggested_actions:[…]}. Each item's visual_state is computed in priority order stale > skipped > verified > implemented (pending+impl_refs+test_refs) > in_progress (pending+impl_refs only) > untouched. action=\"generate\": turns a linked spec/design document's level-2 section headings into done_criteria items using hardcoded defaults (no config template) — format '[{doc_type}§{seq}] {heading}', seq=0 (preamble) plus any skip_seqs excluded. doc_id defaults to the first linked document with doc_type 'spec' or 'design' when omitted. mode=\"preview\" (default) returns the generated items without writing; \"append\" adds them to the task's existing done_criteria; \"replace\" overwrites done_criteria entirely — both writes go through the same optimistic-concurrency path as handoff_check_criterion. Returns {task_id,generated_criteria:[{item,fragment_seq}],applied,skipped_seqs,fixed_items}, where fixed_items is a doc_type-specific list of non-section checklist items (spec: 2 items; design: 1 item; other doc_types: []).".to_string(),
1457
- input_schema: json!({
1458
- "type": "object",
1459
- "properties": {
1460
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1461
- "task_id": { "type": "string", "description": "Task ID to build the checklist for (e.g. 't1', 't1.2')." },
1462
- "action": { "type": "string", "description": "Checklist action.", "enum": ["view", "generate"], "default": "view" },
1463
- "doc_id": { "type": "string", "description": "generate only: document id or slug to generate from. Defaults to the first task-linked document with doc_type 'spec' or 'design'." },
1464
- "mode": { "type": "string", "description": "generate only: 'preview' returns items without writing, 'append' adds to existing done_criteria, 'replace' overwrites done_criteria entirely.", "enum": ["preview", "append", "replace"], "default": "preview" },
1465
- "skip_seqs": { "type": "array", "items": { "type": "integer" }, "description": "generate only: additional section seqs to exclude, beyond the always-skipped seq=0 preamble." }
1466
- },
1467
- "required": ["task_id"]
1468
- }),
1469
- },
1470
- ]
1471
- }
1472
-
1473
- /// Shared input schema for add/update assignee. `key` is required either way.
1474
- fn assignee_write_schema(_is_add: bool) -> Value {
1475
- json!({
1476
- "type": "object",
1477
- "properties": {
1478
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1479
- "key": { "type": "string", "description": "Stable assignee key (used as [assignees.<key>])." },
1480
- "display_name": { "type": "string", "description": "Human-readable name." },
1481
- "color": { "type": "string", "description": "Display color (hex or name)." },
1482
- "work_hours_per_day": { "type": "number", "description": "This member's daily working hours." },
1483
- "closed_weekdays": { "type": "array", "description": "Non-working weekdays (0=Sun..6=Sat or names).", "items": {} },
1484
- "closed_dates": { "type": "array", "description": "Non-working YYYY-MM-DD dates.", "items": { "type": "string" } },
1485
- "open_dates": { "type": "array", "description": "Working YYYY-MM-DD override dates.", "items": { "type": "string" } },
1486
- "day_hours": { "type": "object", "description": "Per-weekday/date hour overrides.", "additionalProperties": { "type": "number" } }
1487
- },
1488
- "required": ["key"]
1489
- })
1490
- }
1491
-
1492
- /// Shared input schema for add/update milestone. `name` is required.
1493
- fn milestone_write_schema() -> Value {
1494
- json!({
1495
- "type": "object",
1496
- "properties": {
1497
- "project_dir": { "type": "string", "description": "Project directory path. Defaults to current working directory." },
1498
- "name": { "type": "string", "description": "Milestone name (used as [milestones.<name>])." },
1499
- "date": { "type": "string", "description": "Target date YYYY-MM-DD." },
1500
- "color": { "type": "string", "description": "Display color." },
1501
- "description": { "type": "string", "description": "Free-form description." }
1502
- },
1503
- "required": ["name"]
1504
- })
1505
- }
1506
-
1507
- pub fn all_resource_definitions() -> Vec<Value> {
1508
- vec![
1509
- json!({
1510
- "uri": "handoff://sessions",
1511
- "name": "Active Sessions",
1512
- "description": "All active session files for the current project",
1513
- "mimeType": "application/json"
1514
- }),
1515
- json!({
1516
- "uri": "handoff://config",
1517
- "name": "Project Configuration",
1518
- "description": "Current project's config.toml content",
1519
- "mimeType": "application/toml"
1520
- }),
1521
- ]
1522
- }