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
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from "next/server";
2
+ import { tick } from "@/lib/instance/upgrade-poller";
3
+
4
+ /**
5
+ * POST /api/instance/upgrade/check
6
+ *
7
+ * Force-run the upgrade availability poller. Rate-limited to one call per
8
+ * ~5 minutes via the same lock file the scheduled poller uses. Returns the
9
+ * new UpgradeState on success, or a skipped reason if the lock was held or
10
+ * dev-mode was active.
11
+ */
12
+ export async function POST() {
13
+ try {
14
+ const result = await tick();
15
+ if (result.updated) {
16
+ return NextResponse.json({ ok: true, state: result.updated });
17
+ }
18
+ return NextResponse.json(
19
+ { ok: false, skipped: result.skipped, error: result.error },
20
+ { status: 202 }
21
+ );
22
+ } catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ return NextResponse.json({ error: message }, { status: 500 });
25
+ }
26
+ }
@@ -0,0 +1,96 @@
1
+ import { NextResponse } from "next/server";
2
+ import { randomUUID } from "crypto";
3
+ import { db } from "@/lib/db";
4
+ import { tasks } from "@/lib/db/schema";
5
+ import {
6
+ getInstanceConfig,
7
+ getUpgradeState,
8
+ setUpgradeState,
9
+ } from "@/lib/instance/settings";
10
+
11
+ /**
12
+ * POST /api/instance/upgrade
13
+ *
14
+ * Spawns an upgrade task with the `upgrade-assistant` agent profile. Returns
15
+ * 202 Accepted with the task id; the client then navigates to the upgrade
16
+ * session view to watch streaming progress and respond to conflict prompts.
17
+ *
18
+ * The task description includes the instance context (branch name, commits
19
+ * behind, data directory) as template variables that the profile's SKILL.md
20
+ * references. The claude-agent runtime interpolates them when building the
21
+ * system prompt.
22
+ *
23
+ * Fire-and-forget per TDR-001: the route returns immediately; task execution
24
+ * runs in the background through the existing execution-manager pipeline.
25
+ */
26
+ export async function POST() {
27
+ try {
28
+ const config = getInstanceConfig();
29
+ if (!config) {
30
+ return NextResponse.json(
31
+ { error: "Instance not yet initialized — run POST /api/instance/init first" },
32
+ { status: 409 }
33
+ );
34
+ }
35
+
36
+ const upgrade = getUpgradeState();
37
+ if (!upgrade.upgradeAvailable) {
38
+ return NextResponse.json(
39
+ { error: "No upgrade available", upgradeState: upgrade },
40
+ { status: 409 }
41
+ );
42
+ }
43
+
44
+ const branchName = config.branchName;
45
+ const commitsBehind = upgrade.commitsBehind;
46
+ const dataDir = process.env.STAGENT_DATA_DIR ?? "~/.stagent";
47
+
48
+ const description = [
49
+ `Upgrade instance branch \`${branchName}\` with ${commitsBehind} upstream commit(s) from origin/main.`,
50
+ "",
51
+ "Context for the upgrade-assistant profile:",
52
+ `- INSTANCE_BRANCH=${branchName}`,
53
+ `- COMMITS_BEHIND=${commitsBehind}`,
54
+ `- DATA_DIR=${dataDir}`,
55
+ "",
56
+ "Follow the standard merge flow defined in SKILL.md. Stop and ask the user on any merge conflict. Abort and roll back on any failure. Do not push any branch.",
57
+ ].join("\n");
58
+
59
+ const id = randomUUID();
60
+ const now = new Date();
61
+
62
+ db.insert(tasks)
63
+ .values({
64
+ id,
65
+ title: `Upgrade ${branchName} — ${commitsBehind} upstream commit${commitsBehind === 1 ? "" : "s"}`,
66
+ description,
67
+ projectId: null,
68
+ priority: 1,
69
+ assignedAgent: null,
70
+ agentProfile: "upgrade-assistant",
71
+ sourceType: "manual",
72
+ status: "planned",
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ })
76
+ .run();
77
+
78
+ // Record which task id owns this upgrade so the UI can deep-link to it,
79
+ // and optimistically clear the pending-count so the sidebar badge and
80
+ // settings card reflect the user's intent immediately. If the merge task
81
+ // fails or is cancelled, the next scheduled poll (or a manual "Check for
82
+ // upgrades") will restore the real count by re-running git rev-list.
83
+ await setUpgradeState({
84
+ ...upgrade,
85
+ lastUpgradeTaskId: id,
86
+ commitsBehind: 0,
87
+ upgradeAvailable: false,
88
+ lastSuccessfulUpgradeAt: Math.floor(Date.now() / 1000),
89
+ });
90
+
91
+ return NextResponse.json({ taskId: id }, { status: 202 });
92
+ } catch (err) {
93
+ const message = err instanceof Error ? err.message : String(err);
94
+ return NextResponse.json({ error: message }, { status: 500 });
95
+ }
96
+ }
@@ -0,0 +1,35 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getUpgradeState } from "@/lib/instance/settings";
3
+ import { isDevMode } from "@/lib/instance/detect";
4
+
5
+ /**
6
+ * GET /api/instance/upgrade/status
7
+ *
8
+ * Returns the current UpgradeState for client components that need to poll
9
+ * (e.g. the upgrade modal pre-flight). Server Components should read directly
10
+ * from settings per TDR-004 rather than calling this route.
11
+ *
12
+ * When running on the canonical dev repo, returns a synthetic state with
13
+ * `devMode: true` and `upgradeAvailable: false` so the sidebar upgrade
14
+ * button never renders on main.
15
+ */
16
+ export async function GET() {
17
+ try {
18
+ if (isDevMode()) {
19
+ return NextResponse.json({
20
+ devMode: true,
21
+ lastPolledAt: null,
22
+ upgradeAvailable: false,
23
+ commitsBehind: 0,
24
+ lastSuccessfulUpgradeAt: null,
25
+ pollFailureCount: 0,
26
+ lastPollError: null,
27
+ });
28
+ }
29
+ const state = getUpgradeState();
30
+ return NextResponse.json({ devMode: false, ...state });
31
+ } catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return NextResponse.json({ error: message }, { status: 500 });
34
+ }
35
+ }
@@ -3,9 +3,6 @@ import { db } from "@/lib/db";
3
3
  import { agentMemory } from "@/lib/db/schema";
