stagent 0.9.6 → 0.11.0

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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -10,7 +10,8 @@ import {
10
10
  workflowDocumentInputs,
11
11
  } from "@/lib/db/schema";
12
12
  import { eq, and, desc, inArray, like } from "drizzle-orm";
13
- import { ok, err, type ToolContext } from "./helpers";
13
+ import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
14
+ import { extractKeywords, jaccard } from "@/lib/util/similarity";
14
15
 
15
16
  const VALID_WORKFLOW_STATUSES = [
16
17
  "draft",
@@ -20,6 +21,148 @@ const VALID_WORKFLOW_STATUSES = [
20
21
  "failed",
21
22
  ] as const;
22
23
 
24
+ /**
25
+ * Minimum weighted-Jaccard score for two workflows to count as "near
26
+ * duplicates". Combined score = NAME_WEIGHT * nameJaccard +
27
+ * STEPS_WEIGHT * stepsJaccard.
28
+ *
29
+ * Why weighted-and-split rather than a single pooled Jaccard? A pooled
30
+ * Jaccard over name+step text at threshold 0.7 was flagging legitimate
31
+ * target-entity variants (e.g. "Enrich contacts" vs "Enrich accounts",
32
+ * "Daily standup digest" vs "Weekly standup digest") as duplicates,
33
+ * forcing users to pass `force: true` for every such pair and eroding
34
+ * trust in the guardrail. Splitting the signal lets the one-token
35
+ * difference in names AND prompts contribute to two independent
36
+ * Jaccards, which together pull combined similarity below 0.7 while
37
+ * structural duplicates (identical steps + near-identical name) still
38
+ * exceed the threshold.
39
+ *
40
+ * Tuning rationale:
41
+ * - 0.7 threshold preserved from the original implementation.
42
+ * - 0.5/0.5 weights (no tags). The feature spec sketched a 0.3/0.5/0.2
43
+ * split over name/steps/tags, but workflows do not persist tags in
44
+ * their definition JSON today. Without a tags signal, 0.5/0.5
45
+ * empirically separates legitimate variants (Enrich contacts vs
46
+ * accounts: 0.60; Daily vs Weekly standup: 0.68) from structural
47
+ * duplicates (identical steps with renamed workflow: 0.75+) with
48
+ * headroom on both sides. Revisit weights if tag data lands.
49
+ *
50
+ * If a future false-positive case surfaces, add a regression test in
51
+ * `workflow-tools-dedup.test.ts` → "legitimate variant tolerance" and
52
+ * re-tune rather than bumping `force: true` everywhere.
53
+ *
54
+ * See `features/chat-dedup-variant-tolerance.md`.
55
+ */
56
+ const WORKFLOW_DEDUP_THRESHOLD = 0.7;
57
+ const WORKFLOW_NAME_WEIGHT = 0.5;
58
+ const WORKFLOW_STEPS_WEIGHT = 0.5;
59
+
60
+ /**
61
+ * Split a workflow into its two comparable text signals: the name alone,
62
+ * and a concatenation of every step's name + prompt. Callers pass each
63
+ * signal through `extractKeywords` separately so name-level tokens don't
64
+ * get drowned out by the much larger step-text bag, and vice versa.
65
+ *
66
+ * Malformed definition JSON falls back to `stepsText = ""`.
67
+ */
68
+ function workflowSignals(
69
+ name: string,
70
+ definitionJson: string | null
71
+ ): { nameText: string; stepsText: string } {
72
+ if (!definitionJson) return { nameText: name, stepsText: "" };
73
+ try {
74
+ const def = JSON.parse(definitionJson);
75
+ const stepParts: string[] = [];
76
+ if (Array.isArray(def?.steps)) {
77
+ for (const step of def.steps) {
78
+ if (typeof step?.name === "string") stepParts.push(step.name);
79
+ if (typeof step?.prompt === "string") stepParts.push(step.prompt);
80
+ }
81
+ }
82
+ return { nameText: name, stepsText: stepParts.join(" ") };
83
+ } catch {
84
+ return { nameText: name, stepsText: "" };
85
+ }
86
+ }
87
+
88
+ export interface SimilarWorkflowMatch {
89
+ id: string;
90
+ name: string;
91
+ similarity: number;
92
+ reason: string;
93
+ }
94
+
95
+ /**
96
+ * Find workflows in the same project that look similar to a candidate.
97
+ *
98
+ * Two-tier check:
99
+ * 1. Exact name match (case-insensitive) → similarity 1.0
100
+ * 2. Jaccard similarity over extracted keywords from name+step titles+prompts
101
+ *
102
+ * Returns up to 3 matches with similarity >= WORKFLOW_DEDUP_THRESHOLD,
103
+ * sorted by similarity descending. Used by `create_workflow` to warn the
104
+ * LLM before blindly inserting another row in long conversations where
105
+ * the sliding-window context builder evicts earlier creations.
106
+ *
107
+ * When projectId is null (no active project), returns [] — cross-project
108
+ * dedup would be misleading, and the handful of null-project rows that
109
+ * exist aren't worth de-duplicating against each other.
110
+ */
111
+ export async function findSimilarWorkflows(
112
+ projectId: string | null,
113
+ candidateName: string,
114
+ candidateDefinitionJson: string
115
+ ): Promise<SimilarWorkflowMatch[]> {
116
+ if (!projectId) return [];
117
+
118
+ const existing = await db
119
+ .select({
120
+ id: workflows.id,
121
+ name: workflows.name,
122
+ definition: workflows.definition,
123
+ })
124
+ .from(workflows)
125
+ .where(eq(workflows.projectId, projectId));
126
+
127
+ const matches: SimilarWorkflowMatch[] = [];
128
+ const candidateSignals = workflowSignals(candidateName, candidateDefinitionJson);
129
+ const candidateNameKeywords = extractKeywords(candidateSignals.nameText);
130
+ const candidateStepKeywords = extractKeywords(candidateSignals.stepsText);
131
+ const candidateNameLower = candidateName.trim().toLowerCase();
132
+
133
+ for (const row of existing) {
134
+ // Tier 1: exact name match (case-insensitive)
135
+ if (row.name.trim().toLowerCase() === candidateNameLower) {
136
+ matches.push({
137
+ id: row.id,
138
+ name: row.name,
139
+ similarity: 1,
140
+ reason: `Same name: "${row.name}"`,
141
+ });
142
+ continue;
143
+ }
144
+
145
+ // Tier 2: weighted Jaccard — name and step signals scored separately,
146
+ // then combined with WORKFLOW_NAME_WEIGHT / WORKFLOW_STEPS_WEIGHT so
147
+ // target-entity variants (same verb, different noun) are not flagged.
148
+ const existingSignals = workflowSignals(row.name, row.definition);
149
+ const nameJ = jaccard(candidateNameKeywords, extractKeywords(existingSignals.nameText));
150
+ const stepsJ = jaccard(candidateStepKeywords, extractKeywords(existingSignals.stepsText));
151
+ const similarity =
152
+ WORKFLOW_NAME_WEIGHT * nameJ + WORKFLOW_STEPS_WEIGHT * stepsJ;
153
+ if (similarity >= WORKFLOW_DEDUP_THRESHOLD) {
154
+ matches.push({
155
+ id: row.id,
156
+ name: row.name,
157
+ similarity,
158
+ reason: `Similar content to "${row.name}" (${Math.round(similarity * 100)}%)`,
159
+ });
160
+ }
161
+ }
162
+
163
+ return matches.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
164
+ }
165
+
23
166
  export function workflowTools(ctx: ToolContext) {
24
167
  return [
25
168
  defineTool(
@@ -69,7 +212,7 @@ export function workflowTools(ctx: ToolContext) {
69
212
 
70
213
  defineTool(
71
214
  "create_workflow",
72
- "Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array.",
215
+ "Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array. Sequence-pattern steps can be either task steps (with prompt + assignedAgent/agentProfile) or delay steps (with delayDuration like '3d', '2h', '30m', '1w') that pause the workflow between tasks — use delay steps for time-distributed sequences (outreach cadences, drip campaigns, cooling periods) rather than creating separate workflows or schedules. IMPORTANT: for the 'run agent on every row of a table' pattern, prefer enrich_table over create_workflow — enrich_table generates the optimal loop configuration, binds each row as {{row.field}} context, wires up the postAction row writeback, and handles idempotent skip of already-populated rows. Hand-rolled equivalents miss these safeguards.",
73
216
  {
74
217
  name: z.string().min(1).max(200).describe("Workflow name"),
75
218
  projectId: z
@@ -79,7 +222,11 @@ export function workflowTools(ctx: ToolContext) {
79
222
  definition: z
80
223
  .string()
81
224
  .describe(
82
- 'Workflow definition as JSON string. Must include "pattern" and "steps" array. Example: {"pattern":"sequence","steps":[{"id":"step-1","name":"step1","prompt":"Do X","assignedAgent":"claude"}]}'
225
+ 'Workflow definition as JSON string. Must include "pattern" and "steps" array. ' +
226
+ 'Task step example: {"id":"s1","name":"Research","prompt":"Do X","assignedAgent":"claude"}. ' +
227
+ 'Delay step example (sequence pattern only): {"id":"s2","name":"Wait 3 days","delayDuration":"3d"}. ' +
228
+ 'A complete drip sequence: {"pattern":"sequence","steps":[{"id":"s1","name":"Initial","prompt":"Send first email","assignedAgent":"claude"},{"id":"s2","name":"Wait","delayDuration":"3d"},{"id":"s3","name":"Follow-up","prompt":"Send follow-up","assignedAgent":"claude"}]}. ' +
229
+ 'Delay bounds: 1m to 30d. Delay steps must NOT have prompt/profile fields.'
83
230
  ),
84
231
  documentIds: z
85
232
  .array(z.string())
@@ -93,6 +240,12 @@ export function workflowTools(ctx: ToolContext) {
93
240
  .describe(
94
241
  "Runtime to use for workflow execution (e.g., 'openai-direct', 'anthropic-direct'). Use list_runtimes to see available options. Omit to use the system default."
95
242
  ),
243
+ force: z
244
+ .boolean()
245
+ .optional()
246
+ .describe(
247
+ "Set to true to bypass the near-duplicate check and always create a new workflow. Only use this when the user has explicitly confirmed they want a second workflow alongside a similar existing one (e.g., 'v2', 'alternate approach'). The dedup check already tolerates target-entity variants (e.g., 'Enrich contacts' vs 'Enrich accounts', 'Daily' vs 'Weekly' standup digest) — so you should NOT pass force=true for those. Default false."
248
+ ),
96
249
  },
97
250
  async (args) => {
98
251
  try {
@@ -127,6 +280,28 @@ export function workflowTools(ctx: ToolContext) {
127
280
  }
128
281
 
129
282
  const effectiveProjectId = args.projectId ?? ctx.projectId ?? null;
283
+
284
+ // Dedup guard: long chat conversations can truncate the earlier
285
+ // create_workflow tool call out of the sliding-window context, so
286
+ // the LLM loses its own history and re-creates on "redesign"
287
+ // requests. Check for near-duplicates in the same project before
288
+ // inserting. Pass force=true to bypass.
289
+ if (!args.force) {
290
+ const similar = await findSimilarWorkflows(
291
+ effectiveProjectId,
292
+ args.name,
293
+ args.definition
294
+ );
295
+ if (similar.length > 0) {
296
+ return ok({
297
+ status: "similar-found",
298
+ message:
299
+ "Found similar workflow(s) in this project. Use update_workflow to modify an existing one, or pass force=true to create a new workflow alongside them.",
300
+ matches: similar,
301
+ });
302
+ }
303
+ }
304
+
130
305
  const now = new Date();
131
306
  const id = crypto.randomUUID();
132
307
 
@@ -211,13 +386,17 @@ export function workflowTools(ctx: ToolContext) {
211
386
  },
212
387
  async (args) => {
213
388
  try {
214
- const workflow = await db
389
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
390
+ if ("error" in resolved) return err(resolved.error);
391
+ const workflowId = resolved.id;
392
+
393
+ const workflow = db
215
394
  .select()
216
395
  .from(workflows)
217
- .where(eq(workflows.id, args.workflowId))
396
+ .where(eq(workflows.id, workflowId))
218
397
  .get();
219
398
 
220
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
399
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
221
400
 
222
401
  const { parseWorkflowState } = await import("@/lib/workflows/engine");
223
402
  const { definition, state } = parseWorkflowState(workflow.definition);
@@ -266,13 +445,17 @@ export function workflowTools(ctx: ToolContext) {
266
445
  },
267
446
  async (args) => {
268
447
  try {
269
- const existing = await db
448
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
449
+ if ("error" in resolved) return err(resolved.error);
450
+ const workflowId = resolved.id;
451
+
452
+ const existing = db
270
453
  .select()
271
454
  .from(workflows)
272
- .where(eq(workflows.id, args.workflowId))
455
+ .where(eq(workflows.id, workflowId))
273
456
  .get();
274
457
 
275
- if (!existing) return err(`Workflow not found: ${args.workflowId}`);
458
+ if (!existing) return err(`Workflow not found: ${workflowId}`);
276
459
  if (existing.status !== "draft")
277
460
  return err(`Cannot edit a workflow in '${existing.status}' status. Only draft workflows can be edited.`);
278
461
 
@@ -293,12 +476,12 @@ export function workflowTools(ctx: ToolContext) {
293
476
  await db
294
477
  .update(workflows)
295
478
  .set(updates)
296
- .where(eq(workflows.id, args.workflowId));
479
+ .where(eq(workflows.id, workflowId));
297
480
 
298
481
  const [workflow] = await db
299
482
  .select()
300
483
  .from(workflows)
301
- .where(eq(workflows.id, args.workflowId));
484
+ .where(eq(workflows.id, workflowId));
302
485
 
303
486
  ctx.onToolResult?.("update_workflow", workflow);
304
487
  return ok({
@@ -321,13 +504,17 @@ export function workflowTools(ctx: ToolContext) {
321
504
  },
322
505
  async (args) => {
323
506
  try {
324
- const existing = await db
507
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
508
+ if ("error" in resolved) return err(resolved.error);
509
+ const workflowId = resolved.id;
510
+
511
+ const existing = db
325
512
  .select()
326
513
  .from(workflows)
327
- .where(eq(workflows.id, args.workflowId))
514
+ .where(eq(workflows.id, workflowId))
328
515
  .get();
329
516
 
330
- if (!existing) return err(`Workflow not found: ${args.workflowId}`);
517
+ if (!existing) return err(`Workflow not found: ${workflowId}`);
331
518
  if (existing.status === "active")
332
519
  return err("Cannot delete an active workflow. Pause or stop it first.");
333
520
 
@@ -335,7 +522,7 @@ export function workflowTools(ctx: ToolContext) {
335
522
  const childTasks = await db
336
523
  .select({ id: tasks.id })
337
524
  .from(tasks)
338
- .where(eq(tasks.workflowId, args.workflowId));
525
+ .where(eq(tasks.workflowId, workflowId));
339
526
 
340
527
  const taskIds = childTasks.map((t) => t.id);
341
528
  for (const taskId of taskIds) {
@@ -343,10 +530,10 @@ export function workflowTools(ctx: ToolContext) {
343
530
  await db.delete(agentLogs).where(eq(agentLogs.taskId, taskId));
344
531
  await db.delete(documents).where(eq(documents.taskId, taskId));
345
532
  }
346
- await db.delete(tasks).where(eq(tasks.workflowId, args.workflowId));
347
- await db.delete(workflows).where(eq(workflows.id, args.workflowId));
533
+ await db.delete(tasks).where(eq(tasks.workflowId, workflowId));
534
+ await db.delete(workflows).where(eq(workflows.id, workflowId));
348
535
 
349
- return ok({ message: "Workflow deleted", workflowId: args.workflowId, name: existing.name });
536
+ return ok({ message: "Workflow deleted", workflowId, name: existing.name });
350
537
  } catch (e) {
351
538
  return err(e instanceof Error ? e.message : "Failed to delete workflow");
352
539
  }
@@ -361,13 +548,17 @@ export function workflowTools(ctx: ToolContext) {
361
548
  },
362
549
  async (args) => {
363
550
  try {
364
- const workflow = await db
551
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
552
+ if ("error" in resolved) return err(resolved.error);
553
+ const workflowId = resolved.id;
554
+
555
+ const workflow = db
365
556
  .select()
366
557
  .from(workflows)
367
- .where(eq(workflows.id, args.workflowId))
558
+ .where(eq(workflows.id, workflowId))
368
559
  .get();
369
560
 
370
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
561
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
371
562
 
372
563
  // Allow re-execution from crashed "active" if no live tasks
373
564
  if (workflow.status === "active") {
@@ -376,7 +567,7 @@ export function workflowTools(ctx: ToolContext) {
376
567
  .from(tasks)
377
568
  .where(
378
569
  and(
379
- eq(tasks.workflowId, args.workflowId),
570
+ eq(tasks.workflowId, workflowId),
380
571
  inArray(tasks.status, ["running", "queued"])
381
572
  )
382
573
  );
@@ -404,7 +595,7 @@ export function workflowTools(ctx: ToolContext) {
404
595
  .set({ status: "cancelled", updatedAt: new Date() })
405
596
  .where(
406
597
  and(
407
- eq(tasks.workflowId, args.workflowId),
598
+ eq(tasks.workflowId, workflowId),
408
599
  inArray(tasks.status, ["running", "queued"])
409
600
  )
410
601
  );
@@ -419,27 +610,71 @@ export function workflowTools(ctx: ToolContext) {
419
610
  status: "draft",
420
611
  updatedAt: new Date(),
421
612
  })
422
- .where(eq(workflows.id, args.workflowId));
613
+ .where(eq(workflows.id, workflowId));
423
614
  }
424
615
 
425
616
  // Atomic claim: set to active
426
617
  await db
427
618
  .update(workflows)
428
619
  .set({ status: "active", updatedAt: new Date() })
429
- .where(eq(workflows.id, args.workflowId));
620
+ .where(eq(workflows.id, workflowId));
430
621
 
431
622
  // Fire-and-forget
432
623
  const { executeWorkflow } = await import("@/lib/workflows/engine");
433
- executeWorkflow(args.workflowId).catch(() => {});
624
+ executeWorkflow(workflowId).catch(() => {});
434
625
 
435
- ctx.onToolResult?.("execute_workflow", { id: args.workflowId, name: workflow.name });
436
- return ok({ message: "Workflow execution started", workflowId: args.workflowId, name: workflow.name });
626
+ ctx.onToolResult?.("execute_workflow", { id: workflowId, name: workflow.name });
627
+ return ok({ message: "Workflow execution started", workflowId, name: workflow.name });
437
628
  } catch (e) {
438
629
  return err(e instanceof Error ? e.message : "Failed to execute workflow");
439
630
  }
440
631
  }
441
632
  ),
442
633
 
634
+ defineTool(
635
+ "resume_workflow",
636
+ "Resume a workflow that is paused at a delay step, immediately skipping the remaining delay. Use when the user says 'resume now' or 'skip the wait' for a paused workflow. Only works if the workflow status is 'paused' — a 409 response means the scheduler already resumed it. Requires approval.",
637
+ {
638
+ workflowId: z.string().describe("The workflow ID to resume"),
639
+ },
640
+ async (args) => {
641
+ try {
642
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
643
+ if ("error" in resolved) return err(resolved.error);
644
+ const workflowId = resolved.id;
645
+
646
+ const workflow = db
647
+ .select()
648
+ .from(workflows)
649
+ .where(eq(workflows.id, workflowId))
650
+ .get();
651
+
652
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
653
+
654
+ if (workflow.status !== "paused") {
655
+ return err(
656
+ `Workflow is not paused (current status: ${workflow.status}). Only paused workflows can be resumed.`,
657
+ );
658
+ }
659
+
660
+ const { resumeWorkflow } = await import("@/lib/workflows/engine");
661
+ // Fire-and-forget: resumeWorkflow performs atomic status transition internally.
662
+ resumeWorkflow(workflowId).catch((error) => {
663
+ console.error(`Workflow ${workflowId} resume failed:`, error);
664
+ });
665
+
666
+ ctx.onToolResult?.("resume_workflow", { id: workflowId, name: workflow.name });
667
+ return ok({
668
+ message: "Workflow resume dispatched",
669
+ workflowId,
670
+ name: workflow.name,
671
+ });
672
+ } catch (e) {
673
+ return err(e instanceof Error ? e.message : "Failed to resume workflow");
674
+ }
675
+ },
676
+ ),
677
+
443
678
  defineTool(
444
679
  "get_workflow_status",
445
680
  "Get the current execution status of a workflow, including step-by-step progress.",
@@ -448,13 +683,17 @@ export function workflowTools(ctx: ToolContext) {
448
683
  },
449
684
  async (args) => {
450
685
  try {
451
- const workflow = await db
686
+ const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
687
+ if ("error" in resolved) return err(resolved.error);
688
+ const workflowId = resolved.id;
689
+
690
+ const workflow = db
452
691
  .select()
453
692
  .from(workflows)
454
- .where(eq(workflows.id, args.workflowId))
693
+ .where(eq(workflows.id, workflowId))
455
694
  .get();
456
695
 
457
- if (!workflow) return err(`Workflow not found: ${args.workflowId}`);
696
+ if (!workflow) return err(`Workflow not found: ${workflowId}`);
458
697
 
459
698
  const { parseWorkflowState } = await import("@/lib/workflows/engine");
460
699
  const { definition, state } = parseWorkflowState(workflow.definition);
@@ -531,11 +770,15 @@ export function workflowTools(ctx: ToolContext) {
531
770
  }
532
771
 
533
772
  if (args.sourceWorkflowId) {
773
+ const resolvedSrc = await resolveEntityId(workflows, workflows.id, args.sourceWorkflowId);
774
+ if ("error" in resolvedSrc) return err(resolvedSrc.error);
775
+ const srcWorkflowId = resolvedSrc.id;
776
+
534
777
  // Find task IDs belonging to the source workflow
535
778
  const workflowTasks = await db
536
779
  .select({ id: tasks.id })
537
780
  .from(tasks)
538
- .where(eq(tasks.workflowId, args.sourceWorkflowId));
781
+ .where(eq(tasks.workflowId, srcWorkflowId));
539
782
 
540
783
  const taskIds = workflowTasks.map((t) => t.id);
541
784
  if (taskIds.length > 0) {
@@ -1,3 +1,9 @@
1
+ import {
2
+ getRuntimeFeatures,
3
+ resolveAgentRuntime,
4
+ type RuntimeFeatures,
5
+ } from "@/lib/agents/runtime/catalog";
6
+
1
7
  /** Screenshot attachment metadata stored in message metadata.attachments */
2
8
  export interface ScreenshotAttachment {
3
9
  documentId: string;
@@ -107,6 +113,15 @@ export function getRuntimeForModel(modelId: string): string {
107
113
  return /^(gpt|o\d)/.test(modelId) ? "openai-codex-app-server" : "claude-code";
108
114
  }
109
115
 
116
+ /**
117
+ * Model → LLM-surface features. Thin wrapper around getRuntimeForModel +
118
+ * getRuntimeFeatures so chat callers don't need to know runtime IDs.
119
+ */
120
+ export function getFeaturesForModel(modelId: string): RuntimeFeatures {
121
+ const runtimeId = resolveAgentRuntime(getRuntimeForModel(modelId));
122
+ return getRuntimeFeatures(runtimeId);
123
+ }
124
+
110
125
  /** Suggested prompt category with expandable sub-prompts */
111
126
  export interface PromptCategory {
112
127
  id: string;
@@ -2,8 +2,12 @@ export const SETTINGS_KEYS = {
2
2
  AUTH_METHOD: "auth.method",
3
3
  AUTH_API_KEY: "auth.apiKey",
4
4
  AUTH_API_KEY_SOURCE: "auth.apiKeySource",
5
+ OPENAI_AUTH_METHOD: "openai.authMethod",
5
6
  OPENAI_AUTH_API_KEY: "openai.authApiKey",
6
7
  OPENAI_AUTH_API_KEY_SOURCE: "openai.authApiKeySource",
8
+ OPENAI_AUTH_OAUTH_CONNECTED: "openai.oauthConnected",
9
+ OPENAI_AUTH_ACCOUNT: "openai.account",
10
+ OPENAI_AUTH_RATE_LIMITS: "openai.rateLimits",
7
11
  PERMISSIONS_ALLOW: "permissions.allow",
8
12
  BUDGET_POLICY: "usage.budgetPolicy",
9
13
  BUDGET_WARNING_STATE: "usage.budgetWarningState",
@@ -19,24 +23,12 @@ export const SETTINGS_KEYS = {
19
23
  ROUTING_PREFERENCE: "routing.preference",
20
24
  OLLAMA_BASE_URL: "ollama.baseUrl",
21
25
  OLLAMA_DEFAULT_MODEL: "ollama.defaultModel",
22
- // License / PLG
23
- LICENSE_TIER: "license.tier",
24
- LICENSE_EMAIL: "license.email",
25
- LICENSE_ACTIVATED_AT: "license.activatedAt",
26
- LICENSE_EXPIRES_AT: "license.expiresAt",
27
- LICENSE_GRACE_UNTIL: "license.graceUntil",
28
- // Supabase cloud
29
- SUPABASE_URL: "cloud.supabaseUrl",
30
- SUPABASE_ANON_KEY: "cloud.supabaseAnonKey",
31
- // Telemetry (opt-in)
32
- TELEMETRY_ENABLED: "telemetry.enabled",
33
- TELEMETRY_RUNTIME_ID: "telemetry.runtimeId",
34
- TELEMETRY_BATCH: "telemetry.batch",
35
- // Cloud sync
36
- DEVICE_ID: "sync.deviceId",
37
- LAST_SYNC_AT: "sync.lastSyncAt",
38
- // Stripe
39
- STRIPE_CUSTOMER_ID: "billing.stripeCustomerId",
26
+ // Schedule orchestration
27
+ SCHEDULE_MAX_CONCURRENT: "schedule.maxConcurrent",
28
+ SCHEDULE_MAX_RUN_DURATION_SEC: "schedule.maxRunDurationSec",
29
+ SCHEDULE_CHAT_PRESSURE_DELAY_SEC: "schedule.chatPressureDelaySec",
30
+ // Environment / profile sync
31
+ AUTO_PROMOTE_SKILLS: "environment.autoPromoteSkills",
40
32
  } as const;
41
33
 
42
34
  export type RoutingPreference = "cost" | "latency" | "quality" | "manual";
@@ -2,10 +2,16 @@ import { describe, expect, it } from "vitest";
2
2
  import { readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import * as schema from "@/lib/db/schema";
5
+ import { db } from "@/lib/db";
6
+ import { conversations, documents } from "@/lib/db/schema";
7
+ import { clearAllData } from "../clear";
5
8
 
6
9
  /**
7
10
  * Safety-net test: every table exported from schema.ts must appear in clear.ts
8
- * (except `settings`, which is intentionally preserved across clears).
11
+ * (except tables in INTENTIONALLY_PRESERVED, which are kept across clears:
12
+ * - settings: auth config
13
+ * - snapshots: backups, not working data
14
+ * - license: paid tier activation — clearing data must not silently downgrade)
9
15
  *
10
16
  * When you add a new table to schema.ts, this test will fail until you add a
11
17
  * corresponding db.delete() call to clear.ts in the correct FK-safe order.
@@ -13,7 +19,7 @@ import * as schema from "@/lib/db/schema";
13
19
  describe("clearAllData coverage", () => {
14
20
  const INTENTIONALLY_PRESERVED = ["settings", "snapshots"];
15
21
 
16
- it("deletes every schema table (except settings)", () => {
22
+ it("deletes every schema table (except preserved ones)", () => {
17
23
  const clearSource = readFileSync(
18
24
  join(__dirname, "..", "clear.ts"),
19
25
  "utf-8"
@@ -40,3 +46,51 @@ describe("clearAllData coverage", () => {
40
46
  expect(missing, `Tables missing from clear.ts: ${missing.join(", ")}`).toEqual([]);
41
47
  });
42
48
  });
49
+
50
+ /**
51
+ * FK ordering regression: `documents.conversation_id` references `conversations.id`.
52
+ * If clearAllData deletes `conversations` before `documents`, SQLite raises
53
+ * FOREIGN KEY constraint failed. This test seeds a document attached to a
54
+ * conversation and then calls clearAllData to ensure the ordering holds.
55
+ *
56
+ * Incident: the stagent-growth domain clone (2026-04-07) hit this because its
57
+ * seeded data included chat-attached documents.
58
+ */
59
+ describe("clearAllData FK ordering", () => {
60
+ it("clears a conversation that has an attached document without FK violation", () => {
61
+ const now = new Date();
62
+ const conversationId = "test-conv-fk-ordering";
63
+ const documentId = "test-doc-fk-ordering";
64
+
65
+ db.insert(conversations)
66
+ .values({
67
+ id: conversationId,
68
+ runtimeId: "test-runtime",
69
+ status: "active",
70
+ createdAt: now,
71
+ updatedAt: now,
72
+ })
73
+ .run();
74
+
75
+ db.insert(documents)
76
+ .values({
77
+ id: documentId,
78
+ filename: "fk-ordering-test.txt",
79
+ originalName: "fk-ordering-test.txt",
80
+ mimeType: "text/plain",
81
+ size: 10,
82
+ storagePath: "/tmp/fk-ordering-test.txt",
83
+ conversationId,
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ })
87
+ .run();
88
+
89
+ expect(() => clearAllData()).not.toThrow();
90
+
91
+ const remainingConvs = db.select().from(conversations).all();
92
+ const remainingDocs = db.select().from(documents).all();
93
+ expect(remainingConvs).toHaveLength(0);
94
+ expect(remainingDocs).toHaveLength(0);
95
+ });
96
+ });