@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
 
9
9
  import { bootstrapConversation } from "../../memory/conversation-bootstrap.js";
10
10
  import { getConversation } from "../../memory/conversation-crud.js";
11
+ import { runScript } from "../../schedule/run-script.js";
11
12
  import {
12
13
  cancelSchedule,
13
14
  completeScheduleRun,
@@ -16,6 +17,7 @@ import {
16
17
  describeCronExpression,
17
18
  getLastScheduleConversationId,
18
19
  getSchedule,
20
+ getScheduleRuns,
19
21
  listSchedules,
20
22
  updateSchedule,
21
23
  } from "../../schedule/schedule-store.js";
@@ -46,6 +48,7 @@ function handleListSchedules(): Response {
46
48
  cronExpression: j.cronExpression,
47
49
  timezone: j.timezone,
48
50
  message: j.message,
51
+ script: j.script,
49
52
  nextRunAt: j.nextRunAt,
50
53
  lastRunAt: j.lastRunAt,
51
54
  lastStatus: j.lastStatus,
@@ -108,7 +111,7 @@ function handleCancelSchedule(id: string): Response {
108
111
  return handleListSchedules();
109
112
  }
110
113
 
111
- const VALID_MODES = ["notify", "execute"] as const;
114
+ const VALID_MODES = ["notify", "execute", "script"] as const;
112
115
  const VALID_ROUTING_INTENTS = [
113
116
  "single_channel",
114
117
  "multi_channel",
@@ -149,6 +152,7 @@ function handleUpdateSchedule(
149
152
  "expression",
150
153
  "timezone",
151
154
  "message",
155
+ "script",
152
156
  "mode",
153
157
  "routingIntent",
154
158
  "quiet",
@@ -178,6 +182,28 @@ function handleUpdateSchedule(
178
182
  return handleListSchedules();
179
183
  }
180
184
 
185
+ function handleListScheduleRuns(id: string, limit: number): Response {
186
+ const schedule = getSchedule(id);
187
+ if (!schedule) {
188
+ return httpError("NOT_FOUND", "Schedule not found", 404);
189
+ }
190
+ const runs = getScheduleRuns(id, limit);
191
+ return Response.json({
192
+ runs: runs.map((r) => ({
193
+ id: r.id,
194
+ jobId: r.jobId,
195
+ status: r.status,
196
+ startedAt: r.startedAt,
197
+ finishedAt: r.finishedAt,
198
+ durationMs: r.durationMs,
199
+ output: r.output,
200
+ error: r.error,
201
+ conversationId: r.conversationId,
202
+ createdAt: r.createdAt,
203
+ })),
204
+ });
205
+ }
206
+
181
207
  async function handleRunScheduleNow(
182
208
  id: string,
183
209
  sendMessageDeps?: SendMessageDeps,
@@ -187,6 +213,38 @@ async function handleRunScheduleNow(
187
213
  return httpError("NOT_FOUND", "Schedule not found", 404);
188
214
  }
189
215
 
216
+ // ── Script mode (shell command, no LLM) ──────────────────────────
217
+ if (schedule.mode === "script") {
218
+ if (!schedule.script) {
219
+ return httpError(
220
+ "BAD_REQUEST",
221
+ "Script schedule has no script command",
222
+ 400,
223
+ );
224
+ }
225
+ const runId = createScheduleRun(schedule.id, `script:${schedule.id}`);
226
+ try {
227
+ log.info(
228
+ { jobId: schedule.id, name: schedule.name },
229
+ "Executing script schedule manually via HTTP (run now)",
230
+ );
231
+ const result = await runScript(schedule.script);
232
+ completeScheduleRun(runId, {
233
+ status: result.exitCode === 0 ? "ok" : "error",
234
+ output: result.stdout || undefined,
235
+ error: result.stderr || undefined,
236
+ });
237
+ } catch (err) {
238
+ const errorMsg = err instanceof Error ? err.message : String(err);
239
+ log.warn(
240
+ { err, jobId: schedule.id, name: schedule.name },
241
+ "Manual script schedule execution failed",
242
+ );
243
+ completeScheduleRun(runId, { status: "error", error: errorMsg });
244
+ }
245
+ return handleListSchedules();
246
+ }
247
+
190
248
  // Check if message is a task invocation (run_task:<task_id>)
191
249
  const taskMatch = schedule.message.match(/^run_task:(\S+)$/);
192
250
  if (taskMatch) {
@@ -205,10 +263,12 @@ async function handleRunScheduleNow(
205
263
  "sendMessageDeps not available for schedule execution",
206
264
  );
207
265
  }
208
- const conversation =
209
- await sendMessageDeps.getOrCreateConversation(conversationId, {
266
+ const conversation = await sendMessageDeps.getOrCreateConversation(
267
+ conversationId,
268
+ {
210
269
  trustContext: SCHEDULE_GUARDIAN_TRUST_CONTEXT,
211
- });
270
+ },
271
+ );
212
272
  conversation.taskRunId = taskRunId;
213
273
  try {
214
274
  await conversation.processMessage(
@@ -285,10 +345,12 @@ async function handleRunScheduleNow(
285
345
  if (!sendMessageDeps) {
286
346
  throw new Error("sendMessageDeps not available for schedule execution");
287
347
  }
288
- const activeConversation =
289
- await sendMessageDeps.getOrCreateConversation(conversationId, {
348
+ const activeConversation = await sendMessageDeps.getOrCreateConversation(
349
+ conversationId,
350
+ {
290
351
  trustContext: SCHEDULE_GUARDIAN_TRUST_CONTEXT,
291
- });
352
+ },
353
+ );
292
354
  activeConversation.taskRunId = undefined;
293
355
  await activeConversation.processMessage(
294
356
  schedule.message,
@@ -331,6 +393,24 @@ export function scheduleRouteDefinitions(deps: {
331
393
  }),
332
394
  handler: () => handleListSchedules(),
333
395
  },
396
+ {
397
+ endpoint: "schedules/:id/runs",
398
+ method: "GET",
399
+ policyKey: "schedules",
400
+ summary: "List schedule runs",
401
+ description: "Return recent invocation history for a schedule.",
402
+ tags: ["schedules"],
403
+ responseBody: z.object({
404
+ runs: z.array(z.unknown()).describe("Schedule run objects"),
405
+ }),
406
+ handler: ({ params, url }) => {
407
+ const rawLimit = Number(url.searchParams.get("limit") ?? 10);
408
+ const limit = Number.isFinite(rawLimit)
409
+ ? Math.min(Math.max(Math.floor(rawLimit), 1), 100)
410
+ : 10;
411
+ return handleListScheduleRuns(params.id, limit);
412
+ },
413
+ },
334
414
  {
335
415
  endpoint: "schedules/:id/toggle",
336
416
  method: "POST",
@@ -376,7 +456,8 @@ export function scheduleRouteDefinitions(deps: {
376
456
  expression: z.string(),
377
457
  timezone: z.string(),
378
458
  message: z.string(),
379
- mode: z.string().describe("notify or execute"),
459
+ script: z.string().nullable().describe("Shell command for script mode"),
460
+ mode: z.string().describe("notify, execute, or script"),
380
461
  routingIntent: z
381
462
  .string()
382
463
  .describe("single_channel, multi_channel, or all_channels"),
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * Registry for skill-provided HTTP route handlers.
3
3
  *
4
- * Skills register route matchers + handlers at initialization time. The
5
- * runtime HTTP server checks the registry for each inbound request before
6
- * falling through to its own route table.
4
+ * Skills and plugins register route matchers + handlers at initialization
5
+ * time. The runtime HTTP server checks the registry for each inbound request
6
+ * before falling through to its own route table.
7
+ *
8
+ * Registrations are identified by an opaque {@link SkillRouteHandle} returned
9
+ * from {@link registerSkillRoute}. Callers must pass that exact handle back
10
+ * to {@link unregisterSkillRoute} to remove the registration — pattern text
11
+ * is intentionally not a stable key, because two owners can legitimately
12
+ * register the same regex, and keying on `source + flags` would let one
13
+ * owner's teardown silently drop another owner's route.
7
14
  */
8
15
 
9
16
  import { getLogger } from "../util/logger.js";
@@ -23,17 +30,62 @@ export type SkillRouteMatch =
23
30
  | { kind: "match"; route: SkillRoute; match: RegExpMatchArray }
24
31
  | { kind: "methodMismatch"; allow: string[] };
25
32
 
26
- const routes: SkillRoute[] = [];
33
+ /**
34
+ * Opaque token returned from {@link registerSkillRoute}. The token has no
35
+ * observable fields — callers must treat it as a black box whose only valid
36
+ * use is to pass it to {@link unregisterSkillRoute}. Identity comparison on
37
+ * the token is what the registry keys against, so every call to
38
+ * `registerSkillRoute` returns a fresh handle even when the route's
39
+ * `pattern`/`methods`/`handler` are deep-equal to an existing entry.
40
+ */
41
+ declare const skillRouteHandleBrand: unique symbol;
42
+ export interface SkillRouteHandle {
43
+ readonly [skillRouteHandleBrand]: true;
44
+ }
45
+
46
+ interface RegisteredRoute {
47
+ readonly handle: SkillRouteHandle;
48
+ readonly route: SkillRoute;
49
+ }
50
+
51
+ const routes: RegisteredRoute[] = [];
27
52
 
28
53
  /**
29
- * Register a skill-provided HTTP route. Called by skills at initialization time.
54
+ * Register a skill- or plugin-provided HTTP route. Called at initialization
55
+ * time. Returns an opaque handle the caller must retain and pass back to
56
+ * {@link unregisterSkillRoute} at teardown time. Do not attempt to derive
57
+ * the handle from the route's pattern — identity is the only stable key.
30
58
  */
31
- export function registerSkillRoute(route: SkillRoute): void {
32
- routes.push(route);
59
+ export function registerSkillRoute(route: SkillRoute): SkillRouteHandle {
60
+ const handle = Object.freeze({}) as SkillRouteHandle;
61
+ routes.push({ handle, route });
33
62
  log.info(
34
63
  { pattern: route.pattern.source, methods: route.methods },
35
64
  "Skill route registered",
36
65
  );
66
+ return handle;
67
+ }
68
+
69
+ /**
70
+ * Unregister a previously-registered skill route by handle.
71
+ *
72
+ * Returns `true` if a route was removed, `false` otherwise. Not finding a
73
+ * match is not an error: the plugin-shutdown path calls this best-effort for
74
+ * every handle a plugin retained, and a stale handle (e.g. the registry was
75
+ * cleared externally) should not crash shutdown.
76
+ */
77
+ export function unregisterSkillRoute(handle: SkillRouteHandle): boolean {
78
+ const index = routes.findIndex((entry) => entry.handle === handle);
79
+ if (index === -1) {
80
+ log.warn({}, "unregisterSkillRoute: no matching route found for handle");
81
+ return false;
82
+ }
83
+ const [removed] = routes.splice(index, 1);
84
+ log.info(
85
+ { pattern: removed!.route.pattern.source },
86
+ "Skill route unregistered",
87
+ );
88
+ return true;
37
89
  }
38
90
 
39
91
  /**
@@ -55,17 +107,25 @@ export function matchSkillRoute(
55
107
  method: string,
56
108
  ): SkillRouteMatch | null {
57
109
  const pathMatches: SkillRoute[] = [];
58
- for (const route of routes) {
59
- const match = path.match(route.pattern);
110
+ for (const entry of routes) {
111
+ const match = path.match(entry.route.pattern);
60
112
  if (!match) continue;
61
- if (route.methods.includes(method)) {
62
- return { kind: "match", route, match };
113
+ if (entry.route.methods.includes(method)) {
114
+ return { kind: "match", route: entry.route, match };
63
115
  }
64
- pathMatches.push(route);
116
+ pathMatches.push(entry.route);
65
117
  }
66
118
  if (pathMatches.length === 0) return null;
67
- const allow = Array.from(
68
- new Set(pathMatches.flatMap((r) => r.methods)),
69
- );
119
+ const allow = Array.from(new Set(pathMatches.flatMap((r) => r.methods)));
70
120
  return { kind: "methodMismatch", allow };
71
121
  }
122
+
123
+ /**
124
+ * Test-only helper — drops every registered route. Production code has no
125
+ * legitimate need for this; a real shutdown walks the handles each owner
126
+ * retained. Exported so tests that bypass the normal shutdown path (e.g.
127
+ * those that crash mid-bootstrap) can reset registry state between cases.
128
+ */
129
+ export function resetSkillRoutesForTests(): void {
130
+ routes.length = 0;
131
+ }
@@ -0,0 +1,68 @@
1
+ import { buildSanitizedEnv } from "../tools/terminal/safe-env.js";
2
+ import { getLogger } from "../util/logger.js";
3
+ import { getWorkspaceDir } from "../util/platform.js";
4
+
5
+ const log = getLogger("run-script");
6
+
7
+ /** Maximum combined stdout + stderr captured (bytes). */
8
+ const MAX_OUTPUT_BYTES = 10_000;
9
+ /** Default timeout for script execution (ms). */
10
+ const DEFAULT_TIMEOUT_MS = 60_000;
11
+
12
+ export interface ScriptResult {
13
+ exitCode: number;
14
+ stdout: string;
15
+ stderr: string;
16
+ }
17
+
18
+ /**
19
+ * Run a shell command and capture its output.
20
+ *
21
+ * Uses Bun.spawn with /bin/sh so the command string supports pipes,
22
+ * redirects, and shell builtins. Output is truncated to
23
+ * {@link MAX_OUTPUT_BYTES} to keep schedule_runs rows bounded.
24
+ */
25
+ export async function runScript(
26
+ command: string,
27
+ options?: { timeoutMs?: number; cwd?: string },
28
+ ): Promise<ScriptResult> {
29
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
30
+ const cwd = options?.cwd ?? getWorkspaceDir();
31
+
32
+ log.info({ command, cwd, timeoutMs }, "Running script");
33
+
34
+ const proc = Bun.spawn(["sh", "-c", command], {
35
+ cwd,
36
+ stdout: "pipe",
37
+ stderr: "pipe",
38
+ env: buildSanitizedEnv(),
39
+ });
40
+
41
+ // Race process completion against a timeout
42
+ const timeoutPromise = new Promise<never>((_, reject) => {
43
+ const timer = setTimeout(() => {
44
+ proc.kill("SIGKILL");
45
+ reject(new Error(`Script timed out after ${timeoutMs}ms`));
46
+ }, timeoutMs);
47
+ timer.unref();
48
+ // Clean up timer if process finishes first
49
+ proc.exited.then(() => clearTimeout(timer));
50
+ });
51
+
52
+ const exitCode = await Promise.race([proc.exited, timeoutPromise]);
53
+
54
+ const stdout = truncate(await new Response(proc.stdout).text());
55
+ const stderr = truncate(await new Response(proc.stderr).text());
56
+
57
+ log.info(
58
+ { command, exitCode, stdoutLen: stdout.length, stderrLen: stderr.length },
59
+ "Script completed",
60
+ );
61
+
62
+ return { exitCode, stdout, stderr };
63
+ }
64
+
65
+ function truncate(text: string): string {
66
+ if (text.length <= MAX_OUTPUT_BYTES) return text;
67
+ return text.slice(0, MAX_OUTPUT_BYTES) + "\n... (truncated)";
68
+ }
@@ -14,7 +14,7 @@ import type { ScheduleSyntax } from "./recurrence-types.js";
14
14
 
15
15
  const logger = getLogger("schedule-store");
16
16
 
17
- export type ScheduleMode = "notify" | "execute";
17
+ export type ScheduleMode = "notify" | "execute" | "script";
18
18
  export type RoutingIntent = "single_channel" | "multi_channel" | "all_channels";
19
19
  export type ScheduleStatus = "active" | "firing" | "fired" | "cancelled";
20
20
 
@@ -27,6 +27,7 @@ export interface ScheduleJob {
27
27
  cronExpression: string | null;
28
28
  timezone: string | null;
29
29
  message: string;
30
+ script: string | null;
30
31
  nextRunAt: number;
31
32
  lastRunAt: number | null;
32
33
  lastStatus: string | null;
@@ -85,6 +86,7 @@ export function createSchedule(params: {
85
86
  cronExpression?: string | null;
86
87
  timezone?: string | null;
87
88
  message: string;
89
+ script?: string | null;
88
90
  enabled?: boolean;
89
91
  createdBy?: string;
90
92
  syntax?: ScheduleSyntax;
@@ -142,6 +144,7 @@ export function createSchedule(params: {
142
144
  scheduleSyntax: syntax,
143
145
  timezone,
144
146
  message: params.message,
147
+ script: params.script ?? null,
145
148
  nextRunAt,
146
149
  lastRunAt: null as number | null,
147
150
  lastStatus: null as string | null,
@@ -217,6 +220,7 @@ export function updateSchedule(
217
220
  cronExpression?: string;
218
221
  timezone?: string | null;
219
222
  message?: string;
223
+ script?: string | null;
220
224
  enabled?: boolean;
221
225
  syntax?: ScheduleSyntax;
222
226
  expression?: string;
@@ -273,6 +277,7 @@ export function updateSchedule(
273
277
  if (updates.syntax !== undefined) set.scheduleSyntax = newSyntax;
274
278
  if (updates.timezone !== undefined) set.timezone = updates.timezone;
275
279
  if (updates.message !== undefined) set.message = updates.message;
280
+ if (updates.script !== undefined) set.script = updates.script;
276
281
  if (updates.enabled !== undefined) set.enabled = updates.enabled;
277
282
  if (updates.mode !== undefined) set.mode = updates.mode;
278
283
  if (updates.routingIntent !== undefined)
@@ -777,6 +782,7 @@ function parseJobRow(row: typeof scheduleJobs.$inferSelect): ScheduleJob {
777
782
  cronExpression: row.cronExpression,
778
783
  timezone: row.timezone,
779
784
  message: row.message,
785
+ script: row.script ?? null,
780
786
  nextRunAt: row.nextRunAt,
781
787
  lastRunAt: row.lastRunAt,
782
788
  lastStatus: row.lastStatus,
@@ -11,6 +11,7 @@ import {
11
11
  type WatcherNotifier,
12
12
  } from "../watcher/engine.js";
13
13
  import { hasSetConstructs } from "./recurrence-engine.js";
14
+ import { runScript, type ScriptResult } from "./run-script.js";
14
15
  import {
15
16
  claimDueSchedules,
16
17
  completeOneShot,
@@ -49,8 +50,6 @@ export type ScheduleNotifyModeNotifier = (payload: {
49
50
  routingHints: Record<string, unknown>;
50
51
  }) => void | Promise<void>;
51
52
 
52
- export type ScheduleNotifier = (schedule: { id: string; name: string }) => void;
53
-
54
53
  export type ScheduleConversationCreatedNotifier = (info: {
55
54
  conversationId: string;
56
55
  scheduleJobId: string;
@@ -67,7 +66,6 @@ const TICK_INTERVAL_MS = 15_000;
67
66
  export function startScheduler(
68
67
  processMessage: ScheduleMessageProcessor,
69
68
  notifyScheduleOneShot: ScheduleNotifyModeNotifier,
70
- notifySchedule: ScheduleNotifier,
71
69
  watcherNotifier?: WatcherNotifier,
72
70
  watcherEscalator?: WatcherEscalator,
73
71
  onScheduleConversationCreated?: ScheduleConversationCreatedNotifier,
@@ -82,7 +80,6 @@ export function startScheduler(
82
80
  await runScheduleOnce(
83
81
  processMessage,
84
82
  notifyScheduleOneShot,
85
- notifySchedule,
86
83
  watcherNotifier,
87
84
  watcherEscalator,
88
85
  onScheduleConversationCreated,
@@ -105,7 +102,6 @@ export function startScheduler(
105
102
  return runScheduleOnce(
106
103
  processMessage,
107
104
  notifyScheduleOneShot,
108
- notifySchedule,
109
105
  watcherNotifier,
110
106
  watcherEscalator,
111
107
  onScheduleConversationCreated,
@@ -121,7 +117,6 @@ export function startScheduler(
121
117
  async function runScheduleOnce(
122
118
  processMessage: ScheduleMessageProcessor,
123
119
  notifyScheduleOneShot: ScheduleNotifyModeNotifier,
124
- notifySchedule: ScheduleNotifier,
125
120
  watcherNotifier?: WatcherNotifier,
126
121
  watcherEscalator?: WatcherEscalator,
127
122
  onScheduleConversationCreated?: ScheduleConversationCreatedNotifier,
@@ -185,6 +180,53 @@ async function runScheduleOnce(
185
180
  continue;
186
181
  }
187
182
 
183
+ // ── Script mode (shell command, no LLM) ────────────────────────
184
+ if (job.mode === "script") {
185
+ if (!job.script) {
186
+ log.warn(
187
+ { jobId: job.id, name: job.name },
188
+ "Script schedule has no script command — skipping",
189
+ );
190
+ processed += 1;
191
+ continue;
192
+ }
193
+ const runId = createScheduleRun(job.id, `script:${job.id}`);
194
+ try {
195
+ log.info(
196
+ { jobId: job.id, name: job.name, isOneShot },
197
+ "Executing script schedule",
198
+ );
199
+ const result: ScriptResult = await runScript(job.script);
200
+ completeScheduleRun(runId, {
201
+ status: result.exitCode === 0 ? "ok" : "error",
202
+ output: result.stdout || undefined,
203
+ error: result.stderr || undefined,
204
+ });
205
+ if (result.exitCode === 0) {
206
+ if (!job.quiet) {
207
+ emitScheduleFeedEvent({
208
+ title: job.name,
209
+ summary: "Script ran.",
210
+ dedupKey: `schedule-run:${runId}`,
211
+ });
212
+ }
213
+ if (isOneShot) completeOneShot(job.id);
214
+ } else {
215
+ if (isOneShot) failOneShot(job.id);
216
+ }
217
+ } catch (err) {
218
+ const errorMsg = err instanceof Error ? err.message : String(err);
219
+ log.warn(
220
+ { err, jobId: job.id, name: job.name, isOneShot },
221
+ "Script schedule execution failed",
222
+ );
223
+ completeScheduleRun(runId, { status: "error", error: errorMsg });
224
+ if (isOneShot) failOneShot(job.id);
225
+ }
226
+ processed += 1;
227
+ continue;
228
+ }
229
+
188
230
  // ── Execute mode ────────────────────────────────────────────────
189
231
 
190
232
  // Check if message is a task invocation (run_task:<task_id>)
@@ -241,7 +283,6 @@ async function runScheduleOnce(
241
283
  } else {
242
284
  completeScheduleRun(runId, { status: "ok" });
243
285
  if (!job.quiet) {
244
- notifySchedule({ id: job.id, name: job.name });
245
286
  emitScheduleFeedEvent({
246
287
  title: job.name,
247
288
  summary: "Scheduled task ran.",
@@ -340,7 +381,6 @@ async function runScheduleOnce(
340
381
  });
341
382
  completeScheduleRun(runId, { status: "ok" });
342
383
  if (!job.quiet) {
343
- notifySchedule({ id: job.id, name: job.name });
344
384
  emitScheduleFeedEvent({
345
385
  title: job.name,
346
386
  summary: isOneShot ? "One-shot reminder ran." : "Scheduled job ran.",
@@ -37,6 +37,16 @@ export async function getCatalog(): Promise<CatalogSkill[]> {
37
37
  catalog = remote;
38
38
  }
39
39
  } catch (err) {
40
+ if (cachedCatalog) {
41
+ log.warn(
42
+ { err },
43
+ "Failed to fetch Vellum catalog, keeping stale merged cache",
44
+ );
45
+ // Reset the TTL window so subsequent calls during the outage are served
46
+ // from cache instead of re-entering fetchCatalog() on every call.
47
+ cacheTimestamp = Date.now();
48
+ return cachedCatalog;
49
+ }
40
50
  if (local.length > 0) {
41
51
  log.warn(
42
52
  { err },
@@ -44,11 +54,8 @@ export async function getCatalog(): Promise<CatalogSkill[]> {
44
54
  );
45
55
  catalog = local;
46
56
  } else {
47
- log.warn(
48
- { err },
49
- "Failed to fetch Vellum catalog, using stale cache or empty",
50
- );
51
- return cachedCatalog ?? [];
57
+ log.warn({ err }, "Failed to fetch Vellum catalog, returning empty");
58
+ return [];
52
59
  }
53
60
  }
54
61
  cachedCatalog = catalog;