@vellumai/assistant 0.8.4 → 0.8.5

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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
@@ -387,17 +387,20 @@ async function buildPkbReminderWithHints(
387
387
  * `memory-v2-static` injector — order 38, after-memory-prefix.
388
388
  *
389
389
  * Injects the v2 static memory block (essentials/threads/recent/buffer
390
- * concatenated under markdown headings) wrapped in `<memory>...</memory>`
390
+ * concatenated under markdown headings) wrapped in `<info>...</info>`
391
391
  * onto the user message. The agent loop only forwards `memoryV2Static` on
392
392
  * full-mode turns (first turn / post-compaction), mirroring the PKB
393
393
  * auto-inject cadence — subsequent turns get `null` and the prior block
394
394
  * stays cached on its original user message.
395
395
  *
396
- * Sits between `pkb-reminder` (35) and `now-md` (40) so the rendered order
397
- * after the memory prefix is `[pkb-reminder, pkb-context, memory-v2-static,
398
- * now-md, ...user text]` when every PKB injector also fires (transitional
399
- * state). Once PKB is fully retired under v2 this is the only block
400
- * adjacent to the memory prefix.
396
+ * Sits between `pkb-reminder` (35) and `now-md` (40). Because every
397
+ * after-memory-prefix splice lands at the memory-prefix boundary in
398
+ * ascending `order`, higher-order blocks end up closer to the memory
399
+ * prefix. The rendered layout is therefore `[<memory>dynamic</memory>,
400
+ * <info>memory-v2-static</info>, <NOW.md>, <system_reminder>,
401
+ * <knowledge_base>, ...user text]` when every PKB injector also fires.
402
+ * `countMemoryPrefixBlocks` treats the `<info>` static block as part of
403
+ * the memory prefix so `now-md` (40) splices after it.
401
404
  *
402
405
  * Gating:
403
406
  * - `mode === "full"`.
@@ -420,14 +423,18 @@ const memoryV2StaticInjector: Injector = {
420
423
  },
421
424
  };
422
425
 
426
+ const INFO_CLOSE_TAG_RE = /<\/info\s*>/gi;
427
+
423
428
  /**
424
- * Wrap the static memory content in `<memory>...</memory>`. Escapes any
425
- * closing `</memory>` inside the content so authored memory files cannot
426
- * accidentally break out of the wrapper.
429
+ * Wrap the static memory content in `<info>...</info>`. Escapes any
430
+ * closing `</info>` inside the content so authored memory files cannot
431
+ * accidentally break out of the wrapper. Distinct from the dynamic
432
+ * activation block (which uses `<memory>...</memory>`) so downstream
433
+ * logic can address the two differently.
427
434
  */
428
435
  function buildMemoryV2StaticBlock(content: string): string {
429
- const escaped = content.replace(/<\/memory\s*>/gi, "&lt;/memory&gt;");
430
- return `<memory>\n${escaped}\n</memory>`;
436
+ const escaped = content.replace(INFO_CLOSE_TAG_RE, "&lt;/info&gt;");
437
+ return `<info>\n${escaped}\n</info>`;
431
438
  }
432
439
 
