@vellumai/assistant 0.8.7 → 0.8.8

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 (387) hide show
  1. package/Dockerfile +20 -4
  2. package/docker-entrypoint.sh +4 -2
  3. package/docker-init-apt-root.sh +3 -1
  4. package/docker-kata-apt-env.sh +3 -1
  5. package/docker-kata-runtime-family.sh +12 -0
  6. package/docs/architecture/memory.md +1 -1
  7. package/docs/plugins.md +75 -79
  8. package/examples/plugins/echo/README.md +6 -12
  9. package/examples/plugins/echo/register.ts +0 -41
  10. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  11. package/openapi.yaml +3381 -348
  12. package/package.json +1 -1
  13. package/scripts/generate-openapi.ts +68 -41
  14. package/src/__tests__/agent-loop-exit-reason.test.ts +34 -39
  15. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  16. package/src/__tests__/agent-loop.test.ts +37 -87
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  18. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  19. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  20. package/src/__tests__/anthropic-provider.test.ts +95 -2
  21. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  22. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  23. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  24. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  25. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  26. package/src/__tests__/btw-routes.test.ts +62 -3
  27. package/src/__tests__/build-persisted-content.test.ts +184 -0
  28. package/src/__tests__/catalog-files.test.ts +1 -1
  29. package/src/__tests__/clawhub-files.test.ts +1 -1
  30. package/src/__tests__/compaction-pipeline.test.ts +1 -1
  31. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  32. package/src/__tests__/config-watcher.test.ts +1 -1
  33. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  34. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -2
  35. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -4
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +313 -1136
  37. package/src/__tests__/conversation-agent-loop.test.ts +596 -1616
  38. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  39. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  40. package/src/__tests__/conversation-pairing.test.ts +4 -31
  41. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +26 -5
  43. package/src/__tests__/conversation-queue.test.ts +2 -0
  44. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  45. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +170 -229
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +3 -24
  48. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  49. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  50. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  51. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  52. package/src/__tests__/conversation-title-service.test.ts +135 -2
  53. package/src/__tests__/conversation-workspace-injection.test.ts +6 -1
  54. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  55. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  56. package/src/__tests__/dm-persistence.test.ts +5 -1
  57. package/src/__tests__/empty-response-hook.test.ts +304 -0
  58. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  59. package/src/__tests__/gemini-image-service.test.ts +13 -0
  60. package/src/__tests__/helpers/mock-provider.ts +110 -0
  61. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  62. package/src/__tests__/history-repair-hook.test.ts +1 -0
  63. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  64. package/src/__tests__/identity-routes.test.ts +248 -7
  65. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  66. package/src/__tests__/injector-background-turn.test.ts +2 -8
  67. package/src/__tests__/injector-chain.test.ts +106 -270
  68. package/src/__tests__/injector-disk-pressure.test.ts +3 -12
  69. package/src/__tests__/injector-document-comments.test.ts +2 -2
  70. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  71. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  72. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  73. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  74. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  75. package/src/__tests__/llm-usage-store.test.ts +223 -1
  76. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  77. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  78. package/src/__tests__/native-web-search.test.ts +191 -0
  79. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  80. package/src/__tests__/openai-image-service.test.ts +17 -0
  81. package/src/__tests__/openai-provider.test.ts +31 -1
  82. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  83. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  84. package/src/__tests__/pipeline-runner.test.ts +29 -39
  85. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  86. package/src/__tests__/plugin-bootstrap.test.ts +13 -28
  87. package/src/__tests__/plugin-registry.test.ts +0 -27
  88. package/src/__tests__/plugin-types.test.ts +2 -125
  89. package/src/__tests__/process-message-display-content.test.ts +6 -2
  90. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  91. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  92. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  93. package/src/__tests__/schedule-routes.test.ts +603 -2
  94. package/src/__tests__/schedule-store.test.ts +41 -0
  95. package/src/__tests__/schedule-tools.test.ts +35 -0
  96. package/src/__tests__/server-history-render.test.ts +314 -1
  97. package/src/__tests__/skillssh-files.test.ts +1 -1
  98. package/src/__tests__/system-prompt.test.ts +20 -0
  99. package/src/__tests__/task-scheduler.test.ts +162 -1
  100. package/src/__tests__/terminal-tools.test.ts +6 -1
  101. package/src/__tests__/title-generate-hook.test.ts +319 -0
  102. package/src/__tests__/tool-error-hook.test.ts +278 -0
  103. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  104. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  106. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  107. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  108. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  109. package/src/__tests__/usage-routes.test.ts +285 -1
  110. package/src/__tests__/user-plugin-loader.test.ts +2 -2
  111. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  112. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  113. package/src/agent/loop.ts +346 -442
  114. package/src/api/events/assistant-thinking-delta.ts +33 -0
  115. package/src/api/events/tool-output-chunk.ts +45 -0
  116. package/src/api/events/tool-use-preview-start.ts +32 -0
  117. package/src/api/events/trace-event.ts +69 -0
  118. package/src/api/index.ts +48 -13
  119. package/src/api/responses/conversation-message.ts +368 -0
  120. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  121. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  122. package/src/cli/commands/notifications.ts +112 -60
  123. package/src/config/assistant-feature-flags.ts +22 -11
  124. package/src/config/bundled-skills/app-builder/SKILL.md +3 -20
  125. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  126. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  127. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  128. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  129. package/src/config/bundled-skills/document-editor/SKILL.md +1 -1
  130. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  131. package/src/config/feature-flag-cache.ts +3 -3
  132. package/src/config/feature-flag-registry.json +35 -3
  133. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  134. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  135. package/src/config/schemas/llm.ts +1 -0
  136. package/src/config/schemas/memory-v2.ts +8 -0
  137. package/src/config/schemas/memory-v3.ts +8 -0
  138. package/src/config/schemas/platform.ts +8 -0
  139. package/src/config/seed-inference-profiles.ts +2 -2
  140. package/src/config/skills.ts +13 -0
  141. package/src/context/compactor.ts +1 -1
  142. package/src/context/strip-injections.ts +122 -0
  143. package/src/context/token-estimator.ts +23 -0
  144. package/src/context/tool-result-truncation.ts +0 -23
  145. package/src/context/window-manager.ts +3 -6
  146. package/src/credential-execution/executable-discovery.ts +16 -0
  147. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  148. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  149. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  150. package/src/daemon/assistant-attachments.ts +1 -1
  151. package/src/daemon/config-watcher.ts +2 -2
  152. package/src/daemon/context-overflow-reducer.ts +0 -1
  153. package/src/daemon/conversation-agent-loop-handlers.ts +605 -153
  154. package/src/daemon/conversation-agent-loop.ts +281 -760
  155. package/src/daemon/conversation-history.ts +5 -4
  156. package/src/daemon/conversation-lifecycle.ts +3 -4
  157. package/src/daemon/conversation-messaging.ts +7 -6
  158. package/src/daemon/conversation-process.ts +11 -16
  159. package/src/daemon/conversation-runtime-assembly.ts +130 -347
  160. package/src/daemon/conversation-slash.ts +6 -25
  161. package/src/daemon/conversation-surfaces.ts +222 -4
  162. package/src/daemon/conversation-tool-setup.ts +2 -29
  163. package/src/daemon/conversation.ts +32 -14
  164. package/src/daemon/external-plugins-bootstrap.ts +9 -10
  165. package/src/daemon/handlers/config-a2a.ts +51 -36
  166. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  167. package/src/daemon/handlers/config-telegram.ts +16 -2
  168. package/src/daemon/handlers/shared.ts +156 -84
  169. package/src/daemon/handlers/skills.ts +39 -10
  170. package/src/daemon/lifecycle.ts +4 -0
  171. package/src/daemon/message-types/apps.ts +1 -29
  172. package/src/daemon/message-types/messages.ts +9 -57
  173. package/src/daemon/message-types/skills.ts +2 -0
  174. package/src/daemon/message-types/surfaces.ts +136 -3
  175. package/src/daemon/now-scratchpad.ts +21 -0
  176. package/src/daemon/orphan-reaper.test.ts +210 -0
  177. package/src/daemon/orphan-reaper.ts +240 -0
  178. package/src/daemon/persist-unsendable-image.ts +117 -0
  179. package/src/daemon/process-message.ts +1 -3
  180. package/src/daemon/trace-emitter.ts +6 -4
  181. package/src/daemon/trust-context.ts +19 -0
  182. package/src/daemon/wake-target-adapter.ts +3 -1
  183. package/src/home/home-greeting-cache.ts +24 -1
  184. package/src/ipc/gateway-client.test.ts +2 -2
  185. package/src/ipc/gateway-client.ts +3 -3
  186. package/src/media/gemini-image-service.ts +15 -0
  187. package/src/media/openai-image-service.ts +14 -0
  188. package/src/media/types.ts +34 -0
  189. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  190. package/src/memory/auth-fallback-events-store.ts +94 -0
  191. package/src/memory/conversation-title-service.ts +65 -41
  192. package/src/memory/db-init.ts +4 -0
  193. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  194. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  195. package/src/memory/jobs-store.ts +33 -0
  196. package/src/memory/jobs-worker.ts +31 -4
  197. package/src/memory/llm-usage-store.ts +224 -50
  198. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  199. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  200. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  201. package/src/memory/migrations/index.ts +2 -0
  202. package/src/memory/pkb/autoinject.ts +61 -0
  203. package/src/memory/pkb/context.ts +50 -0
  204. package/src/memory/pkb/types.ts +14 -0
  205. package/src/memory/schedule-attribution-sql.ts +104 -0
  206. package/src/memory/schema/infrastructure.ts +16 -0
  207. package/src/memory/usage-grouped-buckets.ts +6 -1
  208. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -1
  209. package/src/memory/v2/consolidation-job.ts +1 -1
  210. package/src/memory/v3/__tests__/health.test.ts +16 -0
  211. package/src/memory/v3/__tests__/orchestrate.test.ts +45 -9
  212. package/src/memory/v3/__tests__/provider-blocks.test.ts +13 -0
  213. package/src/memory/v3/__tests__/router.test.ts +101 -29
  214. package/src/memory/v3/__tests__/selector.test.ts +93 -27
  215. package/src/memory/v3/__tests__/shadow-plugin.test.ts +23 -5
  216. package/src/memory/v3/health.ts +0 -0
  217. package/src/memory/v3/llm-retry.ts +32 -0
  218. package/src/memory/v3/orchestrate.ts +26 -14
  219. package/src/memory/v3/provider-blocks.ts +15 -5
  220. package/src/memory/v3/router.ts +48 -42
  221. package/src/memory/v3/selector.ts +57 -42
  222. package/src/memory/v3/shadow-plugin.ts +47 -15
  223. package/src/memory/v3/types.ts +8 -0
  224. package/src/notifications/conversation-pairing.ts +8 -15
  225. package/src/notifications/decision-engine.ts +6 -3
  226. package/src/notifications/home-feed-side-effect.ts +12 -1
  227. package/src/permissions/prompter.ts +4 -0
  228. package/src/plugin-api/constants.ts +4 -0
  229. package/src/plugin-api/index.ts +8 -1
  230. package/src/plugin-api/types.ts +151 -1
  231. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  232. package/src/plugins/defaults/empty-response/register.ts +8 -13
  233. package/src/plugins/defaults/index.ts +1 -15
  234. package/src/plugins/defaults/injectors/register.ts +243 -74
  235. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +91 -0
  236. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  237. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  238. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  239. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  240. package/src/plugins/defaults/title-generate/package.json +1 -1
  241. package/src/plugins/defaults/title-generate/register.ts +18 -18
  242. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  243. package/src/plugins/defaults/tool-error/package.json +1 -1
  244. package/src/plugins/defaults/tool-error/register.ts +9 -21
  245. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  246. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  247. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  248. package/src/plugins/pipeline.ts +6 -18
  249. package/src/plugins/registry.ts +8 -25
  250. package/src/plugins/types.ts +43 -474
  251. package/src/proactive-artifact/aux-message-injector.ts +3 -3
  252. package/src/proactive-artifact/job.test.ts +7 -12
  253. package/src/prompts/__tests__/system-prompt.test.ts +36 -0
  254. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +62 -0
  255. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  256. package/src/prompts/templates/system-sections.ts +15 -0
  257. package/src/providers/anthropic/client.ts +37 -29
  258. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  259. package/src/providers/openai/chat-completions-provider.ts +44 -0
  260. package/src/providers/openrouter/client.ts +1 -0
  261. package/src/providers/placeholder-sentinels.ts +35 -0
  262. package/src/runtime/__tests__/agent-wake.test.ts +5 -1
  263. package/src/runtime/agent-wake.ts +2 -2
  264. package/src/runtime/assistant-event-hub.ts +36 -6
  265. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  266. package/src/runtime/http-router.ts +16 -21
  267. package/src/runtime/http-types.ts +16 -70
  268. package/src/runtime/pending-interactions.ts +1 -0
  269. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  270. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  271. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  272. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  273. package/src/runtime/routes/app-management-routes.ts +6 -117
  274. package/src/runtime/routes/app-routes.ts +13 -15
  275. package/src/runtime/routes/attachment-routes.ts +26 -15
  276. package/src/runtime/routes/avatar-routes.ts +26 -0
  277. package/src/runtime/routes/btw-routes.ts +29 -23
  278. package/src/runtime/routes/consolidation-routes.ts +120 -20
  279. package/src/runtime/routes/conversation-query-routes.ts +2 -0
  280. package/src/runtime/routes/conversation-routes.ts +358 -184
  281. package/src/runtime/routes/documents-routes.ts +4 -0
  282. package/src/runtime/routes/domain-routes.ts +51 -37
  283. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  284. package/src/runtime/routes/events-routes.ts +28 -34
  285. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  286. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  287. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  288. package/src/runtime/routes/identity-routes.ts +208 -17
  289. package/src/runtime/routes/image-generation-routes.ts +40 -2
  290. package/src/runtime/routes/index.ts +2 -0
  291. package/src/runtime/routes/integrations/a2a.ts +12 -10
  292. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  293. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  294. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  295. package/src/runtime/routes/integrations/telegram.ts +6 -0
  296. package/src/runtime/routes/integrations/twilio.ts +42 -0
  297. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  298. package/src/runtime/routes/log-export-routes.ts +8 -0
  299. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  300. package/src/runtime/routes/memory-v3-routes.ts +50 -28
  301. package/src/runtime/routes/oauth-apps.ts +66 -12
  302. package/src/runtime/routes/oauth-providers.ts +44 -5
  303. package/src/runtime/routes/platform-routes.ts +81 -5
  304. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  305. package/src/runtime/routes/playground/force-compact.ts +1 -1
  306. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  307. package/src/runtime/routes/schedule-routes.ts +152 -42
  308. package/src/runtime/routes/secret-routes.ts +14 -2
  309. package/src/runtime/routes/skills-routes.ts +43 -14
  310. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  311. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  312. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  313. package/src/runtime/routes/tts-routes.ts +35 -0
  314. package/src/runtime/routes/types.ts +66 -8
  315. package/src/runtime/routes/usage-routes.ts +47 -39
  316. package/src/runtime/routes/webhook-routes.ts +41 -2
  317. package/src/runtime/routes/workspace-routes.ts +4 -0
  318. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  319. package/src/runtime/services/analyze-conversation.ts +2 -2
  320. package/src/schedule/schedule-store.ts +20 -1
  321. package/src/schedule/schedule-usage-store.ts +83 -0
  322. package/src/schedule/scheduler.ts +12 -5
  323. package/src/skills/catalog-files.ts +2 -2
  324. package/src/skills/catalog-install.ts +3 -0
  325. package/src/skills/categories-cache.ts +118 -0
  326. package/src/skills/clawhub-files.ts +1 -2
  327. package/src/skills/skillssh-files.ts +1 -2
  328. package/src/telemetry/types.ts +29 -1
  329. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  330. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  331. package/src/tools/executor.ts +1 -53
  332. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  333. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  334. package/src/tools/network/web-search-error.test.ts +248 -0
  335. package/src/tools/network/web-search-error.ts +267 -0
  336. package/src/tools/network/web-search.ts +207 -48
  337. package/src/tools/schedule/create.ts +2 -0
  338. package/src/tools/terminal/safe-env.ts +10 -1
  339. package/src/tools/ui-surface/definitions.ts +9 -1
  340. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  341. package/src/tts/provider-catalog.ts +76 -1
  342. package/src/util/mutex.ts +47 -0
  343. package/src/workspace/git-service.ts +1 -42
  344. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  345. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  346. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +93 -0
  347. package/src/workspace/migrations/registry.ts +6 -0
  348. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  349. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  350. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  351. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  352. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  353. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  354. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  355. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  356. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  357. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  358. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  359. package/src/gallery/default-gallery.ts +0 -1359
  360. package/src/gallery/gallery-manifest.ts +0 -28
  361. package/src/home/feature-gate.ts +0 -22
  362. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  363. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  364. package/src/plugins/defaults/injectors/package.json +0 -15
  365. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  366. package/src/plugins/defaults/llm-call/package.json +0 -15
  367. package/src/plugins/defaults/llm-call/register.ts +0 -45
  368. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  369. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  370. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  371. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  372. package/src/plugins/defaults/persistence/package.json +0 -15
  373. package/src/plugins/defaults/persistence/register.ts +0 -38
  374. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  375. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  376. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  377. package/src/plugins/defaults/token-estimate/package.json +0 -15
  378. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  379. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  380. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  381. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  382. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  383. package/src/plugins/defaults/tool-execute/package.json +0 -15
  384. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  385. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  386. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  387. package/src/skills/category-inference.ts +0 -111
