vellum 0.2.0 → 0.2.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 (80) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +28 -0
  3. package/src/__tests__/app-bundler.test.ts +12 -33
  4. package/src/__tests__/browser-skill-endstate.test.ts +1 -5
  5. package/src/__tests__/call-orchestrator.test.ts +328 -0
  6. package/src/__tests__/call-state.test.ts +133 -0
  7. package/src/__tests__/call-store.test.ts +476 -0
  8. package/src/__tests__/commit-message-enrichment-service.test.ts +409 -0
  9. package/src/__tests__/config-schema.test.ts +49 -0
  10. package/src/__tests__/doordash-session.test.ts +9 -0
  11. package/src/__tests__/ipc-snapshot.test.ts +34 -0
  12. package/src/__tests__/registry.test.ts +13 -8
  13. package/src/__tests__/run-orchestrator-assistant-events.test.ts +218 -0
  14. package/src/__tests__/run-orchestrator.test.ts +3 -3
  15. package/src/__tests__/runtime-attachment-metadata.test.ts +17 -19
  16. package/src/__tests__/runtime-runs-http.test.ts +1 -19
  17. package/src/__tests__/runtime-runs.test.ts +7 -7
  18. package/src/__tests__/session-queue.test.ts +50 -0
  19. package/src/__tests__/turn-commit.test.ts +56 -0
  20. package/src/__tests__/workspace-git-service.test.ts +217 -0
  21. package/src/__tests__/workspace-heartbeat-service.test.ts +129 -0
  22. package/src/bundler/app-bundler.ts +29 -12
  23. package/src/calls/call-constants.ts +10 -0
  24. package/src/calls/call-orchestrator.ts +364 -0
  25. package/src/calls/call-state.ts +64 -0
  26. package/src/calls/call-store.ts +229 -0
  27. package/src/calls/relay-server.ts +298 -0
  28. package/src/calls/twilio-config.ts +34 -0
  29. package/src/calls/twilio-provider.ts +169 -0
  30. package/src/calls/twilio-routes.ts +236 -0
  31. package/src/calls/types.ts +37 -0
  32. package/src/calls/voice-provider.ts +14 -0
  33. package/src/cli/doordash.ts +5 -24
  34. package/src/config/bundled-skills/doordash/SKILL.md +104 -0
  35. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  36. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -1
  37. package/src/config/defaults.ts +11 -0
  38. package/src/config/schema.ts +57 -0
  39. package/src/config/system-prompt.ts +50 -1
  40. package/src/config/types.ts +1 -0
  41. package/src/daemon/handlers/config.ts +30 -0
  42. package/src/daemon/handlers/index.ts +6 -0
  43. package/src/daemon/handlers/work-items.ts +142 -2
  44. package/src/daemon/ipc-contract-inventory.json +12 -0
  45. package/src/daemon/ipc-contract.ts +52 -0
  46. package/src/daemon/lifecycle.ts +27 -5
  47. package/src/daemon/server.ts +10 -12
  48. package/src/daemon/session-tool-setup.ts +6 -0
  49. package/src/daemon/session.ts +40 -1
  50. package/src/index.ts +2 -0
  51. package/src/media/gemini-image-service.ts +1 -1
  52. package/src/memory/db.ts +266 -0
  53. package/src/memory/schema.ts +42 -0
  54. package/src/runtime/http-server.ts +189 -25
  55. package/src/runtime/http-types.ts +0 -2
  56. package/src/runtime/routes/attachment-routes.ts +6 -6
  57. package/src/runtime/routes/channel-routes.ts +16 -18
  58. package/src/runtime/routes/conversation-routes.ts +5 -9
  59. package/src/runtime/routes/run-routes.ts +4 -8
  60. package/src/runtime/run-orchestrator.ts +32 -5
  61. package/src/tools/calls/call-end.ts +117 -0
  62. package/src/tools/calls/call-start.ts +134 -0
  63. package/src/tools/calls/call-status.ts +97 -0
  64. package/src/tools/credentials/vault.ts +1 -1
  65. package/src/tools/registry.ts +2 -4
  66. package/src/tools/tasks/index.ts +2 -0
  67. package/src/tools/tasks/task-delete.ts +49 -8
  68. package/src/tools/tasks/task-run.ts +9 -1
  69. package/src/tools/tasks/work-item-enqueue.ts +93 -3
  70. package/src/tools/tasks/work-item-list.ts +10 -25
  71. package/src/tools/tasks/work-item-remove.ts +112 -0
  72. package/src/tools/tasks/work-item-update.ts +186 -0
  73. package/src/tools/tool-manifest.ts +39 -31
  74. package/src/tools/ui-surface/definitions.ts +3 -0
  75. package/src/work-items/work-item-store.ts +209 -0
  76. package/src/workspace/commit-message-enrichment-service.ts +260 -0
  77. package/src/workspace/commit-message-provider.ts +95 -0
  78. package/src/workspace/git-service.ts +187 -32
  79. package/src/workspace/heartbeat-service.ts +70 -13
  80. package/src/workspace/turn-commit.ts +39 -49
