@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -436,6 +436,108 @@ describe("installPlugin — marketplace resolution", () => {
436
436
  expect(meta.contentHash).toMatch(/^v2:[0-9a-f]{64}$/);
437
437
  });
438
438
 
439
+ test("installs a plugin rooted at a sub-path, copying only that subtree", async () => {
440
+ // GIVEN a marketplace entry whose source pins a directory *within* a repo
441
+ // (a monorepo that ships several plugins) rather than the repo root.
442
+ const NESTED_SHA = "0f".repeat(20);
443
+ const NESTED_MANIFEST = {
444
+ name: "vellum-assistant",
445
+ plugins: [
446
+ {
447
+ name: "nested-plugin",
448
+ source: {
449
+ source: "github",
450
+ repo: "example-org/monorepo",
451
+ path: "packages/my-plugin",
452
+ ref: NESTED_SHA,
453
+ },
454
+ description: "A plugin that lives in a monorepo sub-directory.",
455
+ },
456
+ ],
457
+ };
458
+ const fetch = makeContentsFetch({ tree: {}, manifest: NESTED_MANIFEST });
459
+ // The clone carries files both at the repo root and under the pinned
460
+ // sub-path; only the sub-path subtree should be materialized.
461
+ const runGit = fakeGitRunner({
462
+ tree: {
463
+ "package.json": '{"name":"monorepo-root"}',
464
+ "README.md": "# monorepo root",
465
+ "packages/my-plugin/package.json": '{"name":"nested-plugin"}',
466
+ "packages/my-plugin/README.md": "# nested plugin",
467
+ "packages/my-plugin/hooks/init.ts": "export default async () => {};",
468
+ "packages/other-plugin/package.json": '{"name":"other"}',
469
+ },
470
+ commit: NESTED_SHA,
471
+ });
472
+
473
+ // WHEN we install by name
474
+ const result = await installPlugin(
475
+ { name: "nested-plugin", ref: "main" },
476
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
477
+ );
478
+
479
+ // THEN only the sub-path subtree lands, rooted at <pluginsDir>/nested-plugin
480
+ const target = join(pluginsDir, "nested-plugin");
481
+ expect(result.target).toBe(target);
482
+ expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
483
+ '{"name":"nested-plugin"}',
484
+ );
485
+ expect(existsSync(join(target, "README.md"))).toBe(true);
486
+ expect(existsSync(join(target, "hooks", "init.ts"))).toBe(true);
487
+ // AND the repo-root and sibling-package files are NOT copied in — the
488
+ // install is scoped to the pinned directory.
489
+ expect(result.fileCount).toBe(3);
490
+ expect(readFileSync(join(target, "package.json"), "utf-8")).not.toContain(
491
+ "monorepo-root",
492
+ );
493
+ expect(existsSync(join(target, "packages"))).toBe(false);
494
+
495
+ // AND provenance records the sub-path so an upgrade/diff re-resolves the
496
+ // same directory rather than the repo root.
497
+ const meta = readInstallMeta(target);
498
+ expect(meta?.source.owner).toBe("example-org");
499
+ expect(meta?.source.repo).toBe("monorepo");
500
+ expect(meta?.source.path).toBe("packages/my-plugin");
501
+ expect(meta?.source.ref).toBe(NESTED_SHA);
502
+ });
503
+
504
+ test("a sub-path that does not exist in the clone surfaces a clean not-found", async () => {
505
+ // GIVEN an entry pinning a directory the cloned ref doesn't contain
506
+ const MISSING_SHA = "1a".repeat(20);
507
+ const MISSING_PATH_MANIFEST = {
508
+ name: "vellum-assistant",
509
+ plugins: [
510
+ {
511
+ name: "nested-plugin",
512
+ source: {
513
+ source: "github",
514
+ repo: "example-org/monorepo",
515
+ path: "packages/does-not-exist",
516
+ ref: MISSING_SHA,
517
+ },
518
+ },
519
+ ],
520
+ };
521
+ const fetch = makeContentsFetch({
522
+ tree: {},
523
+ manifest: MISSING_PATH_MANIFEST,
524
+ });
525
+ const runGit = fakeGitRunner({
526
+ tree: { "package.json": '{"name":"monorepo-root"}' },
527
+ commit: MISSING_SHA,
528
+ });
529
+
530
+ // WHEN we install
531
+ // THEN the absent sub-path yields a hard not-found and nothing is staged
532
+ await expect(
533
+ installPlugin(
534
+ { name: "nested-plugin", ref: "main" },
535
+ { fetch, runGit, workspacePluginsDir: pluginsDir },
536
+ ),
537
+ ).rejects.toBeInstanceOf(PluginNotFoundError);
538
+ expect(readdirSync(pluginsDir)).toEqual([]);
539
+ });
540
+
439
541
  test("refuses to install when the checked-out commit differs from the pinned SHA", async () => {
440
542
  // GIVEN a clone whose resolved HEAD does not match the manifest's pinned
441
543
  // commit SHA — i.e. the upstream object served something unexpected
@@ -17,7 +17,7 @@ import { tmpdir } from "node:os";
17
17
  import { join } from "node:path";
18
18
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
19
19
 
20
- import { listInstalledPlugins } from "../list-installed-plugins.js";
20
+ import { listAllPlugins, listInstalledPlugins } from "../list-installed-plugins.js";
21
21
 
22
22
  let pluginsDir: string;
23
23
 
@@ -152,3 +152,162 @@ describe("listInstalledPlugins", () => {
152
152
  expect(result.map((p) => p.name)).toEqual(["valid"]);
153
153
  });
154
154
  });