@@ -41,6 +41,7 @@ const IMAGE_FILENAME = "avatar-image.png";
41
41
  const TRAITS_FILENAME = "character-traits.json";
42
42
  const ASCII_FILENAME = "character-ascii.txt";
43
43
  const MANIFEST_FILENAME = "avatar.json";
44
+ const NATIVE_RENDER_TEST_TIMEOUT_MS = 15_000;
44
45
 
45
46
  interface ManifestShape {
46
47
  kind: string;
@@ -84,35 +85,39 @@ describe("avatar-store", () => {
84
85
  };
85
86
 
86
87
  describe("setCharacter", () => {
87
- test("writes traits + PNG and a character manifest when render succeeds", () => {
88
- const result = setCharacter(VALID_TRAITS);
89
-
90
- // The native @resvg/resvg-js binding may be absent in this environment.
91
- // When it is, the store returns `native_unavailable` and writes nothing —
92
- // we assert that contract instead of the success path so the suite is
93
- // deterministic either way.
94
- if (!result.ok) {
95
- expect(result.reason).toBe("native_unavailable");
96
- expect(existsSync(path(TRAITS_FILENAME))).toBe(false);
97
- expect(existsSync(path(IMAGE_FILENAME))).toBe(false);
98
- expect(existsSync(path(MANIFEST_FILENAME))).toBe(false);
99
- return;
100
- }
101
-
102
- expect(existsSync(path(TRAITS_FILENAME))).toBe(true);
103
- expect(existsSync(path(IMAGE_FILENAME))).toBe(true);
104
- expect(JSON.parse(readFileSync(path(TRAITS_FILENAME), "utf-8"))).toEqual(
105
- VALID_TRAITS,
106
- );
107
-
108
- const manifest = readManifestFile();
109
- expect(manifest).not.toBeNull();
110
- expect(manifest!.kind).toBe("character");
111
- expect(manifest!.traits).toEqual(VALID_TRAITS);
112
- expect(manifest!.source).toBe("builder");
113
- expect(manifest!.image).not.toBeNull();
114
- expect(manifest!.image!.etag).toMatch(/^[0-9a-f]{16}$/);
115
- });
88
+ test(
89
+ "writes traits + PNG and a character manifest when render succeeds",
90
+ () => {
91
+ const result = setCharacter(VALID_TRAITS);
92
+
93
+ // The native @resvg/resvg-js binding may be absent in this environment.
94
+ // When it is, the store returns `native_unavailable` and writes nothing —
95
+ // we assert that contract instead of the success path so the suite is
96
+ // deterministic either way.
97
+ if (!result.ok) {
98
+ expect(result.reason).toBe("native_unavailable");
99
+ expect(existsSync(path(TRAITS_FILENAME))).toBe(false);
100
+ expect(existsSync(path(IMAGE_FILENAME))).toBe(false);
101
+ expect(existsSync(path(MANIFEST_FILENAME))).toBe(false);
102
+ return;
103
+ }
104
+
105
+ expect(existsSync(path(TRAITS_FILENAME))).toBe(true);
106
+ expect(existsSync(path(IMAGE_FILENAME))).toBe(true);
107
+ expect(
108
+ JSON.parse(readFileSync(path(TRAITS_FILENAME), "utf-8")),
109
+ ).toEqual(VALID_TRAITS);
110
+
111
+ const manifest = readManifestFile();
112
+ expect(manifest).not.toBeNull();
113
+ expect(manifest!.kind).toBe("character");
114
+ expect(manifest!.traits).toEqual(VALID_TRAITS);
115
+ expect(manifest!.source).toBe("builder");
116
+ expect(manifest!.image).not.toBeNull();
117
+ expect(manifest!.image!.etag).toMatch(/^[0-9a-f]{16}$/);
118
+ },
119
+ NATIVE_RENDER_TEST_TIMEOUT_MS,
120
+ );
116
121
 
117
122
  test("propagates invalid_traits without writing a manifest", () => {
118
123
  const result = setCharacter({ bodyShape: "", eyeStyle: "", color: "" });
@@ -66,6 +66,12 @@ interface CommandResult {
66
66
  exitCode: number;
67
67
  }
68
68
 
69
+ interface RawCommandResult {
70
+ stdout: string;
71
+ stderr: string;
72
+ exitCode: number;
73
+ }
74
+
69
75
  /**
70
76
  * Run a notifications subcommand and capture the JSON output.
71
77
  * Always passes --json to get compact, single-line JSON output and suppress log messages.
@@ -74,6 +80,17 @@ interface CommandResult {
74
80
  * reset to 0, capture, then reset back to 0 so bun test exits cleanly.
75
81
  */
76
82
  async function runCommand(args: string[]): Promise<CommandResult> {
83
+ const raw = await runRawCommand(["notifications", "--json", ...args]);
84
+
85
+ const firstLine = raw.stdout.trim().split("\n")[0];
86
+ const parsed = firstLine
87
+ ? (JSON.parse(firstLine) as Record<string, unknown>)
88
+ : {};
89
+
90
+ return { parsed, stderr: raw.stderr, exitCode: raw.exitCode };
91
+ }
92
+
93
+ async function runRawCommand(args: string[]): Promise<RawCommandResult> {
77
94
  const chunks: string[] = [];
78
95
  const stderrChunks: string[] = [];
79
96
  const originalWrite = process.stdout.write;
@@ -95,13 +112,7 @@ async function runCommand(args: string[]): Promise<CommandResult> {
95
112
  const program = new Command();
96
113
  program.exitOverride();
97
114
  registerNotificationsCommand(program);
98
- await program.parseAsync([
99
- "node",
100
- "test",
101
- "notifications",
102
- "--json",
103
- ...args,
104
- ]);
115
+ await program.parseAsync(["node", "test", ...args]);
105
116
  } catch {
106
117
  // Commander throws on .exitOverride() for --help/errors; ignore
107
118
  } finally {
@@ -112,13 +123,11 @@ async function runCommand(args: string[]): Promise<CommandResult> {
112
123
  const exitCode = process.exitCode ?? 0;
113
124
  process.exitCode = 0;
114
125
 
115
- const output = chunks.join("");
116
- const firstLine = output.trim().split("\n")[0];
117
- const parsed = firstLine
118
- ? (JSON.parse(firstLine) as Record<string, unknown>)
119
- : {};
120
-
121
- return { parsed, stderr: stderrChunks.join(""), exitCode };
126
+ return {
127
+ stdout: chunks.join(""),
128
+ stderr: stderrChunks.join(""),
129
+ exitCode,
130
+ };
122
131
  }
123
132
 
124
133
  function lastSendBody(): Record<string, unknown> {
@@ -244,6 +253,41 @@ describe("notifications send", () => {
244
253
  expect(ipcCalls).toHaveLength(0);
245
254
  });
246
255
 
256
+ test("send rejects invalid source-channel", async () => {
257
+ const { parsed, exitCode } = await runCommand([
258
+ "send",
259
+ "--source-channel",
260
+ "water_reminder",
261
+ "--source-event-name",
262
+ "user.send_notification",
263
+ "--message",
264
+ "Hello",
265
+ ]);
266
+
267
+ expect(exitCode).toBe(1);
268
+ expect(parsed).toEqual({
269
+ ok: false,
270
+ error:
271
+ 'Invalid source-channel "water_reminder". Must be one of: assistant_tool, vellum, phone, telegram, slack, scheduler, watcher',
272
+ });
273
+ expect(ipcCalls).toHaveLength(0);
274
+ });
275
+
276
+ test("send help lists source-channel values", async () => {
277
+ const { stdout, exitCode } = await runRawCommand([
278
+ "notifications",
279
+ "send",
280
+ "--help",
281
+ ]);
282
+
283
+ expect(exitCode).toBe(0);
284
+ const normalizedHelp = stdout.replace(/\s+/g, " ");
285
+ expect(normalizedHelp).toContain(
286
+ "One of: assistant_tool, vellum, phone, telegram, slack, scheduler, watcher",
287
+ );
288
+ expect(normalizedHelp).toContain("(default: assistant_tool)");
289
+ });
290
+
247
291
  test("send --conversation-id pins the vellum affinity hint", async () => {
248
292
  const { parsed, exitCode } = await runCommand([
249
293
  "send",
@@ -23,6 +23,39 @@ interface ListHomeFeedPayload {
23
23
  updatedAt: string;
24
24
  }
25
25
 
26
+ const NOTIFICATION_SOURCE_CHANNEL_VALUES = [
27
+ "assistant_tool",
28
+ "vellum",
29
+ "phone",
30
+ "telegram",
31
+ "slack",
32
+ "scheduler",
33
+ "watcher",
34
+ ] as const;
35
+
36
+ const URGENCY_VALUES = ["low", "medium", "high", "critical"] as const;
37
+
38
+ const NOTIFICATION_STATUS_VALUES = [
39
+ "new",
40
+ "seen",
41
+ "acted_on",
42
+ "dismissed",
43
+ ] as const;
44
+
45
+ const NOTIFICATION_CATEGORY_VALUES = [
46
+ "security",
47
+ "scheduling",
48
+ "background",
49
+ "email",
50
+ "system",
51
+ ] as const;
52
+
53
+ const DEFAULT_SOURCE_CHANNEL = "assistant_tool";
54
+ const SOURCE_CHANNEL_HELP = NOTIFICATION_SOURCE_CHANNEL_VALUES.join(", ");
55
+ const URGENCY_HELP = URGENCY_VALUES.join(", ");
56
+ const STATUS_HELP = NOTIFICATION_STATUS_VALUES.join(", ");
57
+ const CATEGORY_HELP = NOTIFICATION_CATEGORY_VALUES.join(", ");
58
+
26
59
  function parseBoundedInt(
27
60
  raw: string | undefined,
28
61
  label: string,
@@ -71,6 +104,30 @@ function renderFeedItemsHuman(payload: ListHomeFeedPayload): void {
71
104
  }
72
105
  }
73
106
 
107
+ function validateEnumValue(
108
+ value: string | undefined,
109
+ label: string,
110
+ allowed: readonly string[],
111
+ ): { error?: string } {
112
+ if (value === undefined || allowed.includes(value)) return {};
113
+ return {
114
+ error: `Invalid ${label} "${value}". Must be one of: ${allowed.join(", ")}`,
115
+ };
116
+ }
117
+
118
+ function validateEnumFlag(
119
+ values: string[] | undefined,
120
+ label: string,
121
+ allowed: readonly string[],
122
+ ): { error?: string } {
123
+ if (!values) return {};
124
+ for (const v of values) {
125
+ const result = validateEnumValue(v, label, allowed);
126
+ if (result.error) return result;
127
+ }
128
+ return {};
129
+ }
130
+
74
131
  // ---------------------------------------------------------------------------
75
132
  // Command registration
76
133
  // ---------------------------------------------------------------------------
@@ -122,7 +179,7 @@ Examples:
122
179
  )
123
180
  .option(
124
181
  "--source-channel <channel>",
125
- "Source channel producing this notification (default: assistant_tool)",
182
+ `Source channel producing this notification. One of: ${SOURCE_CHANNEL_HELP} (default: ${DEFAULT_SOURCE_CHANNEL})`,
126
183
  )
127
184
  .option(
128
185
  "--source-event-name <name>",
@@ -134,7 +191,7 @@ Examples:
134
191
  )
135
192
  .option(
136
193
  "--urgency <urgency>",
137
- "Urgency hint: low, medium, high, critical (default: low; use --urgent for critical)",
194
+ `Urgency hint. One of: ${URGENCY_HELP} (default: low; use --urgent for critical)`,
138
195
  )
139
196
  .option(
140
197
  "--requires-action",
@@ -233,9 +290,24 @@ Examples:
233
290
  try {
234
291
  // Apply defaults for optional source fields (minimal-surface
235
292
  // ergonomics; explicit values from the CLI still win).
236
- const sourceChannel = opts.sourceChannel ?? "assistant_tool";
293
+ const sourceChannel =
294
+ opts.sourceChannel ?? DEFAULT_SOURCE_CHANNEL;
237
295
  const sourceEventName = opts.sourceEventName ?? "assistant.share";
238
296
 
297
+ const sourceChannelError = validateEnumValue(
298
+ sourceChannel,
299
+ "source-channel",
300
+ NOTIFICATION_SOURCE_CHANNEL_VALUES,
301
+ );
302
+ if (sourceChannelError.error) {
303
+ writeOutput(cmd, {
304
+ ok: false,
305
+ error: sourceChannelError.error,
306
+ });
307
+ process.exitCode = 1;
308
+ return;
309
+ }
310
+
239
311
  // Validate --message (keep basic validation for immediate CLI feedback)
240
312
  const message = opts.message.trim();
241
313
  if (message.length === 0) {
@@ -256,15 +328,15 @@ Examples:
256
328
 
257
329
  // Validate --urgency
258
330
  const urgency = opts.urgency ?? urgentDefaults.urgency;
259
- if (
260
- urgency !== "low" &&
261
- urgency !== "medium" &&
262
- urgency !== "high" &&
263
- urgency !== "critical"
264
- ) {
331
+ const urgencyError = validateEnumValue(
332
+ urgency,
333
+ "urgency",
334
+ URGENCY_VALUES,
335
+ );
336
+ if (urgencyError.error) {
265
337
  writeOutput(cmd, {
266
338
  ok: false,
267
- error: `Invalid urgency "${opts.urgency}". Must be one of: low, medium, high, critical`,
339
+ error: urgencyError.error,
268
340
  });
269
341
  process.exitCode = 1;
270
342
  return;
@@ -425,22 +497,6 @@ Examples:
425
497
  prev: string[] | undefined,
426
498
  ): string[] => [...(prev ?? []), value];
427
499
 
428
- function validateEnumFlag(
429
- values: string[] | undefined,
430
- label: string,
431
- allowed: readonly string[],
432
- ): { error?: string } {
433
- if (!values) return {};
434
- for (const v of values) {
435
- if (!allowed.includes(v)) {
436
- return {
437
- error: `Invalid ${label} "${v}". Must be one of: ${allowed.join(", ")}`,
438
- };
439
- }
440
- }
441
- return {};
442
- }
443
-
444
500
  notifications
445
501
  .command("list")
446
502
  .description(
@@ -453,7 +509,7 @@ Examples:
453
509
  )
454
510
  .option(
455
511
  "--status <status>",
456
- "Filter by status (new|seen|acted_on|dismissed); repeatable. Overrides --all default behavior.",
512
+ `Filter by status. One of: ${STATUS_HELP}; repeatable. Overrides --all default behavior.`,
457
513
  collectFlag,
458
514
  )
459
515
  .option(
@@ -466,12 +522,12 @@ Examples:
466
522
  )
467
523
  .option(
468
524
  "--urgency <urgency>",
469
- "Filter by urgency (low|medium|high|critical); repeatable",
525
+ `Filter by urgency. One of: ${URGENCY_HELP}; repeatable`,
470
526
  collectFlag,
471
527
  )
472
528
  .option(
473
529
  "--category <category>",
474
- "Filter by category (security|scheduling|background|email|system); repeatable",
530
+ `Filter by category. One of: ${CATEGORY_HELP}; repeatable`,
475
531
  collectFlag,
476
532
  )
477
533
  .option(
@@ -525,25 +581,17 @@ Examples:
525
581
  ) => {
526
582
  try {
527
583
  const enumChecks: Array<{ error?: string }> = [
528
- validateEnumFlag(opts.status, "status", [
529
- "new",
530
- "seen",
531
- "acted_on",
532
- "dismissed",
533
- ]),
534
- validateEnumFlag(opts.urgency, "urgency", [
535
- "low",
536
- "medium",
537
- "high",
538
- "critical",
539
- ]),
540
- validateEnumFlag(opts.category, "category", [
541
- "security",
542
- "scheduling",
543
- "background",
544
- "email",
545
- "system",
546
- ]),
584
+ validateEnumFlag(
585
+ opts.status,
586
+ "status",
587
+ NOTIFICATION_STATUS_VALUES,
588
+ ),
589
+ validateEnumFlag(opts.urgency, "urgency", URGENCY_VALUES),
590
+ validateEnumFlag(
591
+ opts.category,
592
+ "category",
593
+ NOTIFICATION_CATEGORY_VALUES,
594
+ ),
547
595
  ];
548
596
  const enumError = enumChecks.find((c) => c.error);
549
597
  if (enumError) {
@@ -641,11 +689,11 @@ Examples:
641
689
  .option("--title <title>", "New short headline (≤ 8 words).")
642
690
  .option(
643
691
  "--urgency <urgency>",
644
- "Set urgency (low|medium|high|critical). Feed-only — does not re-push channel messages.",
692
+ `Set urgency. One of: ${URGENCY_HELP}. Feed-only — does not re-push channel messages.`,
645
693
  )
646
694
  .option(
647
695
  "--status <status>",
648
- "Set lifecycle status (new|seen|acted_on|dismissed). Feed-only.",
696
+ `Set lifecycle status. One of: ${STATUS_HELP}. Feed-only.`,
649
697
  )
650
698
  .addHelpText(
651
699
  "after",
@@ -701,24 +749,28 @@ Examples:
701
749
  return;
702
750
  }
703
751
 
704
- if (
705
- opts.urgency != null &&
706
- !["low", "medium", "high", "critical"].includes(opts.urgency)
707
- ) {
752
+ const urgencyError = validateEnumValue(
753
+ opts.urgency,
754
+ "urgency",
755
+ URGENCY_VALUES,
756
+ );
757
+ if (urgencyError.error) {
708
758
  writeOutput(cmd, {
709
759
  ok: false,
710
- error: `Invalid urgency "${opts.urgency}". Must be one of: low, medium, high, critical`,
760
+ error: urgencyError.error,
711
761
  });
712
762
  process.exitCode = 1;
713
763
  return;
714
764
  }
715
- if (
716
- opts.status != null &&
717
- !["new", "seen", "acted_on", "dismissed"].includes(opts.status)
718
- ) {
765
+ const statusError = validateEnumValue(
766
+ opts.status,
767
+ "status",
768
+ NOTIFICATION_STATUS_VALUES,
769
+ );
770
+ if (statusError.error) {
719
771
  writeOutput(cmd, {
720
772
  ok: false,
721
- error: `Invalid status "${opts.status}". Must be one of: new, seen, acted_on, dismissed`,
773
+ error: statusError.error,
722
774
  });
723
775
  process.exitCode = 1;
724
776
  return;
@@ -33,7 +33,7 @@ const log = getLogger("assistant-feature-flags");
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
35
  export interface FeatureFlagDefault {
36
- defaultEnabled: boolean;
36
+ defaultEnabled: boolean | string;
37
37
  description: string;
38
38
  label: string;
39
39
  }
@@ -103,7 +103,7 @@ function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
103
103
  const entry = flag as Record<string, unknown>;
104
104
  if (entry.scope !== "assistant" && entry.scope !== "both") continue;
105
105
  if (typeof entry.key !== "string") continue;
106
- if (typeof entry.defaultEnabled !== "boolean") continue;
106
+ if (typeof entry.defaultEnabled !== "boolean" && typeof entry.defaultEnabled !== "string") continue;
107
107
 
108
108
  result[entry.key as string] = {
109
109
  defaultEnabled: entry.defaultEnabled,
@@ -133,7 +133,7 @@ function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
133
133
  */
134
134
  async function fetchOverridesFromGateway(
135
135
  timeoutMs?: number,
136
- ): Promise<Record<string, boolean>> {
136
+ ): Promise<Record<string, boolean | string>> {
137
137
  try {
138
138
  return await ipcGetFeatureFlags(timeoutMs);
139
139
  } catch {
@@ -238,7 +238,7 @@ export async function initFeatureFlagOverrides(options?: {
238
238
  * Returns the gateway-populated cache if `initFeatureFlagOverrides()` was
239
239
  * called at startup, or an empty record otherwise.
240
240
  */
241
- function loadOverrides(): Record<string, boolean> {
241
+ function loadOverrides(): Record<string, boolean | string> {
242
242
  return getCachedOverrides() ?? {};
243
243
  }
244
244
 
@@ -273,7 +273,7 @@ export async function refreshOverridesFromGateway(): Promise<void> {
273
273
  // ---------------------------------------------------------------------------
274
274
 
275
275
  /**
276
- * Resolve whether an assistant feature flag is enabled.
276
+ * Resolve the raw value for an assistant feature flag.
277
277
  *
278
278
  * Resolution order:
279
279
  * 1. Override from the gateway IPC fetch (includes platform-pushed remote
@@ -282,21 +282,32 @@ export async function refreshOverridesFromGateway(): Promise<void> {
282
282
  * 2. Registry `defaultEnabled` (for declared assistant-scope keys)
283
283
  * 3. `false` (for undeclared keys with no override)
284
284
  */
285
- export function isAssistantFeatureFlagEnabled(
285
+ export function getAssistantFeatureFlagValue(
286
286
  key: string,
287
287
  _config: AssistantConfig,
288
- ): boolean {
288
+ ): boolean | string {
289
289
  const defaults = loadDefaultsRegistry();
290
290
  const declared = defaults[key];
291
291
  const overrides = loadOverrides();
292
292
 
293
- // 1. Check overrides from the gateway IPC cache.
294
293
  const explicit = overrides[key];
295
- if (typeof explicit === "boolean") return explicit;
294
+ if (explicit !== undefined) return explicit;
296
295
 
297
- // 2. For declared keys, use the registry default.
298
296
  if (declared) return declared.defaultEnabled;
299
297
 
300
- // 3. Undeclared keys with no override fail closed.
301
298
  return false;
302
299
  }
300
+
301
+ /**
302
+ * Resolve whether an assistant feature flag is enabled (boolean coercion).
303
+ *
304
+ * For boolean flags, returns the resolved value directly.
305
+ * For string flags, returns true if the value is non-empty.
306
+ * Undeclared keys return `false` (fail closed).
307
+ */
308
+ export function isAssistantFeatureFlagEnabled(
309
+ key: string,
310
+ config: AssistantConfig,
311
+ ): boolean {
312
+ return !!getAssistantFeatureFlagValue(key, config);
313
+ }
@@ -18,7 +18,7 @@ You are an expert app builder and visual designer. When the user asks you to cre
18
18
 
19
19
  **Your default behavior:** Build immediately. The user types "build me a habit tracker" and you deliver a complete, polished app with a domain-matched color palette, atmospheric background, and thoughtful interactions. Don't ask what colors they want. Don't show wireframes. Just build something stunning and let them refine from there.
20
20
 
21
- **Design quality is delegated to the `frontend-design` skill, so you must also load/install that before proceeding.** That skill defines your aesthetic principles: typography, color strategy, motion, spatial composition, and visual detail. Follow it completely for every build. This skill (app-builder) handles the technical infrastructure: sandbox constraints, data bridge, widget API, app lifecycle, and interaction patterns.
21
+ **Design quality is delegated to the `frontend-design` skill, so you must also load/install that before proceeding.** That skill defines your aesthetic principles: typography, color strategy, motion, spatial composition, and visual detail. Follow it completely for every build. This skill (app-builder) handles the technical infrastructure: sandbox constraints, data persistence, widget API, app lifecycle, and interaction patterns.
22
22
 
23
23
  ## Filesystem Layout
24
24
 
@@ -411,31 +411,14 @@ A CSS/JS widget library is auto-injected alongside the design system. Use `.v-*`
411
411
 
412
412
  For the full widget reference (class names, JS APIs, chart functions, formatting utilities), see **[Widget Component Library](references/WIDGETS.md)**.
413
413
 
414
- #### Data bridge API (deprecated)
415
-
416
- > **Prefer custom route handlers** for new apps. The data bridge (`window.vellum.data`) only works for assistants that run on the same machine as the desktop app, which will also be deprecated soon.
417
-
418
- The native WebView can read and write app records via `window.vellum.data`. All methods return Promises.
419
-
420
- - `window.vellum.data.query()` - Returns all records: `{ id, appId, data, createdAt, updatedAt }[]`
421
- - `window.vellum.data.create(data)` - Creates a record. Returns the created record.
422
- - `window.vellum.data.update(recordId, data)` - Updates a record by ID. Returns updated record.
423
- - `window.vellum.data.delete(recordId)` - Deletes a record by ID. Returns void.
424
-
425
- Important:
426
-
427
- - Call `query()` on page load to populate initial state
428
- - User fields live in `record.data` (e.g., `record.data.title`)
429
- - Record IDs are UUID strings
430
- - All operations are async - use `async/await`
431
- - Wrap all calls in `try/catch`
432
-
433
414
  #### Custom route handlers (user-defined routes)
434
415
 
435
416
  When the app needs server-side persistence, custom API logic, or workspace file access, use **user-defined routes**. Route handlers are TypeScript/JavaScript files in the workspace `routes/` directory, served under `/v1/x/`. Call them from the frontend via `window.vellum.fetch("/v1/x/...")`. **Never use raw `fetch()` for `/v1/x/` routes** — it will fail in the sandboxed origin.
436
417
 
437
418
  For handler conventions, examples, key rules, and frontend usage patterns, see **[Custom Route Handlers](references/CUSTOM_ROUTES.md)**.
438
419
 
420
+ For complete, copyable apps wiring this persistence pattern end-to-end (multi-file TSX frontend + `routes/*.ts` handler), see the **[example apps](references/examples/README.md)**: a [Focus Timer](references/examples/focus-timer.md) (append-only log), a [Habit Tracker](references/examples/habit-tracker.md) (full CRUD), and an [Expense Tracker](references/examples/expense-tracker.md) (create/read/delete + aggregation).
421
+
439
422
  #### Client-side state management
440
423
 
441
424
  `localStorage` and `sessionStorage` are available for ephemeral UI state (filters, view modes, collapsed state, preferences, form drafts). Use custom routes for persistent app records, `localStorage` for UI preferences.
@@ -0,0 +1,17 @@
1
+ # Example Apps
2
+
3
+ Complete, copyable reference apps that demonstrate the recommended patterns from this skill:
4
+ multi-file TSX (`formatVersion: 2`), web-compatible persistence via **custom route handlers**
5
+ (`window.vellum.fetch("/v1/x/…")`), and disciplined error handling. Use them as starting
6
+ points — match the structure, not the styling (give every new app its own visual identity per
7
+ the `frontend-design` skill).
8
+
9
+ | Example | Persistence pattern | Route methods |
10
+ | --------------------------------------- | ------------------------------------------------ | -------------------------------- |
11
+ | [Focus Timer](./focus-timer.md) | Append-only log + aggregate stats read on mount | `GET`, `POST` |
12
+ | [Habit Tracker](./habit-tracker.md) | Full CRUD addressed by `id` query param | `GET`, `POST`, `PATCH`, `DELETE` |
13
+ | [Expense Tracker](./expense-tracker.md) | Create / read / delete + client-side aggregation | `GET`, `POST`, `DELETE` |
14
+
15
+ All three persist through a `routes/*.ts` handler — never through `window.vellum.data`, which
16
+ is a desktop-only bridge that silently no-ops on web. See
17
+ [CUSTOM_ROUTES.md](../CUSTOM_ROUTES.md) for the full route handler reference.