433
440
  /**
@@ -49,12 +49,8 @@ import semver from "semver";
49
49
  import { z } from "zod";
50
50
 
51
51
  import assistantPkg from "../../package.json" with { type: "json" };
52
- import type {
53
- LoadedPluginTool,
54
- PluginTool,
55
- RiskLevel,
56
- ToolExecutionResult,
57
- } from "../tools/types.js";
52
+ import { finalizeTool } from "../tools/tool-defaults.js";
53
+ import type { LoadedTool, ToolDefinition } from "../tools/types.js";
58
54
  import { getLogger } from "../util/logger.js";
59
55
  import { registerPlugin } from "./registry.js";
60
56
  import type {
@@ -62,7 +58,6 @@ import type {
62
58
  PluginHookFn,
63
59
  PluginHooks,
64
60
  PluginManifest,
65
- PluginToolRegistration,
66
61
  } from "./types.js";
67
62
 
68
63
  const PLUGIN_API_PEER_DEP = "@vellumai/plugin-api";
@@ -122,64 +117,6 @@ function deriveToolName(toolFileBaseName: string): string {
122
117
  return toToolNameSegment(toolFileBaseName);
123
118
  }
124
119
 
125
- /**
126
- * Defaults applied by {@link applyPluginToolDefaults} when a plugin tool
127
- * omits one of the normally-required fields. Exported as a constant so
128
- * tests and callers can reference the same source of truth.
129
- *
130
- * The default `execute` returns an error result so the model sees a clear
131
- * "this tool isn't wired up" signal at call time. The plugin still loads
132
- * cleanly — broken individual tools must never block daemon boot.
133
- */
134
- export const PLUGIN_TOOL_DEFAULTS = Object.freeze({
135
- description: "",
136
- defaultRiskLevel: "medium" as RiskLevel,
137
- input_schema: Object.freeze({
138
- type: "object",
139
- properties: {},
140
- additionalProperties: false,
141
- }) as object,
142
- });
143
-
144
- /**
145
- * Fill the four normally-required {@link PluginTool} fields with documented
146
- * defaults when the author omitted them. Returns a {@link LoadedPluginTool}
147
- * that is safe to register.
148
- */
149
- function applyPluginToolDefaults(
150
- tool: PluginTool,
151
- name: string,
152
- ): LoadedPluginTool {
153
- const description =
154
- typeof tool.description === "string"
155
- ? tool.description
156
- : PLUGIN_TOOL_DEFAULTS.description;
157
- const defaultRiskLevel =
158
- typeof tool.defaultRiskLevel === "string"
159
- ? tool.defaultRiskLevel
160
- : PLUGIN_TOOL_DEFAULTS.defaultRiskLevel;
161
- const input_schema =
162
- tool.input_schema !== null &&
163
- typeof tool.input_schema === "object"
164
- ? tool.input_schema
165
- : PLUGIN_TOOL_DEFAULTS.input_schema;
166
- const execute =
167
- typeof tool.execute === "function"
168
- ? tool.execute
169
- : async (): Promise<ToolExecutionResult> => ({
170
- content: `plugin tool ${name} has no execute implementation`,
171
- isError: true,
172
- });
173
- return {
174
- ...tool,
175
- name,
176
- description,
177
- defaultRiskLevel,
178
- input_schema,
179
- execute,
180
- };
181
- }
182
-
183
120
  /**
184
121
  * Dynamic-import `absolutePath` and return its default export. Throws when
185
122
  * the module has no default export — callers attribute the error.
@@ -339,17 +276,17 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
339
276
  const hooks = await loadHooks(pluginDir, name);
340
277
  if (hooks !== undefined) plugin.hooks = hooks;
341
278
 
342
- const tools: PluginToolRegistration[] = [];
279
+ const tools: LoadedTool[] = [];
343
280
  for (const { name: toolName, path: toolPath } of listSurfaceDir(
344
281
  join(pluginDir, "tools"),
345
282
  )) {
346
- const tool = await importDefault<PluginTool>(toolPath);
283
+ const tool = await importDefault<ToolDefinition>(toolPath);
347
284
  if (tool === null || typeof tool !== "object") {
348
285
  throw new Error(
349
286
  `external plugin ${name}: ${toolPath} default export must be an object`,
350
287
  );
351
288
  }
352
- tools.push(applyPluginToolDefaults(tool, deriveToolName(toolName)));
289
+ tools.push(finalizeTool(tool, deriveToolName(toolName)));
353
290
  }
354
291
  if (tools.length > 0) plugin.tools = tools;
355
292
 
@@ -43,7 +43,7 @@ import type {
43
43
  } from "../providers/types.js";
44
44
  import type { SkillRoute } from "../runtime/skill-route-registry.js";
45
45
  import type {
46
- LoadedPluginTool,
46
+ LoadedTool,
47
47
  ToolContext,
48
48
  ToolExecutionResult,
49
49
  } from "../tools/types.js";
@@ -1007,19 +1007,6 @@ export interface Injector {
1007
1007
  // `PluginSkillRegistration` shape below so plugins can declare
1008
1008
  // catalog-discoverable skills today.
1009
1009
 
1010
- /**
1011
- * Tool registration contributed by a plugin. Uses the narrow
1012
- * {@link LoadedPluginTool} shape. External plugin authors declare the
1013
- * nameless `PluginTool` file shape; the loader derives `name` from the
1014
- * `tools/<name>.ts` basename before storing it on `plugin.tools`. Authors
1015
- * also leave category / ownership metadata to the bootstrap, which stamps
1016
- * `category: "plugin"`, `origin: "plugin"`, and
1017
- * `ownerPluginId: <plugin.name>` before handing the batch to
1018
- * `registerPluginTools`. The registration boundary synthesizes
1019
- * `getDefinition()` from `{name, description, input_schema}` so the canonical
1020
- * {@link Tool} interface used by the internal registry stays unchanged.
1021
- */
1022
- export type PluginToolRegistration = LoadedPluginTool;
1023
1010
  /**
1024
1011
  * HTTP route registration contributed by a plugin. Plugins express routes as
1025
1012
  * {@link SkillRoute} values — the same shape the skill-route registry
@@ -1120,8 +1107,16 @@ export interface Plugin {
1120
1107
  manifest: PluginManifest;
1121
1108
  /** Lifecycle hooks (init, shutdown). See {@link PluginHooks}. */
