@vellumai/assistant 0.4.30 → 0.4.32

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 (194) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +14 -8
  3. package/README.md +2 -2
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +1 -4
  6. package/package.json +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  8. package/src/__tests__/anthropic-provider.test.ts +86 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/checker.test.ts +37 -98
  11. package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
  12. package/src/__tests__/config-schema.test.ts +6 -14
  13. package/src/__tests__/conflict-policy.test.ts +76 -0
  14. package/src/__tests__/conflict-store.test.ts +14 -20
  15. package/src/__tests__/contacts-tools.test.ts +8 -61
  16. package/src/__tests__/contradiction-checker.test.ts +5 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  19. package/src/__tests__/followup-tools.test.ts +0 -30
  20. package/src/__tests__/gemini-provider.test.ts +79 -1
  21. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  22. package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
  23. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  24. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  25. package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
  26. package/src/__tests__/memory-regressions.test.ts +6 -6
  27. package/src/__tests__/openai-provider.test.ts +82 -0
  28. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  29. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  30. package/src/__tests__/recurrence-types.test.ts +0 -15
  31. package/src/__tests__/registry.test.ts +0 -10
  32. package/src/__tests__/schedule-tools.test.ts +28 -44
  33. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  34. package/src/__tests__/session-agent-loop.test.ts +0 -2
  35. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  36. package/src/__tests__/session-profile-injection.test.ts +0 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  38. package/src/__tests__/session-skill-tools.test.ts +0 -49
  39. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  40. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  41. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  42. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  43. package/src/__tests__/task-management-tools.test.ts +111 -0
  44. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  45. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  46. package/src/__tests__/twilio-config.test.ts +0 -3
  47. package/src/amazon/session.ts +30 -91
  48. package/src/approvals/guardian-decision-primitive.ts +11 -7
  49. package/src/approvals/guardian-request-resolvers.ts +5 -3
  50. package/src/calls/call-controller.ts +423 -571
  51. package/src/calls/finalize-call.ts +20 -0
  52. package/src/calls/relay-access-wait.ts +340 -0
  53. package/src/calls/relay-server.ts +269 -899
  54. package/src/calls/relay-setup-router.ts +307 -0
  55. package/src/calls/relay-verification.ts +280 -0
  56. package/src/calls/twilio-config.ts +1 -8
  57. package/src/calls/voice-control-protocol.ts +184 -0
  58. package/src/calls/voice-session-bridge.ts +1 -8
  59. package/src/config/agent-schema.ts +1 -1
  60. package/src/config/bundled-skills/contacts/SKILL.md +7 -18
  61. package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
  62. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
  63. package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
  64. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
  65. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  66. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  67. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  68. package/src/config/bundled-tool-registry.ts +0 -5
  69. package/src/config/core-schema.ts +1 -1
  70. package/src/config/env.ts +0 -10
  71. package/src/config/feature-flag-registry.json +1 -1
  72. package/src/config/loader.ts +19 -0
  73. package/src/config/memory-schema.ts +0 -10
  74. package/src/config/schema.ts +2 -2
  75. package/src/config/system-prompt.ts +6 -0
  76. package/src/contacts/contact-store.ts +36 -62
  77. package/src/contacts/contacts-write.ts +14 -3
  78. package/src/contacts/types.ts +9 -4
  79. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  80. package/src/daemon/handlers/contacts.ts +2 -2
  81. package/src/daemon/handlers/guardian-actions.ts +1 -1
  82. package/src/daemon/handlers/session-history.ts +398 -0
  83. package/src/daemon/handlers/session-user-message.ts +982 -0
  84. package/src/daemon/handlers/sessions.ts +9 -1337
  85. package/src/daemon/ipc-contract/contacts.ts +2 -2
  86. package/src/daemon/ipc-contract/sessions.ts +0 -6
  87. package/src/daemon/ipc-contract-inventory.json +0 -1
  88. package/src/daemon/lifecycle.ts +0 -29
  89. package/src/daemon/session-agent-loop.ts +1 -45
  90. package/src/daemon/session-conflict-gate.ts +21 -82
  91. package/src/daemon/session-memory.ts +7 -52
  92. package/src/daemon/session-process.ts +3 -1
  93. package/src/daemon/session-runtime-assembly.ts +18 -35
  94. package/src/heartbeat/heartbeat-service.ts +5 -1
  95. package/src/home-base/app-link-store.ts +0 -7
  96. package/src/memory/conflict-intent.ts +3 -6
  97. package/src/memory/conflict-policy.ts +34 -0
  98. package/src/memory/conflict-store.ts +10 -18
  99. package/src/memory/contradiction-checker.ts +2 -2
  100. package/src/memory/conversation-attention-store.ts +1 -1
  101. package/src/memory/conversation-store.ts +0 -51
  102. package/src/memory/db-init.ts +8 -0
  103. package/src/memory/job-handlers/conflict.ts +24 -7
  104. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  105. package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
  106. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  107. package/src/memory/migrations/index.ts +2 -0
  108. package/src/memory/migrations/registry.ts +6 -0
  109. package/src/memory/recall-cache.ts +0 -5
  110. package/src/memory/schema/calls.ts +274 -0
  111. package/src/memory/schema/contacts.ts +125 -0
  112. package/src/memory/schema/conversations.ts +129 -0
  113. package/src/memory/schema/guardian.ts +172 -0
  114. package/src/memory/schema/index.ts +8 -0
  115. package/src/memory/schema/infrastructure.ts +205 -0
  116. package/src/memory/schema/memory-core.ts +196 -0
  117. package/src/memory/schema/notifications.ts +191 -0
  118. package/src/memory/schema/tasks.ts +78 -0
  119. package/src/memory/schema.ts +1 -1402
  120. package/src/memory/slack-thread-store.ts +0 -69
  121. package/src/messaging/index.ts +0 -1
  122. package/src/messaging/types.ts +0 -38
  123. package/src/notifications/decisions-store.ts +2 -105
  124. package/src/notifications/deliveries-store.ts +0 -11
  125. package/src/notifications/preferences-store.ts +1 -58
  126. package/src/permissions/checker.ts +6 -17
  127. package/src/providers/anthropic/client.ts +6 -2
  128. package/src/providers/gemini/client.ts +13 -2
  129. package/src/providers/managed-proxy/constants.ts +55 -0
  130. package/src/providers/managed-proxy/context.ts +77 -0
  131. package/src/providers/registry.ts +112 -0
  132. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
  133. package/src/runtime/guardian-action-service.ts +3 -2
  134. package/src/runtime/guardian-outbound-actions.ts +3 -3
  135. package/src/runtime/guardian-reply-router.ts +4 -4
  136. package/src/runtime/http-server.ts +83 -710
  137. package/src/runtime/http-types.ts +0 -16
  138. package/src/runtime/middleware/auth.ts +0 -12
  139. package/src/runtime/routes/app-routes.ts +33 -0
  140. package/src/runtime/routes/approval-routes.ts +32 -0
  141. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  142. package/src/runtime/routes/attachment-routes.ts +32 -0
  143. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  144. package/src/runtime/routes/call-routes.ts +41 -0
  145. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  146. package/src/runtime/routes/channel-routes.ts +70 -0
  147. package/src/runtime/routes/contact-routes.ts +371 -29
  148. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  149. package/src/runtime/routes/conversation-routes.ts +192 -194
  150. package/src/runtime/routes/debug-routes.ts +15 -0
  151. package/src/runtime/routes/events-routes.ts +16 -0
  152. package/src/runtime/routes/global-search-routes.ts +17 -2
  153. package/src/runtime/routes/guardian-action-routes.ts +23 -1
  154. package/src/runtime/routes/guardian-approval-interception.ts +2 -1
  155. package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
  156. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  157. package/src/runtime/routes/identity-routes.ts +20 -0
  158. package/src/runtime/routes/inbound-message-handler.ts +8 -0
  159. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
  160. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  161. package/src/runtime/routes/integration-routes.ts +83 -0
  162. package/src/runtime/routes/invite-routes.ts +31 -0
  163. package/src/runtime/routes/migration-routes.ts +47 -17
  164. package/src/runtime/routes/pairing-routes.ts +18 -0
  165. package/src/runtime/routes/secret-routes.ts +20 -0
  166. package/src/runtime/routes/surface-action-routes.ts +26 -0
  167. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  168. package/src/runtime/routes/twilio-routes.ts +79 -0
  169. package/src/schedule/recurrence-types.ts +1 -11
  170. package/src/tools/followups/followup_create.ts +9 -3
  171. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  172. package/src/tools/memory/definitions.ts +0 -6
  173. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  174. package/src/tools/schedule/create.ts +1 -3
  175. package/src/tools/schedule/update.ts +9 -6
  176. package/src/twitter/session.ts +29 -77
  177. package/src/util/cookie-session.ts +114 -0
  178. package/src/workspace/git-service.ts +6 -4
  179. package/src/__tests__/conversation-routes.test.ts +0 -99
  180. package/src/__tests__/get-weather.test.ts +0 -393
  181. package/src/__tests__/task-tools.test.ts +0 -685
  182. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  183. package/src/autonomy/autonomy-resolver.ts +0 -62
  184. package/src/autonomy/autonomy-store.ts +0 -138
  185. package/src/autonomy/disposition-mapper.ts +0 -31
  186. package/src/autonomy/index.ts +0 -11
  187. package/src/autonomy/types.ts +0 -43
  188. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  189. package/src/config/bundled-skills/weather/TOOLS.json +0 -36
  190. package/src/config/bundled-skills/weather/icon.svg +0 -24
  191. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  192. package/src/contacts/startup-migration.ts +0 -21
  193. package/src/messaging/triage-engine.ts +0 -344
  194. package/src/tools/weather/service.ts +0 -712
