@vellumai/assistant 0.8.12 → 0.9.0-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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -18,6 +18,14 @@ mock.module("../config/loader.js", () => ({
18
18
  timezone: null,
19
19
  },
20
20
  }),
21
+ getConfigReadOnly: () => ({
22
+ llm: {
23
+ profiles: {
24
+ balanced: { model: "test-model" },
25
+ "cost-optimized": { model: "test-model-small" },
26
+ },
27
+ },
28
+ }),
21
29
  invalidateConfigCache: () => {},
22
30
  loadRawConfig: () => ({}),
23
31
  saveRawConfig: () => {},
@@ -76,7 +84,7 @@ import { recordUsageEvent } from "../memory/llm-usage-store.js";
76
84
  import { rawRun } from "../memory/raw-query.js";
77
85
  import type { AssistantEvent } from "../runtime/assistant-event.js";
78
86
  import { assistantEventHub } from "../runtime/assistant-event-hub.js";
79
- import { BadRequestError } from "../runtime/routes/errors.js";
87
+ import { BadRequestError, NotFoundError } from "../runtime/routes/errors.js";
80
88
  import { ROUTES as HEARTBEAT_ROUTES } from "../runtime/routes/heartbeat-routes.js";
81
89
  import { ROUTES as SCHEDULE_ROUTES } from "../runtime/routes/schedule-routes.js";
82
90
  import type { RouteDefinition } from "../runtime/routes/types.js";
@@ -587,6 +595,71 @@ describe("GET /schedules — default defer exclusion", () => {
587
595
  });
588
596
  });
589
597
 
598
+ // ── GET /schedules/:id ────────────────────────────────────────────────────
599
+
600
+ describe("GET /schedules/:id", () => {
601
+ beforeEach(() => {
602
+ clearTables();
603
+ });
604
+
605
+ test("returns the full schedule by ID", () => {
606
+ const source = createConversation("Schedule source");
607
+ const schedule = createSchedule({
608
+ name: "Single schedule",
609
+ description: "Fetch me by ID",
610
+ cronExpression: "0 9 * * *",
611
+ message: "review queue",
612
+ syntax: "cron",
613
+ createdFromConversationId: source.id,
614
+ });
615
+
616
+ const route = findRoute("schedules/:id", "GET");
617
+ const result = route.handler({ pathParams: { id: schedule.id } }) as {
618
+ schedule: Record<string, unknown>;
619
+ };
620
+
621
+ expect(result.schedule).toMatchObject({
622
+ id: schedule.id,
623
+ name: "Single schedule",
624
+ description: "Fetch me by ID",
625
+ cadenceDescription: "Every day at 9:00 AM",
626
+ message: "review queue",
627
+ mode: "execute",
628
+ enabled: true,
629
+ isOneShot: false,
630
+ createdFromConversationId: source.id,
631
+ createdFromConversationExists: true,
632
+ createdFromConversationArchivedAt: null,
633
+ });
634
+ });
635
+
636
+ test("returns deferred schedules that the list hides by default", () => {
637
+ const deferred = createSchedule({
638
+ name: "Deferred wake",
639
+ cronExpression: "0 9 * * *",
640
+ message: "wake up",
641
+ syntax: "cron",
642
+ createdBy: "defer",
643
+ });
644
+
645
+ const route = findRoute("schedules/:id", "GET");
646
+ const result = route.handler({ pathParams: { id: deferred.id } }) as {
647
+ schedule: { id: string };
648
+ };
649
+ expect(result.schedule.id).toBe(deferred.id);
650
+ });
651
+
652
+ test("throws NotFoundError for a missing schedule", () => {
653
+ const route = findRoute("schedules/:id", "GET");
654
+ expect(() =>
655
+ route.handler({ pathParams: { id: "missing-schedule" } }),
656
+ ).toThrow(NotFoundError);
657
+ expect(() =>
658
+ route.handler({ pathParams: { id: "missing-schedule" } }),
659
+ ).toThrow("Schedule not found");
660
+ });
661
+ });
662
+
590
663
  // ── schedules/:id/runs limit handling ─────────────────────────────────────
