@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d

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 (367) hide show
  1. package/docs/workspace-tools.md +42 -33
  2. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  3. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  5. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  6. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  7. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  8. package/openapi.yaml +74 -1
  9. package/package.json +1 -1
  10. package/scripts/test.sh +36 -15
  11. package/src/__tests__/actor-token-service.test.ts +36 -14
  12. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  13. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  14. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  15. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  16. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  17. package/src/__tests__/approval-cascade.test.ts +2 -0
  18. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  19. package/src/__tests__/btw-routes.test.ts +2 -0
  20. package/src/__tests__/build-persisted-content.test.ts +2 -0
  21. package/src/__tests__/call-controller.test.ts +19 -0
  22. package/src/__tests__/channel-guardian.test.ts +94 -58
  23. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  24. package/src/__tests__/compaction-events.test.ts +2 -0
  25. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  26. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  27. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  28. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  29. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  30. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  31. package/src/__tests__/computer-use-tools.test.ts +13 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +5 -1
  33. package/src/__tests__/config-schema.test.ts +1 -0
  34. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  35. package/src/__tests__/contacts-relay-reads.test.ts +13 -15
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  37. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  38. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  40. package/src/__tests__/conversation-agent-loop.test.ts +7 -0
  41. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  42. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  44. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  45. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  46. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  47. package/src/__tests__/conversation-pairing.test.ts +2 -0
  48. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  51. package/src/__tests__/conversation-queue.test.ts +91 -0
  52. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  53. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  54. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  55. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  56. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  57. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
  58. package/src/__tests__/conversation-title-service.test.ts +2 -0
  59. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  60. package/src/__tests__/conversation-usage.test.ts +2 -0
  61. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  64. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  65. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  66. package/src/__tests__/db-test-helpers.ts +5 -4
  67. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  68. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  69. package/src/__tests__/dm-persistence.test.ts +2 -0
  70. package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
  71. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  72. package/src/__tests__/filing-service.test.ts +2 -0
  73. package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
  74. package/src/__tests__/guardian-dispatch.test.ts +95 -1
  75. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  76. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  77. package/src/__tests__/heartbeat-service.test.ts +2 -0
  78. package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
  79. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  80. package/src/__tests__/host-bash-routes.test.ts +31 -41
  81. package/src/__tests__/host-browser-routes.test.ts +26 -32
  82. package/src/__tests__/host-cu-proxy.test.ts +299 -0
  83. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  84. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  85. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  86. package/src/__tests__/http-user-message-parity.test.ts +167 -8
  87. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  88. package/src/__tests__/invite-redemption-service.test.ts +43 -0
  89. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  90. package/src/__tests__/llm-usage-store.test.ts +25 -0
  91. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  92. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  93. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  94. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  95. package/src/__tests__/native-web-search.test.ts +2 -0
  96. package/src/__tests__/non-member-access-request.test.ts +189 -17
  97. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  98. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  99. package/src/__tests__/notification-deep-link.test.ts +6 -0
  100. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  101. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  102. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  103. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  104. package/src/__tests__/plugin-bootstrap.test.ts +3 -73
  105. package/src/__tests__/plugin-route-contribution.test.ts +4 -17
  106. package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
  107. package/src/__tests__/plugin-types.test.ts +0 -2
  108. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  109. package/src/__tests__/process-message-display-content.test.ts +2 -0
  110. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  111. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  112. package/src/__tests__/registry.test.ts +3 -0
  113. package/src/__tests__/relay-server.test.ts +694 -25
  114. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  115. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  116. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  117. package/src/__tests__/skills.test.ts +44 -0
  118. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  119. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
  120. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  121. package/src/__tests__/stt-hints.test.ts +44 -13
  122. package/src/__tests__/subagent-detail.test.ts +27 -0
  123. package/src/__tests__/subagent-disposal.test.ts +65 -0
  124. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  125. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  126. package/src/__tests__/subagent-tools.test.ts +2 -0
  127. package/src/__tests__/suggestion-routes.test.ts +2 -0
  128. package/src/__tests__/title-generate-hook.test.ts +2 -0
  129. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  130. package/src/__tests__/tool-executor.test.ts +16 -11
  131. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  132. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  133. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  134. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
  135. package/src/__tests__/twilio-routes.test.ts +96 -0
  136. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  137. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  138. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  139. package/src/agent/loop-exclusive-tool.test.ts +150 -0
  140. package/src/agent/loop.ts +56 -0
  141. package/src/api/constants/sse-replay.ts +41 -0
  142. package/src/api/index.ts +6 -0
  143. package/src/api/responses/llm-request-log-entry.ts +25 -0
  144. package/src/api/responses/subagent-detail.ts +17 -0
  145. package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
  146. package/src/calls/call-domain.ts +3 -3
  147. package/src/calls/guardian-dispatch.ts +10 -8
  148. package/src/calls/inbound-trust-reader.ts +17 -1
  149. package/src/calls/media-stream-server.ts +21 -0
  150. package/src/calls/relay-server.ts +167 -50
  151. package/src/calls/relay-setup-router.ts +37 -7
  152. package/src/calls/relay-verification.ts +4 -4
  153. package/src/calls/stt-hints.ts +9 -12
  154. package/src/calls/twilio-routes.ts +14 -4
  155. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  156. package/src/cli/commands/cache.ts +194 -181
  157. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  158. package/src/cli/commands/db/status.ts +37 -1
  159. package/src/cli/commands/mcp.ts +252 -218
  160. package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
  161. package/src/cli/commands/memory/index.ts +2 -0
  162. package/src/cli/commands/memory/worker.ts +175 -0
  163. package/src/cli/commands/plugins.ts +75 -3
  164. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  165. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  166. package/src/cli/lib/list-installed-plugins.ts +179 -1
  167. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  168. package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
  169. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  170. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  171. package/src/config/feature-flag-registry.json +0 -8
  172. package/src/config/loader.ts +36 -5
  173. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  174. package/src/config/schemas/memory-lifecycle.ts +12 -0
  175. package/src/config/schemas/memory-v3.ts +7 -0
  176. package/src/config/schemas/memory.ts +4 -0
  177. package/src/config/schemas/timeouts.ts +8 -0
  178. package/src/config/seed-inference-profiles.ts +14 -5
  179. package/src/config/skills.ts +27 -5
  180. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  181. package/src/contacts/contacts-write.ts +3 -0
  182. package/src/contacts/guardian-delivery-reader.ts +223 -0
  183. package/src/daemon/conversation-agent-loop.ts +9 -0
  184. package/src/daemon/conversation-process.ts +39 -17
  185. package/src/daemon/conversation-surfaces.ts +8 -0
  186. package/src/daemon/conversation-tool-setup.ts +49 -16
  187. package/src/daemon/conversation.ts +21 -2
  188. package/src/daemon/disk-pressure-guard.ts +12 -2
  189. package/src/daemon/event-loop-watchdog.ts +28 -1
  190. package/src/daemon/external-plugins-bootstrap.ts +4 -34
  191. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
  192. package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
  193. package/src/daemon/handlers/config-a2a.ts +6 -14
  194. package/src/daemon/handlers/config-channels.ts +78 -22
  195. package/src/daemon/handlers/conversations.ts +77 -0
  196. package/src/daemon/host-cu-proxy.ts +102 -11
  197. package/src/daemon/lifecycle.ts +4 -0
  198. package/src/daemon/memory-v2-startup.test.ts +72 -0
  199. package/src/daemon/memory-v2-startup.ts +87 -19
  200. package/src/daemon/server.ts +0 -4
  201. package/src/daemon/shutdown-handlers.ts +20 -0
  202. package/src/daemon/tool-setup-types.ts +9 -0
  203. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  204. package/src/ipc/assistant-server.ts +2 -2
  205. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  206. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  207. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  208. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  209. package/src/memory/conversation-crud.ts +38 -0
  210. package/src/memory/db-connection.ts +22 -3
  211. package/src/memory/db-init.ts +36 -502
  212. package/src/memory/db-singleton.ts +6 -4
  213. package/src/memory/jobs-worker.ts +58 -0
  214. package/src/memory/llm-usage-store.ts +48 -20
  215. package/src/memory/memory-retrospective-job.ts +9 -8
  216. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
  217. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
  218. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
  219. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  220. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  221. package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
  222. package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
  223. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
  224. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  225. package/src/memory/migrations/run-migrations.ts +90 -6
  226. package/src/memory/migrations/schema-introspection.ts +14 -0
  227. package/src/memory/migrations/validate-migration-state.ts +101 -66
  228. package/src/memory/prompt-override.ts +129 -0
  229. package/src/memory/schema/conversations.ts +9 -0
  230. package/src/memory/schema/infrastructure.ts +20 -0
  231. package/src/memory/steps.ts +573 -0
  232. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  233. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  234. package/src/memory/v2/cli-command-store.ts +75 -38
  235. package/src/memory/v2/prompts/consolidation.ts +13 -82
  236. package/src/memory/v2/prompts/router.ts +21 -93
  237. package/src/memory/v2/skill-store.ts +68 -31
  238. package/src/memory/watchdog-events-store.ts +87 -0
  239. package/src/memory/worker-control.ts +118 -0
  240. package/src/memory/worker-process.ts +72 -0
  241. package/src/notifications/__tests__/broadcaster.test.ts +16 -8
  242. package/src/notifications/__tests__/connected-channels.test.ts +114 -0
  243. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  244. package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
  245. package/src/notifications/broadcaster.ts +8 -1
  246. package/src/notifications/decision-engine.ts +15 -7
  247. package/src/notifications/destination-resolver.ts +68 -24
  248. package/src/notifications/emit-signal.ts +39 -14
  249. package/src/onboarding/checkin-event.test.ts +220 -0
  250. package/src/onboarding/checkin-event.ts +321 -0
  251. package/src/onboarding/schedule-checkin.ts +190 -0
  252. package/src/permissions/question-prompter.test.ts +1 -1
  253. package/src/permissions/question-prompter.ts +7 -4
  254. package/src/plugin-api/index.ts +6 -6
  255. package/src/plugin-api/types.ts +3 -5
  256. package/src/plugin-api/vision-support.test.ts +28 -4
  257. package/src/plugin-api/vision-support.ts +66 -31
  258. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
  259. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
  260. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
  261. package/src/plugins/defaults/advisor/consult.ts +110 -6
  262. package/src/plugins/defaults/advisor/context-pack.ts +288 -0
  263. package/src/plugins/defaults/advisor/steering.ts +14 -2
  264. package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
  265. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  266. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
  267. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
  268. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  269. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  270. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  271. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
  272. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
  273. package/src/plugins/mtime-cache.ts +7 -2
  274. package/src/plugins/types.ts +0 -2
  275. package/src/providers/anthropic/client.ts +5 -0
  276. package/src/providers/call-site-routing.ts +4 -0
  277. package/src/providers/model-catalog.ts +16 -0
  278. package/src/providers/openai/responses-provider.ts +5 -0
  279. package/src/providers/openrouter/client.ts +5 -0
  280. package/src/providers/provider-send-message.ts +4 -0
  281. package/src/providers/ratelimit.ts +4 -0
  282. package/src/providers/retry.ts +4 -0
  283. package/src/providers/types.ts +9 -0
  284. package/src/providers/usage-tracking.ts +4 -0
  285. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  286. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
  287. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  288. package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
  289. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
  290. package/src/runtime/access-request-helper.ts +19 -39
  291. package/src/runtime/actor-trust-resolver.ts +2 -2
  292. package/src/runtime/anchored-guardian.test.ts +156 -0
  293. package/src/runtime/anchored-guardian.ts +135 -0
  294. package/src/runtime/assistant-event-hub.ts +1 -1
  295. package/src/runtime/assistant-stream-state.ts +9 -2
  296. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  297. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  298. package/src/runtime/channel-verification-service.ts +56 -31
  299. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  300. package/src/runtime/guardian-vellum-migration.ts +66 -7
  301. package/src/runtime/invite-redemption-service.ts +50 -18
  302. package/src/runtime/local-actor-identity.ts +76 -11
  303. package/src/runtime/local-principal-trust.ts +52 -0
  304. package/src/runtime/pending-interactions.ts +11 -1
  305. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  306. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  307. package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
  308. package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
  309. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
  310. package/src/runtime/routes/browser-routes.ts +1 -1
  311. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  312. package/src/runtime/routes/contact-routes.ts +8 -32
  313. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  314. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  315. package/src/runtime/routes/conversation-routes.ts +74 -81
  316. package/src/runtime/routes/events-routes.ts +2 -2
  317. package/src/runtime/routes/global-search-routes.ts +3 -1
  318. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  319. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  320. package/src/runtime/routes/host-bash-routes.ts +5 -4
  321. package/src/runtime/routes/host-browser-routes.ts +9 -11
  322. package/src/runtime/routes/host-cu-routes.ts +5 -4
  323. package/src/runtime/routes/host-file-routes.ts +5 -4
  324. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  325. package/src/runtime/routes/http-adapter.ts +1 -1
  326. package/src/runtime/routes/identity-routes.ts +3 -2
  327. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  328. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
  331. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  332. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  333. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  334. package/src/runtime/routes/index.ts +2 -0
  335. package/src/runtime/routes/llm-context-normalization.ts +71 -0
  336. package/src/runtime/routes/mcp-auth-routes.ts +38 -15
  337. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  338. package/src/runtime/routes/migration-routes.ts +4 -1
  339. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  340. package/src/runtime/routes/subagents-routes.ts +5 -0
  341. package/src/runtime/routes/surface-action-routes.ts +51 -55
  342. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  343. package/src/runtime/services/conversation-serializer.ts +7 -9
  344. package/src/runtime/tool-grant-request-helper.ts +3 -3
  345. package/src/runtime/trust-verdict-consumer.ts +85 -9
  346. package/src/runtime/verification-outbound-actions.ts +18 -18
  347. package/src/signals/user-message.ts +16 -0
  348. package/src/subagent/manager.ts +9 -0
  349. package/src/telemetry/types.ts +34 -1
  350. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  351. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  352. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  353. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  354. package/src/tools/computer-use/definitions.ts +8 -2
  355. package/src/tools/executor.ts +4 -4
  356. package/src/tools/registry.ts +18 -0
  357. package/src/tools/tool-approval-handler.ts +1 -1
  358. package/src/tools/tool-defaults.ts +9 -2
  359. package/src/tools/types.ts +17 -2
  360. package/src/tools/workspace-tools/loader.ts +348 -244
  361. package/src/util/platform.ts +5 -0
  362. package/src/util/telemetry-db-path.ts +24 -0
  363. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  364. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  365. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  366. package/src/daemon/workspace-tools-watcher.ts +0 -328
  367. package/src/memory/migrations/registry.ts +0 -573