155
+
156
+ describe("listAllPlugins", () => {
157
+ test("includes default plugins with source=default", () => {
158
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
159
+ const defaults = result.filter((p) => p.source === "default");
160
+ // All 15 default plugins should be present.
161
+ expect(defaults.length).toBe(15);
162
+ // Names should all start with "default-".
163
+ expect(defaults.every((p) => p.name.startsWith("default-"))).toBe(true);
164
+ // None should be disabled by default in a fresh temp dir.
165
+ expect(defaults.every((p) => !p.disabled)).toBe(true);
166
+ });
167
+
168
+ test("includes user plugins with source=user", () => {
169
+ mkdirSync(join(pluginsDir, "my-plugin"));
170
+ writeFileSync(
171
+ join(pluginsDir, "my-plugin", "package.json"),
172
+ JSON.stringify({ name: "my-plugin", version: "1.0.0" }),
173
+ );
174
+
175
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
176
+ const user = result.filter((p) => p.source === "user");
177
+ expect(user).toHaveLength(1);
178
+ expect(user[0]!.name).toBe("my-plugin");
179
+ expect(user[0]!.disabled).toBe(false);
180
+ });
181
+
182
+ test("user plugins appear before default plugins", () => {
183
+ mkdirSync(join(pluginsDir, "zzz-user"));
184
+ writeFileSync(
185
+ join(pluginsDir, "zzz-user", "package.json"),
186
+ JSON.stringify({ name: "zzz-user", version: "1.0.0" }),
187
+ );
188
+
189
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
190
+ const firstDefaultIdx = result.findIndex((p) => p.source === "default");
191
+ const lastUserIdx = result
192
+ .map((p, i) => (p.source === "user" ? i : -1))
193
+ .filter((i) => i >= 0)
194
+ .pop();
195
+
196
+ expect(lastUserIdx).toBeDefined();
197
+ expect(firstDefaultIdx).toBeGreaterThan(lastUserIdx!);
198
+ });
199
+
200
+ test("detects disabled state for user plugins", () => {
201
+ mkdirSync(join(pluginsDir, "my-plugin"));
202
+ writeFileSync(
203
+ join(pluginsDir, "my-plugin", "package.json"),
204
+ JSON.stringify({ name: "my-plugin", version: "1.0.0" }),
205
+ );
206
+ writeFileSync(join(pluginsDir, "my-plugin", ".disabled"), "");
207
+
208
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
209
+ const entry = result.find((p) => p.name === "my-plugin");
210
+ expect(entry).toBeDefined();
211
+ expect(entry!.disabled).toBe(true);
212
+ });
213
+
214
+ test("detects disabled state for default plugins via stub directory", () => {
215
+ mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
216
+ writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
217
+
218
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
219
+ const advisor = result.find((p) => p.name === "default-advisor");
220
+ expect(advisor).toBeDefined();
221
+ expect(advisor!.disabled).toBe(true);
222
+ });
223
+
224
+ test("default stub directory is excluded from user listing", () => {
225
+ mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
226
+ writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
227
+
228
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
229
+ // Should appear exactly once, as a default entry (not a user entry).
230
+ const advisorEntries = result.filter((p) => p.name === "default-advisor");
231
+ expect(advisorEntries).toHaveLength(1);
232
+ expect(advisorEntries[0]!.source).toBe("default");
233
+ });
234
+
235
+ test("sort order: enabled user, disabled user, enabled default, disabled default", () => {
236
+ // User plugins
237
+ mkdirSync(join(pluginsDir, "aaa-enabled"));
238
+ writeFileSync(
239
+ join(pluginsDir, "aaa-enabled", "package.json"),
240
+ JSON.stringify({ name: "aaa-enabled", version: "1.0.0" }),
241
+ );
242
+ mkdirSync(join(pluginsDir, "bbb-disabled"));
243
+ writeFileSync(
244
+ join(pluginsDir, "bbb-disabled", "package.json"),
245
+ JSON.stringify({ name: "bbb-disabled", version: "1.0.0" }),
246
+ );
247
+ writeFileSync(join(pluginsDir, "bbb-disabled", ".disabled"), "");
248
+
249
+ // Disable one default plugin
250
+ mkdirSync(join(pluginsDir, "default-advisor"), { recursive: true });
251
+ writeFileSync(join(pluginsDir, "default-advisor", ".disabled"), "");
252
+
253
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
254
+
255
+ const enabledUserIdx = result.findIndex(
256
+ (p) => p.source === "user" && !p.disabled,
257
+ );
258
+ const disabledUserIdx = result.findIndex(
259
+ (p) => p.source === "user" && p.disabled,
260
+ );
261
+ const enabledDefaultIdx = result.findIndex(
262
+ (p) => p.source === "default" && !p.disabled,
263
+ );
264
+ const disabledDefaultIdx = result.findIndex(
265
+ (p) => p.source === "default" && p.disabled,
266
+ );
267
+
268
+ // All groups present.
269
+ expect(enabledUserIdx).toBeGreaterThanOrEqual(0);
270
+ expect(disabledUserIdx).toBeGreaterThan(enabledUserIdx);
271
+ expect(enabledDefaultIdx).toBeGreaterThan(disabledUserIdx);
272
+ expect(disabledDefaultIdx).toBeGreaterThan(enabledDefaultIdx);
273
+ });
274
+
275
+ test("user plugins sorted by install date within each group", () => {
276
+ // Create two user plugins with different install dates.
277
+ // First plugin (older).
278
+ mkdirSync(join(pluginsDir, "older-plugin"));
279
+ writeFileSync(
280
+ join(pluginsDir, "older-plugin", "package.json"),
281
+ JSON.stringify({ name: "older-plugin", version: "1.0.0" }),
282
+ );
283
+ writeFileSync(
284
+ join(pluginsDir, "older-plugin", "install-meta.json"),
285
+ JSON.stringify({ installedAt: "2025-01-01T00:00:00.000Z" }),
286
+ );
287
+
288
+ // Second plugin (newer).
289
+ mkdirSync(join(pluginsDir, "newer-plugin"));
290
+ writeFileSync(
291
+ join(pluginsDir, "newer-plugin", "package.json"),
292
+ JSON.stringify({ name: "newer-plugin", version: "1.0.0" }),
293
+ );
294
+ writeFileSync(
295
+ join(pluginsDir, "newer-plugin", "install-meta.json"),
296
+ JSON.stringify({ installedAt: "2025-06-01T00:00:00.000Z" }),
297
+ );
298
+
299
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
300
+ const userPlugins = result.filter((p) => p.source === "user");
301
+ expect(userPlugins).toHaveLength(2);
302
+ // Older plugin should come first (install date ascending).
303
+ expect(userPlugins[0]!.name).toBe("older-plugin");
304
+ expect(userPlugins[1]!.name).toBe("newer-plugin");
305
+ });
306
+
307
+ test("default plugins have version from their manifest", () => {
308
+ const result = listAllPlugins({ workspacePluginsDir: pluginsDir });
309
+ const advisor = result.find((p) => p.name === "default-advisor");
310
+ expect(advisor).toBeDefined();
311
+ expect(advisor!.packageJson?.version).toBeTruthy();
312
+ });
313
+ });
@@ -13,10 +13,25 @@
13
13
  */
