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,147 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ generateEnrichmentDefinition,
4
+ filterUnpopulatedRows,
5
+ wrapPromptWithOutputContract,
6
+ } from "../enrichment";
7
+
8
+ describe("generateEnrichmentDefinition", () => {
9
+ const baseInput = {
10
+ rows: [
11
+ { id: "row_1", data: { name: "Alice", company: "Acme" } },
12
+ { id: "row_2", data: { name: "Bob", company: "Beta" } },
13
+ { id: "row_3", data: { name: "Carol", company: "Gamma" } },
14
+ ],
15
+ tableId: "tbl_contacts",
16
+ prompt: "Find LinkedIn URL for {{row.name}} at {{row.company}}",
17
+ targetColumn: "linkedin",
18
+ agentProfile: "sales-researcher",
19
+ };
20
+
21
+ it("produces a loop-pattern workflow definition", () => {
22
+ const def = generateEnrichmentDefinition(baseInput);
23
+ expect(def.pattern).toBe("loop");
24
+ expect(def.steps).toHaveLength(1);
25
+ expect(def.loopConfig).toBeDefined();
26
+ });
27
+
28
+ it("binds rows into loopConfig.items merged with their id", () => {
29
+ const def = generateEnrichmentDefinition(baseInput);
30
+ expect(def.loopConfig?.items).toEqual([
31
+ { id: "row_1", name: "Alice", company: "Acme" },
32
+ { id: "row_2", name: "Bob", company: "Beta" },
33
+ { id: "row_3", name: "Carol", company: "Gamma" },
34
+ ]);
35
+ });
36
+
37
+ it("defaults itemVariable to 'row' and caps maxIterations to row count", () => {
38
+ const def = generateEnrichmentDefinition(baseInput);
39
+ expect(def.loopConfig?.itemVariable).toBe("row");
40
+ expect(def.loopConfig?.maxIterations).toBe(3);
41
+ });
42
+
43
+ it("respects a custom itemVariable", () => {
44
+ const def = generateEnrichmentDefinition({
45
+ ...baseInput,
46
+ itemVariable: "contact",
47
+ });
48
+ expect(def.loopConfig?.itemVariable).toBe("contact");
49
+ });
50
+
51
+ it("attaches a postAction with templated rowId on the loop step", () => {
52
+ const def = generateEnrichmentDefinition(baseInput);
53
+ const step = def.steps[0];
54
+ expect(step.postAction).toEqual({
55
+ type: "update_row",
56
+ tableId: "tbl_contacts",
57
+ rowId: "{{row.id}}",
58
+ column: "linkedin",
59
+ });
60
+ });
61
+
62
+ it("templates the postAction rowId against the custom itemVariable", () => {
63
+ const def = generateEnrichmentDefinition({
64
+ ...baseInput,
65
+ itemVariable: "contact",
66
+ });
67
+ expect(def.steps[0].postAction?.rowId).toBe("{{contact.id}}");
68
+ });
69
+
70
+ it("wraps the supplied prompt with an output-format contract", () => {
71
+ const def = generateEnrichmentDefinition(baseInput);
72
+ const prompt = def.steps[0].prompt;
73
+ // User prompt is preserved
74
+ expect(prompt).toContain(baseInput.prompt);
75
+ // Output contract is appended so the agent returns a bare value
76
+ expect(prompt).toContain("RESPONSE FORMAT");
77
+ expect(prompt).toContain("NOT_FOUND");
78
+ expect(prompt).toContain('"linkedin"');
79
+ expect(def.steps[0].agentProfile).toBe("sales-researcher");
80
+ });
81
+
82
+ it("produces a single-step definition even when there are zero rows", () => {
83
+ const def = generateEnrichmentDefinition({ ...baseInput, rows: [] });
84
+ expect(def.steps).toHaveLength(1);
85
+ expect(def.loopConfig?.items).toEqual([]);
86
+ expect(def.loopConfig?.maxIterations).toBe(0);
87
+ });
88
+ });
89
+
90
+ describe("wrapPromptWithOutputContract", () => {
91
+ it("preserves the user's original prompt at the top", () => {
92
+ const wrapped = wrapPromptWithOutputContract(
93
+ "Find the LinkedIn URL for {{row.name}}",
94
+ "linkedin"
95
+ );
96
+ expect(wrapped.startsWith("Find the LinkedIn URL for {{row.name}}")).toBe(true);
97
+ });
98
+
99
+ it("appends a strict output contract that names the target column", () => {
100
+ const wrapped = wrapPromptWithOutputContract("Do x", "email");
101
+ expect(wrapped).toContain('"email"');
102
+ expect(wrapped).toContain("Return ONLY the value");
103
+ });
104
+
105
+ it("teaches the agent to use NOT_FOUND, not prose like 'Not found'", () => {
106
+ const wrapped = wrapPromptWithOutputContract("Do x", "value");
107
+ expect(wrapped).toContain("NOT_FOUND");
108
+ // Explicit guard against the verbose-prose failure mode we saw in E2E:
109
+ expect(wrapped).toContain("Do NOT return prose");
110
+ });
111
+
112
+ it("trims the user prompt before wrapping", () => {
113
+ const wrapped = wrapPromptWithOutputContract(" spaced \n", "x");
114
+ expect(wrapped.startsWith("spaced")).toBe(true);
115
+ });
116
+ });
117
+
118
+ describe("filterUnpopulatedRows", () => {
119
+ const rows = [
120
+ { id: "r1", data: { name: "A", linkedin: "https://linkedin.com/in/a" } },
121
+ { id: "r2", data: { name: "B", linkedin: "" } },
122
+ { id: "r3", data: { name: "C" } }, // missing key entirely
123
+ { id: "r4", data: { name: "D", linkedin: " " } }, // whitespace
124
+ { id: "r5", data: { name: "E", linkedin: null as unknown as string } },
125
+ ];
126
+
127
+ it("keeps rows whose target column is missing, null, empty, or whitespace", () => {
128
+ const result = filterUnpopulatedRows(rows, "linkedin");
129
+ expect(result.map((r) => r.id)).toEqual(["r2", "r3", "r4", "r5"]);
130
+ });
131
+
132
+ it("keeps all rows when none have the target column populated", () => {
133
+ const blankRows = [
134
+ { id: "r1", data: { name: "A" } },
135
+ { id: "r2", data: { name: "B" } },
136
+ ];
137
+ expect(filterUnpopulatedRows(blankRows, "linkedin")).toHaveLength(2);
138
+ });
139
+
140
+ it("returns an empty array when every row is already populated", () => {
141
+ const populated = [
142
+ { id: "r1", data: { linkedin: "x" } },
143
+ { id: "r2", data: { linkedin: "y" } },
144
+ ];
145
+ expect(filterUnpopulatedRows(populated, "linkedin")).toEqual([]);
146
+ });
147
+ });
@@ -0,0 +1,454 @@
1
+ import type { ColumnDef, FilterSpec } from "@/lib/tables/types";
2
+ import type { WorkflowEnrichmentTargetContract } from "@/lib/workflows/types";
3
+
4
+ export type EnrichmentPromptMode = "auto" | "custom";
5
+ export type EnrichmentStrategy =
6
+ | "single-pass-lookup"
7
+ | "single-pass-classify"
8
+ | "research-and-synthesize";
9
+
10
+ export interface EnrichmentRow {
11
+ id: string;
12
+ data: Record<string, unknown>;
13
+ }
14
+
15
+ export interface EnrichmentPlanStep {
16
+ id: string;
17
+ name: string;
18
+ purpose: string;
19
+ prompt: string;
20
+ agentProfile?: string;
21
+ }
22
+
23
+ export interface EnrichmentPlan {
24
+ promptMode: EnrichmentPromptMode;
25
+ strategy: EnrichmentStrategy;
26
+ agentProfile: string;
27
+ reasoning: string;
28
+ steps: EnrichmentPlanStep[];
29
+ targetContract: WorkflowEnrichmentTargetContract;
30
+ eligibleRowCount: number;
31
+ sampleBindings: Array<Record<string, unknown>>;
32
+ }
33
+
34
+ export interface BuildEnrichmentPlanInput {
35
+ targetColumn: ColumnDef;
36
+ sampleRows: EnrichmentRow[];
37
+ eligibleRowCount: number;
38
+ promptMode: EnrichmentPromptMode;
39
+ prompt?: string;
40
+ agentProfileOverride?: string;
41
+ filter?: FilterSpec;
42
+ }
43
+
44
+ const LOOKUP_TYPES = new Set(["url", "email", "number"]);
45
+ const CLASSIFY_TYPES = new Set(["select", "boolean"]);
46
+ const SUPPORTED_TYPES = new Set([
47
+ "text",
48
+ "number",
49
+ "boolean",
50
+ "select",
51
+ "url",
52
+ "email",
53
+ ]);
54
+
55
+ export function assertEnrichmentCompatibleColumn(column: ColumnDef): void {
56
+ if (!SUPPORTED_TYPES.has(column.dataType)) {
57
+ throw new Error(
58
+ `Column "${column.displayName}" uses unsupported data type "${column.dataType}" for enrichment`
59
+ );
60
+ }
61
+ }
62
+
63
+ export function buildTargetContract(
64
+ column: ColumnDef
65
+ ): WorkflowEnrichmentTargetContract {
66
+ assertEnrichmentCompatibleColumn(column);
67
+ return {
68
+ columnName: column.name,
69
+ columnLabel: column.displayName,
70
+ dataType: column.dataType as WorkflowEnrichmentTargetContract["dataType"],
71
+ allowedOptions:
72
+ column.dataType === "select"
73
+ ? [...(column.config?.options ?? [])]
74
+ : undefined,
75
+ };
76
+ }
77
+
78
+ export function buildEnrichmentPlan(
79
+ input: BuildEnrichmentPlanInput
80
+ ): EnrichmentPlan {
81
+ const targetContract = buildTargetContract(input.targetColumn);
82
+ const strategy = selectStrategy(targetContract, input.promptMode, input.prompt);
83
+ const agentProfile =
84
+ input.agentProfileOverride?.trim() ||
85
+ recommendAgentProfile(strategy, targetContract);
86
+ const reasoning = buildReasoning({
87
+ targetContract,
88
+ strategy,
89
+ filter: input.filter,
90
+ hasPromptOverride: Boolean(input.prompt?.trim()),
91
+ });
92
+
93
+ if (input.promptMode === "custom") {
94
+ const customPrompt = input.prompt?.trim();
95
+ if (!customPrompt) {
96
+ throw new Error("Custom enrichment requires a prompt");
97
+ }
98
+ return {
99
+ promptMode: "custom",
100
+ strategy,
101
+ agentProfile,
102
+ reasoning,
103
+ steps: [
104
+ {
105
+ id: "final",
106
+ name: "Write final value",
107
+ purpose: "Generate the final typed value for the target column",
108
+ prompt: wrapPromptWithOutputContract(customPrompt, targetContract),
109
+ agentProfile,
110
+ },
111
+ ],
112
+ targetContract,
113
+ eligibleRowCount: input.eligibleRowCount,
114
+ sampleBindings: input.sampleRows.slice(0, 2).map((row) => ({
115
+ id: row.id,
116
+ ...row.data,
117
+ })),
118
+ };
119
+ }
120
+
121
+ return {
122
+ promptMode: "auto",
123
+ strategy,
124
+ agentProfile,
125
+ reasoning,
126
+ steps: buildAutoPlanSteps(strategy, targetContract, input.prompt, agentProfile),
127
+ targetContract,
128
+ eligibleRowCount: input.eligibleRowCount,
129
+ sampleBindings: input.sampleRows.slice(0, 2).map((row) => ({
130
+ id: row.id,
131
+ ...row.data,
132
+ })),
133
+ };
134
+ }
135
+
136
+ export function validateEnrichmentPlan(
137
+ plan: EnrichmentPlan,
138
+ column: ColumnDef
139
+ ): void {
140
+ const targetContract = buildTargetContract(column);
141
+
142
+ if (plan.steps.length === 0) {
143
+ throw new Error("Enrichment plan must contain at least one step");
144
+ }
145
+
146
+ if (plan.targetContract.dataType !== targetContract.dataType) {
147
+ throw new Error("Enrichment plan target contract does not match the table column");
148
+ }
149
+
150
+ if (plan.targetContract.columnName !== column.name) {
151
+ throw new Error("Enrichment plan target column does not match the table column");
152
+ }
153
+
154
+ if ((plan.agentProfile ?? "").trim() === "") {
155
+ throw new Error("Enrichment plan must specify an agent profile");
156
+ }
157
+
158
+ const lastStep = plan.steps[plan.steps.length - 1];
159
+ if (!lastStep.prompt.includes("RESPONSE FORMAT")) {
160
+ throw new Error("Enrichment plan final step is missing the typed response contract");
161
+ }
162
+
163
+ if (plan.targetContract.dataType === "select") {
164
+ const expected = targetContract.allowedOptions ?? [];
165
+ const actual = plan.targetContract.allowedOptions ?? [];
166
+ if (
167
+ expected.length !== actual.length ||
168
+ expected.some((option, index) => actual[index] !== option)
169
+ ) {
170
+ throw new Error("Enrichment plan select options do not match the table column");
171
+ }
172
+ }
173
+
174
+ if (plan.promptMode === "custom" && plan.steps.length !== 1) {
175
+ throw new Error("Custom enrichment plans must be single-step");
176
+ }
177
+ }
178
+
179
+ export function wrapPromptWithOutputContract(
180
+ userPrompt: string,
181
+ target:
182
+ | WorkflowEnrichmentTargetContract
183
+ | string
184
+ ): string {
185
+ const base = userPrompt.trim();
186
+ const targetContract =
187
+ typeof target === "string"
188
+ ? {
189
+ columnName: target,
190
+ columnLabel: target,
191
+ dataType: "text" as const,
192
+ }
193
+ : target;
194
+ const instructions = contractInstructions(targetContract);
195
+ return [base, "", "---", "RESPONSE FORMAT (strict):", ...instructions].join(
196
+ "\n"
197
+ );
198
+ }
199
+
200
+ export function normalizeEnrichmentOutput(
201
+ raw: string | undefined | null,
202
+ targetContract: WorkflowEnrichmentTargetContract
203
+ ):
204
+ | { kind: "skip"; reason: "empty" | "not_found" }
205
+ | { kind: "invalid"; reason: string }
206
+ | { kind: "valid"; value: string | number | boolean } {
207
+ const value = (raw ?? "").trim();
208
+ if (value === "") {
209
+ return { kind: "skip", reason: "empty" };
210
+ }
211
+ if (value.toUpperCase() === "NOT_FOUND") {
212
+ return { kind: "skip", reason: "not_found" };
213
+ }
214
+
215
+ switch (targetContract.dataType) {
216
+ case "text":
217
+ return { kind: "valid", value };
218
+ case "url": {
219
+ try {
220
+ const url = new URL(value);
221
+ return { kind: "valid", value: url.toString() };
222
+ } catch {
223
+ return { kind: "invalid", reason: "Expected a valid URL or NOT_FOUND" };
224
+ }
225
+ }
226
+ case "email": {
227
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
228
+ if (!emailPattern.test(value)) {
229
+ return {
230
+ kind: "invalid",
231
+ reason: "Expected a valid email address or NOT_FOUND",
232
+ };
233
+ }
234
+ return { kind: "valid", value };
235
+ }
236
+ case "boolean": {
237
+ const lower = value.toLowerCase();
238
+ if (lower === "true") return { kind: "valid", value: true };
239
+ if (lower === "false") return { kind: "valid", value: false };
240
+ return {
241
+ kind: "invalid",
242
+ reason: "Expected exactly true, false, or NOT_FOUND",
243
+ };
244
+ }
245
+ case "number": {
246
+ const numeric = Number(value);
247
+ if (!Number.isFinite(numeric)) {
248
+ return {
249
+ kind: "invalid",
250
+ reason: "Expected a bare numeric value or NOT_FOUND",
251
+ };
252
+ }
253
+ return { kind: "valid", value: numeric };
254
+ }
255
+ case "select": {
256
+ const options = targetContract.allowedOptions ?? [];
257
+ const match = options.find(
258
+ (option) => option.toLowerCase() === value.toLowerCase()
259
+ );
260
+ if (!match) {
261
+ return {
262
+ kind: "invalid",
263
+ reason: `Expected one of ${options.join(", ")} or NOT_FOUND`,
264
+ };
265
+ }
266
+ return { kind: "valid", value: match };
267
+ }
268
+ }
269
+ }
270
+
271
+ function selectStrategy(
272
+ contract: WorkflowEnrichmentTargetContract,
273
+ promptMode: EnrichmentPromptMode,
274
+ prompt?: string
275
+ ): EnrichmentStrategy {
276
+ if (promptMode === "custom") {
277
+ if (CLASSIFY_TYPES.has(contract.dataType)) return "single-pass-classify";
278
+ return "single-pass-lookup";
279
+ }
280
+
281
+ if (CLASSIFY_TYPES.has(contract.dataType)) {
282
+ return "single-pass-classify";
283
+ }
284
+ if (LOOKUP_TYPES.has(contract.dataType)) {
285
+ return "single-pass-lookup";
286
+ }
287
+
288
+ const hint = (prompt ?? "").toLowerCase();
289
+ if (
290
+ hint.includes("research") ||
291
+ hint.includes("synthesize") ||
292
+ hint.includes("score") ||
293
+ hint.includes("summary") ||
294
+ hint.includes("why ")
295
+ ) {
296
+ return "research-and-synthesize";
297
+ }
298
+
299
+ return "single-pass-lookup";
300
+ }
301
+
302
+ function recommendAgentProfile(
303
+ strategy: EnrichmentStrategy,
304
+ contract: WorkflowEnrichmentTargetContract
305
+ ): string {
306
+ if (strategy === "research-and-synthesize") {
307
+ return "data-analyst";
308
+ }
309
+ if (contract.dataType === "url" || contract.dataType === "email") {
310
+ return "sales-researcher";
311
+ }
312
+ if (contract.dataType === "text") {
313
+ return "general";
314
+ }
315
+ return "data-analyst";
316
+ }
317
+
318
+ function buildReasoning(input: {
319
+ targetContract: WorkflowEnrichmentTargetContract;
320
+ strategy: EnrichmentStrategy;
321
+ filter?: FilterSpec;
322
+ hasPromptOverride: boolean;
323
+ }): string {
324
+ const reasons: string[] = [];
325
+ if (input.targetContract.dataType === "select") {
326
+ reasons.push("Target column is categorical, so the plan uses a classification path.");
327
+ } else if (input.targetContract.dataType === "boolean") {
328
+ reasons.push("Target column expects a boolean, so the plan enforces a strict true/false contract.");
329
+ } else if (input.targetContract.dataType === "text") {
330
+ reasons.push(
331
+ input.strategy === "research-and-synthesize"
332
+ ? "Target column is free-form text and the prompt hints at synthesis, so the plan separates research from final writeback."
333
+ : "Target column is free-form text, so the plan keeps the row flow lightweight unless the prompt asks for deeper synthesis."
334
+ );
335
+ } else {
336
+ reasons.push("Target column expects a concrete factual value, so the plan keeps the row flow single-pass.");
337
+ }
338
+
339
+ if (input.filter) {
340
+ reasons.push(`Preview applies the current filter on "${input.filter.column}" before fan-out.`);
341
+ }
342
+ if (input.hasPromptOverride) {
343
+ reasons.push("Planner includes the operator's extra instructions when generating prompts.");
344
+ }
345
+
346
+ return reasons.join(" ");
347
+ }
348
+
349
+ function buildAutoPlanSteps(
350
+ strategy: EnrichmentStrategy,
351
+ targetContract: WorkflowEnrichmentTargetContract,
352
+ prompt: string | undefined,
353
+ agentProfile: string
354
+ ): EnrichmentPlanStep[] {
355
+ const extraGuidance = prompt?.trim()
356
+ ? `\n\nAdditional operator guidance:\n${prompt.trim()}`
357
+ : "";
358
+
359
+ if (strategy === "research-and-synthesize") {
360
+ return [
361
+ {
362
+ id: "research",
363
+ name: "Research row context",
364
+ purpose: "Gather concise evidence about the current row before final synthesis",
365
+ prompt:
366
+ `Research the current row and capture only the evidence needed to determine a value for the "${targetContract.columnLabel}" column.` +
367
+ "\nFocus on facts, cite the specific row fields that shaped the search, and keep the output concise enough for a second agent step to consume." +
368
+ extraGuidance,
369
+ agentProfile,
370
+ },
371
+ {
372
+ id: "final",
373
+ name: "Write final value",
374
+ purpose: "Convert the research output into the final typed value",
375
+ prompt: wrapPromptWithOutputContract(
376
+ `Using the research summary below, produce the final value for the "${targetContract.columnLabel}" column.\n\nResearch summary:\n{{previous}}`,
377
+ targetContract
378
+ ),
379
+ agentProfile,
380
+ },
381
+ ];
382
+ }
383
+
384
+ if (strategy === "single-pass-classify") {
385
+ return [
386
+ {
387
+ id: "final",
388
+ name: "Classify row",
389
+ purpose: "Determine the final typed category for this row",
390
+ prompt: wrapPromptWithOutputContract(
391
+ `Classify the current row into the correct value for the "${targetContract.columnLabel}" column.${extraGuidance}`,
392
+ targetContract
393
+ ),
394
+ agentProfile,
395
+ },
396
+ ];
397
+ }
398
+
399
+ return [
400
+ {
401
+ id: "final",
402
+ name: "Lookup value",
403
+ purpose: "Determine the final typed value for this row",
404
+ prompt: wrapPromptWithOutputContract(
405
+ `Determine the best value for the "${targetContract.columnLabel}" column for the current row.${extraGuidance}`,
406
+ targetContract
407
+ ),
408
+ agentProfile,
409
+ },
410
+ ];
411
+ }
412
+
413
+ function contractInstructions(
414
+ targetContract: WorkflowEnrichmentTargetContract
415
+ ): string[] {
416
+ const intro = [
417
+ `- Return ONLY the value to write into the "${targetContract.columnLabel}" column.`,
418
+ "- No explanations, no markdown, no preamble, and no source citations.",
419
+ '- Do NOT return prose like "Not found" or "Insufficient data" — use NOT_FOUND instead.',
420
+ ];
421
+
422
+ switch (targetContract.dataType) {
423
+ case "text":
424
+ return [
425
+ ...intro,
426
+ "- Return a plain text value, or the literal string NOT_FOUND.",
427
+ ];
428
+ case "url":
429
+ return [
430
+ ...intro,
431
+ "- Return one valid absolute URL, or the literal string NOT_FOUND.",
432
+ ];
433
+ case "email":
434
+ return [
435
+ ...intro,
436
+ "- Return one valid email address, or the literal string NOT_FOUND.",
437
+ ];
438
+ case "boolean":
439
+ return [
440
+ ...intro,
441
+ "- Return exactly one of: true, false, NOT_FOUND.",
442
+ ];
443
+ case "number":
444
+ return [
445
+ ...intro,
446
+ "- Return one bare numeric value with no units or prose, or the literal string NOT_FOUND.",
447
+ ];
448
+ case "select":
449
+ return [
450
+ ...intro,
451
+ `- Return exactly one of: ${(targetContract.allowedOptions ?? []).join(", ")}, NOT_FOUND.`,
452
+ ];
453
+ }
454
+ }