1122
1109
  hooks?: PluginHooks;
1123
- /** Tool registrations visible to the model. */
1124
- tools?: PluginToolRegistration[];
1110
+ /**
1111
+ * Tool registrations visible to the model. External plugin authors
1112
+ * declare the nameless `ToolDefinition` file shape (from
1113
+ * `@vellumai/plugin-api`); the loader derives `name` from the
1114
+ * `tools/<name>.ts` basename and runs the definition through
1115
+ * `finalizeTool` to fill omitted required fields, producing the
1116
+ * `LoadedTool` values stored here. Category / ownership metadata is
1117
+ * stamped by `registerPluginTools` at registration time.
1118
+ */
1119
+ tools?: LoadedTool[];
1125
1120
  /** HTTP route registrations served by the assistant. */
1126
1121
  routes?: PluginRouteRegistration[];
1127
1122
  /** Skill registrations loaded at startup. */
@@ -71,10 +71,23 @@ export async function injectAuxAssistantMessage(params: {
71
71
  });
72
72
  }
73
73
 
74
- params.broadcastMessage({
75
- type: "conversation_list_invalidated",
76
- reason: "reordered",
77
- });
74
+ // The injected aux message bumps `lastMessageAt`, which can move the
75
+ // row in the paginated sidebar window — that's a shape change. macOS
76
+ // refreshes its full list on `conversation_list_invalidated`; web
77
+ // refreshes via the paired `sync_changed` `conversationsList` tag.
78
+ //
79
+ // TODO(electron-cutover): drop the `conversation_list_invalidated`
80
+ // emission once macOS migrates to the Electron client and consumes
81
+ // `sync_changed` directly. See `runtime/sync/resource-sync-events.ts`
82
+ // for the symmetric helper used by route handlers.
83
+ params.broadcastMessage(
84
+ {
85
+ type: "conversation_list_invalidated",
86
+ reason: "reordered",
87
+ },
88
+ undefined,
89
+ { targetInterfaceId: "macos" },
90
+ );
78
91
  params.broadcastMessage({
79
92
  type: "sync_changed",
80
93
  tags: [
@@ -54,8 +54,9 @@ mock.module("../../config/loader.js", () => ({
54
54
  setNestedValue: () => {},
55
55
  }));
56
56
 
57
- const { buildSystemPrompt, ensurePromptFiles, SYSTEM_PROMPT_CACHE_BOUNDARY } =
58
- await import("../system-prompt.js");
57
+ const { buildSystemPrompt, ensurePromptFiles } = await import(
58
+ "../system-prompt.js"
59
+ );
59
60
 
60
61
  describe("task_progress hint in parallel-tool-calls section", () => {
61
62
  beforeEach(() => {
@@ -85,11 +86,4 @@ describe("task_progress hint in parallel-tool-calls section", () => {
85
86
  expect(withExcludePrefix).toContain("task_progress");
86
87
  });
87
88
 
88
- test("hint lives in the static (cached) block before SYSTEM_PROMPT_CACHE_BOUNDARY", () => {
89
- const result = buildSystemPrompt();
90
- const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
91
- expect(boundaryIdx).toBeGreaterThan(-1);
92
- const staticBlock = result.slice(0, boundaryIdx);
93
- expect(staticBlock).toContain("task_progress");
94
- });
95
89
  });
@@ -77,31 +77,46 @@ function resolveUserFilename(
77
77
  ): string | null {
78
78
  let filename: string | null = null;
79
79
 
80
- if (trustContext === undefined) {
81
- // Desktop / native (no gateway) — resolve via guardian contact,
82
- // preferring the vellum-channel guardian when multiple exist.
83
- const vellumGuardian = findGuardianForChannel("vellum");
84
- const guardian = vellumGuardian ?? listGuardianChannels();
85
- if (guardian) {
86
- filename = guardian.contact.userFile ?? "guardian.md";
87
- }
88
- } else if (trustContext.requesterExternalUserId) {
89
- // Channel-routed request — look up contact by channel identity
90
- const contactWithChannels = findContactByChannelExternalId(
91
- trustContext.sourceChannel,
92
- trustContext.requesterExternalUserId,
93
- );
94
- if (contactWithChannels) {
95
- filename = contactWithChannels.userFile ?? null;
96
- } else if (trustContext.trustClass === "guardian") {
97
- // Managed desktop: the JWT principal ID used as requesterExternalUserId
98
- // may differ from the contact channel's external_user_id (they are
99
- // separate identity concepts). Fall back to the channel-type guardian.
100
- const guardian = findGuardianForChannel(trustContext.sourceChannel);
80
+ try {
81
+ if (trustContext === undefined) {
82
+ // Desktop / native (no gateway) resolve via guardian contact,
83
+ // preferring the vellum-channel guardian when multiple exist.
84
+ const vellumGuardian = findGuardianForChannel("vellum");
85
+ const guardian = vellumGuardian ?? listGuardianChannels();
101
86
  if (guardian) {
102
87
  filename = guardian.contact.userFile ?? "guardian.md";
103
88
  }
89
+ } else if (trustContext.requesterExternalUserId) {
90
+ // Channel-routed request — look up contact by channel identity
91
+ const contactWithChannels = findContactByChannelExternalId(
92
+ trustContext.sourceChannel,
93
+ trustContext.requesterExternalUserId,
94
+ );
95
+ if (contactWithChannels) {
96
+ filename = contactWithChannels.userFile ?? null;
97
+ } else if (trustContext.trustClass === "guardian") {
98
+ // Managed desktop: the JWT principal ID used as requesterExternalUserId
99
+ // may differ from the contact channel's external_user_id (they are
100
+ // separate identity concepts). Fall back to the channel-type guardian.
101
+ const guardian = findGuardianForChannel(trustContext.sourceChannel);
102
+ if (guardian) {
103
+ filename = guardian.contact.userFile ?? "guardian.md";
104
+ }
105
+ }
104
106
  }
107
+ } catch (err) {
108
+ // Contacts table may be absent — happens during early bootstrap
109
+ // before migrations run, in CLI smoke commands that don't touch
110
+ // the DB, and in tests that build the system prompt without first
111
+ // initializing a schema. Treat the same as "no guardian found"
112
+ // so callers fall back to `users/default.md`. Mirrors the same
113
+ // try/catch pattern used by `renderConnectedServices()` around
114
+ // `listConnections()`.
115
+ log.debug(
116
+ { err: err instanceof Error ? err.message : String(err) },
117
+ "Contacts lookup failed during persona resolution; treating as no guardian",
118
+ );
119
+ return null;
105
120
  }
106
121
 
107
122
  // Validate basename to prevent path traversal
@@ -110,6 +110,7 @@ interface ResolvedSection {
110
110
 
111
111
  function resolveSection(
112
112
  id: string,
113
+ ctx: SectionRenderContext,
113
114
  workspaceDir: string,
114
115
  ): ResolvedSection | null {
115
116
  const workspacePath = join(workspaceDir, `${id}.md`);
@@ -139,15 +140,28 @@ function resolveSection(
139
140
 
140
141
  // A bundled section may delegate its body to a workspace file outside
141
142
  // the section override directory (e.g. `SOUL.md` at the workspace
142
- // root). Read it now; missing/empty files yield "", which
143
- // `renderSection` then gates off via its empty-body check (or via the
144
- // section's `transform`, if set).
143
+ // root). `workspacePath` may be a single path or an array of paths
144
+ // tried in order the first one whose file exists and has non-empty
145
+ // content wins. Each entry may reference `{{ctx-key}}` variables
146
+ // (e.g. `users/{{userSlug}}.md`) that are interpolated against the
147
+ // render context before resolution. Missing/empty files yield "",
148
+ // which `renderSection` then gates off via its empty-body check (or
149
+ // via the section's `transform`, if set).
145
150
  if (bundled.workspacePath) {
146
- const filePath = getWorkspacePromptPath(bundled.workspacePath);
151
+ const paths = Array.isArray(bundled.workspacePath)
152
+ ? bundled.workspacePath
153
+ : [bundled.workspacePath];
147
154
  let body = "";
148
- if (existsSync(filePath)) {
155
+ for (const pathTemplate of paths) {
156
+ const interpolated = interpolateWorkspacePath(pathTemplate, ctx);
157
+ const filePath = getWorkspacePromptPath(interpolated);
158
+ if (!existsSync(filePath)) continue;
149
159
  try {
150
- body = readFileSync(filePath, "utf-8");
160
+ const content = readFileSync(filePath, "utf-8");
161
+ if (content.trim().length > 0) {
162
+ body = content;
163
+ break;
164
+ }
151
165
  } catch (err) {
152
166
  log.warn({ err, filePath, id }, "Failed to read section workspacePath");
153
167
  }
@@ -162,12 +176,30 @@ function resolveSection(
162
176
  };
163
177
  }
164
178
 
179
+ /**
180
+ * Interpolate `{{key}}` references in a workspace-path template against
181
+ * `ctx`. Section / inverted-section tags are not supported in paths —
182
+ * only flat variable substitution. Unresolved keys stay literal so a
183
+ * typo surfaces as a missing file rather than silently rendering an
184
+ * unrelated section.
185
+ */
186
+ function interpolateWorkspacePath(
187
+ template: string,
188
+ ctx: SectionRenderContext,
189
+ ): string {
190
+ return template.replace(VARIABLE, (match, key: string) => {
191
+ const value = ctx[key];
192
+ if (value === undefined || value === null) return match;
193
+ return String(value);
194
+ });
195
+ }
196
+
165
197
  function renderSection(
166
198
  id: string,
167
199
  ctx: SectionRenderContext,
168
200
  workspaceDir: string,
169
201
  ): string | null {
170
- const section = resolveSection(id, workspaceDir);
202
+ const section = resolveSection(id, ctx, workspaceDir);
171
203
  if (section === null) return null;
172
204
 
173
205
  if (!isEnabled(section.enabled, ctx)) return null;