agent-tasks 1.7.1 → 1.9.1

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 (68) hide show
  1. package/README.md +17 -15
  2. package/dist/domain/agent-bridge.d.ts.map +1 -1
  3. package/dist/domain/agent-bridge.js +22 -2
  4. package/dist/domain/agent-bridge.js.map +1 -1
  5. package/dist/domain/approvals.d.ts.map +1 -1
  6. package/dist/domain/approvals.js +4 -1
  7. package/dist/domain/approvals.js.map +1 -1
  8. package/dist/domain/cleanup.d.ts +1 -0
  9. package/dist/domain/cleanup.d.ts.map +1 -1
  10. package/dist/domain/cleanup.js +36 -4
  11. package/dist/domain/cleanup.js.map +1 -1
  12. package/dist/domain/comments.d.ts +1 -0
  13. package/dist/domain/comments.d.ts.map +1 -1
  14. package/dist/domain/comments.js +10 -0
  15. package/dist/domain/comments.js.map +1 -1
  16. package/dist/domain/rules.js +11 -10
  17. package/dist/domain/rules.js.map +1 -1
  18. package/dist/domain/task-validator.d.ts +9 -0
  19. package/dist/domain/task-validator.d.ts.map +1 -0
  20. package/dist/domain/task-validator.js +70 -0
  21. package/dist/domain/task-validator.js.map +1 -0
  22. package/dist/domain/tasks.d.ts +19 -9
  23. package/dist/domain/tasks.d.ts.map +1 -1
  24. package/dist/domain/tasks.js +242 -111
  25. package/dist/domain/tasks.js.map +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/storage/database.d.ts.map +1 -1
  29. package/dist/storage/database.js +11 -3
  30. package/dist/storage/database.js.map +1 -1
  31. package/dist/transport/mcp-handlers.d.ts +31 -0
  32. package/dist/transport/mcp-handlers.d.ts.map +1 -0
  33. package/dist/transport/mcp-handlers.js +426 -0
  34. package/dist/transport/mcp-handlers.js.map +1 -0
  35. package/dist/transport/mcp.d.ts.map +1 -1
  36. package/dist/transport/mcp.js +207 -656
  37. package/dist/transport/mcp.js.map +1 -1
  38. package/dist/transport/rest.d.ts.map +1 -1
  39. package/dist/transport/rest.js +23 -7
  40. package/dist/transport/rest.js.map +1 -1
  41. package/dist/transport/ws.d.ts.map +1 -1
  42. package/dist/transport/ws.js +6 -4
  43. package/dist/transport/ws.js.map +1 -1
  44. package/dist/ui/app.js +186 -1608
  45. package/dist/ui/board.js +401 -0
  46. package/dist/ui/drag.js +143 -0
  47. package/dist/ui/index.html +5 -0
  48. package/dist/ui/inline-edit.js +242 -0
  49. package/dist/ui/panel.js +574 -0
  50. package/dist/ui/styles.css +109 -0
  51. package/dist/ui/ui-utils.js +323 -0
  52. package/package.json +1 -1
  53. package/dist/db.d.ts +0 -10
  54. package/dist/db.d.ts.map +0 -1
  55. package/dist/db.js +0 -112
  56. package/dist/db.js.map +0 -1
  57. package/dist/event-bus.d.ts +0 -10
  58. package/dist/event-bus.d.ts.map +0 -1
  59. package/dist/event-bus.js +0 -38
  60. package/dist/event-bus.js.map +0 -1
  61. package/dist/session.d.ts +0 -7
  62. package/dist/session.d.ts.map +0 -1
  63. package/dist/session.js +0 -11
  64. package/dist/session.js.map +0 -1
  65. package/dist/tasks.d.ts +0 -32
  66. package/dist/tasks.d.ts.map +0 -1
  67. package/dist/tasks.js +0 -410
  68. package/dist/tasks.js.map +0 -1
@@ -4,61 +4,90 @@
4
4
  // Maps MCP tool calls to the TaskService. Thin adapter — validation lives
5
5
  // in the domain layer, not here.
6
6
  // =============================================================================
7
- import { writeFileSync, mkdirSync } from 'fs';
8
- import { join } from 'path';
9
- import { homedir } from 'os';
10
7
  import { ValidationError } from '../types.js';
11
- import { generateRules } from '../domain/rules.js';
8
+ import { handlers } from './mcp-handlers.js';
12
9
  // ---------------------------------------------------------------------------
13
- // Tool definitions
10
+ // Tool definitions (13 tools)
14
11
  // ---------------------------------------------------------------------------
