@vellumai/assistant 0.7.2 → 0.7.3

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 (347) hide show
  1. package/ARCHITECTURE.md +16 -1
  2. package/docs/architecture/memory.md +5 -2
  3. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
  4. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
  5. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  6. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  7. package/openapi.yaml +449 -22
  8. package/package.json +1 -1
  9. package/src/__tests__/app-control-flow.test.ts +21 -11
  10. package/src/__tests__/assistant-event-hub.test.ts +48 -0
  11. package/src/__tests__/assistant-event.test.ts +0 -10
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
  13. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
  14. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  15. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  16. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  18. package/src/__tests__/channel-readiness-service.test.ts +4 -2
  19. package/src/__tests__/config-loader-backfill.test.ts +379 -0
  20. package/src/__tests__/config-schema.test.ts +1 -0
  21. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
  22. package/src/__tests__/config-watcher.test.ts +140 -69
  23. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  24. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  25. package/src/__tests__/context-search-fanout.test.ts +0 -1
  26. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  27. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  28. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  29. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  30. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  31. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  32. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  33. package/src/__tests__/conversation-error.test.ts +150 -3
  34. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  35. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  36. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  37. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  38. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  39. package/src/__tests__/conversation-store.test.ts +0 -18
  40. package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
  41. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  42. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  43. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  44. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  45. package/src/__tests__/credentials-cli.test.ts +7 -0
  46. package/src/__tests__/cu-unified-flow.test.ts +176 -10
  47. package/src/__tests__/date-context.test.ts +164 -2
  48. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  49. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  50. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  51. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  52. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  53. package/src/__tests__/disk-usage.test.ts +150 -0
  54. package/src/__tests__/events-client-registration.test.ts +52 -0
  55. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  56. package/src/__tests__/file-write-tool.test.ts +4 -10
  57. package/src/__tests__/filing-service.test.ts +3 -4
  58. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  59. package/src/__tests__/heartbeat-service.test.ts +260 -11
  60. package/src/__tests__/host-app-control-proxy.test.ts +195 -25
  61. package/src/__tests__/host-bash-proxy.test.ts +227 -34
  62. package/src/__tests__/host-bash-routes.test.ts +178 -13
  63. package/src/__tests__/host-cu-proxy.test.ts +210 -3
  64. package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
  65. package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
  66. package/src/__tests__/host-file-proxy.test.ts +268 -6
  67. package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
  68. package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
  69. package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
  70. package/src/__tests__/http-user-message-parity.test.ts +107 -1
  71. package/src/__tests__/injector-chain.test.ts +18 -6
  72. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  73. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  74. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  75. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  77. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  78. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  79. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  80. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  81. package/src/__tests__/openai-provider.test.ts +45 -8
  82. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  83. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  84. package/src/__tests__/platform.test.ts +2 -1
  85. package/src/__tests__/playbook-execution.test.ts +0 -43
  86. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  87. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
  88. package/src/__tests__/provider-tool-name.test.ts +23 -0
  89. package/src/__tests__/relay-server.test.ts +15 -4
  90. package/src/__tests__/runtime-events-sse.test.ts +4 -8
  91. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  92. package/src/__tests__/secret-ingress-http.test.ts +0 -1
  93. package/src/__tests__/suggestion-routes.test.ts +46 -0
  94. package/src/__tests__/twilio-validation.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  96. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  97. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  98. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  99. package/src/approvals/guardian-decision-primitive.ts +13 -0
  100. package/src/approvals/guardian-request-resolvers.ts +16 -17
  101. package/src/backup/snapshot-lock.ts +2 -27
  102. package/src/bundler/compiler-tools.ts +3 -2
  103. package/src/calls/call-conversation-messages.ts +46 -10
  104. package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
  105. package/src/cli/commands/bash.ts +35 -108
  106. package/src/cli/commands/contacts.ts +64 -25
  107. package/src/cli/commands/credentials.ts +56 -0
  108. package/src/cli/commands/memory-v2.ts +7 -6
  109. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  110. package/src/cli/commands/oauth/connect.ts +127 -1
  111. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
  112. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
  113. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  114. package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
  115. package/src/cli/commands/platform/index.ts +16 -7
  116. package/src/cli/commands/status.ts +57 -0
  117. package/src/cli/program.ts +4 -2
  118. package/src/config/assistant-feature-flags.ts +13 -3
  119. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  120. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
  121. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  122. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  123. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  124. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  125. package/src/config/env.ts +0 -8
  126. package/src/config/feature-flag-registry.json +27 -3
  127. package/src/config/loader.ts +127 -8
  128. package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
  129. package/src/config/schemas/call-site-catalog.ts +14 -0
  130. package/src/config/schemas/channels.ts +0 -5
  131. package/src/config/schemas/heartbeat.ts +1 -1
  132. package/src/config/schemas/llm.ts +2 -0
  133. package/src/config/schemas/memory-lifecycle.ts +13 -0
  134. package/src/config/schemas/memory-v2.ts +75 -11
  135. package/src/config/schemas/platform.ts +43 -3
  136. package/src/config/schemas/services.ts +28 -0
  137. package/src/config/seed-inference-profiles.ts +230 -33
  138. package/src/contacts/contact-store.ts +0 -25
  139. package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
  140. package/src/daemon/assistant-attachments.ts +4 -4
  141. package/src/daemon/config-watcher.ts +85 -57
  142. package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
  143. package/src/daemon/conversation-agent-loop.ts +170 -33
  144. package/src/daemon/conversation-error.ts +87 -15
  145. package/src/daemon/conversation-lifecycle.ts +1 -3
  146. package/src/daemon/conversation-process.ts +8 -0
  147. package/src/daemon/conversation-runtime-assembly.ts +26 -0
  148. package/src/daemon/conversation-store.ts +2 -2
  149. package/src/daemon/conversation-surfaces.ts +195 -15
  150. package/src/daemon/conversation-tool-setup.ts +57 -14
  151. package/src/daemon/conversation.ts +17 -22
  152. package/src/daemon/date-context.ts +71 -22
  153. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  154. package/src/daemon/disk-pressure-guard.ts +343 -0
  155. package/src/daemon/disk-pressure-policy.ts +163 -0
  156. package/src/daemon/handlers/shared.ts +0 -1
  157. package/src/daemon/handlers/skills.ts +3 -4
  158. package/src/daemon/host-app-control-proxy.ts +137 -41
  159. package/src/daemon/host-bash-proxy.ts +46 -21
  160. package/src/daemon/host-cu-proxy.ts +49 -3
  161. package/src/daemon/host-file-proxy.ts +43 -7
  162. package/src/daemon/host-transfer-proxy.ts +95 -4
  163. package/src/daemon/lifecycle.ts +79 -28
  164. package/src/daemon/meet-host-supervisor.ts +4 -4
  165. package/src/daemon/meet-manifest-loader.ts +0 -1
  166. package/src/daemon/memory-v2-startup.ts +14 -4
  167. package/src/daemon/message-protocol.ts +3 -0
  168. package/src/daemon/message-types/conversations.ts +4 -0
  169. package/src/daemon/message-types/disk-pressure.ts +9 -0
  170. package/src/daemon/message-types/messages.ts +3 -0
  171. package/src/daemon/profiler-run-store.ts +5 -5
  172. package/src/daemon/tool-setup-types.ts +2 -2
  173. package/src/documents/document-store.ts +85 -0
  174. package/src/filing/filing-service.ts +30 -5
  175. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
  176. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
  177. package/src/heartbeat/heartbeat-run-store.ts +13 -0
  178. package/src/heartbeat/heartbeat-service.ts +205 -31
  179. package/src/home/feed-scheduler.ts +18 -0
  180. package/src/inbound/platform-callback-registration.ts +8 -15
  181. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  182. package/src/ipc/assistant-server.ts +56 -2
  183. package/src/ipc/gateway-client.ts +37 -3
  184. package/src/live-voice/live-voice-archive.ts +4 -4
  185. package/src/live-voice/protocol.ts +5 -7
  186. package/src/media/image-service.ts +1 -7
  187. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  188. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  189. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  190. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  191. package/src/memory/admin.ts +5 -9
  192. package/src/memory/context-search/agent-runner.ts +19 -2
  193. package/src/memory/context-search/sources/conversations.ts +2 -11
  194. package/src/memory/context-search/sources/memory-v2.ts +5 -4
  195. package/src/memory/context-search/sources/memory.ts +0 -1
  196. package/src/memory/context-search/types.ts +0 -1
  197. package/src/memory/conversation-crud.ts +4 -12
  198. package/src/memory/db-init.ts +2 -0
  199. package/src/memory/embedding-runtime-manager.ts +119 -5
  200. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
  201. package/src/memory/graph/conversation-graph-memory.ts +42 -54
  202. package/src/memory/graph/extraction.ts +1 -3
  203. package/src/memory/graph/graph-search.test.ts +10 -67
  204. package/src/memory/graph/graph-search.ts +1 -20
  205. package/src/memory/graph/retriever.test.ts +6 -0
  206. package/src/memory/graph/retriever.ts +6 -10
  207. package/src/memory/indexer.ts +54 -45
  208. package/src/memory/job-handlers/backfill.ts +2 -11
  209. package/src/memory/job-handlers/cleanup.ts +43 -0
  210. package/src/memory/job-handlers/embedding.ts +6 -8
  211. package/src/memory/job-handlers/summarization.ts +2 -7
  212. package/src/memory/jobs-store.ts +48 -0
  213. package/src/memory/jobs-worker.ts +81 -43
  214. package/src/memory/memory-v2-activation-log-store.ts +32 -14
  215. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  216. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  217. package/src/memory/migrations/index.ts +1 -0
  218. package/src/memory/pkb/pkb-search.test.ts +6 -0
  219. package/src/memory/qdrant-client.ts +0 -13
  220. package/src/memory/rerank-local.ts +374 -0
  221. package/src/memory/search/semantic.ts +6 -67
  222. package/src/memory/trace-event-store.ts +1 -17
  223. package/src/memory/v2/__tests__/activation.test.ts +311 -250
  224. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  225. package/src/memory/v2/__tests__/injection.test.ts +157 -167
  226. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  227. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  228. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  229. package/src/memory/v2/__tests__/sim.test.ts +5 -199
  230. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  231. package/src/memory/v2/__tests__/static-context.test.ts +76 -1
  232. package/src/memory/v2/activation.ts +149 -156
  233. package/src/memory/v2/consolidation-job.ts +62 -12
  234. package/src/memory/v2/injection.ts +47 -60
  235. package/src/memory/v2/prompts/consolidation.ts +36 -1
  236. package/src/memory/v2/qdrant.ts +99 -0
  237. package/src/memory/v2/reranker.ts +177 -0
  238. package/src/memory/v2/sim.ts +10 -84
  239. package/src/memory/v2/skill-content.ts +4 -3
  240. package/src/memory/v2/skill-store.ts +82 -59
  241. package/src/memory/v2/static-context.ts +22 -0
  242. package/src/memory/v2/types.ts +10 -10
  243. package/src/notifications/copy-composer.ts +13 -0
  244. package/src/notifications/signal.ts +4 -0
  245. package/src/oauth/AGENTS.md +3 -1
  246. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  247. package/src/oauth/connect-orchestrator.ts +2 -0
  248. package/src/oauth/connection-resolver.test.ts +66 -1
  249. package/src/oauth/connection-resolver.ts +55 -1
  250. package/src/oauth/oauth-connect-state.ts +77 -0
  251. package/src/oauth/seed-providers.ts +58 -1
  252. package/src/plugins/defaults/injectors.ts +35 -2
  253. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  254. package/src/plugins/types.ts +7 -0
  255. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  256. package/src/proactive-artifact/decision.test.ts +226 -0
  257. package/src/proactive-artifact/decision.ts +165 -0
  258. package/src/proactive-artifact/index.ts +7 -0
  259. package/src/proactive-artifact/job.test.ts +867 -0
  260. package/src/proactive-artifact/job.ts +352 -0
  261. package/src/proactive-artifact/message-copy.ts +41 -0
  262. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  263. package/src/proactive-artifact/trigger-state.ts +119 -0
  264. package/src/prompts/normalize-onboarding.ts +80 -0
  265. package/src/prompts/persona-resolver.ts +101 -9
  266. package/src/prompts/system-prompt.ts +21 -7
  267. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  268. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  269. package/src/providers/model-intents.ts +7 -0
  270. package/src/providers/openrouter/client.ts +8 -0
  271. package/src/providers/retry.ts +50 -0
  272. package/src/providers/types.ts +1 -0
  273. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  274. package/src/runtime/agent-wake.ts +238 -100
  275. package/src/runtime/assistant-event-hub.ts +36 -6
  276. package/src/runtime/assistant-event.ts +0 -1
  277. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  278. package/src/runtime/auth/route-policy.ts +14 -1
  279. package/src/runtime/auth/same-actor.ts +216 -0
  280. package/src/runtime/channel-retry-sweep.ts +65 -1
  281. package/src/runtime/guardian-reply-router.ts +10 -0
  282. package/src/runtime/local-actor-identity.ts +52 -11
  283. package/src/runtime/pending-interactions.ts +8 -0
  284. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  285. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
  286. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  287. package/src/runtime/routes/client-routes.ts +20 -2
  288. package/src/runtime/routes/contact-routes.ts +0 -25
  289. package/src/runtime/routes/conversation-routes.ts +35 -26
  290. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  291. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  292. package/src/runtime/routes/document-pdf-renderer.ts +6 -2
  293. package/src/runtime/routes/documents-routes.ts +2 -75
  294. package/src/runtime/routes/events-routes.ts +41 -9
  295. package/src/runtime/routes/host-bash-routes.ts +23 -3
  296. package/src/runtime/routes/host-cu-routes.ts +33 -6
  297. package/src/runtime/routes/host-file-routes.ts +32 -6
  298. package/src/runtime/routes/host-transfer-routes.ts +79 -16
  299. package/src/runtime/routes/identity-routes.ts +7 -138
  300. package/src/runtime/routes/inbound-message-handler.ts +77 -12
  301. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  302. package/src/runtime/routes/index.ts +6 -0
  303. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  304. package/src/runtime/routes/memory-v2-routes.ts +33 -0
  305. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  306. package/src/runtime/verification-outbound-actions.ts +4 -4
  307. package/src/schedule/run-script.ts +37 -5
  308. package/src/schedule/scheduler.ts +20 -1
  309. package/src/security/encrypted-store.ts +2 -0
  310. package/src/security/secure-keys.ts +55 -0
  311. package/src/skills/remote-skill-policy.ts +4 -10
  312. package/src/subagent/index.ts +1 -7
  313. package/src/subagent/manager.ts +1 -15
  314. package/src/tasks/task-runner.ts +0 -1
  315. package/src/tasks/task-store.ts +0 -3
  316. package/src/tools/background-tool-registry.ts +17 -3
  317. package/src/tools/host-filesystem/edit.test.ts +151 -0
  318. package/src/tools/host-filesystem/edit.ts +43 -1
  319. package/src/tools/host-filesystem/read.test.ts +129 -0
  320. package/src/tools/host-filesystem/read.ts +43 -1
  321. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  322. package/src/tools/host-filesystem/transfer.ts +56 -11
  323. package/src/tools/host-filesystem/write.test.ts +134 -0
  324. package/src/tools/host-filesystem/write.ts +43 -1
  325. package/src/tools/host-terminal/host-shell.ts +13 -6
  326. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  327. package/src/tools/memory/register.test.ts +12 -9
  328. package/src/tools/memory/register.ts +1 -2
  329. package/src/tools/provider-tool-name.ts +28 -0
  330. package/src/tools/registry.ts +30 -9
  331. package/src/tools/terminal/shell.ts +9 -1
  332. package/src/tools/tool-approval-handler.ts +31 -6
  333. package/src/tools/types.ts +24 -2
  334. package/src/tts/provider-catalog.ts +3 -5
  335. package/src/util/disk-usage.ts +138 -0
  336. package/src/util/platform.ts +21 -11
  337. package/src/util/process-liveness.ts +26 -0
  338. package/src/workspace/heartbeat-service.ts +19 -0
  339. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  340. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  341. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  342. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  343. package/src/workspace/migrations/registry.ts +8 -0
  344. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  345. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  346. package/src/memory/v2/skill-qdrant.ts +0 -404
  347. package/src/signals/bash.ts +0 -198