@@ -619,6 +619,86 @@ Write a local article draft.
619
619
  expect(after).toEqual(before);
620
620
  });
621
621
 
622
+ test("populates the cache from the local catalog on the first seed even when the embedding backend is unavailable (cold-start needle resilience)", async () => {
623
+ // Regression: on a brand-new managed assistant the startup seed runs before
624
+ // the platform provisions the managed embedding credential, so the very
625
+ // first `embedWithBackend` throws. The in-memory cache (which the v3 needle
626
+ // lane and the page index read) must still populate from the local catalog
627
+ // so skills are discoverable from first boot; only the dense Qdrant upsert
628
+ // is deferred until the backend recovers.
629
+ const skillA = makeSummary({
630
+ id: "example-skill-a",
631
+ displayName: "Skill A",
632
+ });
633
+ state.catalog = [skillA];
634
+ state.resolved = [{ summary: skillA, state: "enabled" }];
635
+ // No prior successful seed — the backend is unconfigured from the start.
636
+ state.embedThrows = new Error(
637
+ 'Embedding backend "gemini" is not configured',
638
+ );
639
+
640
+ // Best-effort callers (the startup seed) must resolve.
641
+ await expect(seedV2SkillEntries()).resolves.toBeUndefined();
642
+
643
+ // The needle-lane cache is populated despite the dense-embed failure.
644
+ const entry = getSkillCapability("example-skill-a");
645
+ expect(entry).not.toBeNull();
646
+ expect(entry?.id).toBe("example-skill-a");
647
+ expect(entry?.content).toContain("Skill A");
648
+ expect(listSkillEntries().map((e) => e.id)).toEqual(["example-skill-a"]);
649
+
650
+ // No dense vectors were produced, so the Qdrant write is skipped entirely.
651
+ expect(state.upsertCalls).toHaveLength(0);
652
+ expect(state.pruneCalls).toHaveLength(0);
653
+ });
654
+
655
+ test("surfaces the dense-embed failure to throwOnError callers while still populating the needle cache", async () => {
656
+ // The managed-credential reseed and the operator reembed route pass
657
+ // `throwOnError` so they learn the dense lane didn't complete and the
658
+ // retry/maintain machinery backfills it — but the needle cache is fixed
659
+ // regardless before the error propagates.
660
+ const skillA = makeSummary({ id: "example-skill-a" });
661
+ state.catalog = [skillA];
662
+ state.resolved = [{ summary: skillA, state: "enabled" }];
663
+ state.embedThrows = new Error("backend down");
664
+
665
+ await expect(seedV2SkillEntries({ throwOnError: true })).rejects.toThrow(
666
+ "backend down",
667
+ );
668
+
669
+ expect(getSkillCapability("example-skill-a")).not.toBeNull();
670
+ expect(state.upsertCalls).toHaveLength(0);
671
+ });
672
+
673
+ test("drops a locally-disabled installed skill even when the catalog is unavailable (local state is authoritative)", async () => {
674
+ // Regression: the local resolution is authoritative, so a skill that is
675
+ // still installed but explicitly disabled must NOT be kept alive by remote-
676
+ // catalog uncertainty — `getSkillCapability` and the page index must drop it.
677
+ const skillA = makeSummary({ id: "example-skill-a" });
678
+ state.catalog = [skillA];
679
+ state.resolved = [{ summary: skillA, state: "enabled" }];
680
+ state.fullCatalog = [
681
+ { id: "example-skill-a", name: "example-skill-a", description: "A" },
682
+ ];
683
+ state.embedReturn = [[0.1, 0.2, 0.3]];
684
+
685
+ // First run: skillA enabled and cached.
686
+ await seedV2SkillEntries();
687
+ expect(getSkillCapability("example-skill-a")).not.toBeNull();
688
+
689
+ // Second run: skillA is still locally installed but now disabled; the remote
690
+ // catalog is unavailable. It stays in `installedIds`, so the carry-forward
691
+ // must skip it and the cache drops it.
692
+ state.resolved = [{ summary: skillA, state: "disabled" }];
693
+ state.fullCatalog = [];
694
+ state.fullCatalogThrows = new Error("catalog fetch failed");
695
+
696
+ await seedV2SkillEntries();
697
+
698
+ expect(getSkillCapability("example-skill-a")).toBeNull();
699
+ expect(listSkillEntries()).toEqual([]);
700
+ });
701
+
622
702
  test("no enabled skills yields empty cache and no prune when catalog is empty", async () => {
623
703
  state.catalog = [];
624
704
  state.resolved = [];
@@ -161,22 +161,23 @@ async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
161
161
  seeds.push({ id: name, description, content });
162
162
  }
163
163
 
164
+ // Sparse (BM25/TF) encoding is computed locally; only the dense vectors
165
+ // require `embedWithBackend`, which is unconfigured during the cold-start
166
+ // window before a managed-proxy embedding credential is provisioned. A
167
+ // dense-embed failure is non-fatal to the in-memory cache: the v3 needle
168
+ // finder lane reads CLI capabilities from `entries` / the page index, NOT
169
+ // from Qdrant, so the cache is populated from the local Commander tree
170
+ // regardless of backend state and commands stay discoverable from first
171
+ // boot. Only the dense Qdrant upsert is skipped; the managed-credential
172
+ // reseed and the v3 maintain pass backfill the dense vectors on recovery.
164
173
  const nextEntries = new Map<string, CliCommandEntry>();
165
174
  let denseVectors: number[][] = [];
175
+ let denseAvailable = false;
176
+ let denseError: unknown = null;
166
177
  let encodeSparse: (
167
178
  input: string,
168
179
  ) => ReturnType<typeof generateSparseEmbedding> = generateSparseEmbedding;
169
180
  if (seeds.length > 0) {
170
- const embedded = await embedWithBackend(
171
- config,
172
- seeds.map((s) => s.content),
173
- );
174
- denseVectors = await Promise.all(
175
- embedded.vectors.map((v) =>
176
- applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
177
- ),
178
- );
179
-
180
181
  // CLI commands share the concept-page Qdrant collection, so the sparse
181
182
  // vector must use the same stemmed BM25 encoding as the concept-page
182
183
  // documents. Fall back to the legacy TF encoder only during the cold-
@@ -190,6 +191,24 @@ async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
190
191
  b: config.memory.v2.bm25_b,
191
192
  })
