agent-tasks 1.7.1 → 1.9.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 (63) 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.map +1 -1
  9. package/dist/domain/cleanup.js +8 -3
  10. package/dist/domain/cleanup.js.map +1 -1
  11. package/dist/domain/rules.js +11 -10
  12. package/dist/domain/rules.js.map +1 -1
  13. package/dist/domain/task-validator.d.ts +9 -0
  14. package/dist/domain/task-validator.d.ts.map +1 -0
  15. package/dist/domain/task-validator.js +70 -0
  16. package/dist/domain/task-validator.js.map +1 -0
  17. package/dist/domain/tasks.d.ts +13 -9
  18. package/dist/domain/tasks.d.ts.map +1 -1
  19. package/dist/domain/tasks.js +165 -111
  20. package/dist/domain/tasks.js.map +1 -1
  21. package/dist/index.js +4 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/storage/database.d.ts.map +1 -1
  24. package/dist/storage/database.js +4 -2
  25. package/dist/storage/database.js.map +1 -1
  26. package/dist/transport/mcp-handlers.d.ts +30 -0
  27. package/dist/transport/mcp-handlers.d.ts.map +1 -0
  28. package/dist/transport/mcp-handlers.js +408 -0
  29. package/dist/transport/mcp-handlers.js.map +1 -0
  30. package/dist/transport/mcp.d.ts.map +1 -1
  31. package/dist/transport/mcp.js +196 -656
  32. package/dist/transport/mcp.js.map +1 -1
  33. package/dist/transport/rest.d.ts.map +1 -1
  34. package/dist/transport/rest.js +4 -1
  35. package/dist/transport/rest.js.map +1 -1
  36. package/dist/transport/ws.d.ts.map +1 -1
  37. package/dist/transport/ws.js +6 -4
  38. package/dist/transport/ws.js.map +1 -1
  39. package/dist/ui/app.js +186 -1608
  40. package/dist/ui/board.js +401 -0
  41. package/dist/ui/drag.js +143 -0
  42. package/dist/ui/index.html +5 -0
  43. package/dist/ui/inline-edit.js +242 -0
  44. package/dist/ui/panel.js +574 -0
  45. package/dist/ui/styles.css +109 -0
  46. package/dist/ui/ui-utils.js +323 -0
  47. package/package.json +1 -1
  48. package/dist/db.d.ts +0 -10
  49. package/dist/db.d.ts.map +0 -1
  50. package/dist/db.js +0 -112
  51. package/dist/db.js.map +0 -1
  52. package/dist/event-bus.d.ts +0 -10
  53. package/dist/event-bus.d.ts.map +0 -1
  54. package/dist/event-bus.js +0 -38
  55. package/dist/event-bus.js.map +0 -1
  56. package/dist/session.d.ts +0 -7
  57. package/dist/session.d.ts.map +0 -1
  58. package/dist/session.js +0 -11
  59. package/dist/session.js.map +0 -1
  60. package/dist/tasks.d.ts +0 -32
  61. package/dist/tasks.d.ts.map +0 -1
  62. package/dist/tasks.js +0 -410
  63. package/dist/tasks.js.map +0 -1
@@ -4,61 +4,79 @@
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
  },