@@ -0,0 +1,316 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ── Module mocks (must precede imports) ───────────────────────────────────────
4
+
5
+ type OrchestrateOptions = {
6
+ service: string;
7
+ clientId: string;
8
+ clientSecret?: string;
9
+ callbackTransport?: string;
10
+ requestedScopes?: string[];
11
+ isInteractive: boolean;
12
+ onDeferredComplete?: (r: {
13
+ success: boolean;
14
+ service: string;
15
+ accountInfo?: string;
16
+ grantedScopes?: string[];
17
+ error?: string;
18
+ }) => void;
19
+ };
20
+
21
+ let capturedOnDeferredComplete: OrchestrateOptions["onDeferredComplete"] | undefined;
22
+ let mockOrchestrateResult: Record<string, unknown> = {
23
+ success: true,
24
+ deferred: true,
25
+ authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
26
+ state: "test-state-uuid-abc123",
27
+ service: "google",
28
+ };
29
+
30
+ mock.module("../oauth/connect-orchestrator.js", () => ({
31
+ orchestrateOAuthConnect: async (opts: OrchestrateOptions) => {
32
+ capturedOnDeferredComplete = opts.onDeferredComplete;
33
+ return mockOrchestrateResult;
34
+ },
35
+ }));
36
+
37
+ mock.module("../util/logger.js", () => ({
38
+ getLogger: () =>
39
+ new Proxy({} as Record<string, unknown>, {
40
+ get: () => () => {},
41
+ }),
42
+ }));
43
+
44
+ // NOTE: Do NOT mock oauth-connect-state — use the real module so we can
45
+ // verify state transitions via getOAuthConnectState.
46
+
47
+ // ── Import SUT after mocks ─────────────────────────────────────────────────────
48
+
49
+ const { ROUTES } = await import("../runtime/routes/oauth-connect-routes.js");
50
+ const { BadRequestError, InternalError, NotFoundError } = await import(
51
+ "../runtime/routes/errors.js"
52
+ );
53
+ const { _clearAllOAuthConnectStates, getOAuthConnectState } = await import(
54
+ "../oauth/oauth-connect-state.js"
55
+ );
56
+
57
+ // ── Helpers ────────────────────────────────────────────────────────────────────
58
+
59
+ function findRoute(operationId: string) {
60
+ const route = ROUTES.find((r) => r.operationId === operationId);
61
+ if (!route) throw new Error(`Route ${operationId} not found`);
62
+ return route;
63
+ }
64
+
65
+ // ── Tests ──────────────────────────────────────────────────────────────────────
66
+
67
+ describe("oauth-connect-routes", () => {
68
+ describe("POST internal/oauth/connect/start", () => {
69
+ beforeEach(() => {
70
+ capturedOnDeferredComplete = undefined;
71
+ mockOrchestrateResult = {
72
+ success: true,
73
+ deferred: true,
74
+ authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
75
+ state: "test-state-uuid-abc123",
76
+ service: "google",
77
+ };
78
+ _clearAllOAuthConnectStates();
79
+ });
80
+
81
+ test("happy path returns auth_url and state, sets pending in state map", async () => {
82
+ const result = await findRoute("internal_oauth_connect_start").handler({
83
+ body: {
84
+ service: "google",
85
+ clientId: "my-client-id",
86
+ callbackTransport: "gateway",
87
+ },
88
+ });
89
+ expect(result).toEqual({
90
+ auth_url: "https://accounts.google.com/o/oauth2/auth?client_id=test",
91
+ state: "test-state-uuid-abc123",
92
+ });
93
+ // State map should have pending entry
94
+ expect(getOAuthConnectState("test-state-uuid-abc123")).toMatchObject({
95
+ status: "pending",
96
+ service: "google",
97
+ });
98
+ });
99
+
100
+ test("invalid callbackTransport throws BadRequestError", async () => {
101
+ await expect(
102
+ findRoute("internal_oauth_connect_start").handler({
103
+ body: {
104
+ service: "google",
105
+ clientId: "my-client-id",
106
+ callbackTransport: "ftp",
107
+ },
108
+ }),
109
+ ).rejects.toBeInstanceOf(BadRequestError);
110
+ });
111
+
112
+ test("missing clientId throws BadRequestError", async () => {
113
+ await expect(
114
+ findRoute("internal_oauth_connect_start").handler({
115
+ body: {
116
+ service: "google",
117
+ callbackTransport: "gateway",
118
+ },
119
+ }),
120
+ ).rejects.toBeInstanceOf(BadRequestError);
121
+ });
122
+
123
+ test("missing service throws BadRequestError", async () => {
124
+ await expect(
125
+ findRoute("internal_oauth_connect_start").handler({
126
+ body: {
127
+ clientId: "my-client-id",
128
+ callbackTransport: "gateway",
129
+ },
130
+ }),
131
+ ).rejects.toBeInstanceOf(BadRequestError);
132
+ });
133
+
134
+ test("orchestrator returns success:false throws InternalError", async () => {
135
+ mockOrchestrateResult = {
136
+ success: false,
137
+ error: "provider configuration error",
138
+ deferred: false,
139
+ };
140
+ await expect(
141
+ findRoute("internal_oauth_connect_start").handler({
142
+ body: {
143
+ service: "google",
144
+ clientId: "my-client-id",
145
+ callbackTransport: "gateway",
146
+ },
147
+ }),
148
+ ).rejects.toBeInstanceOf(InternalError);
149
+ });
150
+
151
+ test("loopback callbackTransport is also accepted", async () => {
152
+ const result = await findRoute("internal_oauth_connect_start").handler({
153
+ body: {
154
+ service: "google",
155
+ clientId: "my-client-id",
156
+ callbackTransport: "loopback",
157
+ },
158
+ });
159
+ expect(result).toMatchObject({
160
+ auth_url: "https://accounts.google.com/o/oauth2/auth?client_id=test",
161
+ state: "test-state-uuid-abc123",
162
+ });
163
+ });
164
+
165
+ test("success:true, deferred:false throws InternalError (synchronous completion not supported via daemon route)", async () => {
166
+ // The daemon-owned route requires a deferred flow so the CLI can poll for status.
167
+ // When the orchestrator returns { success: true, deferred: false } (e.g., already
168
+ // authenticated), the handler has no auth_url or state to return and throws an
169
+ // InternalError rather than silently returning a malformed response.
170
+ mockOrchestrateResult = {
171
+ success: true,
172
+ deferred: false,
173
+ service: "google",
174
+ grantedScopes: [],
175
+ };
176
+ await expect(
177
+ findRoute("internal_oauth_connect_start").handler({
178
+ body: {
179
+ service: "google",
180
+ clientId: "my-client-id",
181
+ callbackTransport: "gateway",
182
+ },
183
+ }),
184
+ ).rejects.toBeInstanceOf(InternalError);
185
+ });
186
+ });
187
+
188
+ describe("GET internal/oauth/connect/status/:state", () => {
189
+ beforeEach(() => {
190
+ _clearAllOAuthConnectStates();
191
+ capturedOnDeferredComplete = undefined;
192
+ mockOrchestrateResult = {
193
+ success: true,
194
+ deferred: true,
195
+ authorizeUrl: "https://accounts.google.com/o/oauth2/auth?client_id=test",
196
+ state: "test-state-uuid-abc123",
197
+ service: "google",
198
+ };
199
+ });
200
+
201
+ test("returns pending after start", async () => {
202
+ await findRoute("internal_oauth_connect_start").handler({
203
+ body: {
204
+ service: "google",
205
+ clientId: "my-client-id",
206
+ callbackTransport: "gateway",
207
+ },
208
+ });
209
+ const result = findRoute("internal_oauth_connect_status").handler({
210
+ pathParams: { state: "test-state-uuid-abc123" },
211
+ });
212
+ expect(result).toMatchObject({ status: "pending", service: "google" });
213
+ });
214
+
215
+ test("returns complete after onDeferredComplete fires with success", async () => {
216
+ await findRoute("internal_oauth_connect_start").handler({
217
+ body: {
218
+ service: "google",
219
+ clientId: "my-client-id",
220
+ callbackTransport: "gateway",
221
+ },
222
+ });
223
+ // Fire the onDeferredComplete callback manually
224
+ capturedOnDeferredComplete?.({
225
+ success: true,
226
+ service: "google",
227
+ accountInfo: "user@example.com",
228
+ });
229
+ const result = findRoute("internal_oauth_connect_status").handler({
230
+ pathParams: { state: "test-state-uuid-abc123" },
231
+ });
232
+ expect(result).toMatchObject({
233
+ status: "complete",
234
+ service: "google",
235
+ account_info: "user@example.com",
236
+ });
237
+ });
238
+
239
+ test("returns error after onDeferredComplete fires with failure", async () => {
240
+ await findRoute("internal_oauth_connect_start").handler({
241
+ body: {
242
+ service: "google",
243
+ clientId: "my-client-id",
244
+ callbackTransport: "gateway",
245
+ },
246
+ });
247
+ capturedOnDeferredComplete?.({
248
+ success: false,
249
+ service: "google",
250
+ error: "exchange failed",
251
+ });
252
+ const result = findRoute("internal_oauth_connect_status").handler({
253
+ pathParams: { state: "test-state-uuid-abc123" },
254
+ });
255
+ expect(result).toMatchObject({
256
+ status: "error",
257
+ service: "google",
258
+ error: "exchange failed",
259
+ });
260
+ });
261
+
262
+ test("throws NotFoundError for unknown state", () => {
263
+ expect(() =>
264
+ findRoute("internal_oauth_connect_status").handler({
265
+ pathParams: { state: "nonexistent-state" },
266
+ }),
267
+ ).toThrow(NotFoundError);
268
+ });
269
+
270
+ test("returns complete with granted_scopes after onDeferredComplete fires with grantedScopes", async () => {
271
+ await findRoute("internal_oauth_connect_start").handler({
272
+ body: {
273
+ service: "google",
274
+ clientId: "my-client-id",
275
+ callbackTransport: "gateway",
276
+ },
277
+ });
278
+ // Fire the onDeferredComplete callback with grantedScopes
279
+ capturedOnDeferredComplete?.({
280
+ success: true,
281
+ service: "google",
282
+ accountInfo: "user@example.com",
283
+ grantedScopes: ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.readonly"],
284
+ });
285
+ const result = findRoute("internal_oauth_connect_status").handler({
286
+ pathParams: { state: "test-state-uuid-abc123" },
287
+ }) as Record<string, unknown>;
288
+ expect(result).toMatchObject({
289
+ status: "complete",
290
+ service: "google",
291
+ account_info: "user@example.com",
292
+ granted_scopes: ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.readonly"],
293
+ });
294
+ });
295
+
296
+ test("complete without accountInfo does not include account_info field", async () => {
297
+ await findRoute("internal_oauth_connect_start").handler({
298
+ body: {
299
+ service: "google",
300
+ clientId: "my-client-id",
301
+ callbackTransport: "gateway",
302
+ },
303
+ });
304
+ capturedOnDeferredComplete?.({
305
+ success: true,
306
+ service: "google",
307
+ // No accountInfo
308
+ });
309
+ const result = findRoute("internal_oauth_connect_status").handler({
310
+ pathParams: { state: "test-state-uuid-abc123" },
311
+ }) as Record<string, unknown>;
312
+ expect(result.status).toBe("complete");
313
+ expect(result.account_info).toBeUndefined();
314
+ });
315
+ });
316
+ });
@@ -2,8 +2,26 @@ import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import { PROVIDER_SEED_DATA } from "../oauth/seed-providers.js";
4
4
 
