@vellumai/assistant 0.8.8 → 0.8.9-staging.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 (360) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/bun.lock +2 -2
  3. package/examples/plugins/echo/README.md +61 -60
  4. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  5. package/examples/plugins/echo/hooks/stop.ts +16 -0
  6. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  7. package/examples/plugins/echo/package.json +1 -2
  8. package/examples/plugins/echo/src/emit.ts +19 -0
  9. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  10. package/openapi.yaml +235 -6
  11. package/package.json +2 -2
  12. package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
  13. package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
  14. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
  15. package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
  16. package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
  17. package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
  18. package/src/__tests__/agent-loop-thinking.test.ts +36 -20
  19. package/src/__tests__/agent-loop.test.ts +441 -96
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
  21. package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
  22. package/src/__tests__/anthropic-provider.test.ts +1 -1
  23. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  24. package/src/__tests__/app-control-flow.test.ts +1 -1
  25. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  26. package/src/__tests__/app-executors.test.ts +132 -0
  27. package/src/__tests__/approval-cascade.test.ts +5 -4
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
  30. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  31. package/src/__tests__/channel-approvals.test.ts +1 -1
  32. package/src/__tests__/compaction-circuit.test.ts +258 -0
  33. package/src/__tests__/compaction-direct.test.ts +132 -0
  34. package/src/__tests__/compaction-events.test.ts +5 -5
  35. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  36. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  37. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
  38. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
  39. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
  40. package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
  41. package/src/__tests__/conversation-agent-loop.test.ts +297 -586
  42. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  43. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
  44. package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
  45. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  46. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
  47. package/src/__tests__/conversation-process-callsite.test.ts +14 -14
  48. package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
  49. package/src/__tests__/conversation-queue.test.ts +9 -9
  50. package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
  51. package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
  52. package/src/__tests__/conversation-slash-queue.test.ts +6 -4
  53. package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
  54. package/src/__tests__/conversation-speed-override.test.ts +10 -9
  55. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  56. package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
  57. package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
  59. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  60. package/src/__tests__/credential-security-invariants.test.ts +0 -1
  61. package/src/__tests__/db-acp-history.test.ts +101 -0
  62. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  63. package/src/__tests__/empty-response-hook.test.ts +1 -1
  64. package/src/__tests__/file-write-tool.test.ts +63 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  66. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  67. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  68. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  69. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/heartbeat-service.test.ts +1 -0
  71. package/src/__tests__/history-repair-hook.test.ts +1 -1
  72. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  73. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  74. package/src/__tests__/inference-profile-reaper.test.ts +62 -0
  75. package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
  76. package/src/__tests__/injector-background-turn.test.ts +13 -23
  77. package/src/__tests__/injector-chain.test.ts +268 -44
  78. package/src/__tests__/injector-disk-pressure.test.ts +210 -52
  79. package/src/__tests__/injector-document-comments.test.ts +97 -114
  80. package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
  81. package/src/__tests__/injector-v3-suppression.test.ts +4 -4
  82. package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
  83. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  84. package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
  85. package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
  86. package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
  87. package/src/__tests__/plugin-api-shim.test.ts +6 -9
  88. package/src/__tests__/plugin-bootstrap.test.ts +12 -23
  89. package/src/__tests__/plugin-registry.test.ts +3 -49
  90. package/src/__tests__/plugin-types.test.ts +0 -70
  91. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  92. package/src/__tests__/reaction-persistence.test.ts +1 -1
  93. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  94. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  95. package/src/__tests__/steer-tool-repair.test.ts +1 -1
  96. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  97. package/src/__tests__/subagent-detail.test.ts +25 -7
  98. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  99. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  100. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  101. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  102. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  103. package/src/__tests__/title-generate-hook.test.ts +1 -1
  104. package/src/__tests__/tool-error-hook.test.ts +1 -1
  105. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
  106. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  107. package/src/acp/__tests__/agent-process.test.ts +161 -0
  108. package/src/acp/__tests__/client-handler.test.ts +40 -0
  109. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  110. package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
  111. package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
  112. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  113. package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
  114. package/src/acp/agent-process.ts +61 -1
  115. package/src/acp/auto-install.test.ts +280 -0
  116. package/src/acp/auto-install.ts +232 -0
  117. package/src/acp/client-handler.ts +31 -0
  118. package/src/acp/feature-gate.test.ts +48 -0
  119. package/src/acp/feature-gate.ts +34 -0
  120. package/src/acp/prepare-agent-env.ts +80 -27
  121. package/src/acp/resolve-agent.test.ts +225 -9
  122. package/src/acp/resolve-agent.ts +122 -17
  123. package/src/acp/resume-hint.ts +23 -0
  124. package/src/acp/session-manager.ts +507 -73
  125. package/src/agent/compaction-circuit.ts +60 -102
  126. package/src/agent/loop.ts +414 -248
  127. package/src/api/responses/conversation-message.ts +14 -1
  128. package/src/approvals/guardian-request-resolvers.ts +1 -1
  129. package/src/background-wake/next-wake.ts +1 -0
  130. package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
  131. package/src/cli/commands/plugins.ts +43 -37
  132. package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
  133. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
  134. package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
  135. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
  136. package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
  137. package/src/cli/lib/install-from-github.ts +464 -55
  138. package/src/cli/lib/plugin-catalog-cache.ts +84 -0
  139. package/src/cli/lib/plugin-details.ts +409 -0
  140. package/src/cli/lib/plugin-marketplace.ts +197 -0
  141. package/src/cli/lib/search-plugins.ts +195 -29
  142. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  143. package/src/config/acp-defaults.test.ts +10 -0
  144. package/src/config/acp-defaults.ts +6 -0
  145. package/src/config/bundled-skills/acp/SKILL.md +85 -33
  146. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  147. package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
  148. package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
  149. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  150. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  151. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  152. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  153. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  154. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  155. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  156. package/src/config/bundled-tool-registry.ts +4 -0
  157. package/src/config/call-site-defaults.ts +0 -2
  158. package/src/config/feature-flag-registry.json +15 -6
  159. package/src/config/schemas/call-site-catalog.ts +0 -14
  160. package/src/config/schemas/heartbeat.ts +9 -0
  161. package/src/config/schemas/llm.ts +0 -2
  162. package/src/context/compactor.ts +22 -4
  163. package/src/context/strip-injections.ts +8 -2
  164. package/src/context/window-manager.ts +27 -13
  165. package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
  166. package/src/daemon/conversation-agent-loop.ts +175 -1055
  167. package/src/daemon/conversation-lifecycle.ts +11 -255
  168. package/src/daemon/conversation-process.ts +8 -136
  169. package/src/daemon/conversation-registry.ts +159 -0
  170. package/src/daemon/conversation-runtime-assembly.ts +293 -392
  171. package/src/daemon/conversation-store.ts +9 -90
  172. package/src/daemon/conversation-surfaces.ts +24 -8
  173. package/src/daemon/conversation-workspace.ts +17 -0
  174. package/src/daemon/conversation.ts +404 -57
  175. package/src/daemon/disk-pressure-policy.ts +0 -1
  176. package/src/daemon/external-plugins-bootstrap.ts +14 -19
  177. package/src/daemon/handlers/conversations.ts +3 -1
  178. package/src/daemon/handlers/skills.ts +4 -1
  179. package/src/daemon/host-proxy-preactivation.ts +1 -3
  180. package/src/daemon/lifecycle.ts +21 -7
  181. package/src/daemon/server.ts +2 -0
  182. package/src/daemon/wake-conversation-ops.ts +269 -0
  183. package/src/embedded/plugin-api.ts +2 -2
  184. package/src/export/__tests__/transcript-formatter.test.ts +5 -0
  185. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  186. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  187. package/src/heartbeat/heartbeat-service.ts +26 -0
  188. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  189. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  190. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  191. package/src/ipc/skill-routes/memory.ts +4 -2
  192. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
  193. package/src/memory/conversation-crud.ts +29 -19
  194. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  195. package/src/memory/db-init.ts +2 -0
  196. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  197. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
  198. package/src/memory/jobs-worker.ts +25 -1
  199. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  200. package/src/memory/migrations/index.ts +1 -0
  201. package/src/memory/schema/acp.ts +4 -0
  202. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  203. package/src/memory/v2/consolidation-job.ts +13 -4
  204. package/src/plugin-api/constants.ts +4 -0
  205. package/src/plugin-api/index.ts +6 -5
  206. package/src/plugin-api/types.ts +75 -0
  207. package/src/plugins/defaults/compaction/compact.ts +59 -0
  208. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  209. package/src/plugins/defaults/compaction/manager-store.ts +57 -0
  210. package/src/plugins/defaults/compaction/package.json +1 -2
  211. package/src/plugins/defaults/empty-response/package.json +0 -1
  212. package/src/plugins/defaults/history-repair/package.json +0 -1
  213. package/src/plugins/defaults/index.ts +135 -26
  214. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
  215. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
  216. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  217. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
  218. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
  219. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  220. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
  221. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  222. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
  223. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  224. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  225. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
  226. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  227. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
  228. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
  229. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
  230. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
  231. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  235. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
  236. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
  237. package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
  238. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  239. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
  240. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
  241. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  242. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
  243. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  244. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
  245. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
  246. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  247. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
  248. package/src/plugins/defaults/title-generate/package.json +0 -1
  249. package/src/plugins/defaults/tool-error/package.json +0 -1
  250. package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
  251. package/src/plugins/pipeline.ts +6 -293
  252. package/src/plugins/registry.ts +9 -37
  253. package/src/plugins/types.ts +76 -381
  254. package/src/plugins/user-loader.ts +30 -127
  255. package/src/prompts/__tests__/system-prompt.test.ts +6 -0
  256. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
  257. package/src/runtime/__tests__/agent-wake.test.ts +555 -691
  258. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  259. package/src/runtime/agent-wake.ts +108 -209
  260. package/src/runtime/assistant-event-hub.ts +1 -1
  261. package/src/runtime/channel-approvals.ts +1 -1
  262. package/src/runtime/interactive-ui.ts +1 -1
  263. package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
  264. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  265. package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
  266. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  267. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  268. package/src/runtime/routes/acp-routes.test.ts +89 -25
  269. package/src/runtime/routes/acp-routes.ts +81 -29
  270. package/src/runtime/routes/approval-routes.ts +1 -1
  271. package/src/runtime/routes/browser-routes.ts +1 -1
  272. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  273. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  274. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  275. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  276. package/src/runtime/routes/conversation-routes.ts +28 -2
  277. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  278. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  279. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  280. package/src/runtime/routes/host-cu-routes.ts +1 -1
  281. package/src/runtime/routes/identity-routes.ts +18 -3
  282. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  283. package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
  284. package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
  285. package/src/runtime/routes/memory-v3-routes.ts +16 -6
  286. package/src/runtime/routes/playground/helpers.ts +1 -1
  287. package/src/runtime/routes/plugins-routes.ts +337 -35
  288. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  289. package/src/runtime/routes/work-items-routes.ts +2 -4
  290. package/src/runtime/services/conversation-serializer.ts +1 -1
  291. package/src/signals/cancel.ts +2 -4
  292. package/src/subagent/manager.ts +21 -5
  293. package/src/tools/acp/context.ts +20 -0
  294. package/src/tools/acp/list-agents.test.ts +8 -2
  295. package/src/tools/acp/spawn.test.ts +176 -195
  296. package/src/tools/acp/spawn.ts +37 -172
  297. package/src/tools/acp/steer.test.ts +105 -8
  298. package/src/tools/acp/steer.ts +48 -17
  299. package/src/tools/apps/executors.ts +166 -50
  300. package/src/tools/filesystem/write.ts +34 -0
  301. package/src/tools/subagent/spawn.ts +2 -4
  302. package/src/tools/ui-surface/definitions.ts +25 -5
  303. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  304. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
  305. package/docs/plugins.md +0 -832
  306. package/examples/plugins/echo/register.ts +0 -143
  307. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  308. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  309. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  310. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
  311. package/src/__tests__/pipeline-runner.test.ts +0 -554
  312. package/src/__tests__/plugin-external-api.test.ts +0 -68
  313. package/src/daemon/wake-target-adapter.ts +0 -253
  314. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  315. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  316. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  317. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  318. package/src/plugins/defaults/compaction/register.ts +0 -35
  319. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  320. package/src/plugins/defaults/empty-response/register.ts +0 -23
  321. package/src/plugins/defaults/history-repair/register.ts +0 -24
  322. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  323. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  324. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  325. package/src/plugins/defaults/title-generate/register.ts +0 -35
  326. package/src/plugins/defaults/tool-error/register.ts +0 -23
  327. package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
  328. package/src/plugins/external-api.ts +0 -104
  329. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  330. package/src/proactive-artifact/decision.test.ts +0 -226
  331. package/src/proactive-artifact/decision.ts +0 -165
  332. package/src/proactive-artifact/index.ts +0 -7
  333. package/src/proactive-artifact/job.test.ts +0 -962
  334. package/src/proactive-artifact/job.ts +0 -372
  335. package/src/proactive-artifact/message-copy.ts +0 -58
  336. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  337. package/src/proactive-artifact/trigger-state.ts +0 -123
  338. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  339. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  340. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  341. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  342. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
  343. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  344. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
  345. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  346. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  347. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  348. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  349. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  350. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  351. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  352. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  353. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  354. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  355. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  356. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  357. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
  358. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  359. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  360. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tests for the ACP feature gate.