37
40
  {
38
41
  name: 'task_list',
39
- description: 'List tasks with optional filters and pagination.',
42
+ 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
43
  inputSchema: {
41
44
  type: 'object',
42
45
  properties: {
46
+ query: {
47
+ type: 'string',
48
+ description: 'Full-text search across task titles and descriptions (FTS5)',
49
+ },
50
+ next: {
51
+ type: 'boolean',
52
+ description: 'Return the single best available task — highest priority, unassigned, all dependencies met. Uses affinity scoring when "agent" is provided.',
53
+ },
54
+ agent: {
55
+ type: 'string',
56
+ 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',
57
+ },
43
58
  status: {
44
59
  type: 'string',
45
60
  enum: ['pending', 'in_progress', 'completed', 'failed', 'cancelled'],
46
61
  description: 'Filter by status',
47
62
  },
48
- assigned_to: { type: 'string', description: 'Filter by assigned agent name' },
63
+ assign_to: { type: 'string', description: 'Filter by assigned agent name' },
49
64
  stage: { type: 'string', description: 'Filter by pipeline stage' },
50
65
  project: { type: 'string', description: 'Filter by project' },
51
66
  collaborator: { type: 'string', description: 'Filter tasks where agent is a collaborator' },
52
67
  root_only: { type: 'boolean', description: 'Only show top-level tasks (no subtasks)' },
53
68
  parent_id: { type: 'number', description: 'Filter subtasks of a specific parent' },
54
- limit: { type: 'number', description: 'Max results (default/max: 500)' },
69
+ limit: {
70
+ type: 'number',
71
+ description: 'Max results (default: 500 for list, 50 for search)',
72
+ },
55
73
  offset: { type: 'number', description: 'Skip first N results (for pagination)' },
56
74
  },
57
75
  },
58
76
  },