14
14
 
15
15
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
16
- import { join } from "node:path";
16
+ import { dirname, join } from "node:path";
17
17
 
18
18
  import { getWorkspacePluginsDir } from "../../util/platform.js";
19
19
 
20
+ /**
21
+ * Directory containing first-party default plugin packages. Each subdirectory
22
+ * has a `package.json` with `name` (prefixed `default-`) and `version`.
23
+ * Read from the filesystem at call time to avoid pulling hook/tool
24
+ * implementations into the CLI process (which would create circular
25
+ * dependencies in test environments).
26
+ */
27
+ const DEFAULT_PLUGINS_DIR = join(
28
+ dirname(new URL(import.meta.url).pathname),
29
+ "..",
30
+ "..",
31
+ "plugins",
32
+ "defaults",
33
+ );
34
+
20
35
  /** Minimal manifest fields surfaced to the CLI. */
21
36
  export interface PluginPackageMetadata {
22
37
  readonly name?: string;
@@ -40,6 +55,20 @@ export interface InstalledPluginInfo {
40
55
  readonly issues: readonly string[];
41
56
  }
42
57
 
58
+ /** Where the plugin comes from. */
59
+ export type PluginSource = "user" | "default";
60
+
61
+ /**
62
+ * Extended plugin entry that includes source (`user` vs `default`) and
63
+ * disabled status. Used by {@link listAllPlugins}.
64
+ */
65
+ export interface AllPluginInfo extends InstalledPluginInfo {
66
+ /** Whether this is a user-installed or first-party default plugin. */
67
+ readonly source: PluginSource;
68
+ /** Whether the plugin is disabled via a `.disabled` sentinel file. */
69
+ readonly disabled: boolean;
70
+ }
71
+
43
72
  /** Options accepted by {@link listInstalledPlugins}. */
44
73
  export interface ListInstalledPluginsOptions {
45
74
  /** Override the workspace plugins directory. Falls back to {@link getWorkspacePluginsDir}. */
@@ -146,3 +175,152 @@ function readPluginEntry(
146
175
 
147
176
  return { name, target, packageJson, issues };
148
177
  }
178
+
179
+ /**
180
+ * List all plugins — both user-installed (from `<workspace>/plugins/`) and
181
+ * first-party defaults (from the source tree). Each entry is annotated with
182
+ * its `source` (`"user"` or `"default"`) and `disabled` status (whether a
183
+ * `.disabled` sentinel file exists in the plugin's workspace directory).
184
+ *
185
+ * For user plugins, the `.disabled` file lives in the plugin's own install
186
+ * directory. For default plugins, it lives in a stub directory at
187
+ * `<workspace>/plugins/<manifest-name>/` (created by `plugins disable`).
188
+ *
189
+ * Stub directories created by `plugins disable <default-name>` are excluded
190
+ * from the user listing so a disabled default plugin appears only once (as a
191
+ * default entry, not a duplicate user entry with "missing package.json").
192
+ *
193
+ * Sort order:
194
+ * 1. Enabled user plugins (by install date, oldest first — matches
195
+ * hook/tool resolution order)
196
+ * 2. Disabled user plugins (by install date)
197
+ * 3. Enabled default plugins (by repo array order — matches registration
198
+ * order which fixes hook-chain order)
199
+ * 4. Disabled default plugins (by repo array order)
200
+ */
201
+ export function listAllPlugins(
202
+ opts: ListInstalledPluginsOptions = {},
203
+ ): AllPluginInfo[] {
204
+ const pluginsDir = opts.workspacePluginsDir ?? getWorkspacePluginsDir();
205
+
206
+ // ── User plugins ───────────────────────────────────────────────────────
207
+ // Filter out default-plugin stub directories (created by `plugins disable
208
+ // default-<name>`) so they don't show up as duplicate user entries.
209
+ const defaultNames = new Set(
210
+ readDefaultPluginManifests().map((m) => m.name),
211
+ );
212
+ const userPlugins: AllPluginInfo[] = listInstalledPlugins(opts)
213
+ .filter((entry) => !defaultNames.has(entry.name))
214
+ .map((entry) => ({
215
+ ...entry,
216
+ source: "user" as const,
217
+ disabled: existsSync(join(entry.target, ".disabled")),
218
+ }));
219
+
220
+ // ── Default plugins ────────────────────────────────────────────────────
221
+ // Default plugins live in the source tree at src/plugins/defaults/<name>/.
222
+ // Read each package.json from the filesystem to get name+version without
223
+ // importing hook/tool implementations (which would create circular
224
+ // dependencies in test environments). The .disabled sentinel lives in a
225
+ // stub directory at <workspace>/plugins/<manifest-name>/.
226
+ // readDefaultPluginManifests returns in repo array (registration) order.
227
+ const defaultPlugins: AllPluginInfo[] = readDefaultPluginManifests().map(
228
+ (manifest) => {
229
+ const target = join(pluginsDir, manifest.name);
230
+ const disabled = existsSync(join(target, ".disabled"));
231
+ return {
232
+ name: manifest.name,
233
+ target,
234
+ packageJson: {
235
+ name: manifest.name,
236
+ version: manifest.version,
237
+ },
238
+ issues: [],
239
+ source: "default" as const,
240
+ disabled,
241
+ };
242
+ },
243
+ );
244
+
245
+ // Sort: enabled user (install date), disabled user (install date),
246
+ // enabled default (repo order), disabled default (repo order).
247
+ const enabledUser = userPlugins.filter((p) => !p.disabled);
248
+ const disabledUser = userPlugins.filter((p) => p.disabled);
249
+ const enabledDefault = defaultPlugins.filter((p) => !p.disabled);
250
+ const disabledDefault = defaultPlugins.filter((p) => p.disabled);
251
+
252
+ enabledUser.sort((a, b) => getPluginInstallDate(a) - getPluginInstallDate(b));
253
+ disabledUser.sort(
254
+ (a, b) => getPluginInstallDate(a) - getPluginInstallDate(b),
255
+ );
256
+ // enabledDefault and disabledDefault keep repo array order (no sort).
257
+
258
+ return [...enabledUser, ...disabledUser, ...enabledDefault, ...disabledDefault];
259
+ }
260
+
261
+ interface DefaultPluginManifest {
262
+ readonly name: string;
263
+ readonly version?: string;
264
+ }
265
+
266
+ /**
267
+ * Read first-party default plugin manifests from the filesystem. Each
268
+ * subdirectory under {@link DEFAULT_PLUGINS_DIR} that has a `package.json`
269
+ * with a `name` field is included. This avoids importing `defaults/index.ts`
270
+ * (which would pull in hook/tool implementations and create circular
271
+ * dependencies in test environments).
272
+ */
273
+ function readDefaultPluginManifests(): readonly DefaultPluginManifest[] {
274
+ if (!existsSync(DEFAULT_PLUGINS_DIR)) return [];
275
+
276
+ const entries = readdirSync(DEFAULT_PLUGINS_DIR, { withFileTypes: true })
277
+ .filter((e) => e.isDirectory())
278
+ .map((e) => e.name)
279
+ .sort();
280
+
281
+ const manifests: DefaultPluginManifest[] = [];
282
+ for (const name of entries) {
283
+ const pkgJsonPath = join(DEFAULT_PLUGINS_DIR, name, "package.json");
284
+ if (!existsSync(pkgJsonPath)) continue;
285
+ try {
286
+ const raw = readFileSync(pkgJsonPath, "utf8");
287
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
288
+ if (typeof parsed.name === "string") {
289
+ manifests.push({
290
+ name: parsed.name,
291
+ version:
292
+ typeof parsed.version === "string" ? parsed.version : undefined,
293
+ });
294
+ }
295
+ } catch {
296
+ // Skip malformed entries — lenient like listInstalledPlugins.
297
+ }
298
+ }
299
+ return manifests;
300
+ }
301
+
302
+ /**
303
+ * Resolve the install date for a user plugin directory, in epoch ms.
304
+ * Reads `install-meta.json`'s `installedAt` field first, falling back to
305
+ * the directory's birthtime. Mirrors the logic in mtime-cache's
306
+ * `getInstallDate` so the sort order matches hook/tool resolution order.
307
+ */
308
+ function getPluginInstallDate(plugin: AllPluginInfo): number {
309
+ const metaPath = join(plugin.target, "install-meta.json");
310
+ try {
311
+ if (existsSync(metaPath)) {
312
+ const raw = JSON.parse(readFileSync(metaPath, "utf8")) as Record<string, unknown>;
313
+ if (typeof raw.installedAt === "string") {
314
+ const ms = Date.parse(raw.installedAt);
315
+ if (Number.isFinite(ms)) return ms;
316
+ }
317
+ }
318
+ } catch {
319
+ // Fall through to birthtime.
320
+ }
321
+ try {
322
+ return statSync(plugin.target).birthtimeMs;
323
+ } catch {
324
+ return 0;
325
+ }
326
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeEach,
7
+ describe,
8
+ expect,
9
+ mock,
10
+ test,
11
+ } from "bun:test";
12
+
13
+ const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
14
+ const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
15
+
16
+ function ensureTestDir(): void {
17
+ const dirs = [
18
+ WORKSPACE_DIR,
19
+ join(WORKSPACE_DIR, "data"),
20
+ join(WORKSPACE_DIR, "data", "memory"),
21
+ join(WORKSPACE_DIR, "data", "logs"),
22
+ ];
23
+ for (const dir of dirs) {
24
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
25
+ }
26
+ }
27
+
28
+ function makeLoggerStub(): Record<string, unknown> {
29
+ const stub: Record<string, unknown> = {};
30
+ for (const m of [
31
+ "info",
32
+ "warn",
33
+ "error",
34
+ "debug",
35
+ "trace",
36
+ "fatal",
37
+ "silent",
38
+ "child",
39
+ ]) {
40
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
41
+ }
42
+ return stub;
43
+ }
44
+
45
+ mock.module("../../util/logger.js", () => ({
46
+ getLogger: () => makeLoggerStub(),
47
+ }));
48
+
49
+ afterAll(() => {
50
+ mock.restore();
51
+ });
52
+
53
+ import { resolveCallSiteConfig } from "../llm-resolver.js";
54
+ import { invalidateConfigCache, loadConfig } from "../loader.js";
55
+
56
+ function writeConfig(obj: unknown): void {
57
+ writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n");
58
+ }
59
+
60
+ /**
61
+ * Base config where the active profile resolves to a DIFFERENT model than the
62
+ * `balanced` profile that the shipped `memoryV3SelectL2` call-site default
63
+ * points at. This lets each test distinguish the two failure modes:
64
+ * - call-site default applied (fixed) -> model "balanced-model"
65
+ * - silently downgraded to active -> model "active-model"
66
+ */
67
+ function baseLlm(callSites: Record<string, unknown>): unknown {
68
+ return {
69
+ llm: {
70
+ default: { provider: "anthropic", model: "default-model" },
71
+ profiles: {
72
+ balanced: { provider: "anthropic", model: "balanced-model" },
73
+ speedy: { provider: "anthropic", model: "active-model" },
74
+ },
75
+ activeProfile: "speedy",
76
+ callSites,
77
+ },
78
+ };
79
+ }
80
+
81
+ describe("config recovery prunes call-site overrides emptied by a strip", () => {
82
+ beforeEach(() => {
83
+ ensureTestDir();
84
+ if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH, { force: true });
85
+ delete process.env.IS_PLATFORM;
86
+ invalidateConfigCache();
87
+ });
88
+
89
+ afterEach(() => {
90
+ delete process.env.IS_PLATFORM;
91
+ if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH, { force: true });
92
+ invalidateConfigCache();
93
+ });
94
+
95
+ test("an undefined call-site profile ref falls back to the shipped default, not the active profile", () => {
96
+ // The `.profile` ref is invalid (no such profile), so schema recovery
97
+ // strips it. Before the fix this left `callSites.memoryV3SelectL2 = {}`,
98
+ // which the resolver treats as a present override and so skips the shipped
99
+ // `{profile:"balanced"}` default — silently resolving to the active profile.
100
+ writeConfig(baseLlm({ memoryV3SelectL2: { profile: "ghost-profile" } }));
101
+
102
+ const config = loadConfig();
103
+
104
+ // The emptied call-site entry must be pruned entirely, not left as `{}`.
105
+ expect(config.llm.callSites?.memoryV3SelectL2).toBeUndefined();
106
+
107
+ // Resolution now lands on the shipped call-site default (balanced), not the
108
+ // active profile ("active-model"), which is what the bug produced.
109
+ const resolved = resolveCallSiteConfig("memoryV3SelectL2", config.llm);
110
+ expect(resolved.model).toBe("balanced-model");
111
+ });
112
+
113
+ test("a valid sibling call-site override survives while the invalid one is pruned", () => {
114
+ // memoryRouter -> balanced is valid and must be preserved; only the invalid
115
+ // memoryV3SelectL2 entry is pruned. Guards against over-pruning the parent.
116
+ writeConfig(
117
+ baseLlm({
118
+ memoryV3SelectL2: { profile: "missing" },
119
+ memoryRouter: { profile: "balanced" },
120
+ }),
121
+ );
122
+
123
+ const config = loadConfig();
124
+
125
+ expect(config.llm.callSites?.memoryV3SelectL2).toBeUndefined();
126
+ expect(config.llm.callSites?.memoryRouter).toEqual({ profile: "balanced" });
127
+ });
128
+
129
+ test("a partial override keeping other fields is not pruned", () => {
130
+ // Stripping the invalid `.profile` leaves a non-empty `{temperature:0.5}`,
131
+ // a legitimate user override the resolver should keep (and which therefore
132
+ // still shadows the shipped default per existing either/or semantics).
133
+ writeConfig(
134
+ baseLlm({ memoryV3SelectL2: { profile: "missing", temperature: 0.5 } }),
135
+ );
136
+
137
+ const config = loadConfig();
138
+
139
+ expect(config.llm.callSites?.memoryV3SelectL2).toEqual({
140
+ temperature: 0.5,
141
+ });
142
+ });
143
+ });
@@ -8,7 +8,12 @@
8
8
  "risk": "low",
9
9
  "input_schema": {
10
10
  "type": "object",
11
- "properties": {},
11
+ "properties": {
12
+ "target_client_id": {
13
+ "type": "string",
14
+ "description": "ID of the specific client to target. Required when multiple clients support host_cu; omit when only one is connected. Obtain IDs from `assistant clients list --capability host_cu`."
15
+ }
16
+ },
12
17
  "required": []
13
18
  },
14
19
  "executor": "tools/computer-use-observe.ts",