@vellumai/assistant 0.6.0 → 0.6.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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -75,6 +75,7 @@ function injectSubagent(
75
75
  }
76
76
  >;
77
77
  parentToChildren: Map<string, Set<string>>;
78
+ labelIndex: Map<string, string>;
78
79
  };
79
80
  const state: SubagentState = {
80
81
  config: {
@@ -108,6 +109,14 @@ function injectSubagent(
108
109
  internals.parentToChildren.set(parentConversationId, new Set());
109
110
  }
110
111
  internals.parentToChildren.get(parentConversationId)!.add(subagentId);
112
+
113
+ // Populate label index so label-based lookups work in tests.
114
+ const label = state.config.label;
115
+ internals.labelIndex.set(
116
+ `${parentConversationId}:${label.toLowerCase().trim()}`,
117
+ subagentId,
118
+ );
119
+
111
120
  return state;
112
121
  }
113
122
 
@@ -135,25 +144,29 @@ describe("Subagent tool definitions", () => {
135
144
  test("abort tool has correct definition", () => {
136
145
  const def = findTool("subagent_abort");
137
146
  expect(def).toBeDefined();
138
- expect(def.input_schema.required).toEqual(["subagent_id"]);
147
+ expect(def.input_schema.required).toEqual([]);
148
+ expect(def.input_schema.properties.label).toBeDefined();
139
149
  });
140
150
 
141
151
  test("message tool has correct definition", () => {
142
152
  const def = findTool("subagent_message");
143
153
  expect(def).toBeDefined();
144
- expect(def.input_schema.required).toEqual(["subagent_id", "content"]);
154
+ expect(def.input_schema.required).toEqual(["content"]);
155
+ expect(def.input_schema.properties.label).toBeDefined();
145
156
  });
146
157
 
147
158
  test("read tool has correct definition", () => {
148
159
  const def = findTool("subagent_read");
149
160
  expect(def).toBeDefined();
150
- expect(def.input_schema.required).toEqual(["subagent_id"]);
161
+ expect(def.input_schema.required).toEqual([]);
162
+ expect(def.input_schema.properties.label).toBeDefined();
151
163
  });
152
164
 
153
165
  test("status tool has correct definition", () => {
154
166
  const def = findTool("subagent_status");
155
167
  expect(def).toBeDefined();
156
168
  expect(def.input_schema.required).toEqual([]);
169
+ expect(def.input_schema.properties.label).toBeDefined();
157
170
  });
158
171
  });
159
172
 
@@ -247,7 +260,7 @@ describe("Subagent tool execute validation", () => {
247
260
  expect(result.content).toContain("required");
248
261
  });
249
262
 
250
- test("message returns error when missing subagent_id", async () => {
263
+ test("message returns error when missing subagent_id and label", async () => {
251
264
  const result = await executeSubagentMessage(
252
265
  { content: "hello" },
253
266
  makeContext("sess-1"),
@@ -790,6 +803,111 @@ describe("Subagent read tool", () => {
790
803
  }
791
804
  });
792
805
 
806
+ test("read with last_n: 1 returns only the last message", async () => {
807
+ const manager = getSubagentManager();
808
+ const subagentId = "read-last-n-1";
809
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
810
+
811
+ mockGetMessages = (convId: string) => {
812
+ if (convId !== `conv-${subagentId}`) return null;
813
+ return [
814
+ { role: "assistant", content: "First message" },
815
+ { role: "assistant", content: "Second message" },
816
+ { role: "assistant", content: "Third message" },
817
+ ];
818
+ };
819
+
820
+ try {
821
+ const result = await executeSubagentRead(
822
+ { subagent_id: subagentId, last_n: 1 },
823
+ makeContext(ownerConversation),
824
+ );
825
+ expect(result.isError).toBe(false);
826
+ expect(result.content).toBe("Third message");
827
+ } finally {
828
+ mockGetMessages = () => null;
829
+ }
830
+ });
831
+
832
+ test("read with last_n: 2 returns last 2 messages joined", async () => {
833
+ const manager = getSubagentManager();
834
+ const subagentId = "read-last-n-2";
835
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
836
+
837
+ mockGetMessages = (convId: string) => {
838
+ if (convId !== `conv-${subagentId}`) return null;
839
+ return [
840
+ { role: "assistant", content: "First message" },
841
+ { role: "assistant", content: "Second message" },
842
+ { role: "assistant", content: "Third message" },
843
+ ];
844
+ };
845
+
846
+ try {
847
+ const result = await executeSubagentRead(
848
+ { subagent_id: subagentId, last_n: 2 },
849
+ makeContext(ownerConversation),
850
+ );
851
+ expect(result.isError).toBe(false);
852
+ expect(result.content).toBe("Second message\n\nThird message");
853
+ } finally {
854
+ mockGetMessages = () => null;
855
+ }
856
+ });
857
+
858
+ test("read with last_n omitted returns all messages", async () => {
859
+ const manager = getSubagentManager();
860
+ const subagentId = "read-last-n-omit";
861
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
862
+
863
+ mockGetMessages = (convId: string) => {
864
+ if (convId !== `conv-${subagentId}`) return null;
865
+ return [
866
+ { role: "assistant", content: "First message" },
867
+ { role: "assistant", content: "Second message" },
868
+ { role: "assistant", content: "Third message" },
869
+ ];
870
+ };
871
+
872
+ try {
873
+ const result = await executeSubagentRead(
874
+ { subagent_id: subagentId },
875
+ makeContext(ownerConversation),
876
+ );
877
+ expect(result.isError).toBe(false);
878
+ expect(result.content).toBe(
879
+ "First message\n\nSecond message\n\nThird message",
880
+ );
881
+ } finally {
882
+ mockGetMessages = () => null;
883
+ }
884
+ });
885
+
886
+ test("read with last_n larger than available returns all messages", async () => {
887
+ const manager = getSubagentManager();
888
+ const subagentId = "read-last-n-large";
889
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
890
+
891
+ mockGetMessages = (convId: string) => {
892
+ if (convId !== `conv-${subagentId}`) return null;
893
+ return [
894
+ { role: "assistant", content: "First message" },
895
+ { role: "assistant", content: "Second message" },
896
+ ];
897
+ };
898
+
899
+ try {
900
+ const result = await executeSubagentRead(
901
+ { subagent_id: subagentId, last_n: 100 },
902
+ makeContext(ownerConversation),
903
+ );
904
+ expect(result.isError).toBe(false);
905
+ expect(result.content).toBe("First message\n\nSecond message");
906
+ } finally {
907
+ mockGetMessages = () => null;
908
+ }
909
+ });
910
+
793
911
  test("read concatenates multiple assistant messages", async () => {
794
912
  const manager = getSubagentManager();
795
913
  const subagentId = "read-multi-1";
@@ -822,6 +940,113 @@ describe("Subagent read tool", () => {
822
940
  mockGetMessages = () => null;
823
941
  }
824
942
  });
943
+
944
+ test("read with last_n: 1 returns only the last message", async () => {
945
+ const manager = getSubagentManager();
946
+ const subagentId = "read-last-n-1";
947
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
948
+
949
+ mockGetMessages = (convId: string) => {
950
+ if (convId !== `conv-${subagentId}`) return null;
951
+ return [
952
+ { role: "assistant", content: "First response" },
953
+ { role: "user", content: "Follow up" },
954
+ { role: "assistant", content: "Second response" },
955
+ { role: "assistant", content: "Third response" },
956
+ ];
957
+ };
958
+
959
+ try {
960
+ const result = await executeSubagentRead(
961
+ { subagent_id: subagentId, last_n: 1 },
962
+ makeContext(ownerConversation),
963
+ );
964
+ expect(result.isError).toBe(false);
965
+ expect(result.content).toBe("Third response");
966
+ } finally {
967
+ mockGetMessages = () => null;
968
+ }
969
+ });
970
+
971
+ test("read with last_n: 2 returns last two messages", async () => {
972
+ const manager = getSubagentManager();
973
+ const subagentId = "read-last-n-2";
974
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
975
+
976
+ mockGetMessages = (convId: string) => {
977
+ if (convId !== `conv-${subagentId}`) return null;
978
+ return [
979
+ { role: "assistant", content: "First response" },
980
+ { role: "user", content: "Follow up" },
981
+ { role: "assistant", content: "Second response" },
982
+ { role: "assistant", content: "Third response" },
983
+ ];
984
+ };
985
+
986
+ try {
987
+ const result = await executeSubagentRead(
988
+ { subagent_id: subagentId, last_n: 2 },
989
+ makeContext(ownerConversation),
990
+ );
991
+ expect(result.isError).toBe(false);
992
+ expect(result.content).toBe("Second response\n\nThird response");
993
+ } finally {
994
+ mockGetMessages = () => null;
995
+ }
996
+ });
997
+
998
+ test("read without last_n returns all messages", async () => {
999
+ const manager = getSubagentManager();
1000
+ const subagentId = "read-no-last-n-1";
1001
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
1002
+
1003
+ mockGetMessages = (convId: string) => {
1004
+ if (convId !== `conv-${subagentId}`) return null;
1005
+ return [
1006
+ { role: "assistant", content: "First response" },
1007
+ { role: "assistant", content: "Second response" },
1008
+ { role: "assistant", content: "Third response" },
1009
+ ];
1010
+ };
1011
+
1012
+ try {
1013
+ const result = await executeSubagentRead(
1014
+ { subagent_id: subagentId },
1015
+ makeContext(ownerConversation),
1016
+ );
1017
+ expect(result.isError).toBe(false);
1018
+ expect(result.content).toBe(
1019
+ "First response\n\nSecond response\n\nThird response",
1020
+ );
1021
+ } finally {
1022
+ mockGetMessages = () => null;
1023
+ }
1024
+ });
1025
+
1026
+ test("read with last_n larger than available messages returns all", async () => {
1027
+ const manager = getSubagentManager();
1028
+ const subagentId = "read-last-n-big-1";
1029
+ injectSubagent(manager, subagentId, ownerConversation, "completed");
1030
+
1031
+ mockGetMessages = (convId: string) => {
1032
+ if (convId !== `conv-${subagentId}`) return null;
1033
+ return [
1034
+ { role: "assistant", content: "First response" },
1035
+ { role: "assistant", content: "Second response" },
1036
+ ];
1037
+ };
1038
+
1039
+ try {
1040
+ const result = await executeSubagentRead(
1041
+ { subagent_id: subagentId, last_n: 100 },
1042
+ makeContext(ownerConversation),
1043
+ );
1044
+ expect(result.isError).toBe(false);
1045
+ expect(result.content).toBe("First response\n\nSecond response");
1046
+ } finally {
1047
+ mockGetMessages = () => null;
1048
+ }
1049
+ });
825
1050
  });
826
1051
 
827
1052
  // ── Abort success path details ──────────────────────────────────────
@@ -869,3 +1094,238 @@ describe("Subagent abort success responses", () => {
869
1094
  expect(result.content).toContain("Could not abort");
870
1095
  });
871
1096
  });
1097
+
1098
+ // ── Label-based subagent lookup ────────────────────────────────────
1099
+
1100
+ describe("Label-based subagent lookup", () => {
1101
+ const parentConversation = "label-test-sess";
1102
+ const subagentId = "label-sub-1";
1103
+
1104
+ // Inject a subagent with a specific label for the test suite.
1105
+ const manager = getSubagentManager();
1106
+ injectSubagent(manager, subagentId, parentConversation, "running", {
1107
+ config: {
1108
+ id: subagentId,
1109
+ parentConversationId: parentConversation,
1110
+ label: "Research task",
1111
+ objective: "research something",
1112
+ },
1113
+ });
1114
+
1115
+ test("subagent_status with label returns status", async () => {
1116
+ const result = await executeSubagentStatus(
1117
+ { label: "Research task" },
1118
+ makeContext(parentConversation),
1119
+ );
1120
+ expect(result.isError).toBe(false);
1121
+ const parsed = JSON.parse(result.content);
1122
+ expect(parsed.subagentId).toBe(subagentId);
1123
+ expect(parsed.label).toBe("Research task");
1124
+ expect(parsed.status).toBe("running");
1125
+ });
1126
+
1127
+ test("subagent_status with lowercase label (case-insensitive)", async () => {
1128
+ const result = await executeSubagentStatus(
1129
+ { label: "research task" },
1130
+ makeContext(parentConversation),
1131
+ );
1132
+ expect(result.isError).toBe(false);
1133
+ const parsed = JSON.parse(result.content);
1134
+ expect(parsed.subagentId).toBe(subagentId);
1135
+ });
1136
+
1137
+ test("subagent_status with nonexistent label returns error", async () => {
1138
+ const result = await executeSubagentStatus(
1139
+ { label: "nonexistent" },
1140
+ makeContext(parentConversation),
1141
+ );
1142
+ expect(result.isError).toBe(true);
1143
+ expect(result.content).toContain("No subagent found");
1144
+ });
1145
+
1146
+ test("subagent_message with label succeeds", async () => {
1147
+ const result = await executeSubagentMessage(
1148
+ { label: "Research task", content: "hello" },
1149
+ makeContext(parentConversation),
1150
+ );
1151
+ expect(result.isError).toBe(false);
1152
+ const parsed = JSON.parse(result.content);
1153
+ expect(parsed.subagentId).toBe(subagentId);
1154
+ expect(parsed.message).toContain("Message sent");
1155
+ });
1156
+
1157
+ test("subagent_read with label on completed subagent returns output", async () => {
1158
+ // Inject a completed subagent for the read test.
1159
+ const readSubId = "label-read-sub-1";
1160
+ injectSubagent(manager, readSubId, parentConversation, "completed", {
1161
+ config: {
1162
+ id: readSubId,
1163
+ parentConversationId: parentConversation,
1164
+ label: "Read task",
1165
+ objective: "read something",
1166
+ },
1167
+ });
1168
+
1169
+ mockGetMessages = (convId: string) => {
1170
+ if (convId !== `conv-${readSubId}`) return null;
1171
+ return [
1172
+ {
1173
+ role: "assistant",
1174
+ content: JSON.stringify([
1175
+ { type: "text", text: "Research findings here" },
1176
+ ]),
1177
+ },
1178
+ ];
1179
+ };
1180
+
1181
+ try {
1182
+ const result = await executeSubagentRead(
1183
+ { label: "Read task" },
1184
+ makeContext(parentConversation),
1185
+ );
1186
+ expect(result.isError).toBe(false);
1187
+ expect(result.content).toContain("Research findings here");
1188
+ } finally {
1189
+ mockGetMessages = () => null;
1190
+ }
1191
+ });
1192
+ });
1193
+
1194
+ // ── Label collision & dispose guard ─────────────────────────────────
1195
+
1196
+ describe("Label collision and dispose guard", () => {
1197
+ test("disposing second subagent with same label keeps first reachable by label", () => {
1198
+ const manager = getSubagentManager();
1199
+ const parentConversation = "label-collision-sess";
1200
+ const firstId = "collision-sub-1";
1201
+ const secondId = "collision-sub-2";
1202
+ const sharedLabel = "Shared Worker";
1203
+
1204
+ // Inject two subagents with the same label — second overwrites label index.
1205
+ injectSubagent(manager, firstId, parentConversation, "running", {
1206
+ config: {
1207
+ id: firstId,
1208
+ parentConversationId: parentConversation,
1209
+ label: sharedLabel,
1210
+ objective: "first task",
1211
+ },
1212
+ });
1213
+ injectSubagent(manager, secondId, parentConversation, "completed", {
1214
+ config: {
1215
+ id: secondId,
1216
+ parentConversationId: parentConversation,
1217
+ label: sharedLabel,
1218
+ objective: "second task",
1219
+ },
1220
+ });
1221
+
1222
+ // Label should currently resolve to the second subagent.
1223
+ expect(manager.getByLabel(sharedLabel, parentConversation)?.config.id).toBe(
1224
+ secondId,
1225
+ );
1226
+
1227
+ // Dispose the FIRST subagent — its label was already overwritten,
1228
+ // so the label index entry (pointing to second) must survive.
1229
+ manager.dispose(firstId);
1230
+
1231
+ const afterDispose = manager.getByLabel(sharedLabel, parentConversation);
1232
+ expect(afterDispose).toBeDefined();
1233
+ expect(afterDispose!.config.id).toBe(secondId);
1234
+
1235
+ // The second subagent should still be directly accessible too.
1236
+ expect(manager.getState(secondId)).toBeDefined();
1237
+ // And the first should be gone.
1238
+ expect(manager.getState(firstId)).toBeUndefined();
1239
+ });
1240
+ });
1241
+
1242
+ // ── Role-based spawn ──────────────────────────────────────────────
1243
+
1244
+ describe("Subagent role-based spawn", () => {
1245
+ test("spawn with role 'researcher' passes role to manager", async () => {
1246
+ const manager = getSubagentManager();
1247
+ const originalSpawn = manager.spawn.bind(manager);
1248
+ let capturedConfig: Record<string, unknown> | undefined;
1249
+
1250
+ manager.spawn = async (config: Record<string, unknown>) => {
1251
+ capturedConfig = config;
1252
+ return "role-researcher-id";
1253
+ };
1254
+
1255
+ try {
1256
+ const result = await executeSubagentSpawn(
1257
+ {
1258
+ label: "Research task",
1259
+ objective: "Find pricing data",
1260
+ role: "researcher",
1261
+ },
1262
+ makeContext("sess-role-1", { sendToClient: () => {} }),
1263
+ );
1264
+ expect(result.isError).toBe(false);
1265
+ const parsed = JSON.parse(result.content);
1266
+ expect(parsed.subagentId).toBe("role-researcher-id");
1267
+ expect(capturedConfig).toBeDefined();
1268
+ expect(capturedConfig!.role).toBe("researcher");
1269
+ } finally {
1270
+ manager.spawn = originalSpawn;
1271
+ }
1272
+ });
1273
+
1274
+ test("spawn without role defaults to general (backwards compat)", async () => {
1275
+ const manager = getSubagentManager();
1276
+ const originalSpawn = manager.spawn.bind(manager);
1277
+ let capturedConfig: Record<string, unknown> | undefined;
1278
+
1279
+ manager.spawn = async (config: Record<string, unknown>) => {
1280
+ capturedConfig = config;
1281
+ return "role-default-id";
1282
+ };
1283
+
1284
+ try {
1285
+ const result = await executeSubagentSpawn(
1286
+ { label: "General task", objective: "Do something" },
1287
+ makeContext("sess-role-2", { sendToClient: () => {} }),
1288
+ );
1289
+ expect(result.isError).toBe(false);
1290
+ const parsed = JSON.parse(result.content);
1291
+ expect(parsed.subagentId).toBe("role-default-id");
1292
+ expect(capturedConfig).toBeDefined();
1293
+ // When role is not specified, it should not be present in config
1294
+ expect(capturedConfig!.role).toBeUndefined();
1295
+ } finally {
1296
+ manager.spawn = originalSpawn;
1297
+ }
1298
+ });
1299
+
1300
+ test("spawn with invalid role returns clear error message", async () => {
1301
+ const result = await executeSubagentSpawn(
1302
+ {
1303
+ label: "Bad role task",
1304
+ objective: "Should fail",
1305
+ role: "nonexistent-role",
1306
+ },
1307
+ makeContext("sess-role-invalid", { sendToClient: () => {} }),
1308
+ );
1309
+ expect(result.isError).toBe(true);
1310
+ expect(result.content).toContain("Invalid subagent role");
1311
+ expect(result.content).toContain("nonexistent-role");
1312
+ expect(result.content).toContain("Must be one of");
1313
+ expect(result.content).toContain("general");
1314
+ expect(result.content).toContain("researcher");
1315
+ });
1316
+
1317
+ test("spawn tool definition includes role property", () => {
1318
+ const def = findTool("subagent_spawn");
1319
+ expect(def).toBeDefined();
1320
+ expect(def.input_schema.properties.role).toBeDefined();
1321
+ expect(def.input_schema.properties.role.type).toBe("string");
1322
+ expect(def.input_schema.properties.role.enum).toEqual([
1323
+ "general",
1324
+ "researcher",
1325
+ "coder",
1326
+ "planner",
1327
+ ]);
1328
+ // role is not required
1329
+ expect(def.input_schema.required).not.toContain("role");
1330
+ });
1331
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Tests for the "Action Confirmation Mode" system prompt injection.
3
+ *
4
+ * Verifies:
5
+ * - Prompt includes the section when `permission-controls-v2` flag is enabled
6
+ * AND `askBeforeActing` is `true`.
7
+ * - Prompt excludes the section when the flag is disabled.
8
+ * - Prompt excludes the section when `askBeforeActing` is `false`.
9
+ */
10
+ import { mkdirSync } from "node:fs";
11
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ // Mock platform to use a temp directory
14
+ const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
15
+
16
+ const noopLogger: Record<string, unknown> = new Proxy(
17
+ {} as Record<string, unknown>,
18
+ {
19
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
20
+ },
21
+ );
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
24
+ const realLogger = require("../util/logger.js");
25
+ mock.module("../util/logger.js", () => ({
26
+ ...realLogger,
27
+ getLogger: () => noopLogger,
28
+ getCliLogger: () => noopLogger,
29
+ truncateForLog: (v: string) => v,
30
+ initLogger: () => {},
31
+ pruneOldLogFiles: () => 0,
32
+ }));
33
+
34
+ mock.module("../config/loader.js", () => ({
35
+ getConfig: () => ({
36
+ ui: {},
37
+ services: {
38
+ inference: {
39
+ mode: "your-own",
40
+ provider: "anthropic",
41
+ model: "claude-opus-4-6",
42
+ },
43
+ "image-generation": {
44
+ mode: "your-own",
45
+ provider: "gemini",
46
+ model: "gemini-3.1-flash-image-preview",
47
+ },
48
+ "web-search": { mode: "your-own", provider: "inference-provider-native" },
49
+ },
50
+ }),
51
+ loadConfig: () => ({}),
52
+ loadRawConfig: () => ({}),
53
+ saveConfig: () => {},
54
+ saveRawConfig: () => {},
55
+ invalidateConfigCache: () => {},
56
+ getNestedValue: () => undefined,
57
+ setNestedValue: () => {},
58
+ }));
59
+
60
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
61
+ const realUserReference = require("../prompts/user-reference.js");
62
+ mock.module("../prompts/user-reference.js", () => ({
63
+ ...realUserReference,
64
+ resolveUserReference: () => "John",
65
+ resolveUserPronouns: () => null,
66
+ }));
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Controllable mocks for feature flags and permission mode
70
+ // ---------------------------------------------------------------------------
71
+
72
+ let flagEnabled = false;
73
+ let askBeforeActing = true;
74
+
75
+ mock.module("../config/assistant-feature-flags.js", () => ({
76
+ isAssistantFeatureFlagEnabled: (key: string) => {
77
+ if (key === "permission-controls-v2") return flagEnabled;
78
+ return true;
79
+ },
80
+ _setOverridesForTesting: () => {},
81
+ clearFeatureFlagOverridesCache: () => {},
82
+ getAssistantFeatureFlagDefaults: () => ({}),
83
+ }));
84
+
85
+ mock.module("../permissions/permission-mode-store.js", () => ({
86
+ getMode: () => ({ askBeforeActing, hostAccess: false }),
87
+ initPermissionModeStore: () => {},
88
+ setAskBeforeActing: () => {},
89
+ setHostAccess: () => {},
90
+ onModeChanged: () => () => {},
91
+ resetForTesting: () => {},
92
+ }));
93
+
94
+ // Import after mocks
95
+ const { buildSystemPrompt } = await import("../prompts/system-prompt.js");
96
+
97
+ const ACTION_CONFIRMATION_HEADING = "## Action Confirmation Mode";
98
+
99
+ describe("Action Confirmation Mode system prompt injection", () => {
100
+ beforeEach(() => {
101
+ mkdirSync(TEST_DIR, { recursive: true });
102
+ flagEnabled = false;
103
+ askBeforeActing = true;
104
+ });
105
+
106
+ afterEach(() => {
107
+ flagEnabled = false;
108
+ askBeforeActing = true;
109
+ });
110
+
111
+ test("includes section when flag enabled and askBeforeActing is true", () => {
112
+ flagEnabled = true;
113
+ askBeforeActing = true;
114
+ const result = buildSystemPrompt();
115
+ expect(result).toContain(ACTION_CONFIRMATION_HEADING);
116
+ expect(result).toContain('"Ask before acting" mode');
117
+ });
118
+
119
+ test("excludes section when flag is disabled", () => {
120
+ flagEnabled = false;
121
+ askBeforeActing = true;
122
+ const result = buildSystemPrompt();
123
+ expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
124
+ });
125
+
126
+ test("excludes section when askBeforeActing is false", () => {
127
+ flagEnabled = true;
128
+ askBeforeActing = false;
129
+ const result = buildSystemPrompt();
130
+ expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
131
+ });
132
+
133
+ test("excludes section when both flag disabled and askBeforeActing false", () => {
134
+ flagEnabled = false;
135
+ askBeforeActing = false;
136
+ const result = buildSystemPrompt();
137
+ expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
138
+ });
139
+ });