59
77
  {
60
78
  name: 'task_claim',
61
- description: 'Claim a pending task. Assigns it and advances from backlog to the next stage.',
79
+ 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
80
  inputSchema: {
63
81
  type: 'object',
64
82
  properties: {
@@ -72,189 +90,170 @@ export const tools = [
72
90
  },
73
91
  },
74
92
  {
75
- name: 'task_complete',
76
- description: 'Mark a task as completed with a result.',
93
+ name: 'task_update',
94
+ description: 'Update task metadata title, description, priority, tags, project, or assignment. Does not change stage or status (use task_stage for that).',
77
95
  inputSchema: {
78
96
  type: 'object',
79
97
  properties: {
80
98
  task_id: { type: 'number', description: 'Task ID' },
81
- result: { type: 'string', description: 'Result or output' },
99
+ title: { type: 'string', description: 'New title' },
100
+ description: { type: 'string', description: 'New description' },
101
+ priority: { type: 'number', description: 'New priority' },
102
+ project: { type: 'string', description: 'New project' },
103
+ tags: { type: 'array', items: { type: 'string' }, description: 'New tags' },
104
+ assign_to: {
105
+ type: 'string',
106
+ description: 'New assignee (empty string to unassign)',
107
+ },
82
108
  },
83
- required: ['task_id', 'result'],
109
+ required: ['task_id'],
84
110
  },
85
111
  },
86
112
  {
87
- name: 'task_fail',
88
- description: 'Mark a task as failed with an error message.',
113
+ name: 'task_delete',
114
+ description: 'Delete a task and all its artifacts, comments, and dependencies (cascading delete). Cannot be undone.',
89
115
  inputSchema: {
90
116
  type: 'object',
91
117
  properties: {
92
- task_id: { type: 'number', description: 'Task ID' },
93
- result: { type: 'string', description: 'Error description' },
118
+ task_id: { type: 'number', description: 'Task ID to delete' },
94
119
  },
95
- required: ['task_id', 'result'],
120
+ required: ['task_id'],
96
121
  },
97
122
  },
98
123
  {
99
- name: 'task_cancel',
100
- description: 'Cancel a task — moves it to the cancelled stage.',
124
+ name: 'task_comment',
125
+ 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
126
  inputSchema: {
102
127
  type: 'object',
103
128
  properties: {
104
129
  task_id: { type: 'number', description: 'Task ID' },
105
- reason: { type: 'string', description: 'Why the task was cancelled' },
130
+ content: { type: 'string', description: 'Comment text' },
131
+ parent_comment_id: { type: 'number', description: 'Reply to this comment (threading)' },
106
132
  },
107
- required: ['task_id', 'reason'],
133
+ required: ['task_id', 'content'],
108
134
  },
109
135
  },
110
136
  {
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.',
137
+ name: 'task_stage',
138
+ 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
139
  inputSchema: {
114
140
  type: 'object',
115
141
  properties: {
142
+ action: {
143
+ type: 'string',
144
+ enum: ['advance', 'regress', 'complete', 'fail', 'cancel'],
145
+ description: 'Lifecycle action to perform',
146
+ },
116
147
  task_id: { type: 'number', description: 'Task ID' },
117
- stage: { type: 'string', description: 'Target stage (omit to advance to next stage)' },
148
+ stage: {
149
+ type: 'string',
150
+ description: 'Target stage (advance: optional — advances to next if omitted; regress: required)',
151
+ },
118
152
  comment: {
119
153
  type: 'string',
120
- description: 'Optional comment to attach (also satisfies stage-gate require_comment check)',
154
+ description: 'Comment (advance: optional, also satisfies stage-gate require_comment check)',
121
155
  },
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
156
  reason: {
135
157
  type: 'string',
136
- description: 'Reason for regression (stored as artifact)',
158
+ description: 'Reason for regression, failure, or cancellation',
137
159
  },
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: {
160
+ result: {
155
161
  type: 'string',
156
- description: 'New assignee (empty string to unassign)',
162
+ description: 'Result summary (complete) or error description (fail)',
157
163
  },
158
164
  },
159
- required: ['task_id'],
160
- },
161
- },
162
- {
163
- name: 'task_next',
164
- description: 'Get the highest-priority unassigned task with all dependencies met. Returns null if none.',
165
- inputSchema: {
166
- type: 'object',
167
- properties: {
168
- project: { type: 'string', description: 'Filter by project' },
169
- stage: { type: 'string', description: 'Filter by stage' },
170
- },
165
+ required: ['action', 'task_id'],
171
166
  },
172
167
  },
173
168
  {
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.',
169
+ name: 'task_query',
170
+ description: 'Read task-related data. Types: "subtasks" → child tasks, "artifacts" attached documents/decisions/learnings (filterable by stage), "comments" discussion threads.',
176
171
  inputSchema: {
177
172
  type: 'object',
178
173
  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: {
174
+ type: {
185
175
  type: 'string',
186
- enum: ['blocks', 'related', 'duplicate'],
187
- description: 'Relationship type (default: blocks)',
176
+ enum: ['subtasks', 'artifacts', 'comments'],
177
+ description: 'What to query',
188
178
  },
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
179
  task_id: { type: 'number', description: 'Task ID' },
200
- depends_on: { type: 'number', description: 'Dependency to remove' },
180
+ stage: {
181
+ type: 'string',
182
+ description: 'Filter artifacts by stage (only used with type: "artifacts")',
183
+ },
184
+ limit: {
185
+ type: 'number',
186
+ description: 'Max comments to return (only used with type: "comments", default: 100)',
187
+ },
201
188
  },
202
- required: ['task_id', 'depends_on'],
189
+ required: ['type', 'task_id'],
203
190
  },
204
191
  },
205
192
  {
206
- name: 'task_add_artifact',
207
- description: 'Attach a document/artifact to a task (spec, plan, test results, review notes, etc.).',
193
+ name: 'task_artifact',
194
+ 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
195
  inputSchema: {
209
196
  type: 'object',
210
197
  properties: {
198
+ type: {
199
+ type: 'string',
200
+ enum: ['general', 'decision', 'learning'],
201
+ description: 'Artifact type',
202
+ },
211
203
  task_id: { type: 'number', description: 'Task ID' },
212
204
  name: {
213
205
  type: 'string',
214
- description: 'Artifact name (e.g. "spec", "test-results", "review-notes")',
206
+ description: 'Artifact name (type: "general" only, e.g. "spec", "test-results", "review-notes")',
215
207
  },
216
208
  content: {
217
209
  type: 'string',
218
- description: 'Artifact content (text, markdown, JSON, max 100K)',
210
+ description: 'Artifact content (type: "general": text/markdown/JSON, max 100K; type: "learning": the insight)',
219
211
  },
220
212
  stage: {
221
213
  type: 'string',
222
- description: 'Stage to attach to (defaults to task current stage)',
214
+ description: 'Stage to attach to (type: "general" only, defaults to current stage)',
215
+ },
216
+ chose: { type: 'string', description: 'What was chosen (type: "decision" only)' },
217
+ over: {
218
+ type: 'string',
219
+ description: 'What alternatives were considered (type: "decision" only)',
220
+ },
221
+ because: {
222
+ type: 'string',
223
+ description: 'Rationale for the decision (type: "decision" only)',
224
+ },
225
+ category: {
226
+ type: 'string',
227
+ enum: ['technique', 'pitfall', 'decision', 'pattern'],
228
+ description: 'Learning category (type: "learning" only, default: technique)',
223
229
  },
224
230
  },
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'],
231
+ required: ['type', 'task_id'],
238
232
  },
239
233
  },
240
234
  {
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.',
235
+ name: 'task_config',
236
+ 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
237
  inputSchema: {
244
238
  type: 'object',
245
239
  properties: {
240
+ action: {
241
+ type: 'string',
242
+ enum: ['pipeline', 'session', 'cleanup', 'rules'],
243
+ description: 'Config action to perform',
244
+ },
246
245
  project: {
247
246
  type: 'string',
248
- description: 'Project name (omit for default pipeline)',
247
+ description: 'Project name (pipeline: scope, rules: project-specific rules)',
249
248
  },
250
249
  stages: {
251
250
  type: 'array',
252
251
  items: { type: 'string' },
253
- description: 'Stage names in order (set mode)',
252
+ description: 'Stage names in order (pipeline set mode)',
254
253
  },
255
254
  gate_config: {
256
255
  type: 'object',
257
- description: 'Stage-gate enforcement config. Example: { "require_comment": true, "require_artifact": false, "exempt_stages": ["backlog"] }',
256
+ description: 'Stage-gate enforcement config (pipeline only). Example: { "require_comment": true, "require_artifact": false, "exempt_stages": ["backlog"] }',
258
257
  properties: {
259
258
  require_comment: {
260
259
  type: 'boolean',
@@ -297,588 +296,129 @@ export const tools = [
297
296
  },
298
297
  },
299
298
  },
299
+ id: { type: 'string', description: 'Session ID (session only)' },
300
+ name: { type: 'string', description: 'Session name (session only)' },
301
+ mode: {
302
+ type: 'string',
303
+ enum: ['retention', 'stale_agents', 'all'],
304
+ description: 'Cleanup mode (cleanup only, default: retention)',
305
+ },
306
+ timeout_minutes: {
307
+ type: 'number',
308
+ description: 'Heartbeat timeout in minutes for stale agent detection (cleanup only, default: 30)',
309
+ },
310
+ format: {
311
+ type: 'string',
312
+ enum: ['mdc', 'claude_md'],
313
+ description: 'Output format for rules: mdc (Cursor) or claude_md (Claude Code)',
314
+ },
300
315
  },
316
+ required: ['action'],
301
317
  },
302
318
  },
303
319
  {
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.',
320
+ name: 'task_dependency',
321
+ description: 'Manage task dependencies. "add" create a blocks/related/duplicate relationship between two tasks (blocks advancement until resolved). "remove" → delete a dependency.',
342
322
  inputSchema: {
343
323
  type: 'object',
344
324
  properties: {
345
- task_id: { type: 'number', description: 'Task ID' },
346
- limit: { type: 'number', description: 'Max comments (default: 100)' },
325
+ action: {
326
+ type: 'string',
327
+ enum: ['add', 'remove'],
328
+ description: 'Action to perform',
329
+ },
330
+ task_id: { type: 'number', description: 'Task that depends on another' },
331
+ depends_on: {
332
+ type: 'number',
333
+ description: 'Task that must complete first (for blocks) or related task',
334
+ },
335
+ relationship: {
336
+ type: 'string',
337
+ enum: ['blocks', 'related', 'duplicate'],
338
+ description: 'Relationship type (default: blocks, only used with "add")',
339
+ },
347
340
  },
348
- required: ['task_id'],
341
+ required: ['action', 'task_id', 'depends_on'],
349
342
  },
350
343
  },
351
344
  {
352
- name: 'task_add_collaborator',
353
- description: 'Add an agent as collaborator on a task (roles: collaborator, reviewer, watcher).',
345
+ name: 'task_collaborator',
346
+ 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
347
  inputSchema: {
355
348
  type: 'object',
356
349
  properties: {
350
+ action: {
351
+ type: 'string',
352
+ enum: ['add', 'remove'],
353
+ description: 'Action to perform',
354
+ },
357
355
  task_id: { type: 'number', description: 'Task ID' },
358
356
  agent_id: { type: 'string', description: 'Agent name or ID' },
359
357
  role: {
360
358
  type: 'string',
361
359
  enum: ['collaborator', 'reviewer', 'watcher'],
362
- description: 'Role (default: collaborator)',
360
+ description: 'Role (default: collaborator, only used with "add")',
363
361
  },
364
362
  },
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'],
363
+ required: ['action', 'task_id', 'agent_id'],
440
364
  },
441
365
  },
442
366
  {
443
- name: 'task_pending_approvals',
444
- description: 'List pending approval requests, optionally filtered by reviewer.',
367
+ name: 'task_approval',
368
+ 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.',
445
369
  inputSchema: {
446
370
  type: 'object',
447
371
  properties: {
448
- reviewer: { type: 'string', description: 'Filter by reviewer' },
449
- },
450
- },
451
- },
452
- {
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.',
455
- inputSchema: {
456
- type: 'object',
457
- properties: {
458
- task_id: { type: 'number', description: 'Task ID to review' },
459
372
  action: {
460
373
  type: 'string',
461
- enum: ['approve', 'reject'],
462
- description: 'Approve or reject',
374
+ enum: ['request', 'approve', 'reject', 'list', 'review'],
375
+ description: 'Action to perform',
463
376
  },
464
- reason: { type: 'string', description: 'Rejection reason (required for reject)' },
465
- regress_to: {
377
+ task_id: {
378
+ type: 'number',
379
+ description: 'Task ID (required for request and review)',
380
+ },
381
+ approval_id: {
382
+ type: 'number',
383
+ description: 'Approval ID (required for approve and reject)',
384
+ },
385
+ stage: {
466
386
  type: 'string',
467
- description: 'Stage to regress to on rejection (default: implement)',
387
+ description: 'Stage requiring approval (request only, defaults to current)',
468
388
  },
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',
389
+ reviewer: {
390
+ type: 'string',
391
+ description: 'Reviewer to assign (request) or filter by (list)',
495
392
  },
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: {
393
+ comment: {
507
394
  type: 'string',
508
- enum: ['retention', 'stale_agents', 'all'],
509
- description: 'Cleanup mode (default: retention)',
395
+ description: 'Comment (optional for approve, required for reject)',
510
396
  },
511
- timeout_minutes: {
512
- type: 'number',
513
- description: 'Heartbeat timeout in minutes for stale agent detection (default: 30, only used with stale_agents mode)',
397
+ decision: {
398
+ type: 'string',
399
+ enum: ['approve', 'reject'],
400
+ description: 'Decision for review action',
514
401
  },
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: {
402
+ reason: {
525
403
  type: 'string',
526
- enum: ['mdc', 'claude_md'],
527
- description: 'Output format: mdc (Cursor) or claude_md (Claude Code)',
404
+ description: 'Rejection reason (required for review+reject)',
405
+ },
406
+ regress_to: {
407
+ type: 'string',
408
+ description: 'Stage to regress to on rejection (reject/review, default: implement)',
528
409
  },
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
410
  },
545
- required: ['task_id', 'chose', 'over', 'because'],
411
+ required: ['action'],
546
412
  },
547
413
  },
548
414
  ];
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
415
  export function createToolHandler(ctx) {
600
- let currentSession = null;
601
- function sessionName() {
602
- return currentSession?.name ?? 'system';
603
- }
416
+ const session = { current: null };
604
417
  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
- }
418
+ const handler = handlers[name];
419
+ if (!handler)
420
+ throw new ValidationError(`Unknown tool: ${name}`);
421
+ return handler(ctx, args, session);
871
422
  };
872
423
  }
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
424
  //# sourceMappingURL=mcp.js.map