@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -14,9 +14,11 @@ import type {
14
14
  ParsedCommand,
15
15
  } from "../tools/terminal/parser.js";
16
16
  import { getLogger } from "../util/logger.js";
17
+ import { parseArgs } from "./arg-parser.js";
17
18
  import { DEFAULT_COMMAND_REGISTRY } from "./command-registry.js";
18
19
  import type {
19
20
  ArgRule,
21
+ ArgSchema,
20
22
  BashClassifierInput,
21
23
  CommandRiskSpec,
22
24
  Risk,
@@ -26,6 +28,7 @@ import type {
26
28
  UserRule,
27
29
  } from "./risk-types.js";
28
30
  import { cachedParse } from "./shell-identity.js";
31
+ import type { AllowlistOption } from "./types.js";
29
32
 
30
33
  const log = getLogger("bash-risk-classifier");
31
34
 
@@ -176,20 +179,17 @@ function getWrappedProgramWithArgs(seg: {
176
179
 
177
180
  /**
178
181
  * Extract the first positional (non-flag) arg, skipping value-consuming flags.
182
+ * Delegates to the shared `parseArgs()` utility for consistent arg parsing.
179
183
  */
180
184
  function firstPositionalArg(
181
185
  args: string[],
182
186
  valueFlags?: Set<string>,
183
187
  ): string | undefined {
184
- for (let i = 0; i < args.length; i++) {
185
- const arg = args[i];
186
- if (arg.startsWith("-")) {
187
- if (valueFlags?.has(arg)) i++;
188
- continue;
189
- }
190
- return arg;
191
- }
192
- return undefined;
188
+ const schema: ArgSchema = valueFlags
189
+ ? { valueFlags: [...valueFlags], positionals: "none" }
190
+ : { positionals: "none" };
191
+ const parsed = parseArgs(args, schema);
192
+ return parsed.positionals[0];
193
193
  }
194
194
 
195
195
  // ── Safe-file downgrade for rm ────────────────────────────────────────────────
@@ -226,9 +226,8 @@ function resolveSubcommand(
226
226
  return { spec, remainingArgs: args };
227
227
  }
228
228
 
229
- const valueFlags = spec.globalValueFlags
230
- ? new Set(spec.globalValueFlags)
231
- : undefined;
229
+ const valueFlagsList = spec.argSchema?.valueFlags;
230
+ const valueFlags = valueFlagsList ? new Set(valueFlagsList) : undefined;
232
231
  const subcommandName = firstPositionalArg(args, valueFlags);
233
232
 
234
233
  if (!subcommandName || !spec.subcommands[subcommandName]) {
@@ -367,12 +366,50 @@ export function classifySegment(
367
366
  let argRuleReason = "";
368
367
 
369
368
  const allArgs = segment.args;
370
- for (let i = 0; i < allArgs.length; i++) {
371
- const arg = allArgs[i];
372
- let matched = false;
373
- for (const rule of argRules) {
374
- // Standard match: flag or positional against this arg
375
- if (matchesArgRule(rule, arg)) {
369
+
370
+ // Parse args using the resolved spec's argSchema for structured lookups.
371
+ const schema = resolvedSpec.argSchema ?? {};
372
+ const parsed = parseArgs(allArgs, schema);
373
+
374
+ // Track which positionals have been covered by a rule.
375
+ const matchedPositionalIndices = new Set<number>();
376
+
377
+ for (const rule of argRules) {
378
+ if (rule.flags && rule.flags.length > 0 && rule.valuePattern) {
379
+ // ── Rules with flags + valuePattern ──────────────────────────────
380
+ // Look up each rule flag in parsed.flags. If the flag has a string
381
+ // value (consumed by parseArgs), test that value against the pattern.
382
+ // This replaces the manual next-token lookahead.
383
+ // Also check for --flag=value forms already handled by parseArgs.
384
+ //
385
+ // Known limitation: parseArgs stores flags in a Map (last value wins),
386
+ // so repeated flags like `curl -d @/etc/shadow -d safe` only check
387
+ // the last value. A future improvement could store flag values as
388
+ // arrays to catch all occurrences.
389
+ let flagValueMatched = false;
390
+ for (const flag of rule.flags) {
391
+ const flagVal = parsed.flags.get(flag);
392
+ if (typeof flagVal === "string") {
393
+ if (getCompiledPattern(rule.valuePattern).test(flagVal)) {
394
+ flagValueMatched = true;
395
+ break;
396
+ }
397
+ }
398
+ }
399
+
400
+ // Also check raw args for inline --flag=value forms where the flag
401
+ // name is NOT in the argSchema.valueFlags (parseArgs wouldn't split
402
+ // it). matchesArgRule handles this case.
403
+ if (!flagValueMatched) {
404
+ for (const arg of allArgs) {
405
+ if (matchesArgRule(rule, arg)) {
406
+ flagValueMatched = true;
407
+ break;
408
+ }
409
+ }
410
+ }
411
+
412
+ if (flagValueMatched) {
376
413
  if (
377
414
  !anyArgRuleMatched ||
378
415
  riskOrd(rule.risk) > riskOrd(argRuleMaxRisk)
@@ -381,38 +418,125 @@ export function classifySegment(
381
418
  argRuleReason = rule.reason;
382
419
  }
383
420
  anyArgRuleMatched = true;
384
- matched = true;
385
- break; // first match per arg wins
386
421
  }
387
- // Flag+value lookahead: if this arg is a flag listed in the rule and
388
- // the rule has a valuePattern, check the NEXT arg against the pattern.
389
- // This handles `curl -d @file` where `-d` is the flag and `@file` is
390
- // the value in the next token.
391
- if (
392
- rule.flags &&
393
- rule.valuePattern &&
394
- rule.flags.includes(arg) &&
395
- i + 1 < allArgs.length
396
- ) {
397
- const nextArg = allArgs[i + 1];
398
- if (getCompiledPattern(rule.valuePattern).test(nextArg)) {
399
- if (
400
- !anyArgRuleMatched ||
401
- riskOrd(rule.risk) > riskOrd(argRuleMaxRisk)
402
- ) {
403
- argRuleMaxRisk = rule.risk;
404
- argRuleReason = rule.reason;
405
- }
406
- anyArgRuleMatched = true;
407
- matched = true;
422
+ } else if (rule.flags && rule.flags.length > 0) {
423
+ // ── Rules with flags only (no valuePattern) ──────────────────────
424
+ // Check flag presence in parsed.flags Map.
425
+ // Also scan raw allArgs for combined short flags like `-rf` that
426
+ // parseArgs treats as a single boolean flag token.
427
+ let flagMatched = false;
428
+ for (const flag of rule.flags) {
429
+ if (parsed.flags.has(flag)) {
430
+ flagMatched = true;
408
431
  break;
409
432
  }
410
433
  }
434
+
435
+ // Fallback: scan raw args for combined short flags (e.g. `-rf`)
436
+ // and --flag=value forms (e.g. `--set=managed`) that parseArgs
437
+ // doesn't split when the flag isn't in argSchema.valueFlags.
438
+ // matchesArgRule handles both cases via its flag splitting logic.
439
+ if (!flagMatched) {
440
+ for (const arg of allArgs) {
441
+ if (matchesArgRule(rule, arg)) {
442
+ flagMatched = true;
443
+ break;
444
+ }
445
+ }
446
+ }
447
+
448
+ if (flagMatched) {
449
+ if (
450
+ !anyArgRuleMatched ||
451
+ riskOrd(rule.risk) > riskOrd(argRuleMaxRisk)
452
+ ) {
453
+ argRuleMaxRisk = rule.risk;
454
+ argRuleReason = rule.reason;
455
+ }
456
+ anyArgRuleMatched = true;
457
+ }
458
+ } else if (rule.valuePattern) {
459
+ // ── Rules with valuePattern only (no flags) ──────────────────────
460
+ // Test each positional against the pattern.
461
+ const re = getCompiledPattern(rule.valuePattern);
462
+ let positionalMatched = false;
463
+ for (let pi = 0; pi < parsed.positionals.length; pi++) {
464
+ if (re.test(parsed.positionals[pi])) {
465
+ positionalMatched = true;
466
+ matchedPositionalIndices.add(pi);
467
+ }
468
+ }
469
+
470
+ // Also check raw allArgs for backward compatibility — some patterns
471
+ // may match flag-like tokens or args that parseArgs classified
472
+ // differently.
473
+ if (!positionalMatched) {
474
+ for (const arg of allArgs) {
475
+ if (re.test(arg)) {
476
+ positionalMatched = true;
477
+ break;
478
+ }
479
+ }
480
+ }
481
+
482
+ if (positionalMatched) {
483
+ if (
484
+ !anyArgRuleMatched ||
485
+ riskOrd(rule.risk) > riskOrd(argRuleMaxRisk)
486
+ ) {
487
+ argRuleMaxRisk = rule.risk;
488
+ argRuleReason = rule.reason;
489
+ }
490
+ anyArgRuleMatched = true;
491
+ }
492
+ } else {
493
+ // No flags and no valuePattern — always matches (unusual but allowed)
494
+ if (
495
+ !anyArgRuleMatched ||
496
+ riskOrd(rule.risk) > riskOrd(argRuleMaxRisk)
497
+ ) {
498
+ argRuleMaxRisk = rule.risk;
499
+ argRuleReason = rule.reason;
500
+ }
501
+ anyArgRuleMatched = true;
411
502
  }
412
- // Track unmatched non-flag args. Flags (starting with -) are structural
413
- // and don't need rule coverage for de-escalation safety.
414
- if (!matched && !arg.startsWith("-")) {
503
+ }
504
+
505
+ // Check for unmatched positionals — any positional not covered by a
506
+ // valuePattern-only rule prevents de-escalation.
507
+ for (let pi = 0; pi < parsed.positionals.length; pi++) {
508
+ if (!matchedPositionalIndices.has(pi)) {
415
509
  hasUnmatchedNonFlagArg = true;
510
+ break;
511
+ }
512
+ }
513
+
514
+ // Also check raw allArgs for non-flag args that parseArgs may have
515
+ // classified as flags (e.g. combined short flags like `-rf` are boolean
516
+ // flags in parseArgs but are non-flag args in the old iteration model).
517
+ // We only need to track unmatched non-flag args from the raw iteration
518
+ // perspective for backward compatibility.
519
+ if (!hasUnmatchedNonFlagArg) {
520
+ for (const arg of allArgs) {
521
+ if (arg.startsWith("-")) continue;
522
+ // Check if this positional was matched by any rule
523
+ let wasMatched = false;
524
+ for (const rule of argRules) {
525
+ if (matchesArgRule(rule, arg)) {
526
+ wasMatched = true;
527
+ break;
528
+ }
529
+ // Check flag+value lookahead match (arg as a flag value)
530
+ if (rule.flags && rule.valuePattern && rule.flags.includes(arg)) {
531
+ // This arg is a flag that matched a rule flag — it's structural
532
+ wasMatched = true;
533
+ break;
534
+ }
535
+ }
536
+ if (!wasMatched) {
537
+ hasUnmatchedNonFlagArg = true;
538
+ break;
539
+ }
416
540
  }
417
541
  }
418
542
 
@@ -500,10 +624,19 @@ export function generateScopeOptions(
500
624
  options.push({ pattern, label });
501
625
  }
502
626
 
503
- // For multi-segment commands (pipelines), use the full command as exact match
504
- // and individual segment programs for broader options
627
+ // For multi-segment commands (pipelines, &&, etc.), use the full command as
628
+ // exact match and individual segment programs for broader options.
629
+ // Reconstruct using actual operators from the parsed segments (not hardcoded " | ").
505
630
  if (parsed.segments.length > 1) {
506
- const fullCommand = parsed.segments.map((s) => s.command).join(" | ");
631
+ const parts: string[] = [];
632
+ for (let i = 0; i < parsed.segments.length; i++) {
633
+ const seg = parsed.segments[i];
634
+ if (i > 0 && seg.operator) {
635
+ parts.push(seg.operator);
636
+ }
637
+ parts.push(seg.command);
638
+ }
639
+ const fullCommand = parts.join(" ");
507
640
  addOption(`^${escapeRegex(fullCommand)}$`, fullCommand);
508
641
  // Add command-level wildcards for each unique program
509
642
  const programs = new Set(parsed.segments.map((s) => s.program));
@@ -530,14 +663,35 @@ export function generateScopeOptions(
530
663
  return options;
531
664
  }
532
665
 
533
- // Separate args into flags and positionals
534
- const flags: string[] = [];
535
- const positionals: string[] = [];
536
- for (const arg of seg.args) {
537
- if (arg.startsWith("-")) {
538
- flags.push(arg);
539
- } else {
540
- positionals.push(arg);
666
+ // Separate args into flags and positionals.
667
+ // When the command has an argSchema, use parseArgs for accurate flag/positional
668
+ // separation (correctly handles value-consuming flags like `find -name "*.ts"`).
669
+ // Otherwise, fall back to the naive `startsWith("-")` heuristic.
670
+ let flags: string[];
671
+ let positionals: string[];
672
+
673
+ if (spec?.argSchema) {
674
+ const parsedArgs = parseArgs(seg.args, spec.argSchema);
675
+ // Convert the flags Map to a flat string array: for value-consuming flags,
676
+ // include both the flag and its value as separate entries; for boolean flags,
677
+ // include just the flag.
678
+ flags = [];
679
+ for (const [flagName, flagValue] of parsedArgs.flags) {
680
+ flags.push(flagName);
681
+ if (typeof flagValue === "string") {
682
+ flags.push(flagValue);
683
+ }
684
+ }
685
+ positionals = parsedArgs.positionals;
686
+ } else {
687
+ flags = [];
688
+ positionals = [];
689
+ for (const arg of seg.args) {
690
+ if (arg.startsWith("-")) {
691
+ flags.push(arg);
692
+ } else {
693
+ positionals.push(arg);
694
+ }
541
695
  }
542
696
  }
543
697
 
@@ -551,12 +705,19 @@ export function generateScopeOptions(
551
705
  }
552
706
 
553
707
  // 2. Wildcard positionals right-to-left
554
- if (positionals.length > 1) {
555
- for (let drop = 1; drop < positionals.length; drop++) {
556
- const kept = positionals.slice(0, positionals.length - drop);
557
- const parts = [programName, ...flags, ...kept].filter(Boolean);
708
+ // When a subcommand is detected, exclude it from the positionals that get
709
+ // wildcarded it's placed explicitly before flags in the label.
710
+ const wildcardPositionals = subcommand ? positionals.slice(1) : positionals;
711
+ if (wildcardPositionals.length > 1) {
712
+ for (let drop = 1; drop < wildcardPositionals.length; drop++) {
713
+ const kept = wildcardPositionals.slice(
714
+ 0,
715
+ wildcardPositionals.length - drop,
716
+ );
717
+ const sub = subcommand ? [subcommand] : [];
718
+ const parts = [programName, ...sub, ...flags, ...kept].filter(Boolean);
558
719
  const pattern = `^${parts.map(escapeRegex).join("\\s+")}\\s+.*$`;
559
- const label = [programName, ...flags, ...kept, "*"].join(" ");
720
+ const label = [programName, ...sub, ...flags, ...kept, "*"].join(" ");
560
721
  addOption(pattern, label);
561
722
  }
562
723
  }
@@ -589,6 +750,82 @@ function escapeRegex(s: string): string {
589
750
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
590
751
  }
591
752
 
753
+ // ── Scope → Allowlist conversion ─────────────────────────────────────────────
754
+
755
+ /**
756
+ * Extract stable tokens from a scope option label: program and subcommand
757
+ * words, skipping flags (starting with `-`) and wildcards (`*`).
758
+ * Returns an `action:` prefixed pattern that matches the action key
759
+ * candidates produced by `buildCommandCandidates()`.
760
+ */
761
+ function labelToActionPattern(label: string): string {
762
+ const tokens = label
763
+ .split(/\s+/)
764
+ .filter((t) => !t.startsWith("-") && t !== "*");
765
+ return `action:${tokens.join(" ")}`;
766
+ }
767
+
768
+ /**
769
+ * Convert classifier-produced `ScopeOption[]` to `AllowlistOption[]` format.
770
+ *
771
+ * Patterns must be glob-compatible (not regex) because trust rules use
772
+ * Minimatch for matching against command candidates produced by
773
+ * `buildCommandCandidates()`. The format:
774
+ * - First option (exact match): raw command string
775
+ * - Intermediate options: `action:<program> <subcommand>` patterns that match
776
+ * action key candidates (labels reorder args so can't be used as globs directly)
777
+ * - Command-level wildcards: `action:<program>` matching the broadest action key
778
+ *
779
+ * Deduplicates by pattern to avoid redundant options when multiple scope levels
780
+ * collapse to the same action key.
781
+ */
782
+ export function scopeOptionsToAllowlistOptions(
783
+ scopeOptions: ScopeOption[],
784
+ _parsed: ParsedCommand,
785
+ ): AllowlistOption[] {
786
+ if (scopeOptions.length === 0) return [];
787
+
788
+ const results: AllowlistOption[] = [];
789
+ const seenPatterns = new Set<string>();
790
+
791
+ for (let i = 0; i < scopeOptions.length; i++) {
792
+ const opt = scopeOptions[i];
793
+ let description: string;
794
+ let pattern: string;
795
+
796
+ if (i === 0) {
797
+ // Exact match: raw command string (matches the raw candidate)
798
+ description = "This exact command";
799
+ pattern = opt.label;
800
+ } else if (
801
+ opt.label.endsWith(" *") &&
802
+ !opt.label.slice(0, -2).includes(" ")
803
+ ) {
804
+ // Command-level wildcard (label is "<program> *"): use action: prefix
805
+ // to match action key candidates from buildCommandCandidates()
806
+ const prog = opt.label.slice(0, -2);
807
+ description = `Any ${prog} command`;
808
+ pattern = `action:${prog}`;
809
+ } else {
810
+ // Intermediate wildcard: use action:<tokens> pattern to match action key
811
+ // candidates. We can't use the label as a glob directly because the scope
812
+ // ladder reorders args (flags before positionals), but command candidates
813
+ // preserve user arg order.
814
+ const actionPattern = labelToActionPattern(opt.label);
815
+ description = "Commands matching this pattern";
816
+ pattern = actionPattern;
817
+ }
818
+
819
+ // Deduplicate: skip options that produce the same pattern as a prior one
820
+ if (seenPatterns.has(pattern)) continue;
821
+ seenPatterns.add(pattern);
822
+
823
+ results.push({ label: opt.label, description, pattern });
824
+ }
825
+
826
+ return results;
827
+ }
828
+
592
829
  // ── Main classifier ──────────────────────────────────────────────────────────
593
830
 
594
831
  /**
@@ -619,6 +856,7 @@ export class BashRiskClassifier implements RiskClassifier<BashClassifierInput> {
619
856
  reason: "Empty command",
620
857
  scopeOptions: [],
621
858
  matchType: "registry",
859
+ allowlistOptions: [],
622
860
  };
623
861
  }
624
862
 
@@ -678,12 +916,17 @@ export class BashRiskClassifier implements RiskClassifier<BashClassifierInput> {
678
916
  }
679
917
 
680
918
  const scopeOptions = generateScopeOptions(parsed, this.registry);
919
+ const allowlistOptions = scopeOptionsToAllowlistOptions(
920
+ scopeOptions,
921
+ parsed,
922
+ );
681
923
 
682
924
  const assessment: RiskAssessment = {
683
925
  riskLevel: maxRiskLevel,
684
926
  reason: maxReason,
685
927
  scopeOptions,
686
928
  matchType,
929
+ allowlistOptions,
687
930
  };
688
931
 
689
932
  // Risk assessment analytics