192
193
  : generateSparseEmbedding(input);
194
+ try {
195
+ const embedded = await embedWithBackend(
196
+ config,
197
+ seeds.map((s) => s.content),
198
+ );
199
+ denseVectors = await Promise.all(
200
+ embedded.vectors.map((v) =>
201
+ applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
202
+ ),
203
+ );
204
+ denseAvailable = true;
205
+ } catch (err) {
206
+ denseError = err;
207
+ log.warn(
208
+ { err },
209
+ "Embedding backend unavailable — seeding CLI-command cache without dense Qdrant vectors; the needle lane surfaces commands from the cache and the dense lane backfills when the backend recovers",
210
+ );
211
+ }
193
212
  }
194
213
 
195
214
  if (generation !== requestedSeedGeneration) {
@@ -201,7 +220,17 @@ async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
201
220
  return;
202
221
  }
203
222
 
204
- if (seeds.length > 0) {
223
+ // Populate the in-memory cache (and therefore the page index / needle lane)
224
+ // from the local Commander tree regardless of dense availability.
225
+ for (const seed of seeds) {
226
+ nextEntries.set(seed.id, seed);
227
+ }
228
+
229
+ // Write the dense+sparse Qdrant points only when dense vectors were
230
+ // produced. In the degraded (backend-unavailable) path we skip the upsert
231
+ // so we never write half-formed points; the dense lane backfills on
232
+ // recovery.
233
+ if (seeds.length > 0 && denseAvailable) {
205
234
  const now = Date.now();
206
235
  await Promise.all(
207
236
  seeds.map((seed, i) =>
@@ -214,40 +243,48 @@ async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
214
243
  }),
215
244
  ),
216
245
  );