15
12
  export const tools = [
16
13
  {
17
14
  name: 'task_create',
18
- description: 'Create a pipeline task with optional stage, priority, and project.',
15
+ description: 'Create a pipeline task. Tasks start in "backlog" and move through stages: backlog → spec → plan → implement → test → review → done. Use parent_id to create subtasks under an existing task.',
19
16
  inputSchema: {
20
17
  type: 'object',
21
18
  properties: {
22
19
  title: { type: 'string', description: 'Task title (max 500 chars)' },
23
20
  description: { type: 'string', description: 'Detailed instructions (max 50K chars)' },
24
21
  assign_to: { type: 'string', description: 'Agent name to assign to' },
25
- stage: { type: 'string', description: 'Pipeline stage (default: backlog)' },
22
+ stage: {
23
+ type: 'string',
24
+ description: 'Initial pipeline stage (default: backlog)',
25
+ },
26
26
  priority: {
27
27
  type: 'number',
28
- description: 'Priority (higher = more important, default: 0)',
28
+ description: 'Priority higher number = more important (default: 0)',
29
29
  },
30
30
  project: { type: 'string', description: 'Project name for grouping' },
31
31
  tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization' },
32
- parent_id: { type: 'number', description: 'Parent task ID (creates a subtask)' },
32
+ parent_id: {
33
+ type: 'number',
34
+ description: 'Parent task ID — creates a subtask that inherits project and priority',
35
+ },
33
36
  },
34
37
  required: ['title'],
35
38
  },
36
39
  },
40
+ {
41
+ name: 'task_get',
42
+ description: 'Get a single task by ID with full details including artifacts count, comments count, dependencies, and collaborators.',
43
+ inputSchema: {
44
+ type: 'object',
45
+ properties: {
46
+ task_id: { type: 'number', description: 'Task ID to retrieve' },
47
+ },
48
+ required: ['task_id'],
49
+ },
50
+ },
37
51
  {
38
52
  name: 'task_list',
39
- description: 'List tasks with optional filters and pagination.',
53
+ description: 'List, search, or pick tasks. Without params: returns all tasks. With filters: narrow by status/stage/project/assignee. With "query": full-text search across titles and descriptions. With "next": true: returns the single highest-priority unassigned task with all dependencies met (uses affinity scoring when agent is provided).',
40
54
  inputSchema: {
41
55
  type: 'object',
42
56
  properties: {
57
+ query: {
58
+ type: 'string',
59
+ description: 'Full-text search across task titles and descriptions (FTS5)',
60
+ },
61
+ next: {
62
+ type: 'boolean',
63
+ description: 'Return the single best available task — highest priority, unassigned, all dependencies met. Uses affinity scoring when "agent" is provided.',
64
+ },
65
+ agent: {
66
+ type: 'string',
67
+ description: 'Agent name for affinity scoring (only with next: true) — prefers tasks where the agent worked on the parent, a dependency, or the same project',
68
+ },
43
69
  status: {
44
70
  type: 'string',
45
71
  enum: ['pending', 'in_progress', 'completed', 'failed', 'cancelled'],
46
72
  description: 'Filter by status',
47
73
  },
48
- assigned_to: { type: 'string', description: 'Filter by assigned agent name' },
74
+ assign_to: { type: 'string', description: 'Filter by assigned agent name' },
49
75
  stage: { type: 'string', description: 'Filter by pipeline stage' },
50
76
  project: { type: 'string', description: 'Filter by project' },
51
77
  collaborator: { type: 'string', description: 'Filter tasks where agent is a collaborator' },
52
78
  root_only: { type: 'boolean', description: 'Only show top-level tasks (no subtasks)' },
53
79
  parent_id: { type: 'number', description: 'Filter subtasks of a specific parent' },
54
- limit: { type: 'number', description: 'Max results (default/max: 500)' },
80
+ limit: {
81
+ type: 'number',
82
+ description: 'Max results (default: 500 for list, 50 for search)',
83
+ },
55
84
  offset: { type: 'number', description: 'Skip first N results (for pagination)' },
56
85
  },
57
86
  },
58
87
  },
59
88
  {
60
89
  name: 'task_claim',
61
- description: 'Claim a pending task. Assigns it and advances from backlog to the next stage.',
90
+ description: 'Claim a pending task assigns it to you and advances from backlog to spec. This is the standard way to start working on a task.',
62
91
  inputSchema: {
63
92
  type: 'object',
64
93
  properties: {
@@ -72,189 +101,170 @@ export const tools = [
72
101
  },
73
102
  },
74
103
  {
75
- name: 'task_complete',
76
- description: 'Mark a task as completed with a result.',
104
+ name: 'task_update',
105
+ description: 'Update task metadata title, description, priority, tags, project, or assignment. Does not change stage or status (use task_stage for that).',
77
106
  inputSchema: {
78
107
  type: 'object',
79
108
  properties: {
80
109
  task_id: { type: 'number', description: 'Task ID' },
81
- result: { type: 'string', description: 'Result or output' },
110
+ title: { type: 'string', description: 'New title' },
111
+ description: { type: 'string', description: 'New description' },
112
+ priority: { type: 'number', description: 'New priority' },
113
+ project: { type: 'string', description: 'New project' },
114
+ tags: { type: 'array', items: { type: 'string' }, description: 'New tags' },
115
+ assign_to: {
116
+ type: 'string',
117
+ description: 'New assignee (empty string to unassign)',
118
+ },
82
119
  },
83
- required: ['task_id', 'result'],
120
+ required: ['task_id'],
84
121
  },
85
122
  },
86
123
  {
87
- name: 'task_fail',
88
- description: 'Mark a task as failed with an error message.',
124
+ name: 'task_delete',
125
+ description: 'Delete a task and all its artifacts, comments, and dependencies (cascading delete). Cannot be undone.',
89
126
  inputSchema: {
90
127
  type: 'object',
91
128
  properties: {
92
- task_id: { type: 'number', description: 'Task ID' },
93
- result: { type: 'string', description: 'Error description' },
129
+ task_id: { type: 'number', description: 'Task ID to delete' },
94
130
  },
95
- required: ['task_id', 'result'],
131
+ required: ['task_id'],
96
132
  },
97
133
  },
98
134
  {
99
- name: 'task_cancel',
100
- description: 'Cancel a task — moves it to the cancelled stage.',
135
+ name: 'task_comment',
136
+ description: 'Add a comment to a task — used for async discussion between agents. Supports threading via parent_comment_id. Comments also satisfy stage-gate "require_comment" checks.',
101
137
  inputSchema: {
102
138
  type: 'object',
103
139
  properties: {
104
140
  task_id: { type: 'number', description: 'Task ID' },
105
- reason: { type: 'string', description: 'Why the task was cancelled' },
141
+ content: { type: 'string', description: 'Comment text' },
142
+ parent_comment_id: { type: 'number', description: 'Reply to this comment (threading)' },
106
143
  },
107
- required: ['task_id', 'reason'],
144
+ required: ['task_id', 'content'],
108
145
  },
109
146
  },
110
147
  {
111
- name: 'task_advance',
112
- description: 'Advance a task to the next pipeline stage (or a specific stage). Validates dependencies and stage gates. Optionally attach a comment in the same call.',
148
+ name: 'task_stage',
149
+ description: 'Move a task through its lifecycle. Actions: "advance" → next stage (or jump to a specific one), "regress" earlier stage (requires reason), "complete" marks done with result, "fail" → marks failed with error, "cancel" → cancels with reason.',
113
150
  inputSchema: {
114
151
  type: 'object',
115
152
  properties: {
153
+ action: {
154
+ type: 'string',
155
+ enum: ['advance', 'regress', 'complete', 'fail', 'cancel'],
156
+ description: 'Lifecycle action to perform',
157
+ },
116
158
  task_id: { type: 'number', description: 'Task ID' },
117
- stage: { type: 'string', description: 'Target stage (omit to advance to next stage)' },
159
+ stage: {
160
+ type: 'string',
161
+ description: 'Target stage (advance: optional — advances to next if omitted; regress: required)',
162
+ },
118
163
  comment: {
119
164
  type: 'string',
120
- description: 'Optional comment to attach (also satisfies stage-gate require_comment check)',
165
+ description: 'Comment (advance: optional, also satisfies stage-gate require_comment check)',
121
166
  },
122
- },
123
- required: ['task_id'],
124
- },
125
- },
126
- {
127
- name: 'task_regress',
128
- description: 'Send a task back to an earlier stage (e.g. review rejection).',
129
- inputSchema: {
130
- type: 'object',
131
- properties: {
132
- task_id: { type: 'number', description: 'Task ID' },
133
- stage: { type: 'string', description: 'Target stage to regress to' },
134
167
  reason: {
135
168
  type: 'string',
136
- description: 'Reason for regression (stored as artifact)',
169
+ description: 'Reason for regression, failure, or cancellation',
137
170
  },
138
- },
139
- required: ['task_id', 'stage'],
140
- },
141
- },
142
- {
143
- name: 'task_update',
144
- description: 'Update task metadata (title, description, priority, tags, assignment).',
145
- inputSchema: {
146
- type: 'object',
147
- properties: {
148
- task_id: { type: 'number', description: 'Task ID' },
149
- title: { type: 'string', description: 'New title' },
150
- description: { type: 'string', description: 'New description' },
151
- priority: { type: 'number', description: 'New priority' },
152
- project: { type: 'string', description: 'New project' },
153
- tags: { type: 'array', items: { type: 'string' }, description: 'New tags' },
154
- assign_to: {
171
+ result: {
155
172
  type: 'string',
156
- description: 'New assignee (empty string to unassign)',
173
+ description: 'Result summary (complete) or error description (fail)',
157
174
  },
158
175
  },
159
- required: ['task_id'],
176
+ required: ['action', 'task_id'],
160
177
  },
161
178
  },
162
179
  {
163
- name: 'task_next',
164
- description: 'Get the highest-priority unassigned task with all dependencies met. Returns null if none.',
180
+ name: 'task_query',
181
+ description: 'Read task-related data. Types: "subtasks" child tasks, "artifacts" attached documents/decisions/learnings (filterable by stage), "comments" → discussion threads.',
165
182
  inputSchema: {
166
183
  type: 'object',
167
184
  properties: {
168
- project: { type: 'string', description: 'Filter by project' },
169
- stage: { type: 'string', description: 'Filter by stage' },
170
- },
171
- },
172
- },
173
- {
174
- name: 'task_add_dependency',
175
- description: 'Add a relationship between tasks. "blocks" prevents advancement until dependency is done. "related" and "duplicate" are informational only.',
176
- inputSchema: {
177
- type: 'object',
178
- properties: {
179
- task_id: { type: 'number', description: 'Task that depends on another' },
180
- depends_on: {
181
- type: 'number',
182
- description: 'Task that must complete first (for blocks) or related task',
183
- },
184
- relationship: {
185
+ type: {
185
186
  type: 'string',
186
- enum: ['blocks', 'related', 'duplicate'],
187
- description: 'Relationship type (default: blocks)',
187
+ enum: ['subtasks', 'artifacts', 'comments'],
188
+ description: 'What to query',
188
189
  },
189
- },
190
- required: ['task_id', 'depends_on'],
191
- },
192
- },
193
- {
194
- name: 'task_remove_dependency',
195
- description: 'Remove a dependency between tasks.',
196
- inputSchema: {
197
- type: 'object',
198
- properties: {
199
190
  task_id: { type: 'number', description: 'Task ID' },
200
- depends_on: { type: 'number', description: 'Dependency to remove' },
191
+ stage: {
192
+ type: 'string',
193
+ description: 'Filter artifacts by stage (only used with type: "artifacts")',
194
+ },
195
+ limit: {
196
+ type: 'number',
197
+ description: 'Max comments to return (only used with type: "comments", default: 100)',
198
+ },
201
199
  },
202
- required: ['task_id', 'depends_on'],
200
+ required: ['type', 'task_id'],
203
201
  },
204
202
  },
205
203
  {
206
- name: 'task_add_artifact',
207
- description: 'Attach a document/artifact to a task (spec, plan, test results, review notes, etc.).',
204
+ name: 'task_artifact',
205
+ description: 'Attach artifacts to a task. Types: "general" → document (spec, test-results, review-notes), "decision" → structured decision record (chose/over/because), "learning" → insight that auto-propagates to parent and sibling tasks on completion.',
208
206
  inputSchema: {
209
207
  type: 'object',
210
208
  properties: {
209
+ type: {
210
+ type: 'string',
211
+ enum: ['general', 'decision', 'learning'],
212
+ description: 'Artifact type',
213
+ },
211
214
  task_id: { type: 'number', description: 'Task ID' },
212
215
  name: {
213
216
  type: 'string',
214
- description: 'Artifact name (e.g. "spec", "test-results", "review-notes")',
217
+ description: 'Artifact name (type: "general" only, e.g. "spec", "test-results", "review-notes")',
215
218
  },
216
219
  content: {
217
220
  type: 'string',
218
- description: 'Artifact content (text, markdown, JSON, max 100K)',
221
+ description: 'Artifact content (type: "general": text/markdown/JSON, max 100K; type: "learning": the insight)',
219
222
  },
220
223
  stage: {
221
224
  type: 'string',
222
- description: 'Stage to attach to (defaults to task current stage)',
225
+ description: 'Stage to attach to (type: "general" only, defaults to current stage)',
226
+ },
227
+ chose: { type: 'string', description: 'What was chosen (type: "decision" only)' },
228
+ over: {
229
+ type: 'string',
230
+ description: 'What alternatives were considered (type: "decision" only)',
231
+ },
232
+ because: {
233
+ type: 'string',
234
+ description: 'Rationale for the decision (type: "decision" only)',
235
+ },
236
+ category: {
237
+ type: 'string',
238
+ enum: ['technique', 'pitfall', 'decision', 'pattern'],
239
+ description: 'Learning category (type: "learning" only, default: technique)',
223
240
  },
224
241
  },
225
- required: ['task_id', 'name', 'content'],
226
- },
227
- },
228
- {
229
- name: 'task_get_artifacts',
230
- description: 'Get artifacts attached to a task.',
231
- inputSchema: {
232
- type: 'object',
233
- properties: {
234
- task_id: { type: 'number', description: 'Task ID' },
235
- stage: { type: 'string', description: 'Filter by stage' },
236
- },
237
- required: ['task_id'],
242
+ required: ['type', 'task_id'],
238
243
  },
239
244
  },
240
245
  {
241
- name: 'task_pipeline_config',
242
- description: 'Get or set pipeline stages and gate config for a project. Call without stages/gate_config to get current config.',
246
+ name: 'task_config',
247
+ description: 'Configuration and admin. Actions: "session" → set agent identity (call this first), "pipeline" → get/set pipeline stages and gate config for a project, "cleanup" purge old completed tasks, "rules" → generate IDE rule files (.mdc or CLAUDE.md).',
243
248
  inputSchema: {
244
249
  type: 'object',
245
250
  properties: {
251
+ action: {
252
+ type: 'string',
253
+ enum: ['pipeline', 'session', 'cleanup', 'rules'],
254
+ description: 'Config action to perform',
255
+ },
246
256
  project: {
247
257
  type: 'string',
248
- description: 'Project name (omit for default pipeline)',
258
+ description: 'Project name (pipeline: scope, rules: project-specific rules)',
249
259
  },
250
260
  stages: {
251
261
  type: 'array',
252
262
  items: { type: 'string' },
253
- description: 'Stage names in order (set mode)',
263
+ description: 'Stage names in order (pipeline set mode)',
254
264
  },
255
265
  gate_config: {
256
266
  type: 'object',
257
- description: 'Stage-gate enforcement config. Example: { "require_comment": true, "require_artifact": false, "exempt_stages": ["backlog"] }',
267
+ description: 'Stage-gate enforcement config (pipeline only). Example: { "require_comment": true, "require_artifact": false, "exempt_stages": ["backlog"] }',
258
268
  properties: {
259
269
  require_comment: {
260
270
  type: 'boolean',
@@ -297,588 +307,129 @@ export const tools = [
297
307
  },
298
308
  },
299
309
  },
310
+ id: { type: 'string', description: 'Session ID (session only)' },
311
+ name: { type: 'string', description: 'Session name (session only)' },
312
+ mode: {
313
+ type: 'string',
314
+ enum: ['retention', 'stale_agents', 'all'],
315
+ description: 'Cleanup mode (cleanup only, default: retention)',
316
+ },
317
+ timeout_minutes: {
318
+ type: 'number',
319
+ description: 'Heartbeat timeout in minutes for stale agent detection (cleanup only, default: 30)',
320
+ },
321
+ format: {
322
+ type: 'string',
323
+ enum: ['mdc', 'claude_md'],
324
+ description: 'Output format for rules: mdc (Cursor) or claude_md (Claude Code)',
325
+ },
300
326
  },
327
+ required: ['action'],
301
328
  },
302
329
  },
303
330
  {
304
- name: 'task_set_session',
305
- description: 'Set the session identity for this connection (used to track who creates/claims tasks).',
306
- inputSchema: {
307
- type: 'object',
308
- properties: {
309
- id: { type: 'string', description: 'Session ID' },
310
- name: { type: 'string', description: 'Session name' },
311
- },
312
- required: ['id', 'name'],
313
- },
314
- },
315
- {
316
- name: 'task_delete',
317
- description: 'Delete a task and all its artifacts and dependencies (cascading delete).',
318
- inputSchema: {
319
- type: 'object',
320
- properties: {
321
- task_id: { type: 'number', description: 'Task ID to delete' },
322
- },
323
- required: ['task_id'],
324
- },
325
- },
326
- {
327
- name: 'task_comment',
328
- description: 'Add a comment to a task for async discussion between agents.',
329
- inputSchema: {
330
- type: 'object',
331
- properties: {
332
- task_id: { type: 'number', description: 'Task ID' },
333
- content: { type: 'string', description: 'Comment text' },
334
- parent_comment_id: { type: 'number', description: 'Reply to this comment (threading)' },
335
- },
336
- required: ['task_id', 'content'],
337
- },
338
- },
339
- {
340
- name: 'task_get_comments',
341
- description: 'Get comments on a task.',
331
+ name: 'task_dependency',
332
+ description: 'Manage task dependencies. "add" create a blocks/related/duplicate relationship between two tasks (blocks advancement until resolved). "remove" → delete a dependency.',
342
333
  inputSchema: {
343
334
  type: 'object',
344
335
  properties: {
345
- task_id: { type: 'number', description: 'Task ID' },
346
- limit: { type: 'number', description: 'Max comments (default: 100)' },
336
+ action: {
337
+ type: 'string',
338
+ enum: ['add', 'remove'],
339
+ description: 'Action to perform',
340
+ },
341
+ task_id: { type: 'number', description: 'Task that depends on another' },
342
+ depends_on: {
343
+ type: 'number',
344
+ description: 'Task that must complete first (for blocks) or related task',
345
+ },
346
+ relationship: {
347
+ type: 'string',
348
+ enum: ['blocks', 'related', 'duplicate'],
349
+ description: 'Relationship type (default: blocks, only used with "add")',
350
+ },
347
351
  },
348
- required: ['task_id'],
352
+ required: ['action', 'task_id', 'depends_on'],
349
353
  },
350
354
  },
351
355
  {
352
- name: 'task_add_collaborator',
353
- description: 'Add an agent as collaborator on a task (roles: collaborator, reviewer, watcher).',
356
+ name: 'task_collaborator',
357
+ description: 'Manage task collaborators. "add" → assign an agent as collaborator (can work on it), reviewer (reviews artifacts), or watcher (gets notifications). "remove" → unassign an agent.',
354
358
  inputSchema: {
355
359
  type: 'object',
356
360
  properties: {
361
+ action: {
362
+ type: 'string',
363
+ enum: ['add', 'remove'],
364
+ description: 'Action to perform',
365
+ },
357
366
  task_id: { type: 'number', description: 'Task ID' },
358
367
  agent_id: { type: 'string', description: 'Agent name or ID' },
359
368
  role: {
360
369
  type: 'string',
361
370
  enum: ['collaborator', 'reviewer', 'watcher'],
362
- description: 'Role (default: collaborator)',
371
+ description: 'Role (default: collaborator, only used with "add")',
363
372
  },
364
373
  },
365
- required: ['task_id', 'agent_id'],
366
- },
367
- },
368
- {
369
- name: 'task_remove_collaborator',
370
- description: 'Remove a collaborator from a task.',
371
- inputSchema: {
372
- type: 'object',
373
- properties: {
374
- task_id: { type: 'number', description: 'Task ID' },
375
- agent_id: { type: 'string', description: 'Agent name or ID' },
376
- },
377
- required: ['task_id', 'agent_id'],
378
- },
379
- },
380
- {
381
- name: 'task_search',
382
- description: 'Full-text search across task titles and descriptions.',
383
- inputSchema: {
384
- type: 'object',
385
- properties: {
386
- query: { type: 'string', description: 'Search query' },
387
- project: { type: 'string', description: 'Filter by project' },
388
- limit: { type: 'number', description: 'Max results (default: 50)' },
389
- },
390
- required: ['query'],
391
- },
392
- },
393
- {
394
- name: 'task_get_subtasks',
395
- description: 'Get subtasks of a parent task.',
396
- inputSchema: {
397
- type: 'object',
398
- properties: {
399
- task_id: { type: 'number', description: 'Parent task ID' },
400
- },
401
- required: ['task_id'],
402
- },
403
- },
404
- {
405
- name: 'task_request_approval',
406
- description: 'Request approval for a task at a specific stage.',
407
- inputSchema: {
408
- type: 'object',
409
- properties: {
410
- task_id: { type: 'number', description: 'Task ID' },
411
- stage: { type: 'string', description: 'Stage requiring approval (defaults to current)' },
412
- reviewer: { type: 'string', description: 'Specific reviewer to assign' },
413
- },
414
- required: ['task_id'],
415
- },
416
- },
417
- {
418
- name: 'task_approve',
419
- description: 'Approve a pending approval request.',
420
- inputSchema: {
421
- type: 'object',
422
- properties: {
423
- approval_id: { type: 'number', description: 'Approval ID' },
424
- comment: { type: 'string', description: 'Approval comment' },
425
- },
426
- required: ['approval_id'],
427
- },
428
- },
429
- {
430
- name: 'task_reject',
431
- description: 'Reject a pending approval and optionally regress the task.',
432
- inputSchema: {
433
- type: 'object',
434
- properties: {
435
- approval_id: { type: 'number', description: 'Approval ID' },
436
- comment: { type: 'string', description: 'Rejection reason (required)' },
437
- regress_to: { type: 'string', description: 'Stage to regress task to' },
438
- },
439
- required: ['approval_id', 'comment'],
440
- },
441
- },
442
- {
443
- name: 'task_pending_approvals',
444
- description: 'List pending approval requests, optionally filtered by reviewer.',
445
- inputSchema: {
446
- type: 'object',
447
- properties: {
448
- reviewer: { type: 'string', description: 'Filter by reviewer' },
449
- },
374
+ required: ['action', 'task_id', 'agent_id'],
450
375
  },
451
376
  },
452
377
  {
453
- name: 'task_review_cycle',
454
- description: 'Review a task: approve (advance to next stage) or reject (regress with reason). Convenience wrapper for the maker-checker pattern.',
378
+ name: 'task_approval',
379
+ description: 'Manage approval workflows for stage gates. Actions: "request" create approval request at current stage, "approve"/"reject" decide on a pending approval, "list" show pending approvals, "review" → convenience action that approves+advances or rejects+regresses in one call.',
455
380
  inputSchema: {
456
381
  type: 'object',
457
382
  properties: {
458
- task_id: { type: 'number', description: 'Task ID to review' },
459
383
  action: {
460
384
  type: 'string',
461
- enum: ['approve', 'reject'],
462
- description: 'Approve or reject',
385
+ enum: ['request', 'approve', 'reject', 'list', 'review'],
386
+ description: 'Action to perform',
463
387
  },
464
- reason: { type: 'string', description: 'Rejection reason (required for reject)' },
465
- regress_to: {
388
+ task_id: {
389
+ type: 'number',
390
+ description: 'Task ID (required for request and review)',
391
+ },
392
+ approval_id: {
393
+ type: 'number',
394
+ description: 'Approval ID (required for approve and reject)',
395
+ },
396
+ stage: {
466
397
  type: 'string',
467
- description: 'Stage to regress to on rejection (default: implement)',
398
+ description: 'Stage requiring approval (request only, defaults to current)',
468
399
  },
469
- },
470
- required: ['task_id', 'action'],
471
- },
472
- },
473
- {
474
- name: 'task_expand',
475
- description: 'Break a task into subtasks. Creates subtasks with parent_id pointing to the given task, inheriting project and priority.',
476
- inputSchema: {
477
- type: 'object',
478
- properties: {
479
- task_id: { type: 'number', description: 'Parent task ID to expand' },
480
- subtasks: {
481
- type: 'array',
482
- items: {
483
- type: 'object',
484
- properties: {
485
- title: { type: 'string', description: 'Subtask title' },
486
- description: { type: 'string', description: 'Subtask description' },
487
- priority: {
488
- type: 'number',
489
- description: 'Priority override (inherits from parent if omitted)',
490
- },
491
- },
492
- required: ['title'],
493
- },
494
- description: 'Array of subtasks to create',
400
+ reviewer: {
401
+ type: 'string',
402
+ description: 'Reviewer to assign (request) or filter by (list)',
495
403
  },
496
- },
497
- required: ['task_id', 'subtasks'],
498
- },
499
- },
500
- {
501
- name: 'task_cleanup',
502
- description: 'Run data cleanup. Modes: "retention" (default) purges old completed/cancelled tasks, "stale_agents" checks agent heartbeats and fails tasks from dead agents, "all" runs both.',
503
- inputSchema: {
504
- type: 'object',
505
- properties: {
506
- mode: {
404
+ comment: {
507
405
  type: 'string',
508
- enum: ['retention', 'stale_agents', 'all'],
509
- description: 'Cleanup mode (default: retention)',
406
+ description: 'Comment (optional for approve, required for reject)',
510
407
  },
511
- timeout_minutes: {
512
- type: 'number',
513
- description: 'Heartbeat timeout in minutes for stale agent detection (default: 30, only used with stale_agents mode)',
408
+ decision: {
409
+ type: 'string',
410
+ enum: ['approve', 'reject'],
411
+ description: 'Decision for review action',
514
412
  },
515
- },
516
- },
517
- },
518
- {
519
- name: 'task_generate_rules',
520
- description: 'Generate IDE-specific rule files that instruct agents to use the pipeline. Supports Cursor (.mdc) and Claude Code (CLAUDE.md) formats.',
521
- inputSchema: {
522
- type: 'object',
523
- properties: {
524
- format: {
413
+ reason: {
525
414
  type: 'string',
526
- enum: ['mdc', 'claude_md'],
527
- description: 'Output format: mdc (Cursor) or claude_md (Claude Code)',
415
+ description: 'Rejection reason (required for review+reject)',
416
+ },
417
+ regress_to: {
418
+ type: 'string',
419
+ description: 'Stage to regress to on rejection (reject/review, default: implement)',
528
420
  },
529
- project: { type: 'string', description: 'Project name for project-specific rules' },
530
- },
531
- required: ['format'],
532
- },
533
- },
534
- {
535
- name: 'task_decision',
536
- description: 'Record an architectural or design decision as a structured artifact on a task. Stored at the current stage.',
537
- inputSchema: {
538
- type: 'object',
539
- properties: {
540
- task_id: { type: 'number', description: 'Task ID to record the decision on' },
541
- chose: { type: 'string', description: 'What was chosen' },
542
- over: { type: 'string', description: 'What alternatives were considered' },
543
- because: { type: 'string', description: 'Rationale for the decision' },
544
421
  },
545
- required: ['task_id', 'chose', 'over', 'because'],
422
+ required: ['action'],
546
423
  },
547
424
  },
548
425
  ];
549
- // ---------------------------------------------------------------------------
550
- // Input validation helpers
551
- // ---------------------------------------------------------------------------
552
- function requireString(args, key) {
553
- const val = args[key];
554
- if (typeof val !== 'string' || !val.trim()) {
555
- throw new ValidationError(`"${key}" must be a non-empty string.`);
556
- }
557
- return val;
558
- }
559
- function optString(args, key) {
560
- const val = args[key];
561
- if (val === undefined || val === null)
562
- return undefined;
563
- if (typeof val !== 'string')
564
- throw new ValidationError(`"${key}" must be a string.`);
565
- return val;
566
- }
567
- function requireNumber(args, key) {
568
- const val = args[key];
569
- if (typeof val !== 'number') {
570
- throw new ValidationError(`"${key}" is required and must be a number.`);
571
- }
572
- return val;
573
- }
574
- function optNumber(args, key) {
575
- const val = args[key];
576
- if (val === undefined || val === null)
577
- return undefined;
578
- if (typeof val !== 'number')
579
- throw new ValidationError(`"${key}" must be a number.`);
580
- return val;
581
- }
582
- function optBoolean(args, key) {
583
- const val = args[key];
584
- if (val === undefined || val === null)
585
- return undefined;
586
- if (typeof val !== 'boolean')
587
- throw new ValidationError(`"${key}" must be a boolean.`);
588
- return val;
589
- }
590
- function optStringArray(args, key) {
591
- const val = args[key];
592
- if (val === undefined || val === null)
593
- return undefined;
594
- if (!Array.isArray(val) || !val.every((v) => typeof v === 'string')) {
595
- throw new ValidationError(`"${key}" must be an array of strings.`);
596
- }
597
- return val;
598
- }
599
426
  export function createToolHandler(ctx) {
600
- let currentSession = null;
601
- function sessionName() {
602
- return currentSession?.name ?? 'system';
603
- }
427
+ const session = { current: null };
604
428
  return function handleTool(name, args) {
605
- switch (name) {
606
- case 'task_set_session': {
607
- const id = requireString(args, 'id');
608
- const sName = requireString(args, 'name');
609
- currentSession = { id, name: sName };
610
- writeSessionFile(id, sName);
611
- return { success: true, id, name: sName };
612
- }
613
- case 'task_create': {
614
- return ctx.tasks.create({
615
- title: requireString(args, 'title'),
616
- description: optString(args, 'description'),
617
- assign_to: optString(args, 'assign_to'),
618
- stage: optString(args, 'stage'),
619
- priority: optNumber(args, 'priority'),
620
- project: optString(args, 'project'),
621
- tags: optStringArray(args, 'tags'),
622
- parent_id: optNumber(args, 'parent_id'),
623
- }, sessionName());
624
- }
625
- case 'task_list': {
626
- return ctx.tasks.list({
627
- status: optString(args, 'status'),
628
- assigned_to: optString(args, 'assigned_to'),
629
- stage: optString(args, 'stage'),
630
- project: optString(args, 'project'),
631
- parent_id: optNumber(args, 'parent_id'),
632
- root_only: optBoolean(args, 'root_only'),
633
- collaborator: optString(args, 'collaborator'),
634
- limit: optNumber(args, 'limit'),
635
- offset: optNumber(args, 'offset'),
636
- });
637
- }
638
- case 'task_claim': {
639
- const claimer = optString(args, 'claimer') ?? sessionName();
640
- return ctx.tasks.claim(requireNumber(args, 'task_id'), claimer);
641
- }
642
- case 'task_complete':
643
- return ctx.tasks.complete(requireNumber(args, 'task_id'), requireString(args, 'result'));
644
- case 'task_fail':
645
- return ctx.tasks.fail(requireNumber(args, 'task_id'), requireString(args, 'result'));
646
- case 'task_cancel':
647
- return ctx.tasks.cancel(requireNumber(args, 'task_id'), requireString(args, 'reason'));
648
- case 'task_advance': {
649
- const advanceTaskId = requireNumber(args, 'task_id');
650
- const advanceComment = optString(args, 'comment');
651
- const advanced = ctx.tasks.advance(advanceTaskId, optString(args, 'stage'), advanceComment);
652
- if (advanceComment) {
653
- ctx.comments.add(advanceTaskId, sessionName(), advanceComment);
654
- }
655
- return advanced;
656
- }
657
- case 'task_regress':
658
- return ctx.tasks.regress(requireNumber(args, 'task_id'), requireString(args, 'stage'), optString(args, 'reason'));
659
- case 'task_update':
660
- return ctx.tasks.update(requireNumber(args, 'task_id'), {
661
- title: optString(args, 'title'),
662
- description: optString(args, 'description'),
663
- priority: optNumber(args, 'priority'),
664
- project: optString(args, 'project'),
665
- tags: optStringArray(args, 'tags'),
666
- assigned_to: optString(args, 'assign_to'),
667
- });
668
- case 'task_next': {
669
- return (ctx.tasks.next(optString(args, 'project'), optString(args, 'stage')) ?? {
670
- message: 'No available tasks.',
671
- });
672
- }
673
- case 'task_add_dependency': {
674
- const relationship = (optString(args, 'relationship') ?? 'blocks');
675
- ctx.tasks.addDependency(requireNumber(args, 'task_id'), requireNumber(args, 'depends_on'), relationship);
676
- return { success: true, task_id: args.task_id, depends_on: args.depends_on, relationship };
677
- }
678
- case 'task_remove_dependency': {
679
- ctx.tasks.removeDependency(requireNumber(args, 'task_id'), requireNumber(args, 'depends_on'));
680
- return { success: true };
681
- }
682
- case 'task_add_artifact': {
683
- return ctx.tasks.addArtifact(requireNumber(args, 'task_id'), requireString(args, 'name'), requireString(args, 'content'), sessionName(), optString(args, 'stage'));
684
- }
685
- case 'task_get_artifacts':
686
- return ctx.tasks.getArtifacts(requireNumber(args, 'task_id'), optString(args, 'stage'));
687
- case 'task_pipeline_config': {
688
- const stages = optStringArray(args, 'stages');
689
- const gateConfig = args.gate_config;
690
- const project = optString(args, 'project') || 'default';
691
- if (stages) {
692
- ctx.tasks.setPipelineConfig(project, stages);
693
- }
694
- if (gateConfig && typeof gateConfig === 'object') {
695
- const parsedGates = {};
696
- if (gateConfig.gates && typeof gateConfig.gates === 'object') {
697
- for (const [stageName, stageRule] of Object.entries(gateConfig.gates)) {
698
- parsedGates[stageName] = {
699
- require_artifacts: Array.isArray(stageRule.require_artifacts)
700
- ? stageRule.require_artifacts
701
- : undefined,
702
- require_min_artifacts: typeof stageRule.require_min_artifacts === 'number'
703
- ? stageRule.require_min_artifacts
704
- : undefined,
705
- require_comment: stageRule.require_comment === true ? true : undefined,
706
- require_approval: stageRule.require_approval === true ? true : undefined,
707
- };
708
- }
709
- }
710
- ctx.tasks.setGateConfig(project, {
711
- require_comment: gateConfig.require_comment === true,
712
- require_artifact: gateConfig.require_artifact === true,
713
- exempt_stages: Array.isArray(gateConfig.exempt_stages)
714
- ? gateConfig.exempt_stages
715
- : undefined,
716
- gates: Object.keys(parsedGates).length > 0 ? parsedGates : undefined,
717
- });
718
- }
719
- if (stages || gateConfig) {
720
- const config = ctx.tasks.getGateConfig(project);
721
- return {
722
- stages: ctx.tasks.getPipelineStages(project),
723
- gate_config: config ?? { require_comment: false },
724
- };
725
- }
726
- return {
727
- stages: ctx.tasks.getPipelineStages(optString(args, 'project')),
728
- gate_config: ctx.tasks.getGateConfig(optString(args, 'project')) ?? {
729
- require_comment: false,
730
- },
731
- };
732
- }
733
- case 'task_delete': {
734
- ctx.tasks.delete(requireNumber(args, 'task_id'));
735
- return { success: true };
736
- }
737
- case 'task_comment': {
738
- return ctx.comments.add(requireNumber(args, 'task_id'), sessionName(), requireString(args, 'content'), optNumber(args, 'parent_comment_id'));
739
- }
740
- case 'task_get_comments':
741
- return ctx.comments.list(requireNumber(args, 'task_id'), optNumber(args, 'limit'));
742
- case 'task_add_collaborator': {
743
- return ctx.collaborators.add(requireNumber(args, 'task_id'), requireString(args, 'agent_id'), (optString(args, 'role') ?? 'collaborator'));
744
- }
745
- case 'task_remove_collaborator': {
746
- ctx.collaborators.remove(requireNumber(args, 'task_id'), requireString(args, 'agent_id'));
747
- return { success: true };
748
- }
749
- case 'task_search':
750
- return ctx.tasks.search(requireString(args, 'query'), {
751
- project: optString(args, 'project'),
752
- limit: optNumber(args, 'limit'),
753
- });
754
- case 'task_get_subtasks':
755
- return ctx.tasks.getSubtasks(requireNumber(args, 'task_id'));
756
- case 'task_request_approval': {
757
- const taskId = requireNumber(args, 'task_id');
758
- const task = ctx.tasks.getById(taskId);
759
- if (!task)
760
- throw new ValidationError(`Task ${taskId} not found.`);
761
- const stage = optString(args, 'stage') ?? task.stage;
762
- return ctx.approvals.request(taskId, stage, optString(args, 'reviewer'));
763
- }
764
- case 'task_approve':
765
- return ctx.approvals.approve(requireNumber(args, 'approval_id'), sessionName(), optString(args, 'comment'));
766
- case 'task_reject': {
767
- const approval = ctx.approvals.reject(requireNumber(args, 'approval_id'), sessionName(), requireString(args, 'comment'));
768
- const regressTo = optString(args, 'regress_to');
769
- if (regressTo) {
770
- ctx.tasks.regress(approval.task_id, regressTo, requireString(args, 'comment'));
771
- }
772
- return approval;
773
- }
774
- case 'task_pending_approvals':
775
- return ctx.approvals.getPending(optString(args, 'reviewer'));
776
- case 'task_review_cycle': {
777
- const taskId = requireNumber(args, 'task_id');
778
- const action = requireString(args, 'action');
779
- const task = ctx.tasks.getById(taskId);
780
- if (!task)
781
- throw new ValidationError(`Task ${taskId} not found.`);
782
- if (action === 'approve') {
783
- ctx.tasks.advance(taskId);
784
- return { success: true, action: 'approved', task: ctx.tasks.getById(taskId) };
785
- }
786
- else if (action === 'reject') {
787
- const reason = requireString(args, 'reason');
788
- const regressTo = optString(args, 'regress_to') ?? 'implement';
789
- ctx.tasks.regress(taskId, regressTo, reason);
790
- return { success: true, action: 'rejected', task: ctx.tasks.getById(taskId) };
791
- }
792
- else {
793
- throw new ValidationError(`Invalid action: ${action}. Use "approve" or "reject".`);
794
- }
795
- }
796
- case 'task_expand': {
797
- const parentId = requireNumber(args, 'task_id');
798
- const parent = ctx.tasks.getById(parentId);
799
- if (!parent)
800
- throw new ValidationError(`Task ${parentId} not found.`);
801
- const subtaskDefs = args.subtasks;
802
- if (!Array.isArray(subtaskDefs) || subtaskDefs.length === 0) {
803
- throw new ValidationError('"subtasks" must be a non-empty array.');
804
- }
805
- const created = [];
806
- for (const sub of subtaskDefs) {
807
- if (typeof sub !== 'object' ||
808
- sub === null ||
809
- typeof sub.title !== 'string') {
810
- throw new ValidationError('Each subtask must have a "title" string.');
811
- }
812
- const s = sub;
813
- created.push(ctx.tasks.create({
814
- title: s.title,
815
- description: s.description ?? undefined,
816
- priority: typeof s.priority === 'number' ? s.priority : parent.priority,
817
- project: parent.project ?? undefined,
818
- parent_id: parentId,
819
- }, sessionName()));
820
- }
821
- return created;
822
- }
823
- case 'task_cleanup': {
824
- const cleanupMode = (optString(args, 'mode') ?? 'retention');
825
- const timeoutMinutes = optNumber(args, 'timeout_minutes');
826
- if (cleanupMode === 'stale_agents') {
827
- return ctx.cleanup.failStaleAgentTasks(timeoutMinutes).then((stale) => ({
828
- stale_agents: stale,
829
- }));
830
- }
831
- if (cleanupMode === 'all') {
832
- const retention = ctx.cleanup.run();
833
- return ctx.cleanup.failStaleAgentTasks(timeoutMinutes).then((stale) => ({
834
- retention,
835
- stale_agents: stale,
836
- }));
837
- }
838
- return ctx.cleanup.run();
839
- }
840
- case 'task_generate_rules': {
841
- const format = requireString(args, 'format');
842
- if (format !== 'mdc' && format !== 'claude_md') {
843
- throw new ValidationError('Format must be "mdc" or "claude_md".');
844
- }
845
- const project = optString(args, 'project');
846
- const stages = ctx.tasks.getPipelineStages(project);
847
- return { rules: generateRules(format, stages, project) };
848
- }
849
- case 'task_decision': {
850
- const taskId = requireNumber(args, 'task_id');
851
- const chose = requireString(args, 'chose');
852
- const over = requireString(args, 'over');
853
- const because = requireString(args, 'because');
854
- const task = ctx.tasks.getById(taskId);
855
- if (!task)
856
- throw new ValidationError(`Task ${taskId} not found.`);
857
- const decisionStage = task.stage;
858
- const decisionContent = [
859
- '## Decision',
860
- `**Chose:** ${chose}`,
861
- `**Over:** ${over}`,
862
- `**Because:** ${because}`,
863
- '',
864
- `_Recorded at stage: ${decisionStage}_`,
865
- ].join('\n');
866
- return ctx.tasks.addArtifact(taskId, decisionStage, 'decision', decisionContent);
867
- }
868
- default:
869
- throw new ValidationError(`Unknown tool: ${name}`);
870
- }
429
+ const handler = handlers[name];
430
+ if (!handler)
431
+ throw new ValidationError(`Unknown tool: ${name}`);
432
+ return handler(ctx, args, session);
871
433
  };
872
434
  }
873
- function writeSessionFile(id, name) {
874
- try {
875
- const claudeDir = join(homedir(), '.claude');
876
- mkdirSync(claudeDir, { recursive: true });
877
- const filePath = join(claudeDir, `hub-session.${id}.json`);
878
- writeFileSync(filePath, JSON.stringify({ pid: process.pid, name, id, timestamp: new Date().toISOString() }));
879
- }
880
- catch {
881
- /* non-critical */
882
- }
883
- }
884
435
  //# sourceMappingURL=mcp.js.map