4
4
  import { and, eq, desc } from "drizzle-orm";
5
5
  import { randomUUID } from "crypto";
6
- import { checkLimit, buildLimitErrorBody } from "@/lib/license/limit-check";
7
- import { getMemoryCount } from "@/lib/license/limit-queries";
8
- import { createTierLimitNotification } from "@/lib/license/notifications";
9
6
 
10
7
  /**
11
8
  * GET /api/memory?profileId=xxx&category=fact&status=active
@@ -78,14 +75,6 @@ export async function POST(req: NextRequest) {
78
75
  );
79
76
  }
80
77
 
81
- // Tier limit check — memory cap per profile
82
- const currentCount = getMemoryCount(profileId);
83
- const limitResult = checkLimit("agentMemories", currentCount);
84
- if (!limitResult.allowed) {
85
- createTierLimitNotification("agentMemories", currentCount, limitResult.limit).catch(() => {});
86
- return NextResponse.json(buildLimitErrorBody("agentMemories", limitResult), { status: 402 });
87
- }
88
-
89
78
  const now = new Date();
90
79
  const id = randomUUID();
91
80
  // Convert 0-1 confidence to 0-1000, default 700
@@ -1,7 +1,9 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { notifications } from "@/lib/db/schema";
4
- import { eq, and, desc, sql, count } from "drizzle-orm";
4
+ import { eq, and, desc, count } from "drizzle-orm";
5
+
6
+ import { buildDefaultNotificationVisibilityCondition } from "@/lib/notifications/visibility";
5
7
 
6
8
  export async function GET(req: NextRequest) {
7
9
  const url = new URL(req.url);
@@ -9,7 +11,7 @@ export async function GET(req: NextRequest) {
9
11
  const type = url.searchParams.get("type");
10
12
  const countOnly = url.searchParams.get("countOnly");
11
13
 
12
- const conditions = [];
14
+ const conditions = [buildDefaultNotificationVisibilityCondition()];
13
15
  if (unread === "true") conditions.push(eq(notifications.read, false));
14
16
  if (type) conditions.push(eq(notifications.type, type as typeof notifications.type.enumValues[number]));
15
17
 
@@ -2,36 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import {
4
4
  projects,
5
- tasks,
6
- workflows,
7
- documents,
8
- schedules,
9
- agentLogs,
10
- notifications,
11
- learnedContext,
12
- usageLedger,
13
- environmentSyncOps,
14
- environmentCheckpoints,
15
- environmentArtifacts,
16
- environmentScans,
17
- chatMessages,
18
- conversations,
19
5
  projectDocumentDefaults,
20
- userTables,
21
- userTableColumns,
22
- userTableRows,
23
- userTableViews,
24
- userTableImports,
25
- userTableRelationships,
26
- tableDocumentInputs,
27
- taskTableInputs,
28
- workflowTableInputs,
29
- scheduleTableInputs,
30
- userTableTriggers,
31
- userTableRowHistory,
32
6
  } from "@/lib/db/schema";
33
- import { eq, inArray } from "drizzle-orm";
7
+ import { eq } from "drizzle-orm";
34
8
  import { updateProjectSchema } from "@/lib/validators/project";
9
+ import { deleteProjectCascade } from "@/lib/data/delete-project";
35
10
 
36
11
  export async function GET(
37
12
  _req: NextRequest,
@@ -109,137 +84,12 @@ export async function DELETE(
109
84
  { params }: { params: Promise<{ id: string }> }
110
85
  ) {
111
86
  const { id } = await params;
112
- const [existing] = await db
113
- .select()
114
- .from(projects)
115
- .where(eq(projects.id, id));
116
-
117
- if (!existing) {
118
- return NextResponse.json({ error: "Not found" }, { status: 404 });
119
- }
120
87
 
121
88
  try {
122
- // Cascade-delete in FK-safe order (children before parents)
123
- // Follows the same pattern as clear.ts and workflow DELETE
124
-
125
- // 1. Collect child IDs for nested FK chains
126
- const taskIds = db
127
- .select({ id: tasks.id })
128
- .from(tasks)
129
- .where(eq(tasks.projectId, id))
130
- .all()
131
- .map((r) => r.id);
132
-
133
- const workflowIds = db
134
- .select({ id: workflows.id })
135
- .from(workflows)
136
- .where(eq(workflows.projectId, id))
137
- .all()
138
- .map((r) => r.id);
139
-
140
- const conversationIds = db
141
- .select({ id: conversations.id })
142
- .from(conversations)
143
- .where(eq(conversations.projectId, id))
144
- .all()
145
- .map((r) => r.id);
146
-
147
- const scanIds = db
148
- .select({ id: environmentScans.id })
149
- .from(environmentScans)
150
- .where(eq(environmentScans.projectId, id))
151
- .all()
152
- .map((r) => r.id);
153
-
154
- const checkpointIds = db
155
- .select({ id: environmentCheckpoints.id })
156
- .from(environmentCheckpoints)
157
- .where(eq(environmentCheckpoints.projectId, id))
158
- .all()
159
- .map((r) => r.id);
160
-
161
- // 2. Environment tables (deepest children first)
162
- if (checkpointIds.length > 0) {
163
- db.delete(environmentSyncOps)
164
- .where(inArray(environmentSyncOps.checkpointId, checkpointIds))
165
- .run();
166
- db.delete(environmentCheckpoints)
167
- .where(inArray(environmentCheckpoints.id, checkpointIds))
168
- .run();
169
- }
170
- if (scanIds.length > 0) {
171
- db.delete(environmentArtifacts)
172
- .where(inArray(environmentArtifacts.scanId, scanIds))
173
- .run();
174
- db.delete(environmentScans)
175
- .where(inArray(environmentScans.id, scanIds))
176
- .run();
89
+ const deleted = deleteProjectCascade(id);
90
+ if (!deleted) {
91
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
177
92
  }
178
-
179
- // 3. Chat tables (messages before conversations)
180
- if (conversationIds.length > 0) {
181
- db.delete(chatMessages)
182
- .where(inArray(chatMessages.conversationId, conversationIds))
183
- .run();
184
- db.delete(conversations)
185
- .where(inArray(conversations.id, conversationIds))
186
- .run();
187
- }
188
-
189
- // 4. Usage ledger (references projectId, workflowId, taskId)
190
- db.delete(usageLedger).where(eq(usageLedger.projectId, id)).run();
191
-
192
- // 5. Task children (logs, notifications, documents, learned context)
193
- if (taskIds.length > 0) {
194
- db.delete(agentLogs).where(inArray(agentLogs.taskId, taskIds)).run();
195
- db.delete(notifications)
196
- .where(inArray(notifications.taskId, taskIds))
197
- .run();
198
- db.delete(documents).where(inArray(documents.taskId, taskIds)).run();
199
- db.delete(learnedContext)
200
- .where(inArray(learnedContext.sourceTaskId, taskIds))
201
- .run();
202
- }
203
-
204
- // 6. Project document defaults (junction table)
205
- db.delete(projectDocumentDefaults).where(eq(projectDocumentDefaults.projectId, id)).run();
206
-
207
- // 6b. User-defined tables — cascade-delete children before parent
208
- const tableIds = db
209
- .select({ id: userTables.id })
210
- .from(userTables)
211
- .where(eq(userTables.projectId, id))
212
- .all()
213
- .map((r) => r.id);
214
-
215
- if (tableIds.length > 0) {
216
- // Junction tables first
217
- db.delete(tableDocumentInputs).where(inArray(tableDocumentInputs.tableId, tableIds)).run();
218
- db.delete(taskTableInputs).where(inArray(taskTableInputs.tableId, tableIds)).run();
219
- db.delete(workflowTableInputs).where(inArray(workflowTableInputs.tableId, tableIds)).run();
220
- db.delete(scheduleTableInputs).where(inArray(scheduleTableInputs.tableId, tableIds)).run();
221
- // Children
222
- db.delete(userTableRowHistory).where(inArray(userTableRowHistory.tableId, tableIds)).run();
223
- db.delete(userTableTriggers).where(inArray(userTableTriggers.tableId, tableIds)).run();
224
- db.delete(userTableImports).where(inArray(userTableImports.tableId, tableIds)).run();
225
- db.delete(userTableViews).where(inArray(userTableViews.tableId, tableIds)).run();
226
- db.delete(userTableRelationships).where(inArray(userTableRelationships.fromTableId, tableIds)).run();
227
- db.delete(userTableRows).where(inArray(userTableRows.tableId, tableIds)).run();
228
- db.delete(userTableColumns).where(inArray(userTableColumns.tableId, tableIds)).run();
229
- db.delete(userTables).where(inArray(userTables.id, tableIds)).run();
230
- }
231
-
232
- // 7. Direct project children
233
- db.delete(documents).where(eq(documents.projectId, id)).run();
234
- db.delete(tasks).where(eq(tasks.projectId, id)).run();
235
- if (workflowIds.length > 0) {
236
- db.delete(workflows).where(inArray(workflows.id, workflowIds)).run();
237
- }
238
- db.delete(schedules).where(eq(schedules.projectId, id)).run();
239
-
240
- // 7. Finally delete the project
241
- db.delete(projects).where(eq(projects.id, id)).run();
242
-
243
93
  return NextResponse.json({ success: true });
244
94
  } catch (err) {
245
95
  console.error("Project delete failed:", err);
@@ -6,14 +6,14 @@ import * as schema from "@/lib/db/schema";
6
6
  /**
7
7
  * Safety-net regression tests for project cascade deletion.
8
8
  *
9
- * These verify that the DELETE handler in projects/[id]/route.ts
10
- * properly handles all FK relationships before deleting a project.
11
- * This prevents the "Failed to delete project" FK constraint error
12
- * that occurs when related records exist.
9
+ * These verify that the shared deleteProjectCascade function in
10
+ * src/lib/data/delete-project.ts properly handles all FK relationships
11
+ * before deleting a project. This prevents "Failed to delete project"
12
+ * FK constraint errors when related records exist.
13
13
  */
