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
@@ -1,6 +1,28 @@
1
1
  /**
2
2
  * Enhanced system prompt for the Stagent chat LLM.
3
3
  * Provides identity, tool catalog, and intent routing guidance.
4
+ *
5
+ * ## Tier 0 vs CLAUDE.md partition (DD-CE-002)
6
+ *
7
+ * When the chat engine runs on the `claude-code` runtime, the Claude Agent
8
+ * SDK loads project-level `CLAUDE.md` and user-level `~/.claude/CLAUDE.md`
9
+ * via `settingSources: ["user", "project"]`. To avoid double-prompting,
10
+ * this system prompt MUST stay scoped to:
11
+ *
12
+ * (a) Stagent identity
13
+ * (b) Stagent tool catalog and routing
14
+ * (c) Stagent domain semantics (delay steps, enrich_table, workflow dedup)
15
+ * (d) LLM interaction style
16
+ *
17
+ * Content that is project-specific (coding conventions, testing rules,
18
+ * git workflow, repo-specific gotchas) belongs in `CLAUDE.md` — NOT here.
19
+ *
20
+ * Audit (2026-04-13): every current block in this prompt passes the rubric.
21
+ * No content migration was required for Stagent's current CLAUDE.md state.
22
+ * The worktree note on line 110 is borderline and flagged for revisit if
23
+ * CLAUDE.md gains an explicit worktree section.
24
+ *
25
+ * Reference: features/chat-claude-sdk-skills.md (§"Tier 0 vs CLAUDE.md").
4
26
  */
5
27
 
6
28
  export const STAGENT_SYSTEM_PROMPT = `You are Stagent, an AI workspace assistant for managing software projects, tasks, workflows, documents, and schedules. You are a full alternate UI for the Stagent app — users can do everything through chat that they can do in the GUI.
@@ -21,11 +43,12 @@ export const STAGENT_SYSTEM_PROMPT = `You are Stagent, an AI workspace assistant
21
43
 
22
44
  ### Workflows
23
45
  - list_workflows: List all workflows
24
- - create_workflow: Create a multi-step workflow with a definition
46
+ - create_workflow: Create a multi-step workflow with a definition. Steps can be task steps (profile + prompt) or **delay steps** (delayDuration like '3d', '2h', '30m', '1w') that pause the workflow before the next step. Delay steps enable time-distributed sequences.
25
47
  - get_workflow: Get workflow details and definition
26
48
  - update_workflow: Update a draft workflow
27
49
  - delete_workflow: Delete a workflow and its children [requires approval]
28
50
  - execute_workflow: Start workflow execution [requires approval]
51
+ - resume_workflow: Resume a paused (delayed) workflow immediately instead of waiting for its scheduled resume time [requires approval]
29
52
  - get_workflow_status: Get current execution status with step progress
30
53
  - find_related_documents: Search the project document pool for documents to attach as workflow context
31
54
 
