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.
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +28 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/browser-skill-endstate.test.ts +1 -5
- package/src/__tests__/call-orchestrator.test.ts +328 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +476 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +409 -0
- package/src/__tests__/config-schema.test.ts +49 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/ipc-snapshot.test.ts +34 -0
- package/src/__tests__/registry.test.ts +13 -8
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +218 -0
- package/src/__tests__/run-orchestrator.test.ts +3 -3
- package/src/__tests__/runtime-attachment-metadata.test.ts +17 -19
- package/src/__tests__/runtime-runs-http.test.ts +1 -19
- package/src/__tests__/runtime-runs.test.ts +7 -7
- package/src/__tests__/session-queue.test.ts +50 -0
- package/src/__tests__/turn-commit.test.ts +56 -0
- package/src/__tests__/workspace-git-service.test.ts +217 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +129 -0
- package/src/bundler/app-bundler.ts +29 -12
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-orchestrator.ts +364 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +229 -0
- package/src/calls/relay-server.ts +298 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +169 -0
- package/src/calls/twilio-routes.ts +236 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/doordash.ts +5 -24
- package/src/config/bundled-skills/doordash/SKILL.md +104 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -1
- package/src/config/defaults.ts +11 -0
- package/src/config/schema.ts +57 -0
- package/src/config/system-prompt.ts +50 -1
- package/src/config/types.ts +1 -0
- package/src/daemon/handlers/config.ts +30 -0
- package/src/daemon/handlers/index.ts +6 -0
- package/src/daemon/handlers/work-items.ts +142 -2
- package/src/daemon/ipc-contract-inventory.json +12 -0
- package/src/daemon/ipc-contract.ts +52 -0
- package/src/daemon/lifecycle.ts +27 -5
- package/src/daemon/server.ts +10 -12
- package/src/daemon/session-tool-setup.ts +6 -0
- package/src/daemon/session.ts +40 -1
- package/src/index.ts +2 -0
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/db.ts +266 -0
- package/src/memory/schema.ts +42 -0
- package/src/runtime/http-server.ts +189 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +6 -6
- package/src/runtime/routes/channel-routes.ts +16 -18
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +32 -5
- package/src/tools/calls/call-end.ts +117 -0
- package/src/tools/calls/call-start.ts +134 -0
- package/src/tools/calls/call-status.ts +97 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/registry.ts +2 -4
- package/src/tools/tasks/index.ts +2 -0
- package/src/tools/tasks/task-delete.ts +49 -8
- package/src/tools/tasks/task-run.ts +9 -1
- package/src/tools/tasks/work-item-enqueue.ts +93 -3
- package/src/tools/tasks/work-item-list.ts +10 -25
- package/src/tools/tasks/work-item-remove.ts +112 -0
- package/src/tools/tasks/work-item-update.ts +186 -0
- package/src/tools/tool-manifest.ts +39 -31
- package/src/tools/ui-surface/definitions.ts +3 -0
- package/src/work-items/work-item-store.ts +209 -0
- package/src/workspace/commit-message-enrichment-service.ts +260 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +187 -32
- package/src/workspace/heartbeat-service.ts +70 -13
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
51
|
+
const count = items.length;
|
|
52
|
+
const filtered = statusFilter !== undefined;
|
|
60
53
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
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. ' +
|