stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -1,17 +1,39 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { readStagentCodexAuthState } from "@/lib/agents/runtime/openai-codex-auth";
2
3
  import { getRuntimeSetupStates } from "@/lib/settings/runtime-setup";
3
4
  import { getRoutingPreference } from "@/lib/settings/routing";
4
5
  import { getAuthSettings } from "@/lib/settings/auth";
5
6
  import { getOpenAIAuthSettings } from "@/lib/settings/openai-auth";
7
+ import { getOpenAILoginState } from "@/lib/settings/openai-login-manager";
6
8
 
7
9
  export async function GET() {
8
- const [runtimeStates, routingPreference, anthropicAuth, openaiAuth] =
9
- await Promise.all([
10
- getRuntimeSetupStates(),
11
- getRoutingPreference(),
12
- getAuthSettings(),
13
- getOpenAIAuthSettings(),
14
- ]);
10
+ const [routingPreference, anthropicAuth, initialOpenaiAuth] = await Promise.all([
11
+ getRoutingPreference(),
12
+ getAuthSettings(),
13
+ getOpenAIAuthSettings(),
14
+ ]);
15
+
16
+ let openaiAuth = initialOpenaiAuth;
17
+ if (openaiAuth.method === "oauth") {
18
+ try {
19
+ const current = await readStagentCodexAuthState({ refreshToken: true });
20
+ openaiAuth = {
21
+ ...openaiAuth,
22
+ oauthConnected: current.connected,
23
+ account: current.account,
24
+ rateLimits: current.rateLimits,
25
+ };
26
+ } catch {
27
+ openaiAuth = {
28
+ ...openaiAuth,
29
+ oauthConnected: false,
30
+ account: null,
31
+ rateLimits: null,
32
+ };
33
+ }
34
+ }
35
+
36
+ const runtimeStates = await getRuntimeSetupStates();
15
37
 
16
38
  const anthropicConfigured =
17
39
  runtimeStates["claude-code"].configured ||
@@ -42,9 +64,14 @@ export async function GET() {
42
64
  },
43
65
  openai: {
44
66
  configured: openaiConfigured,
67
+ authMethod: openaiAuth.method,
45
68
  hasKey: openaiAuth.hasKey,
46
69
  apiKeySource: openaiAuth.apiKeySource,
47
- dualBilling: false,
70
+ oauthConnected: openaiAuth.oauthConnected,
71
+ account: openaiAuth.account,
72
+ rateLimits: openaiAuth.rateLimits,
73
+ login: getOpenAILoginState(),
74
+ dualBilling: openaiAuth.oauthConnected && openaiAuth.hasKey,
48
75
  runtimes: [
49
76
  runtimeStates["openai-codex-app-server"],
50
77
  runtimeStates["openai-direct"],
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const { mockCreateEnrichmentWorkflow } = vi.hoisted(() => ({
4
+ mockCreateEnrichmentWorkflow: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("@/lib/tables/enrichment", () => ({
8
+ createEnrichmentWorkflow: mockCreateEnrichmentWorkflow,
9
+ }));
10
+
11
+ import { POST } from "../route";
12
+
13
+ function makeRequest(body: unknown): Request {
14
+ return new Request("http://test/api/tables/tbl_x/enrich", {
15
+ method: "POST",
16
+ headers: { "content-type": "application/json" },
17
+ body: JSON.stringify(body),
18
+ });
19
+ }
20
+
21
+ const params = Promise.resolve({ id: "tbl_x" });
22
+
23
+ describe("POST /api/tables/[id]/enrich", () => {
24
+ beforeEach(() => {
25
+ mockCreateEnrichmentWorkflow.mockReset();
26
+ });
27
+
28
+ it("rejects requests with missing required fields (400)", async () => {
29
+ const res = await POST(makeRequest({}) as never, {
30
+ params,
31
+ });
32
+ expect(res.status).toBe(400);
33
+ expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
34
+ });
35
+
36
+ it("returns 202 with workflowId and rowCount on success", async () => {
37
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
38
+ workflowId: "wf_123",
39
+ rowCount: 3,
40
+ });
41
+
42
+ const res = await POST(
43
+ makeRequest({
44
+ prompt: "Find LinkedIn for {{row.name}}",
45
+ targetColumn: "linkedin",
46
+ }) as never,
47
+ { params }
48
+ );
49
+
50
+ expect(res.status).toBe(202);
51
+ const json = (await res.json()) as { workflowId: string; rowCount: number };
52
+ expect(json.workflowId).toBe("wf_123");
53
+ expect(json.rowCount).toBe(3);
54
+ expect(mockCreateEnrichmentWorkflow).toHaveBeenCalledWith(
55
+ "tbl_x",
56
+ expect.objectContaining({
57
+ prompt: "Find LinkedIn for {{row.name}}",
58
+ targetColumn: "linkedin",
59
+ })
60
+ );
61
+ });
62
+
63
+ it("caps batchSize to 200 before delegating", async () => {
64
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
65
+ workflowId: "wf_456",
66
+ rowCount: 200,
67
+ });
68
+
69
+ await POST(
70
+ makeRequest({
71
+ prompt: "Enrich {{row.name}}",
72
+ targetColumn: "linkedin",
73
+ batchSize: 5000,
74
+ }) as never,
75
+ { params }
76
+ );
77
+
78
+ const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
79
+ batchSize: number;
80
+ };
81
+ expect(callArg.batchSize).toBe(200);
82
+ });
83
+
84
+ it("rejects batchSize less than 1", async () => {
85
+ const res = await POST(
86
+ makeRequest({
87
+ prompt: "x",
88
+ targetColumn: "linkedin",
89
+ batchSize: 0,
90
+ }) as never,
91
+ { params }
92
+ );
93
+ expect(res.status).toBe(400);
94
+ expect(mockCreateEnrichmentWorkflow).not.toHaveBeenCalled();
95
+ });
96
+
97
+ it("returns 404 when the table is missing", async () => {
98
+ mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
99
+ new Error("Table tbl_x not found")
100
+ );
101
+
102
+ const res = await POST(
103
+ makeRequest({
104
+ prompt: "x",
105
+ targetColumn: "linkedin",
106
+ }) as never,
107
+ { params }
108
+ );
109
+ expect(res.status).toBe(404);
110
+ });
111
+
112
+ it("returns 400 when the column does not exist on the table", async () => {
113
+ mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
114
+ new Error('Column "ghost" does not exist on table tbl_x')
115
+ );
116
+
117
+ const res = await POST(
118
+ makeRequest({
119
+ prompt: "x",
120
+ targetColumn: "ghost",
121
+ }) as never,
122
+ { params }
123
+ );
124
+ expect(res.status).toBe(400);
125
+ });
126
+
127
+ it("forwards filter, agentProfile, and projectId to the generator", async () => {
128
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
129
+ workflowId: "wf_789",
130
+ rowCount: 1,
131
+ });
132
+
133
+ await POST(
134
+ makeRequest({
135
+ prompt: "x",
136
+ targetColumn: "linkedin",
137
+ filter: { column: "linkedin", operator: "is_empty" },
138
+ agentProfile: "researcher",
139
+ projectId: "proj_1",
140
+ }) as never,
141
+ { params }
142
+ );
143
+
144
+ const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
145
+ filter: unknown;
146
+ agentProfile: string;
147
+ projectId: string;
148
+ };
149
+ expect(callArg.filter).toEqual({ column: "linkedin", operator: "is_empty" });
150
+ expect(callArg.agentProfile).toBe("researcher");
151
+ expect(callArg.projectId).toBe("proj_1");
152
+ });
153
+ });
@@ -0,0 +1,98 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { previewEnrichmentPlan } from "@/lib/tables/enrichment";
4
+
5
+ const MAX_BATCH_SIZE = 200;
6
+
7
+ const filterSchema = z.object({
8
+ column: z.string().min(1),
9
+ operator: z.enum([
10
+ "eq",
11
+ "neq",
12
+ "gt",
13
+ "gte",
14
+ "lt",
15
+ "lte",
16
+ "contains",
17
+ "starts_with",
18
+ "in",
19
+ "is_empty",
20
+ "is_not_empty",
21
+ ]),
22
+ value: z
23
+ .union([z.string(), z.number(), z.boolean(), z.array(z.string())])
24
+ .optional(),
25
+ });
26
+
27
+ const previewRequestSchema = z.object({
28
+ targetColumn: z.string().min(1).max(128),
29
+ promptMode: z.enum(["auto", "custom"]).optional(),
30
+ prompt: z.string().min(1).max(8192).optional(),
31
+ filter: filterSchema.optional(),
32
+ agentProfile: z.string().min(1).max(128).optional(),
33
+ agentProfileOverride: z.string().min(1).max(128).optional(),
34
+ batchSize: z.number().int().min(1).optional(),
35
+ }).superRefine((value, ctx) => {
36
+ const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
37
+ if (mode === "custom" && !value.prompt?.trim()) {
38
+ ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: ["prompt"],
41
+ message: "Custom enrichment requires a prompt",
42
+ });
43
+ }
44
+ });
45
+
46
+ export async function POST(
47
+ req: NextRequest,
48
+ { params }: { params: Promise<{ id: string }> }
49
+ ) {
50
+ const { id } = await params;
51
+
52
+ let body: unknown;
53
+ try {
54
+ body = await req.json();
55
+ } catch {
56
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
57
+ }
58
+
59
+ const parsed = previewRequestSchema.safeParse(body);
60
+ if (!parsed.success) {
61
+ return NextResponse.json(
62
+ {
63
+ error: parsed.error.issues.map((issue) => ({
64
+ path: issue.path.join("."),
65
+ message: issue.message,
66
+ })),
67
+ },
68
+ { status: 400 }
69
+ );
70
+ }
71
+
72
+ const { batchSize, ...rest } = parsed.data;
73
+ const cappedBatchSize =
74
+ batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
75
+
76
+ try {
77
+ const preview = await previewEnrichmentPlan(id, {
78
+ ...rest,
79
+ batchSize: cappedBatchSize,
80
+ });
81
+ return NextResponse.json(preview);
82
+ } catch (err) {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+
85
+ if (/not found/i.test(message)) {
86
+ return NextResponse.json({ error: message }, { status: 404 });
87
+ }
88
+ if (/does not exist|unsupported/i.test(message)) {
89
+ return NextResponse.json({ error: message }, { status: 400 });
90
+ }
91
+
92
+ console.error("[tables/enrich/plan] POST error:", err);
93
+ return NextResponse.json(
94
+ { error: "Failed to build enrichment plan" },
95
+ { status: 500 }
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,147 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { createEnrichmentWorkflow } from "@/lib/tables/enrichment";
4
+
5
+ const MAX_BATCH_SIZE = 200;
6
+
7
+ const filterSchema = z.object({
8
+ column: z.string().min(1),
9
+ operator: z.enum([
10
+ "eq",
11
+ "neq",
12
+ "gt",
13
+ "gte",
14
+ "lt",
15
+ "lte",
16
+ "contains",
17
+ "starts_with",
18
+ "in",
19
+ "is_empty",
20
+ "is_not_empty",
21
+ ]),
22
+ value: z
23
+ .union([z.string(), z.number(), z.boolean(), z.array(z.string())])
24
+ .optional(),
25
+ });
26
+
27
+ const enrichRequestSchema = z.object({
28
+ targetColumn: z.string().min(1).max(128),
29
+ promptMode: z.enum(["auto", "custom"]).optional(),
30
+ prompt: z.string().min(1).max(8192).optional(),
31
+ filter: filterSchema.optional(),
32
+ agentProfile: z.string().min(1).max(128).optional(),
33
+ agentProfileOverride: z.string().min(1).max(128).optional(),
34
+ projectId: z.string().nullable().optional(),
35
+ // Reject non-positive ints; the upper bound is *clamped* in the handler so
36
+ // callers asking for too much get a working (smaller) batch instead of a 400.
37
+ batchSize: z.number().int().min(1).optional(),
38
+ itemVariable: z.string().min(1).max(64).optional(),
39
+ workflowName: z.string().min(1).max(256).optional(),
40
+ plan: z
41
+ .object({
42
+ promptMode: z.enum(["auto", "custom"]),
43
+ strategy: z.enum([
44
+ "single-pass-lookup",
45
+ "single-pass-classify",
46
+ "research-and-synthesize",
47
+ ]),
48
+ agentProfile: z.string().min(1),
49
+ reasoning: z.string(),
50
+ steps: z.array(
51
+ z.object({
52
+ id: z.string().min(1),
53
+ name: z.string().min(1),
54
+ purpose: z.string().min(1),
55
+ prompt: z.string().min(1),
56
+ agentProfile: z.string().min(1).optional(),
57
+ })
58
+ ),
59
+ targetContract: z.object({
60
+ columnName: z.string().min(1),
61
+ columnLabel: z.string().min(1),
62
+ dataType: z.enum(["text", "number", "boolean", "select", "url", "email"]),
63
+ allowedOptions: z.array(z.string()).optional(),
64
+ }),
65
+ eligibleRowCount: z.number().int().min(0),
66
+ sampleBindings: z.array(z.record(z.string(), z.unknown())),
67
+ })
68
+ .optional(),
69
+ }).superRefine((value, ctx) => {
70
+ const hasPlan = Boolean(value.plan);
71
+ const mode = value.promptMode ?? (value.prompt ? "custom" : "auto");
72
+ if (!hasPlan && mode === "custom" && !value.prompt?.trim()) {
73
+ ctx.addIssue({
74
+ code: z.ZodIssueCode.custom,
75
+ path: ["prompt"],
76
+ message: "Custom enrichment requires a prompt",
77
+ });
78
+ }
79
+ });
80
+
81
+ /**
82
+ * POST /api/tables/[id]/enrich
83
+ *
84
+ * Kicks off a row-driven enrichment workflow for a user table. The workflow
85
+ * runs fire-and-forget (TDR-001); the response includes the workflow id and
86
+ * the number of rows that will actually be processed (already-populated rows
87
+ * are filtered out for idempotency).
88
+ *
89
+ * Status codes:
90
+ * - 202: workflow created and queued
91
+ * - 400: invalid body, unknown column, or other validation failure
92
+ * - 404: table not found
93
+ * - 500: unexpected error
94
+ */
95
+ export async function POST(
96
+ req: NextRequest,
97
+ { params }: { params: Promise<{ id: string }> }
98
+ ) {
99
+ const { id } = await params;
100
+
101
+ let body: unknown;
102
+ try {
103
+ body = await req.json();
104
+ } catch {
105
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
106
+ }
107
+
108
+ const parsed = enrichRequestSchema.safeParse(body);
109
+ if (!parsed.success) {
110
+ return NextResponse.json(
111
+ {
112
+ error: parsed.error.issues.map((issue) => ({
113
+ path: issue.path.join("."),
114
+ message: issue.message,
115
+ })),
116
+ },
117
+ { status: 400 }
118
+ );
119
+ }
120
+
121
+ const { batchSize, ...rest } = parsed.data;
122
+ const cappedBatchSize =
123
+ batchSize !== undefined ? Math.min(batchSize, MAX_BATCH_SIZE) : undefined;
124
+
125
+ try {
126
+ const result = await createEnrichmentWorkflow(id, {
127
+ ...rest,
128
+ batchSize: cappedBatchSize,
129
+ });
130
+ return NextResponse.json(result, { status: 202 });
131
+ } catch (err) {
132
+ const message = err instanceof Error ? err.message : String(err);
133
+
134
+ if (/not found/i.test(message)) {
135
+ return NextResponse.json({ error: message }, { status: 404 });
136
+ }
137
+ if (/does not exist|unsupported/i.test(message)) {
138
+ return NextResponse.json({ error: message }, { status: 400 });
139
+ }
140
+
141
+ console.error("[tables/enrich] POST error:", err);
142
+ return NextResponse.json(
143
+ { error: "Failed to start enrichment workflow" },
144
+ { status: 500 }
145
+ );
146
+ }
147
+ }
@@ -0,0 +1,25 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { listRecentEnrichmentRuns } from "@/lib/tables/enrichment";
3
+
4
+ export async function GET(
5
+ req: NextRequest,
6
+ { params }: { params: Promise<{ id: string }> }
7
+ ) {
8
+ const { id } = await params;
9
+ const searchParams = req.nextUrl.searchParams;
10
+ const rawLimit = Number(searchParams.get("limit") ?? "5");
11
+ const limit = Number.isFinite(rawLimit)
12
+ ? Math.min(Math.max(Math.trunc(rawLimit), 1), 10)
13
+ : 5;
14
+
15
+ try {
16
+ const runs = await listRecentEnrichmentRuns(id, limit);
17
+ return NextResponse.json(runs);
18
+ } catch (err) {
19
+ console.error("[tables/enrich/runs] GET error:", err);
20
+ return NextResponse.json(
21
+ { error: "Failed to load recent enrichment runs" },
22
+ { status: 500 }
23
+ );
24
+ }
25
+ }
@@ -10,8 +10,6 @@ import {
10
10
  enforceTaskBudgetGuardrails,
11
11
  } from "@/lib/settings/budget-guardrails";
12
12
  import { ensureFreshScan } from "@/lib/environment/auto-scan";
13
- import { getAllExecutions } from "@/lib/agents/execution-manager";
14
- import { licenseManager } from "@/lib/license/manager";
15
13
 
16
14
  export async function POST(
17
15
  _req: NextRequest,
@@ -95,25 +93,6 @@ export async function POST(
95
93
  return NextResponse.json({ error: compatibilityError }, { status: 400 });
96
94
  }
97
95
 
98
- // Pre-check parallel workflow limit before fire-and-forget
99
- const parallelLimit = licenseManager.getLimit("parallelWorkflows");
100
- if (Number.isFinite(parallelLimit) && getAllExecutions().size >= parallelLimit) {
101
- // Revert task to queued since we can't execute it
102
- db.update(tasks)
103
- .set({ status: "queued", updatedAt: new Date() })
104
- .where(eq(tasks.id, id))
105
- .run();
106
- return NextResponse.json(
107
- {
108
- error: `Parallel workflow limit reached (${getAllExecutions().size}/${parallelLimit}). Wait for a running task to finish or upgrade.`,
109
- limitType: "parallelWorkflows",
110
- current: getAllExecutions().size,
111
- max: parallelLimit,
112
- },
113
- { status: 429 }
114
- );
115
- }
116
-
117
96
  // Fire-and-forget — task already marked as running
118
97
  executeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch(
119
98
  (err) => console.error(`Task ${id} execution error:`, err)
@@ -0,0 +1,59 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { workflows } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { resumeWorkflow } from "@/lib/workflows/engine";
6
+
7
+ /**
8
+ * POST /api/workflows/[id]/resume
9
+ *
10
+ * Manually resume a workflow that was paused at a delay step. Called by the
11
+ * "Resume Now" button in WorkflowStatusView to skip the remaining delay. The
12
+ * scheduler tick also calls resumeWorkflow() directly (not through this route)
13
+ * when workflows.resume_at is reached.
14
+ *
15
+ * Response codes:
16
+ * 202 Accepted — resume dispatched (fire-and-forget)
17
+ * 404 Not Found — workflow does not exist
18
+ * 409 Conflict — workflow is not in paused state (already resumed, racing
19
+ * scheduler, or was never paused). resumeWorkflow handles
20
+ * this internally with its atomic status transition, so the
21
+ * conflict is reported here for correct UX feedback.
22
+ */
23
+ export async function POST(
24
+ _req: NextRequest,
25
+ { params }: { params: Promise<{ id: string }> },
26
+ ) {
27
+ const { id } = await params;
28
+
29
+ const [workflow] = await db
30
+ .select()
31
+ .from(workflows)
32
+ .where(eq(workflows.id, id));
33
+
34
+ if (!workflow) {
35
+ return NextResponse.json({ error: "Workflow not found" }, { status: 404 });
36
+ }
37
+
38
+ if (workflow.status !== "paused") {
39
+ return NextResponse.json(
40
+ {
41
+ error: `Workflow is not paused (current status: ${workflow.status})`,
42
+ status: workflow.status,
43
+ },
44
+ { status: 409 },
45
+ );
46
+ }
47
+
48
+ // Fire-and-forget. resumeWorkflow performs its own atomic status check, so
49
+ // if the scheduler tick beats this request by microseconds, the DB UPDATE
50
+ // will match zero rows and resumeWorkflow returns silently without harm.
51
+ resumeWorkflow(id).catch((error) => {
52
+ console.error(`Workflow ${id} resume failed:`, error);
53
+ });
54
+
55
+ return NextResponse.json(
56
+ { status: "resuming", workflowId: id },
57
+ { status: 202 },
58
+ );
59
+ }
@@ -3,6 +3,11 @@ import { db } from "@/lib/db";
3
3
  import { workflows, tasks, documents } from "@/lib/db/schema";
4
4
  import { eq, and, inArray, count, desc, sql as drizzleSql } from "drizzle-orm";
5
5
  import { parseWorkflowState } from "@/lib/workflows/engine";
6
+ import type {
7
+ WorkflowStatusResponse,
8
+ NonLoopPattern,
9
+ StepWithState,
10
+ } from "@/lib/workflows/types";
6
11
 
7
12
  /** Collect output documents for workflow step tasks + input documents from parent task */
8
13
  async function getWorkflowDocuments(
@@ -97,15 +102,18 @@ export async function GET(
97
102
  const sourceTaskId: string | undefined = definition.sourceTaskId;
98
103
  const { stepDocuments, parentDocuments } = await getWorkflowDocuments(state, sourceTaskId);
99
104
 
100
- // Loop pattern returns loop-specific data instead of step states
105
+ // Loop pattern returns loop-specific data instead of step states.
106
+ // The `satisfies` annotation enforces the TDR-031 contract: the loop arm
107
+ // of WorkflowStatusResponse cannot emit `workflowState` or `resumeAt`, and
108
+ // its `steps` field is raw WorkflowStep[] (not StepWithState[]).
101
109
  if (definition.pattern === "loop") {
102
- return NextResponse.json({
110
+ const loopBody = {
103
111
  id: workflow.id,
104
112
  name: workflow.name,
105
113
  status: workflow.status,
106
114
  projectId: workflow.projectId,
107
115
  definition: workflow.definition,
108
- pattern: definition.pattern,
116
+ pattern: "loop" as const,
109
117
  loopConfig: definition.loopConfig,
110
118
  swarmConfig: definition.swarmConfig,
111
119
  loopState,
@@ -114,18 +122,23 @@ export async function GET(
114
122
  parentDocuments,
115
123
  runNumber: workflow.runNumber,
116
124
  runHistory,
117
- });
125
+ } satisfies WorkflowStatusResponse;
126
+ return NextResponse.json(loopBody);
118
127
  }
119
128
 
120
- return NextResponse.json({
129
+ // Non-loop arm: sequence, parallel, swarm, planner-executor, checkpoint all
130
+ // share the step-state rendering path. `satisfies` enforces that this branch
131
+ // cannot accidentally emit `loopState`, and that every step has `.state`.
132
+ const nonLoopBody = {
121
133
  id: workflow.id,
122
134
  name: workflow.name,
123
135
  status: workflow.status,
136
+ resumeAt: workflow.resumeAt ?? null,
124
137
  projectId: workflow.projectId,
125
138
  definition: workflow.definition,
126
- pattern: definition.pattern,
139
+ pattern: definition.pattern as NonLoopPattern,
127
140
  swarmConfig: definition.swarmConfig,
128
- steps: definition.steps.map((step, i) => ({
141
+ steps: definition.steps.map((step, i): StepWithState => ({
129
142
  ...step,
130
143
  state: state?.stepStates[i] ?? { stepId: step.id, status: "pending" },
131
144
  })),
@@ -134,5 +147,6 @@ export async function GET(
134
147
  parentDocuments,
135
148
  runNumber: workflow.runNumber,
136
149
  runHistory,
137
- });
150
+ } satisfies WorkflowStatusResponse;
151
+ return NextResponse.json(nonLoopBody);
138
152
  }
@@ -1,6 +1,8 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import { getWorkspaceContext } from "@/lib/environment/workspace-context";
3
3
 
4
+ export const dynamic = "force-dynamic";
5
+
4
6
  export function GET() {
5
7
  const context = getWorkspaceContext();
6
8
  return NextResponse.json(context, {