@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -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 +325 -3
  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-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -1,5 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import {
4
+ conversationMessagesSyncTag,
5
+ SYNC_TAGS,
6
+ } from "../daemon/message-types/sync.js";
7
+
3
8
  // ── Mock state ──────────────────────────────────────────────────────────
4
9
 
5
10
  // Provider mock
@@ -691,7 +696,7 @@ describe("runProactiveArtifactJob", () => {
691
696
  });
692
697
 
693
698
  describe("injectAuxAssistantMessage", () => {
694
- test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list_invalidated", async () => {
699
+ test("idle conversation: persists with skipIndexing, pushes to getMessages(), broadcasts delta + complete(aux) + list sync", async () => {
695
700
  const messages: unknown[] = [];
696
701
  mockConversations.set("conv-inject-1", {
697
702
  processing: false,
@@ -714,7 +719,7 @@ describe("injectAuxAssistantMessage", () => {
714
719
  // Pushed to in-memory messages
715
720
  expect(messages).toHaveLength(1);
716
721
 
717
- // Broadcasts: delta, complete(aux), list_invalidated
722
+ // Broadcasts: delta, complete(aux), list invalidation + sync tag
718
723
  const deltaMsg = broadcastCalls.find(
719
724
  (c) => c.type === "assistant_text_delta",
720
725
  );
@@ -734,6 +739,15 @@ describe("injectAuxAssistantMessage", () => {
734
739
  );
735
740
  expect(listMsg).toBeDefined();
736
741
  expect(listMsg!.reason).toBe("reordered");
742
+
743
+ const syncMsg = broadcastCalls.find((c) => c.type === "sync_changed");
744
+ expect(syncMsg).toEqual({
745
+ type: "sync_changed",
746
+ tags: [
747
+ SYNC_TAGS.conversationsList,
748
+ conversationMessagesSyncTag("conv-inject-1"),
749
+ ],
750
+ });
737
751
  });
738
752
 
739
753
  test("processing → idle: waits for processing to become false before persisting", async () => {
@@ -824,13 +838,22 @@ describe("injectAuxAssistantMessage", () => {
824
838
  broadcastCalls.filter((c) => c.type === "message_complete"),
825
839
  ).toHaveLength(0);
826
840
 
827
- // But list_invalidated IS sent (always sent regardless of processing state)
841
+ // But list invalidation + sync tag ARE sent regardless of processing state.
828
842
  expect(
829
843
  broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
830
844
  ).toHaveLength(1);
845
+ expect(broadcastCalls.filter((c) => c.type === "sync_changed")).toEqual([
846
+ {
847
+ type: "sync_changed",
848
+ tags: [
849
+ SYNC_TAGS.conversationsList,
850
+ conversationMessagesSyncTag("conv-inject-3"),
851
+ ],
852
+ },
853
+ ]);
831
854
  });
832
855
 
833
- test("inactive/unloaded conversation: persists + list_invalidated only", async () => {
856
+ test("inactive/unloaded conversation: persists + list sync only", async () => {
834
857
  // No conversation in the store
835
858
  await injectAuxAssistantMessage({
836
859
  conversationId: "conv-inject-4",
@@ -850,10 +873,19 @@ describe("injectAuxAssistantMessage", () => {
850
873
  broadcastCalls.filter((c) => c.type === "message_complete"),
851
874
  ).toHaveLength(0);
852
875
 
853
- // But list_invalidated IS sent
876
+ // But list invalidation + sync tag ARE sent
854
877
  expect(
855
878
  broadcastCalls.filter((c) => c.type === "conversation_list_invalidated"),
856
879
  ).toHaveLength(1);
880
+ expect(broadcastCalls.filter((c) => c.type === "sync_changed")).toEqual([
881
+ {
882
+ type: "sync_changed",
883
+ tags: [
884
+ SYNC_TAGS.conversationsList,
885
+ conversationMessagesSyncTag("conv-inject-4"),
886
+ ],
887
+ },
888
+ ]);
857
889
  });
858
890
  });
859
891
 
@@ -103,3 +103,15 @@ describe("buildSystemPrompt — Background Conversation gating", () => {
103
103
  expect(dynamicBlock).not.toContain("## Background Conversation");
104
104
  });
105
105
  });
106
+
107
+ describe("buildSystemPrompt — tool routing guidance", () => {
108
+ beforeEach(() => {
109
+ mkdirSync(TEST_DIR, { recursive: true });
110
+ });
111
+
112
+ test("does not include ask_question routing guidance", () => {
113
+ const result = buildSystemPrompt({});
114
+ expect(result).not.toContain("## Clarifying questions");
115
+ expect(result).not.toContain("ask_question");
116
+ });
117
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Tests for the task_progress hint in the 01-parallel-tool-calls workspace
3
+ * system prompt section.
4
+ *
5
+ * Verifies that the task_progress guidance renders unconditionally in the
6
+ * system prompt — no `enabled` frontmatter gating, no options dependency.
7
+ */
8
+
9
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ const noopLogger: Record<string, unknown> = new Proxy(
12
+ {} as Record<string, unknown>,
13
+ {
14
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
15
+ },
16
+ );
17
+
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const realLogger = require("../../util/logger.js");
20
+ mock.module("../../util/logger.js", () => ({
21
+ ...realLogger,
22
+ getLogger: () => noopLogger,
23
+ getCliLogger: () => noopLogger,
24
+ truncateForLog: (v: string) => v,
25
+ initLogger: () => {},
26
+ pruneOldLogFiles: () => 0,
27
+ }));
28
+
29
+ const mockLoadedConfig: Record<string, unknown> = {};
30
+
31
+ mock.module("../../config/loader.js", () => ({
32
+ getConfig: () => ({
33
+ ui: {},
34
+ services: {
35
+ inference: {
36
+ mode: "your-own",
37
+ provider: "anthropic",
38
+ model: "claude-opus-4-6",
39
+ },
40
+ "image-generation": {
41
+ mode: "your-own",
42
+ provider: "gemini",
43
+ model: "gemini-3.1-flash-image-preview",
44
+ },
45
+ "web-search": { mode: "your-own", provider: "inference-provider-native" },
46
+ },
47
+ }),
48
+ loadConfig: () => mockLoadedConfig,
49
+ loadRawConfig: () => ({}),
50
+ saveConfig: () => {},
51
+ saveRawConfig: () => {},
52
+ invalidateConfigCache: () => {},
53
+ getNestedValue: () => undefined,
54
+ setNestedValue: () => {},
55
+ }));
56
+
57
+ const { buildSystemPrompt, ensurePromptFiles, SYSTEM_PROMPT_CACHE_BOUNDARY } =
58
+ await import("../system-prompt.js");
59
+
60
+ describe("task_progress hint in parallel-tool-calls section", () => {
61
+ beforeEach(() => {
62
+ ensurePromptFiles();
63
+ });
64
+
65
+ test("buildSystemPrompt() includes task_progress guidance", () => {
66
+ const result = buildSystemPrompt();
67
+ expect(result).toContain("task_progress");
68
+ expect(result).toContain("No exceptions");
69
+ });
70
+
71
+ test("renders unconditionally — no options required", () => {
72
+ const result = buildSystemPrompt(undefined);
73
+ expect(result).toContain("task_progress");
74
+ });
75
+
76
+ test("renders regardless of options passed", () => {
77
+ const withBackground = buildSystemPrompt({
78
+ isBackgroundConversation: true,
79
+ });
80
+ const withoutBackground = buildSystemPrompt({
81
+ isBackgroundConversation: false,
82
+ });
83
+ const withExcludePrefix = buildSystemPrompt({
84
+ excludeCustomPrefix: true,
85
+ });
86
+
87
+ expect(withBackground).toContain("task_progress");
88
+ expect(withoutBackground).toContain("task_progress");
89
+ expect(withExcludePrefix).toContain("task_progress");
90
+ });
91
+
92
+ test("hint lives in the static (cached) block before SYSTEM_PROMPT_CACHE_BOUNDARY", () => {
93
+ const result = buildSystemPrompt();
94
+ const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
95
+ expect(boundaryIdx).toBeGreaterThan(-1);
96
+ const staticBlock = result.slice(0, boundaryIdx);
97
+ expect(staticBlock).toContain("task_progress");
98
+ });
99
+ });
@@ -62,6 +62,29 @@ export interface NormalizedOnboarding {
62
62
  dailyTools: string[];
63
63
  tone?: string;
64
64
  assistantName?: string;
65
+ googleConnected?: boolean;
66
+ googleServices?: string[];
67
+ }
68
+
69
+ const SCOPE_SERVICE_MAP: Record<string, string> = {
70
+ "gmail.readonly": "Gmail",
71
+ "gmail.modify": "Gmail",
72
+ "gmail.send": "Gmail",
73
+ "gmail.settings.basic": "Gmail",
74
+ "calendar.readonly": "Calendar",
75
+ "calendar.events": "Calendar",
76
+ drive: "Drive",
77
+ };
78
+
79
+ export function deriveGoogleServices(scopes?: string[]): string[] {
80
+ if (!scopes?.length) return ["Gmail", "Calendar", "Drive"];
81
+ const services = new Set<string>();
82
+ for (const scope of scopes) {
83
+ const suffix = scope.replace("https://www.googleapis.com/auth/", "");
84
+ const service = SCOPE_SERVICE_MAP[suffix];
85
+ if (service) services.add(service);
86
+ }
87
+ return services.size > 0 ? [...services] : ["Gmail", "Calendar", "Drive"];
65
88
  }
66
89
 
67
90
  /**
@@ -76,5 +99,9 @@ export function normalizeOnboardingContext(
76
99
  dailyTools: normalizeTools(ctx.tools),
77
100
  tone: ctx.tone,
78
101
  assistantName: ctx.assistantName,
102
+ googleConnected: ctx.googleConnected,
103
+ googleServices: ctx.googleConnected
104
+ ? deriveGoogleServices(ctx.googleScopes)
105
+ : undefined,
79
106
  };
80
107
  }
@@ -0,0 +1,302 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { parseFrontmatterFields } from "../skills/frontmatter.js";
5
+ import { getLogger } from "../util/logger.js";
6
+ import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
7
+ import { stripCommentLines } from "../util/strip-comment-lines.js";
8
+ import {
9
+ BUNDLED_SYSTEM_SECTIONS,
10
+ type BundledSection,
11
+ } from "./templates/system-sections.js";
12
+
13
+ const log = getLogger("system-prompt-sections");
14
+
15
+ /**
16
+ * Render context passed by the caller of `renderWorkspaceSections`. Sections
17
+ * declare their `enabled` predicate as a context key (or `!key`), and the
18
+ * predicate is evaluated against fields on this object.
19
+ *
20
+ * Intentionally an open record — the registry never references specific keys.
21
+ * Callers (currently `buildSystemPrompt`) hand in the same options object
22
+ * they received, so any field on `BuildSystemPromptOptions` can be
23
+ * referenced by name in a section's `enabled` predicate or `{{variable}}`
24
+ * interpolation.
25
+ */
26
+ export type SectionRenderContext = Record<string, unknown>;
27
+
28
+ /**
29
+ * Workspace override location for user-authored system prompt sections.
30
+ * Layout: `<workspace>/prompts/system/<NN-name>.md`.
31
+ *
32
+ * The bundled section registry (`templates/system-sections.ts`) is the
33
+ * source of default truth; this directory is an optional override layer.
34
+ * Drop a file with the same id as a bundled section to replace its body,
35
+ * or drop a file with a brand-new `<NN-name>` to add a workspace-only
36
+ * section. Either path is opt-in — the directory may not exist on a
37
+ * fresh install, and the renderer will simply use bundled defaults.
38
+ */
39
+ export function getWorkspaceSystemPromptDir(): string {
40
+ return join(getWorkspaceDir(), "prompts", "system");
41
+ }
42
+
43
+ /**
44
+ * Render every section in id-sort order, returning the trimmed body of
45
+ * each enabled section. Discovery walks the bundled registry plus any
46
+ * `.md` files in the workspace override dir, and takes the union of ids.
47
+ *
48
+ * Resolution per id:
49
+ * - workspace `.md` file present → use workspace body (override)
50
+ * - workspace file absent → use bundled registry entry (default)
51
+ *
52
+ * Bundled is the source of default truth. Workspace acts as an override
53
+ * layer — a user can replace a bundled section by writing the same id in
54
+ * their workspace, or add a brand-new section by writing an id that
55
+ * doesn't appear in the bundled registry. Workspace-only ids skip the
56
+ * bundled lookup entirely.
57
+ *
58
+ * Render contract per section:
59
+ * 1. resolve `{ enabled, body }` (workspace .md wins over bundled TS)
60
+ * 2. evaluate `enabled` against `ctx`; falsy → skip
61
+ * 3. apply mustache section / inverted-section / variable interpolation
62
+ * 4. strip lines starting with `_` (legacy inline-comment convention)
63
+ * 5. trim; emit if non-empty, otherwise skip
64
+ *
65
+ * The empty-body case is intentional — a user can silence a bundled
66
+ * section by overriding it with a file that strips down to nothing
67
+ * (frontmatter `enabled: false`, or a frontmatter-only file, or a body
68
+ * of only `_`-comments). This is the supported "disable a bundled
69
+ * default" path.
70
+ *
71
+ * The numeric prefix on each id is load-bearing for sort order; pick a
72
+ * number that places the section where it should appear in the final
73
+ * prompt.
74
+ */
75
+ export function renderWorkspaceSections(ctx: SectionRenderContext): string[] {
76
+ const workspaceDir = getWorkspaceSystemPromptDir();
77
+ const ids = collectSectionIds(workspaceDir);
78
+
79
+ const out: string[] = [];
80
+ for (const id of ids) {
81
+ const rendered = renderSection(id, ctx, workspaceDir);
82
+ if (rendered) out.push(rendered);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function collectSectionIds(workspaceDir: string): string[] {
88
+ const ids = new Set<string>();
89
+ for (const section of BUNDLED_SYSTEM_SECTIONS) ids.add(section.id);
90
+ if (existsSync(workspaceDir)) {
91
+ try {
92
+ for (const name of readdirSync(workspaceDir)) {
93
+ if (name.endsWith(".md")) ids.add(name.slice(0, -".md".length));
94
+ }
95
+ } catch (err) {
96
+ log.warn({ err, workspaceDir }, "Failed to list workspace system prompt dir");
97
+ }
98
+ }
99
+ return [...ids].sort();
100
+ }
101
+
102
+ interface ResolvedSection {
103
+ enabled: string | boolean | undefined;
104
+ body: string;
105
+ }
106
+
107
+ function resolveSection(
108
+ id: string,
109
+ workspaceDir: string,
110
+ ): ResolvedSection | null {
111
+ const workspacePath = join(workspaceDir, `${id}.md`);
112
+ if (existsSync(workspacePath)) {
113
+ let raw: string;
114
+ try {
115
+ raw = readFileSync(workspacePath, "utf-8");
116
+ } catch (err) {
117
+ log.warn({ err, workspacePath }, "Failed to read workspace section override");
118
+ return null;
119
+ }
120
+ const parsed = parseFrontmatterFields(raw);
121
+ const fields = parsed?.fields ?? {};
122
+ const body = parsed?.body ?? raw;
123
+ return { enabled: fields["enabled"] as string | boolean | undefined, body };
124
+ }
125
+ const bundled = BUNDLED_SYSTEM_SECTIONS.find((s) => s.id === id);
126
+ if (!bundled) return null;
127
+
128
+ // A bundled section may delegate its body to a workspace file outside
129
+ // the section override directory (e.g. `SOUL.md` at the workspace
130
+ // root). Read it now; missing/empty files yield "", which
131
+ // `renderSection` then gates off via its empty-body check.
132
+ if (bundled.workspacePath) {
133
+ const filePath = getWorkspacePromptPath(bundled.workspacePath);
134
+ let body = "";
135
+ if (existsSync(filePath)) {
136
+ try {
137
+ body = readFileSync(filePath, "utf-8");
138
+ } catch (err) {
139
+ log.warn(
140
+ { err, filePath, id },
141
+ "Failed to read section workspacePath",
142
+ );
143
+ }
144
+ }
145
+ return { enabled: bundled.enabled, body };
146
+ }
147
+
148
+ return { enabled: bundled.enabled, body: bundled.body };
149
+ }
150
+
151
+ function renderSection(
152
+ id: string,
153
+ ctx: SectionRenderContext,
154
+ workspaceDir: string,
155
+ ): string | null {
156
+ const section = resolveSection(id, workspaceDir);
157
+ if (section === null) return null;
158
+
159
+ if (!isEnabled(section.enabled, ctx)) return null;
160
+
161
+ const stripped = stripCommentLines(section.body).trim();
162
+ if (stripped.length === 0) return null;
163
+ return interpolateVariables(stripped, ctx);
164
+ }
165
+
166
+ const IDENT_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
167
+
168
+ /**
169
+ * Apply mustache-style interpolation to `body` against `ctx`, in this order:
170
+ *
171
+ * 1. **Standalone-tag normalization.** A section open/close tag occupying
172
+ * its own line (only whitespace on either side) absorbs the trailing
173
+ * newline. This lets authors write block-style templates without
174
+ * orphan blank lines bleeding through into the rendered output.
175
+ * 2. **Sections** — `{{#flag}}body{{/flag}}` renders `body` when
176
+ * `ctx[flag]` is truthy, empty otherwise. **Inverted sections** —
177
+ * `{{^flag}}body{{/flag}}` — render the opposite. The close tag's
178
+ * name must match the open tag's; bodies are matched non-greedily so
179
+ * sibling sections stay independent. Nested same-named sections are
180
+ * *not* supported (no use case yet).
181
+ * 3. **Variables** — `{{key}}` substitutes `String(ctx[key])`.
182
+ *
183
+ * Section *keys* are valid JS identifiers (`[A-Za-z_$][A-Za-z0-9_$]*`) so
184
+ * the construct can't be confused with code-block braces in the markdown.
185
+ * Section keys are coerced via `Boolean(ctx[key])` — `undefined`, `null`,
186
+ * `false`, `0`, and `""` all gate the body off; everything else gates it
187
+ * on. This means callers can pass through optional flags without
188
+ * normalizing each one to a defined boolean first. **Variable** keys
189
+ * whose `ctx` value is `undefined` or `null` stay literal (so an authoring
190
+ * typo on a `{{key}}` substitution surfaces at the warn log rather than
191
+ * inlining the string `"undefined"`).
192
+ */
193
+ function interpolateVariables(
194
+ body: string,
195
+ ctx: SectionRenderContext,
196
+ ): string {
197
+ // Collapse standalone tag lines so multiline section templates render
198
+ // without phantom blank lines from the layout markers.
199
+ const collapsed = body.replace(STANDALONE_TAG_LINE, "$1");
200
+
201
+ // Evaluate `{{#flag}}` / `{{^flag}}` blocks before variables, so a
202
+ // section body may itself contain `{{var}}` substitutions. Section
203
+ // keys are pure gates — the body is either in or out, never inlined —
204
+ // so we treat any falsy value (including `undefined`) as "gate off"
205
+ // rather than surfacing typos. This keeps optional `BuildSystemPromptOptions`
206
+ // flags working when the caller omits them.
207
+ const sectionsResolved = collapsed.replace(
208
+ SECTION,
209
+ (_match, kind: string, key: string, sectionBody: string) => {
210
+ const truthy = Boolean(ctx[key]);
211
+ const include = kind === "#" ? truthy : !truthy;
212
+ return include ? sectionBody : "";
213
+ },
214
+ );
215
+
216
+ return sectionsResolved.replace(VARIABLE, (match, key: string) => {
217
+ const value = ctx[key];
218
+ if (value === undefined || value === null) {
219
+ log.warn(
220
+ { key },
221
+ "Unresolved {{variable}} in workspace system prompt section; leaving literal",
222
+ );
223
+ return match;
224
+ }
225
+ return String(value);
226
+ });
227
+ }
228
+
229
+ const IDENT_PATTERN = "[A-Za-z_$][A-Za-z0-9_$]*";
230
+
231
+ /**
232
+ * Matches a section open/close tag that sits alone on its line (optional
233
+ * whitespace on either side, followed by a line terminator or end of
234
+ * input). The replacement keeps the tag itself and discards the
235
+ * surrounding whitespace + newline.
236
+ */
237
+ const STANDALONE_TAG_LINE = new RegExp(
238
+ `^[ \\t]*(\\{\\{[#^/]${IDENT_PATTERN}\\}\\})[ \\t]*(?:\\r?\\n|$)`,
239
+ "gm",
240
+ );
241
+
242
+ /**
243
+ * Matches a section block `{{#name}}body{{/name}}` or its inverted form
244
+ * `{{^name}}body{{/name}}`. The backreference forces the close tag to
245
+ * name the same key as the open tag; `[\s\S]*?` lets the body span
246
+ * multiple lines without greedy-matching across sibling sections.
247
+ */
248
+ const SECTION = new RegExp(
249
+ `\\{\\{([#^])(${IDENT_PATTERN})\\}\\}([\\s\\S]*?)\\{\\{\\/\\2\\}\\}`,
250
+ "g",
251
+ );
252
+
253
+ const VARIABLE = new RegExp(`\\{\\{(${IDENT_PATTERN})\\}\\}`, "g");
254
+
255
+ /**
256
+ * Evaluate an `enabled:` predicate. Supported shapes:
257
+ *
258
+ * - omitted / undefined → always enabled
259
+ * - boolean → use as-is
260
+ * - `<key>` → render when `ctx[key]` is truthy
261
+ * - `!<key>` → render when `ctx[key]` is falsy
262
+ *
263
+ * Predicate forms are intentionally limited to a single identifier (with
264
+ * optional leading `!`). Anything more elaborate is rejected so the
265
+ * predicate stays declarative — if a section needs richer logic, route a
266
+ * pre-computed boolean through the context map and reference that.
267
+ */
268
+ function isEnabled(value: unknown, ctx: SectionRenderContext): boolean {
269
+ if (value === undefined) return true;
270
+ if (typeof value === "boolean") return value;
271
+ if (typeof value !== "string") {
272
+ log.warn(
273
+ { value },
274
+ "Unsupported `enabled` type in section frontmatter; treating as disabled",
275
+ );
276
+ return false;
277
+ }
278
+
279
+ let trimmed = value.trim();
280
+ if (trimmed.length === 0) return true;
281
+
282
+ let negate = false;
283
+ if (trimmed.startsWith("!")) {
284
+ negate = true;
285
+ trimmed = trimmed.slice(1).trim();
286
+ }
287
+
288
+ if (!IDENT_REGEX.test(trimmed)) {
289
+ log.warn(
290
+ { value },
291
+ "Unsupported `enabled` expression in section frontmatter; treating as disabled",
292
+ );
293
+ return false;
294
+ }
295
+
296
+ const result = Boolean(ctx[trimmed]);
297
+ return negate ? !result : result;
298
+ }
299
+
300
+ // Re-export the registry type so callers (rare) can introspect bundled
301
+ // content without reaching into the templates directory directly.
302
+ export type { BundledSection };