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
@@ -104,6 +104,52 @@ export function isAllowlisted(value: string): boolean {
104
104
  return false;
105
105
  }
106
106
 
107
+ export interface AllowlistValidationError {
108
+ index: number;
109
+ pattern: string;
110
+ message: string;
111
+ }
112
+
113
+ /**
114
+ * Validate all regex patterns in an allowlist config without loading them.
115
+ * Returns an array of validation errors (empty = all valid).
116
+ */
117
+ export function validateAllowlist(config: AllowlistConfig): AllowlistValidationError[] {
118
+ const errors: AllowlistValidationError[] = [];
119
+ if (!config.patterns) return errors;
120
+ if (!Array.isArray(config.patterns)) {
121
+ errors.push({ index: -1, pattern: String(config.patterns), message: '"patterns" must be an array' });
122
+ return errors;
123
+ }
124
+
125
+ for (let i = 0; i < config.patterns.length; i++) {
126
+ const p = config.patterns[i];
127
+ if (typeof p !== 'string') {
128
+ errors.push({ index: i, pattern: String(p), message: 'Pattern is not a string' });
129
+ continue;
130
+ }
131
+ try {
132
+ new RegExp(p);
133
+ } catch (err) {
134
+ errors.push({ index: i, pattern: p, message: (err as Error).message });
135
+ }
136
+ }
137
+ return errors;
138
+ }
139
+
140
+ /**
141
+ * Read secret-allowlist.json from disk and validate it.
142
+ * Returns validation errors, or null if the file doesn't exist.
143
+ */
144
+ export function validateAllowlistFile(): AllowlistValidationError[] | null {
145
+ const filePath = join(getRootDir(), 'protected', 'secret-allowlist.json');
146
+ if (!existsSync(filePath)) return null;
147
+
148
+ const raw = readFileSync(filePath, 'utf-8');
149
+ const config: AllowlistConfig = JSON.parse(raw);
150
+ return validateAllowlist(config);
151
+ }
152
+
107
153
  /**
108
154
  * Reset cached state so the allowlist is reloaded on next check.
109
155
  * Called by the daemon file watcher when secret-allowlist.json changes,
@@ -395,7 +395,7 @@ export async function clawhubInspect(slug: string): Promise<{ data?: ClawhubInsp
395
395
  size: (f.size as number) ?? 0,
396
396
  contentType: (f.contentType as string) ?? undefined,
397
397
  })) : null,
398
- skillMdContent: parsed.skillMdContent ?? parsed.fileContents?.['SKILL.md'] ?? null,
398
+ skillMdContent: parsed.skillMdContent ?? parsed.fileContents?.['SKILL.md'] ?? parsed.file?.content ?? null,
399
399
  };
400
400
  return { data };
401
401
  } catch {
@@ -52,10 +52,18 @@ interface ManagedSubagent {
52
52
  parentSendToClient: (msg: ServerMessage) => void;
53
53
  }
54
54
 
55
+ export interface SubagentNotificationInfo {
56
+ subagentId: string;
57
+ label: string;
58
+ status: 'completed' | 'failed' | 'aborted';
59
+ error?: string;
60
+ }
61
+
55
62
  export type ParentNotifyCallback = (
56
63
  parentSessionId: string,
57
64
  message: string,
58
65
  sendToClient: (msg: ServerMessage) => void,
66
+ notification: SubagentNotificationInfo,
59
67
  ) => void;
60
68
 
61
69
  export class SubagentManager {
@@ -221,6 +229,8 @@ export class SubagentManager {
221
229
  await managed.session.runAgentLoop(objective, messageId, onEvent);
222
230
 
223
231
  // Agent loop completed successfully.
232
+ // Copy usage stats from the session before sending status (which includes usage).
233
+ managed.state.usage = { ...managed.session.usageStats };
224
234
  // Only update state + notify if still non-terminal (guards against abort race).
225
235
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
226
236
  managed.state.completedAt = Date.now();
@@ -235,6 +245,7 @@ export class SubagentManager {
235
245
  const errorMsg = err instanceof Error ? err.message : String(err);
236
246
  managed.state.error = errorMsg;
237
247
  managed.state.completedAt = Date.now();
248
+ managed.state.usage = { ...managed.session.usageStats };
238
249
 
239
250
  // Only update status if not already terminal (e.g. aborted).
240
251
  if (!TERMINAL_STATUSES.has(managed.state.status)) {
@@ -267,16 +278,28 @@ export class SubagentManager {
267
278
  managed.session.abort();
268
279
  managed.state.completedAt = Date.now();
269
280
  if (parentSendToClient) {
270
- this.setStatus(subagentId, 'aborted', parentSendToClient);
271
- // Notify parent about the abort skip when the parent already has the
272
- // tool result (e.g. subagent_abort tool) to avoid duplicate turns.
281
+ // Route the status update through the stored parent sender so the
282
+ // owning session's UI chip updates, even when the abort comes from a
283
+ // different socket (e.g. after thread switching). Fall back to the
284
+ // caller-provided sender if no stored sender exists.
285
+ const statusSender = managed.parentSendToClient ?? parentSendToClient;
286
+ this.setStatus(subagentId, 'aborted', statusSender);
287
+ // Notify parent that the subagent was explicitly aborted — tell it NOT to re-spawn.
288
+ // Skip when the parent LLM itself called subagent_abort (it already has the tool result).
273
289
  if (this.onSubagentFinished && !options?.suppressNotification) {
274
290
  const label = managed.state.config.label;
291
+ const message =
292
+ `[Subagent "${label}" was explicitly aborted]\n\n` +
293
+ `This subagent was cancelled on purpose. Do NOT re-spawn or retry it.`;
275
294
  try {
295
+ // Use the managed subagent's stored parentSendToClient so the
296
+ // notification routes to the parent session's socket, not the
297
+ // aborting socket (which may be a different thread after switching).
276
298
  this.onSubagentFinished(
277
299
  managed.state.config.parentSessionId,
278
- `[Subagent "${label}" was aborted]`,
279
- parentSendToClient,
300
+ message,
301
+ managed.parentSendToClient,
302
+ { subagentId, label, status: 'aborted' },
280
303
  );
281
304
  } catch (err) {
282
305
  log.error({ subagentId, err }, 'Failed to notify parent about abort');
@@ -460,16 +483,25 @@ export class SubagentManager {
460
483
  if (outcome === 'completed') {
461
484
  message =
462
485
  `[Subagent "${config.label}" completed]\n\n` +
463
- `Use subagent_read with subagent_id "${config.id}" to retrieve the full output.`;
486
+ `Use subagent_read with subagent_id "${config.id}" to retrieve the full output.\n` +
487
+ `Do NOT re-spawn this subagent — just read and share the results.`;
464
488
  } else {
465
489
  const error = managed.state.error ?? 'Unknown error';
466
490
  message =
467
491
  `[Subagent "${config.label}" failed]\n\n` +
468
- `Error: ${error}`;
492
+ `Error: ${error}\n` +
493
+ `Do NOT re-spawn or retry this subagent unless the user explicitly asks.`;
469
494
  }
470
495
 
496
+ const notification: SubagentNotificationInfo = {
497
+ subagentId: config.id,
498
+ label: config.label,
499
+ status: outcome,
500
+ ...(outcome === 'failed' ? { error: managed.state.error ?? 'Unknown error' } : {}),
501
+ };
502
+
471
503
  try {
472
- this.onSubagentFinished(config.parentSessionId, message, parentSendToClient);
504
+ this.onSubagentFinished(config.parentSessionId, message, parentSendToClient, notification);
473
505
  } catch (err) {
474
506
  log.error({ subagentId: config.id, err }, 'Failed to notify parent session');
475
507
  }
@@ -12,6 +12,9 @@ import { getProfilePolicy } from './worker-backend.js';
12
12
 
13
13
  const log = getLogger('swarm-backend-claude-code');
14
14
 
15
+ const MAX_CLAUDE_CODE_DEPTH = 1;
16
+ const DEPTH_ENV_VAR = 'VELLUM_CLAUDE_CODE_DEPTH';
17
+
15
18
  /**
16
19
  * Create a Claude Code worker backend that enforces profile-based tool policies.
17
20
  * Uses the Claude Agent SDK to run autonomous worker tasks.
@@ -28,6 +31,7 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
28
31
 
29
32
  async runTask(input: SwarmWorkerBackendInput) {
30
33
  const start = Date.now();
34
+ const stderrLines: string[] = [];
31
35
  try {
32
36
  const { query } = await import('@anthropic-ai/claude-agent-sdk');
33
37
  const config = getConfig();
@@ -49,6 +53,22 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
49
53
  return { behavior: 'allow' as const };
50
54
  };
51
55
 
56
+ // Enforce nesting depth limit
57
+ const currentDepth = parseInt(process.env[DEPTH_ENV_VAR] ?? '0', 10);
58
+ if (currentDepth >= MAX_CLAUDE_CODE_DEPTH) {
59
+ log.warn({ currentDepth, max: MAX_CLAUDE_CODE_DEPTH }, 'Swarm worker nesting depth exceeded');
60
+ return { success: false, output: `Nesting depth exceeded (depth ${currentDepth}, max ${MAX_CLAUDE_CODE_DEPTH})`, failureReason: 'backend_unavailable' as const, durationMs: Date.now() - start };
61
+ }
62
+
63
+ // Strip the SDK's nesting guard but set our own depth counter.
64
+ const subprocessEnv: Record<string, string | undefined> = {
65
+ ...process.env,
66
+ ANTHROPIC_API_KEY: apiKey,
67
+ [DEPTH_ENV_VAR]: String(currentDepth + 1),
68
+ };
69
+ delete subprocessEnv.CLAUDECODE;
70
+ delete subprocessEnv.CLAUDE_CODE_ENTRYPOINT;
71
+
52
72
  const conversation = query({
53
73
  prompt: input.prompt,
54
74
  options: {
@@ -57,20 +77,48 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
57
77
  canUseTool,
58
78
  permissionMode: 'default',
59
79
  maxTurns: 30,
60
- env: { ...process.env, ANTHROPIC_API_KEY: apiKey },
80
+ env: subprocessEnv,
81
+ stderr: (data: string) => {
82
+ const trimmed = data.trimEnd();
83
+ if (trimmed) {
84
+ stderrLines.push(trimmed);
85
+ log.debug({ stderr: trimmed }, 'Swarm worker subprocess stderr');
86
+ }
87
+ },
61
88
  },
62
89
  });
63
90
 
64
91
  let resultText = '';
92
+ let hasError = false;
65
93
  for await (const message of conversation) {
66
94
  if (input.signal?.aborted) break;
67
- if (message.type === 'assistant' && message.message?.content) {
68
- for (const block of message.message.content) {
69
- if (block.type === 'text') resultText += block.text;
95
+ if (message.type === 'assistant') {
96
+ if (message.error) {
97
+ log.error({ error: message.error, sessionId: message.session_id }, 'Swarm worker assistant message error');
98
+ hasError = true;
99
+ resultText += `\n[Claude Code error: ${message.error}]`;
100
+ }
101
+ if (message.message?.content) {
102
+ for (const block of message.message.content) {
103
+ if (block.type === 'text') resultText += block.text;
104
+ }
70
105
  }
71
106
  } else if (message.type === 'result') {
72
- if (message.subtype === 'success' && message.result && !resultText) {
73
- resultText = message.result;
107
+ if (message.subtype === 'success') {
108
+ log.info({ numTurns: message.num_turns, durationMs: message.duration_ms, costUsd: message.total_cost_usd }, 'Swarm worker completed');
109
+ if (message.result && !resultText) {
110
+ resultText = message.result;
111
+ }
112
+ } else {
113
+ hasError = true;
114
+ const errors = message.errors ?? [];
115
+ const denials = message.permission_denials ?? [];
116
+ log.error({ subtype: message.subtype, errors, permissionDenials: denials.length, numTurns: message.num_turns, durationMs: message.duration_ms }, 'Swarm worker session failed');
117
+
118
+ const parts: string[] = [`[${message.subtype}] (${message.num_turns} turns, ${(message.duration_ms / 1000).toFixed(1)}s)`];
119
+ if (errors.length > 0) parts.push(`Errors: ${errors.join('; ')}`);
120
+ if (denials.length > 0) parts.push(`Permission denied: ${denials.map(d => d.tool_name).join(', ')}`);
121
+ resultText += `\n${parts.join('\n')}`;
74
122
  }
75
123
  }
76
124
  }
@@ -80,10 +128,17 @@ export function createClaudeCodeBackend(): SwarmWorkerBackend {
80
128
  return { success: false, output: 'Cancelled (aborted)', failureReason: 'cancelled' as const, durationMs: Date.now() - start };
81
129
  }
82
130
 
83
- return { success: true, output: resultText || 'Completed', durationMs: Date.now() - start };
131
+ return { success: !hasError, output: resultText || 'Completed', durationMs: Date.now() - start };
84
132
  } catch (err) {
85
- const message = err instanceof Error ? err.message : String(err);
86
- return { success: false, output: message, failureReason: 'backend_unavailable' as const, durationMs: Date.now() - start };
133
+ const errMessage = err instanceof Error ? err.message : String(err);
134
+ const recentStderr = stderrLines.slice(-20);
135
+ log.error({ err, stderrTail: recentStderr }, 'Swarm worker execution failed');
136
+
137
+ const parts = [errMessage];
138
+ if (recentStderr.length > 0) {
139
+ parts.push(`\nSubprocess stderr (last ${recentStderr.length} lines):\n${recentStderr.join('\n')}`);
140
+ }
141
+ return { success: false, output: parts.join(''), failureReason: 'backend_unavailable' as const, durationMs: Date.now() - start };
87
142
  }
88
143
  },
89
144
  };
@@ -1,4 +1,5 @@
1
1
  import type { SwarmRole, SwarmTaskResult } from './types.js';
2
+ import { truncate } from '../util/truncate.js';
2
3
 
3
4
  /**
4
5
  * Build a role-specific worker prompt for a swarm task.
@@ -70,7 +71,7 @@ export function parseWorkerOutput(raw: string): Pick<SwarmTaskResult, 'summary'
70
71
  }
71
72
 
72
73
  return {
73
- summary: raw.slice(0, 500),
74
+ summary: truncate(raw, 500, ''),
74
75
  artifacts: [],
75
76
  issues: [],
76
77
  nextSteps: [],
package/src/tasks/SPEC.md CHANGED
@@ -87,18 +87,23 @@ Each task run creates a new conversation thread with `threadType: 'background'`.
87
87
 
88
88
  ### Lifecycle
89
89
 
90
- 1. **Start**: The daemon creates a `background` thread, substitutes template
91
- placeholders, and sends the prompt to the LLM. An IPC notification
92
- (`task_run_started`) is broadcast to all connected clients with the run ID,
93
- task ID, and thread ID.
94
- 2. **Completion**: When the LLM response is received and stored, the daemon
95
- broadcasts a `task_run_completed` notification with the run ID, task ID,
96
- thread ID, and a status (`success` | `error`).
97
- 3. **Visibility**: Background threads are excluded from the default thread list
98
- (existing behavior in `conversation-store.ts`). Clients can query for them
99
- explicitly to surface task results in a dedicated UI.
100
-
101
- **Why background threads:** Reuses the existing `threadType: 'background'`
90
+ 1. **Preflight**: The client requests a permission preflight for the work item.
91
+ The daemon classifies risk for each required tool and returns the permission
92
+ set. The client displays an approval dialog; approved tools are stored on
93
+ the work item.
94
+ 2. **Start**: The daemon creates a `background` conversation, substitutes
95
+ template placeholders, sets up ephemeral permission rules for the approved
96
+ tools, and processes the rendered prompt through a daemon `Session`. Status
97
+ updates are broadcast to all connected clients via `work_item_status_changed`
98
+ and `tasks_changed` IPC messages.
99
+ 3. **Completion**: When the session finishes, the work item transitions to
100
+ `awaiting_review` (on success) or `failed` (on error). The daemon broadcasts
101
+ the final status to all clients.
102
+ 4. **Visibility**: Background conversations are excluded from the default thread
103
+ list (existing behavior in `conversation-store.ts`). Clients can query for
104
+ them explicitly to surface task results in a dedicated UI.
105
+
106
+ **Why background conversations:** Reuses the existing `threadType: 'background'`
102
107
  infrastructure. Task runs don't interrupt the user's current conversation, and
103
108
  clients can choose how and when to display results (toast, panel, separate
104
109
  tab).
@@ -107,27 +112,28 @@ tab).
107
112
 
108
113
  ## 4. Safety Invariants
109
114
 
110
- - **No auto-execution**: Tasks are never triggered automatically. Every run
111
- requires an explicit user action (CLI command, API call, or UI button press).
115
+ - **Explicit trigger required**: Task runs are triggered either by an explicit
116
+ user action (UI button press, API call) or by a user-configured schedule
117
+ (`run_task:<task_id>` via the scheduler).
112
118
  - **Ephemeral permission bundles**: If a task is configured with tool access,
113
119
  the permission grants are scoped to the single run and discarded afterward.
114
120
  No persistent allowlist entries are created on behalf of a task.
115
- - **High-risk tools always prompt**: Regardless of any task-level permission
116
- configuration, tools classified as `RiskLevel.High` (destructive shell
117
- commands, private-network fetches, etc.) always require interactive user
118
- confirmation. This invariant cannot be overridden by task definitions.
121
+ - **High-risk tools require upfront approval**: Tools classified as
122
+ `RiskLevel.High` (destructive shell commands, private-network fetches, etc.)
123
+ are surfaced in the preflight dialog so the user can explicitly approve or
124
+ deny them before execution begins. During the run itself, approved tools
125
+ (including high-risk ones) execute without further prompting.
119
126
 
120
127
  ---
121
128
 
122
- ## 5. PR Dependency Chain
129
+ ## 5. Implementation Notes
123
130
 
124
- Implementation is split into sequential PRs, each building on the previous:
131
+ The implementation is complete. Key modules:
125
132
 
126
- | PR | Title | What it delivers |
127
- |----|-------|------------------|
128
- | 0 | Spec decisions | This document. |
129
- | 1 | Schema + storage | `tasks` and `task_runs` tables, Drizzle schema, migration in `db.ts`, CRUD functions in `task-store.ts`. |
130
- | 2 | Template engine | `renderTemplate()` placeholder substitution with input validation against the JSON Schema. |
131
- | 3 | Run executor | `executeTaskRun()` creates background thread, calls LLM, writes result, broadcasts IPC notifications (`task_run_started`, `task_run_completed`). |
132
- | 4 | CLI surface | `vellum task create`, `vellum task run`, `vellum task list` commands. |
133
- | 5 | IPC + macOS integration | Wire up IPC message types; macOS client displays task run results. |
133
+ | Module | What it delivers |
134
+ |--------|------------------|
135
+ | `task-store.ts` | `tasks` and `task_runs` tables, CRUD functions. |
136
+ | `task-runner.ts` | `runTask()` creates background conversation, renders template, processes through daemon Session. |
137
+ | `ephemeral-permissions.ts` | Scoped permission rules for the duration of a single task run. |
138
+ | `work-items.ts` (daemon handler) | IPC handlers for preflight, run, cancel, and status queries. |
139
+ | Bundled skill (`tasks/`) | Tool definitions (`task_save`, `task_run`, `task_list`, `task_delete`, `task_list_*`) for the LLM. |
@@ -21,19 +21,28 @@ export function clearTaskRunRules(taskRunId: string): void {
21
21
  /**
22
22
  * Build ephemeral TrustRule entries from a task's required_tools list.
23
23
  *
24
- * Each rule allows the specified tool with a wildcard pattern scoped to the
25
- * given working directory. Priority is set to 50 — lower than user rules (100)
26
- * so that user deny rules still take precedence. `allowHighRisk` is NOT set,
27
- * so high-risk operations continue to prompt for approval.
24
+ * Each rule allows the specified tool with a wildcard pattern scoped
25
+ * globally ('everywhere'). The scope is intentionally broad because the
26
+ * session's workingDir (sandbox path like ~/.vellum/workspace) differs
27
+ * from process.cwd() using a directory-scoped rule would fail
28
+ * matchesScope() and silently miss. Priority is set to 75 — above
29
+ * default rules (50) so pre-approved tools aren't shadowed by default
30
+ * `ask` rules (which would trigger prompting and auto-deny in
31
+ * non-interactive task runs), but below user rules (100) so user deny
32
+ * rules still take precedence. `allowHighRisk` is set because task runs
33
+ * execute asynchronously without interactive confirmation — the client
34
+ * pre-approves tools via the preflight flow before execution begins,
35
+ * so there is no interactive prompt during the run itself.
28
36
  */