@@ -58,9 +81,30 @@ export const STAGENT_SYSTEM_PROMPT = `You are Stagent, an AI workspace assistant
58
81
  - get_usage_summary: Get token and cost statistics over a time period
59
82
  - get_settings: Read current configuration (auth method, budgets, runtime)
60
83
 
84
+ ### Tables
85
+ Structured user data lives in Stagent tables (separate from Stagent's own internal records). Every table tool takes a tableId; use list_tables or search_table to find them first.
86
+ - list_tables: List all user tables in a project
87
+ - get_table_schema: Get a table's columns, types, and metadata
88
+ - query_table: Filter, sort, and paginate rows with operators (eq, neq, gt, gte, lt, lte, contains, starts_with, in, is_empty, is_not_empty)
89
+ - search_table: Full-text search across row cell values
90
+ - aggregate_table: Compute count/sum/avg/min/max over a column with optional group-by
91
+ - add_rows: Insert one or more rows
92
+ - update_row: Update a single row's cell values
93
+ - delete_rows: Delete rows matching a filter [requires approval]
94
+ - create_table: Create a new empty table with specified columns
95
+ - import_document_as_table: Parse an uploaded document into a new table
96
+ - export_table: Export rows as CSV/JSON
97
+ - add_column / update_column / delete_column / reorder_columns: Schema edits
98
+ - list_triggers / create_trigger / update_trigger / delete_trigger: Per-row trigger evaluation
99
+ - get_table_history: Read change history for a table
100
+ - save_as_template: Save a table's shape as a reusable template
101
+ - **enrich_table**: Run an agent task for every row in a table matching a filter, writing results to a target column. Use for bulk research, classification, content generation, or any table-row fan-out pattern. Generates the optimal loop workflow, binds each row as context, skips already-populated rows for idempotency [requires approval]
102
+
61
103
  ## When to Use Which Tools
62
104
  - CRUD operations ("create a task", "list workflows", "update the schedule") → Use the appropriate Stagent tool
63
105
  - Execution ("run this task", "execute the workflow") → Use execute_task / execute_workflow
106
+ - Time-distributed multi-step sequences ("send email, wait 3 days, follow up", "drip campaign", "onboarding flow") → Use create_workflow with delay steps in a sequence pattern. Do NOT create separate workflows and schedules for each touch — a single workflow with inline delay steps is the idiomatic pattern.
107
+ - Bulk per-row operations ("research every contact", "classify all tickets", "enrich rows missing X", "for each row do Y") → Use enrich_table. Do NOT hand-roll a loop workflow for this — enrich_table already generates the optimal loop, handles row-data binding, wires up the postAction writeback, and skips already-populated rows for idempotency.
64
108
  - Approvals ("approve that", "allow it", "deny the request") → Use respond_notification
65
109
  - Monitoring ("what's pending?", "any approval requests?") → Use list_notifications
66
110
  - Usage ("how much have I spent?", "token usage this week") → Use get_usage_summary
@@ -82,6 +126,9 @@ Be proactive with tools. If the user asks about project status, use list_tasks t
82
126
  - If a project context is active, scope operations to it unless the user specifies otherwise.
83
127
  - Tools marked [requires approval] will prompt the user before executing.
84
128
  - For workflows, valid patterns are: sequence, parallel, checkpoint, planner-executor, swarm, loop.
129
+ - **Delay steps** (sequence pattern only): a step with \`delayDuration\` (format: Nm|Nh|Nd|Nw, bounds 1m..30d) pauses the workflow between task steps. Format examples: "30m", "2h", "3d", "1w". Delay steps must have NO profile or prompt — they are pure waits. Use them for outreach sequences, drip campaigns, cooling periods, staged rollouts. A paused workflow resumes automatically when its scheduled time arrives, or immediately when the user clicks "Resume Now".
130
+ - **enrich_table idempotency:** \`enrich_table\` skips rows where the target column already has a non-empty value. If the user wants to overwrite existing values, explain that force re-enrichment is not supported in v1 — they must manually clear the target column first (e.g. via update_row) before re-running.
131
+ - **create_workflow dedup:** Before calling \`create_workflow\`, call \`list_workflows\` (filtered by the current project) to check whether a similar workflow already exists. If the user asks to "redesign", "redo", or "update" an existing workflow, call \`update_workflow\` on the matching row instead of creating a new one. \`create_workflow\` performs its own near-duplicate check and will return \`{status: "similar-found", matches: [...]}\` instead of inserting when it finds one — when that happens, surface the matches to the user and confirm intent. Only pass \`force: true\` to \`create_workflow\` when the user has explicitly confirmed they want a second workflow alongside a similar one (e.g., "v2", "alternate approach").
85
132
  - When a working directory is specified, always create files relative to it. Never assume the git root is the working directory — they may differ in worktree environments.
86
133
 
87
134
  ## Document Pool Awareness
@@ -11,9 +11,9 @@ import {
11
11
  Clock,
12
12
  Globe,
13
13
  Sun,
14
- CheckCheck,
15
14
  Sparkles,
16
15
  Table2,
16
+ Zap,
17
17
  } from "lucide-react";
18
18
  import type { LucideIcon } from "lucide-react";
19
19
 
@@ -33,7 +33,8 @@ export type ToolGroup =
33
33
  | "Settings"
34
34
  | "Chat"
35
35
  | "Browser"
36
- | "Utility";
36
+ | "Utility"
37
+ | "Session";
37
38
 
38
39
  export interface ToolCatalogEntry {
39
40
  /** MCP tool name, e.g. "list_tasks" */
@@ -51,6 +52,7 @@ export interface ToolCatalogEntry {
51
52
  // ── Group → Icon mapping ─────────────────────────────────────────────────
52
53
 
53
54
  export const TOOL_GROUP_ICONS: Record<ToolGroup, LucideIcon> = {
55
+ Session: Zap,
54
56
  Tasks: ListTodo,
55
57
  Projects: FolderKanban,
56
58
  Workflows: GitBranch,
@@ -69,6 +71,7 @@ export const TOOL_GROUP_ICONS: Record<ToolGroup, LucideIcon> = {
69
71
 
70
72
  /** Display order for groups in the popover */
71
73
  export const TOOL_GROUP_ORDER: ToolGroup[] = [
74
+ "Session",
72
75
  "Tasks",
73
76
  "Projects",
74
77
  "Workflows",
@@ -110,6 +113,11 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
110
113
  { name: "execute_workflow", description: "Start executing a workflow", group: "Workflows", paramHint: "workflowId" },
111
114
  { name: "delete_workflow", description: "Delete a workflow", group: "Workflows", paramHint: "workflowId" },
112
115
  { name: "get_workflow_status", description: "Get workflow execution progress", group: "Workflows", paramHint: "workflowId" },
116
+ { name: "list_blueprints", description: "List available workflow blueprints", group: "Workflows", paramHint: "domain, search" },
117
+ { name: "get_blueprint", description: "Get blueprint details and variables", group: "Workflows", paramHint: "blueprintId" },
118
+ { name: "instantiate_blueprint", description: "Create a workflow from a blueprint", group: "Workflows", paramHint: "blueprintId, variables" },
119
+ { name: "create_blueprint", description: "Create a custom workflow blueprint", group: "Workflows", paramHint: "yaml" },
120
+ { name: "delete_blueprint", description: "Delete a custom blueprint", group: "Workflows", paramHint: "blueprintId" },
113
121
 
114
122
  // ── Schedules ──
115
123
  { name: "list_schedules", description: "List scheduled prompt loops", group: "Schedules", paramHint: "status" },
@@ -134,6 +142,9 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
134
142
  // ── Profiles ──
135
143
  { name: "list_profiles", description: "List available agent profiles", group: "Profiles" },
136
144
  { name: "get_profile", description: "Get agent profile configuration", group: "Profiles", paramHint: "profileId" },
145
+ { name: "create_profile", description: "Create a new agent profile", group: "Profiles", paramHint: "config, skillMd" },
146
+ { name: "update_profile", description: "Update a custom agent profile", group: "Profiles", paramHint: "profileId, config, skillMd" },
147
+ { name: "delete_profile", description: "Delete a custom agent profile", group: "Profiles", paramHint: "profileId" },
137
148
 
138
149
  // ── Usage ──
139
150
  { name: "get_usage_summary", description: "Get spending and token usage stats", group: "Usage", paramHint: "days" },
@@ -142,6 +153,12 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
142
153
  { name: "get_settings", description: "Get current Stagent settings", group: "Settings", paramHint: "key" },
143
154
  { name: "set_settings", description: "Update a Stagent setting (approval required)", group: "Settings", paramHint: "key, value" },
144
155
 
156
+ // ── Skills ──
157
+ { name: "list_skills", description: "List all discoverable skills (user + project scopes)", group: "Skills" },
158
+ { name: "get_skill", description: "Get full SKILL.md content + metadata for one skill", group: "Skills", paramHint: "id" },
159
+ { name: "activate_skill", description: "Bind a skill to a conversation — SKILL.md is injected into every turn's system prompt. Pass mode='add' to compose (runtime-gated).", group: "Skills", paramHint: "conversationId, skillId, mode?" },
160
+ { name: "deactivate_skill", description: "Clear the active skill from a conversation", group: "Skills", paramHint: "conversationId" },
161
+
145
162
  // ── Tables ──
146
163
  { name: "list_tables", description: "List tables, filter by project or source", group: "Tables", paramHint: "projectId, source" },
147
164
  { name: "get_table_schema", description: "Get column definitions for a table", group: "Tables", paramHint: "tableId" },
@@ -171,11 +188,13 @@ const STAGENT_TOOLS: ToolCatalogEntry[] = [
171
188
  { name: "delete_trigger", description: "Delete a trigger", group: "Tables", paramHint: "tableId, triggerId" },
172
189
  { name: "get_table_history", description: "Get row change history for a table", group: "Tables", paramHint: "tableId, limit" },
173
190
  { name: "save_as_template", description: "Save a table as a reusable template", group: "Tables", paramHint: "tableId, name, category" },
191
+ { name: "enrich_table", description: "Bulk-enrich rows by running an agent task per row, writing results to a target column", group: "Tables", paramHint: "tableId, prompt, targetColumn, filter" },
174
192
 
175
193
  // ── Chat History ──
176
194
  { name: "list_conversations", description: "List recent chat conversations", group: "Chat", paramHint: "search, limit" },
177
195
  { name: "get_conversation_messages", description: "Get messages from a past conversation", group: "Chat", paramHint: "conversationId, limit" },
178
196
  { name: "search_messages", description: "Search across all conversations", group: "Chat", paramHint: "query" },
197
+
179
198
  ];
180
199
 
181
200
  const BROWSER_TOOLS: ToolCatalogEntry[] = [
@@ -187,6 +206,18 @@ const BROWSER_TOOLS: ToolCatalogEntry[] = [
187
206
  { name: "take_snapshot", description: "Take an accessibility snapshot", group: "Browser" },
188
207
  ];
189
208
 
209
+ const SESSION_ENTRIES: ToolCatalogEntry[] = [
210
+ { name: "clear", description: "Start a new conversation", group: "Session", behavior: "execute_immediately" },
211
+ { name: "compact", description: "Summarize and compact conversation history", group: "Session", behavior: "execute_immediately" },
212
+ { name: "export", description: "Save current conversation as a document", group: "Session", behavior: "execute_immediately" },
213
+ { name: "help", description: "Show chat shortcuts and commands", group: "Session", behavior: "execute_immediately" },
214
+ { name: "settings", description: "Open Stagent settings", group: "Session", behavior: "execute_immediately" },
215
+ { name: "new-task", description: "Create a new task", group: "Session", paramHint: "title" },
216
+ { name: "new-workflow", description: "Create a new workflow", group: "Session", paramHint: "name" },
217
+ { name: "new-schedule", description: "Create a new schedule", group: "Session", paramHint: "name, interval" },
218
+ { name: "new-from-template", description: "Start a conversation from a workflow blueprint", group: "Session", behavior: "execute_immediately" },
219
+ ];
220
+
190
221
  const UTILITY_ENTRIES: ToolCatalogEntry[] = [
191
222
  { name: "toggle_theme", description: "Switch dark/light mode", group: "Utility", behavior: "execute_immediately" },
192
223
  { name: "mark_all_read", description: "Mark all notifications as read", group: "Utility", behavior: "execute_immediately" },
@@ -202,13 +233,13 @@ export function getToolCatalog(opts?: { includeBrowser?: boolean }): ToolCatalog
202
233
 
203
234
  if (withBrowser) {
204
235
  if (!cachedWithBrowser) {
205
- cachedWithBrowser = [...STAGENT_TOOLS, ...BROWSER_TOOLS, ...UTILITY_ENTRIES];
236
+ cachedWithBrowser = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...BROWSER_TOOLS, ...UTILITY_ENTRIES];
206
237
  }
207
238
  return cachedWithBrowser;
208
239
  }
209
240
 
210
241
  if (!cachedCatalog) {
211
- cachedCatalog = [...STAGENT_TOOLS, ...UTILITY_ENTRIES];
242
+ cachedCatalog = [...SESSION_ENTRIES, ...STAGENT_TOOLS, ...UTILITY_ENTRIES];
212
243
  }
213
244
  return cachedCatalog;
214
245
  }
@@ -0,0 +1,127 @@
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
+ // Stub the rest of @/lib/data/tables so importing table-tools doesn't drag in DB.
12
+ vi.mock("@/lib/data/tables", () => ({
13
+ listTables: vi.fn(),
14
+ getTable: vi.fn(),
15
+ createTable: vi.fn(),
16
+ updateTable: vi.fn(),
17
+ deleteTable: vi.fn(),
18
+ listRows: vi.fn(),
19
+ addRows: vi.fn(),
20
+ updateRow: vi.fn(),
21
+ deleteRows: vi.fn(),
22
+ listTemplates: vi.fn(),
23
+ cloneFromTemplate: vi.fn(),
24
+ addColumn: vi.fn(),
25
+ updateColumn: vi.fn(),
26
+ deleteColumn: vi.fn(),
27
+ reorderColumns: vi.fn(),
28
+ }));
29
+
30
+ vi.mock("@/lib/tables/history", () => ({ getTableHistory: vi.fn() }));
31
+ vi.mock("@/lib/tables/import", () => ({
32
+ extractStructuredData: vi.fn(),
33
+ inferColumnTypes: vi.fn(),
34
+ importRows: vi.fn(),
35
+ createImportRecord: vi.fn(),
36
+ }));
37
+
38
+ import { tableTools } from "../table-tools";
39
+
40
+ function findEnrichTool() {
41
+ const tools = tableTools({ projectId: "proj_test" });
42
+ const tool = tools.find((t) => t.name === "enrich_table");
43
+ if (!tool) throw new Error("enrich_table tool not registered");
44
+ return tool;
45
+ }
46
+
47
+ describe("enrich_table tool", () => {
48
+ beforeEach(() => {
49
+ mockCreateEnrichmentWorkflow.mockReset();
50
+ });
51
+
52
+ it("is registered in tableTools", () => {
53
+ const tools = tableTools({ projectId: "proj_test" });
54
+ const names = tools.map((t) => t.name);
55
+ expect(names).toContain("enrich_table");
56
+ });
57
+
58
+ it("delegates to createEnrichmentWorkflow with the supplied params", async () => {
59
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
60
+ workflowId: "wf_xyz",
61
+ rowCount: 4,
62
+ });
63
+
64
+ const tool = findEnrichTool();
65
+ const result = await tool.handler({
66
+ tableId: "tbl_contacts",
67
+ prompt: "Find LinkedIn for {{row.name}}",
68
+ targetColumn: "linkedin",
69
+ filter: { column: "linkedin", operator: "is_empty" },
70
+ agentProfile: "sales-researcher",
71
+ });
72
+
73
+ expect(mockCreateEnrichmentWorkflow).toHaveBeenCalledWith(
74
+ "tbl_contacts",
75
+ expect.objectContaining({
76
+ prompt: "Find LinkedIn for {{row.name}}",
77
+ targetColumn: "linkedin",
78
+ filter: { column: "linkedin", operator: "is_empty" },
79
+ agentProfile: "sales-researcher",
80
+ })
81
+ );
82
+
83
+ expect(result.isError).toBeFalsy();
84
+ const payload = JSON.parse(result.content[0].text) as {
85
+ workflowId: string;
86
+ rowCount: number;
87
+ };
88
+ expect(payload.workflowId).toBe("wf_xyz");
89
+ expect(payload.rowCount).toBe(4);
90
+ });
91
+
92
+ it("falls back to ctx.projectId when projectId is not supplied", async () => {
93
+ mockCreateEnrichmentWorkflow.mockResolvedValueOnce({
94
+ workflowId: "wf_a",
95
+ rowCount: 1,
96
+ });
97
+
98
+ const tool = findEnrichTool();
99
+ await tool.handler({
100
+ tableId: "tbl_x",
101
+ prompt: "x",
102
+ targetColumn: "linkedin",
103
+ });
104
+
105
+ const callArg = mockCreateEnrichmentWorkflow.mock.calls[0][1] as {
106
+ projectId?: string;
107
+ };
108
+ expect(callArg.projectId).toBe("proj_test");
109
+ });
110
+
111
+ it("returns an error result when createEnrichmentWorkflow throws", async () => {
112
+ mockCreateEnrichmentWorkflow.mockRejectedValueOnce(
113
+ new Error("Table tbl_missing not found")
114
+ );
115
+
116
+ const tool = findEnrichTool();
117
+ const result = await tool.handler({
118
+ tableId: "tbl_missing",
119
+ prompt: "x",
120
+ targetColumn: "linkedin",
121
+ });
122
+
123
+ expect(result.isError).toBe(true);
124
+ const payload = JSON.parse(result.content[0].text) as { error: string };
125
+ expect(payload.error).toContain("not found");
126
+ });
127
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it, vi, beforeEach } from "vitest";
2
+
3
+ vi.mock("@/lib/agents/profiles/list-fused-profiles", () => ({
4
+ listFusedProfiles: vi.fn(async (projectDir: string | null) =>
5
+ [
6
+ {
7
+ id: "general",
8
+ name: "General",
9
+ description: "Reg",
10
+ domain: "general",
11
+ tags: [],
12
+ },
13
+ projectDir
14
+ ? {
15
+ id: "project-only",
16
+ name: "Project Only",
17
+ description: "Proj",
18
+ domain: "skill",
19
+ tags: [],
20
+ origin: "filesystem-project",
21
+ }
22
+ : null,
23
+ ].filter(Boolean)
24
+ ),
25
+ }));
26
+
27
+ describe("list_profiles chat tool", () => {
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ it("returns fused profiles when called with a projectDir", async () => {
33
+ const { getListProfilesTool } = await import("@/lib/chat/tools/profile-tools");
34
+ const tool = getListProfilesTool("/fake/project");
35
+ const result = await tool.handler({});
36
+ // ok() wraps data as MCP content — parse the JSON text back out
37
+ const text = result.content[0].text;
38
+ const list = JSON.parse(text) as { id: string }[];
39
+ expect(Array.isArray(list)).toBe(true);
40
+ expect(list.some((p) => p.id === "project-only")).toBe(true);
41
+ });
42
+
43
+ it("returns registry-only profiles when projectDir is null", async () => {
44
+ const { getListProfilesTool } = await import("@/lib/chat/tools/profile-tools");
45
+ const tool = getListProfilesTool(null);
46
+ const result = await tool.handler({});
47
+ const text = result.content[0].text;
48
+ const list = JSON.parse(text) as { id: string }[];
49
+ expect(list.every((p) => p.id !== "project-only")).toBe(true);
50
+ });
51
+ });
@@ -0,0 +1,261 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { z } from "zod";
3
+
4
+ interface ScheduleRow {
5
+ id: string;
6
+ maxTurns: number | null;
7
+ [key: string]: unknown;
8
+ }
9
+
10
+ const { mockState } = vi.hoisted(() => ({
11
+ mockState: {
12
+ rows: [] as ScheduleRow[],
13
+ lastInsertValues: null as Record<string, unknown> | null,
14
+ lastUpdateValues: null as Record<string, unknown> | null,
15
+ },
16
+ }));
17
+
18
+ vi.mock("@/lib/db", () => {
19
+ const selectBuilder = {
20
+ from() { return this; },
21
+ where() { return this; },
22
+ orderBy() { return this; },
23
+ limit() { return this; },
24
+ get() { return Promise.resolve(mockState.rows[0]); },
25
+ then<TResolve>(resolve: (rows: ScheduleRow[]) => TResolve) {
26
+ return Promise.resolve(mockState.rows).then(resolve);
27
+ },
28
+ };
29
+ return {
30
+ db: {
31
+ select: () => selectBuilder,
32
+ insert: () => ({
33
+ values: (v: Record<string, unknown>) => {
34
+ mockState.lastInsertValues = v;
35
+ mockState.rows = [{ id: "sched-1", maxTurns: null, ...v } as ScheduleRow];
36
+ return Promise.resolve();
37
+ },
38
+ }),
39
+ update: () => ({
40
+ set: (v: Record<string, unknown>) => {
41
+ mockState.lastUpdateValues = v;
42
+ mockState.rows[0] = { ...mockState.rows[0], ...v } as ScheduleRow;
43
+ return { where: () => Promise.resolve() };
44
+ },
45
+ }),
46
+ delete: () => ({ where: () => Promise.resolve() }),
47
+ },
48
+ };
49
+ });
50
+
51
+ vi.mock("@/lib/db/schema", () => ({
52
+ schedules: {
53
+ id: "id",
54
+ status: "status",
55
+ projectId: "projectId",
56
+ updatedAt: "updatedAt",
57
+ cronExpression: "cronExpression",
58
+ },
59
+ }));
60
+
61
+ vi.mock("drizzle-orm", () => ({
62
+ eq: () => ({}),
63
+ and: () => ({}),
64
+ desc: () => ({}),
65
+ like: () => ({}),
66
+ }));
67
+
68
+ vi.mock("@/lib/schedules/interval-parser", () => ({
69
+ parseInterval: () => "*/30 * * * *",
70
+ computeNextFireTime: () => new Date("2026-04-11T10:00:00Z"),
71
+ computeStaggeredCron: (cron: string) => ({
72
+ cronExpression: cron,
73
+ offsetApplied: 0,
74
+ collided: false,
75
+ }),
76
+ }));
77
+
78
+ vi.mock("@/lib/schedules/nlp-parser", () => ({
79
+ parseNaturalLanguage: () => null,
80
+ }));
81
+
82
+ vi.mock("@/lib/schedules/prompt-analyzer", () => ({
83
+ analyzePromptEfficiency: () => [],
84
+ }));
85
+
86
+ import { scheduleTools } from "../schedule-tools";
87
+
88
+ function getTool(name: string) {
89
+ const tools = scheduleTools({ projectId: "proj-1" } as never);
90
+ const tool = tools.find((t) => t.name === name);
91
+ if (!tool) throw new Error(`Tool not found: ${name}`);
92
+ return tool;
93
+ }
94
+
95
+ function parseArgs(toolName: string, args: unknown) {
96
+ const tool = getTool(toolName);
97
+ return z.object(tool.zodShape).safeParse(args);
98
+ }
99
+
100
+ beforeEach(() => {
101
+ mockState.rows = [];
102
+ mockState.lastInsertValues = null;
103
+ mockState.lastUpdateValues = null;
104
+ });
105
+
106
+ describe("create_schedule maxTurns Zod validation", () => {
107
+ const base = {
108
+ name: "test",
109
+ prompt: "hello",
110
+ interval: "every 30 minutes",
111
+ };
112
+
113
+ it("accepts a valid maxTurns value", () => {
114
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 50 });
115
+ expect(result.success).toBe(true);
116
+ });
117
+
118
+ it("accepts omitted maxTurns (inherit default)", () => {
119
+ const result = parseArgs("create_schedule", base);
120
+ expect(result.success).toBe(true);
121
+ });
122
+
123
+ it("rejects maxTurns below 10", () => {
124
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 9 });
125
+ expect(result.success).toBe(false);
126
+ });
127
+
128
+ it("rejects maxTurns above 500", () => {
129
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 501 });
130
+ expect(result.success).toBe(false);
131
+ });
132
+
133
+ it("rejects non-integer maxTurns", () => {
134
+ const result = parseArgs("create_schedule", { ...base, maxTurns: 50.5 });
135
+ expect(result.success).toBe(false);
136
+ });
137
+
138
+ it("rejects explicit null on create (only update supports clear-to-null)", () => {
139
+ const result = parseArgs("create_schedule", { ...base, maxTurns: null });
140
+ expect(result.success).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("update_schedule maxTurns Zod validation", () => {
145
+ const base = { scheduleId: "sched-1" };
146
+
147
+ it("accepts a valid maxTurns value", () => {
148
+ const result = parseArgs("update_schedule", { ...base, maxTurns: 100 });
149
+ expect(result.success).toBe(true);
150
+ });
151
+
152
+ it("accepts explicit null to clear an override", () => {
153
+ const result = parseArgs("update_schedule", { ...base, maxTurns: null });
154
+ expect(result.success).toBe(true);
155
+ });
156
+
157
+ it("accepts omitted maxTurns (unchanged)", () => {
158
+ const result = parseArgs("update_schedule", base);
159
+ expect(result.success).toBe(true);
160
+ });
161
+
162
+ it("rejects out-of-range maxTurns on update", () => {
163
+ const result = parseArgs("update_schedule", { ...base, maxTurns: 9 });
164
+ expect(result.success).toBe(false);
165
+ });
166
+ });
167
+
168
+ describe("create_schedule maxTurns persistence", () => {
169
+ it("writes maxTurns to the insert payload when provided", async () => {
170
+ const tool = getTool("create_schedule");
171
+ await tool.handler({
172
+ name: "test",
173
+ prompt: "hello",
174
+ interval: "every 30 minutes",
175
+ maxTurns: 75,
176
+ });
177
+ expect(mockState.lastInsertValues).not.toBeNull();
178
+ expect(mockState.lastInsertValues?.maxTurns).toBe(75);
179
+ });
180
+
181
+ it("writes null to maxTurns when omitted (inherit default)", async () => {
182
+ const tool = getTool("create_schedule");
183
+ await tool.handler({
184
+ name: "test",
185
+ prompt: "hello",
186
+ interval: "every 30 minutes",
187
+ });
188
+ expect(mockState.lastInsertValues?.maxTurns).toBe(null);
189
+ });
190
+
191
+ it("sets maxTurnsSetAt to a Date when maxTurns is provided", async () => {
192
+ const tool = getTool("create_schedule");
193
+ await tool.handler({
194
+ name: "test",
195
+ prompt: "hello",
196
+ interval: "every 30 minutes",
197
+ maxTurns: 75,
198
+ });
199
+ expect(mockState.lastInsertValues?.maxTurnsSetAt).toBeInstanceOf(Date);
200
+ });
201
+
202
+ it("sets maxTurnsSetAt to null when maxTurns is omitted", async () => {
203
+ const tool = getTool("create_schedule");
204
+ await tool.handler({
205
+ name: "test",
206
+ prompt: "hello",
207
+ interval: "every 30 minutes",
208
+ });
209
+ expect(mockState.lastInsertValues?.maxTurnsSetAt).toBe(null);
210
+ });
211
+ });
212
+
213
+ describe("update_schedule maxTurns persistence", () => {
214
+ beforeEach(() => {
215
+ mockState.rows = [{
216
+ id: "sched-1",
217
+ name: "existing",
218
+ status: "active",
219
+ maxTurns: 50,
220
+ } as ScheduleRow];
221
+ });
222
+
223
+ it("writes the new maxTurns value when provided", async () => {
224
+ const tool = getTool("update_schedule");
225
+ await tool.handler({ scheduleId: "sched-1", maxTurns: 120 });
226
+ expect(mockState.lastUpdateValues?.maxTurns).toBe(120);
227
+ });
228
+
229
+ it("writes null when explicitly clearing the override", async () => {
230
+ const tool = getTool("update_schedule");
231
+ await tool.handler({ scheduleId: "sched-1", maxTurns: null });
232
+ expect(mockState.lastUpdateValues).not.toBeNull();
233
+ expect("maxTurns" in (mockState.lastUpdateValues ?? {})).toBe(true);
234
+ expect(mockState.lastUpdateValues?.maxTurns).toBe(null);
235
+ });
236
+
237
+ it("does not touch maxTurns when the field is omitted", async () => {
238
+ const tool = getTool("update_schedule");
239
+ await tool.handler({ scheduleId: "sched-1", name: "renamed" });
240
+ expect("maxTurns" in (mockState.lastUpdateValues ?? {})).toBe(false);
241
+ });
242
+
243
+ it("sets maxTurnsSetAt to a Date when maxTurns is set to a number", async () => {
244
+ const tool = getTool("update_schedule");
245
+ await tool.handler({ scheduleId: "sched-1", maxTurns: 120 });
246
+ expect(mockState.lastUpdateValues?.maxTurnsSetAt).toBeInstanceOf(Date);
247
+ });
248
+
249
+ it("sets maxTurnsSetAt to null when maxTurns is cleared", async () => {
250
+ const tool = getTool("update_schedule");
251
+ await tool.handler({ scheduleId: "sched-1", maxTurns: null });
252
+ expect("maxTurnsSetAt" in (mockState.lastUpdateValues ?? {})).toBe(true);
253
+ expect(mockState.lastUpdateValues?.maxTurnsSetAt).toBe(null);
254
+ });
255
+
256
+ it("does not touch maxTurnsSetAt when maxTurns is omitted", async () => {
257
+ const tool = getTool("update_schedule");
258
+ await tool.handler({ scheduleId: "sched-1", name: "renamed" });
259
+ expect("maxTurnsSetAt" in (mockState.lastUpdateValues ?? {})).toBe(false);
260
+ });
261
+ });