@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -22,11 +22,7 @@ import { getConversation } from "../memory/conversation-crud.js";
22
22
  import { isBackgroundConversationType } from "../memory/conversation-types.js";
23
23
  import { getLogger } from "../util/logger.js";
24
24
  import type { NotificationSignal } from "./signal.js";
25
- import type {
26
- NotificationDecision,
27
- NotificationDeliveryResult,
28
- RenderedChannelCopy,
29
- } from "./types.js";
25
+ import type { NotificationDecision, RenderedChannelCopy } from "./types.js";
30
26
 
31
27
  const log = getLogger("home-feed-side-effect");
32
28
 
@@ -41,6 +37,16 @@ const FEED_ITEM_URGENCIES: ReadonlySet<string> = new Set<FeedItemUrgency>([
41
37
  * Append a `FeedItem` for the given notification signal when the
42
38
  * filter criteria pass.
43
39
  *
40
+ * `fallbackConversationId` is used as the feed item's "Go to Convo"
41
+ * navigation target when `signal.sourceContextId` doesn't resolve to a
42
+ * real conversation row. The notification broadcaster pairs the vellum
43
+ * delivery with a conversation (newly created or reused) before this
44
+ * function runs, so callers can thread that paired id through here for
45
+ * producers whose `sourceContextId` is a sentinel (heartbeat startup,
46
+ * credential health, watcher emits, scheduler retries-exhausted) — the
47
+ * feed item will then carry the paired delivery conversation and the
48
+ * "Go to Convo" button can render.
49
+ *
44
50
  * Returns the persisted `FeedItem`, or `null` if the signal does not
45
51
  * qualify for home-feed mirroring (non-background origin AND no
46
52
  * `isAsyncBackground` hint) or if schema validation fails.
@@ -48,9 +54,13 @@ const FEED_ITEM_URGENCIES: ReadonlySet<string> = new Set<FeedItemUrgency>([
48
54
  export async function writeHomeFeedItemForSignal(
49
55
  signal: NotificationSignal,
50
56
  decision: NotificationDecision,
51
- deliveryResults: NotificationDeliveryResult[],
57
+ fallbackConversationId?: string,
52
58
  ): Promise<FeedItem | null> {
53
- if (!shouldMirrorToHomeFeed(signal)) return null;
59
+ const { mirror, sourceConversationId } = resolveHomeFeedMirror(
60
+ signal,
61
+ fallbackConversationId,
62
+ );
63
+ if (!mirror) return null;
54
64
 
55
65
  const renderedCopy =
56
66
  decision.renderedCopy.vellum ??
@@ -77,9 +87,6 @@ export async function writeHomeFeedItemForSignal(
77
87
  return null;
78
88
  }
79
89
 
80
- const conversationId = deliveryResults.find(
81
- (r) => r.channel === "vellum",
82
- )?.conversationId;
83
90
  const urgency = FEED_ITEM_URGENCIES.has(signal.attentionHints.urgency)
84
91
  ? (signal.attentionHints.urgency as FeedItemUrgency)
85
92
  : undefined;
@@ -107,7 +114,7 @@ export async function writeHomeFeedItemForSignal(
107
114
  noteworthy: deriveNoteworthy(signal),
108
115
  fromAssistant: signal.sourceChannel === "assistant_tool",
109
116
  ...(urgency ? { urgency } : {}),
110
- ...(conversationId ? { conversationId } : {}),
117
+ ...(sourceConversationId ? { conversationId: sourceConversationId } : {}),
111
118
  ...(panelKind ? { detailPanel: { kind: panelKind } } : {}),
112
119
  ...(metadata ? { metadata } : {}),
113
120
  };
@@ -166,28 +173,51 @@ function deriveDetailPanelKind(
166
173
  }
167
174
 
168
175
  /**
169
- * `sourceContextId` is best-effort it may not be a conversation id
170
- * (e.g. scheduler job id, watcher event id), so a lookup failure
171
- * falls through to "not a background conversation" rather than throwing.
176
+ * The lookup is best-effort and unified: a single `getConversation` call
177
+ * both gates the "background conversation" mirror branch and populates
178
+ * `sourceConversationId` for the "Go to Thread" navigation target. Misses
179
+ * (scheduler job ids, watcher event ids, CLI tool-call ids) leave
180
+ * `sourceConversationId` undefined so the client hides the affordance.
172
181
  *
173
- * `assistant_tool` is the source channel used by the `notifications send`
174
- * skill (and by background-job failure emits). These signals represent
175
- * the assistant actively choosing to share, so we mirror them into the
176
- * home feed without requiring a background-typed conversation or the
177
- * `isAsyncBackground` hint — the documented (SKILL.md) CLI surface
178
- * intentionally does not expose either; internal call sites that still set
179
- * the hint keep working unchanged.
182
+ * `assistant_tool` mirrors unconditionally because the documented
183
+ * `notifications send` skill (and background-job failure emits) deliberately
184
+ * does not require a background-typed conversation or the
185
+ * `isAsyncBackground` hint.
180
186
  */
181
- function shouldMirrorToHomeFeed(signal: NotificationSignal): boolean {
182
- if (signal.sourceChannel === "assistant_tool") return true;
183
- if (signal.attentionHints.isAsyncBackground) return true;
184
- if (!signal.sourceContextId) return false;
185
- try {
186
- const row = getConversation(signal.sourceContextId);
187
- return isBackgroundConversationType(row?.conversationType);
188
- } catch {
189
- return false;
187
+ function resolveHomeFeedMirror(
188
+ signal: NotificationSignal,
189
+ fallbackConversationId?: string,
190
+ ): {
191
+ mirror: boolean;
192
+ sourceConversationId?: string;
193
+ } {
194
+ let sourceRow: { conversationType?: string } | null = null;
195
+ if (signal.sourceContextId) {
196
+ try {
197
+ sourceRow = getConversation(signal.sourceContextId) ?? null;
198
+ } catch {
199
+ sourceRow = null;
200
+ }
201
+ }
202
+ // Prefer the producer's source context (e.g. the heartbeat / background
203
+ // job conversation that emitted the signal) for the "Go to Convo" target,
204
+ // since that's where the work actually happened. Fall back to the paired
205
+ // delivery conversation only when the source context didn't resolve —
206
+ // covers producers whose `sourceContextId` is a sentinel string.
207
+ const sourceConversationId = sourceRow
208
+ ? signal.sourceContextId
209
+ : fallbackConversationId;
210
+
211
+ if (signal.sourceChannel === "assistant_tool") {
212
+ return { mirror: true, sourceConversationId };
213
+ }
214
+ if (signal.attentionHints.isAsyncBackground) {
215
+ return { mirror: true, sourceConversationId };
216
+ }
217
+ if (isBackgroundConversationType(sourceRow?.conversationType)) {
218
+ return { mirror: true, sourceConversationId };
190
219
  }
220
+ return { mirror: false };
191
221
  }
192
222
 
193
223
  function readPayloadString(payload: unknown, key: string): string | undefined {
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { emitPostConnectNudge } from "../home/post-connect-feed.js";
23
+ import { invalidateAssistantSuggestedPromptsCache } from "../home/suggested-prompts.js";
23
24
  import type { TokenEndpointAuthMethod } from "../security/oauth2.js";
24
25
  import { prepareOAuth2Flow, startOAuth2Flow } from "../security/oauth2.js";
25
26
  import { getLogger } from "../util/logger.js";
@@ -252,6 +253,7 @@ export async function orchestrateOAuthConnect(
252
253
  },
253
254
  "Deferred OAuth2 flow completed — tokens stored",
254
255
  );
256
+ invalidateAssistantSuggestedPromptsCache();
255
257
  void emitPostConnectNudge(options.service);
256
258
  options.onDeferredComplete?.({
257
259
  success: true,
@@ -375,6 +377,7 @@ export async function orchestrateOAuthConnect(
375
377
  "orchestrateOAuthConnect: tokens stored, connect complete",
376
378
  );
377
379
 
380
+ invalidateAssistantSuggestedPromptsCache();
378
381
  void emitPostConnectNudge(options.service);
379
382
 
380
383
  return {
@@ -49,6 +49,8 @@ function manualTokenAccessCredentialKey(provider: string): string | null {
49
49
  return credentialKey("slack_channel", "bot_token");
50
50
  case "telegram":
51
51
  return credentialKey("telegram", "bot_token");
52
+ case "sanity":
53
+ return credentialKey("sanity", "api_token");
52
54
  default:
53
55
  return null;
54
56
  }
@@ -122,6 +122,24 @@ export async function syncManualTokenConnection(
122
122
  return;
123
123
  }
124
124
 
125
+ case "sanity": {
126
+ const tokenResult = await getSecureKeyResultAsync(
127
+ credentialKey("sanity", "api_token"),
128
+ );
129
+ if (tokenResult.unreachable) {
130
+ log.warn(
131
+ "Skipping sanity manual-token reconciliation — credential backend unreachable",
132
+ );
133
+ return;
134
+ }
135
+ if (tokenResult.value) {
136
+ await ensureManualTokenConnection(provider, accountInfo);
137
+ } else {
138
+ removeManualTokenConnection(provider);
139
+ }
140
+ return;
141
+ }
142
+
125
143
  default:
126
144
  return;
127
145
  }
@@ -142,4 +160,5 @@ export async function syncManualTokenConnection(
142
160
  export async function backfillManualTokenConnections(): Promise<void> {
143
161
  await syncManualTokenConnection("telegram");
144
162
  await syncManualTokenConnection("slack_channel");
163
+ await syncManualTokenConnection("sanity");
145
164
  }
@@ -1100,5 +1100,17 @@ export async function disconnectOAuthProvider(
1100
1100
 
1101
1101
  deleteConnection(conn.id);
1102
1102
 
1103
+ // Dynamic import: `suggested-prompts.ts` imports from this module, so a
1104
+ // static import here would create a cycle. The cache invalidation is
1105
+ // best-effort — failures must not block disconnect.
1106
+ void import("../home/suggested-prompts.js")
1107
+ .then((m) => m.invalidateAssistantSuggestedPromptsCache())
1108
+ .catch((err) => {
1109
+ log.warn(
1110
+ { err: err instanceof Error ? err.message : String(err) },
1111
+ "Failed to invalidate suggested-prompts cache after disconnect",
1112
+ );
1113
+ });
1114
+
1103
1115
  return "disconnected";
1104
1116
  }
@@ -746,6 +746,28 @@ export const PROVIDER_SEED_DATA: Record<
746
746
  logoUrl: "https://cdn.simpleicons.org/telegram",
747
747
  defaultScopes: [],
748
748
  },
749
+
750
+ sanity: {
751
+ provider: "sanity",
752
+ authorizeUrl: "urn:manual-token",
753
+ tokenExchangeUrl: "urn:manual-token",
754
+ baseUrl: "https://api.sanity.io",
755
+ displayLabel: "Sanity",
756
+ description: "Content management platform",
757
+ dashboardUrl: "https://www.sanity.io/manage",
758
+ clientIdPlaceholder: null,
759
+ requiresClientSecret: false,
760
+ logoUrl: "https://cdn.simpleicons.org/sanity",
761
+ defaultScopes: [],
762
+ injectionTemplates: [
763
+ {
764
+ hostPattern: "*.sanity.io",
765
+ injectionType: "header",
766
+ headerName: "Authorization",
767
+ valuePrefix: "Bearer ",
768
+ },
769
+ ],
770
+ },
749
771
  };
750
772
 
751
773
  export const SEEDED_PROVIDER_KEYS = new Set(Object.keys(PROVIDER_SEED_DATA));
@@ -196,7 +196,10 @@ export class PermissionPrompter {
196
196
  }
197
197
  // The prompter owns deregistration; all callers use get() to peek before
198
198
  // routing to resolveConfirmation, which fires the rpcResolve callback.
199
- const interaction = pendingInteractions.resolve(requestId);
199
+ const interaction = pendingInteractions.resolve(
200
+ requestId,
201
+ decision === "allow" ? "approved" : "rejected",
202
+ );
200
203
  this.ownedIds.delete(requestId);
201
204
  (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
202
205
  { decision, selectedPattern, selectedScope, decisionContext },
@@ -210,7 +213,7 @@ export class PermissionPrompter {
210
213
  */
211
214
  denyAllPending(): void {
212
215
  for (const requestId of [...this.ownedIds]) {
213
- const interaction = pendingInteractions.resolve(requestId);
216
+ const interaction = pendingInteractions.resolve(requestId, "superseded");
214
217
  this.ownedIds.delete(requestId);
215
218
  (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
216
219
  {
@@ -130,7 +130,10 @@ export class SecretPrompter {
130
130
  }
131
131
  // approval-routes calls pendingInteractions.get() before routing here;
132
132
  // the prompter owns deregistration so it fires the Promise callback cleanly.
133
- const interaction = pendingInteractions.resolve(requestId);
133
+ const interaction = pendingInteractions.resolve(
134
+ requestId,
135
+ value === undefined ? "cancelled" : "answered",
136
+ );
134
137
  this.ownedIds.delete(requestId);
135
138
  (interaction?.rpcResolve as ((v: SecretPromptResult) => void) | undefined)?.(
136
139
  { value: value ?? null, delivery: delivery ?? "store" },
@@ -20,6 +20,7 @@
20
20
  * | `memory-v2-static` | 38 | after-memory-prefix |
21
21
  * | `now-md` | 40 | after-memory-prefix |
22
22
  * | `active-documents` | 45 | prepend-user-tail |
23
+ * | `document-comments` | 46 | prepend-user-tail |
23
24
  * | `subagent-status` | 50 | append-user-tail |
24
25
  * | `slack-messages` | 60 | replace-run-messages |
25
26
  * | `thread-focus` | 70 | append-user-tail |
@@ -51,6 +52,7 @@ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-fl
51
52
  import { getConfig } from "../../config/loader.js";
52
53
  import { getInContextPkbPaths } from "../../daemon/pkb-context-tracker.js";
53
54
  import { buildPkbReminder } from "../../daemon/pkb-reminder-builder.js";
55
+ import { listComments } from "../../documents/document-comments-store.js";
54
56
  import { searchPkbFiles } from "../../memory/pkb/pkb-search.js";
55
57
  import { getLogger } from "../../util/logger.js";
56
58
  import { registerPlugin } from "../registry.js";
@@ -94,6 +96,7 @@ export const DEFAULT_INJECTOR_ORDER = {
94
96
  memoryV2Static: 38,
95
97
  nowMd: 40,
96
98
  activeDocuments: 45,
99
+ documentComments: 46,
97
100
  subagentStatus: 50,
98
101
  slackMessages: 60,
99
102
  threadFocus: 70,
@@ -133,12 +136,11 @@ const diskPressureWarningInjector: Injector = {
133
136
  };
134
137
 
135
138
  /**
136
- * v2 read-side cutover guard. The `pkb-context` injector silences itself
137
- * under v2 because the `<knowledge_base>` block surfaces PKB content the v2
138
- * activation block already covers. The `pkb-reminder` injector still fires
139
- * (its body is generic recall/remember guidance) but skips the hybrid-search
140
- * hints — those name PKB paths v2 is moving away from. NOW.md is workspace
141
- * state independent of PKB and fires unchanged.
139
+ * v2 read-side cutover guard. Under v2 both `pkb-context` and `pkb-reminder`
140
+ * silence themselves entirely the `<knowledge_base>` content and the
141
+ * generic recall/remember nudge are both supplanted by the v2 static
142
+ * `<memory>` block. NOW.md is workspace state independent of PKB and fires
143
+ * unchanged.
142
144
  */
143
145
  function isPkbInjectionSilencedByV2(): boolean {
144
146
  return getConfig().memory.v2.enabled;
@@ -287,9 +289,8 @@ const pkbReminderInjector: Injector = {
287
289
  const mode = inputs.mode ?? "full";
288
290
  if (mode !== "full") return null;
289
291
  if (!inputs.pkbActive) return null;
290
- const reminder = isPkbInjectionSilencedByV2()
291
- ? buildPkbReminder([])
292
- : await buildPkbReminderWithHints(inputs);
292
+ if (isPkbInjectionSilencedByV2()) return null;
293
+ const reminder = await buildPkbReminderWithHints(inputs);
293
294
  return {
294
295
  id: "pkb-reminder",
295
296
  text: reminder,
@@ -491,6 +492,77 @@ const activeDocumentsInjector: Injector = {
491
492
  },
492
493
  };
493
494
 
495
+ /** Maximum open comments surfaced per document to limit context bloat. */
496
+ const DOCUMENT_COMMENTS_CAP = 10;
497
+
498
+ /**
499
+ * Escape closing `</document_comments>` inside user-controlled strings so
500
+ * they cannot break out of the XML wrapper — same pattern as
501
+ * {@link buildPkbContextBlock} and {@link buildMemoryV2StaticBlock}.
502
+ */
503
+ function escapeDocCommentTag(s: string): string {
504
+ return s.replace(/<\/document_comments\s*>/gi, "&lt;/document_comments&gt;");
505
+ }
506
+
507
+ /**
508
+ * `document-comments` injector — order 46, prepend-user-tail.
509
+ *
510
+ * Surfaces open top-level comments on active documents so the assistant
511
+ * knows what feedback to address. For each active document, queries the
512
+ * comment store for open top-level comments (capped at
513
+ * {@link DOCUMENT_COMMENTS_CAP} most recent per document). Inline comments
514
+ * include the quoted anchor text; doc-level comments are labelled as such.
515
+ *
516
+ * Gating:
517
+ * - `mode === "full"`.
518
+ * - `activeDocuments` has at least one entry.
519
+ * - At least one document has open comments (returns null otherwise).
520
+ */
521
+ const documentCommentsInjector: Injector = {
522
+ name: "document-comments",
523
+ order: DEFAULT_INJECTOR_ORDER.documentComments,
524
+ async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
525
+ const inputs = readInjectionInputs(ctx);
526
+ const mode = inputs.mode ?? "full";
527
+ if (mode !== "full") return null;
528
+ const docs = inputs.activeDocuments;
529
+ if (!docs || docs.length === 0) return null;
530
+
531
+ const sections: string[] = [];
532
+ for (const doc of docs) {
533
+ const comments = listComments(doc.surfaceId, {
534
+ status: "open",
535
+ topLevelOnly: true,
536
+ }).slice(-DOCUMENT_COMMENTS_CAP);
537
+ if (comments.length === 0) continue;
538
+
539
+ const lines = comments.map((c) => {
540
+ const anchor =
541
+ c.anchorText != null ? escapeDocCommentTag(c.anchorText) : null;
542
+ const label =
543
+ anchor != null ? `inline, anchored to "${anchor}"` : "doc-level";
544
+ return `- Comment #${c.id} (${label}): "${escapeDocCommentTag(c.content)}"`;
545
+ });
546
+ sections.push(
547
+ `Document: "${escapeDocCommentTag(doc.title)}" (surface_id: "${doc.surfaceId}")\n${lines.join("\n")}`,
548
+ );
549
+ }
550
+
551
+ if (sections.length === 0) return null;
552
+
553
+ const text = `<document_comments>
554
+ Open comments on your documents. Address these by editing the document, then use comment_resolve to mark each resolved.
555
+
556
+ ${sections.join("\n\n")}
557
+ </document_comments>`;
558
+ return {
559
+ id: "document-comments",
560
+ text,
561
+ placement: "prepend-user-tail",
562
+ };
563
+ },
564
+ };
565
+
494
566
  /**
495
567
  * `subagent-status` injector — order 50, append-user-tail.
496
568
  *
@@ -626,6 +698,7 @@ export const defaultInjectorsPlugin: Plugin = {
626
698
  memoryV2StaticInjector,
627
699
  nowMdInjector,
628
700
  activeDocumentsInjector,
701
+ documentCommentsInjector,
629
702
  subagentStatusInjector,
630
703
  slackMessagesInjector,
631
704
  threadFocusInjector,
@@ -6,7 +6,8 @@
6
6
  * user-message injection that replaced it.
7
7
  */
8
8
 
9
- import { mkdirSync } from "node:fs";
9
+ import { copyFileSync, mkdirSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
10
11
  import { beforeEach, describe, expect, mock, test } from "bun:test";
11
12
 
12
13
  const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
@@ -57,7 +58,8 @@ mock.module("../../config/loader.js", () => ({
57
58
  setNestedValue: () => {},
58
59
  }));
59
60
 
60
- const { buildSystemPrompt } = await import("../system-prompt.js");
61
+ const { buildSystemPrompt, maybeReseedBootstrapForCohort } =
62
+ await import("../system-prompt.js");
61
63
 
62
64
  describe("buildSystemPrompt — tool routing guidance", () => {
63
65
  beforeEach(() => {
@@ -70,3 +72,45 @@ describe("buildSystemPrompt — tool routing guidance", () => {
70
72
  expect(result).not.toContain("ask_question");
71
73
  });
72
74
  });
75
+
76
+ describe("maybeReseedBootstrapForCohort — content-automation template", () => {
77
+ const templatesDir = join(import.meta.dirname!, "..", "templates");
78
+
79
+ beforeEach(() => {
80
+ mkdirSync(TEST_DIR, { recursive: true });
81
+ // Seed the workspace with the generic BOOTSTRAP.md so the cohort
82
+ // reseed detects it as an unmodified template and overwrites it.
83
+ copyFileSync(
84
+ join(templatesDir, "BOOTSTRAP.md"),
85
+ join(TEST_DIR, "BOOTSTRAP.md"),
86
+ );
87
+ });
88
+
89
+ function reseedAndRead(): string {
90
+ maybeReseedBootstrapForCohort("content-automation");
91
+ return readFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "utf-8");
92
+ }
93
+
94
+ test("loads the geo-writing skill on first turn", () => {
95
+ const content = reseedAndRead();
96
+ expect(content).toContain("geo-writing");
97
+ });
98
+
99
+ test("uses skill-first onboarding approach", () => {
100
+ const content = reseedAndRead();
101
+ expect(content).toContain("Skill-First Onboarding");
102
+ expect(content).toContain("The skill is the onboarding");
103
+ });
104
+
105
+ test("includes comment-driven edit loop", () => {
106
+ const content = reseedAndRead();
107
+ expect(content).toContain("comment-driven");
108
+ expect(content).toContain("comment_resolve");
109
+ expect(content).toContain("document_update");
110
+ });
111
+
112
+ test("references VOICE.md for voice capture", () => {
113
+ const content = reseedAndRead();
114
+ expect(content).toContain("VOICE.md");
115
+ });
116
+ });
@@ -19,6 +19,20 @@ export const TOOL_DISPLAY_NAMES: Record<string, string> = {
19
19
  "apple-notes": "Apple Notes",
20
20
  };
21
21
 
22
+ /**
23
+ * Map of known prior-assistant IDs (from the client onboarding UI) to display names.
24
+ * Unknown IDs pass through with first-letter capitalization via `normalizePriorAssistants`.
25
+ */
26
+ export const PRIOR_ASSISTANT_DISPLAY_NAMES: Record<string, string> = {
27
+ chatgpt: "ChatGPT",
28
+ claude: "Claude",
29
+ openclaw: "OpenClaw",
30
+ hermes: "Hermes",
31
+ manus: "Manus",
32
+ gemini: "Gemini",
33
+ copilot: "Copilot",
34
+ };
35
+
22
36
  /**
23
37
  * Map of known task IDs to plain-language labels describing what the assistant
24
38
  * does for each task category.
@@ -56,14 +70,28 @@ export function normalizeTasks(tasks: string[]): string[] {
56
70
  return tasks.map((id) => TASK_DISPLAY_LABELS[id] ?? id);
57
71
  }
58
72
 
73
+ /**
74
+ * Maps each prior-assistant ID through `PRIOR_ASSISTANT_DISPLAY_NAMES`,
75
+ * falling back to first-letter capitalization for unknown IDs.
76
+ */
77
+ export function normalizePriorAssistants(assistants: string[]): string[] {
78
+ return assistants.map(
79
+ (id) => PRIOR_ASSISTANT_DISPLAY_NAMES[id] ?? capitalizeFirst(id),
80
+ );
81
+ }
82
+
59
83
  export interface NormalizedOnboarding {
60
84
  preferredName?: string;
61
85
  commonWork: string[];
62
86
  dailyTools: string[];
63
87
  tone?: string;
64
88
  assistantName?: string;
89
+ priorAssistants?: string[];
65
90
  googleConnected?: boolean;
66
91
  googleServices?: string[];
92
+ cohort?: string;
93
+ websiteUrl?: string;
94
+ contentSourceUrl?: string;
67
95
  }
68
96
 
69
97
  const SCOPE_SERVICE_MAP: Record<string, string> = {
@@ -103,5 +131,17 @@ export function normalizeOnboardingContext(
103
131
  googleServices: ctx.googleConnected
104
132
  ? deriveGoogleServices(ctx.googleScopes)
105
133
  : undefined,
134
+ priorAssistants: ctx.priorAssistants?.length
135
+ ? normalizePriorAssistants(ctx.priorAssistants)
136
+ : undefined,
137
+ cohort: ctx.cohort,
138
+ websiteUrl:
139
+ typeof ctx.websiteUrl === "string"
140
+ ? ctx.websiteUrl.trim().replace(/[\r\n\t]/g, "") || undefined
141
+ : undefined,
142
+ contentSourceUrl:
143
+ typeof ctx.contentSourceUrl === "string"
144
+ ? ctx.contentSourceUrl.trim().replace(/[\r\n\t]/g, "") || undefined
145
+ : undefined,
106
146
  };
107
147
  }
@@ -93,7 +93,10 @@ function collectSectionIds(workspaceDir: string): string[] {
93
93
  if (name.endsWith(".md")) ids.add(name.slice(0, -".md".length));
94
94
  }
95
95
  } catch (err) {
96
- log.warn({ err, workspaceDir }, "Failed to list workspace system prompt dir");
96
+ log.warn(
97
+ { err, workspaceDir },
98
+ "Failed to list workspace system prompt dir",
99
+ );
97
100
  }
98
101
  }
99
102
  return [...ids].sort();
@@ -102,6 +105,7 @@ function collectSectionIds(workspaceDir: string): string[] {
102
105
  interface ResolvedSection {
103
106
  enabled: string | boolean | undefined;
104
107
  body: string;
108
+ transform?: BundledSection["transform"];
105
109
  }
106
110
 
107
111
  function resolveSection(
@@ -114,12 +118,20 @@ function resolveSection(
114
118
  try {
115
119
  raw = readFileSync(workspacePath, "utf-8");
116
120
  } catch (err) {
117
- log.warn({ err, workspacePath }, "Failed to read workspace section override");
121
+ log.warn(
122
+ { err, workspacePath },
123
+ "Failed to read workspace section override",
124
+ );
118
125
  return null;
119
126
  }
120
127
  const parsed = parseFrontmatterFields(raw);
121
128
  const fields = parsed?.fields ?? {};
122
129
  const body = parsed?.body ?? raw;
130
+ // Workspace override skips the bundled transform: when the user has
131
+ // written their own `prompts/system/<id>.md` they've taken full
132
+ // control of the body shape, and re-running the bundled transform
133
+ // (e.g. unmodified-template detection on IDENTITY.md) would
134
+ // misclassify their override.
123
135
  return { enabled: fields["enabled"] as string | boolean | undefined, body };
124
136
  }
125
137
  const bundled = BUNDLED_SYSTEM_SECTIONS.find((s) => s.id === id);
@@ -128,7 +140,8 @@ function resolveSection(
128
140
  // A bundled section may delegate its body to a workspace file outside
129
141
  // the section override directory (e.g. `SOUL.md` at the workspace
130
142
  // root). Read it now; missing/empty files yield "", which
131
- // `renderSection` then gates off via its empty-body check.
143
+ // `renderSection` then gates off via its empty-body check (or via the
144
+ // section's `transform`, if set).
132
145
  if (bundled.workspacePath) {
133
146
  const filePath = getWorkspacePromptPath(bundled.workspacePath);
134
147
  let body = "";
@@ -136,16 +149,17 @@ function resolveSection(
136
149
  try {
137
150
  body = readFileSync(filePath, "utf-8");
138
151
  } catch (err) {
139
- log.warn(
140
- { err, filePath, id },
141
- "Failed to read section workspacePath",
142
- );
152
+ log.warn({ err, filePath, id }, "Failed to read section workspacePath");
143
153
  }
144
154
  }
145
- return { enabled: bundled.enabled, body };
155
+ return { enabled: bundled.enabled, body, transform: bundled.transform };
146
156
  }
147
157
 
148
- return { enabled: bundled.enabled, body: bundled.body };
158
+ return {
159
+ enabled: bundled.enabled,
160
+ body: bundled.body,
161
+ transform: bundled.transform,
162
+ };
149
163
  }
150
164
 
151
165
  function renderSection(
@@ -158,7 +172,14 @@ function renderSection(
158
172
 
159
173
  if (!isEnabled(section.enabled, ctx)) return null;
160
174
 
161
- const stripped = stripCommentLines(section.body).trim();
175
+ let body = section.body;
176
+ if (section.transform) {
177
+ const transformed = section.transform(body, ctx);
178
+ if (transformed === null) return null;
179
+ body = transformed;
180
+ }
181
+
182
+ const stripped = stripCommentLines(body).trim();
162
183
  if (stripped.length === 0) return null;
163
184
  return interpolateVariables(stripped, ctx);
164
185
  }
@@ -190,10 +211,7 @@ const IDENT_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
190
211
  * typo on a `{{key}}` substitution surfaces at the warn log rather than
191
212
  * inlining the string `"undefined"`).
192
213
  */
193
- function interpolateVariables(
194
- body: string,
195
- ctx: SectionRenderContext,
196
- ): string {
214
+ function interpolateVariables(body: string, ctx: SectionRenderContext): string {
197
215
  // Collapse standalone tag lines so multiline section templates render
198
216
  // without phantom blank lines from the layout markers.
199
217
  const collapsed = body.replace(STANDALONE_TAG_LINE, "$1");