@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
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Plugin bootstrap — runs every registered plugin's `init()` hook once during
3
+ * daemon startup.
4
+ *
5
+ * Plugins register themselves via side-effect imports elsewhere in the boot
6
+ * sequence (first-party) or at runtime (hot-reload). By the time
7
+ * {@link bootstrapPlugins} runs, the registry has been fully populated for
8
+ * this boot cycle. This function:
9
+ *
10
+ * 1. Registers the canonical first-party default plugins via
11
+ * {@link registerDefaultPlugins} (one per pipeline). Registration is
12
+ * idempotent so repeat calls (e.g. during integration tests) do not throw.
13
+ * 2. Walks {@link getRegisteredPlugins} in registration order.
14
+ * 3. For each plugin, consults `manifest.requiresFlag` against
15
+ * {@link isAssistantFeatureFlagEnabled}. If any listed flag is disabled,
16
+ * the plugin is skipped wholesale — no `init()`, no tool/route/skill
17
+ * contributions, and no entry in the shutdown hook. This is the primary
18
+ * mechanism for shipping experimental plugins behind a feature flag.
19
+ * 4. Resolves the plugin's `manifest.requiresCredential` entries via the
20
+ * credential store helper ({@link getSecureKeyAsync}). In Docker mode
21
+ * that helper goes through the CES HTTP API transparently; in local mode
22
+ * it hits the encrypted file store / CES RPC backend.
23
+ * 5. Validates the config block under `plugins.<name>` against
24
+ * `manifest.config` if the manifest supplies a parser-like validator
25
+ * (Zod schemas with `.parse()` are supported; anything else is passed
26
+ * through untouched).
27
+ * 6. Creates `~/.vellum/plugins-data/<plugin>/` on demand for per-plugin
28
+ * writable state and exposes it via {@link PluginInitContext.pluginStorageDir}.
29
+ * 7. Awaits `plugin.init(ctx)` sequentially. One init failure surfaces as a
30
+ * {@link PluginExecutionError} naming the offending plugin and aborts
31
+ * bootstrap — later plugins' `init()` never runs and the daemon fails
32
+ * startup cleanly rather than coming up in a half-wired state.
33
+ * 8. After a plugin's `init()` succeeds, registers any tools declared on
34
+ * `plugin.tools` with the global tool registry via
35
+ * {@link registerPluginTools}. Tool contributions land after `init()` so
36
+ * a plugin that fails mid-init never leaves partial tool registrations
37
+ * behind.
38
+ *
39
+ * A single shutdown hook is registered via
40
+ * {@link registerShutdownHook} that walks the plugin list in **reverse
41
+ * registration order**. Only plugins that actually initialized (i.e. were
42
+ * not skipped by the feature-flag gate) appear in that walk. For each such
43
+ * plugin it first unregisters the contributed tools (so `onShutdown()`
44
+ * observes a clean model-visible surface) and then awaits the optional
45
+ * `onShutdown()`. Per-plugin shutdown failures are logged and swallowed —
46
+ * the hook registry already swallows hook-level throws, but we log at the
47
+ * plugin level so the plugin name is attributed.
48
+ */
49
+
50
+ import { mkdirSync } from "node:fs";
51
+ import { join } from "node:path";
52
+
53
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
54
+ import type { AssistantConfig } from "../config/schema.js";
55
+ import { registerDefaultPlugins } from "../plugins/defaults/index.js";
56
+ import {
57
+ registerPluginSkills,
58
+ unregisterPluginSkills,
59
+ } from "../plugins/plugin-skill-contributions.js";
60
+ import {
61
+ ASSISTANT_API_VERSIONS,
62
+ getRegisteredPlugins,
63
+ } from "../plugins/registry.js";
64
+ import {
65
+ type Plugin,
66
+ PluginExecutionError,
67
+ type PluginInitContext,
68
+ type PluginSkillRegistration,
69
+ } from "../plugins/types.js";
70
+ import {
71
+ registerSkillRoute,
72
+ type SkillRouteHandle,
73
+ unregisterSkillRoute,
74
+ } from "../runtime/skill-route-registry.js";
75
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
76
+ import {
77
+ registerPluginTools,
78
+ unregisterPluginTools,
79
+ } from "../tools/registry.js";
80
+ import { getLogger } from "../util/logger.js";
81
+ import { vellumRoot } from "../util/platform.js";
82
+ import { registerShutdownHook } from "./shutdown-registry.js";
83
+
84
+ const log = getLogger("plugins-bootstrap");
85
+
86
+ /**
87
+ * Minimal context required to bootstrap the plugin layer. Kept intentionally
88
+ * small so the call site in `lifecycle.ts` can construct it from whatever
89
+ * state is already available at that point in startup.
90
+ */
91
+ export interface DaemonContext {
92
+ config: AssistantConfig;
93
+ assistantVersion: string;
94
+ }
95
+
96
+ /**
97
+ * Resolve one credential value. Returns the raw secret string or throws a
98
+ * {@link PluginExecutionError} tagged with the plugin name so the caller can
99
+ * fail startup with clear attribution.
100
+ */
101
+ async function resolveCredentialOrThrow(
102
+ pluginName: string,
103
+ credentialKey: string,
104
+ ): Promise<string> {
105
+ const value = await getSecureKeyAsync(credentialKey);
106
+ if (value === undefined || value === "") {
107
+ throw new PluginExecutionError(
108
+ `plugin ${pluginName} requires credential "${credentialKey}" but the credential store returned no value`,
109
+ pluginName,
110
+ );
111
+ }
112
+ return value;
113
+ }
114
+
115
+ /**
116
+ * Validate a plugin config block. If the manifest supplies a parser-like
117
+ * validator (Zod schemas expose `.parse()`), use it. Otherwise pass the
118
+ * config through untouched.
119
+ */
120
+ function validatePluginConfig(
121
+ pluginName: string,
122
+ validator: unknown,
123
+ raw: unknown,
124
+ ): unknown {
125
+ if (
126
+ validator != null &&
127
+ typeof validator === "object" &&
128
+ "parse" in validator &&
129
+ typeof (validator as { parse: unknown }).parse === "function"
130
+ ) {
131
+ try {
132
+ return (validator as { parse: (input: unknown) => unknown }).parse(raw);
133
+ } catch (err) {
134
+ throw new PluginExecutionError(
135
+ `plugin ${pluginName} config validation failed: ${
136
+ err instanceof Error ? err.message : String(err)
137
+ }`,
138
+ pluginName,
139
+ { cause: err },
140
+ );
141
+ }
142
+ }
143
+ return raw;
144
+ }
145
+
146
+ /**
147
+ * Read `config.plugins.<name>`. `AssistantConfigSchema` declares `plugins` as
148
+ * an optional `Record<string, unknown>`, so the field is type-safe at the
149
+ * schema boundary; per-plugin validation happens downstream via
150
+ * `plugin.manifest.config` in `validatePluginConfig`.
151
+ */
152
+ function getPluginConfigRaw(
153
+ config: AssistantConfig,
154
+ pluginName: string,
155
+ ): unknown {
156
+ return config.plugins?.[pluginName];
157
+ }
158
+
159
+ /**
160
+ * Ensure `~/.vellum/plugins-data/<name>/` exists and return its absolute path.
161
+ */
162
+ function ensurePluginStorageDir(pluginName: string): string {
163
+ const dir = join(vellumRoot(), "plugins-data", pluginName);
164
+ mkdirSync(dir, { recursive: true });
165
+ return dir;
166
+ }
167
+
168
+ /**
169
+ * Run every registered plugin's `init()` hook sequentially and install a
170
+ * reverse-order shutdown hook. See the module docstring for full semantics.
171
+ *
172
+ * Returns once every plugin has finished initialising successfully. Throws a
173
+ * {@link PluginExecutionError} on the first failure — the error message names
174
+ * the offending plugin so operators see which `init()` bailed.
175
+ *
176
+ * Must be called after any custom/third-party plugin registrations have run
177
+ * and before the first conversation is served. First-party defaults are
178
+ * registered inline via {@link registerDefaultPlugins}.
179
+ */
180
+ export async function bootstrapPlugins(ctx: DaemonContext): Promise<void> {
181
+ // Register first-party default plugins. Each default wraps one of the
182
+ // assistant's canonical pipelines (`toolExecute`, `llmCall`, ...) with a
183
+ // passthrough so the pipeline shape is explicit at boot even when no
184
+ // third-party plugins are loaded. Registration is idempotent via the
185
+ // already-registered guard so repeated calls (e.g. during integration
186
+ // tests) do not throw.
187
+ registerDefaultPlugins();
188
+
189
+ const plugins = getRegisteredPlugins();
190
+ if (plugins.length === 0) {
191
+ // No-op fast path. The default injectors normally populate the registry,
192
+ // so this branch is primarily for tests that call
193
+ // `resetPluginRegistryForTests()` and stub the default registration.
194
+ log.debug("bootstrapPlugins: registry empty — skipping");
195
+ return;
196
+ }
197
+
198
+ log.info({ count: plugins.length }, "bootstrapPlugins: initializing plugins");
199
+
200
+ // Plugins that passed `requiresFlag` gating and therefore need the full
201
+ // init → contribute → shutdown lifecycle. Plugins skipped by the flag gate
202
+ // are omitted from this list so the shutdown hook below never tears down
203
+ // capabilities that were never wired up in the first place. Each entry
204
+ // carries the opaque route handles returned by `registerSkillRoute` so
205
+ // teardown can key on identity rather than regex-pattern text — two plugins
206
+ // registering the same pattern would otherwise step on each other's routes
207
+ // during shutdown, violating the "no traffic hits a plugin handler during
208
+ // onShutdown" invariant.
209
+ const activePlugins: ActivePlugin[] = [];
210
+
211
+ // If one plugin's init or contribution phase throws, tear down any plugins
212
+ // that already fully initialized (in reverse registration order) before
213
+ // re-throwing. Without this, a mid-loop failure would leave earlier plugins
214
+ // with live tools/routes/skills and no `onShutdown()` call — the shutdown
215
+ // hook is only registered once the loop completes successfully.
216
+ async function teardownPartialInit(): Promise<void> {
217
+ for (let i = activePlugins.length - 1; i >= 0; i--) {
218
+ await teardownPlugin(activePlugins[i]!, "bootstrap-failed");
219
+ }
220
+ }
221
+
222
+ for (const plugin of plugins) {
223
+ const name = plugin.manifest.name;
224
+
225
+ // Feature-flag gating — if any key in `manifest.requiresFlag` is
226
+ // disabled, skip this plugin entirely. Skipping means: no `init()`, no
227
+ // tool / route / skill contributions, and no shutdown hook entry. A
228
+ // later boot with the flag flipped ON picks up the plugin cleanly.
229
+ const requiredFlags = plugin.manifest.requiresFlag ?? [];
230
+ let disabledFlag: string | undefined;
231
+ for (const flagKey of requiredFlags) {
232
+ if (!isAssistantFeatureFlagEnabled(flagKey, ctx.config)) {
233
+ disabledFlag = flagKey;
234
+ break;
235
+ }
236
+ }
237
+ if (disabledFlag !== undefined) {
238
+ log.info(
239
+ { plugin: name, flag: disabledFlag },
240
+ `skipping plugin ${name}: feature flag ${disabledFlag} is disabled`,
241
+ );
242
+ continue;
243
+ }
244
+
245
+ try {
246
+ // Credential resolution — gather every entry in `requiresCredential`
247
+ // before calling `init()` so the plugin receives a fully-populated map.
248
+ const credentials: Record<string, string> = {};
249
+ const required = plugin.manifest.requiresCredential ?? [];
250
+ for (const key of required) {
251
+ credentials[key] = await resolveCredentialOrThrow(name, key);
252
+ }
253
+
254
+ // Per-plugin config block, validated against the manifest's parser-like
255
+ // validator when one is declared.
256
+ const rawConfig = getPluginConfigRaw(ctx.config, name);
257
+ const config = validatePluginConfig(
258
+ name,
259
+ plugin.manifest.config,
260
+ rawConfig,
261
+ );
262
+
263
+ // Per-plugin writable data directory. Created lazily during bootstrap
264
+ // rather than at registration time so the side effect is isolated to
265
+ // the boot path.
266
+ const pluginStorageDir = ensurePluginStorageDir(name);
267
+
268
+ const initContext: PluginInitContext = {
269
+ config,
270
+ credentials,
271
+ logger: log.child({ plugin: name }),
272
+ pluginStorageDir,
273
+ assistantVersion: ctx.assistantVersion,
274
+ apiVersions: ASSISTANT_API_VERSIONS,
275
+ };
276
+
277
+ if (plugin.init) {
278
+ try {
279
+ await plugin.init(initContext);
280
+ } catch (err) {
281
+ throw new PluginExecutionError(
282
+ `plugin ${name} init() failed: ${
283
+ err instanceof Error ? err.message : String(err)
284
+ }`,
285
+ name,
286
+ { cause: err },
287
+ );
288
+ }
289
+ }
290
+
291
+ // After init succeeds, wire in the plugin's model-visible capabilities.
292
+ // Tool contributions (PR 31) register only after `init()` succeeds so a
293
+ // plugin that fails mid-init never leaves partially-wired tools behind.
294
+ // Tool registration failures are wrapped in a PluginExecutionError so
295
+ // the offending plugin name surfaces in the abort — matching the
296
+ // strict-fail semantics of `init()` errors.
297
+ if (plugin.tools && plugin.tools.length > 0) {
298
+ try {
299
+ const accepted = registerPluginTools(name, plugin.tools);
300
+ log.info(
301
+ { plugin: name, count: accepted.length },
302
+ "plugin tools registered",
303
+ );
304
+ } catch (err) {
305
+ throw new PluginExecutionError(
306
+ `plugin ${name} tool registration failed: ${
307
+ err instanceof Error ? err.message : String(err)
308
+ }`,
309
+ name,
310
+ { cause: err },
311
+ );
312
+ }
313
+ }
314
+
315
+ // Route contributions (PR 32) — registered after init() succeeds so a
316
+ // plugin that fails to initialize never exposes a half-wired HTTP
317
+ // surface. Mirrors the skill-route registry shape; see
318
+ // {@link PluginRouteRegistration}. Retain every returned handle so the
319
+ // teardown path unregisters by identity rather than pattern text — two
320
+ // plugins (or a plugin and a skill) that happen to register the same
321
+ // regex must not evict each other's routes during shutdown.
322
+ const routeHandles: SkillRouteHandle[] = [];
323
+ if (plugin.routes && plugin.routes.length > 0) {
324
+ for (const route of plugin.routes) {
325
+ routeHandles.push(registerSkillRoute(route));
326
+ }
327
+ log.info(
328
+ { plugin: name, count: plugin.routes.length },
329
+ "plugin routes registered",
330
+ );
331
+ }
332
+
333
+ // Skills register into the in-memory plugin-skill catalog so
334
+ // `skill_load` / `skill_execute` can resolve them alongside filesystem
335
+ // skills. A registration failure aborts bootstrap with the plugin named
336
+ // — same strict-fail posture as init() throws.
337
+ if (plugin.skills && plugin.skills.length > 0) {
338
+ try {
339
+ // `plugin.skills` is typed as `PluginSkillRegistration[]` at the
340
+ // Plugin interface — the type assertion here is a narrowing from
341
+ // that generic slot into the concrete shape the registry expects.
342
+ registerPluginSkills(
343
+ name,
344
+ plugin.skills as readonly PluginSkillRegistration[],
345
+ );
346
+ } catch (err) {
347
+ throw new PluginExecutionError(
348
+ `plugin ${name} skill registration failed: ${
349
+ err instanceof Error ? err.message : String(err)
350
+ }`,
351
+ name,
352
+ { cause: err },
353
+ );
354
+ }
355
+ }
356
+
357
+ activePlugins.push({ plugin, routeHandles });
358
+
359
+ log.info({ plugin: name }, "plugin initialized");
360
+ } catch (err) {
361
+ // Tear down every plugin that already made it through its full init +
362
+ // contribution phase, in reverse order, before propagating the error.
363
+ // Without this, the caller would see a partially-wired registry (tools,
364
+ // routes, skills all live) with no shutdown hook to clean them up, since
365
+ // `registerShutdownHook` is only called after the loop completes.
366
+ await teardownPartialInit();
367
+ throw err;
368
+ }
369
+ }
370
+
371
+ // Shutdown hook — walks plugins in REVERSE registration order. We snapshot
372
+ // only the plugins that actually initialized so later registrations (if
373
+ // any) don't end up being torn down by this hook, and plugins skipped by
374
+ // `requiresFlag` gating do not appear in the tear-down list. Subsequent
375
+ // bootstraps (hot-reload) would register their own hook.
376
+ //
377
+ // For each plugin we:
378
+ // 1. Unregister contributed HTTP routes so incoming requests stop hitting
379
+ // the plugin's handlers before its state is torn down.
380
+ // 2. Unregister contributed tools so the model-visible tool surface is
381
+ // cleared before `onShutdown()` runs.
382
+ // 3. Call `onShutdown()` (if defined) so the plugin can release resources
383
+ // (background tasks, timers, connections) with its tools and routes
384
+ // already removed.
385
+ // 4. Unregister contributed skills via the ref-counted helper. Skills tear
386
+ // down last so `onShutdown()` can still invoke skill-resolving code
387
+ // (e.g. to flush pending skill work) before the catalog is emptied.
388
+ // This mirrors the symmetry of registerPluginSkills() — every
389
+ // successful registration must get a matching unregister call,
390
+ // regardless of whether onShutdown throws.
391
+ const shutdownSnapshot: ActivePlugin[] = [...activePlugins];
392
+ registerShutdownHook("plugins", async (reason) => {
393
+ for (let i = shutdownSnapshot.length - 1; i >= 0; i--) {
394
+ await teardownPlugin(shutdownSnapshot[i]!, reason);
395
+ }
396
+ });
397
+ }
398
+
399
+ /**
400
+ * One plugin that made it through the full init + contribution phase. Holds
401
+ * every opaque {@link SkillRouteHandle} issued by `registerSkillRoute` so
402
+ * teardown can revoke exactly the routes this plugin contributed, even when
403
+ * the regex pattern text collides with another owner's registration.
404
+ */
405
+ interface ActivePlugin {
406
+ readonly plugin: Plugin;
407
+ readonly routeHandles: readonly SkillRouteHandle[];
408
+ }
409
+
410
+ /**
411
+ * Tear down a single fully-initialized plugin: unregister routes, unregister
412
+ * tools, invoke `onShutdown()` if present, then unregister skills. Every step
413
+ * swallows errors and logs with plugin attribution so one bad plugin can't
414
+ * block teardown of the rest.
415
+ *
416
+ * Shared between the normal shutdown hook and the bootstrap error path; both
417
+ * consume plugins that already cleared every contribution step.
418
+ */
419
+ async function teardownPlugin(
420
+ active: ActivePlugin,
421
+ reason: string,
422
+ ): Promise<void> {
423
+ const { plugin, routeHandles } = active;
424
+ const name = plugin.manifest.name;
425
+
426
+ // Unregister model-visible surfaces before invoking `onShutdown()` so the
427
+ // plugin's onShutdown hook observes a registry state where its tools and
428
+ // routes are already gone. `unregisterPluginTools` is a no-op when the
429
+ // plugin never contributed tools, so we don't need to guard on
430
+ // `plugin.tools` here. Route unregistration keys on the opaque handles
431
+ // retained at registration time — pattern text is not a stable key because
432
+ // two owners can legitimately register the same regex.
433
+ for (const handle of routeHandles) {
434
+ try {
435
+ unregisterSkillRoute(handle);
436
+ } catch (err) {
437
+ log.warn(
438
+ { err, plugin: name },
439
+ "plugin route unregister failed (continuing)",
440
+ );
441
+ }
442
+ }
443
+
444
+ try {
445
+ unregisterPluginTools(name);
446
+ } catch (err) {
447
+ log.warn(
448
+ { err, plugin: name, reason },
449
+ "plugin tool unregister failed (continuing with remaining plugins)",
450
+ );
451
+ }
452
+
453
+ if (plugin.onShutdown) {
454
+ try {
455
+ await plugin.onShutdown();
456
+ } catch (err) {
457
+ // Swallow — we want every plugin's onShutdown to get a chance to run
458
+ // even when an earlier one throws. The outer runShutdownHooks already
459
+ // logs at hook level, but the plugin-name attribution here is what
460
+ // operators read first.
461
+ log.warn(
462
+ { err, plugin: name, reason },
463
+ "plugin onShutdown failed (continuing with remaining plugins)",
464
+ );
465
+ }
466
+ }
467
+
468
+ if (plugin.skills && plugin.skills.length > 0) {
469
+ try {
470
+ unregisterPluginSkills(name);
471
+ } catch (err) {
472
+ log.warn(
473
+ { err, plugin: name, reason },
474
+ "plugin skill unregistration failed (continuing with remaining plugins)",
475
+ );
476
+ }
477
+ }
478
+ }
@@ -2,14 +2,19 @@ import { existsSync } from "node:fs";
2
2
 