@@ -1,685 +0,0 @@
1
- import { mkdtempSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
-
6
- const testDir = mkdtempSync(join(tmpdir(), "task-tools-test-"));
7
-
8
- mock.module("../util/platform.js", () => ({
9
- getDataDir: () => testDir,
10
- isMacOS: () => process.platform === "darwin",
11
- isLinux: () => process.platform === "linux",
12
- isWindows: () => process.platform === "win32",
13
- getSocketPath: () => join(testDir, "test.sock"),
14
- getPidPath: () => join(testDir, "test.pid"),
15
- getDbPath: () => join(testDir, "test.db"),
16
- getLogPath: () => join(testDir, "test.log"),
17
- ensureDataDir: () => {},
18
- migrateToDataLayout: () => {},
19
- migrateToWorkspaceLayout: () => {},
20
- }));
21
-
22
- mock.module("../util/logger.js", () => ({
23
- getLogger: () =>
24
- new Proxy({} as Record<string, unknown>, {
25
- get: () => () => {},
26
- }),
27
- }));
28
-
29
- mock.module("../config/loader.js", () => ({
30
- getConfig: () => ({
31
- ui: {},
32
- memory: {},
33
- }),
34
- }));
35
-
36
- mock.module("./indexer.js", () => ({
37
- indexMessageNow: () => {},
38
- }));
39
-
40
- import type { Database } from "bun:sqlite";
41
-
42
- import { getDb, initializeDb, resetDb } from "../memory/db.js";
43
- import { createTask, createTaskRun } from "../tasks/task-store.js";
44
- import { executeTaskDelete } from "../tools/tasks/task-delete.js";
45
- import { executeTaskList } from "../tools/tasks/task-list.js";
46
- import { executeTaskRun } from "../tools/tasks/task-run.js";
47
- import { executeTaskSave } from "../tools/tasks/task-save.js";
48
- import { executeTaskListAdd } from "../tools/tasks/work-item-enqueue.js";
49
- import { executeTaskListShow } from "../tools/tasks/work-item-list.js";
50
- import type { ToolContext } from "../tools/types.js";
51
- import { createWorkItem } from "../work-items/work-item-store.js";
52
-
53
- initializeDb();
54
-
55
- afterAll(() => {
56
- resetDb();
57
- try {
58
- rmSync(testDir, { recursive: true });
59
- } catch {
60
- /* best effort */
61
- }
62
- });
63
-
64
- // ── Helpers ──────────────────────────────────────────────────────────
65
-
66
- function getRawDb(): Database {
67
- return (getDb() as unknown as { $client: Database }).$client;
68
- }
69
-
70
- function createTestConversation(id: string): string {
71
- const raw = getRawDb();
72
- const now = Date.now();
73
- raw
74
- .query(
75
- `INSERT INTO conversations (id, title, created_at, updated_at, thread_type, memory_scope_id) VALUES (?, 'Test', ?, ?, 'standard', 'default')`,
76
- )
77
- .run(id, now, now);
78
- return id;
79
- }
80
-
81
- function addTestMessage(
82
- conversationId: string,
83
- role: string,
84
- content: string,
85
- ): void {
86
- const raw = getRawDb();
87
- const id = crypto.randomUUID();
88
- const now = Date.now();
89
- raw
90
- .query(
91
- `INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)`,
92
- )
93
- .run(id, conversationId, role, content, now);
94
- }
95
-
96
- const stubContext: ToolContext = {
97
- workingDir: "/tmp",
98
- sessionId: "test-session",
99
- conversationId: "test-conversation",
100
- trustClass: "guardian",
101
- };
102
-
103
- // ── task_save ────────────────────────────────────────────────────────
104
-
105
- describe("task_save tool", () => {
106
- beforeEach(() => {
107
- const raw = getRawDb();
108
- raw.run("DELETE FROM task_runs");
109
- raw.run("DELETE FROM tasks");
110
- raw.run("DELETE FROM messages");
111
- raw.run("DELETE FROM conversations");
112
- });
113
-
114
- test("creates a task from a conversation", async () => {
115
- const convId = createTestConversation("conv-save-1");
116
- addTestMessage(convId, "user", "Please summarize the document");
117
- addTestMessage(
118
- convId,
119
- "assistant",
120
- JSON.stringify([
121
- {
122
- type: "tool_use",
123
- id: "tu1",
124
- name: "file_read",
125
- input: { path: "/tmp/doc.txt" },
126
- },
127
- ]),
128
- );
129
- addTestMessage(convId, "assistant", "Here is the summary...");
130
-
131
- const result = await executeTaskSave(
132
- { conversation_id: convId },
133
- stubContext,
134
- );
135
-
136
- expect(result.isError).toBe(false);
137
- expect(result.content).toContain("Task saved successfully");
138
- expect(result.content).toContain("Please summarize the document");
139
- expect(result.content).toContain("file_read");
140
- });
141
-
142
- test("uses title override when provided", async () => {
143
- const convId = createTestConversation("conv-save-2");
144
- addTestMessage(convId, "user", "Read and analyze the logs");
145
- addTestMessage(convId, "assistant", "Done!");
146
-
147
- const result = await executeTaskSave(
148
- { conversation_id: convId, title: "My Custom Title" },
149
- stubContext,
150
- );
151
-
152
- expect(result.isError).toBe(false);
153
- expect(result.content).toContain("My Custom Title");
154
- });
155
-
156
- test("uses context conversation_id when missing", async () => {
157
- const convId = createTestConversation(stubContext.conversationId);
158
- addTestMessage(convId, "user", "Summarize the report");
159
- addTestMessage(convId, "assistant", "Done.");
160
-
161
- const result = await executeTaskSave({}, stubContext);
162
-
163
- expect(result.isError).toBe(false);
164
- expect(result.content).toContain("Task saved successfully");
165
- expect(result.content).toContain("Summarize the report");
166
- });
167
-
168
- test("returns error for nonexistent conversation", async () => {
169
- const result = await executeTaskSave(
170
- { conversation_id: "nonexistent" },
171
- stubContext,
172
- );
173
-
174
- expect(result.isError).toBe(true);
175
- expect(result.content).toContain("No messages found");
176
- });
177
- });
178
-
179
- // ── task_run ─────────────────────────────────────────────────────────
180
-
181
- describe("task_run tool", () => {
182
- beforeEach(() => {
183
- const raw = getRawDb();
184
- raw.run("DELETE FROM task_runs");
185
- raw.run("DELETE FROM tasks");
186
- raw.run("DELETE FROM messages");
187
- raw.run("DELETE FROM conversations");
188
- });
189
-
190
- test("resolves task by name (fuzzy match)", async () => {
191
- createTask({
192
- title: "Summarize Document",
193
- template: "Please summarize {{file_path}}",
194
- inputSchema: {
195
- type: "object",
196
- properties: {
197
- file_path: { type: "string", description: "The file path" },
198
- },
199
- },
200
- requiredTools: ["file_read"],
201
- });
202
-
203
- const result = await executeTaskRun(
204
- { task_name: "summarize", inputs: { file_path: "/tmp/report.txt" } },
205
- stubContext,
206
- );
207
-
208
- expect(result.isError).toBe(false);
209
- expect(result.content).toContain("Summarize Document");
210
- expect(result.content).toContain("Please summarize /tmp/report.txt");
211
- });
212
-
213
- test("resolves task by ID", async () => {
214
- const task = createTask({
215
- title: "Deploy App",
216
- template: "Deploy the application to {{url}}",
217
- inputSchema: {
218
- type: "object",
219
- properties: { url: { type: "string", description: "URL" } },
220
- },
221
- });
222
-
223
- const result = await executeTaskRun(
224
- { task_id: task.id, inputs: { url: "https://prod.example.com" } },
225
- stubContext,
226
- );
227
-
228
- expect(result.isError).toBe(false);
229
- expect(result.content).toContain("Deploy App");
230
- expect(result.content).toContain(
231
- "Deploy the application to https://prod.example.com",
232
- );
233
- });
234
-
235
- test("returns error when task not found by name", async () => {
236
- createTask({
237
- title: "Existing Task",
238
- template: "Do something",
239
- });
240
-
241
- const result = await executeTaskRun(
242
- { task_name: "nonexistent" },
243
- stubContext,
244
- );
245
-
246
- expect(result.isError).toBe(true);
247
- expect(result.content).toContain(
248
- 'No task template matching "nonexistent" found',
249
- );
250
- expect(result.content).toContain("Existing Task");
251
- });
252
-
253
- test("returns error when task not found by ID", async () => {
254
- const result = await executeTaskRun({ task_id: "bad-id" }, stubContext);
255
-
256
- expect(result.isError).toBe(true);
257
- expect(result.content).toContain('No task template found with ID "bad-id"');
258
- });
259
-
260
- test("renders template with inputs", async () => {
261
- createTask({
262
- title: "Multi-input Task",
263
- template: "Read {{file_path}} and post to {{url}}",
264
- inputSchema: {
265
- type: "object",
266
- properties: {
267
- file_path: { type: "string", description: "File" },
268
- url: { type: "string", description: "URL" },
269
- },
270
- },
271
- });
272
-
273
- const result = await executeTaskRun(
274
- {
275
- task_name: "multi-input",
276
- inputs: {
277
- file_path: "/home/user/data.csv",
278
- url: "https://api.example.com/upload",
279
- },
280
- },
281
- stubContext,
282
- );
283
-
284
- expect(result.isError).toBe(false);
285
- expect(result.content).toContain(
286
- "Read /home/user/data.csv and post to https://api.example.com/upload",
287
- );
288
- });
289
-
290
- test("returns error when required inputs are missing", async () => {
291
- createTask({
292
- title: "Input Required Task",
293
- template: "Process {{file_path}}",
294
- inputSchema: {
295
- type: "object",
296
- properties: { file_path: { type: "string", description: "File" } },
297
- },
298
- });
299
-
300
- const result = await executeTaskRun(
301
- { task_name: "input required" },
302
- stubContext,
303
- );
304
-
305
- expect(result.isError).toBe(true);
306
- expect(result.content).toContain("Missing required inputs: file_path");
307
- });
308
-
309
- test("returns error when neither task_name nor task_id provided", async () => {
310
- const result = await executeTaskRun({}, stubContext);
311
-
312
- expect(result.isError).toBe(true);
313
- expect(result.content).toContain(
314
- "At least one of task_name or task_id must be provided",
315
- );
316
- });
317
-
318
- test("returns error with helpful message when no tasks exist", async () => {
319
- const result = await executeTaskRun({ task_name: "anything" }, stubContext);
320
-
321
- expect(result.isError).toBe(true);
322
- expect(result.content).toContain("No task templates found");
323
- });
324
-
325
- test("includes required tools in output", async () => {
326
- createTask({
327
- title: "Tools Task",
328
- template: "Do the thing",
329
- requiredTools: ["file_read", "bash"],
330
- });
331
-
332
- const result = await executeTaskRun({ task_name: "tools" }, stubContext);
333
-
334
- expect(result.isError).toBe(false);
335
- expect(result.content).toContain("Required tools: file_read, bash");
336
- });
337
- });
338
-
339
- // ── task_list ────────────────────────────────────────────────────────
340
-
341
- describe("task_list tool", () => {
342
- beforeEach(() => {
343
- const raw = getRawDb();
344
- raw.run("DELETE FROM task_runs");
345
- raw.run("DELETE FROM tasks");
346
- raw.run("DELETE FROM messages");
347
- raw.run("DELETE FROM conversations");
348
- });
349
-
350
- test("returns formatted list of tasks", async () => {
351
- createTask({
352
- title: "Task Alpha",
353
- template: "Do alpha things",
354
- requiredTools: ["file_read"],
355
- });
356
- createTask({
357
- title: "Task Beta",
358
- template: "Do beta {{file_path}}",
359
- inputSchema: {
360
- type: "object",
361
- properties: { file_path: { type: "string", description: "File" } },
362
- },
363
- requiredTools: ["file_read", "file_write"],
364
- });
365
-
366
- const result = await executeTaskList({}, stubContext);
367
-
368
- expect(result.isError).toBe(false);
369
- expect(result.content).toContain("Found 2 task template(s)");
370
- expect(result.content).toContain("Task Alpha");
371
- expect(result.content).toContain("Task Beta");
372
- expect(result.content).toContain("file_read");
373
- expect(result.content).toContain("file_write");
374
- expect(result.content).toContain("file_path");
375
- expect(result.content).toContain(
376
- "Tip: To see your active Tasks (work items in the queue), use the task_list_show tool.",
377
- );
378
- });
379
-
380
- test("returns empty message when no tasks exist", async () => {
381
- const result = await executeTaskList({}, stubContext);
382
-
383
- expect(result.isError).toBe(false);
384
- expect(result.content).toContain("No task templates found");
385
- expect(result.content).toContain(
386
- "Tip: To see your active Tasks (work items in the queue), use the task_list_show tool.",
387
- );
388
- });
389
-
390
- test("shows task status and creation date", async () => {
391
- createTask({
392
- title: "Dated Task",
393
- template: "Something",
394
- });
395
-
396
- const result = await executeTaskList({}, stubContext);
397
-
398
- expect(result.isError).toBe(false);
399
- expect(result.content).toContain("Status: active");
400
- expect(result.content).toContain("Created:");
401
- });
402
- });
403
-
404
- // ── task_list_show ───────────────────────────────────────────────────
405
-
406
- describe("task_list_show tool", () => {
407
- beforeEach(() => {
408
- const raw = getRawDb();
409
- raw.run("DELETE FROM work_items");
410
- raw.run("DELETE FROM task_runs");
411
- raw.run("DELETE FROM tasks");
412
- });
413
-
414
- test("lists work items when they exist", async () => {
415
- const task = createTask({ title: "My Task", template: "Do it" });
416
- createWorkItem({
417
- taskId: task.id,
418
- title: "Work Item Alpha",
419
- priorityTier: 0,
420
- });
421
- createWorkItem({
422
- taskId: task.id,
423
- title: "Work Item Beta",
424
- notes: "some notes",
425
- priorityTier: 1,
426
- });
427
-
428
- const result = await executeTaskListShow({}, stubContext);
429
-
430
- expect(result.isError).toBe(false);
431
- expect(result.content).toContain("Task queue (2 items)");
432
- });
433
-
434
- test("returns empty message when no work items", async () => {
435
- const result = await executeTaskListShow({}, stubContext);
436
-
437
- expect(result.isError).toBe(false);
438
- expect(result.content).toContain("No tasks queued");
439
- });
440
-
441
- test("filters by status when status param is provided", async () => {
442
- const task = createTask({ title: "Filter Task", template: "Do it" });
443
- createWorkItem({ taskId: task.id, title: "Queued Item", priorityTier: 1 });
444
- const raw = getRawDb();
445
- // Create a second work item and manually set its status to 'done'
446
- const doneItem = createWorkItem({
447
- taskId: task.id,
448
- title: "Done Item",
449
- priorityTier: 1,
450
- });
451
- raw
452
- .query("UPDATE work_items SET status = ? WHERE id = ?")
453
- .run("done", doneItem.id);
454
-
455
- const resultQueued = await executeTaskListShow(
456
- { status: "queued" },
457
- stubContext,
458
- );
459
- expect(resultQueued.isError).toBe(false);
460
- expect(resultQueued.content).toContain("1 queued item");
461
-
462
- const resultDone = await executeTaskListShow(
463
- { status: "done" },
464
- stubContext,
465
- );
466
- expect(resultDone.isError).toBe(false);
467
- expect(resultDone.content).toContain("1 done item");
468
- });
469
- });
470
-
471
- // ── task_list_add ────────────────────────────────────────────────────
472
-
473
- describe("task_list_add tool", () => {
474
- beforeEach(() => {
475
- const raw = getRawDb();
476
- raw.run("DELETE FROM work_items");
477
- raw.run("DELETE FROM task_runs");
478
- raw.run("DELETE FROM tasks");
479
- });
480
-
481
- test("successfully enqueues by task_id", async () => {
482
- const task = createTask({ title: "Deploy Service", template: "deploy it" });
483
-
484
- const result = await executeTaskListAdd({ task_id: task.id }, stubContext);
485
-
486
- expect(result.isError).toBe(false);
487
- expect(result.content).toContain("Enqueued work item");
488
- expect(result.content).toContain("Deploy Service");
489
- expect(result.content).toContain("Status: queued");
490
- });
491
-
492
- test("successfully enqueues by task_name (case-insensitive match)", async () => {
493
- createTask({ title: "Run Database Migration", template: "migrate" });
494
-
495
- const result = await executeTaskListAdd(
496
- { task_name: "database migration" },
497
- stubContext,
498
- );
499
-
500
- expect(result.isError).toBe(false);
501
- expect(result.content).toContain("Enqueued work item");
502
- expect(result.content).toContain("Run Database Migration");
503
- });
504
-
505
- test("returns disambiguation when multiple name matches", async () => {
506
- createTask({ title: "Deploy Frontend", template: "deploy fe" });
507
- createTask({ title: "Deploy Backend", template: "deploy be" });
508
-
509
- const result = await executeTaskListAdd(
510
- { task_name: "deploy" },
511
- stubContext,
512
- );
513
-
514
- expect(result.isError).toBe(true);
515
- expect(result.content).toContain("Multiple task definitions match");
516
- expect(result.content).toContain("Deploy Frontend");
517
- expect(result.content).toContain("Deploy Backend");
518
- });
519
-
520
- test("returns error when no matching task found", async () => {
521
- createTask({ title: "Existing Task", template: "do something" });
522
-
523
- const result = await executeTaskListAdd(
524
- { task_name: "nonexistent" },
525
- stubContext,
526
- );
527
-
528
- expect(result.isError).toBe(true);
529
- expect(result.content).toContain(
530
- 'No task definition found matching "nonexistent"',
531
- );
532
- });
533
-
534
- test("returns error when no identifiers provided at all", async () => {
535
- const result = await executeTaskListAdd({}, stubContext);
536
-
537
- expect(result.isError).toBe(true);
538
- expect(result.content).toContain(
539
- "You must provide either task_id, task_name, or title",
540
- );
541
- });
542
-
543
- test("creates ad-hoc work item with just title (no task_id or task_name)", async () => {
544
- const result = await executeTaskListAdd(
545
- { title: "Check Gmail" },
546
- stubContext,
547
- );
548
-
549
- expect(result.isError).toBe(false);
550
- expect(result.content).toContain("Enqueued work item");
551
- expect(result.content).toContain("Check Gmail");
552
- expect(result.content).toContain("Status: queued");
553
- // Ad-hoc items should not show "Task definition:" line
554
- expect(result.content).not.toContain("Task definition:");
555
- });
556
-
557
- test("ad-hoc work item with notes and priority", async () => {
558
- const result = await executeTaskListAdd(
559
- {
560
- title: "Buy groceries",
561
- notes: "Milk, eggs, bread",
562
- priority_tier: 1,
563
- },
564
- stubContext,
565
- );
566
-
567
- expect(result.isError).toBe(false);
568
- expect(result.content).toContain("Buy groceries");
569
- expect(result.content).toContain("Notes: Milk, eggs, bread");
570
- expect(result.content).toContain("Priority: medium");
571
- });
572
-
573
- test("ad-hoc work item shows up in task_list_show", async () => {
574
- await executeTaskListAdd({ title: "Call dentist" }, stubContext);
575
-
576
- const listResult = await executeTaskListShow({}, stubContext);
577
-
578
- expect(listResult.isError).toBe(false);
579
- expect(listResult.content).toContain("Task queue (1 item)");
580
- });
581
-
582
- test("applies optional overrides (title, notes, priority_tier)", async () => {
583
- const task = createTask({ title: "Generic Task", template: "do it" });
584
-
585
- const result = await executeTaskListAdd(
586
- {
587
- task_id: task.id,
588
- title: "Custom Title Override",
589
- notes: "Important context here",
590
- priority_tier: 0,
591
- },
592
- stubContext,
593
- );
594
-
595
- expect(result.isError).toBe(false);
596
- expect(result.content).toContain("Custom Title Override");
597
- expect(result.content).toContain("Notes: Important context here");
598
- expect(result.content).toContain("Priority: high");
599
- });
600
- });
601
-
602
- // ── task_delete ──────────────────────────────────────────────────────
603
-
604
- describe("task_delete tool", () => {
605
- beforeEach(() => {
606
- const raw = getRawDb();
607
- raw.run("DELETE FROM work_items");
608
- raw.run("DELETE FROM task_runs");
609
- raw.run("DELETE FROM tasks");
610
- });
611
-
612
- test("successfully deletes a single task", async () => {
613
- const task = createTask({ title: "Doomed Task", template: "bye" });
614
-
615
- const result = await executeTaskDelete(
616
- { task_ids: [task.id] },
617
- stubContext,
618
- );
619
-
620
- expect(result.isError).toBe(false);
621
- expect(result.content).toContain("Deleted task: Doomed Task");
622
- });
623
-
624
- test("successfully deletes multiple tasks", async () => {
625
- const t1 = createTask({ title: "Task One", template: "one" });
626
- const t2 = createTask({ title: "Task Two", template: "two" });
627
-
628
- const result = await executeTaskDelete(
629
- { task_ids: [t1.id, t2.id] },
630
- stubContext,
631
- );
632
-
633
- expect(result.isError).toBe(false);
634
- expect(result.content).toContain("Deleted 2 task(s)");
635
- expect(result.content).toContain("Task One");
636
- expect(result.content).toContain("Task Two");
637
- });
638
-
639
- test("returns error for non-existent task ID", async () => {
640
- const result = await executeTaskDelete(
641
- { task_ids: ["nonexistent-id"] },
642
- stubContext,
643
- );
644
-
645
- expect(result.isError).toBe(true);
646
- expect(result.content).toContain(
647
- 'No task template or work item found with ID "nonexistent-id"',
648
- );
649
- });
650
-
651
- test("cascades deletion to associated task runs and work items", async () => {
652
- const task = createTask({ title: "Parent Task", template: "parent" });
653
- createTaskRun(task.id);
654
- createWorkItem({ taskId: task.id, title: "Child Work Item" });
655
-
656
- const raw = getRawDb();
657
- // Verify the associated records exist before deletion
658
- const runsBefore = raw
659
- .query("SELECT COUNT(*) as count FROM task_runs WHERE task_id = ?")
660
- .get(task.id) as { count: number };
661
- const itemsBefore = raw
662
- .query("SELECT COUNT(*) as count FROM work_items WHERE task_id = ?")
663
- .get(task.id) as { count: number };
664
- expect(runsBefore.count).toBe(1);
665
- expect(itemsBefore.count).toBe(1);
666
-
667
- const result = await executeTaskDelete(
668
- { task_ids: [task.id] },
669
- stubContext,
670
- );
671
-
672
- expect(result.isError).toBe(false);
673
- expect(result.content).toContain("Deleted task: Parent Task");
674
-
675
- // Verify cascade: associated records should be gone
676
- const runsAfter = raw
677
- .query("SELECT COUNT(*) as count FROM task_runs WHERE task_id = ?")
678
- .get(task.id) as { count: number };
679
- const itemsAfter = raw
680
- .query("SELECT COUNT(*) as count FROM work_items WHERE task_id = ?")
681
- .get(task.id) as { count: number };
682
- expect(runsAfter.count).toBe(0);
683
- expect(itemsAfter.count).toBe(0);
684
- });
685
- });