3
+ *
4
+ * `isAcpEnabled` is an OR of the `acp` feature flag and the legacy
5
+ * `config.acp.enabled` field: either switch enables the subsystem, and
6
+ * neither implicitly flips the other.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
10
+
11
+ import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
12
+ import type { AssistantConfig } from "../config/schema.js";
13
+ import { ACP_FLAG_KEY, isAcpEnabled } from "./feature-gate.js";
14
+
15
+ beforeEach(() => {
16
+ setOverridesForTesting({});
17
+ });
18
+
19
+ afterEach(() => {
20
+ setOverridesForTesting({});
21
+ });
22
+
23
+ /** Minimal AssistantConfig carrying only the `acp` section the gate reads. */
24
+ function makeConfig(acpEnabled: boolean): AssistantConfig {
25
+ return {
26
+ acp: { enabled: acpEnabled, maxConcurrentSessions: 4, agents: {} },
27
+ } as AssistantConfig;
28
+ }
29
+
30
+ describe("isAcpEnabled", () => {
31
+ test("returns false when both the flag and config.acp.enabled are off", () => {
32
+ expect(isAcpEnabled(makeConfig(false))).toBe(false);
33
+ });
34
+
35
+ test("returns true when config.acp.enabled is true and the flag is off", () => {
36
+ expect(isAcpEnabled(makeConfig(true))).toBe(true);
37
+ });
38
+
39
+ test("returns true when the flag is on and config.acp.enabled is false", () => {
40
+ setOverridesForTesting({ [ACP_FLAG_KEY]: true });
41
+ expect(isAcpEnabled(makeConfig(false))).toBe(true);
42
+ });
43
+
44
+ test("explicit flag-off override does not defeat config.acp.enabled", () => {
45
+ setOverridesForTesting({ [ACP_FLAG_KEY]: false });
46
+ expect(isAcpEnabled(makeConfig(true))).toBe(true);
47
+ });
48
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) feature gate.
3
+ *
4
+ * Single source of truth for whether the ACP subsystem is enabled. Modeled
5
+ * on `credential-execution/feature-gates.ts`: the flag key is declared in
6
+ * `meta/feature-flags/feature-flag-registry.json` and resolved through the
7
+ * unified feature-flag resolver.
8
+ *
9
+ * The gate is an OR of two independent switches:
10
+ * 1. `config.acp.enabled` — the original workspace config field. Preserved
11
+ * so existing workspaces with `acp.enabled: true` keep working without
12
+ * any migration.
13
+ * 2. The `acp` feature flag — adds a UI toggle. Flag state is persisted by
14
+ * the gateway and hot-refreshed in the daemon via the
15
+ * `feature_flags_changed` SSE event, so toggling it takes effect
16
+ * without a restart or config-file edit.
17
+ */
18
+
19
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
20
+ import type { AssistantConfig } from "../config/schema.js";
21
+
22
+ /** Gate for the ACP coding-agent subsystem (must match the registry). */
23
+ export const ACP_FLAG_KEY = "acp" as const;
24
+
25
+ /**
26
+ * Whether ACP agent spawning/steering is enabled, via either the legacy
27
+ * `acp.enabled` config field or the `acp` feature flag (see module doc).
28
+ */
29
+ export function isAcpEnabled(config: AssistantConfig): boolean {
30
+ return (
31
+ config.acp?.enabled === true ||
32
+ isAssistantFeatureFlagEnabled(ACP_FLAG_KEY, config)
33
+ );
34
+ }
@@ -28,48 +28,82 @@ import {
28
28
  getCredentialMetadata,
29
29
  upsertCredentialMetadata,
30
30
  } from "../tools/credentials/metadata-store.js";
31
+ import { getLogger } from "../util/logger.js";
31
32
  import type { AcpAgentConfig } from "./types.js";
32
33
 
34
+ const log = getLogger("acp:prepare-agent-env");
35
+
33
36
  const ACP_SPAWN_TOOL = "acp_spawn";
37
+ const ACP_SERVICE = "acp";
34
38
 
35
39
  /**
36
- * Ensure the `acp/claude_oauth_token` credential has metadata that allows
37
- * the `acp_spawn` tool to read it, but only for legacy/unmanaged cases:
40
+ * Ensure an `acp/<field>` credential has metadata that allows the
41
+ * `acp_spawn` tool to read it, but only for legacy/unmanaged cases:
38
42
  *
39
- * - No metadata at all create with `allowedTools: ["acp_spawn"]`.
40
- * - Metadata exists with an empty `allowedTools` default provisioning
43
+ * - No metadata at all: create with `allowedTools: ["acp_spawn"]`.
44
+ * - Metadata exists with an empty `allowedTools`: default provisioning
41
45
  * path (user ran `credentials set` without `--allowed-tools`), add it.
42
- * - Metadata exists with a non-empty `allowedTools` explicit policy set
43
- * by the user/admin. Respect it even if `acp_spawn` is absent the
44
- * broker will deny the read and the preflight will throw.
46
+ * - Metadata exists with a non-empty `allowedTools`: explicit policy set
47
+ * by the user/admin. Respect it even if `acp_spawn` is absent; the
48
+ * broker will deny the read and the caller decides whether that's fatal.
45
49
  */
46
- function ensureAcpTokenPolicy(): void {
47
- const meta = getCredentialMetadata("acp", "claude_oauth_token");
50
+ function ensureAcpCredentialPolicy(
51
+ field: string,
52
+ usageDescription: string,
53
+ ): void {
54
+ const meta = getCredentialMetadata(ACP_SERVICE, field);
48
55
  if (!meta) {
49
- upsertCredentialMetadata("acp", "claude_oauth_token", {
56
+ upsertCredentialMetadata(ACP_SERVICE, field, {
50
57
  allowedTools: [ACP_SPAWN_TOOL],
51
- usageDescription:
52
- "Claude OAuth token for ACP agent authentication",
58
+ usageDescription,
53
59
  });
54
60
  return;
55
61
  }
56
62
  const tools = meta.allowedTools ?? [];
57
63
  if (tools.length === 0) {
58
- upsertCredentialMetadata("acp", "claude_oauth_token", {
64
+ upsertCredentialMetadata(ACP_SERVICE, field, {
59
65
  allowedTools: [ACP_SPAWN_TOOL],
60
66
  });
61
67
  }
62
68
  }
63
69
 
70
+ /**
71
+ * Read an `acp/<field>` credential through the broker and inject it into
72
+ * `env` under `envVar`. Returns the broker's failure reason when the value
73
+ * was not injected (missing credential, denied policy, no stored value),
74
+ * or undefined on success. Never throws: `serverUse` signals every failure
75
+ * mode, including a simply-absent credential, as `{ success: false,
76
+ * reason }`, so callers choose whether a miss is fatal.
77
+ */
78
+ async function injectCredential(
79
+ env: Record<string, string>,
80
+ field: string,
81
+ envVar: string,
82
+ usageDescription: string,
83
+ ): Promise<string | undefined> {
84
+ ensureAcpCredentialPolicy(field, usageDescription);
85
+ const result = await credentialBroker.serverUse<void>({
86
+ service: ACP_SERVICE,
87
+ field,
88
+ toolName: ACP_SPAWN_TOOL,
89
+ execute: async (value) => {
90
+ env[envVar] = value;
91
+ },
92
+ });
93
+ return result.success ? undefined : result.reason;
94
+ }
95
+
64
96
  /**
65
97
  * Returns a NEW config with any required credentials merged into `env`.
66
98
  * Does NOT mutate the input. Throws `FailedDependencyError` if a required
67
99
  * credential is missing from both the user-supplied env override and the
68
100
  * secure store.
69
101
  *
70
- * Gating is keyed off the resolved agent COMMAND (basename), not the
71
- * user-facing agent id, so a custom `acp.agents.my-claude = { command:
72
- * "claude-agent-acp", ... }` alias still gets the env it needs.
102
+ * Gating is keyed off the resolved command basename, not the user-facing
103
+ * agent id. A custom `acp.agents.my-claude = { command: "claude-agent-acp",
104
+ * ... }` alias (or a full path like `/opt/bin/claude-agent-acp`) still gets
105
+ * the env it needs. Because resolution always yields the real adapter binary
106
+ * (never a `bun x` wrapper), the basename is the canonical adapter identity.
73
107
  *
74
108
  * For `claude-agent-acp` the only required env var is
75
109
  * `CLAUDE_CODE_OAUTH_TOKEN`. Two provisioning routes converge on it, with
@@ -84,6 +118,11 @@ function ensureAcpTokenPolicy(): void {
84
118
  * before spawning. The "fail-fast" throw is symmetric with the existing
85
119
  * `binary_not_found` preflight in `resolveAcpAgent` and strictly better
86
120
  * than a `warn` + zombie subprocess 10 seconds later.
121
+ *
122
+ * For `gemini` the env var is `GEMINI_API_KEY`, provisioned the same two
123
+ * ways (config.json override wins, vault field `gemini_api_key` second),
124
+ * but it is OPTIONAL: the Gemini CLI supports its own OAuth login, so a
125
+ * vault miss proceeds without the key instead of failing the spawn.
87
126
  */
88
127
  export async function prepareAgentEnv(
89
128
  agentConfig: AcpAgentConfig,
@@ -92,19 +131,16 @@ export async function prepareAgentEnv(
92
131
  // agent reference. The local `env` binding sidesteps TS narrowing
93
132
  // limitations on the optional `AcpAgentConfig.env` field.
94
133
  const env: Record<string, string> = { ...(agentConfig.env ?? {}) };
95
- const commandBasename = basename(agentConfig.command);
134
+ const adapterCommand = basename(agentConfig.command);
96
135
 
97
- if (commandBasename === "claude-agent-acp") {
136
+ if (adapterCommand === "claude-agent-acp") {
98
137
  if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
99
- ensureAcpTokenPolicy();
100
- await credentialBroker.serverUse<void>({
101
- service: "acp",
102
- field: "claude_oauth_token",
103
- toolName: ACP_SPAWN_TOOL,
104
- execute: async (token) => {
105
- env.CLAUDE_CODE_OAUTH_TOKEN = token;
106
- },
107
- });
138
+ await injectCredential(
139
+ env,
140
+ "claude_oauth_token",
141
+ "CLAUDE_CODE_OAUTH_TOKEN",
142
+ "Claude OAuth token for ACP agent authentication",
143
+ );
108
144
  }
109
145
  if (!env.CLAUDE_CODE_OAUTH_TOKEN) {
110
146
  throw new FailedDependencyError(
@@ -113,6 +149,23 @@ export async function prepareAgentEnv(
113
149
  "(or set it under acp.agents.<id>.env in config.json).",
114
150
  );
115
151
  }
152
+ } else if (adapterCommand === "gemini") {
153
+ if (!env.GEMINI_API_KEY) {
154
+ const missReason = await injectCredential(
155
+ env,
156
+ "gemini_api_key",
157
+ "GEMINI_API_KEY",
158
+ "Gemini API key for ACP agent authentication",
159
+ );
160
+ if (missReason !== undefined) {
161
+ // Optional credential: Gemini CLI can authenticate via its own
162
+ // OAuth login, so a vault miss must not fail the spawn.
163
+ log.debug(
164
+ { reason: missReason },
165
+ "Gemini API key unavailable from the vault; spawning without GEMINI_API_KEY",
166
+ );
167
+ }
168
+ }
116
169
  }
117
170
 
118
171
  return { ...agentConfig, env };
@@ -1,19 +1,25 @@
1
1
  import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import { setOverridesForTesting } from "../__tests__/feature-flag-test-helpers.js";
3
4
  import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
4
5
  import { installWhichStub } from "./__tests__/helpers/which-stub.js";
6
+ import { ACP_FLAG_KEY } from "./feature-gate.js";
5
7
 
6
8
  const config = await installAcpConfigStub();
7
9
  const which = installWhichStub();
8
10
 
9
11
  afterAll(() => {
10
12
  which.restore();
13
+ setOverridesForTesting({});
11
14
  });
12
15
 
13
16
  const { resolveAcpAgent, listAcpAgents } = await import("./resolve-agent.js");
14
17
 
15
18
  beforeEach(() => {
16
19
  config.setConfig({});
20
+ // Default: no flag overrides, so the `acp` flag falls back to its registry
21
+ // default (false) and enablement comes from the config stub alone.
22
+ setOverridesForTesting({});
17
23
  // Default: every command on PATH so binary preflight passes unless a test
18
24
  // explicitly says otherwise.
19
25
  which.setWhich((cmd) => `/usr/local/bin/${cmd}`);
@@ -24,7 +30,7 @@ beforeEach(() => {
24
30
  // ---------------------------------------------------------------------------
25
31
 
26
32
  describe("resolveAcpAgent", () => {
27
- test("returns acp_disabled when config.acp.enabled is false", () => {
33
+ test("returns acp_disabled when both the feature flag and config.acp.enabled are off", () => {
28
34
  config.setConfig({ enabled: false });
29
35
 
30
36
  const result = resolveAcpAgent("claude");
@@ -33,10 +39,23 @@ describe("resolveAcpAgent", () => {
33
39
  if (result.ok) return;
34
40
  expect(result.reason).toBe("acp_disabled");
35
41
  if (result.reason !== "acp_disabled") return;
42
+ expect(result.hint).toContain("ACP Coding Agents");
43
+ expect(result.hint).toContain("feature flag");
36
44
  expect(result.hint).toContain("acp.enabled");
37
45
  expect(result.hint).toContain("config.json");
38
46
  });
39
47
 
48
+ test("resolution proceeds when the acp feature flag is on and config.acp.enabled is false", () => {
49
+ config.setConfig({ enabled: false });
50
+ setOverridesForTesting({ [ACP_FLAG_KEY]: true });
51
+
52
+ const result = resolveAcpAgent("claude");
53
+
54
+ expect(result.ok).toBe(true);
55
+ if (!result.ok) return;
56
+ expect(result.agent.command).toBe("claude-agent-acp");
57
+ });
58
+
40
59
  test("user config wins over default profile", () => {
41
60
  config.setConfig({
42
61
  agents: {
@@ -78,6 +97,79 @@ describe("resolveAcpAgent", () => {
78
97
  expect(result.agent.command).toBe("claude-agent-acp");
79
98
  });
80
99
 
100
+ test("falls back to the gemini default profile when no user entry", () => {
101
+ config.setConfig({ agents: {} });
102
+
103
+ const result = resolveAcpAgent("gemini");
104
+
105
+ expect(result.ok).toBe(true);
106
+ if (!result.ok) return;
107
+ expect(result.agent.command).toBe("gemini");
108
+ expect(result.agent.args).toEqual(["--acp"]);
109
+ });
110
+
111
+ test.each([
112
+ ["claude code", "claude-agent-acp"],
113
+ ["Claude Code", "claude-agent-acp"],
114
+ ["claude-code", "claude-agent-acp"],
115
+ ["claude_code", "claude-agent-acp"],
116
+ ["codex cli", "codex-acp"],
117
+ ["OpenAI Codex", "codex-acp"],
118
+ ["gemini cli", "gemini"],
119
+ ["Gemini CLI", "gemini"],
120
+ ["google gemini", "gemini"],
121
+ ])("alias %p resolves to the %p profile", (alias, command) => {
122
+ config.setConfig({ agents: {} });
123
+
124
+ const result = resolveAcpAgent(alias);
125
+
126
+ expect(result.ok).toBe(true);
127
+ if (!result.ok) return;
128
+ expect(result.agent.command).toBe(command);
129
+ });
130
+
131
+ test("user config entry literally keyed 'claude code' beats the alias", () => {
132
+ config.setConfig({
133
+ agents: {
134
+ "claude code": {
135
+ command: "my-claude-fork",
136
+ args: [],
137
+ description: "user-defined agent that happens to share an alias",
138
+ },
139
+ },
140
+ });
141
+
142
+ const result = resolveAcpAgent("claude code");
143
+
144
+ expect(result.ok).toBe(true);
145
+ if (!result.ok) return;
146
+ expect(result.agent.command).toBe("my-claude-fork");
147
+ });
148
+
149
+ test("alias re-runs the normal lookup, so a user override of the canonical id wins", () => {
150
+ config.setConfig({
151
+ agents: {
152
+ claude: { command: "my-custom-claude", args: [] },
153
+ },
154
+ });
155
+
156
+ const result = resolveAcpAgent("claude code");
157
+
158
+ expect(result.ok).toBe(true);
159
+ if (!result.ok) return;
160
+ expect(result.agent.command).toBe("my-custom-claude");
161
+ });
162
+
163
+ test("non-alias unknown id still returns unknown_agent", () => {
164
+ config.setConfig({ agents: {} });
165
+
166
+ const result = resolveAcpAgent("cursor cli");
167
+
168
+ expect(result.ok).toBe(false);
169
+ if (result.ok) return;
170
+ expect(result.reason).toBe("unknown_agent");
171
+ });
172
+
81
173
  test("returns unknown_agent with merged available list when id not found", () => {
82
174
  config.setConfig({
83
175
  agents: {
@@ -92,7 +184,12 @@ describe("resolveAcpAgent", () => {
92
184
  expect(result.reason).toBe("unknown_agent");
93
185
  if (result.reason !== "unknown_agent") return;
94
186
  // Defaults plus user-only ids, deduped, in stable order (defaults first).
95
- expect(result.available).toEqual(["claude", "codex", "user-only"]);
187
+ expect(result.available).toEqual([
188
+ "claude",
189
+ "codex",
190
+ "gemini",
191
+ "user-only",
192
+ ]);
96
193
  });
97
194
 
98
195
  test("unknown_agent available list contains both defaults when user config is empty", () => {
@@ -106,6 +203,7 @@ describe("resolveAcpAgent", () => {
106
203
  if (result.reason !== "unknown_agent") return;
107
204
  expect(result.available).toContain("claude");
108
205
  expect(result.available).toContain("codex");
206
+ expect(result.available).toContain("gemini");
109
207
  });
110
208
 
111
209
  test("returns binary_not_found with the registered install hint", () => {
@@ -118,7 +216,7 @@ describe("resolveAcpAgent", () => {
118
216
  if (result.ok) return;
119
217
  expect(result.reason).toBe("binary_not_found");
120
218
  if (result.reason !== "binary_not_found") return;
121
- expect(result.hint).toBe("npm i -g @agentclientprotocol/claude-agent-acp");
219
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
122
220
  expect(result.command).toBe("claude-agent-acp");
123
221
  });
124
222
 
@@ -158,7 +256,7 @@ describe("resolveAcpAgent", () => {
158
256
  if (result.ok) return;
159
257
  expect(result.reason).toBe("binary_not_found");
160
258
  if (result.reason !== "binary_not_found") return;
161
- expect(result.hint).toBe("npm i -g @zed-industries/codex-acp");
259
+ expect(result.hint).toBe("bun add -g @zed-industries/codex-acp");
162
260
  });
163
261
 
164
262
  test("binary preflight honors agent.env.PATH override (matches spawn env)", () => {
@@ -200,6 +298,60 @@ describe("resolveAcpAgent", () => {
200
298
  if (!result.ok) return;
201
299
  expect(result.agent.args).toEqual(["--verbose"]);
202
300
  });
301
+
302
+ test("resolves a full-path command directly without rewriting it", () => {
303
+ config.setConfig({
304
+ agents: {
305
+ custom: { command: "/opt/bin/claude-agent-acp", args: [] },
306
+ },
307
+ });
308
+
309
+ const direct = resolveAcpAgent("claude");
310
+ expect(direct.ok).toBe(true);
311
+ if (!direct.ok) return;
312
+ expect(direct.agent.command).toBe("claude-agent-acp");
313
+
314
+ const fullPath = resolveAcpAgent("custom");
315
+ expect(fullPath.ok).toBe(true);
316
+ if (!fullPath.ok) return;
317
+ expect(fullPath.agent.command).toBe("/opt/bin/claude-agent-acp");
318
+ });
319
+ });
320
+
321
+ // ---------------------------------------------------------------------------
322
+ // resolveAcpAgent - missing binaries are never run from the task cwd
323
+ // ---------------------------------------------------------------------------
324
+
325
+ describe("resolveAcpAgent - missing binary", () => {
326
+ test("binary missing + bun present: still binary_not_found (no bunx rewrite)", () => {
327
+ // bun on PATH no longer makes a missing adapter "runnable" at resolve
328
+ // time: the sandboxed install happens separately, never as a `bun x`
329
+ // rewrite in the untrusted cwd.
330
+ config.setConfig({ agents: {} });
331
+ which.setWhich({ bun: "/usr/local/bin/bun" });
332
+
333
+ const result = resolveAcpAgent("claude");
334
+
335
+ expect(result.ok).toBe(false);
336
+ if (result.ok) return;
337
+ expect(result.reason).toBe("binary_not_found");
338
+ if (result.reason !== "binary_not_found") return;
339
+ expect(result.command).toBe("claude-agent-acp");
340
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
341
+ });
342
+
343
+ test("binary missing + bun missing: binary_not_found with the bun hint", () => {
344
+ config.setConfig({ agents: {} });
345
+ which.setWhich({});
346
+
347
+ const result = resolveAcpAgent("claude");
348
+
349
+ expect(result.ok).toBe(false);
350
+ if (result.ok) return;
351
+ expect(result.reason).toBe("binary_not_found");
352
+ if (result.reason !== "binary_not_found") return;
353
+ expect(result.hint).toBe("bun add -g @agentclientprotocol/claude-agent-acp");
354
+ });
203
355
  });
204
356
 
205
357
  // ---------------------------------------------------------------------------
@@ -207,7 +359,7 @@ describe("resolveAcpAgent", () => {
207
359
  // ---------------------------------------------------------------------------
208
360
 
209
361
  describe("listAcpAgents", () => {
210
- test("returns enabled: false with empty agents when ACP is disabled", () => {
362
+ test("returns enabled: false with empty agents when both the flag and config are off", () => {
211
363
  config.setConfig({ enabled: false });
212
364
 
213
365
  const result = listAcpAgents();
@@ -216,14 +368,28 @@ describe("listAcpAgents", () => {
216
368
  expect(result.agents).toEqual([]);
217
369
  });
218
370
 
219
- test("includes both bundled defaults when user config is empty", () => {
371
+ test("returns the catalog when the acp feature flag is on and config.acp.enabled is false", () => {
372
+ config.setConfig({ enabled: false });
373
+ setOverridesForTesting({ [ACP_FLAG_KEY]: true });
374
+
375
+ const result = listAcpAgents();
376
+
377
+ expect(result.enabled).toBe(true);
378
+ expect(result.agents.map((a) => a.id)).toEqual([
379
+ "claude",
380
+ "codex",
381
+ "gemini",
382
+ ]);
383
+ });
384
+
385
+ test("includes all bundled defaults when user config is empty", () => {
220
386
  config.setConfig({ agents: {} });
221
387
 
222
388
  const result = listAcpAgents();
223
389
 
224
390
  expect(result.enabled).toBe(true);
225
391
  const ids = result.agents.map((a) => a.id);
226
- expect(ids).toEqual(["claude", "codex"]);
392
+ expect(ids).toEqual(["claude", "codex", "gemini"]);
227
393
  for (const entry of result.agents) {
228
394
  expect(entry.source).toBe("default");
229
395
  expect(entry.available).toBe(true);
@@ -245,6 +411,7 @@ describe("listAcpAgents", () => {
245
411
  which.setWhich({
246
412
  "my-claude": "/usr/bin/my-claude",
247
413
  "codex-acp": "/usr/bin/codex-acp",
414
+ gemini: "/usr/bin/gemini",
248
415
  });
249
416
 
250
417
  const result = listAcpAgents();
@@ -257,6 +424,27 @@ describe("listAcpAgents", () => {
257
424
  expect(codex?.source).toBe("default");
258
425
  });
259
426
 
427
+ test("missing binaries are listed unavailable even when bun is present", () => {
428
+ // bun presence no longer makes a missing adapter available: it is
429
+ // installed on demand at spawn time, not run via `bun x` from the cwd.
430
+ config.setConfig({ agents: {} });
431
+ which.setWhich({ bun: "/usr/local/bin/bun" });
432
+
433
+ const result = listAcpAgents();
434
+
435
+ for (const entry of result.agents) {
436
+ expect(entry.available).toBe(false);
437
+ expect(entry.unavailableReason).toBeDefined();
438
+ expect(entry.setupHint).toContain("bun add -g");
439
+ }
440
+ // The catalog keeps the canonical adapter commands.
441
+ expect(result.agents.map((a) => a.command)).toEqual([
442
+ "claude-agent-acp",
443
+ "codex-acp",
444
+ "gemini",
445
+ ]);
446
+ });
447
+
260
448
  test("unavailable agent surfaces install hint derived from DEFAULT_AGENT_NPM_PACKAGES", () => {
261
449
  config.setConfig({ agents: {} });
262
450
  which.setWhich({ "claude-agent-acp": "/usr/bin/claude-agent-acp" });
@@ -266,7 +454,33 @@ describe("listAcpAgents", () => {
266
454
  const codex = result.agents.find((a) => a.id === "codex");
267
455
  expect(codex?.available).toBe(false);
268
456
  expect(codex?.unavailableReason).toBe("'codex-acp' is not on PATH");
269
- expect(codex?.setupHint).toBe("npm i -g @zed-industries/codex-acp");
457
+ expect(codex?.setupHint).toBe("bun add -g @zed-industries/codex-acp");
458
+ });
459
+
460
+ test("unavailable gemini surfaces the @google/gemini-cli install hint", () => {
461
+ config.setConfig({ agents: {} });
462
+ which.setWhich({
463
+ "claude-agent-acp": "/usr/bin/claude-agent-acp",
464
+ "codex-acp": "/usr/bin/codex-acp",
465
+ });
466
+
467
+ const result = listAcpAgents();
468
+
469
+ const gemini = result.agents.find((a) => a.id === "gemini");
470
+ expect(gemini?.available).toBe(false);
471
+ expect(gemini?.unavailableReason).toBe("'gemini' is not on PATH");
472
+ expect(gemini?.setupHint).toBe("bun add -g @google/gemini-cli");
473
+ });
474
+
475
+ test("aliases are resolution sugar, not catalog entries", () => {
476
+ config.setConfig({ agents: {} });
477
+
478
+ // "gemini cli" resolves via the alias...
479
+ expect(resolveAcpAgent("gemini cli").ok).toBe(true);
480
+
481
+ // ...but the catalog lists only canonical ids.
482
+ const ids = listAcpAgents().agents.map((a) => a.id);
483
+ expect(ids).toEqual(["claude", "codex", "gemini"]);
270
484
  });
271
485
 
272
486
  test("user-only agent appended after defaults in stable order", () => {
@@ -282,6 +496,7 @@ describe("listAcpAgents", () => {
282
496
  which.setWhich({
283
497
  "claude-agent-acp": "/x",
284
498
  "codex-acp": "/x",
499
+ gemini: "/x",
285
500
  "my-binary": "/x",
286
501
  });
287
502
 
@@ -290,9 +505,10 @@ describe("listAcpAgents", () => {
290
505
  expect(result.agents.map((a) => a.id)).toEqual([
291
506
  "claude",
292
507
  "codex",
508
+ "gemini",
293
509
  "my-agent",
294
510
  ]);
295
- const userOnly = result.agents[2];
511
+ const userOnly = result.agents[3];
296
512
  expect(userOnly.source).toBe("config");
297
513
  expect(userOnly.description).toBe("user-only");
298
514
  });