5
+ /**
6
+ * Allowed CDN prefixes for ``logoUrl``. Mirrors the source registry in
7
+ * ``clients/shared/Resources/integration-logos-manifest.json``:
8
+ *
9
+ * - Simple Icons (CC0) is the default for most providers.
10
+ * - thesvg via jsDelivr is the documented fallback for brands Simple Icons
11
+ * doesn't host (e.g. Salesforce, which Simple Icons removed for
12
+ * trademark reasons). Same source is already used for the bundled PDFs
13
+ * of figma/github/gmail/linear/notion/outlook/slack.
14
+ *
15
+ * Adding another CDN should be a deliberate choice — extend this list
16
+ * and update the manifest in tandem.
17
+ */
18
+ const ALLOWED_LOGO_URL_PREFIXES = [
19
+ "https://cdn.simpleicons.org/",
20
+ "https://cdn.jsdelivr.net/gh/glincker/thesvg@",
21
+ ];
22
+
5
23
  describe("PROVIDER_SEED_DATA logo URLs", () => {
6
- test("every well-known provider has a Simple Icons CDN logoUrl", () => {
24
+ test("every well-known provider has a recognised CDN logoUrl", () => {
7
25
  const missing: string[] = [];
8
26
  const invalid: Array<{ provider: string; logoUrl: string }> = [];
9
27
 
@@ -12,7 +30,11 @@ describe("PROVIDER_SEED_DATA logo URLs", () => {
12
30
  missing.push(key);
13
31
  continue;
14
32
  }
15
- if (!seed.logoUrl.startsWith("https://cdn.simpleicons.org/")) {
33
+ if (
34
+ !ALLOWED_LOGO_URL_PREFIXES.some((prefix) =>
35
+ seed.logoUrl!.startsWith(prefix),
36
+ )
37
+ ) {
16
38
  invalid.push({ provider: key, logoUrl: seed.logoUrl });
17
39
  }
18
40
  }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Tests for `writeOnboardingSection` in persona-resolver.
3
+ *
4
+ * The function writes a managed `## Onboarding Context` section to the
5
+ * guardian persona file (with a fallback chain). These tests stub
6
+ * `util/platform.js` and `contacts/contact-store.js` to control the
7
+ * write target and verify idempotency, fallback, and field omission.
8
+ */
9
+
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ mkdtempSync,
14
+ readFileSync,
15
+ rmSync,
16
+ writeFileSync,
17
+ } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import {
21
+ afterAll,
22
+ afterEach,
23
+ beforeAll,
24
+ beforeEach,
25
+ describe,
26
+ expect,
27
+ mock,
28
+ test,
29
+ } from "bun:test";
30
+
31
+ // ── Mock state ────────────────────────────────────────────────────
32
+
33
+ let mockWorkspaceDir: string = "";
34
+ let mockVellumGuardian: {
35
+ contact: { userFile: string | null };
36
+ channel: Record<string, unknown>;
37
+ } | null = null;
38
+
39
+ // ── Mock modules (must precede imports from the module under test) ──
40
+
41
+ mock.module("../util/logger.js", () => ({
42
+ getLogger: () =>
43
+ new Proxy({} as Record<string, unknown>, {
44
+ get: (_target, prop) =>
45
+ prop === "child"
46
+ ? () =>
47
+ new Proxy({} as Record<string, unknown>, { get: () => () => {} })
48
+ : () => {},
49
+ }),
50
+ getCliLogger: () => ({}),
51
+ truncateForLog: (v: string) => v,
52
+ initLogger: () => {},
53
+ pruneOldLogFiles: () => 0,
54
+ }));
55
+
56
+ mock.module("../util/platform.js", () => ({
57
+ getWorkspaceDir: () => mockWorkspaceDir,
58
+ getWorkspacePromptPath: (file: string) => join(mockWorkspaceDir, file),
59
+ }));
60
+
61
+ mock.module("../contacts/contact-store.js", () => ({
62
+ findContactByChannelExternalId: () => null,
63
+ findGuardianForChannel: (channelType: string) =>
64
+ channelType === "vellum" ? mockVellumGuardian : null,
65
+ listGuardianChannels: () => null,
66
+ }));
67
+
68
+ // Import AFTER mocks so the module under test binds to the stubbed
69
+ // implementations.
70
+ import { writeOnboardingSection } from "../prompts/persona-resolver.js";
71
+
72
+ // ── Temp workspace scaffold ───────────────────────────────────────
73
+
74
+ let testRoot: string;
75
+
76
+ function workspacePath(file: string): string {
77
+ return join(mockWorkspaceDir, file);
78
+ }
79
+
80
+ beforeAll(() => {
81
+ testRoot = mkdtempSync(join(tmpdir(), "onboarding-persona-write-test-"));
82
+ });
83
+
84
+ afterAll(() => {
85
+ rmSync(testRoot, { recursive: true, force: true });
86
+ });
87
+
88
+ beforeEach(() => {
89
+ mockWorkspaceDir = mkdtempSync(join(testRoot, "ws-"));
90
+ mockVellumGuardian = null;
91
+ });
92
+
93
+ afterEach(() => {
94
+ rmSync(mockWorkspaceDir, { recursive: true, force: true });
95
+ });
96
+
97
+ // ── Tests ─────────────────────────────────────────────────────────
98
+
99
+ describe("writeOnboardingSection", () => {
100
+ test("writes section to guardian persona file when it exists", () => {
101
+ mockVellumGuardian = {
102
+ contact: { userFile: "alice.md" },
103
+ channel: {},
104
+ };
105
+ const guardianPath = workspacePath("users/alice.md");
106
+ mkdirSync(workspacePath("users"), { recursive: true });
107
+ writeFileSync(guardianPath, "# User Profile\n\n- **Name:** Alice\n");
108
+
109
+ writeOnboardingSection({
110
+ preferredName: "Alice",
111
+ commonWork: ["builds code, apps, or tools", "plans and coordinates work"],
112
+ dailyTools: ["GitHub", "Linear", "Slack"],
113
+ });
114
+
115
+ const content = readFileSync(guardianPath, "utf-8");
116
+ expect(content).toContain("- **Name:** Alice");
117
+ expect(content).toContain("## Onboarding Context");
118
+ expect(content).toContain("- **Preferred name:** Alice");
119
+ expect(content).toContain(
120
+ "- **Common work:** builds code, apps, or tools; plans and coordinates work",
121
+ );
122
+ expect(content).toContain("- **Daily tools:** GitHub, Linear, Slack");
123
+ });
124
+
125
+ test("falls back to users/default.md when guardian path is null", () => {
126
+ mockVellumGuardian = null;
127
+ mkdirSync(workspacePath("users"), { recursive: true });
128
+ writeFileSync(
129
+ workspacePath("users/default.md"),
130
+ "# User Profile\n\n- **Name:** Default User\n",
131
+ );
132
+
133
+ writeOnboardingSection({
134
+ preferredName: "Alice",
135
+ commonWork: [],
136
+ dailyTools: ["Slack"],
137
+ });
138
+
139
+ const content = readFileSync(workspacePath("users/default.md"), "utf-8");
140
+ expect(content).toContain("- **Name:** Default User");
141
+ expect(content).toContain("## Onboarding Context");
142
+ expect(content).toContain("- **Preferred name:** Alice");
143
+ expect(content).toContain("- **Daily tools:** Slack");
144
+
145
+ // USER.md should not be created
146
+ expect(existsSync(workspacePath("USER.md"))).toBe(false);
147
+ });
148
+
149
+ test("falls back to USER.md when no users/ files exist", () => {
150
+ mockVellumGuardian = null;
151
+
152
+ writeOnboardingSection({
153
+ preferredName: "Alice",
154
+ commonWork: [],
155
+ dailyTools: [],
156
+ });
157
+
158
+ expect(existsSync(workspacePath("USER.md"))).toBe(true);
159
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
160
+ expect(content).toContain("## Onboarding Context");
161
+ expect(content).toContain("- **Preferred name:** Alice");
162
+ });
163
+
164
+ test("creates file with header + section when target doesn't exist", () => {
165
+ mockVellumGuardian = null;
166
+
167
+ writeOnboardingSection({
168
+ preferredName: "Alice",
169
+ commonWork: ["builds code, apps, or tools"],
170
+ dailyTools: ["GitHub", "Linear", "Slack"],
171
+ });
172
+
173
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
174
+ expect(content).toContain("# User Profile");
175
+ expect(content).toContain("## Onboarding Context");
176
+ expect(content).toContain("- **Preferred name:** Alice");
177
+ expect(content).toContain("- **Common work:** builds code, apps, or tools");
178
+ expect(content).toContain("- **Daily tools:** GitHub, Linear, Slack");
179
+ });
180
+
181
+ test("idempotent: calling twice produces the same file content", () => {
182
+ mockVellumGuardian = null;
183
+ const normalized = {
184
+ preferredName: "Alice",
185
+ commonWork: ["builds code, apps, or tools"],
186
+ dailyTools: ["GitHub", "Linear"],
187
+ };
188
+
189
+ writeOnboardingSection(normalized);
190
+ const first = readFileSync(workspacePath("USER.md"), "utf-8");
191
+
192
+ writeOnboardingSection(normalized);
193
+ const second = readFileSync(workspacePath("USER.md"), "utf-8");
194
+
195
+ expect(first).toBe(second);
196
+ });
197
+
198
+ test("replaces existing onboarding section with updated data", () => {
199
+ mockVellumGuardian = null;
200
+
201
+ writeOnboardingSection({
202
+ preferredName: "Alice",
203
+ commonWork: ["builds code, apps, or tools"],
204
+ dailyTools: ["GitHub"],
205
+ });
206
+
207
+ writeOnboardingSection({
208
+ preferredName: "Bob",
209
+ commonWork: ["writes docs, emails, or content"],
210
+ dailyTools: ["Notion", "Slack"],
211
+ });
212
+
213
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
214
+ expect(content).toContain("- **Preferred name:** Bob");
215
+ expect(content).toContain(
216
+ "- **Common work:** writes docs, emails, or content",
217
+ );
218
+ expect(content).toContain("- **Daily tools:** Notion, Slack");
219
+ // Old values should be gone
220
+ expect(content).not.toContain("**Preferred name:** Alice");
221
+ expect(content).not.toContain("GitHub");
222
+ });
223
+
224
+ test("preserves content outside the managed section", () => {
225
+ mockVellumGuardian = null;
226
+ writeFileSync(
227
+ workspacePath("USER.md"),
228
+ "# User Profile\n\n- **Name:** Alice\n- **Role:** Engineer\n",
229
+ );
230
+
231
+ writeOnboardingSection({
232
+ preferredName: "Alice",
233
+ commonWork: [],
234
+ dailyTools: ["GitHub"],
235
+ });
236
+
237
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
238
+ expect(content).toContain("- **Name:** Alice");
239
+ expect(content).toContain("- **Role:** Engineer");
240
+ expect(content).toContain("## Onboarding Context");
241
+ expect(content).toContain("- **Daily tools:** GitHub");
242
+ });
243
+
244
+ test("omits empty fields", () => {
245
+ mockVellumGuardian = null;
246
+
247
+ writeOnboardingSection({
248
+ commonWork: [],
249
+ dailyTools: [],
250
+ });
251
+
252
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
253
+ expect(content).toContain("## Onboarding Context");
254
+ expect(content).not.toContain("Preferred name");
255
+ expect(content).not.toContain("Common work");
256
+ expect(content).not.toContain("Daily tools");
257
+ });
258
+
259
+ test("omits preferredName when undefined", () => {
260
+ mockVellumGuardian = null;
261
+
262
+ writeOnboardingSection({
263
+ preferredName: undefined,
264
+ commonWork: ["builds code, apps, or tools"],
265
+ dailyTools: ["GitHub"],
266
+ });
267
+
268
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
269
+ expect(content).not.toContain("Preferred name");
270
+ expect(content).toContain("- **Common work:** builds code, apps, or tools");
271
+ expect(content).toContain("- **Daily tools:** GitHub");
272
+ });
273
+
274
+ test("preserves content after onboarding section when followed by another heading", () => {
275
+ mockVellumGuardian = null;
276
+ writeFileSync(
277
+ workspacePath("USER.md"),
278
+ [
279
+ "# User Profile",
280
+ "",
281
+ "- **Name:** Alice",
282
+ "",
283
+ "## Onboarding Context",
284
+ "",
285
+ "- **Preferred name:** Alice",
286
+ "",
287
+ "## Preferences",
288
+ "",
289
+ "- Likes dark mode",
290
+ "",
291
+ ].join("\n"),
292
+ );
293
+
294
+ writeOnboardingSection({
295
+ preferredName: "Bob",
296
+ commonWork: [],
297
+ dailyTools: ["Slack"],
298
+ });
299
+
300
+ const content = readFileSync(workspacePath("USER.md"), "utf-8");
301
+ expect(content).toContain("- **Preferred name:** Bob");
302
+ expect(content).toContain("- **Daily tools:** Slack");
303
+ expect(content).toContain("## Preferences");
304
+ expect(content).toContain("- Likes dark mode");
305
+ // Old preferred name should be gone from the onboarding section
306
+ expect(content).not.toContain("**Preferred name:** Alice");
307
+ });
308
+ });