vellum 0.2.0 → 0.2.2

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 (361) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -1,8 +1,10 @@
1
- import { RiskLevel } from '../../permissions/types.js';
2
- import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
- import type { ToolDefinition } from '../../providers/types.js';
1
+ import type { ToolContext, ToolExecutionResult } from '../types.js';
4
2
  import { getTask, listTasks, createTask } from '../../tasks/task-store.js';
5
- import { createWorkItem } from '../../work-items/work-item-store.js';
3
+ import { createWorkItemWithPermissions, findActiveWorkItemsByTitle, updateWorkItem, identifyEntityById, buildWorkItemMismatchError } from '../../work-items/work-item-store.js';
4
+ import { sanitizeToolList } from '../../tasks/tool-sanitizer.js';
5
+ import { getLogger } from '../../util/logger.js';
6
+
7
+ const log = getLogger('task-list-add');
6
8
 
7
9
  const PRIORITY_LABELS: Record<number, string> = {
8
10
  0: 'high',
@@ -10,151 +12,114 @@ const PRIORITY_LABELS: Record<number, string> = {
10
12
  2: 'low',
11
13
  };
12
14
 
13
- const definition: ToolDefinition = {
14
- name: 'task_list_add',
15
- description:
16
- 'Add a task to the user\'s Task Queue. Use this when the user says "add to my tasks", "add to my queue", "put this on my task list", "track this task", or any variation of adding a one-off item they want to remember or work on. You can provide just a title for ad-hoc items, or reference an existing task definition by name or ID. Do NOT use schedule_create or reminder for simple "add to tasks" requests — those are for timed/recurring automation only.',
17
- input_schema: {
18
- type: 'object',
19
- properties: {
20
- task_id: {
21
- type: 'string',
22
- description: 'ID of an existing task definition to enqueue. Provide this, task_name, or just title.',
23
- },
24
- task_name: {
25
- type: 'string',
26
- description:
27
- 'Title/name of an existing task definition to search for (case-insensitive substring match). Provide this, task_id, or just title.',
28
- },
29
- title: {
30
- type: 'string',
31
- description:
32
- 'Title for the work item. When provided WITHOUT task_id or task_name, creates an ad-hoc work item directly (a lightweight task template is auto-created behind the scenes). When provided WITH task_id or task_name, overrides the task definition title.',
33
- },
34
- notes: {
35
- type: 'string',
36
- description: 'Notes to attach to the work item.',
37
- },
38
- priority_tier: {
39
- type: 'number',
40
- description: '0 = high, 1 = medium (default), 2 = low.',
41
- },
42
- sort_index: {
43
- type: 'number',
44
- description: 'Manual sort order within the priority tier.',
45
- },
46
- },
47
- },
48
- };
49
-
50
- class TaskListAddTool implements Tool {
51
- name = 'task_list_add';
52
- description = definition.description;
53
- category = 'tasks';
54
- defaultRiskLevel = RiskLevel.Low;
55
-
56
- getDefinition(): ToolDefinition {
57
- return definition;
15
+ function handleDuplicate(
16
+ title: string,
17
+ ifExists: string,
18
+ input: Record<string, unknown>,
19
+ ): ToolExecutionResult | null {
20
+ const existing = findActiveWorkItemsByTitle(title);
21
+ if (existing.length === 0) return null;
22
+
23
+ const match = existing[0];
24
+ log.info({ title, existingId: match.id, ifExists }, 'task_list_add: duplicate detected');
25
+
26
+ if (ifExists === 'reuse_existing') {
27
+ log.info({ title, existingId: match.id }, 'task_list_add: reused existing item');
28
+ return {
29
+ content: `Task "${match.title}" already exists in the queue (ID: ${match.id}, status: ${match.status}). Use task_list_update to modify it.`,
30
+ isError: false,
31
+ };
58
32
  }
59
33
 
60
- async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
61
- try {
62
- const taskId = input.task_id as string | undefined;
63
- const taskName = input.task_name as string | undefined;
64
- const titleOverride = input.title as string | undefined;
65
- const notes = input.notes as string | undefined;
66
- const priorityTier = input.priority_tier as number | undefined;
67
- const sortIndex = input.sort_index as number | undefined;
68
-
69
- // Ad-hoc mode: title provided without task_id or task_name
70
- if (!taskId && !taskName) {
71
- if (!titleOverride) {
72
- return {
73
- content: 'Error: You must provide either task_id, task_name, or title to create a work item.',
74
- isError: true,
75
- };
76
- }
34
+ if (ifExists === 'update_existing') {
35
+ const updates: Partial<{ title: string; notes: string; priorityTier: number; sortIndex: number }> = {};
36
+ if (input.priority_tier !== undefined) updates.priorityTier = input.priority_tier as number;
37
+ if (input.notes !== undefined) updates.notes = input.notes as string;
38
+ if (input.sort_index !== undefined) updates.sortIndex = input.sort_index as number;
39
+ if (Object.keys(updates).length > 0) {
40
+ updateWorkItem(match.id, updates);
41
+ }
42
+ log.info({ title, existingId: match.id, updates }, 'task_list_add: updated existing item');
43
+ return {
44
+ content: `Reused existing task "${match.title}" (ID: ${match.id}) instead of creating a duplicate.${
45
+ Object.keys(updates).length > 0 ? ` Updated: ${Object.keys(updates).join(', ')}.` : ''
46
+ }`,
47
+ isError: false,
48
+ };
49
+ }
77
50
 
78
- // Auto-create a lightweight task template for the ad-hoc item
79
- const adHocTask = createTask({
80
- title: titleOverride,
81
- template: titleOverride,
82
- });
83
-
84
- const workItem = createWorkItem({
85
- taskId: adHocTask.id,
86
- title: titleOverride,
87
- notes,
88
- priorityTier: priorityTier ?? 1,
89
- sortIndex,
90
- });
91
-
92
- const priority = PRIORITY_LABELS[workItem.priorityTier] ?? `tier ${workItem.priorityTier}`;
93
- const lines = [
94
- `Enqueued work item:`,
95
- ` Title: ${workItem.title}`,
96
- ` ID: ${workItem.id}`,
97
- ` Priority: ${priority}`,
98
- ` Status: ${workItem.status}`,
99
- ];
100
- if (workItem.notes) {
101
- lines.push(` Notes: ${workItem.notes}`);
102
- }
103
- if (workItem.sortIndex !== null) {
104
- lines.push(` Sort index: ${workItem.sortIndex}`);
105
- }
51
+ return null;
52
+ }
106
53
 
107
- return { content: lines.join('\n'), isError: false };
54
+ export async function executeTaskListAdd(
55
+ input: Record<string, unknown>,
56
+ _context: ToolContext,
57
+ ): Promise<ToolExecutionResult> {
58
+ try {
59
+ const taskId = input.task_id as string | undefined;
60
+ const taskName = input.task_name as string | undefined;
61
+ const titleOverride = input.title as string | undefined;
62
+ const executionPrompt = input.execution_prompt as string | undefined;
63
+ const notes = input.notes as string | undefined;
64
+ const priorityTier = input.priority_tier as number | undefined;
65
+ const sortIndex = input.sort_index as number | undefined;
66
+ const rawRequiredTools = input.required_tools as string[] | undefined;
67
+
68
+ const ifExists = (input.if_exists as string) || 'reuse_existing';
69
+
70
+ // Sanitize explicit required_tools if provided (including empty array)
71
+ const hasExplicitTools = rawRequiredTools !== undefined;
72
+ const sanitizedTools = hasExplicitTools ? sanitizeToolList(rawRequiredTools) : undefined;
73
+
74
+ // Ad-hoc mode: title provided without task_id or task_name
75
+ if (!taskId && !taskName) {
76
+ if (!titleOverride) {
77
+ return {
78
+ content: 'Error: You must provide either task_id, task_name, or title to create a work item.',
79
+ isError: true,
80
+ };
108
81
  }
109
82
 
110
- let resolvedTask;
111
-
112
- if (taskId) {
113
- resolvedTask = getTask(taskId);
114
- if (!resolvedTask) {
115
- return { content: `Error: No task definition found with ID "${taskId}".`, isError: true };
116
- }
83
+ // Duplicate-prevention guard
84
+ if (ifExists !== 'create_duplicate') {
85
+ const duplicateResult = handleDuplicate(titleOverride, ifExists, input);
86
+ if (duplicateResult) return duplicateResult;
117
87
  } else {
118
- // Search by name (case-insensitive substring match)
119
- const needle = taskName!.toLowerCase();
120
- const allTasks = listTasks();
121
- const matches = allTasks.filter((t) => t.title.toLowerCase().includes(needle));
122
-
123
- if (matches.length === 0) {
124
- return {
125
- content: `Error: No task definition found matching "${taskName}". Use task_list to see available tasks.`,
126
- isError: true,
127
- };
128
- }
88
+ log.info({ title: titleOverride }, 'task_list_add: creating duplicate (if_exists=create_duplicate)');
89
+ }
129
90
 
130
- if (matches.length > 1) {
131
- const lines = [
132
- `Multiple task definitions match "${taskName}". Please specify by ID:`,
133
- '',
134
- ];
135
- for (const m of matches) {
136
- lines.push(`- ${m.title} (ID: ${m.id})`);
137
- }
138
- return { content: lines.join('\n'), isError: true };
139
- }
91
+ log.debug({ title: titleOverride }, 'task_list_add: creating new item');
92
+
93
+ // Auto-create a lightweight task template for the ad-hoc item.
94
+ // Use execution_prompt as the template (the actual instruction sent to the
95
+ // model at run time), falling back to notes then title. This preserves
96
+ // detailed instructions (e.g. full file paths) that may be shortened in
97
+ // the display title.
98
+ const adHocTemplate = executionPrompt ?? notes ?? titleOverride;
99
+ const adHocTask = createTask({
100
+ title: titleOverride,
101
+ template: adHocTemplate,
102
+ });
140
103
 
141
- resolvedTask = matches[0];
142
- }
104
+ // For ad-hoc items: explicit tools → persist; omitted → null
105
+ const adHocRequiredTools = hasExplicitTools ? JSON.stringify(sanitizedTools) : undefined;
143
106
 
144
- const workItem = createWorkItem({
145
- taskId: resolvedTask.id,
146
- title: titleOverride ?? resolvedTask.title,
107
+ const workItem = createWorkItemWithPermissions({
108
+ taskId: adHocTask.id,
109
+ title: titleOverride,
147
110
  notes,
148
111
  priorityTier: priorityTier ?? 1,
149
112
  sortIndex,
113
+ requiredTools: adHocRequiredTools,
150
114
  });
151
115
 
116
+ log.info({ selectorType: 'title', workItemId: workItem.id, title: workItem.title }, 'ad-hoc work item created');
117
+
152
118
  const priority = PRIORITY_LABELS[workItem.priorityTier] ?? `tier ${workItem.priorityTier}`;
153
119
  const lines = [
154
120
  `Enqueued work item:`,
155
121
  ` Title: ${workItem.title}`,
156
122
  ` ID: ${workItem.id}`,
157
- ` Task definition: ${resolvedTask.title} (${resolvedTask.id})`,
158
123
  ` Priority: ${priority}`,
159
124
  ` Status: ${workItem.status}`,
160
125
  ];
@@ -166,11 +131,104 @@ class TaskListAddTool implements Tool {
166
131
  }
167
132
 
168
133
  return { content: lines.join('\n'), isError: false };
169
- } catch (err) {
170
- const msg = err instanceof Error ? err.message : String(err);
171
- return { content: `Error: ${msg}`, isError: true };
172
134
  }
135
+
136
+ let resolvedTask;
137
+
138
+ if (taskId) {
139
+ resolvedTask = getTask(taskId);
140
+ if (!resolvedTask) {
141
+ const entity = identifyEntityById(taskId);
142
+ if (entity.type === 'work_item') {
143
+ return {
144
+ 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')}`,
145
+ isError: true,
146
+ };
147
+ }
148
+ 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 };
149
+ }
150
+ } else {
151
+ // Search by name (case-insensitive substring match)
152
+ const needle = taskName!.toLowerCase();
153
+ const allTasks = listTasks();
154
+ const matches = allTasks.filter((t) => t.title.toLowerCase().includes(needle));
155
+
156
+ if (matches.length === 0) {
157
+ return {
158
+ content: `Error: No task definition found matching "${taskName}". Use task_list to see available tasks.`,
159
+ isError: true,
160
+ };
161
+ }
162
+
163
+ if (matches.length > 1) {
164
+ const lines = [
165
+ `Multiple task definitions match "${taskName}". Please specify by ID:`,
166
+ '',
167
+ ];
168
+ for (const m of matches) {
169
+ lines.push(`- ${m.title} (ID: ${m.id})`);
170
+ }
171
+ return { content: lines.join('\n'), isError: true };
172
+ }
173
+
174
+ resolvedTask = matches[0];
175
+ }
176
+
177
+ const finalTitle = titleOverride ?? resolvedTask.title;
178
+
179
+ // Duplicate-prevention guard
180
+ if (ifExists !== 'create_duplicate') {
181
+ const duplicateResult = handleDuplicate(finalTitle, ifExists, input);
182
+ if (duplicateResult) return duplicateResult;
183
+ } else {
184
+ log.info({ title: finalTitle }, 'task_list_add: creating duplicate (if_exists=create_duplicate)');
185
+ }
186
+
187
+ log.debug({ title: finalTitle }, 'task_list_add: creating new item');
188
+
189
+ const selectorType = taskId ? 'task_id' : 'task_name';
190
+
191
+ // Snapshot precedence: explicit required_tools > template tools > null
192
+ let resolvedRequiredTools: string | undefined;
193
+ if (hasExplicitTools) {
194
+ resolvedRequiredTools = JSON.stringify(sanitizedTools);
195
+ } else if (resolvedTask.requiredTools) {
196
+ // Snapshot template tools at add time
197
+ const templateTools = sanitizeToolList(JSON.parse(resolvedTask.requiredTools));
198
+ resolvedRequiredTools = JSON.stringify(templateTools);
199
+ }
200
+
201
+ const workItem = createWorkItemWithPermissions({
202
+ taskId: resolvedTask.id,
203
+ title: finalTitle,
204
+ notes,
205
+ priorityTier: priorityTier ?? 1,
206
+ sortIndex,
207
+ requiredTools: resolvedRequiredTools,
208
+ });
209
+
210
+ log.info({ selectorType, taskId: resolvedTask.id, workItemId: workItem.id, title: workItem.title }, 'work item created from task definition');
211
+
212
+ const priority = PRIORITY_LABELS[workItem.priorityTier] ?? `tier ${workItem.priorityTier}`;
213
+ const lines = [
214
+ `Enqueued work item:`,
215
+ ` Title: ${workItem.title}`,
216
+ ` ID: ${workItem.id}`,
217
+ ` Task definition: ${resolvedTask.title} (${resolvedTask.id})`,
218
+ ` Priority: ${priority}`,
219
+ ` Status: ${workItem.status}`,
220
+ ];
221
+ if (workItem.notes) {
222
+ lines.push(` Notes: ${workItem.notes}`);
223
+ }
224
+ if (workItem.sortIndex !== null) {
225
+ lines.push(` Sort index: ${workItem.sortIndex}`);
226
+ }
227
+
228
+ return { content: lines.join('\n'), isError: false };
229
+ } catch (err) {
230
+ const msg = err instanceof Error ? err.message : String(err);
231
+ log.error({ error: msg }, 'enqueue failed');
232
+ return { content: `Error: ${msg}`, isError: true };
173
233
  }
174
234
  }
175
-
176
- export const taskListAddTool = new TaskListAddTool();
@@ -1,86 +1,41 @@
1
- import { RiskLevel } from '../../permissions/types.js';
2
- import type { Tool, ToolContext, ToolExecutionResult } from '../types.js';
3
- import type { ToolDefinition } from '../../providers/types.js';
1
+ import type { ToolContext, ToolExecutionResult } from '../types.js';
4
2
  import { listWorkItems, type WorkItemStatus } from '../../work-items/work-item-store.js';
5
3
 
6
- const PRIORITY_LABELS: Record<number, string> = {
7
- 0: 'high',
8
- 1: 'medium',
9
- 2: 'low',
10
- };
11
-
12
- const definition: ToolDefinition = {
13
- name: 'task_list_show',
14
- 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.',
15
- input_schema: {
16
- type: 'object',
17
- properties: {
18
- status: {
19
- oneOf: [
20
- { type: 'string' },
21
- { type: 'array', items: { type: 'string' } },
22
- ],
23
- description:
24
- 'Optional status filter. A single status string (e.g. "queued") or an array of statuses to include.',
25
- },
26
- },
27
- },
28
- };
29
-
30
- class TaskListShowTool implements Tool {
31
- name = 'task_list_show';
32
- description = definition.description;
33
- category = 'tasks';
34
- defaultRiskLevel = RiskLevel.Low;
35
-
36
- getDefinition(): ToolDefinition {
37
- return definition;
38
- }
39
-
40
- async execute(input: Record<string, unknown>, _context: ToolContext): Promise<ToolExecutionResult> {
41
- try {
42
- const statusFilter = input.status as string | string[] | undefined;
43
-
44
- let items;
45
- if (typeof statusFilter === 'string') {
46
- items = listWorkItems({ status: statusFilter as WorkItemStatus });
47
- } else if (Array.isArray(statusFilter)) {
48
- // listWorkItems only supports a single status filter, so we fetch all
49
- // and filter client-side when an array is provided
50
- const allItems = listWorkItems();
51
- const allowed = new Set(statusFilter);
52
- items = allItems.filter((item) => allowed.has(item.status));
53
- } else {
54
- items = listWorkItems();
55
- }
4
+ export async function executeTaskListShow(
5
+ input: Record<string, unknown>,
6
+ _context: ToolContext,
7
+ ): Promise<ToolExecutionResult> {
8
+ try {
9
+ const statusFilter = input.status as string | string[] | undefined;
10
+
11
+ let items;
12
+ if (typeof statusFilter === 'string') {
13
+ items = listWorkItems({ status: statusFilter as WorkItemStatus });
14
+ } else if (Array.isArray(statusFilter)) {
15
+ // listWorkItems only supports a single status filter, so we fetch all
16
+ // and filter client-side when an array is provided
17
+ const allItems = listWorkItems();
18
+ const allowed = new Set(statusFilter);
19
+ items = allItems.filter((item) => allowed.has(item.status));
20
+ } else {
21
+ items = listWorkItems();
22
+ }
56
23
 
57
- if (items.length === 0) {
58
- return { content: 'No Tasks found.', isError: false };
59
- }
24
+ const count = items.length;
25
+ const filtered = statusFilter !== undefined;
60
26
 
61
- const lines = [`Found ${items.length} work item(s):`, ''];
27
+ if (count === 0) {
28
+ const suffix = filtered ? 'no items matching filter.' : 'no tasks queued.';
29
+ return { content: `Opened Tasks window \u2014 ${suffix}`, isError: false };
30
+ }
62
31
 
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('');
76
- }
32
+ const label = filtered
33
+ ? `${count} ${Array.isArray(statusFilter) ? 'matching' : statusFilter} item${count === 1 ? '' : 's'}`
34
+ : `${count} item${count === 1 ? '' : 's'}`;
77
35
 
78
- return { content: lines.join('\n'), isError: false };
79
- } catch (err) {
80
- const msg = err instanceof Error ? err.message : String(err);
81
- return { content: `Error: ${msg}`, isError: true };
82
- }
36
+ return { content: `Opened Tasks window (${label}).`, isError: false };
37
+ } catch (err) {
38
+ const msg = err instanceof Error ? err.message : String(err);
39
+ return { content: `Error: ${msg}`, isError: true };
83
40
  }
84
41
  }
85
-
86
- export const taskListShowTool = new TaskListShowTool();
@@ -0,0 +1,60 @@
1
+ import type { ToolContext, ToolExecutionResult } from '../types.js';
2
+ import { resolveWorkItem, removeWorkItemFromQueue, identifyEntityById, buildTaskTemplateMismatchError, type WorkItemStatus } from '../../work-items/work-item-store.js';
3
+ import { getLogger } from '../../util/logger.js';
4
+
5
+ const log = getLogger('task-list-remove');
6
+
7
+ export async function executeTaskListRemove(
8
+ input: Record<string, unknown>,
9
+ _context: ToolContext,
10
+ ): Promise<ToolExecutionResult> {
11
+ const selectorType = input.work_item_id ? 'work_item_id' : input.task_id ? 'task_id' : input.task_name ? 'task_name' : input.title ? 'title' : 'none';
12
+
13
+ try {
14
+ const selector = {
15
+ workItemId: input.work_item_id as string | undefined,
16
+ taskId: input.task_id as string | undefined,
17
+ title: (input.task_name ?? input.title) as string | undefined,
18
+ priorityTier: input.priority_tier as number | undefined,
19
+ status: input.status as WorkItemStatus | undefined,
20
+ createdOrder: input.created_order as number | undefined,
21
+ };
22
+
23
+ const resolveResult = resolveWorkItem(selector);
24
+
25
+ if (resolveResult.status === 'not_found') {
26
+ // When the model passes an ID directly, check if it's a task template
27
+ if (selector.workItemId) {
28
+ const entity = identifyEntityById(selector.workItemId);
29
+ if (entity.type === 'task_template') {
30
+ log.warn({ selectorType, inputId: selector.workItemId }, 'task template ID passed as work_item_id');
31
+ return {
32
+ content: `Error: ${buildTaskTemplateMismatchError(selector.workItemId, entity.title!, 'task_delete to delete task templates')}`,
33
+ isError: true,
34
+ };
35
+ }
36
+ }
37
+ log.warn({ selectorType, error: resolveResult.message }, 'work item not found for removal');
38
+ return { content: `Error: ${resolveResult.message}`, isError: true };
39
+ }
40
+
41
+ if (resolveResult.status === 'ambiguous') {
42
+ log.warn({ selectorType, matchCount: resolveResult.matches.length }, 'ambiguous selector for removal');
43
+ return { content: `Error: ${resolveResult.message}`, isError: true };
44
+ }
45
+
46
+ const item = resolveResult.workItem;
47
+
48
+ log.info({ selectorType, selectorValue: input[selectorType], resolvedWorkItemId: item.id, title: item.title }, 'resolved work item for removal');
49
+
50
+ const removeResult = removeWorkItemFromQueue(item.id);
51
+
52
+ log.info({ resolvedWorkItemId: item.id, deletedCount: 1 }, 'work item removed');
53
+
54
+ return { content: removeResult.message, isError: false };
55
+ } catch (err) {
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ log.error({ selectorType, error: msg }, 'remove failed');
58
+ return { content: `Error: ${msg}`, isError: true };
59
+ }
60
+ }
@@ -0,0 +1,114 @@
1
+ import type { ToolContext, ToolExecutionResult } from '../types.js';
2
+ import { resolveWorkItem, updateWorkItem, identifyEntityById, buildTaskTemplateMismatchError, type WorkItemStatus } from '../../work-items/work-item-store.js';
3
+ import { getLogger } from '../../util/logger.js';
4
+
5
+ const log = getLogger('task-list-update');
6
+
7
+ const PRIORITY_LABELS: Record<number, string> = {
8
+ 0: 'high',
9
+ 1: 'medium',
10
+ 2: 'low',
11
+ };
12
+
13
+ export async function executeTaskListUpdate(
14
+ input: Record<string, unknown>,
15
+ _context: ToolContext,
16
+ ): Promise<ToolExecutionResult> {
17
+ const selectorType = input.work_item_id ? 'work_item_id' : input.task_id ? 'task_id' : input.task_name ? 'task_name' : input.title ? 'title' : 'none';
18
+
19
+ try {
20
+ // Build selector from whichever identifier was provided
21
+ const selector = {
22
+ workItemId: input.work_item_id as string | undefined,
23
+ taskId: input.task_id as string | undefined,
24
+ title: (input.task_name ?? input.title) as string | undefined,
25
+ priorityTier: input.filter_priority_tier as number | undefined,
26
+ status: input.filter_status as WorkItemStatus | undefined,
27
+ createdOrder: input.created_order as number | undefined,
28
+ };
29
+
30
+ // Resolve the target work item
31
+ const result = resolveWorkItem(selector);
32
+
33
+ if (result.status === 'not_found') {
34
+ // When the model passes an ID directly, check if it's a task template
35
+ if (selector.workItemId) {
36
+ const entity = identifyEntityById(selector.workItemId);
37
+ if (entity.type === 'task_template') {
38
+ log.warn({ selectorType, inputId: selector.workItemId }, 'task template ID passed as work_item_id');
39
+ return {
40
+ content: `Error: ${buildTaskTemplateMismatchError(selector.workItemId, entity.title!, 'task_delete to remove task templates, or task_list to view them')}`,
41
+ isError: true,
42
+ };
43
+ }
44
+ }
45
+ log.warn({ selectorType, error: result.message }, 'work item not found for update');
46
+ return { content: `Error: ${result.message}`, isError: true };
47
+ }
48
+
49
+ if (result.status === 'ambiguous') {
50
+ log.warn({ selectorType, matchCount: result.matches.length }, 'ambiguous selector for update');
51
+ return { content: `Error: ${result.message}`, isError: true };
52
+ }
53
+
54
+ const item = result.workItem;
55
+
56
+ // Block direct transitions to 'done' — the only path to done is
57
+ // through the Review action (handleWorkItemComplete in the daemon).
58
+ if (input.status === 'done') {
59
+ log.warn({ selectorType, resolvedWorkItemId: item.id }, 'rejected attempt to set status to done directly');
60
+ return {
61
+ content: 'Error: Cannot set status to \'done\' directly. Use the Review action in the Tasks window.',
62
+ isError: true,
63
+ };
64
+ }
65
+
66
+ log.info({ selectorType, selectorValue: input[selectorType], resolvedWorkItemId: item.id }, 'resolved work item for update');
67
+
68
+ // Build updates from provided fields
69
+ const updates: Partial<{
70
+ priorityTier: number;
71
+ notes: string;
72
+ status: WorkItemStatus;
73
+ sortIndex: number;
74
+ }> = {};
75
+ if (input.priority_tier !== undefined) updates.priorityTier = input.priority_tier as number;
76
+ if (input.notes !== undefined) updates.notes = input.notes as string;
77
+ if (input.status !== undefined) updates.status = input.status as WorkItemStatus;
78
+ if (input.sort_index !== undefined) updates.sortIndex = input.sort_index as number;
79
+
80
+ if (Object.keys(updates).length === 0) {
81
+ log.warn({ selectorType, resolvedWorkItemId: item.id }, 'update called with no fields to update');
82
+ return {
83
+ content: 'No updates specified. Provide at least one field to update (priority_tier, notes, status, sort_index).',
84
+ isError: true,
85
+ };
86
+ }
87
+
88
+ const updated = updateWorkItem(item.id, updates);
89
+ if (!updated) {
90
+ log.error({ selectorType, resolvedWorkItemId: item.id, updates }, 'updateWorkItem returned null');
91
+ return {
92
+ content: `Error: Failed to update work item "${item.title}".`,
93
+ isError: true,
94
+ };
95
+ }
96
+
97
+ log.info({ resolvedWorkItemId: item.id, updatedFields: Object.keys(updates) }, 'work item updated');
98
+
99
+ // Build confirmation message
100
+ const parts: string[] = [`Updated "${updated.title}"`];
101
+ if (input.priority_tier !== undefined) {
102
+ parts.push(`priority → ${PRIORITY_LABELS[updated.priorityTier] ?? updated.priorityTier}`);
103
+ }
104
+ if (input.notes !== undefined) parts.push('notes updated');
105
+ if (input.status !== undefined) parts.push(`status → ${updated.status}`);
106
+ if (input.sort_index !== undefined) parts.push(`sort index → ${updated.sortIndex}`);
107
+
108
+ return { content: parts.join(', ') + '.', isError: false };
109
+ } catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ log.error({ selectorType, error: msg }, 'update failed');
112
+ return { content: `Error: ${msg}`, isError: true };
113
+ }
114
+ }