@@ -2,7 +2,10 @@ import { RiskLevel } from '../../permissions/types.js';
2
2
  import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
3
  import type { ToolDefinition } from '../../providers/types.js';
4
4
  import { getTask, listTasks, createTask } from '../../tasks/task-store.js';
5
- import { createWorkItem } from '../../work-items/work-item-store.js';
5
+ import { createWorkItem, findActiveWorkItemsByTitle, updateWorkItem, identifyEntityById, buildWorkItemMismatchError } from '../../work-items/work-item-store.js';
6
+ import { getLogger } from '../../util/logger.js';
7
+
8
+ const log = getLogger('task-list-add');
6
9
 
7
10
  const PRIORITY_LABELS: Record<number, string> = {
8
11
  0: 'high',
@@ -43,6 +46,12 @@ const definition: ToolDefinition = {
43
46
  type: 'number',
44
47
  description: 'Manual sort order within the priority tier.',
45
48
  },
49
+ if_exists: {
50
+ type: 'string',
51
+ enum: ['create_duplicate', 'reuse_existing', 'update_existing'],
52
+ description:
53
+ 'What to do if an active work item with the same title already exists. Defaults to "reuse_existing".',
54
+ },
46
55
  },
47
56
  },
48
57
  };
@@ -57,6 +66,50 @@ class TaskListAddTool implements Tool {
57
66
  return definition;
58
67
  }
59
68
 
69
+ /**
70
+ * Check for an existing active work item with the same title and handle
71
+ * according to the if_exists strategy. Returns a result if a duplicate was
72
+ * found and handled, or null if no duplicate exists (caller should proceed).
73
+ */
74
+ private handleDuplicate(
75
+ title: string,
76
+ ifExists: string,
77
+ input: Record<string, unknown>,
78
+ ): ToolExecutionResult | null {
79
+ const existing = findActiveWorkItemsByTitle(title);
80
+ if (existing.length === 0) return null;
81
+
82
+ const match = existing[0];
83
+ log.info({ title, existingId: match.id, ifExists }, 'task_list_add: duplicate detected');
84
+
85
+ if (ifExists === 'reuse_existing') {
86
+ log.info({ title, existingId: match.id }, 'task_list_add: reused existing item');
87
+ return {
88
+ content: `Task "${match.title}" already exists in the queue (ID: ${match.id}, status: ${match.status}). Use task_list_update to modify it.`,
89
+ isError: false,
90
+ };
91
+ }
92
+
93
+ if (ifExists === 'update_existing') {
94
+ const updates: Partial<{ title: string; notes: string; priorityTier: number; sortIndex: number }> = {};
95
+ if (input.priority_tier !== undefined) updates.priorityTier = input.priority_tier as number;
96
+ if (input.notes !== undefined) updates.notes = input.notes as string;
97
+ if (input.sort_index !== undefined) updates.sortIndex = input.sort_index as number;
98
+ if (Object.keys(updates).length > 0) {
99
+ updateWorkItem(match.id, updates);
100
+ }
101
+ log.info({ title, existingId: match.id, updates }, 'task_list_add: updated existing item');
102
+ return {
103
+ content: `Reused existing task "${match.title}" (ID: ${match.id}) instead of creating a duplicate.${
104
+ Object.keys(updates).length > 0 ? ` Updated: ${Object.keys(updates).join(', ')}.` : ''
105
+ }`,
106
+ isError: false,
107
+ };
108
+ }
109
+
110
+ return null;
111
+ }
112
+
60
113
  async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