3
3
  import { getWorkspacePromptPath } from "../util/platform.js";
4
4
 
5
- /**
6
- * The canned assistant response for the wake-up greeting on a fresh workspace.
7
- * Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
8
- * time," and invites the user to lead with whatever they want — a task, a
9
- * question, or getting to know each other.
10
- */
11
- export const CANNED_FIRST_GREETING =
12
- "Hey — I'm brand new. No name, no memories, no idea who you are yet. I'll get sharper the more we work together. What can I do for you?";
5
+ export interface OnboardingGreetingContext {
6
+ tools: string[];
7
+ tasks: string[];
8
+ tone: string;
9
+ userName?: string;
10
+ assistantName?: string;
11
+ }
12
+
13
+ export const CANNED_FIRST_GREETING = [
14
+ "Hey — brand new, no name, no memories, no idea who you are yet. I'll get sharper the more we work together.",
15
+ "",
16
+ "What can I do for you? Or I can ask you some questions to get started.",
17
+ ].join("\n");
13
18
 
14
19
  /**
15
20
  * Returns `true` when all of the following are true:
@@ -31,11 +36,183 @@ export function isWakeUpGreeting(
31
36
  );
32
37
  }
33
38
 
34
- /**
35
- * Returns the canned first-greeting string. Simple getter that exists to keep
36
- * the call site consistent and allow future flexibility (e.g., locale-aware
37
- * greetings) without changing the API.
38
- */
39
- export function getCannedFirstGreeting(): string {
39
+ export function getCannedFirstGreeting(
40
+ onboarding?: OnboardingGreetingContext,
41
+ ): string {
42
+ if (onboarding) {
43
+ return buildPersonalizedGreeting(onboarding);
44
+ }
40
45
  return CANNED_FIRST_GREETING;
41
46
  }
47
+
48
+ const TOOL_LABELS: Record<string, string> = {
49
+ gmail: "Gmail",
50
+ outlook: "Outlook",
51
+ "google-calendar": "Google Calendar",
52
+ slack: "Slack",
53
+ notion: "Notion",
54
+ linear: "Linear",
55
+ jira: "Jira",
56
+ github: "GitHub",
57
+ figma: "Figma",
58
+ "google-drive": "Google Drive",
59
+ excel: "Excel",
60
+ "apple-notes": "Apple Notes",
61
+ };
62
+
63
+ const TASK_PRIORITY: string[] = [
64
+ "code-building",
65
+ "project-management",
66
+ "writing",
67
+ "research",
68
+ "scheduling",
69
+ "personal",
70
+ ];
71
+
72
+ interface Guess {
73
+ text: string;
74
+ preferredTools: string[];
75
+ }
76
+
77
+ const SINGLE_GUESSES: Record<string, Guess> = {
78
+ "code-building": {
79
+ text: "shipping something or debugging",
80
+ preferredTools: ["github", "linear", "jira"],
81
+ },
82
+ writing: {
83
+ text: "drafting something or cleaning up docs",
84
+ preferredTools: ["notion", "google-drive", "apple-notes"],
85
+ },
86
+ research: {
87
+ text: "digging into a topic or making sense of something",
88
+ preferredTools: ["notion", "google-drive"],
89
+ },
90
+ "project-management": {
91
+ text: "planning the week, writing a spec, or pushing something forward",
92
+ preferredTools: ["notion", "linear", "google-drive"],
93
+ },
94
+ scheduling: {
95
+ text: "planning the week or prepping for meetings",
96
+ preferredTools: ["google-calendar", "outlook", "linear"],
97
+ },
98
+ personal: {
99
+ text: "juggling travel, bills, or household stuff",
100
+ preferredTools: ["gmail", "google-calendar", "apple-notes"],
101
+ },
102
+ };
103
+
104
+ const COMBO_GUESSES: Record<string, Guess> = {
105
+ "code-building+project-management": {
106
+ text: "shipping code or figuring out what to ship next",
107
+ preferredTools: ["github", "linear", "jira"],
108
+ },
109
+ "code-building+writing": {
110
+ text: "shipping code or writing something up",
111
+ preferredTools: ["github", "linear", "jira"],
112
+ },
113
+ "project-management+writing": {
114
+ text: "writing a spec or pushing something forward",
115
+ preferredTools: ["notion", "linear", "google-drive"],
116
+ },
117
+ "research+writing": {
118
+ text: "drafting something or digging into a topic",
119
+ preferredTools: ["notion", "google-drive"],
120
+ },
121
+ "project-management+scheduling": {
122
+ text: "planning the week or prepping for something",
123
+ preferredTools: ["google-calendar", "outlook", "linear"],
124
+ },
125
+ };
126
+
127
+ function comboKey(a: string, b: string): string {
128
+ return [a, b].sort().join("+");
129
+ }
130
+
131
+ function highestPriorityTask(tasks: string[]): string | undefined {
132
+ for (const t of TASK_PRIORITY) {
133
+ if (tasks.includes(t)) return t;
134
+ }
135
+ return tasks[0];
136
+ }
137
+
138
+ function buildIntroLine(userName?: string, assistantName?: string): string {
139
+ const namepart = userName ? `Hey ${userName},` : "Hey,";
140
+ const who = assistantName
141
+ ? `I'm ${assistantName}. Brand new, and I'll get sharper the more we work together.`
142
+ : "brand new, and I'll get sharper the more we work together.";
143
+ return `${namepart} ${who}`;
144
+ }
145
+
146
+ function pickRelevantTools(
147
+ preferredTools: string[],
148
+ userTools: string[],
149
+ ): string[] {
150
+ const userSet = new Set(userTools);
151
+ const matched: string[] = [];
152
+ for (const t of preferredTools) {
153
+ if (userSet.has(t)) {
154
+ matched.push(TOOL_LABELS[t] ?? t);
155
+ if (matched.length === 2) break;
156
+ }
157
+ }
158
+ return matched;
159
+ }
160
+
161
+ function buildSpecificGuess(tasks: string[], tools: string[]): string {
162
+ let guess: Guess | undefined;
163
+
164
+ if (tasks.length === 2) {
165
+ guess = COMBO_GUESSES[comboKey(tasks[0], tasks[1])];
166
+ }
167
+
168
+ if (!guess) {
169
+ const top = highestPriorityTask(tasks);
170
+ guess = top ? SINGLE_GUESSES[top] : undefined;
171
+ }
172
+
173
+ if (!guess) return "";
174
+
175
+ const relevant = pickRelevantTools(guess.preferredTools, tools);
176
+
177
+ if (relevant.length === 2) {
178
+ return `You mentioned using ${relevant[0]} and ${relevant[1]} — probably ${guess.text}? Am I on the right track, or something else on your mind?`;
179
+ }
180
+ if (relevant.length === 1) {
181
+ return `You mentioned using ${relevant[0]} — probably ${guess.text}? Am I on the right track, or something else on your mind?`;
182
+ }
183
+
184
+ return `Probably ${guess.text}? Am I on the right track, or something else on your mind?`;
185
+ }
186
+
187
+ function buildPersonalizedGreeting(ctx: OnboardingGreetingContext): string {
188
+ const userName = ctx.userName?.trim();
189
+ const assistantName = ctx.assistantName?.trim();
190
+
191
+ const hasName = userName && userName.length > 0;
192
+ const hasTasks = ctx.tasks.length > 0;
193
+ const hasTools = ctx.tools.length > 0;
194
+
195
+ const hasAssistantName = assistantName && assistantName.length > 0;
196
+
197
+ if (!hasName && !hasTasks && !hasTools && !hasAssistantName) {
198
+ return CANNED_FIRST_GREETING;
199
+ }
200
+
201
+ const intro = buildIntroLine(hasName ? userName : undefined, assistantName);
202
+
203
+ let secondParagraph: string;
204
+
205
+ if (ctx.tasks.length >= 4) {
206
+ secondParagraph =
207
+ "Looks like you wear a lot of hats. Where should we start?";
208
+ } else if (ctx.tasks.length === 0) {
209
+ secondParagraph =
210
+ "What's on your plate? Or if it's easier, I can ask you a few questions to get oriented.";
211
+ } else {
212
+ secondParagraph =
213
+ buildSpecificGuess(ctx.tasks, ctx.tools) ||
214
+ "What's on your plate? Or if it's easier, I can ask you a few questions to get oriented.";
215
+ }
216
+
217
+ return [intro, "", secondParagraph].join("\n");
218
+ }