591
664
 
592
665
  describe("schedule runs list — limit handling", () => {
@@ -1076,7 +1149,9 @@ describe("POST /schedules — create", () => {
1076
1149
 
1077
1150
  function postCreate(body: Record<string, unknown>) {
1078
1151
  const route = findRoute("schedules", "POST");
1079
- return route.handler({ body }) as { schedules: Array<{ id: string }> };
1152
+ return route.handler({ body }) as {
1153
+ schedules: Array<{ id: string; inferenceProfile: string | null }>;
1154
+ };
1080
1155
  }
1081
1156
 
1082
1157
  test("creates a recurring execute schedule with defaults", () => {
@@ -1156,7 +1231,7 @@ describe("POST /schedules — create", () => {
1156
1231
  expect(job.description).toBe("Description fallback");
1157
1232
  });
1158
1233
 
1159
- test("rejects non-execute modes", () => {
1234
+ test("rejects modes other than execute/workflow", () => {
1160
1235
  expect(() =>
1161
1236
  postCreate({
1162
1237
  name: "x",
@@ -1165,7 +1240,37 @@ describe("POST /schedules — create", () => {
1165
1240
  message: "hi",
1166
1241
  mode: "notify",
1167
1242
  }),
1168
- ).toThrow("Only 'execute' mode is supported");
1243
+ ).toThrow("Only 'execute' and 'workflow' modes are supported");
1244
+ });
1245
+
1246
+ test("rejects workflow-mode creation when the workflows flag is off", () => {
1247
+ // The mocked getConfig returns no feature flags, so `workflows` is off.
1248
+ expect(() =>
1249
+ postCreate({
1250
+ name: "Triage",
1251
+ description: "Triage the inbox every morning",
1252
+ expression: "0 9 * * *",
1253
+ message: "triage inbox",
1254
+ mode: "workflow",
1255
+ workflowName: "triage-inbox",
1256
+ }),
1257
+ ).toThrow("Workflows are not enabled");
1258
+ });
1259
+
1260
+ test("rejects PATCH switching to workflow mode when the flag is off", () => {
1261
+ const schedule = createSchedule({
1262
+ name: "Plain execute",
1263
+ cronExpression: "0 9 * * *",
1264
+ message: "hi",
1265
+ syntax: "cron",
1266
+ });
1267
+ const patch = findRoute("schedules/:id", "PATCH");
1268
+ expect(() =>
1269
+ patch.handler({
1270
+ pathParams: { id: schedule.id },
1271
+ body: { mode: "workflow", workflowName: "triage-inbox" },
1272
+ }),
1273
+ ).toThrow("Workflows are not enabled");
1169
1274
  });
1170
1275
 
1171
1276
  test("rejects an unparseable expression", () => {
@@ -1190,6 +1295,38 @@ describe("POST /schedules — create", () => {
1190
1295
  ).toThrow();
1191
1296
  });
1192
1297
 
1298
+ test("persists and round-trips workflowName/workflowArgs in the list response", () => {
1299
+ // Store-level round trip: the create route is flag-gated (and the mocked
1300
+ // config disables it), so exercise persistence directly via the store.
1301
+ createSchedule({
1302
+ name: "Workflow schedule",
1303
+ cronExpression: "0 9 * * *",
1304
+ message: "trigger",
1305
+ syntax: "cron",
1306
+ mode: "workflow",
1307
+ workflowName: "triage-inbox",
1308
+ workflowArgs: { folder: "primary", limit: 10 },
1309
+ });
1310
+
1311
+ const route = findRoute("schedules", "GET");
1312
+ const result = route.handler({}) as {
1313
+ schedules: Array<{
1314
+ name: string;
1315
+ mode: string;
1316
+ workflowName: string | null;
1317
+ }>;
1318
+ };
1319
+ const job = result.schedules.find((s) => s.name === "Workflow schedule");
1320
+ expect(job).toBeDefined();
1321
+ expect(job!.mode).toBe("workflow");
1322
+ expect(job!.workflowName).toBe("triage-inbox");
1323
+
1324
+ // workflowArgs is not in the list projection; assert it round-trips at the
1325
+ // store layer (this is what the scheduler reads).
1326
+ const stored = listSchedules().find((s) => s.name === "Workflow schedule")!;
1327
+ expect(stored.workflowArgs).toEqual({ folder: "primary", limit: 10 });
1328
+ });
1329
+
1193
1330
  test("respects enabled=false", () => {
1194
1331
  postCreate({
1195
1332
  name: "Off",
@@ -1201,6 +1338,52 @@ describe("POST /schedules — create", () => {
1201
1338
  const job = listSchedules()[0];
1202
1339
  expect(job.enabled).toBe(false);
1203
1340
  });
1341
+
1342
+ test("persists a valid inferenceProfile and serializes it", () => {
1343
+ const result = postCreate({
1344
+ name: "Cheap digest",
1345
+ description: "High-frequency digest on a cheap model",
1346
+ expression: "0 * * * *",
1347
+ message: "write the digest",
1348
+ inferenceProfile: "cost-optimized",
1349
+ });
1350
+ expect(result.schedules[0].inferenceProfile).toBe("cost-optimized");
1351
+ expect(listSchedules()[0].inferenceProfile).toBe("cost-optimized");
1352
+ });
1353
+
1354
+ test("defaults inferenceProfile to null when omitted", () => {
1355
+ const result = postCreate({
1356
+ name: "Default profile",
1357
+ description: "Runs on the main-agent selection",
1358
+ expression: "0 9 * * *",
1359
+ message: "hi",
1360
+ });
1361
+ expect(result.schedules[0].inferenceProfile).toBeNull();
1362
+ });
1363
+
1364
+ test("rejects an unknown inferenceProfile", () => {
1365
+ expect(() =>
1366
+ postCreate({
1367
+ name: "Bad profile",
1368
+ description: "Unknown profile",
1369
+ expression: "0 9 * * *",
1370
+ message: "hi",
1371
+ inferenceProfile: "does-not-exist",
1372
+ }),
1373
+ ).toThrow('Inference profile "does-not-exist" is not defined');
1374
+ });
1375
+
1376
+ test("rejects a non-string inferenceProfile", () => {
1377
+ expect(() =>
1378
+ postCreate({
1379
+ name: "Bad profile type",
1380
+ description: "Wrong type",
1381
+ expression: "0 9 * * *",
1382
+ message: "hi",
1383
+ inferenceProfile: 42,
1384
+ }),
1385
+ ).toThrow("inferenceProfile must be a string or null");
1386
+ });
1204
1387
  });
1205
1388
 
1206
1389
  // ── PATCH /schedules/:id — description ───────────────────────────────────
@@ -1233,6 +1416,76 @@ describe("PATCH /schedules/:id — description", () => {
1233
1416
  });
1234
1417
  expect(listSchedules()[0].description).toBe("Updated description");
1235
1418
  });
1419
+
1420
+ test("sets, validates, and clears inferenceProfile", () => {
1421
+ const schedule = createSchedule({
1422
+ name: "Profile pin",
1423
+ description: "Pinned profile schedule",
1424
+ cronExpression: "0 9 * * *",
1425
+ message: "hi",
1426
+ syntax: "cron",
1427
+ });
1428
+ const route = findRoute("schedules/:id", "PATCH");
1429
+
1430
+ route.handler({
1431
+ pathParams: { id: schedule.id },
1432
+ body: { inferenceProfile: "balanced" },
1433
+ });
1434
+ expect(listSchedules()[0].inferenceProfile).toBe("balanced");
1435
+
1436
+ expect(() =>
1437
+ route.handler({
1438
+ pathParams: { id: schedule.id },
1439
+ body: { inferenceProfile: "does-not-exist" },
1440
+ }),
1441
+ ).toThrow('Inference profile "does-not-exist" is not defined');
1442
+ expect(listSchedules()[0].inferenceProfile).toBe("balanced");
1443
+
1444
+ route.handler({
1445
+ pathParams: { id: schedule.id },
1446
+ body: { inferenceProfile: null },
1447
+ });
1448
+ expect(listSchedules()[0].inferenceProfile).toBeNull();
1449
+ });
1450
+
1451
+ test("re-derives syntax when the expression switches cron to rrule", () => {
1452
+ const schedule = createSchedule({
1453
+ name: "Syntax switch",
1454
+ description: "Cron schedule",
1455
+ cronExpression: "0 9 * * *",
1456
+ message: "hi",
1457
+ syntax: "cron",
1458
+ });
1459
+
1460
+ const route = findRoute("schedules/:id", "PATCH");
1461
+ const rrule = "DTSTART:20260101T000000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO";
1462
+ route.handler({
1463
+ pathParams: { id: schedule.id },
1464
+ body: { expression: rrule },
1465
+ });
1466
+
1467
+ const updated = listSchedules()[0];
1468
+ expect(updated.syntax).toBe("rrule");
1469
+ expect(updated.cronExpression).toBe(rrule);
1470
+ });
1471
+
1472
+ test("rejects an expression that parses as neither cron nor rrule", () => {
1473
+ const schedule = createSchedule({
1474
+ name: "Bad expression",
1475
+ description: "Cron schedule",
1476
+ cronExpression: "0 9 * * *",
1477
+ message: "hi",
1478
+ syntax: "cron",
1479
+ });
1480
+
1481
+ const route = findRoute("schedules/:id", "PATCH");
1482
+ expect(() =>
1483
+ route.handler({
1484
+ pathParams: { id: schedule.id },
1485
+ body: { expression: "not a schedule" },
1486
+ }),
1487
+ ).toThrow(BadRequestError);
1488
+ });
1236
1489
  });
1237
1490
 
1238
1491
  // ── Wake mode support ─────────────────────────────────────────────────────
@@ -1170,6 +1170,66 @@ describe("script timeout override", () => {
1170
1170
  });
1171
1171
  });
1172
1172
 
1173
+ // ── Capability manifest ─────────────────────────────────────────────
1174
+
1175
+ describe("capability manifest", () => {
1176
+ beforeEach(() => {
1177
+ const db = getDb();
1178
+ db.run("DELETE FROM cron_runs");
1179
+ db.run("DELETE FROM cron_jobs");
1180
+ });
1181
+
1182
+ const manifest = {
1183
+ tools: ["file_write"],
1184
+ hostFunctions: [],
1185
+ persona: false,
1186
+ };
1187
+
1188
+ test("persists a capability manifest through create/read", () => {
1189
+ const job = createSchedule({
1190
+ name: "Capable schedule",
1191
+ cronExpression: "0 9 * * *",
1192
+ message: "do scoped work",
1193
+ syntax: "cron",
1194
+ capabilities: manifest,
1195
+ });
1196
+
1197
+ expect(job.capabilities).toEqual(manifest);
1198
+ expect(getSchedule(job.id)!.capabilities).toEqual(manifest);
1199
+ });
1200
+
1201
+ test("defaults capabilities to null when not provided", () => {
1202
+ const job = createSchedule({
1203
+ name: "No manifest",
1204
+ cronExpression: "0 9 * * *",
1205
+ message: "unconstrained",
1206
+ syntax: "cron",
1207
+ });
1208
+
1209
+ expect(job.capabilities).toBeNull();
1210
+ expect(getSchedule(job.id)!.capabilities).toBeNull();
1211
+ });
1212
+
1213
+ test("updateSchedule sets and clears the capability manifest", () => {
1214
+ const job = createSchedule({
1215
+ name: "Update manifest",
1216
+ cronExpression: "0 9 * * *",
1217
+ message: "update manifest",
1218
+ syntax: "cron",
1219
+ });
1220
+
1221
+ expect(job.capabilities).toBeNull();
1222
+
1223
+ const updated = updateSchedule(job.id, { capabilities: manifest });
1224
+ expect(updated!.capabilities).toEqual(manifest);
1225
+ expect(getSchedule(job.id)!.capabilities).toEqual(manifest);
1226
+
1227
+ const cleared = updateSchedule(job.id, { capabilities: null });
1228
+ expect(cleared!.capabilities).toBeNull();
1229
+ expect(getSchedule(job.id)!.capabilities).toBeNull();
1230
+ });
1231
+ });
1232
+
1173
1233
  // ── listSchedules filters ───────────────────────────────────────────
1174
1234
 
1175
1235
  describe("listSchedules filters", () => {
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
1
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  mock.module("../util/logger.js", () => ({
4
4
  getLogger: () =>
@@ -18,11 +18,18 @@ import type { Database } from "bun:sqlite";
18
18
 
19
19
  import { getDb } from "../memory/db-connection.js";
20
20
  import { initializeDb } from "../memory/db-init.js";
21
+ import { RiskLevel } from "../permissions/types.js";
22
+ import {
23
+ __clearRegistryForTesting,
24
+ __resetRegistryForTesting,
25
+ registerTool,
26
+ } from "../tools/registry.js";
21
27
  import { executeScheduleCreate as rawExecuteScheduleCreate } from "../tools/schedule/create.js";
22
28
  import { executeScheduleDelete } from "../tools/schedule/delete.js";
23
29
  import { executeScheduleList } from "../tools/schedule/list.js";
24
30
  import { executeScheduleUpdate } from "../tools/schedule/update.js";
25
- import type { ToolContext } from "../tools/types.js";
31
+ import type { Tool, ToolContext } from "../tools/types.js";
32
+ import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
26
33
 
27
34
  initializeDb();
28
35
 
@@ -408,6 +415,244 @@ describe("schedule_create with mode and routing", () => {
408
415
  });
409
416
  });
410
417
 
418
+ // ── schedule_create / schedule_update workflow mode ─────────────────
419
+
420
+ describe("schedule tools — workflow mode", () => {
421
+ beforeEach(() => {
422
+ getRawDb().run("DELETE FROM cron_runs");
423
+ getRawDb().run("DELETE FROM cron_jobs");
424
+ setOverridesForTesting({ workflows: true });
425
+ });
426
+ afterAll(() => {
427
+ setOverridesForTesting({});
428
+ });
429
+
430
+ test("creates a workflow-mode schedule with workflow_name + workflow_args", async () => {
431
+ const result = await executeScheduleCreate(
432
+ {
433
+ name: "Morning triage",
434
+ expression: "0 8 * * *",
435
+ mode: "workflow",
436
+ workflow_name: "inbox-triage",
437
+ workflow_args: { limit: 50 },
438
+ },
439
+ ctx,
440
+ );
441
+
442
+ expect(result.isError).toBe(false);
443
+ expect(result.content).toContain("Mode: workflow");
444
+
445
+ const row = getRawDb()
446
+ .query(
447
+ "SELECT mode, workflow_name, workflow_args_json FROM cron_jobs LIMIT 1",
448
+ )
449
+ .get() as {
450
+ mode: string;
451
+ workflow_name: string;
452
+ workflow_args_json: string;
453
+ };
454
+ expect(row.mode).toBe("workflow");
455
+ expect(row.workflow_name).toBe("inbox-triage");
456
+ expect(JSON.parse(row.workflow_args_json)).toEqual({ limit: 50 });
457
+ });
458
+
459
+ test("rejects workflow mode without workflow_name", async () => {
460
+ const result = await executeScheduleCreate(
461
+ { name: "No wf name", expression: "0 8 * * *", mode: "workflow" },
462
+ ctx,
463
+ );
464
+
465
+ expect(result.isError).toBe(true);
466
+ expect(result.content).toContain("workflow_name is required");
467
+ });
468
+
469
+ test("rejects workflow mode when the workflows flag is off", async () => {
470
+ setOverridesForTesting({}); // flag off
471
+ const result = await executeScheduleCreate(
472
+ {
473
+ name: "Flag off",
474
+ expression: "0 8 * * *",
475
+ mode: "workflow",
476
+ workflow_name: "inbox-triage",
477
+ },
478
+ ctx,
479
+ );
480
+
481
+ expect(result.isError).toBe(true);
482
+ expect(result.content).toContain("workflows are not enabled");
483
+ });
484
+
485
+ test("update to workflow mode requires a workflow_name", async () => {
486
+ await executeScheduleCreate(
487
+ { name: "To workflow", expression: "0 9 * * *", message: "test" },
488
+ ctx,
489
+ );
490
+ const { id } = getRawDb()
491
+ .query("SELECT id FROM cron_jobs LIMIT 1")
492
+ .get() as { id: string };
493
+
494
+ const result = await executeScheduleUpdate(
495
+ { job_id: id, mode: "workflow" },
496
+ ctx,
497
+ );
498
+
499
+ expect(result.isError).toBe(true);
500
+ expect(result.content).toContain("workflow_name is required");
501
+ });
502
+
503
+ test("updates a schedule into workflow mode with a workflow_name", async () => {
504
+ await executeScheduleCreate(
505
+ { name: "To workflow ok", expression: "0 9 * * *", message: "test" },
506
+ ctx,
507
+ );
508
+ const { id } = getRawDb()
509
+ .query("SELECT id FROM cron_jobs LIMIT 1")
510
+ .get() as { id: string };
511
+
512
+ const result = await executeScheduleUpdate(
513
+ { job_id: id, mode: "workflow", workflow_name: "nightly-report" },
514
+ ctx,
515
+ );
516
+
517
+ expect(result.isError).toBe(false);
518
+ expect(result.content).toContain("Mode: workflow");
519
+ const dbRow = getRawDb()
520
+ .query("SELECT mode, workflow_name FROM cron_jobs WHERE id = ?")
521
+ .get(id) as { mode: string; workflow_name: string };
522
+ expect(dbRow.mode).toBe("workflow");
523
+ expect(dbRow.workflow_name).toBe("nightly-report");
524
+ });
525
+ });
526
+
527
+ // ── schedule_create workflow capability manifest ────────────────────
528
+
529
+ describe("schedule_create — workflow capability manifest", () => {
530
+ function makeFakeTool(name: string): Tool {
531
+ return {
532
+ name,
533
+ description: `Fake ${name}`,
534
+ category: "test",
535
+ defaultRiskLevel: RiskLevel.Low,
536
+ executionTarget: "sandbox",
537
+ input_schema: { type: "object", properties: {}, required: [] },
538
+ async execute() {
539
+ return { content: "ok", isError: false };
540
+ },
541
+ };
542
+ }
543
+
544
+ beforeEach(() => {
545
+ getRawDb().run("DELETE FROM cron_runs");
546
+ getRawDb().run("DELETE FROM cron_jobs");
547
+ setOverridesForTesting({ workflows: true });
548
+ // Deterministic registry so a declared side-effecting tool resolves.
549
+ __clearRegistryForTesting();
550
+ registerTool(makeFakeTool("file_write"));
551
+ });
552
+ afterAll(() => {
553
+ setOverridesForTesting({});
554
+ __resetRegistryForTesting();
555
+ });
556
+
557
+ test("persists a validated side-effecting manifest verbatim", async () => {
558
+ const result = await executeScheduleCreate(
559
+ {
560
+ name: "Nightly writeback",
561
+ expression: "0 2 * * *",
562
+ mode: "workflow",
563
+ workflow_name: "nightly-report",
564
+ capabilities: { tools: ["file_write"], persona: true },
565
+ },
566
+ ctx,
567
+ );
568
+
569
+ expect(result.isError).toBe(false);
570
+ const row = getRawDb()
571
+ .query("SELECT capabilities_json FROM cron_jobs LIMIT 1")
572
+ .get() as { capabilities_json: string };
573
+ expect(JSON.parse(row.capabilities_json)).toEqual({
574
+ tools: ["file_write"],
575
+ hostFunctions: [],
576
+ persona: true,
577
+ });
578
+ });
579
+
580
+ test("rejects a forbidden manifest at creation", async () => {
581
+ const result = await executeScheduleCreate(
582
+ {
583
+ name: "Recursion vector",
584
+ expression: "0 2 * * *",
585
+ mode: "workflow",
586
+ workflow_name: "nightly-report",
587
+ capabilities: { tools: ["run_workflow"] },
588
+ },
589
+ ctx,
590
+ );
591
+
592
+ expect(result.isError).toBe(true);
593
+ expect(result.content).toContain("invalid capabilities manifest");
594
+ expect(getRawDb().query("SELECT id FROM cron_jobs").all()).toHaveLength(0);
595
+ });
596
+
597
+ test("rejects an unknown tool in the manifest at creation", async () => {
598
+ const result = await executeScheduleCreate(
599
+ {
600
+ name: "Bad tool",
601
+ expression: "0 2 * * *",
602
+ mode: "workflow",
603
+ workflow_name: "nightly-report",
604
+ capabilities: { tools: ["unknown_tool"] },
605
+ },
606
+ ctx,
607
+ );
608
+
609
+ expect(result.isError).toBe(true);
610
+ expect(result.content).toContain("invalid capabilities manifest");
611
+ expect(getRawDb().query("SELECT id FROM cron_jobs").all()).toHaveLength(0);
612
+ });
613
+
614
+ test("a read-only manifest persists as a baseline grant", async () => {
615
+ const result = await executeScheduleCreate(
616
+ {
617
+ name: "Read-only run",
618
+ expression: "0 2 * * *",
619
+ mode: "workflow",
620
+ workflow_name: "nightly-report",
621
+ capabilities: { tools: [], hostFunctions: [], persona: false },
622
+ },
623
+ ctx,
624
+ );
625
+
626
+ expect(result.isError).toBe(false);
627
+ const row = getRawDb()
628
+ .query("SELECT capabilities_json FROM cron_jobs LIMIT 1")
629
+ .get() as { capabilities_json: string };
630
+ expect(JSON.parse(row.capabilities_json)).toEqual({
631
+ tools: [],
632
+ hostFunctions: [],
633
+ persona: false,
634
+ });
635
+ });
636
+
637
+ test("an absent manifest persists no capabilities", async () => {
638
+ const result = await executeScheduleCreate(
639
+ {
640
+ name: "No manifest",
641
+ expression: "0 2 * * *",
642
+ mode: "workflow",
643
+ workflow_name: "nightly-report",
644
+ },
645
+ ctx,
646
+ );
647
+
648
+ expect(result.isError).toBe(false);
649
+ const row = getRawDb()
650
+ .query("SELECT capabilities_json FROM cron_jobs LIMIT 1")
651
+ .get() as { capabilities_json: string | null };
652
+ expect(row.capabilities_json).toBeNull();
653
+ });
654
+ });
655
+
411
656
  // ── script timeout override ─────────────────────────────────────────
412
657
 
413
658
  describe("schedule_create / schedule_update timeout_ms", () => {