61
114
  try {
62
115
  const taskId = input.task_id as string | undefined;
@@ -66,6 +119,8 @@ class TaskListAddTool implements Tool {
66
119
  const priorityTier = input.priority_tier as number | undefined;
67
120
  const sortIndex = input.sort_index as number | undefined;
68
121
 
122
+ const ifExists = (input.if_exists as string) || 'reuse_existing';
123
+
69
124
  // Ad-hoc mode: title provided without task_id or task_name
70
125
  if (!taskId && !taskName) {
71
126
  if (!titleOverride) {
@@ -75,6 +130,16 @@ class TaskListAddTool implements Tool {
75
130
  };
76
131
  }
77
132
 
133
+ // Duplicate-prevention guard
134
+ if (ifExists !== 'create_duplicate') {
135
+ const duplicateResult = this.handleDuplicate(titleOverride, ifExists, input);
136
+ if (duplicateResult) return duplicateResult;
137
+ } else {
138
+ log.info({ title: titleOverride }, 'task_list_add: creating duplicate (if_exists=create_duplicate)');
139
+ }
140
+
141
+ log.debug({ title: titleOverride }, 'task_list_add: creating new item');
142
+
78
143
  // Auto-create a lightweight task template for the ad-hoc item
79
144
  const adHocTask = createTask({
80
145
  title: titleOverride,
@@ -89,6 +154,8 @@ class TaskListAddTool implements Tool {
89
154
  sortIndex,
90
155
  });
91
156
 
157
+ log.info({ selectorType: 'title', workItemId: workItem.id, title: workItem.title }, 'ad-hoc work item created');
158
+
92
159
  const priority = PRIORITY_LABELS[workItem.priorityTier] ?? `tier ${workItem.priorityTier}`;
93
160
  const lines = [
94
161
  `Enqueued work item:`,
@@ -112,7 +179,14 @@ class TaskListAddTool implements Tool {
112
179
  if (taskId) {
113
180
  resolvedTask = getTask(taskId);
114
181
  if (!resolvedTask) {
115
- return { content: `Error: No task definition found with ID "${taskId}".`, isError: true };
182
+ const entity = identifyEntityById(taskId);
183
+ if (entity.type === 'work_item') {
184
+ return {
185
+ content: `Error: ${buildWorkItemMismatchError(taskId, entity.title!, 'task_list_update to modify the existing work item, or task_list_add with just a title for a new ad-hoc item')}`,
186
+ isError: true,
187
+ };
188
+ }
189
+ return { content: `Error: No task definition found with ID "${taskId}". Use task_list to see available task templates, or provide just a title to create an ad-hoc work item.`, isError: true };
116
190
  }
117
191
  } else {
118
192
  // Search by name (case-insensitive substring match)
@@ -141,14 +215,29 @@ class TaskListAddTool implements Tool {
141
215
  resolvedTask = matches[0];
142
216
  }
143
217
 
218
+ const finalTitle = titleOverride ?? resolvedTask.title;
219
+
220
+ // Duplicate-prevention guard
221
+ if (ifExists !== 'create_duplicate') {
222
+ const duplicateResult = this.handleDuplicate(finalTitle, ifExists, input);
223
+ if (duplicateResult) return duplicateResult;
224
+ } else {
225
+ log.info({ title: finalTitle }, 'task_list_add: creating duplicate (if_exists=create_duplicate)');
226
+ }
227
+
228
+ log.debug({ title: finalTitle }, 'task_list_add: creating new item');
229
+
230
+ const selectorType = taskId ? 'task_id' : 'task_name';
144
231
  const workItem = createWorkItem({
145
232
  taskId: resolvedTask.id,
146
- title: titleOverride ?? resolvedTask.title,
233
+ title: finalTitle,
147
234
  notes,
148
235
  priorityTier: priorityTier ?? 1,
149
236
  sortIndex,
150
237
  });
151
238
 
239
+ log.info({ selectorType, taskId: resolvedTask.id, workItemId: workItem.id, title: workItem.title }, 'work item created from task definition');
240
+
152
241
  const priority = PRIORITY_LABELS[workItem.priorityTier] ?? `tier ${workItem.priorityTier}`;
153
242
  const lines = [
154
243
  `Enqueued work item:`,
@@ -168,6 +257,7 @@ class TaskListAddTool implements Tool {
168
257
  return { content: lines.join('\n'), isError: false };
169
258
  } catch (err) {
170
259
  const msg = err instanceof Error ? err.message : String(err);
260
+ log.error({ error: msg }, 'enqueue failed');
171
261
  return { content: `Error: ${msg}`, isError: true };
172
262
  }
173
263
  }
@@ -3,12 +3,6 @@ import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
3
  import type { ToolDefinition } from '../../providers/types.js';
4
4
  import { listWorkItems, type WorkItemStatus } from '../../work-items/work-item-store.js';
5
5
 
6
- const PRIORITY_LABELS: Record<number, string> = {
7
- 0: 'high',
8
- 1: 'medium',
9
- 2: 'low',
10
- };
11
-
12
6
  const definition: ToolDefinition = {
13
7
  name: 'task_list_show',
14
8
  description: 'List the user\'s Task Queue (work items) with their status, priority, and last run info. Use this when the user says "show my tasks", "what\'s in my queue", "what\'s on my task list", or similar.',
@@ -54,28 +48,19 @@ class TaskListShowTool implements Tool {
54
48
  items = listWorkItems();
55
49
  }
56
50
 
57
- if (items.length === 0) {
58
- return { content: 'No Tasks found.', isError: false };
59
- }
51
+ const count = items.length;
52
+ const filtered = statusFilter !== undefined;
60
53
 
61
- const lines = [`Found ${items.length} work item(s):`, ''];
62
-
63
- for (const item of items) {
64
- const priority = PRIORITY_LABELS[item.priorityTier] ?? `tier ${item.priorityTier}`;
65
- lines.push(`- ${item.title}`);
66
- lines.push(` ID: ${item.id}`);
67
- lines.push(` Status: ${item.status}`);
68
- lines.push(` Priority: ${priority}`);
69
- if (item.notes) {
70
- lines.push(` Notes: ${item.notes}`);
71
- }
72
- if (item.lastRunStatus) {
73
- lines.push(` Last run: ${item.lastRunStatus}`);
74
- }
75
- lines.push('');
54
+ if (count === 0) {
55
+ const suffix = filtered ? 'no items matching filter.' : 'no tasks queued.';
56
+ return { content: `Opened Tasks window \u2014 ${suffix}`, isError: false };
76
57
  }
77
58
 
78
- return { content: lines.join('\n'), isError: false };
59
+ const label = filtered
60
+ ? `${count} ${Array.isArray(statusFilter) ? 'matching' : statusFilter} item${count === 1 ? '' : 's'}`
61
+ : `${count} item${count === 1 ? '' : 's'}`;
62
+
63
+ return { content: `Opened Tasks window (${label}).`, isError: false };
79
64
  } catch (err) {
80
65
  const msg = err instanceof Error ? err.message : String(err);
81
66
  return { content: `Error: ${msg}`, isError: true };
@@ -0,0 +1,112 @@
1
+ import { RiskLevel } from '../../permissions/types.js';
2
+ import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
+ import type { ToolDefinition } from '../../providers/types.js';
4
+ import { resolveWorkItem, removeWorkItemFromQueue, identifyEntityById, buildTaskTemplateMismatchError, type WorkItemStatus } from '../../work-items/work-item-store.js';
5
+ import { getLogger } from '../../util/logger.js';
6
+
7
+ const log = getLogger('task-list-remove');
8
+
9
+ const definition: ToolDefinition = {
10
+ name: 'task_list_remove',
11
+ description:
12
+ 'Remove a task from the Task Queue. Identifies the task by work item ID, task ID, task name, or title. When multiple items match, use the disambiguation fields (priority_tier, status, created_order) to narrow down.',
13
+ input_schema: {
14
+ type: 'object',
15
+ properties: {
16
+ work_item_id: {
17
+ type: 'string',
18
+ description: 'Direct work item ID (most precise selector)',
19
+ },
20
+ task_id: {
21
+ type: 'string',
22
+ description: 'Task definition ID to find the work item for',
23
+ },
24
+ task_name: {
25
+ type: 'string',
26
+ description: 'Task name/title to search for (case-insensitive exact match)',
27
+ },
28
+ title: {
29
+ type: 'string',
30
+ description: 'Work item title to search for (case-insensitive exact match)',
31
+ },
32
+ priority_tier: {
33
+ type: 'number',
34
+ description: 'Disambiguator: filter by priority tier (0 = high, 1 = medium, 2 = low)',
35
+ },
36
+ status: {
37
+ type: 'string',
38
+ enum: ['queued', 'running', 'awaiting_review', 'failed'],
39
+ description: 'Disambiguator: filter by work item status',
40
+ },
41
+ created_order: {
42
+ type: 'number',
43
+ description: 'Disambiguator: 1-indexed creation order among matches (1 = oldest, 2 = second oldest, etc.)',
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ class TaskListRemoveTool implements Tool {
50
+ name = 'task_list_remove';
51
+ description = definition.description;
52
+ category = 'tasks';
53
+ defaultRiskLevel = RiskLevel.Low;
54
+
55
+ getDefinition(): ToolDefinition {
56
+ return definition;
57
+ }
58
+
59
+ async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
60
+ const selectorType = input.work_item_id ? 'work_item_id' : input.task_id ? 'task_id' : input.task_name ? 'task_name' : input.title ? 'title' : 'none';
61
+
62
+ try {
63
+ const selector = {
64
+ workItemId: input.work_item_id as string | undefined,
65
+ taskId: input.task_id as string | undefined,
66
+ title: (input.task_name ?? input.title) as string | undefined,
67
+ priorityTier: input.priority_tier as number | undefined,
68
+ status: input.status as WorkItemStatus | undefined,
69
+ createdOrder: input.created_order as number | undefined,
70
+ };
71
+
72
+ const resolveResult = resolveWorkItem(selector);
73
+
74
+ if (resolveResult.status === 'not_found') {
75
+ // When the model passes an ID directly, check if it's a task template
76
+ if (selector.workItemId) {
77
+ const entity = identifyEntityById(selector.workItemId);
78
+ if (entity.type === 'task_template') {
79
+ log.warn({ selectorType, inputId: selector.workItemId }, 'task template ID passed as work_item_id');
80
+ return {
81
+ content: `Error: ${buildTaskTemplateMismatchError(selector.workItemId, entity.title!, 'task_delete to delete task templates')}`,
82
+ isError: true,
83
+ };
84
+ }
85
+ }
86
+ log.warn({ selectorType, error: resolveResult.message }, 'work item not found for removal');
87
+ return { content: `Error: ${resolveResult.message}`, isError: true };
88
+ }
89
+
90
+ if (resolveResult.status === 'ambiguous') {
91
+ log.warn({ selectorType, matchCount: resolveResult.matches.length }, 'ambiguous selector for removal');
92
+ return { content: `Error: ${resolveResult.message}`, isError: true };
93
+ }
94
+
95
+ const item = resolveResult.workItem;
96
+
97
+ log.info({ selectorType, selectorValue: input[selectorType], resolvedWorkItemId: item.id, title: item.title }, 'resolved work item for removal');
98
+
99
+ const removeResult = removeWorkItemFromQueue(item.id);
100
+
101
+ log.info({ resolvedWorkItemId: item.id, deletedCount: 1 }, 'work item removed');
102
+
103
+ return { content: removeResult.message, isError: false };
104
+ } catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ log.error({ selectorType, error: msg }, 'remove failed');
107
+ return { content: `Error: ${msg}`, isError: true };
108
+ }
109
+ }
110
+ }
111
+
112
+ export const taskListRemoveTool = new TaskListRemoveTool();
@@ -0,0 +1,186 @@
1
+ import { RiskLevel } from '../../permissions/types.js';
2
+ import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
+ import type { ToolDefinition } from '../../providers/types.js';
4
+ import { resolveWorkItem, updateWorkItem, identifyEntityById, buildTaskTemplateMismatchError, type WorkItemStatus } from '../../work-items/work-item-store.js';
5
+ import { getLogger } from '../../util/logger.js';
6
+
7
+ const log = getLogger('task-list-update');
8
+
9
+ const PRIORITY_LABELS: Record<number, string> = {
10
+ 0: 'high',
11
+ 1: 'medium',
12
+ 2: 'low',
13
+ };
14
+
15
+ const definition: ToolDefinition = {
16
+ name: 'task_list_update',
17
+ description:
18
+ 'Update an existing task in the Task Queue. Can change priority, notes, status, or sort order. Identifies the task by work item ID, task ID, task name, or title.',
19
+ input_schema: {
20
+ type: 'object',
21
+ properties: {
22
+ work_item_id: {
23
+ type: 'string',
24
+ description: 'Direct work item ID (most precise selector)',
25
+ },
26
+ task_id: {
27
+ type: 'string',
28
+ description: 'Task definition ID to find the work item for',
29
+ },
30
+ task_name: {
31
+ type: 'string',
32
+ description: 'Task name/title to search for (case-insensitive exact match)',
33
+ },
34
+ title: {
35
+ type: 'string',
36
+ description: 'Work item title to search for (case-insensitive exact match)',
37
+ },
38
+ priority_tier: {
39
+ type: 'number',
40
+ description: '0 = high, 1 = medium, 2 = low',
41
+ },
42
+ notes: {
43
+ type: 'string',
44
+ description: 'Updated notes for the work item',
45
+ },
46
+ status: {
47
+ type: 'string',
48
+ enum: ['queued', 'running', 'awaiting_review', 'failed', 'archived'],
49
+ description: 'New status for the work item',
50
+ },
51
+ sort_index: {
52
+ type: 'number',
53
+ description: 'Manual sort order within the same priority tier',
54
+ },
55
+ filter_priority_tier: {
56
+ type: 'number',
57
+ description:
58
+ 'Disambiguation filter: narrow by current priority tier (0=high, 1=medium, 2=low) when multiple items share the same title/task_id. This identifies WHICH item to update — it is NOT the new priority value.',
59
+ },
60
+ filter_status: {
61
+ type: 'string',
62
+ enum: ['queued', 'running', 'awaiting_review', 'failed', 'done', 'archived'],
63
+ description:
64
+ 'Disambiguation filter: narrow by current status when multiple items share the same title/task_id.',
65
+ },
66
+ created_order: {
67
+ type: 'number',
68
+ description:
69
+ 'Disambiguation filter: pick the Nth oldest match (1 = oldest, 2 = second oldest, etc.) when multiple items share the same title/task_id.',
70
+ },
71
+ },
72
+ },
73
+ };
74
+
75
+ class TaskListUpdateTool implements Tool {
76
+ name = 'task_list_update';
77
+ description = definition.description;
78
+ category = 'tasks';
79
+ defaultRiskLevel = RiskLevel.Low;
80
+
81
+ getDefinition(): ToolDefinition {
82
+ return definition;
83
+ }
84
+
85
+ async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
86
+ const selectorType = input.work_item_id ? 'work_item_id' : input.task_id ? 'task_id' : input.task_name ? 'task_name' : input.title ? 'title' : 'none';
87
+
88
+ try {
89
+ // Build selector from whichever identifier was provided
90
+ const selector = {
91
+ workItemId: input.work_item_id as string | undefined,
92
+ taskId: input.task_id as string | undefined,
93
+ title: (input.task_name ?? input.title) as string | undefined,
94
+ priorityTier: input.filter_priority_tier as number | undefined,
95
+ status: input.filter_status as WorkItemStatus | undefined,
96
+ createdOrder: input.created_order as number | undefined,
97
+ };
98
+
99
+ // Resolve the target work item
100
+ const result = resolveWorkItem(selector);
101
+
102
+ if (result.status === 'not_found') {
103
+ // When the model passes an ID directly, check if it's a task template
104
+ if (selector.workItemId) {
105
+ const entity = identifyEntityById(selector.workItemId);
106
+ if (entity.type === 'task_template') {
107
+ log.warn({ selectorType, inputId: selector.workItemId }, 'task template ID passed as work_item_id');
108
+ return {
109
+ content: `Error: ${buildTaskTemplateMismatchError(selector.workItemId, entity.title!, 'task_delete to remove task templates, or task_list to view them')}`,
110
+ isError: true,
111
+ };
112
+ }
113
+ }
114
+ log.warn({ selectorType, error: result.message }, 'work item not found for update');
115
+ return { content: `Error: ${result.message}`, isError: true };
116
+ }
117
+
118
+ if (result.status === 'ambiguous') {
119
+ log.warn({ selectorType, matchCount: result.matches.length }, 'ambiguous selector for update');
120
+ return { content: `Error: ${result.message}`, isError: true };
121
+ }
122
+
123
+ const item = result.workItem;
124
+
125
+ // Block direct transitions to 'done' — the only path to done is
126
+ // through the Review action (handleWorkItemComplete in the daemon).
127
+ if (input.status === 'done') {
128
+ log.warn({ selectorType, resolvedWorkItemId: item.id }, 'rejected attempt to set status to done directly');
129
+ return {
130
+ content: 'Error: Cannot set status to \'done\' directly. Use the Review action in the Tasks window.',
131
+ isError: true,
132
+ };
133
+ }
134
+
135
+ log.info({ selectorType, selectorValue: input[selectorType], resolvedWorkItemId: item.id }, 'resolved work item for update');
136
+
137
+ // Build updates from provided fields
138
+ const updates: Partial<{
139
+ priorityTier: number;
140
+ notes: string;
141
+ status: WorkItemStatus;
142
+ sortIndex: number;
143
+ }> = {};
144
+ if (input.priority_tier !== undefined) updates.priorityTier = input.priority_tier as number;
145
+ if (input.notes !== undefined) updates.notes = input.notes as string;
146
+ if (input.status !== undefined) updates.status = input.status as WorkItemStatus;
147
+ if (input.sort_index !== undefined) updates.sortIndex = input.sort_index as number;
148
+
149
+ if (Object.keys(updates).length === 0) {
150
+ log.warn({ selectorType, resolvedWorkItemId: item.id }, 'update called with no fields to update');
151
+ return {
152
+ content: 'No updates specified. Provide at least one field to update (priority_tier, notes, status, sort_index).',
153
+ isError: true,
154
+ };
155
+ }
156
+
157
+ const updated = updateWorkItem(item.id, updates);
158
+ if (!updated) {
159
+ log.error({ selectorType, resolvedWorkItemId: item.id, updates }, 'updateWorkItem returned null');
160
+ return {
161
+ content: `Error: Failed to update work item "${item.title}".`,
162
+ isError: true,
163
+ };
164
+ }
165
+
166
+ log.info({ resolvedWorkItemId: item.id, updatedFields: Object.keys(updates) }, 'work item updated');
167
+
168
+ // Build confirmation message
169
+ const parts: string[] = [`Updated "${updated.title}"`];
170
+ if (input.priority_tier !== undefined) {
171
+ parts.push(`priority → ${PRIORITY_LABELS[updated.priorityTier] ?? updated.priorityTier}`);
172
+ }
173
+ if (input.notes !== undefined) parts.push('notes updated');
174
+ if (input.status !== undefined) parts.push(`status → ${updated.status}`);
175
+ if (input.sort_index !== undefined) parts.push(`sort index → ${updated.sortIndex}`);
176
+
177
+ return { content: parts.join(', ') + '.', isError: false };
178
+ } catch (err) {
179
+ const msg = err instanceof Error ? err.message : String(err);
180
+ log.error({ selectorType, error: msg }, 'update failed');
181
+ return { content: `Error: ${msg}`, isError: true };
182
+ }
183
+ }
184
+ }
185
+
186
+ export const taskListUpdateTool = new TaskListUpdateTool();
@@ -18,7 +18,7 @@ import { vellumSkillsCatalogTool } from './skills/vellum-catalog.js';
18
18
  import { documentCreateTool, documentUpdateTool } from './document/index.js';
19
19
  import { cliDiscoverTool } from './host-terminal/cli-discover.js';
20
20
  import { followupCreateTool, followupListTool, followupResolveTool } from './followups/index.js';
21
- import { taskSaveTool, taskRunTool, taskListTool, taskDeleteTool, taskListShowTool, taskListAddTool } from './tasks/index.js';
21
+ import { taskSaveTool, taskRunTool, taskListTool, taskDeleteTool, taskListShowTool, taskListAddTool, taskListUpdateTool, taskListRemoveTool } from './tasks/index.js';
22
22
  import {
23
23
  subagentSpawnTool,
24
24
  subagentStatusTool,
@@ -30,36 +30,39 @@ import {
30
30
  // ── Eager side-effect modules ───────────────────────────────────────
31
31
  // Importing these modules triggers a top-level `registerTool()` call.
32
32
 
33
- export const eagerModules: string[] = [
34
- './filesystem/read.js',
35
- './filesystem/write.js',
36
- './filesystem/edit.js',
37
- './network/web-search.js',
38
- './network/web-fetch.js',
39
- './skills/load.js',
40
- './skills/scaffold-managed.js',
41
- './skills/delete-managed.js',
42
- './system/request-permission.js',
43
- './schedule/create.js',
44
- './schedule/list.js',
45
- './schedule/update.js',
46
- './schedule/delete.js',
47
- './watcher/create.js',
48
- './watcher/list.js',
49
- './watcher/update.js',
50
- './watcher/delete.js',
51
- './watcher/digest.js',
52
- './playbooks/playbook-create.js',
53
- './playbooks/playbook-list.js',
54
- './playbooks/playbook-update.js',
55
- './playbooks/playbook-delete.js',
56
- './contacts/contact-upsert.js',
57
- './contacts/contact-search.js',
58
- './contacts/contact-merge.js',
59
- './assets/search.js',
60
- './assets/materialize.js',
61
- './filesystem/view-image.js',
62
- ];
33
+ export async function loadEagerModules(): Promise<void> {
34
+ await import('./filesystem/read.js');
35
+ await import('./filesystem/write.js');
36
+ await import('./filesystem/edit.js');
37
+ await import('./network/web-search.js');
38
+ await import('./network/web-fetch.js');
39
+ await import('./skills/load.js');
40
+ await import('./skills/scaffold-managed.js');
41
+ await import('./skills/delete-managed.js');
42
+ await import('./system/request-permission.js');
43
+ await import('./schedule/create.js');
44
+ await import('./schedule/list.js');
45
+ await import('./schedule/update.js');
46
+ await import('./schedule/delete.js');
47
+ await import('./watcher/create.js');
48
+ await import('./watcher/list.js');
49
+ await import('./watcher/update.js');
50
+ await import('./watcher/delete.js');
51
+ await import('./watcher/digest.js');
52
+ await import('./playbooks/playbook-create.js');
53
+ await import('./playbooks/playbook-list.js');
54
+ await import('./playbooks/playbook-update.js');
55
+ await import('./playbooks/playbook-delete.js');
56
+ await import('./contacts/contact-upsert.js');
57
+ await import('./contacts/contact-search.js');
58
+ await import('./contacts/contact-merge.js');
59
+ await import('./assets/search.js');
60
+ await import('./assets/materialize.js');
61
+ await import('./filesystem/view-image.js');
62
+ await import('./calls/call-start.js');
63
+ await import('./calls/call-status.js');
64
+ await import('./calls/call-end.js');
65
+ }
63
66
 
64
67
  // Tool names registered by the eager modules above. Listed explicitly so
65
68
  // initializeTools() can recognise ESM-cached eager-module tools that were
@@ -94,6 +97,9 @@ export const eagerModuleToolNames: string[] = [
94
97
  'asset_search',
95
98
  'asset_materialize',
96
99
  'view_image',
100
+ 'call_start',
101
+ 'call_status',
102
+ 'call_end',
97
103
  ];
98
104
 
99
105
  // ── Explicit tool instances ─────────────────────────────────────────
@@ -121,6 +127,8 @@ export const explicitTools: Tool[] = [
121
127
  taskDeleteTool,
122
128
  taskListShowTool,
123
129
  taskListAddTool,
130
+ taskListUpdateTool,
131
+ taskListRemoveTool,
124
132
  subagentSpawnTool,
125
133
  subagentStatusTool,
126
134
  subagentAbortTool,
@@ -35,6 +35,9 @@ export const uiShowTool: Tool = {
35
35
  'templateData shape: { location: string, currentTemp: number, feelsLike: number, unit: "F"|"C", condition: string, humidity: number, windSpeed: number, windDirection: string, ' +
36
36
  'hourly: Array<{ time: string, icon: string (SF Symbol name), temp: number }>, ' +
37
37
  'forecast: Array<{ day: string, icon: string (SF Symbol name), low: number, high: number, precip: number|null, condition: string }> }\n' +
38
+ ' Template "task_progress": renders a live-updating task progress widget showing structured step-by-step progress. ' +
39
+ 'templateData shape: { title: string, status: "in_progress"|"completed"|"failed", ' +
40
+ 'steps: Array<{ label: string, status: "pending"|"in_progress"|"completed"|"failed", detail?: string }> }\n' +
38
41
  '- table: Data table with columns, selectable rows, and action buttons. ' +
39
42
  'data shape: { columns: Array<{ id: string, label: string }>, rows: Array<{ id: string, cells: Record<string, string>, selectable?: boolean, selected?: boolean }>, selectionMode?: "none"|"single"|"multiple", caption?: string }\n' +
40
43
  '- form: Input form with typed fields. ' +