@critical-path/mcp 0.18.0 → 0.19.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 (51) hide show
  1. package/CHANGELOG.md +671 -0
  2. package/README.md +73 -25
  3. package/dist/bin/args.d.ts +8 -0
  4. package/dist/bin/args.d.ts.map +1 -1
  5. package/dist/bin/args.js +20 -0
  6. package/dist/bin/args.js.map +1 -1
  7. package/dist/bin/cli.js +37 -3
  8. package/dist/bin/cli.js.map +1 -1
  9. package/dist/bin/skills.d.ts +15 -0
  10. package/dist/bin/skills.d.ts.map +1 -0
  11. package/dist/bin/skills.js +28 -0
  12. package/dist/bin/skills.js.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/server/index.d.ts +6 -0
  18. package/dist/server/index.d.ts.map +1 -1
  19. package/dist/server/index.js +27 -13
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/skills/catalog.generated.d.ts +3 -0
  22. package/dist/skills/catalog.generated.d.ts.map +1 -0
  23. package/dist/skills/catalog.generated.js +33 -0
  24. package/dist/skills/catalog.generated.js.map +1 -0
  25. package/dist/skills/index.d.ts +13 -0
  26. package/dist/skills/index.d.ts.map +1 -0
  27. package/dist/skills/index.js +7 -0
  28. package/dist/skills/index.js.map +1 -0
  29. package/dist/tools/definitions.d.ts +82 -8
  30. package/dist/tools/definitions.d.ts.map +1 -1
  31. package/dist/tools/definitions.js +296 -52
  32. package/dist/tools/definitions.js.map +1 -1
  33. package/dist/tools/instructions.d.ts +10 -0
  34. package/dist/tools/instructions.d.ts.map +1 -0
  35. package/dist/tools/instructions.js +48 -0
  36. package/dist/tools/instructions.js.map +1 -0
  37. package/dist/tools/outputs.d.ts +2311 -0
  38. package/dist/tools/outputs.d.ts.map +1 -0
  39. package/dist/tools/outputs.js +184 -0
  40. package/dist/tools/outputs.js.map +1 -0
  41. package/dist/web/index.d.ts +16 -0
  42. package/dist/web/index.d.ts.map +1 -1
  43. package/dist/web/index.js +13 -12
  44. package/dist/web/index.js.map +1 -1
  45. package/package.json +14 -8
  46. package/skills/analyze-schedule/SKILL.md +42 -0
  47. package/skills/investigate-task/SKILL.md +41 -0
  48. package/skills/plan-capacity/SKILL.md +32 -0
  49. package/skills/plan-project/SKILL.md +45 -0
  50. package/skills/report-status/SKILL.md +46 -0
  51. package/skills/triage-tasks/SKILL.md +41 -0
@@ -1,12 +1,39 @@
1
1
  import { z } from 'zod';
2
2
  import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import { ProjectOutput, TaskOutput, TaskWithDependenciesOutput, DependencyOutput, DeliverableOutput, CommentOutput, CriticalPathOutput, PortfolioCriticalPathOutput, TimelineLadderOutput, TaskLadderOutput, TaskMetricsOutput, TaskProgressHistoryOutput, WorkloadDistributionOutput, SkillOutput } from './outputs.js';
4
+ import { SKILLS, getSkill } from '../skills/index.js';
3
5
  /**
4
- * Declares a tool, generating its JSON Schema `inputSchema` from `zodSchema` so the schema the
5
- * model sees and the one arguments are validated against cannot drift apart.
6
+ * Groups of related tools. Hosts expose only the groups an agent needs (`toolsets` option), which
7
+ * keeps the tool list short and tool choice accurate.
8
+ */
9
+ export const TOOLSETS = {
10
+ core: 'Projects, tasks and task dependencies: find, read, create, update and delete',
11
+ collaboration: 'Deliverables (milestones) and task comments',
12
+ scheduling: 'Critical path, portfolio scheduling and workload/capacity',
13
+ analytics: 'Project status ladder, task metrics, earned value and progress history',
14
+ guidance: 'Skills: step-by-step playbooks for multi-step jobs'
15
+ };
16
+ function toJsonSchema(schema) {
17
+ const { $schema: _schema, ...json } = zodToJsonSchema(schema, { $refStrategy: 'none' });
18
+ return json;
19
+ }
20
+ /**
21
+ * Declares a tool, generating its JSON Schema `inputSchema` and `outputSchema` from the zod schemas
22
+ * so the schemas the model sees and the ones values are checked against cannot drift apart.
6
23
  */