217
- for (const seed of seeds) {
218
- nextEntries.set(seed.id, seed);
219
- }
220
246
  }
221
247
 
222
- // The CLI tree is always available (no remote catalog), so pruning is
223
- // unconditional. Run the legacy `kind` backfill once per process so
224
- // pre-discriminator rows become prunable.
225
- const knownIds = new Set(seeds.map((s) => s.id));
226
- if (!legacyKindBackfillDone) {
227
- try {
228
- await backfillKindOnPointsWithPrefix(
229
- CLI_COMMAND_SLUG_PREFIX,
230
- CLI_COMMAND_PAYLOAD_KIND,
231
- knownIds,
232
- );
233
- legacyKindBackfillDone = true;
234
- } catch (err) {
235
- log.warn(
236
- { err },
237
- "Failed to backfill kind on legacy CLI-command points — pruning may leave orphans this run",
238
- );
248
+ // The CLI tree is always available (no remote catalog), so pruning to clear
249
+ // stale rows runs whenever we wrote the current set this run OR there was
250
+ // nothing to embed (empty tree). The cold-start degraded path (commands
251
+ // present but dense embedding unavailable) skips the prune so we never
252
+ // reconcile the collection against a set we did not persist. Run the legacy
253
+ // `kind` backfill once per process so pre-discriminator rows become prunable.
254
+ if (denseAvailable || seeds.length === 0) {
255
+ const knownIds = new Set(seeds.map((s) => s.id));
256
+ if (!legacyKindBackfillDone) {
257
+ try {
258
+ await backfillKindOnPointsWithPrefix(
259
+ CLI_COMMAND_SLUG_PREFIX,
260
+ CLI_COMMAND_PAYLOAD_KIND,
261
+ knownIds,
262
+ );
263
+ legacyKindBackfillDone = true;
264
+ } catch (err) {
265
+ log.warn(
266
+ { err },
267
+ "Failed to backfill kind on legacy CLI-command points — pruning may leave orphans this run",
268
+ );
269
+ }
239
270
  }
271
+ await pruneSlugsWithPrefixExcept(
272
+ CLI_COMMAND_SLUG_PREFIX,
273
+ seeds.map((s) => s.id),
274
+ { kind: CLI_COMMAND_PAYLOAD_KIND },
275
+ );
240
276
  }
241
- await pruneSlugsWithPrefixExcept(
242
- CLI_COMMAND_SLUG_PREFIX,
243
- seeds.map((s) => s.id),
244
- { kind: CLI_COMMAND_PAYLOAD_KIND },
245
- );
246
277
 
247
- // Atomically replace the cache only after every step above succeeds.
278
+ // Atomically replace the cache from the freshly enumerated commands. Drop
279
+ // the page-index cache so the next router invocation observes the new
280
+ // command set.
248
281
  entries = nextEntries;
249
282
  invalidatePageIndex();
250
- lastSeedError = null;
283
+
284
+ // Surface a dense-embed failure to `throwOnError` callers so the existing
285
+ // retry + maintain machinery backfills the dense lane. The in-memory cache
286
+ // is already updated above, so the needle lane is fixed regardless.
287
+ lastSeedError = denseError;
251
288
  } catch (err) {
252
289
  lastSeedError = err;
253
290
  log.warn({ err }, "Failed to seed v2 CLI-command entries");
@@ -18,12 +18,9 @@
18
18
  * the convention established for the sweep prompt.
19
19
  */
20
20
 
21
- import { lstatSync, readFileSync } from "node:fs";
22
- import { homedir } from "node:os";
23
- import { isAbsolute, join } from "node:path";
24
-
25
21
  import { getLogger } from "../../../util/logger.js";
26
22
  import { getWorkspaceDir } from "../../../util/platform.js";
23
+ import { loadPromptOverride } from "../../prompt-override.js";
27
24
 
28
25
  const log = getLogger("memory-v2-consolidate-prompt");
29
26
 
@@ -61,14 +58,6 @@ export const CORE_PAGES_CONSOLIDATION_SECTION = `## 10. Review \`memory/core-pag
61
58
 
62
59
  `;
63
60
 
64
- /**
65
- * Upper bound for the override file. Real consolidation prompts are kilobytes;
66
- * 1 MiB is generous headroom while preventing a `settings.write` principal from
67
- * pointing the field at a multi-gigabyte file (or `/dev/zero`-like stream that
68
- * `lstat` can't size cap on its own) and exfiltrating it through the wake hint.
69
- */
70
- const MAX_PROMPT_BYTES = 1 * 1024 * 1024;
71
-
72
61
  /**
73
62
  * Consolidation prompt — live-mode only. The agent runs as itself (full
74
63
  * SOUL.md + IDENTITY.md + persona + memory autoloads) with the standard
@@ -837,90 +826,32 @@ export function renderConsolidationPrompt(
837
826
  /**
838
827
  * Load the consolidation prompt template, optionally overridden from the file
839
828
  * referenced by `memory.v2.consolidation_prompt_path`, then substitute
840
- * `{{CUTOFF}}`. Path-resolution rules are documented on the schema field.
829
+ * `{{CUTOFF}}`. File loading (path resolution, size guard, and the permissive
830
+ * fall-back to the bundled prompt on a missing/unreadable/empty/oversized
831
+ * override) is handled by the shared {@link loadPromptOverride}.
841
832
  *
842
833
  * Override files get the same placeholder substitutions as the bundled
843
834
  * template: `{{CUTOFF}}` always, and `{{CORE_PAGES_SECTION}}` per the same
844
835
  * flag gate — so a prompt copied from the bundled source never leaks a raw
845
836
  * placeholder, and a customized prompt can opt into the managed section.
846
- *
847
- * Failure handling is intentionally permissive — missing file, read error, or
848
- * empty/whitespace-only body all log a warning and fall back to the bundled
849
- * prompt. Consolidation must never break because of a bad override: the
850
- * daemon's startup philosophy is "log and recover."
851
837
  */
852
838
  export function resolveConsolidationPrompt(
853
839
  overridePath: string | null,
854
840
  cutoff: string,
855
841
  options: ConsolidationPromptOptions,
856
842
  ): string {
857
- if (overridePath === null) return renderConsolidationPrompt(cutoff, options);
858
-
859
- const resolvedPath = resolveOverridePath(overridePath);
860
- let contents: string;
861
- try {
862
- const stat = lstatSync(resolvedPath);
863
- if (!stat.isFile()) {
864
- log.warn(
865
- {
866
- configuredPath: overridePath,
867
- resolvedPath,
868
- reason: "not_regular_file",
869
- fallback: "bundled",
870
- },
871
- "consolidation prompt override is not a regular file; using bundled prompt",
872
- );
873
- return renderConsolidationPrompt(cutoff, options);
874
- }
875
- if (stat.size > MAX_PROMPT_BYTES) {
876
- log.warn(
877
- {
878
- configuredPath: overridePath,
879
- resolvedPath,
880
- size: stat.size,
881
- limit: MAX_PROMPT_BYTES,
882
- reason: "oversized_override",
883
- fallback: "bundled",
884
- },
885
- "consolidation prompt override exceeds size limit; using bundled prompt",
886
- );
887
- return renderConsolidationPrompt(cutoff, options);
888
- }
889
- contents = readFileSync(resolvedPath, "utf-8");
890
- } catch (err) {
891
- const code = (err as NodeJS.ErrnoException).code;
892
- log.warn(
893
- { configuredPath: overridePath, resolvedPath, code, fallback: "bundled" },
894
- "consolidation prompt override unreadable; using bundled prompt",
895
- );
896
- return renderConsolidationPrompt(cutoff, options);
897
- }
898
-
899
- if (contents.trim().length === 0) {
900
- log.warn(
901
- {
902
- configuredPath: overridePath,
903
- resolvedPath,
904
- reason: "empty_override",
905
- fallback: "bundled",
906
- },
907
- "consolidation prompt override is empty; using bundled prompt",
908
- );
909
- return renderConsolidationPrompt(cutoff, options);
910
- }
911
-
912
- return contents
843
+ const override = loadPromptOverride({
844
+ overridePath,
845
+ workspaceDir: getWorkspaceDir(),
846
+ log,
847
+ label: "consolidation prompt",
848
+ });
849
+ if (override === null) return renderConsolidationPrompt(cutoff, options);
850
+
851
+ return override
913
852
  .replaceAll(CUTOFF_PLACEHOLDER, cutoff)
914
853
  .replaceAll(
915
854
  CORE_PAGES_PLACEHOLDER,
916
855
  options.includeCorePagesSection ? CORE_PAGES_CONSOLIDATION_SECTION : "",
917
856
  );
918
857
  }
919
-
920
- function resolveOverridePath(overridePath: string): string {
921
- if (overridePath.startsWith("~/")) {
922
- return join(homedir(), overridePath.slice(2));
923
- }
924
- if (isAbsolute(overridePath)) return overridePath;
925
- return join(getWorkspaceDir(), overridePath);
926
- }
@@ -22,22 +22,14 @@
22
22
  * same placeholder substitution applies to overrides.
23
23
  */
24
24
 
25
- import { lstatSync, readFileSync } from "node:fs";
26
- import { homedir } from "node:os";
27
- import { isAbsolute, join } from "node:path";
28
-
29
25
  import { getLogger } from "../../../util/logger.js";
26
+ import {
27
+ loadPromptOverride,
28
+ MAX_PROMPT_OVERRIDE_BYTES,
29
+ } from "../../prompt-override.js";
30
30
 
31
31
  const log = getLogger("memory-v2-router-prompt");
32
32
 
33
- /**
34
- * Hard upper bound on the override file size. The bundled prompt is well
35
- * under 4 KiB; 1 MiB is generous-enough for any reasonable hand-edit while
36
- * still preventing pathological inputs from being slurped into memory on
37
- * every router call.
38
- */
39
- const MAX_PROMPT_BYTES = 1 * 1024 * 1024;
40
-
41
33
  /** Sentinel substituted with the assistant's display name at runtime. */
42
34
  const ASSISTANT_NAME_PLACEHOLDER = "{{ASSISTANT_NAME}}";
43
35
 
@@ -102,14 +94,12 @@ export function renderRouterPrompt(opts: RenderRouterPromptOpts): string {
102
94
  /**
103
95
  * Load the router prompt template, optionally overridden from the file
104
96
  * referenced by `memory.v2.router.router_prompt_path`, then substitute the
105
- * standard placeholders. Path-resolution rules mirror the consolidation
106
- * prompt override: absolute paths used as-is, leading `~/` expanded to home,
107
- * relative paths resolved under `workspaceDir`.
97
+ * standard placeholders. File loading (path resolution, size guard, and the
98
+ * permissive fall-back to the bundled prompt on a missing/unreadable/empty/
99
+ * oversized override) is handled by the shared {@link loadPromptOverride}.
108
100
  *
109
- * Failure handling is intentionally permissive missing file, read error,
110
- * oversized file, or empty/whitespace-only body all log a warning and fall
111
- * back to the bundled prompt. Router selection must never break because of
112
- * a bad override.
101
+ * An `inlineOverride` (e.g. the simulator playground) takes precedence over the
102
+ * configured file path; same placeholder substitution and size guard apply.
113
103
  */
114
104
  export function resolveRouterPrompt(
115
105
  overridePath: string | null,
@@ -117,17 +107,15 @@ export function resolveRouterPrompt(
117
107
  opts: RenderRouterPromptOpts,
118
108
  inlineOverride?: string | null,
119
109
  ): string {
120
- // Inline override (e.g. simulator playground) takes precedence over the
121
- // configured file path and the bundled prompt. Same placeholder
122
- // substitution + size guard as the file-path branch; empty/whitespace
123
- // bodies fall through to file/bundled resolution so a "cleared" textarea
124
- // is treated as no override.
110
+ // Inline override takes precedence over the configured file path and the
111
+ // bundled prompt. Empty/whitespace bodies fall through to file/bundled
112
+ // resolution so a "cleared" textarea is treated as no override.
125
113
  if (inlineOverride !== undefined && inlineOverride !== null) {
126
- if (inlineOverride.length > MAX_PROMPT_BYTES) {
114
+ if (inlineOverride.length > MAX_PROMPT_OVERRIDE_BYTES) {
127
115
  log.warn(
128
116
  {
129
117
  size: inlineOverride.length,
130
- limit: MAX_PROMPT_BYTES,
118
+ limit: MAX_PROMPT_OVERRIDE_BYTES,
131
119
  reason: "oversized_inline_override",
132
120
  fallback: "path_or_bundled",
133
121
  },
@@ -138,62 +126,13 @@ export function resolveRouterPrompt(
138
126
  }
139
127
  }
140
128
 
141
- if (overridePath === null) return renderRouterPrompt(opts);
142
-
143
- const resolvedPath = resolveOverridePath(overridePath, workspaceDir);
144
- let contents: string;
145
- try {
146
- const stat = lstatSync(resolvedPath);
147
- if (!stat.isFile()) {
148
- log.warn(
149
- {
150
- configuredPath: overridePath,
151
- resolvedPath,
152
- reason: "not_regular_file",
153
- fallback: "bundled",
154
- },
155
- "router prompt override is not a regular file; using bundled prompt",
156
- );
157
- return renderRouterPrompt(opts);
158
- }
159
- if (stat.size > MAX_PROMPT_BYTES) {
160
- log.warn(
161
- {
162
- configuredPath: overridePath,
163
- resolvedPath,
164
- size: stat.size,
165
- limit: MAX_PROMPT_BYTES,
166
- reason: "oversized_override",
167
- fallback: "bundled",
168
- },
169
- "router prompt override exceeds size limit; using bundled prompt",
170
- );
171
- return renderRouterPrompt(opts);
172
- }
173
- contents = readFileSync(resolvedPath, "utf-8");
174
- } catch (err) {
175
- const code = (err as NodeJS.ErrnoException).code;
176
- log.warn(
177
- { configuredPath: overridePath, resolvedPath, code, fallback: "bundled" },
178
- "router prompt override unreadable; using bundled prompt",
179
- );
180
- return renderRouterPrompt(opts);
181
- }
182
-
183
- if (contents.trim().length === 0) {
184
- log.warn(
185
- {
186
- configuredPath: overridePath,
187
- resolvedPath,
188
- reason: "empty_override",
189
- fallback: "bundled",
190
- },
191
- "router prompt override is empty; using bundled prompt",
192
- );
193
- return renderRouterPrompt(opts);
194
- }
195
-
196
- return substitutePlaceholders(contents, opts);
129
+ const override = loadPromptOverride({
130
+ overridePath,
131
+ workspaceDir,
132
+ log,
133
+ label: "router prompt",
134
+ });
135
+ return substitutePlaceholders(override ?? ROUTER_PROMPT, opts);
197
136
  }
198
137
 
199
138
  function substitutePlaceholders(
@@ -207,14 +146,3 @@ function substitutePlaceholders(
207
146
  .replaceAll(USER_NAME_PLACEHOLDER, () => user)
208
147
  .replaceAll(PAGE_INDEX_PLACEHOLDER, () => opts.pageIndexBlock);
209
148
  }
210
-
211
- function resolveOverridePath(
212
- overridePath: string,
213
- workspaceDir: string,
214
- ): string {
215
- if (overridePath.startsWith("~/")) {
216
- return join(homedir(), overridePath.slice(2));
217
- }
218
- if (isAbsolute(overridePath)) return overridePath;
219
- return join(workspaceDir, overridePath);
220
- }