@vellumai/assistant 0.8.1 → 0.8.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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +642 -5
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -32,7 +32,7 @@ mock.module("../util/logger.js", () => ({
32
32
  }));
33
33
 
34
34
  // Mutable config used by the mocked loader so individual tests can override
35
- // specific fields (e.g. systemPromptPrefix) without touching other sections.
35
+ // specific fields without touching other sections.
36
36
  const mockLoadedConfig: Record<string, unknown> = {};
37
37
 
38
38
  mock.module("../config/loader.js", () => ({
@@ -78,12 +78,13 @@ const {
78
78
  } = await import("../prompts/system-prompt.js");
79
79
 
80
80
  /**
81
- * Extract just the workspace-file content (IDENTITY.md, SOUL.md,
82
- * BOOTSTRAP.md) from the full system prompt, stripping all static
83
- * instruction sections, configuration, and skills catalog.
81
+ * Extract IDENTITY.md / BOOTSTRAP.md content + the user persona from the
82
+ * dynamic block of the system prompt, stripping configuration, skills
83
+ * catalog, and connected services.
84
84
  *
85
- * After the cache-boundary refactor, workspace content lives in the
86
- * dynamic block (after SYSTEM_PROMPT_CACHE_BOUNDARY).
85
+ * SOUL.md no longer flows through this helper it renders as the
86
+ * `09-soul` workspace-backed section in the static (cached) prefix.
87
+ * Tests that assert on SOUL.md content slice the static block directly.
87
88
  */
88
89
  function basePrompt(result: string): string {
89
90
  // The workspace files are in the dynamic block after the cache boundary.
@@ -127,7 +128,9 @@ describe("buildSystemPrompt", () => {
127
128
  const p = join(TEST_DIR, name);
128
129
  if (existsSync(p)) rmSync(p, { recursive: true, force: true });
129
130
  }
130
- delete mockLoadedConfig.systemPromptPrefix;
131
+ for (const key of Object.keys(mockLoadedConfig)) {
132
+ delete mockLoadedConfig[key];
133
+ }
131
134
  });
132
135
 
133
136
  test("returns empty string when no files exist", () => {
@@ -138,7 +141,11 @@ describe("buildSystemPrompt", () => {
138
141
  test("uses SOUL.md when it exists", () => {
139
142
  writeFileSync(join(TEST_DIR, "SOUL.md"), "# My Soul\n\nBe awesome.");
140
143
  const result = buildSystemPrompt();
141
- expect(basePrompt(result)).toBe("# My Soul\n\nBe awesome.");
144
+ // SOUL.md renders as the `09-soul` workspace-backed section in the
145
+ // static (cached) prefix before SYSTEM_PROMPT_CACHE_BOUNDARY.
146
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
147
+ expect(boundaryIdx).toBeGreaterThan(-1);
148
+ expect(result.slice(0, boundaryIdx)).toContain("# My Soul\n\nBe awesome.");
142
149
  });
143
150
 
144
151
  test("uses IDENTITY.md when it exists", () => {
@@ -154,9 +161,11 @@ describe("buildSystemPrompt", () => {
154
161
  writeFileSync(join(TEST_DIR, "IDENTITY.md"), "# Identity\n\nI am Vellum.");
155
162
  writeFileSync(join(TEST_DIR, "SOUL.md"), "# Soul\n\nBe thoughtful.");
156
163
  const result = buildSystemPrompt();
157
- expect(basePrompt(result)).toBe(
158
- "# Identity\n\nI am Vellum.\n\n# Soul\n\nBe thoughtful.",
159
- );
164
+ // SOUL renders as the workspace-backed section in the static prefix;
165
+ // IDENTITY renders in the dynamic suffix.
166
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
167
+ expect(result.slice(0, boundaryIdx)).toContain("# Soul\n\nBe thoughtful.");
168
+ expect(basePrompt(result)).toBe("# Identity\n\nI am Vellum.");
160
169
  });
161
170
 
162
171
  test("ignores empty SOUL.md", () => {
@@ -174,7 +183,12 @@ describe("buildSystemPrompt", () => {
174
183
  test("trims whitespace from file content", () => {
175
184
  writeFileSync(join(TEST_DIR, "SOUL.md"), "\n Be kind \n\n");
176
185
  const result = buildSystemPrompt();
177
- expect(basePrompt(result)).toBe("Be kind");
186
+ // SOUL.md renders via the `09-soul` workspace-backed section;
187
+ // stripCommentLines + trim run inside the section renderer.
188
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
189
+ const staticBlock = result.slice(0, boundaryIdx);
190
+ expect(staticBlock).toContain("Be kind");
191
+ expect(staticBlock).not.toContain("\n Be kind \n");
178
192
  });
179
193
 
180
194
  test("does not include skills catalog in system prompt", () => {
@@ -184,7 +198,6 @@ describe("buildSystemPrompt", () => {
184
198
  join(skillsDir, "release-checklist", "SKILL.md"),
185
199
  '---\nname: "Release Checklist"\ndescription: "Deployment checks."\n---\n\nRun checks.\n',
186
200
  );
187
- writeFileSync(join(skillsDir, "SKILLS.md"), "- release-checklist\n");
188
201
 
189
202
  writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Custom identity");
190
203
  const result = buildSystemPrompt();
@@ -200,29 +213,19 @@ describe("buildSystemPrompt", () => {
200
213
  join(skillsDir, "incident-response", "SKILL.md"),
201
214
  '---\nname: "Incident Response"\ndescription: "Triage and mitigation."\n---\n\nFollow runbook.\n',
202
215
  );
203
- writeFileSync(join(skillsDir, "SKILLS.md"), "- incident-response\n");
204
216
  writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Identity content");
205
217
  writeFileSync(join(TEST_DIR, "SOUL.md"), "Soul content");
206
218
 
207
219
  const result = buildSystemPrompt();
208
- expect(result).toContain("Identity content\n\nSoul content");
220
+ // After SOUL.md became the `09-soul` workspace-backed section, it
221
+ // renders in the static prefix while IDENTITY stays in the dynamic
222
+ // suffix — the two are no longer adjacent. Verify both are present
223
+ // and the skills catalog is still suppressed.
224
+ expect(result).toContain("Identity content");
225
+ expect(result).toContain("Soul content");
209
226
  expect(result).not.toContain("## Available Skills");
210
227
  });
211
228
 
212
- test("includes external service access section", () => {
213
- const result = buildSystemPrompt();
214
- expect(result).toContain("## External Service Access");
215
- expect(result).toContain("browser automation as last resort");
216
- });
217
-
218
- test("includes inline media attachment guidance", () => {
219
- const result = buildSystemPrompt();
220
- expect(result).toContain(
221
- "Image and video attachments can render inline in chat.",
222
- );
223
- expect(result).toContain("attach it instead of only printing its path");
224
- });
225
-
226
229
  test("does not include removed sections", () => {
227
230
  const result = buildSystemPrompt();
228
231
  expect(result).not.toContain("## External Communications Identity");
@@ -259,7 +262,12 @@ describe("buildSystemPrompt", () => {
259
262
  writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Identity");
260
263
  writeFileSync(join(TEST_DIR, "SOUL.md"), "Soul");
261
264
  const result = buildSystemPrompt();
262
- expect(basePrompt(result)).toBe("Identity\n\nSoul");
265
+ // SOUL.md now renders in the static (cached) prefix via the 09-soul
266
+ // section, so it doesn't flow through basePrompt. Assert that
267
+ // IDENTITY is the only dynamic content and that SOUL is still in the
268
+ // full prompt.
269
+ expect(basePrompt(result)).toBe("Identity");
270
+ expect(result).toContain("Soul");
263
271
  });
264
272
 
265
273
  test("does not read USER.md content from disk even when the file is present", () => {
@@ -281,9 +289,12 @@ describe("buildSystemPrompt", () => {
281
289
  const result = buildSystemPrompt({
282
290
  userPersona: "# User persona\n\nName: Alice",
283
291
  });
292
+ // SOUL.md renders in the static (cached) prefix via the 09-soul section
293
+ // and is no longer part of the dynamic block sliced by basePrompt.
284
294
  expect(basePrompt(result)).toBe(
285
- "Identity\n\nSoul\n\n# User persona\n\nName: Alice",
295
+ "Identity\n\n# User persona\n\nName: Alice",
286
296
  );
297
+ expect(result).toContain("Soul");
287
298
  });
288
299
 
289
300
  describe("BOOTSTRAP.md user persona placeholder", () => {
@@ -478,7 +489,11 @@ describe("buildSystemPrompt", () => {
478
489
  "First paragraph\n\n_ Comment between paragraphs\n\nSecond paragraph",
479
490
  );
480
491
  const result = buildSystemPrompt();
481
- expect(basePrompt(result)).toBe("First paragraph\n\nSecond paragraph");
492
+ // SOUL.md renders in the static prefix via the 09-soul section, so we
493
+ // assert against the full prompt rather than basePrompt. Comment lines
494
+ // are stripped and surrounding whitespace collapsed by renderSection.
495
+ expect(result).toContain("First paragraph\n\nSecond paragraph");
496
+ expect(result).not.toContain("Comment between paragraphs");
482
497
  });
483
498
 
484
499
  test("file with only comment lines is treated as empty", () => {
@@ -487,55 +502,100 @@ describe("buildSystemPrompt", () => {
487
502
  expect(basePrompt(result)).toBe("");
488
503
  });
489
504
 
490
- describe("custom systemPromptPrefix", () => {
491
- test("omits prefix when config value is unset", () => {
492
- const result = buildSystemPrompt();
493
- // With no prefix, the prompt should start with the parallel tool calls
494
- // section (the first static section when no prefix is injected).
495
- expect(result.startsWith("<use_parallel_tool_calls>")).toBe(true);
505
+ describe("workspace system prompt sections", () => {
506
+ const SYSTEM_PROMPTS_DIR = join(TEST_DIR, "prompts", "system");
507
+ const PREFIX_FILE = join(SYSTEM_PROMPTS_DIR, "00-prefix.md");
508
+ const PARALLEL_FILE = join(SYSTEM_PROMPTS_DIR, "01-parallel-tool-calls.md");
509
+ const PREFIX_FRONTMATTER = '---\nenabled: "!excludeCustomPrefix"\n---\n';
510
+
511
+ afterEach(() => {
512
+ if (existsSync(SYSTEM_PROMPTS_DIR))
513
+ rmSync(SYSTEM_PROMPTS_DIR, { recursive: true, force: true });
496
514
  });
497
515
 
498
- test("omits prefix when config value is null", () => {
499
- mockLoadedConfig.systemPromptPrefix = null;
516
+ test("no workspace section files bundled defaults render directly", () => {
517
+ // Bundled `templates/system/` files are the source of default truth.
518
+ // With no workspace overrides in place, the renderer falls through to
519
+ // the bundled body so `01-parallel-tool-calls.md` ships its default
520
+ // guidance even though `ensurePromptFiles()` no longer seeds section
521
+ // files into the workspace.
500
522
  const result = buildSystemPrompt();
501
- expect(result.startsWith("<use_parallel_tool_calls>")).toBe(true);
523
+ expect(result).toContain("<use_parallel_tool_calls>");
524
+ expect(result).toContain("Batch independent tool calls");
502
525
  });
503
526
 
504
- test("omits prefix when config value is an empty string", () => {
505
- mockLoadedConfig.systemPromptPrefix = "";
527
+ test("workspace prefix with frontmatter renders body at the very top", () => {
528
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
529
+ writeFileSync(
530
+ PREFIX_FILE,
531
+ PREFIX_FRONTMATTER + "You are operating in demo mode.\n",
532
+ );
506
533
  const result = buildSystemPrompt();
507
- expect(result.startsWith("<use_parallel_tool_calls>")).toBe(true);
534
+ expect(result.startsWith("You are operating in demo mode.")).toBe(true);
535
+ // Prefix lives in the static (cached) block.
536
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
537
+ expect(boundaryIdx).toBeGreaterThan(-1);
538
+ const staticBlock = result.slice(0, boundaryIdx);
539
+ expect(staticBlock).toContain("You are operating in demo mode.");
508
540
  });
509
541
 
510
- test("omits prefix when config value is whitespace-only", () => {
511
- mockLoadedConfig.systemPromptPrefix = " \n\n ";
542
+ test("workspace file without frontmatter is rendered as-is (always-on)", () => {
543
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
544
+ writeFileSync(PREFIX_FILE, "Plain prefix, no frontmatter.\n");
512
545
  const result = buildSystemPrompt();
513
- expect(result.startsWith("<use_parallel_tool_calls>")).toBe(true);
546
+ expect(result.startsWith("Plain prefix, no frontmatter.")).toBe(true);
514
547
  });
515
548
 
516
- test("injects prefix at the very start of the prompt when set", () => {
517
- mockLoadedConfig.systemPromptPrefix = "You are operating in demo mode.";
549
+ test("renders nothing when workspace prefix body is empty after stripping", () => {
550
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
551
+ writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER);
518
552
  const result = buildSystemPrompt();
519
- expect(result.startsWith("You are operating in demo mode.")).toBe(true);
520
- // The standard static sections should still follow the prefix.
553
+ // Frontmatter-only override workspace wins (existsSync(workspace) is
554
+ // true) but body strips to empty prefix renders nothing. No leaked
555
+ // frontmatter at top, but the bundled `01-parallel-tool-calls.md`
556
+ // default still renders because that slot has no workspace override.
557
+ expect(result.startsWith("---")).toBe(false);
521
558
  expect(result).toContain("<use_parallel_tool_calls>");
522
- // Prefix lives in the static (cached) block, not the dynamic block.
523
- const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
524
- expect(boundaryIdx).toBeGreaterThan(-1);
525
- const staticBlock = result.slice(0, boundaryIdx);
526
- expect(staticBlock).toContain("You are operating in demo mode.");
527
559
  });
528
560
 
529
- test("trims leading/trailing whitespace from the prefix", () => {
530
- mockLoadedConfig.systemPromptPrefix =
531
- "\n\n Pretend you are a pirate. \n\n";
561
+ test("comment-only workspace prefix body strips to nothing — no comment text leaks", () => {
562
+ // Bundled `00-prefix.md` ships frontmatter-only (empty body), so
563
+ // either way the prefix slot contributes nothing — workspace
564
+ // override stripped to empty by `_` comment lines, or bundled
565
+ // fallback already empty. This test asserts only that the
566
+ // `_`-prefixed comment text does not bleed into the output.
567
+ // Bundled sections at higher slots still render (covered by
568
+ // other tests).
569
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
570
+ writeFileSync(
571
+ PREFIX_FILE,
572
+ PREFIX_FRONTMATTER +
573
+ "_ UNIQUE_COMMENT_MARKER_PURPLE_OCTOPUS\n_ UNIQUE_COMMENT_MARKER_GREEN_HELICOPTER\n",
574
+ );
575
+ const result = buildSystemPrompt();
576
+ expect(result).not.toContain("UNIQUE_COMMENT_MARKER_PURPLE_OCTOPUS");
577
+ expect(result).not.toContain("UNIQUE_COMMENT_MARKER_GREEN_HELICOPTER");
578
+ });
579
+
580
+ test("strips comment lines and trims whitespace from rendered body", () => {
581
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
582
+ writeFileSync(
583
+ PREFIX_FILE,
584
+ PREFIX_FRONTMATTER +
585
+ "_ inline note\n\n Pretend you are a pirate. \n\n",
586
+ );
532
587
  const result = buildSystemPrompt();
533
588
  expect(result.startsWith("Pretend you are a pirate.")).toBe(true);
589
+ expect(result).not.toContain("inline note");
534
590
  });
535
591
 
536
- test("multi-line prefixes are preserved verbatim after trimming", () => {
537
- mockLoadedConfig.systemPromptPrefix =
538
- "# Org Guardrails\n\n- Never discuss pricing.\n- Escalate refunds.";
592
+ test("multi-line bodies are preserved verbatim after stripping", () => {
593
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
594
+ writeFileSync(
595
+ PREFIX_FILE,
596
+ PREFIX_FRONTMATTER +
597
+ "# Org Guardrails\n\n- Never discuss pricing.\n- Escalate refunds.\n",
598
+ );
539
599
  const result = buildSystemPrompt();
540
600
  expect(
541
601
  result.startsWith(
@@ -545,12 +605,546 @@ describe("buildSystemPrompt", () => {
545
605
  });
546
606
 
547
607
  test("workspace file content still appears after prefix", () => {
548
- mockLoadedConfig.systemPromptPrefix = "Custom prefix";
608
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
609
+ writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER + "Custom prefix\n");
549
610
  writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
550
611
  const result = buildSystemPrompt();
551
612
  expect(result.startsWith("Custom prefix")).toBe(true);
552
613
  expect(basePrompt(result)).toBe("I am Vellum.");
553
614
  });
615
+
616
+ test("parallel tool calls section is sourced from workspace when present", () => {
617
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
618
+ writeFileSync(
619
+ PARALLEL_FILE,
620
+ "<use_parallel_tool_calls>\nCustomized parallel guidance.\n</use_parallel_tool_calls>\n",
621
+ );
622
+ const result = buildSystemPrompt();
623
+ expect(result).toContain("Customized parallel guidance.");
624
+ // Body of the bundled file must not leak in.
625
+ expect(result).not.toContain("Batch independent tool calls");
626
+ });
627
+
628
+ test("comment-only parallel file suppresses the section entirely", () => {
629
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
630
+ writeFileSync(PARALLEL_FILE, "_ silenced\n");
631
+ const result = buildSystemPrompt();
632
+ expect(result).not.toContain("<use_parallel_tool_calls>");
633
+ });
634
+
635
+ test("frontmatter `enabled: !excludeCustomPrefix` suppresses prefix when flag is true", () => {
636
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
637
+ writeFileSync(
638
+ PREFIX_FILE,
639
+ PREFIX_FRONTMATTER + "Should be excluded by sidechain.\n",
640
+ );
641
+ const result = buildSystemPrompt({ excludeCustomPrefix: true });
642
+ expect(result).not.toContain("Should be excluded by sidechain.");
643
+ });
644
+
645
+ test("frontmatter `enabled: !excludeCustomPrefix` renders prefix when flag is false", () => {
646
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
647
+ writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER + "Default-on prefix.\n");
648
+ const result = buildSystemPrompt({ excludeCustomPrefix: false });
649
+ expect(result.startsWith("Default-on prefix.")).toBe(true);
650
+ });
651
+
652
+ test("frontmatter `enabled: <unknown-key>` treats key as falsy → suppresses", () => {
653
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
654
+ writeFileSync(
655
+ PREFIX_FILE,
656
+ "---\nenabled: someUnknownFlag\n---\nShould not render.\n",
657
+ );
658
+ const result = buildSystemPrompt();
659
+ expect(result).not.toContain("Should not render.");
660
+ });
661
+
662
+ test("frontmatter `enabled: false` (literal boolean) suppresses the section", () => {
663
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
664
+ writeFileSync(
665
+ PREFIX_FILE,
666
+ "---\nenabled: false\n---\nShould not render.\n",
667
+ );
668
+ const result = buildSystemPrompt();
669
+ expect(result).not.toContain("Should not render.");
670
+ });
671
+
672
+ test("workspace `enabled: false` on a slot WITH a bundled file suppresses the bundled default", () => {
673
+ // Override wins regardless of body — the workspace file's `enabled: false`
674
+ // frontmatter wins over the bundled `01-parallel-tool-calls.md` default,
675
+ // so the bundled body must not leak into the rendered output. This is
676
+ // the explicit "user silenced this section" path.
677
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
678
+ writeFileSync(PARALLEL_FILE, "---\nenabled: false\n---\nIgnored body.\n");
679
+ const result = buildSystemPrompt();
680
+ expect(result).not.toContain("<use_parallel_tool_calls>");
681
+ expect(result).not.toContain("Batch independent tool calls");
682
+ expect(result).not.toContain("Ignored body.");
683
+ });
684
+
685
+ test("workspace-only sections (no bundled counterpart) render — discovery union covers both dirs", () => {
686
+ // The renderer collects section ids as the union of bundled and
687
+ // workspace filenames, so any numbered `.md` a user drops into
688
+ // `<workspace>/prompts/system/` joins the render order automatically
689
+ // even when no bundled file shares its id.
690
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
691
+ writeFileSync(
692
+ join(SYSTEM_PROMPTS_DIR, "99-org-policy.md"),
693
+ "# Org policy\n\nUnique workspace-only marker A1B2C3.\n",
694
+ );
695
+ const result = buildSystemPrompt();
696
+ expect(result).toContain("Unique workspace-only marker A1B2C3.");
697
+ // Sort order is filename-driven; the new section sorts after `01-`,
698
+ // so it must appear after the parallel-tool-calls block when both
699
+ // are present.
700
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
701
+ writeFileSync(
702
+ PARALLEL_FILE,
703
+ "<use_parallel_tool_calls>\nbatched.\n</use_parallel_tool_calls>\n",
704
+ );
705
+ const ordered = buildSystemPrompt();
706
+ const parallelIdx = ordered.indexOf("batched.");
707
+ const orgIdx = ordered.indexOf("Unique workspace-only marker A1B2C3.");
708
+ expect(parallelIdx).toBeGreaterThan(-1);
709
+ expect(orgIdx).toBeGreaterThan(parallelIdx);
710
+ });
711
+
712
+ describe("containerized section (slot 02)", () => {
713
+ const CONTAINERIZED_FILE = join(
714
+ SYSTEM_PROMPTS_DIR,
715
+ "02-containerized.md",
716
+ );
717
+
718
+ // The runtime gate is `isContainerized` on the render context, sourced
719
+ // from `getIsContainerized()` which reads `process.env.IS_CONTAINERIZED`.
720
+ // Tests toggle the env var directly and restore it in `finally`.
721
+ let priorIsContainerized: string | undefined;
722
+
723
+ beforeEach(() => {
724
+ priorIsContainerized = process.env.IS_CONTAINERIZED;
725
+ });
726
+
727
+ afterEach(() => {
728
+ if (priorIsContainerized === undefined)
729
+ delete process.env.IS_CONTAINERIZED;
730
+ else process.env.IS_CONTAINERIZED = priorIsContainerized;
731
+ });
732
+
733
+ test("renders the section when IS_CONTAINERIZED=true with {{workspaceDir}} interpolated", () => {
734
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
735
+ writeFileSync(
736
+ CONTAINERIZED_FILE,
737
+ "---\nenabled: isContainerized\n---\n" +
738
+ "Container mounted at `{{workspaceDir}}`. Persist accordingly.\n",
739
+ );
740
+ process.env.IS_CONTAINERIZED = "true";
741
+ const result = buildSystemPrompt();
742
+ expect(result).toContain(
743
+ `Container mounted at \`${TEST_DIR}\`. Persist accordingly.`,
744
+ );
745
+ // The literal `{{workspaceDir}}` must be substituted, not leaked.
746
+ expect(result).not.toContain("{{workspaceDir}}");
747
+ });
748
+
749
+ test("omits the section when IS_CONTAINERIZED is unset", () => {
750
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
751
+ writeFileSync(
752
+ CONTAINERIZED_FILE,
753
+ "---\nenabled: isContainerized\n---\nContainer guidance body.\n",
754
+ );
755
+ delete process.env.IS_CONTAINERIZED;
756
+ const result = buildSystemPrompt();
757
+ expect(result).not.toContain("Container guidance body.");
758
+ });
759
+
760
+ test("omits the section when IS_CONTAINERIZED=false (string)", () => {
761
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
762
+ writeFileSync(
763
+ CONTAINERIZED_FILE,
764
+ "---\nenabled: isContainerized\n---\nContainer guidance body.\n",
765
+ );
766
+ process.env.IS_CONTAINERIZED = "false";
767
+ const result = buildSystemPrompt();
768
+ expect(result).not.toContain("Container guidance body.");
769
+ });
770
+ });
771
+
772
+ describe("cli-reference section (slot 03)", () => {
773
+ const CLI_REFERENCE_FILE = join(
774
+ SYSTEM_PROMPTS_DIR,
775
+ "03-cli-reference.md",
776
+ );
777
+
778
+ test("workspace cli-reference file is rendered into the static block", () => {
779
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
780
+ writeFileSync(
781
+ CLI_REFERENCE_FILE,
782
+ "## Assistant CLI\n\nRun `assistant --help` to discover commands.\n",
783
+ );
784
+ const result = buildSystemPrompt();
785
+ expect(result).toContain("## Assistant CLI");
786
+ expect(result).toContain(
787
+ "Run `assistant --help` to discover commands.",
788
+ );
789
+ // Section lives in the static (cached) block.
790
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
791
+ expect(boundaryIdx).toBeGreaterThan(-1);
792
+ const staticBlock = result.slice(0, boundaryIdx);
793
+ expect(staticBlock).toContain("## Assistant CLI");
794
+ });
795
+
796
+ test("bundled cli-reference default renders when no workspace override", () => {
797
+ // Bundled `03-cli-reference.md` is the source of default truth. No
798
+ // workspace override → renderer falls through to bundled body, so
799
+ // `## Assistant CLI` lands in the static block automatically.
800
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
801
+ const result = buildSystemPrompt();
802
+ expect(result).toContain("## Assistant CLI");
803
+ expect(result).toContain("`assistant` CLI is available");
804
+ });
805
+ });
806
+
807
+ describe("access-preference section (slot 05)", () => {
808
+ const ACCESS_FILE = join(SYSTEM_PROMPTS_DIR, "05-access-preference.md");
809
+ // Mirrors the bundled `templates/system/05-access-preference.md` — both
810
+ // variants live in the markdown body and the renderer picks one via
811
+ // mustache-style `{{#hasNoClient}}` / `{{^hasNoClient}}` conditionals.
812
+ const TEMPLATE_BODY = [
813
+ "## External Service Access",
814
+ "",
815
+ "{{#hasNoClient}}",
816
+ "Priority: (1) sandbox `bash` — install tools yourself; (2) browser automation as last resort (no API, visual interaction, or OAuth consent).",
817
+ "{{/hasNoClient}}",
818
+ "{{^hasNoClient}}",
819
+ "Priority: (1) sandbox `bash` - install tools yourself, only fall back to host when you need local files/auth; (2) `host_bash` with CLIs (gh, aws, etc.) using --json flags; (3) browser automation as last resort (no API, visual interaction, or OAuth consent).",
820
+ "{{/hasNoClient}}",
821
+ "",
822
+ ].join("\n");
823
+
824
+ test("with-client (default) renders the three-tier priority list", () => {
825
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
826
+ writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
827
+ const result = buildSystemPrompt();
828
+ expect(result).toContain("## External Service Access");
829
+ expect(result).toContain("`host_bash` with CLIs");
830
+ expect(result).toContain("browser automation as last resort");
831
+ // The no-client body (em-dash separator after sandbox `bash`) must
832
+ // not leak when the with-client variant is active.
833
+ expect(result).not.toContain("install tools yourself; (2) browser");
834
+ // Section lives in the static (cached) block.
835
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
836
+ expect(boundaryIdx).toBeGreaterThan(-1);
837
+ const staticBlock = result.slice(0, boundaryIdx);
838
+ expect(staticBlock).toContain("## External Service Access");
839
+ });
840
+
841
+ test("hasNoClient=true renders the two-tier (no host_bash) priority list", () => {
842
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
843
+ writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
844
+ const result = buildSystemPrompt({ hasNoClient: true });
845
+ expect(result).toContain("## External Service Access");
846
+ expect(result).toContain("browser automation as last resort");
847
+ // The host_bash tier must be absent in the no-client variant.
848
+ expect(result).not.toContain("`host_bash` with CLIs");
849
+ // The no-client body uses an em-dash + semicolon separator after
850
+ // sandbox `bash`; the with-client body uses a comma — guard against
851
+ // the wrong variant leaking through.
852
+ expect(result).toContain("install tools yourself; (2) browser");
853
+ expect(result).not.toContain(
854
+ "only fall back to host when you need local files/auth",
855
+ );
856
+ });
857
+
858
+ test("standalone tag lines do not bleed extra blank lines into output", () => {
859
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
860
+ writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
861
+ const result = buildSystemPrompt();
862
+ // The heading and the active variant body should sit on adjacent
863
+ // lines separated by exactly one blank line — no triple-newline
864
+ // artifacts from the section markers.
865
+ expect(result).toMatch(
866
+ /## External Service Access\n\nPriority: \(1\) sandbox `bash` -/,
867
+ );
868
+ });
869
+
870
+ test("bundled access-preference default renders when no workspace override", () => {
871
+ // Bundled `05-access-preference.md` carries both with-client and
872
+ // no-client variants inline behind mustache section conditionals,
873
+ // so the default body renders without any workspace file present.
874
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
875
+ const result = buildSystemPrompt();
876
+ expect(result).toContain("## External Service Access");
877
+ expect(result).toContain("`host_bash` with CLIs");
878
+ });
879
+
880
+ test("renders after the attachment section to preserve original order", () => {
881
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
882
+ writeFileSync(
883
+ join(SYSTEM_PROMPTS_DIR, "04-attachment.md"),
884
+ "## Sending Files to the User\n\nbody.\n",
885
+ );
886
+ writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
887
+ const result = buildSystemPrompt();
888
+ const attachmentIdx = result.indexOf("## Sending Files to the User");
889
+ const accessIdx = result.indexOf("## External Service Access");
890
+ expect(attachmentIdx).toBeGreaterThan(-1);
891
+ expect(accessIdx).toBeGreaterThan(-1);
892
+ expect(attachmentIdx).toBeLessThan(accessIdx);
893
+ });
894
+ });
895
+
896
+ describe("mustache section interpolation", () => {
897
+ // Reuse slot 00 (prefix) — its default-on `enabled` predicate is
898
+ // already covered by other tests; here we only care about body
899
+ // interpolation shape.
900
+ const SECTION_FILE = join(SYSTEM_PROMPTS_DIR, "00-prefix.md");
901
+ const FRONTMATTER = '---\nenabled: "!excludeCustomPrefix"\n---\n';
902
+
903
+ test("{{#flag}}body{{/flag}} renders body when ctx[flag] is truthy", () => {
904
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
905
+ writeFileSync(
906
+ SECTION_FILE,
907
+ FRONTMATTER + "before {{#hasNoClient}}YES{{/hasNoClient}} after\n",
908
+ );
909
+ const result = buildSystemPrompt({ hasNoClient: true });
910
+ expect(result).toContain("before YES after");
911
+ });
912
+
913
+ test("{{#flag}}body{{/flag}} omits body when ctx[flag] is falsy", () => {
914
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
915
+ writeFileSync(
916
+ SECTION_FILE,
917
+ FRONTMATTER + "before {{#hasNoClient}}YES{{/hasNoClient}} after\n",
918
+ );
919
+ const result = buildSystemPrompt({ hasNoClient: false });
920
+ expect(result).toContain("before after");
921
+ expect(result).not.toContain("YES");
922
+ });
923
+
924
+ test("{{^flag}}body{{/flag}} renders body when ctx[flag] is falsy", () => {
925
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
926
+ writeFileSync(
927
+ SECTION_FILE,
928
+ FRONTMATTER + "before {{^hasNoClient}}NO{{/hasNoClient}} after\n",
929
+ );
930
+ const result = buildSystemPrompt({ hasNoClient: false });
931
+ expect(result).toContain("before NO after");
932
+ });
933
+
934
+ test("{{^flag}}body{{/flag}} omits body when ctx[flag] is truthy", () => {
935
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
936
+ writeFileSync(
937
+ SECTION_FILE,
938
+ FRONTMATTER + "before {{^hasNoClient}}NO{{/hasNoClient}} after\n",
939
+ );
940
+ const result = buildSystemPrompt({ hasNoClient: true });
941
+ expect(result).toContain("before after");
942
+ expect(result).not.toContain("NO");
943
+ });
944
+
945
+ test("paired {{#flag}} + {{^flag}} acts as if/else", () => {
946
+ // Use long unique markers — single letters collide with substrings
947
+ // in the rest of the system prompt (e.g. "B" lives inside
948
+ // SYSTEM_PROMPT_CACHE_BOUNDARY, "A" inside "API keys").
949
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
950
+ writeFileSync(
951
+ SECTION_FILE,
952
+ FRONTMATTER +
953
+ "{{#hasNoClient}}NO_CLIENT_BRANCH_MARKER{{/hasNoClient}}{{^hasNoClient}}WITH_CLIENT_BRANCH_MARKER{{/hasNoClient}}\n",
954
+ );
955
+ const onTrue = buildSystemPrompt({ hasNoClient: true });
956
+ expect(onTrue).toContain("NO_CLIENT_BRANCH_MARKER");
957
+ expect(onTrue).not.toContain("WITH_CLIENT_BRANCH_MARKER");
958
+ const onFalse = buildSystemPrompt({ hasNoClient: false });
959
+ expect(onFalse).toContain("WITH_CLIENT_BRANCH_MARKER");
960
+ expect(onFalse).not.toContain("NO_CLIENT_BRANCH_MARKER");
961
+ });
962
+
963
+ test("section body may contain a {{variable}} substitution", () => {
964
+ // Gate on `hasNoClient` (passed explicitly, so we don't depend on
965
+ // ambient test-env state for `isContainerized`). The section body
966
+ // includes a `{{workspaceDir}}` interpolation that should resolve
967
+ // to the test workspace path.
968
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
969
+ writeFileSync(
970
+ SECTION_FILE,
971
+ FRONTMATTER +
972
+ "{{#hasNoClient}}cwd={{workspaceDir}}{{/hasNoClient}}\n",
973
+ );
974
+ const result = buildSystemPrompt({ hasNoClient: true });
975
+ expect(result).toMatch(/cwd=\S+/);
976
+ expect(result).not.toContain("{{workspaceDir}}");
977
+ });
978
+
979
+ test("section keys missing from ctx gate the body off (treated as falsy)", () => {
980
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
981
+ writeFileSync(
982
+ SECTION_FILE,
983
+ FRONTMATTER + "{{#noSuchFlag}}hidden{{/noSuchFlag}}\n",
984
+ );
985
+ const result = buildSystemPrompt();
986
+ expect(result).not.toContain("{{#noSuchFlag}}");
987
+ expect(result).not.toContain("hidden");
988
+ });
989
+
990
+ test("inverted section keys missing from ctx render the body (undefined is falsy)", () => {
991
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
992
+ writeFileSync(
993
+ SECTION_FILE,
994
+ FRONTMATTER + "{{^noSuchFlag}}shown{{/noSuchFlag}}\n",
995
+ );
996
+ const result = buildSystemPrompt();
997
+ expect(result).toContain("shown");
998
+ });
999
+ });
1000
+
1001
+ describe("attachment section (slot 04)", () => {
1002
+ const ATTACHMENT_FILE = join(SYSTEM_PROMPTS_DIR, "04-attachment.md");
1003
+
1004
+ test("workspace attachment file is rendered into the static block", () => {
1005
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1006
+ writeFileSync(
1007
+ ATTACHMENT_FILE,
1008
+ "## Sending Files to the User\n\nUse the `<vellum-attachment />` tag.\n",
1009
+ );
1010
+ const result = buildSystemPrompt();
1011
+ expect(result).toContain("## Sending Files to the User");
1012
+ expect(result).toContain("Use the `<vellum-attachment />` tag.");
1013
+ // Section lives in the static (cached) block.
1014
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
1015
+ expect(boundaryIdx).toBeGreaterThan(-1);
1016
+ const staticBlock = result.slice(0, boundaryIdx);
1017
+ expect(staticBlock).toContain("## Sending Files to the User");
1018
+ });
1019
+
1020
+ test("renders after the cli-reference section to preserve original order", () => {
1021
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1022
+ writeFileSync(
1023
+ join(SYSTEM_PROMPTS_DIR, "03-cli-reference.md"),
1024
+ "## Assistant CLI\n\nUse `assistant --help`.\n",
1025
+ );
1026
+ writeFileSync(
1027
+ ATTACHMENT_FILE,
1028
+ "## Sending Files to the User\n\nbody.\n",
1029
+ );
1030
+ const result = buildSystemPrompt();
1031
+ const cliIdx = result.indexOf("## Assistant CLI");
1032
+ const attachmentIdx = result.indexOf("## Sending Files to the User");
1033
+ expect(cliIdx).toBeGreaterThan(-1);
1034
+ expect(attachmentIdx).toBeGreaterThan(-1);
1035
+ expect(cliIdx).toBeLessThan(attachmentIdx);
1036
+ });
1037
+
1038
+ test("bundled attachment default renders when no workspace override", () => {
1039
+ // Bundled `04-attachment.md` is the source of default truth; no
1040
+ // workspace override → renderer falls through to bundled body.
1041
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1042
+ const result = buildSystemPrompt();
1043
+ expect(result).toContain("## Sending Files to the User");
1044
+ expect(result).toContain("<vellum-attachment");
1045
+ });
1046
+ });
1047
+
1048
+ describe("credential-security section (slot 06)", () => {
1049
+ const CREDENTIAL_FILE = join(
1050
+ SYSTEM_PROMPTS_DIR,
1051
+ "06-credential-security.md",
1052
+ );
1053
+
1054
+ test("workspace credential-security file is rendered into the static block", () => {
1055
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1056
+ writeFileSync(
1057
+ CREDENTIAL_FILE,
1058
+ "## Credential Security\n\nWorkspace override marker BRAVO_TANGO_7.\n",
1059
+ );
1060
+ const result = buildSystemPrompt();
1061
+ expect(result).toContain("## Credential Security");
1062
+ expect(result).toContain("Workspace override marker BRAVO_TANGO_7.");
1063
+ // Section lives in the static (cached) block.
1064
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
1065
+ expect(boundaryIdx).toBeGreaterThan(-1);
1066
+ const staticBlock = result.slice(0, boundaryIdx);
1067
+ expect(staticBlock).toContain("## Credential Security");
1068
+ });
1069
+
1070
+ test("bundled credential-security default renders when no workspace override", () => {
1071
+ // Bundled `06-credential-security` registry entry is the source of
1072
+ // default truth; no workspace override → renderer falls through to
1073
+ // bundled body.
1074
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1075
+ const result = buildSystemPrompt();
1076
+ expect(result).toContain("## Credential Security");
1077
+ expect(result).toContain("Never ask users to share secrets");
1078
+ expect(result).toContain("`credential_store` tool");
1079
+ });
1080
+
1081
+ test("renders after the access-preference section to preserve original order", () => {
1082
+ // Static-block order from the pre-registry inline build was
1083
+ // access-preference → credential-security. The numeric prefix on
1084
+ // the registry id (`06-` > `05-`) preserves that order.
1085
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1086
+ const result = buildSystemPrompt();
1087
+ const accessIdx = result.indexOf("## External Service Access");
1088
+ const credentialIdx = result.indexOf("## Credential Security");
1089
+ expect(accessIdx).toBeGreaterThan(-1);
1090
+ expect(credentialIdx).toBeGreaterThan(-1);
1091
+ expect(accessIdx).toBeLessThan(credentialIdx);
1092
+ });
1093
+ });
1094
+
1095
+ describe("external-content section (slot 07)", () => {
1096
+ const EXTERNAL_FILE = join(SYSTEM_PROMPTS_DIR, "07-external-content.md");
1097
+
1098
+ test("workspace external-content file is rendered into the static block", () => {
1099
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1100
+ writeFileSync(
1101
+ EXTERNAL_FILE,
1102
+ "## External Content\n\nWorkspace override marker NEBULA_9X.\n",
1103
+ );
1104
+ const result = buildSystemPrompt();
1105
+ expect(result).toContain("## External Content");
1106
+ expect(result).toContain("Workspace override marker NEBULA_9X.");
1107
+ // Section lives in the static (cached) block.
1108
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
1109
+ expect(boundaryIdx).toBeGreaterThan(-1);
1110
+ const staticBlock = result.slice(0, boundaryIdx);
1111
+ expect(staticBlock).toContain("## External Content");
1112
+ });
1113
+
1114
+ test("bundled external-content default renders when no workspace override", () => {
1115
+ // Bundled `07-external-content` registry entry is the source of
1116
+ // default truth; no workspace override → renderer falls through to
1117
+ // bundled body.
1118
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1119
+ const result = buildSystemPrompt();
1120
+ expect(result).toContain("## External Content");
1121
+ expect(result).toContain("third-party data");
1122
+ expect(result).toContain("`<external_content>`");
1123
+ });
1124
+
1125
+ test("renders after the credential-security section to preserve original order", () => {
1126
+ // Static-block order from the pre-registry inline build was
1127
+ // credential-security → external-content. The numeric prefix on
1128
+ // the registry id (`07-` > `06-`) preserves that order.
1129
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1130
+ const result = buildSystemPrompt();
1131
+ const credentialIdx = result.indexOf("## Credential Security");
1132
+ const externalIdx = result.indexOf("## External Content");
1133
+ expect(credentialIdx).toBeGreaterThan(-1);
1134
+ expect(externalIdx).toBeGreaterThan(-1);
1135
+ expect(credentialIdx).toBeLessThan(externalIdx);
1136
+ });
1137
+ });
1138
+
1139
+ test("unresolved {{variable}} is left as a literal in the body", () => {
1140
+ mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
1141
+ writeFileSync(
1142
+ PREFIX_FILE,
1143
+ PREFIX_FRONTMATTER + "Has {{somethingMissing}} in body.\n",
1144
+ );
1145
+ const result = buildSystemPrompt();
1146
+ expect(result).toContain("Has {{somethingMissing}} in body.");
1147
+ });
554
1148
  });
555
1149
  });
556
1150
 
@@ -748,6 +1342,19 @@ describe("ensurePromptFiles", () => {
748
1342
  expect(content.length).toBeGreaterThan(0);
749
1343
  });
750
1344
 
1345
+ test("does not seed bundled system prompt sections into the workspace", () => {
1346
+ // Bundled `templates/system/*.md` files are the source of default truth.
1347
+ // The renderer reads them directly; the workspace dir is an optional
1348
+ // override layer. On first run we must not pre-populate the workspace
1349
+ // with bundled section copies — leaving the workspace empty keeps the
1350
+ // override layer purely opt-in and lets bundled defaults flow through
1351
+ // automatically as the daemon ships updates.
1352
+ ensurePromptFiles();
1353
+
1354
+ const sectionsDir = join(TEST_DIR, "prompts", "system");
1355
+ expect(existsSync(sectionsDir)).toBe(false);
1356
+ });
1357
+
751
1358
  test("does not recreate BOOTSTRAP.md when other prompt files already exist", () => {
752
1359
  // Simulate a workspace where onboarding completed: core files exist,
753
1360
  // BOOTSTRAP.md was deleted by the user.