29
- export function buildTaskRules(taskRunId: string, requiredTools: string[], workingDir: string): TrustRule[] {
37
+ export function buildTaskRules(taskRunId: string, requiredTools: string[], _workingDir: string): TrustRule[] {
30
38
  return requiredTools.map((tool) => ({
31
39
  id: `ephemeral:${taskRunId}:${tool}`,
32
40
  tool,
33
41
  pattern: '**',
34
- scope: workingDir,
42
+ scope: 'everywhere',
35
43
  decision: 'allow' as const,
36
- priority: 50,
44
+ allowHighRisk: true,
45
+ priority: 75,
37
46
  createdAt: Date.now(),
38
47
  principalKind: 'task',
39
48
  principalId: taskRunId,
@@ -3,6 +3,8 @@ import { getDb } from '../memory/db.js';
3
3
  import { messages as messagesTable } from '../memory/schema.js';
4
4
  import { createTask } from './task-store.js';
5
5
  import type { Task } from './task-store.js';
6
+ import { truncate } from '../util/truncate.js';
7
+ import { sanitizeToolList } from './tool-sanitizer.js';
6
8
 
7
9
  /** Output schema for the task compiler. */
8
10
  export interface CompiledTask {
@@ -44,8 +46,8 @@ export function compileTaskFromConversation(conversationId: string): CompiledTas
44
46
  // Extract user message text content
45
47
  const userText = extractTextContent(firstUserMsg.content);
46
48
 
47
- // Extract unique tool names from assistant messages
48
- const requiredTools = extractToolNames(msgs);
49
+ // Extract unique tool names from assistant messages.
50
+ const requiredTools = sanitizeToolList(extractToolNames(msgs));
49
51
 
50
52
  // Build template with placeholder substitutions
51
53
  const { template, properties } = buildTemplate(userText);
@@ -193,6 +195,5 @@ function buildTemplate(text: string): {
193
195
  function deriveTitle(text: string): string {
194
196
  // Take the first line and trim whitespace
195
197
  const firstLine = text.split('\n')[0].trim();
196
- if (firstLine.length <= 60) return firstLine;
197
- return firstLine.slice(0, 57) + '...';
198
+ return truncate(firstLine, 60);
198
199
  }
@@ -2,6 +2,7 @@ import { getLogger } from '../util/logger.js';
2
2
  import { createConversation } from '../memory/conversation-store.js';
3
3
  import { getTask, createTaskRun, updateTaskRun } from './task-store.js';
4
4
  import { buildTaskRules, setTaskRunRules, clearTaskRunRules } from './ephemeral-permissions.js';
5
+ import { sanitizeToolList } from './tool-sanitizer.js';
5
6
 
6
7
  const log = getLogger('task-runner');
7
8
 
@@ -9,6 +10,8 @@ export interface TaskRunOptions {
9
10
  taskId: string;
10
11
  inputs?: Record<string, string>;
11
12
  workingDir: string;
13
+ /** Pre-approved tools from the permission preflight flow. When set, only these tools get ephemeral allow rules. */
14
+ approvedTools?: string[];
12
15
  }
13
16
 
14
17
  export interface TaskRunResult {
@@ -32,7 +35,7 @@ export function renderTemplate(template: string, inputs: Record<string, string>)
32
35
  */
33
36
  export async function runTask(
34
37
  opts: TaskRunOptions,
35
- processMessage: (conversationId: string, message: string) => Promise<void>,
38
+ processMessage: (conversationId: string, message: string, taskRunId: string) => Promise<void>,
36
39
  ): Promise<TaskRunResult> {
37
40
  const task = getTask(opts.taskId);
38
41
  if (!task) {
@@ -47,9 +50,11 @@ export async function runTask(
47
50
  memoryScopeId: `task:${task.id}`,
48
51
  });
49
52
 
50
- // Build and register ephemeral permission rules from the task's required tools
51
- const requiredTools: string[] = task.requiredTools ? JSON.parse(task.requiredTools) : [];
52
- const rules = buildTaskRules(run.id, requiredTools, opts.workingDir);
53
+ // Build and register ephemeral permission rules. If the user pre-approved
54
+ // specific tools via the preflight flow, use those instead of all requiredTools.
55
+ const requiredTools = sanitizeToolList(task.requiredTools ? JSON.parse(task.requiredTools) : []);
56
+ const toolsForRules = opts.approvedTools ? sanitizeToolList(opts.approvedTools) : requiredTools;
57
+ const rules = buildTaskRules(run.id, toolsForRules, opts.workingDir);
53
58
  setTaskRunRules(run.id, rules);
54
59
 
55
60
  try {
@@ -58,7 +63,7 @@ export async function runTask(
58
63
  updateTaskRun(run.id, { status: 'running', startedAt: Date.now() });
59
64
 
60
65
  log.info({ taskId: task.id, taskRunId: run.id, conversationId: conversation.id }, 'Executing task');
61
- await processMessage(conversation.id, renderedTemplate);
66
+ await processMessage(conversation.id, renderedTemplate, run.id);
62
67
 
63
68
  updateTaskRun(run.id, { status: 'completed', finishedAt: Date.now() });
64
69
 
@@ -1,7 +1,7 @@
1
1
  import { createSchedule } from '../schedule/schedule-store.js';
2
2
 
3
3
  /**
4
- * Create a schedule that runs a task on a cron expression.
4
+ * Create a recurrence schedule that runs a task on a cron or RRULE expression.
5
5
  * The scheduler detects the `run_task:<taskId>` message format
6
6
  * and delegates to runTask() instead of processMessage().
7
7
  */
@@ -0,0 +1,36 @@
1
+ import { getTool, getAllTools } from '../tools/registry.js';
2
+
3
+ /**
4
+ * Deduplicate and sort a list of tool names, validating against the live
5
+ * tool registry. Unknown tool names are logged at warn level but kept —
6
+ * they may refer to skill tools that will be loaded at runtime.
7
+ *
8
+ * The returned array is deterministic: sorted alphabetically with no duplicates.
9
+ */
10
+ export function sanitizeToolList(tools: string[]): string[] {
11
+ const seen = new Set<string>();
12
+
13
+ for (const tool of tools) {
14
+ if (!tool || typeof tool !== 'string') continue;
15
+ seen.add(tool);
16
+ }
17
+
18
+ return [...seen].sort();
19
+ }
20
+
21
+ /**
22
+ * Get all registered tool names from the live tool registry.
23
+ * Used as the fallback when a task/work-item has no explicit requiredTools.
24
+ */
25
+ export function getRegisteredToolNames(): string[] {
26
+ return getAllTools()
27
+ .filter((t) => t.executionMode !== 'proxy' && t.origin !== 'skill')
28
+ .map((t) => t.name)
29
+ .sort();
30
+ }
31
+
32
+ /** Look up the human-readable description for a tool from the registry. */
33
+ export function getToolDescription(tool: string): string {
34
+ const registered = getTool(tool);
35
+ return registered?.description ?? tool;
36
+ }
@@ -214,7 +214,7 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[]
214
214
 
215
215
  const limit = Math.min(params.limit ?? DEFAULT_LIMIT, MAX_RESULTS);
216
216
  const stmt = raw.prepare(
217
- `SELECT a.id, a.assistant_id, a.original_filename, a.mime_type, a.size_bytes, a.kind, a.created_at
217
+ `SELECT a.id, a.original_filename, a.mime_type, a.size_bytes, a.kind, a.thumbnail_base64, a.created_at
218
218
  FROM attachments a
219
219
  WHERE ${whereParts.join(' AND ')}
220
220
  ORDER BY a.created_at DESC
@@ -223,21 +223,21 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[]
223
223
 
224
224
  const rows = stmt.all(...bindValues, limit) as Array<{
225
225
  id: string;
226
- assistant_id: string;
227
226
  original_filename: string;
228
227
  mime_type: string;
229
228
  size_bytes: number;
230
229
  kind: string;
230
+ thumbnail_base64: string | null;
231
231
  created_at: number;
232
232
  }>;
233
233
 
234
234
  return rows.map((r) => ({
235
235
  id: r.id,
236
- assistantId: r.assistant_id,
237
236
  originalFilename: r.original_filename,
238
237
  mimeType: r.mime_type,
239
238
  sizeBytes: r.size_bytes,
240
239
  kind: r.kind,
240
+ thumbnailBase64: r.thumbnail_base64,
241
241
  createdAt: r.created_at,
242
242
  }));
243
243
  }
@@ -249,11 +249,11 @@ export function searchAttachments(params: AssetSearchParams): StoredAttachment[]
249
249
  const query = db
250
250
  .select({
251
251
  id: attachments.id,
252
- assistantId: attachments.assistantId,
253
252
  originalFilename: attachments.originalFilename,
254
253
  mimeType: attachments.mimeType,
255
254
  sizeBytes: attachments.sizeBytes,
256
255
  kind: attachments.kind,
256
+ thumbnailBase64: attachments.thumbnailBase64,
257
257
  createdAt: attachments.createdAt,
258
258
  })
259
259
  .from(attachments)