7
24
  export function defineTool(spec) {
8
- const { $schema: _schema, ...inputSchema } = zodToJsonSchema(spec.zodSchema, { $refStrategy: 'none' });
9
- return { ...spec, inputSchema };
25
+ const readOnlyHint = spec.annotations?.readOnlyHint ?? false;
26
+ return {
27
+ ...spec,
28
+ inputSchema: toJsonSchema(spec.zodSchema),
29
+ outputSchema: toJsonSchema(spec.outputZodSchema),
30
+ annotations: {
31
+ readOnlyHint,
32
+ destructiveHint: spec.annotations?.destructiveHint ?? false,
33
+ idempotentHint: spec.annotations?.idempotentHint ?? readOnlyHint,
34
+ openWorldHint: spec.annotations?.openWorldHint ?? false
35
+ }
36
+ };
10
37
  }
11
38
  export class ToolArgumentError extends Error {
12
39
  constructor(toolName, details) {
@@ -37,17 +64,21 @@ export async function resolveBackend(target) {
37
64
  export const listProjectsTool = defineTool({
38
65
  name: 'list_projects',
39
66
  title: 'List Projects',
40
- description: 'Retrieve all projects in the workspace.',
67
+ description: 'List every project you can read, with its id, name and key. Start here to find a projectId; never guess ids. Returns { projects }.',
68
+ toolset: 'core',
69
+ outputZodSchema: z.object({ projects: z.array(ProjectOutput) }),
41
70
  zodSchema: z.object({}),
42
71
  annotations: { readOnlyHint: true },
43
72
  execute: async (_args, target) => {
44
- return target.getProjects();
73
+ return { projects: await target.getProjects() };
45
74
  }
46
75
  });
47
76
  export const getProjectTool = defineTool({
48
77
  name: 'get_project',
49
78
  title: 'Get Project',
50
- description: 'Get details of a specific project by its ID.',
79
+ description: 'Get one project by id: name, key, workflow statuses, custom field definitions and calendar. Use it before creating or moving tasks to learn the valid status keys and custom fields. Returns the project. For its tasks use `list_tasks`.',
80
+ toolset: 'core',
81
+ outputZodSchema: ProjectOutput,
51
82
  zodSchema: z.object({
52
83
  id: z.string().describe('The project ID')
53
84
  }),
@@ -59,7 +90,9 @@ export const getProjectTool = defineTool({
59
90
  export const createProjectTool = defineTool({
60
91
  name: 'create_project',
61
92
  title: 'Create Project',
62
- description: 'Create a new project workspace.',
93
+ description: 'Create a new, empty project. Use only when the user wants a new project; to add work to an existing one use `create_task`. Returns the created project with its id.',
94
+ toolset: 'core',
95
+ outputZodSchema: ProjectOutput,
63
96
  zodSchema: z.object({
64
97
  name: z.string().describe('Name of the project'),
65
98
  description: z.string().optional().describe('Description of the project'),
@@ -73,7 +106,9 @@ export const createProjectTool = defineTool({
73
106
  export const listTasksTool = defineTool({
74
107
  name: 'list_tasks',
75
108
  title: 'List Tasks',
76
- description: 'List tasks (oldest first), optionally filtered by project, status, priority, assignee, iteration, deliverable, container or parent task (\'none\' for top-level tasks). Returns { tasks, nextCursor }; pass nextCursor back as cursor to get the next page.',
109
+ description: 'Find tasks, oldest first, filtered by project, status, priority, assignee, iteration, deliverable, container or parent task (\'none\' for top-level tasks only). Use it for questions like "what is blocked / assigned to X / in progress" and to look up task ids. Returns { tasks, nextCursor }; pass nextCursor back as cursor for the next page. For one task\'s full record use `get_task`.',
110
+ toolset: 'core',
111
+ outputZodSchema: z.object({ tasks: z.array(TaskOutput), nextCursor: z.string().nullable().optional() }),
77
112
  zodSchema: z.object({
78
113
  projectId: z.string().optional().describe('Filter by project ID'),
79
114
  status: z.string().optional().describe('Filter by task status key'),
@@ -106,19 +141,60 @@ export const listTasksTool = defineTool({
106
141
  export const getTaskTool = defineTool({
107
142
  name: 'get_task',
108
143
  title: 'Get Task',
109
- description: 'Get task details by ID.',
144
+ description: 'Get one task by id: status, priority, assignee, estimate, logged hours, dates, blocked state and custom fields. Set includeDependencies to also see which tasks it waits for and which wait for it. Returns the task, with { dependencies: { dependsOn, dependents } } when asked. For its discussion use `list_comments`; for progress and variance use `get_task_metrics`.',
145
+ toolset: 'core',
146
+ outputZodSchema: TaskWithDependenciesOutput,
110
147
  zodSchema: z.object({
111
- id: z.string().describe('The task ID')
148
+ id: z.string().describe('The task ID'),
149
+ includeDependencies: z
150
+ .boolean()
151
+ .optional()
152
+ .describe('Also return the tasks this one waits for (dependsOn) and the tasks waiting for it (dependents)')
112
153
  }),
113
154
  annotations: { readOnlyHint: true },
114
155
  execute: async (args, target) => {
115
- return target.getTask(args.id);
156
+ const task = await target.getTask(args.id);
157
+ if (!task || !args.includeDependencies)
158
+ return task;
159
+ const graph = await getDependencyGraph(target, args.id);
160
+ const related = new Map([...graph.upstreamTasks, ...graph.downstreamTasks].map((t) => [t.id, t]));
161
+ const dependsOn = [];
162
+ const dependents = [];
163
+ for (const dependency of graph.dependencies) {
164
+ const { waiting, waitedFor } = dependencyDirection(dependency);
165
+ const otherId = waiting === args.id ? waitedFor : waiting;
166
+ const other = related.get(otherId);
167
+ // Tasks the caller cannot read are left out.
168
+ if (!other || otherId === args.id)
169
+ continue;
170
+ const link = { dependencyId: dependency.id, taskId: other.id, title: other.title, status: other.status, type: linkType(dependency) };
171
+ (waiting === args.id ? dependsOn : dependents).push(link);
172
+ }
173
+ return { ...task, dependencies: { dependsOn, dependents } };
116
174
  }
117
175
  });
176
+ /**
177
+ * Which task waits for which. A `blocked_by` record is stored the other way round: its
178
+ * `dependsOnTaskId` waits for its `taskId` (see the scheduler in `@critical-path/core`).
179
+ */
180
+ function dependencyDirection(dependency) {
181
+ return dependency.type === 'blocked_by'
182
+ ? { waiting: dependency.dependsOnTaskId, waitedFor: dependency.taskId }
183
+ : { waiting: dependency.taskId, waitedFor: dependency.dependsOnTaskId };
184
+ }
185
+ const linkType = (dependency) => (dependency.type === 'relates_to' ? 'relates_to' : 'blocking');
186
+ async function getDependencyGraph(target, taskId) {
187
+ const { isEngine } = await resolveBackend(target);
188
+ return isEngine
189
+ ? target.getTaskDependencyGraph(taskId)
190
+ : target.getTaskDependencies(taskId);
191
+ }
118
192
  export const createTaskTool = defineTool({
119
193
  name: 'create_task',
120
194
  title: 'Create Task',
121
- description: 'Create a new task within a project.',
195
+ description: 'Create a task in a project. Check with `list_tasks` first so you do not add a duplicate. Without a status the task starts in the project\'s default workflow status. Returns the created task with its id.',
196
+ toolset: 'core',
197
+ outputZodSchema: TaskOutput,
122
198
  zodSchema: z.object({
123
199
  projectId: z.string().optional().describe('Project ID (inferred from active view in browser if omitted)'),
124
200
  title: z.string().describe('Title of the task'),
@@ -142,7 +218,7 @@ export const createTaskTool = defineTool({
142
218
  execute: async (args, target, ambientContext) => {
143
219
  const projectId = args.projectId || ambientContext?.projectId;
144
220
  if (!projectId) {
145
- throw new Error('projectId is required to create a task, but none was provided or active in context.');
221
+ throw new Error('projectId is required to create a task, but none was provided or active in context. Find one with `list_projects`.');
146
222
  }
147
223
  const input = {
148
224
  projectId,
@@ -166,28 +242,31 @@ export const createTaskTool = defineTool({
166
242
  export const updateTaskTool = defineTool({
167
243
  name: 'update_task',
168
244
  title: 'Update Task',
169
- description: 'Update fields of an existing task (e.g. title, status, priority, assignee).',
245
+ description: 'Change an existing task: title, description, status, priority, assignee, due date, estimate, logged hours, allocation, blocked state, tags or custom fields. Only the fields you pass change. Use it to move a task through its workflow or re-plan it. Returns the updated task.',
246
+ toolset: 'core',
247
+ outputZodSchema: TaskOutput,
170
248
  zodSchema: z.object({
171
249
  id: z.string().describe('The task ID'),
172
- title: z.string().optional(),
173
- description: z.string().optional(),
174
- status: z.string().optional(),
175
- priority: z.enum(['urgent', 'high', 'medium', 'low', 'none']).optional(),
176
- assigneeId: z.string().optional(),
177
- dueDate: z.string().optional(),
178
- tags: z.array(z.string()).optional(),
179
- estimatedHours: z.number().optional(),
250
+ title: z.string().optional().describe('New title'),
251
+ description: z.string().optional().describe('New description (markdown)'),
252
+ status: z.string().optional().describe("Status key from the project's workflow (see `get_project`)"),
253
+ priority: z.enum(['urgent', 'high', 'medium', 'low', 'none']).optional().describe('New priority'),
254
+ assigneeId: z.string().optional().describe('User ID to assign the task to'),
255
+ dueDate: z.string().optional().describe('Due date (ISO string)'),
256
+ tags: z.array(z.string()).optional().describe('Replaces all tags'),
257
+ estimatedHours: z.number().optional().describe('Estimated effort in hours'),
180
258
  allocation: z
181
259
  .number()
182
260
  .gt(0)
183
261
  .max(1)
184
262
  .optional()
185
263
  .describe("Share of the assignee's time the task takes, in (0, 1]; e.g. 0.5 for half-time. Estimates are effort, so a 0.5 task takes twice as long"),
186
- loggedHours: z.number().optional(),
264
+ loggedHours: z.number().optional().describe('Total hours worked so far'),
187
265
  isBlocked: z.boolean().optional().describe('Whether the task is blocked'),
188
266
  blockedReason: z.string().optional().describe('Reason why the task is blocked'),
189
267
  customFields: z.record(z.unknown()).optional().describe('Custom field values as a JSON object (validated against the project\'s field definitions for tasks)')
190
268
  }),
269
+ annotations: { idempotentHint: true },
191
270
  execute: async (args, target) => {
192
271
  const { id, ...updates } = args;
193
272
  return target.updateTask(id, updates);
@@ -196,50 +275,113 @@ export const updateTaskTool = defineTool({
196
275
  export const deleteTaskTool = defineTool({
197
276
  name: 'delete_task',
198
277
  title: 'Delete Task',
199
- description: 'Delete a task by ID.',
278
+ description: 'Permanently delete a task by id. This cannot be undone: confirm with the user first, and prefer `update_task` with a canceled status when the history should be kept. Returns { success }.',
279
+ toolset: 'core',
280
+ outputZodSchema: z.object({ success: z.boolean() }),
200
281
  zodSchema: z.object({
201
282
  id: z.string().describe('The task ID to delete')
202
283
  }),
203
- annotations: { requiresConfirmation: true },
284
+ annotations: { destructiveHint: true, idempotentHint: true },
204
285
  execute: async (args, target) => {
205
286
  const success = await target.deleteTask(args.id);
206
287
  return { success };
207
288
  }
208
289
  });
290
+ export const addDependencyTool = defineTool({
291
+ name: 'add_dependency',
292
+ title: 'Add Dependency',
293
+ description: 'Record that a task cannot start until another task finishes, so the critical path schedules them in order. Use it when planning or re-sequencing work. Adding a link that already exists returns it; a link that would make a loop is refused. Returns the dependency { id, taskId, dependsOnTaskId, type }. To undo use `remove_dependency`.',
294
+ toolset: 'core',
295
+ outputZodSchema: DependencyOutput,
296
+ zodSchema: z.object({
297
+ taskId: z.string().describe('The task that waits'),
298
+ dependsOnTaskId: z.string().describe('The task that must finish first'),
299
+ type: z
300
+ .enum(['blocking', 'relates_to'])
301
+ .optional()
302
+ .describe("'blocking' (default) affects the schedule; 'relates_to' only links the tasks for reference")
303
+ }),
304
+ annotations: { idempotentHint: true },
305
+ execute: async (args, target) => {
306
+ const type = args.type ?? 'blocking';
307
+ const { isEngine } = await resolveBackend(target);
308
+ if (isEngine) {
309
+ return target.addDependency({ taskId: args.taskId, dependsOnTaskId: args.dependsOnTaskId, type });
310
+ }
311
+ return target.addDependency(args.taskId, { dependsOnTaskId: args.dependsOnTaskId, type });
312
+ }
313
+ });
314
+ export const removeDependencyTool = defineTool({
315
+ name: 'remove_dependency',
316
+ title: 'Remove Dependency',
317
+ description: 'Stop a task waiting for another task, for example when the order was wrong or the work no longer depends on it. Identify the link by its two task ids, as given to `add_dependency`. Removing a link that does not exist is not an error. Returns { removed, dependencyIds } with the number of links removed.',
318
+ toolset: 'core',
319
+ outputZodSchema: z.object({
320
+ removed: z.number().describe('How many dependency records were removed (0 if none matched)'),
321
+ dependencyIds: z.array(z.string()).describe('Ids of the removed records')
322
+ }),
323
+ zodSchema: z.object({
324
+ taskId: z.string().describe('The task that waits'),
325
+ dependsOnTaskId: z.string().describe('The task it waits for'),
326
+ type: z.enum(['blocking', 'relates_to']).optional().describe('Remove only links of this type (default: both)')
327
+ }),
328
+ annotations: { idempotentHint: true },
329
+ execute: async (args, target) => {
330
+ const graph = await getDependencyGraph(target, args.taskId);
331
+ const matches = graph.dependencies.filter((dependency) => {
332
+ const { waiting, waitedFor } = dependencyDirection(dependency);
333
+ return waiting === args.taskId && waitedFor === args.dependsOnTaskId && (!args.type || linkType(dependency) === args.type);
334
+ });
335
+ const { isEngine } = await resolveBackend(target);
336
+ const dependencyIds = [];
337
+ for (const dependency of matches) {
338
+ const removed = isEngine
339
+ ? await target.removeDependency(dependency.id)
340
+ : await target.removeDependency(dependency.taskId, dependency.id);
341
+ if (removed)
342
+ dependencyIds.push(dependency.id);
343
+ }
344
+ return { removed: dependencyIds.length, dependencyIds };
345
+ }
346
+ });
209
347
  export const listDeliverablesTool = defineTool({
210
348
  name: 'list_deliverables',
211
349
  title: 'List Deliverables',
212
- description: 'List deliverables/milestones for a project.',
350
+ description: 'List a project\'s deliverables: milestones that group tasks toward a dated outcome. Use it to see what is due and which tasks feed each milestone. Returns { deliverables }.',
351
+ toolset: 'collaboration',
352
+ outputZodSchema: z.object({ deliverables: z.array(DeliverableOutput) }),
213
353
  zodSchema: z.object({
214
- projectId: z.string().optional().describe('Project ID')
354
+ projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)')
215
355
  }),
216
356
  annotations: { readOnlyHint: true },
217
357
  execute: async (args, target, ambientContext) => {
218
358
  const projectId = args.projectId || ambientContext?.projectId;
219
359
  if (!projectId)
220
- throw new Error('projectId is required to list deliverables.');
360
+ throw new Error('projectId is required to list deliverables. Find one with `list_projects`.');
221
361
  if ('getDeliverables' in target && typeof target.getDeliverables === 'function') {
222
- return target.getDeliverables(projectId);
362
+ return { deliverables: await target.getDeliverables(projectId) };
223
363
  }
224
- return [];
364
+ return { deliverables: [] };
225
365
  }
226
366
  });
227
367
  export const createDeliverableTool = defineTool({
228
368
  name: 'create_deliverable',
229
369
  title: 'Create Deliverable',
230
- description: 'Create a milestone deliverable within a project.',
370
+ description: 'Create a deliverable (milestone) in a project, optionally linking existing tasks by id. Use it when planning a dated outcome that several tasks build toward. Returns the created deliverable.',
371
+ toolset: 'collaboration',
372
+ outputZodSchema: DeliverableOutput,
231
373
  zodSchema: z.object({
232
- projectId: z.string().optional(),
233
- title: z.string(),
234
- description: z.string().optional(),
235
- dueDate: z.string().optional(),
236
- taskIds: z.array(z.string()).optional(),
374
+ projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
375
+ title: z.string().describe('Name of the deliverable'),
376
+ description: z.string().optional().describe('What the deliverable is and how it is accepted'),
377
+ dueDate: z.string().optional().describe('Due date (ISO string)'),
378
+ taskIds: z.array(z.string()).optional().describe('IDs of existing tasks that feed this deliverable'),
237
379
  customFields: z.record(z.unknown()).optional().describe('Custom field values as a JSON object (validated against the project\'s field definitions for tasks)')
238
380
  }),
239
381
  execute: async (args, target, ambientContext) => {
240
382
  const projectId = args.projectId || ambientContext?.projectId;
241
383
  if (!projectId)
242
- throw new Error('projectId is required to create a deliverable.');
384
+ throw new Error('projectId is required to create a deliverable. Find one with `list_projects`.');
243
385
  if ('createDeliverable' in target && typeof target.createDeliverable === 'function') {
244
386
  return target.createDeliverable({
245
387
  ...args,
@@ -252,22 +394,26 @@ export const createDeliverableTool = defineTool({
252
394
  export const listCommentsTool = defineTool({
253
395
  name: 'list_comments',
254
396
  title: 'List Comments',
255
- description: 'List comments associated with a task.',
397
+ description: 'List the comments on a task. Use it to read the discussion, decisions or blockers recorded there. Returns { comments }.',
398
+ toolset: 'collaboration',
399
+ outputZodSchema: z.object({ comments: z.array(CommentOutput) }),
256
400
  zodSchema: z.object({
257
401
  taskId: z.string().describe('The task ID')
258
402
  }),
259
403
  annotations: { readOnlyHint: true },
260
404
  execute: async (args, target) => {
261
405
  if ('getComments' in target && typeof target.getComments === 'function') {
262
- return target.getComments(args.taskId);
406
+ return { comments: await target.getComments(args.taskId) };
263
407
  }
264
- return [];
408
+ return { comments: [] };
265
409
  }
266
410
  });
267
411
  export const addCommentTool = defineTool({
268
412
  name: 'add_comment',
269
413
  title: 'Add Comment',
270
- description: 'Add a comment to a task. The comment is attributed to the identity this MCP server runs as.',
414
+ description: 'Post a comment on a task, attributed to the identity this server runs as. Use it to record a decision, status note, question or the reasoning behind a change you made. Returns the created comment.',
415
+ toolset: 'collaboration',
416
+ outputZodSchema: CommentOutput,
271
417
  zodSchema: z.object({
272
418
  taskId: z.string().describe('The task ID'),
273
419
  content: z.string().describe('Comment body/markdown'),
@@ -280,7 +426,9 @@ export const addCommentTool = defineTool({
280
426
  export const calculatePortfolioCriticalPathTool = defineTool({
281
427
  name: 'calculate_portfolio_critical_path',
282
428
  title: 'Calculate Portfolio Critical Path',
283
- description: 'Critical path analysis across several projects at once (or every readable project). With calendars "assignee" and levelResources, people and teams are shared across projects, so nobody is double-booked between them.',
429
+ description: 'Schedule several projects together (default: every project you can read) so people and teams shared between them are not double-booked. Use it for cross-project delivery dates and resource conflicts. Returns { projects, projectEndDate, overallocations } with one analysis per project. For a single project use `calculate_critical_path`.',
430
+ toolset: 'scheduling',
431
+ outputZodSchema: PortfolioCriticalPathOutput,
284
432
  zodSchema: z.object({
285
433
  projectIds: z.array(z.string()).optional().describe('Projects to include (default: every project you can read)'),
286
434
  projectOrder: z.array(z.string()).optional().describe('Project ids in priority order for levelling; unlisted projects come last'),
@@ -305,7 +453,9 @@ export const calculatePortfolioCriticalPathTool = defineTool({
305
453
  export const calculateCriticalPathTool = defineTool({
306
454
  name: 'calculate_critical_path',
307
455
  title: 'Calculate Critical Path',
308
- description: 'Calculate Critical Path Method (CPM) schedule, early/late start and finish, total slack, and critical bottlenecks for a project.',
456
+ description: 'Schedule one project with the Critical Path Method: which tasks drive the end date, each task\'s early and late start and finish, and how much it can slip (slack). Use it for "when will this finish?", "what is on the critical path?" or "can this task slip?". Set calendars \'assignee\' with levelResources so each person works on one task at a time. Returns { projectEndDate, criticalTaskIds, tasks, overallocations }. For several projects that share people use `calculate_portfolio_critical_path`.',
457
+ toolset: 'scheduling',
458
+ outputZodSchema: CriticalPathOutput,
309
459
  zodSchema: z.object({
310
460
  projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
311
461
  calendars: z
@@ -329,7 +479,7 @@ export const calculateCriticalPathTool = defineTool({
329
479
  execute: async (args, target, ambientContext) => {
330
480
  const projectId = args.projectId || ambientContext?.projectId;
331
481
  if (!projectId) {
332
- throw new Error('projectId is required to calculate critical path.');
482
+ throw new Error('projectId is required to calculate the critical path. Find one with `list_projects`.');
333
483
  }
334
484
  const { calendars, levelResources, levelingPriority, effort } = args;
335
485
  return target.calculateCriticalPath(projectId, { calendars, levelResources, levelingPriority, effort });
@@ -338,7 +488,9 @@ export const calculateCriticalPathTool = defineTool({
338
488
  export const getTimelineLadderTool = defineTool({
339
489
  name: 'get_timeline_ladder',
340
490
  title: 'Get Timeline Ladder of Abstraction',
341
- description: 'Retrieve Ladder of Abstraction for a project timeline across macro phase envelopes, standard Gantt tasks & CPM, and concrete deliverables/time/attachments.',
491
+ description: 'Project status at three zoom levels: \'macro\' phases with health (on_track, at_risk, blocked, overdue) and progress, \'standard\' task timeline with the critical path and dependencies, and \'concrete\' evidence of work (time logged, attachments) per task. Use it for project status reports; level \'macro\' gives a compact summary. Returns { macro, standard, concrete }. For a single task use `get_task_ladder`.',
492
+ toolset: 'analytics',
493
+ outputZodSchema: TimelineLadderOutput,
342
494
  zodSchema: z.object({
343
495
  projectId: z.string().optional().describe('Project ID (falls back to ambient context if omitted)'),
344
496
  level: z.enum(['macro', 'standard', 'concrete', 'all']).optional().describe('Abstraction level (macro, standard, concrete, or all)'),
@@ -349,7 +501,7 @@ export const getTimelineLadderTool = defineTool({
349
501
  execute: async (args, target, ambientContext) => {
350
502
  const projectId = args.projectId || ambientContext?.projectId;
351
503
  if (!projectId) {
352
- throw new Error('projectId is required to get timeline ladder.');
504
+ throw new Error('projectId is required to get the timeline ladder. Find one with `list_projects`.');
353
505
  }
354
506
  return target.getTimelineLadder(projectId, {
355
507
  level: args.level,
@@ -361,7 +513,9 @@ export const getTimelineLadderTool = defineTool({
361
513
  export const getTaskLadderTool = defineTool({
362
514
  name: 'get_task_ladder',
363
515
  title: 'Get Task Ladder View',
364
- description: 'Retrieve multi-scale ladder view for a single task, connecting its macro phase, standard CPM timeline position, and concrete work evidence.',
516
+ description: 'One task in context: the phase it belongs to and that phase\'s health, its place in the schedule (dates, slack, critical or not) and evidence of work done. Use it to explain where a task fits and whether it puts its phase at risk. Returns { macroPhase, standard, concrete, metrics }. For the numbers alone use `get_task_metrics`.',
517
+ toolset: 'analytics',
518
+ outputZodSchema: TaskLadderOutput,
365
519
  zodSchema: z.object({
366
520
  taskId: z.string().describe('The task ID')
367
521
  }),
@@ -373,7 +527,9 @@ export const getTaskLadderTool = defineTool({
373
527
  export const getTaskMetricsTool = defineTool({
374
528
  name: 'get_task_metrics',
375
529
  title: 'Get Task Metrics',
376
- description: 'Retrieve multi-dimensional task metrics including inferred actuals, active working duration, effort/duration/schedule variances, progress inference, and Earned Value Management (EVM) metrics.',
530
+ description: 'Is a task on track? Progress percentage, actual against estimated hours, schedule variance, overdue flags and earned value (CPI, SPI). Use it to diagnose a late or over-budget task. Returns { progress, realityDelta, evm, inferredActuals, progressHistory }.',
531
+ toolset: 'analytics',
532
+ outputZodSchema: TaskMetricsOutput,
377
533
  zodSchema: z.object({
378
534
  taskId: z.string().describe('The task ID')
379
535
  }),
@@ -385,7 +541,9 @@ export const getTaskMetricsTool = defineTool({
385
541
  export const getTaskProgressHistoryTool = defineTool({
386
542
  name: 'get_task_progress_history',
387
543
  title: 'Get Task Progress History',
388
- description: 'Retrieve time-series progress data points and curve shape classification (linear, s_curve, early_surge, late_rush, stalled) for a task.',
544
+ description: 'How a task\'s progress has moved over time: dated progress points and a curve shape (linear, s_curve, early_surge, late_rush, stalled). Use it to tell whether work is stalling or being rushed at the end. Returns { points, curveProfile }. `get_task_metrics` includes the same history alongside variance and earned value.',
545
+ toolset: 'analytics',
546
+ outputZodSchema: TaskProgressHistoryOutput,
389
547
  zodSchema: z.object({
390
548
  taskId: z.string().describe('The task ID')
391
549
  }),
@@ -397,7 +555,9 @@ export const getTaskProgressHistoryTool = defineTool({
397
555
  export const getWorkloadDistributionTool = defineTool({
398
556
  name: 'get_workload_distribution',
399
557
  title: 'Get Workload Distribution',
400
- description: 'Retrieve time-series workload and capacity distribution suitable for streamgraphs, stacked charts, and team capacity planning.',
558
+ description: 'Hours of work per person, team, task type, priority or status in each day, week or month, with capacity and utilization. Use it for "who is overloaded?" and capacity planning; a utilizationRatio above 1 means more work than capacity. Omit projectId for the whole workspace. Returns { buckets, seriesKeys, seriesLabels, totalHours, averageUtilization }.',
559
+ toolset: 'scheduling',
560
+ outputZodSchema: WorkloadDistributionOutput,
401
561
  zodSchema: z.object({
402
562
  projectId: z.string().optional().describe('Project ID (optional, defaults to workspace-wide or ambient project)'),
403
563
  startDate: z.string().optional().describe('Start date ISO (YYYY-MM-DD)'),
@@ -420,6 +580,27 @@ export const getWorkloadDistributionTool = defineTool({
420
580
  });
421
581
  }
422
582
  });
583
+ export const getSkillTool = defineTool({
584
+ name: 'get_skill',
585
+ title: 'Get Skill',
586
+ description: 'Load a step-by-step playbook for a multi-step project management job, such as planning a project, triaging tasks, finding schedule risk, writing a status report, checking capacity or investigating a late task. Call it before starting such a job and follow its steps. Omit name to list the skills. Returns { name, description, content }, or { skills } without a name.',
587
+ toolset: 'guidance',
588
+ outputZodSchema: SkillOutput,
589
+ zodSchema: z.object({
590
+ name: z.string().optional().describe('Skill name, e.g. "plan-project"; omit to list all skills')
591
+ }),
592
+ annotations: { readOnlyHint: true },
593
+ execute: async (args) => {
594
+ if (!args.name) {
595
+ return { skills: SKILLS.map(({ name, description }) => ({ name, description })) };
596
+ }
597
+ const skill = getSkill(args.name);
598
+ if (!skill) {
599
+ throw new Error(`Skill "${args.name}" not found. Available skills: ${SKILLS.map((s) => s.name).join(', ')}.`);
600
+ }
601
+ return skill;
602
+ }
603
+ });
423
604
  export const ALL_TOOLS = [
424
605
  listProjectsTool,
425
606
  getProjectTool,
@@ -429,6 +610,8 @@ export const ALL_TOOLS = [
429
610
  createTaskTool,
430
611
  updateTaskTool,
431
612
  deleteTaskTool,
613
+ addDependencyTool,
614
+ removeDependencyTool,
432
615
  listDeliverablesTool,
433
616
  createDeliverableTool,
434
617
  listCommentsTool,
@@ -439,7 +622,68 @@ export const ALL_TOOLS = [
439
622
  getTaskLadderTool,
440
623
  getTaskMetricsTool,
441
624
  getTaskProgressHistoryTool,
442
- getWorkloadDistributionTool
625
+ getWorkloadDistributionTool,
626
+ getSkillTool
443
627
  ];
444
628
  export const TOOL_MAP = new Map(ALL_TOOLS.map((tool) => [tool.name, tool]));
629
+ /**
630
+ * Picks the tools a host exposes. Unknown tool or toolset names throw, so a typo cannot silently
631
+ * leave an agent without the tools it was meant to have. `get_skill` follows the same rules as
632
+ * other tools: include the `guidance` toolset (or name it) to keep skills reachable.
633
+ */
634
+ export function selectTools(selection = {}) {
635
+ const { tools, toolsets, readOnly } = selection;
636
+ const unknownTools = (tools ?? []).filter((name) => !TOOL_MAP.has(name));
637
+ if (unknownTools.length > 0) {
638
+ throw new Error(`Unknown Critical Path tool(s): ${unknownTools.join(', ')}. Known tools: ${ALL_TOOLS.map((t) => t.name).join(', ')}.`);
639
+ }
640
+ const unknownSets = (toolsets ?? []).filter((name) => !(name in TOOLSETS));
641
+ if (unknownSets.length > 0) {
642
+ throw new Error(`Unknown Critical Path toolset(s): ${unknownSets.join(', ')}. Known toolsets: ${Object.keys(TOOLSETS).join(', ')}.`);
643
+ }
644
+ const filtered = !tools && !toolsets;
645
+ return ALL_TOOLS.filter((tool) => (filtered || tools?.includes(tool.name) || toolsets?.includes(tool.toolset)) &&
646
+ (!readOnly || tool.annotations.readOnlyHint));
647
+ }
648
+ export class ToolNotFoundError extends Error {
649
+ constructor(message) {
650
+ super(message);
651
+ this.name = 'ToolNotFoundError';
652
+ }
653
+ }
654
+ const ID_RECOVERY = 'Check the id: find valid project ids with `list_projects` and task ids with `list_tasks`. Ids are never guessable, and items you cannot read are reported as not found.';
655
+ /**
656
+ * Validates arguments, runs a tool and returns a JSON-safe object result. A missing record (a
657
+ * `null` result) becomes an error, and errors carry a hint for what to do next so the agent can
658
+ * recover on its own.
659
+ */
660
+ export async function runTool(tool, args, target, ambientContext) {
661
+ try {
662
+ const result = await tool.execute(parseToolArgs(tool, args), target, ambientContext);
663
+ if (result === null || result === undefined) {
664
+ throw new ToolNotFoundError(`Nothing found for ${JSON.stringify(args ?? {})}.`);
665
+ }
666
+ // Round-trip through JSON: drops undefined fields and serializes dates, as a transport would.
667
+ return JSON.parse(JSON.stringify(result));
668
+ }
669
+ catch (err) {
670
+ throw new Error(withRecoveryHint(tool, err));
671
+ }
672
+ }
673
+ function withRecoveryHint(tool, err) {
674
+ const message = err?.message || String(err);
675
+ if (err instanceof ToolArgumentError) {
676
+ return `${message}. Fix the arguments to match the input schema of "${tool.name}" and call it again.`;
677
+ }
678
+ if (err instanceof ToolNotFoundError || err?.name === 'NotFoundError' || err?.status === 404 || /not found/i.test(message)) {
679
+ return `${message} ${ID_RECOVERY}`;
680
+ }
681
+ if (err?.name === 'CircularDependencyError' || /circular/i.test(message)) {
682
+ return `${message} The tasks are already ordered the other way round. Check with \`get_task\` and includeDependencies, and ask the user which order is right rather than retrying.`;
683
+ }
684
+ if (err?.name === 'ForbiddenError' || err?.status === 403) {
685
+ return `${message} You do not have permission for this; tell the user rather than retrying.`;
686
+ }
687
+ return message;
688
+ }
445
689
  //# sourceMappingURL=definitions.js.map