14
14
  describe("project DELETE cascade coverage", () => {
15
15
  const deleteRouteSource = readFileSync(
16
- join(__dirname, "..", "[id]", "route.ts"),
16
+ join(__dirname, "..", "..", "..", "..", "lib", "data", "delete-project.ts"),
17
17
  "utf-8"
18
18
  );
19
19
 
@@ -115,7 +115,7 @@ describe("project DELETE cascade coverage", () => {
115
115
  // Find the LAST occurrence of db.delete(child) and FIRST occurrence of db.delete(parent)
116
116
  // within the DELETE function (not the import section)
117
117
  const deleteSection = deleteRouteSource.slice(
118
- deleteRouteSource.indexOf("export async function DELETE")
118
+ deleteRouteSource.indexOf("export function deleteProjectCascade")
119
119
  );
120
120
  const childPos = deleteSection.lastIndexOf(`db.delete(${child})`);
121
121
  const parentPos = deleteSection.indexOf(`db.delete(${parent})`);
@@ -129,21 +129,12 @@ describe("project DELETE cascade coverage", () => {
129
129
  ).toEqual([]);
130
130
  });
131
131
 
132
- it("wraps deletion in try/catch for error handling", () => {
132
+ it("checks project existence before deleting", () => {
133
133
  const deleteSection = deleteRouteSource.slice(
134
- deleteRouteSource.indexOf("export async function DELETE")
134
+ deleteRouteSource.indexOf("export function deleteProjectCascade")
135
135
  );
136
- expect(deleteSection).toContain("try {");
137
- expect(deleteSection).toContain("catch");
138
- expect(deleteSection).toContain("status: 500");
139
- });
140
-
141
- it("verifies project exists before attempting delete", () => {
142
- const deleteSection = deleteRouteSource.slice(
143
- deleteRouteSource.indexOf("export async function DELETE")
144
- );
145
- expect(deleteSection).toContain("Not found");
146
- expect(deleteSection).toContain("status: 404");
136
+ // The shared function checks if the project exists and returns false if not
137
+ expect(deleteSection).toContain("if (!existing) return false");
147
138
  });
148
139
 
149
140
  /**
@@ -0,0 +1,111 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { schedules, tasks, usageLedger } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { claimSlot, countRunningScheduledSlots } from "@/lib/schedules/slot-claim";
6
+ import {
7
+ getScheduleMaxConcurrent,
8
+ getScheduleMaxRunDurationSec,
9
+ } from "@/lib/schedules/config";
10
+ import { randomUUID } from "crypto";
11
+ import { startTaskExecution } from "@/lib/agents/task-dispatch";
12
+
13
+ /**
14
+ * Manually fire a schedule. Honors the global concurrency cap by default.
15
+ * Use `?force=true` to bypass the cap (logged to usage_ledger as
16
+ * "manual_force_bypass" for audit).
17
+ *
18
+ * Security note: force bypass is audit-logged synchronously before task
19
+ * execution begins, so every bypass leaves a permanent record regardless of
20
+ * task outcome.
21
+ */
22
+ export async function POST(
23
+ req: NextRequest,
24
+ { params }: { params: Promise<{ id: string }> },
25
+ ) {
26
+ const { id: scheduleId } = await params;
27
+ const force = req.nextUrl.searchParams.get("force") === "true";
28
+
29
+ const [schedule] = db
30
+ .select()
31
+ .from(schedules)
32
+ .where(eq(schedules.id, scheduleId))
33
+ .all();
34
+ if (!schedule) {
35
+ return NextResponse.json({ error: "schedule_not_found" }, { status: 404 });
36
+ }
37
+
38
+ const taskId = randomUUID();
39
+ const firingNumber = schedule.firingCount + 1;
40
+ const now = new Date();
41
+
42
+ db.insert(tasks)
43
+ .values({
44
+ id: taskId,
45
+ projectId: schedule.projectId,
46
+ workflowId: null,
47
+ scheduleId: schedule.id,
48
+ title: `${schedule.name} — manual firing #${firingNumber}`,
49
+ description: schedule.prompt,
50
+ status: "queued",
51
+ assignedAgent: schedule.assignedAgent,
52
+ agentProfile: schedule.agentProfile,
53
+ priority: 2,
54
+ sourceType: "scheduled",
55
+ maxTurns: schedule.maxTurns,
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ })
59
+ .run();
60
+
61
+ const cap = getScheduleMaxConcurrent();
62
+ const leaseSec = schedule.maxRunDurationSec ?? getScheduleMaxRunDurationSec();
63
+
64
+ // When force=true, pass an effectively infinite cap so the subquery COUNT
65
+ // can never exceed it. This lets `claimSlot` atomically transition the task
66
+ // to "running" even when the real cap is full.
67
+ const effectiveCap = force ? Number.MAX_SAFE_INTEGER : cap;
68
+ const { claimed } = claimSlot(taskId, effectiveCap, leaseSec);
69
+
70
+ if (!claimed) {
71
+ db.delete(tasks).where(eq(tasks.id, taskId)).run();
72
+ const slotEtaSec = 60;
73
+ return NextResponse.json(
74
+ {
75
+ error: "capacity_full",
76
+ message: `Swarm at capacity (${countRunningScheduledSlots()}/${cap}). Retry in ~${slotEtaSec}s or add ?force=true to bypass.`,
77
+ slotEtaSec,
78
+ },
79
+ { status: 429 },
80
+ );
81
+ }
82
+
83
+ // Audit log written synchronously before task execution so that a force
84
+ // bypass is always recorded even if the task itself fails immediately.
85
+ if (force) {
86
+ const nowTs = new Date();
87
+ db.insert(usageLedger)
88
+ .values({
89
+ id: randomUUID(),
90
+ taskId,
91
+ scheduleId: schedule.id,
92
+ projectId: schedule.projectId,
93
+ activityType: "manual_force_bypass",
94
+ runtimeId: "manual",
95
+ providerId: "manual",
96
+ status: "completed",
97
+ costMicros: 0,
98
+ startedAt: nowTs,
99
+ finishedAt: nowTs,
100
+ })
101
+ .run();
102
+ }
103
+
104
+ // Fire-and-forget: the route returns immediately with taskId; execution runs
105
+ // in the background. Errors are logged but do not affect the 200 response.
106
+ startTaskExecution(taskId).catch((err) => {
107
+ console.error(`[api/schedules/execute] task ${taskId} failed:`, err);
108
+ });
109
+
110
+ return NextResponse.json({ taskId, forced: force });
111
+ }
@@ -4,6 +4,7 @@ import { schedules, tasks } from "@/lib/db/schema";
4
4
  import { eq, like } from "drizzle-orm";
5
5
  import { parseInterval, computeNextFireTime } from "@/lib/schedules/interval-parser";
6
6
  import { parseNaturalLanguage } from "@/lib/schedules/nlp-parser";
7
+ import { checkCollision } from "@/lib/schedules/collision-check";
7
8
  import { resolveAgentRuntime } from "@/lib/agents/runtime/catalog";
8
9
  import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
9
10
 
@@ -199,7 +200,14 @@ export async function PATCH(
199
200
  .from(schedules)
200
201
  .where(eq(schedules.id, id));
201
202
 
202
- return NextResponse.json(updated);
203
+ const effectiveCron = (updates.cronExpression as string | undefined) ?? schedule.cronExpression;
204
+ const warnings = checkCollision(
205
+ effectiveCron,
206
+ schedule.avgTurnsPerFiring ?? 0,
207
+ schedule.projectId ?? null,
208
+ schedule.id,
209
+ );
210
+ return NextResponse.json({ schedule: updated, warnings });
203
211
  }
204
212
 
205
213
  export async function DELETE(