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,68 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ COMMAND_TABS,
4
+ GROUP_TO_TAB,
5
+ partitionCatalogByTab,
6
+ isCommandTabId,
7
+ type CommandTabId,
8
+ } from "../command-tabs";
9
+ import type { ToolCatalogEntry, ToolGroup } from "../tool-catalog";
10
+
11
+ const entry = (name: string, group: ToolGroup): ToolCatalogEntry => ({
12
+ name,
13
+ description: name,
14
+ group,
15
+ });
16
+
17
+ describe("command-tabs", () => {
18
+ it("exposes four tabs in canonical order", () => {
19
+ expect(COMMAND_TABS.map((t) => t.id)).toEqual([
20
+ "actions",
21
+ "skills",
22
+ "tools",
23
+ "entities",
24
+ ]);
25
+ });
26
+
27
+ it("maps every ToolGroup to exactly one tab", () => {
28
+ const groups: ToolGroup[] = [
29
+ "Session", "Tasks", "Projects", "Workflows", "Schedules", "Documents", "Tables",
30
+ "Notifications", "Profiles", "Skills", "Usage", "Settings", "Chat",
31
+ "Browser", "Utility",
32
+ ];
33
+ for (const g of groups) {
34
+ expect(GROUP_TO_TAB[g]).toBeDefined();
35
+ }
36
+ });
37
+
38
+ it("routes Session group to the Actions tab", () => {
39
+ expect(GROUP_TO_TAB.Session).toBe("actions");
40
+ });
41
+
42
+ it("routes Skills group to the Skills tab", () => {
43
+ expect(GROUP_TO_TAB.Skills).toBe("skills");
44
+ });
45
+
46
+ it("routes Browser + Utility to the Tools tab", () => {
47
+ expect(GROUP_TO_TAB.Browser).toBe("tools");
48
+ expect(GROUP_TO_TAB.Utility).toBe("tools");
49
+ });
50
+
51
+ it("partitions catalog entries by tab", () => {
52
+ const catalog: ToolCatalogEntry[] = [
53
+ entry("list_tasks", "Tasks"),
54
+ entry("researcher", "Skills"),
55
+ entry("take_screenshot", "Browser"),
56
+ ];
57
+ const part = partitionCatalogByTab(catalog);
58
+ expect(part.actions.map((e) => e.name)).toEqual(["list_tasks"]);
59
+ expect(part.skills.map((e) => e.name)).toEqual(["researcher"]);
60
+ expect(part.tools.map((e) => e.name)).toEqual(["take_screenshot"]);
61
+ expect(part.entities).toEqual([]);
62
+ });
63
+
64
+ it("isCommandTabId rejects unknown values", () => {
65
+ expect(isCommandTabId("actions")).toBe(true);
66
+ expect(isCommandTabId("random")).toBe(false);
67
+ });
68
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const { mockFs } = vi.hoisted(() => ({
4
+ mockFs: {
5
+ realpathMap: new Map<string, string>(),
6
+ files: new Map<string, string>(),
7
+ statMap: new Map<string, { size: number }>(),
8
+ },
9
+ }));
10
+
11
+ vi.mock("node:fs", () => {
12
+ const realpathSync = (p: string) => {
13
+ const real = mockFs.realpathMap.get(p);
14
+ if (real === undefined) throw new Error(`ENOENT: realpath ${p}`);
15
+ return real;
16
+ };
17
+ const statSync = (p: string) => {
18
+ const s = mockFs.statMap.get(p);
19
+ if (!s) throw new Error(`ENOENT: stat ${p}`);
20
+ return s;
21
+ };
22
+ const readFileSync = (p: string) => {
23
+ const content = mockFs.files.get(p);
24
+ if (content === undefined) throw new Error(`ENOENT: read ${p}`);
25
+ return content;
26
+ };
27
+ return {
28
+ default: { realpathSync, statSync, readFileSync },
29
+ realpathSync,
30
+ statSync,
31
+ readFileSync,
32
+ };
33
+ });
34
+
35
+ import { expandFileMention } from "../files/expand-mention";
36
+
37
+ const CWD = "/repo";
38
+
39
+ beforeEach(() => {
40
+ mockFs.realpathMap.clear();
41
+ mockFs.files.clear();
42
+ mockFs.statMap.clear();
43
+ mockFs.realpathMap.set(CWD, CWD);
44
+ });
45
+
46
+ function registerFile(relPath: string, content: string) {
47
+ const abs = `${CWD}/${relPath}`;
48
+ mockFs.files.set(abs, content);
49
+ mockFs.statMap.set(abs, { size: Buffer.byteLength(content, "utf8") });
50
+ }
51
+
52
+ describe("expandFileMention", () => {
53
+ it("inlines files under 8 KB with a path header and fenced code block", () => {
54
+ registerFile("src/a.ts", "export const x = 1;\n");
55
+ const out = expandFileMention("src/a.ts", CWD).join("\n");
56
+ expect(out).toContain("### File: src/a.ts");
57
+ expect(out).toContain("```ts");
58
+ expect(out).toContain("export const x = 1;");
59
+ expect(out).toContain("```");
60
+ });
61
+
62
+ it("references files >= 8 KB without inlining their content", () => {
63
+ const big = "A".repeat(10 * 1024);
64
+ registerFile("docs/large.md", big);
65
+ const out = expandFileMention("docs/large.md", CWD).join("\n");
66
+ expect(out).toContain("File (by reference): docs/large.md");
67
+ expect(out).toContain("KB)"); // size hint
68
+ expect(out).toContain("Use the Read tool");
69
+ expect(out).not.toContain(big); // content not inlined
70
+ });
71
+
72
+ it("emits a not-found note when the file no longer exists", () => {
73
+ const out = expandFileMention("src/gone.ts", CWD).join("\n");
74
+ expect(out).toContain("### File: src/gone.ts");
75
+ expect(out).toContain("(file not found at context-build time)");
76
+ });
77
+
78
+ it("rejects paths that resolve outside cwd (security guardrail)", () => {
79
+ const out = expandFileMention("../escape.ts", CWD).join("\n");
80
+ expect(out).toContain("(invalid path — escapes working directory)");
81
+ expect(out).not.toContain("(file not found"); // did not even try to read
82
+ });
83
+
84
+ it("skips pathological files >= 50 MB silently (returns empty)", () => {
85
+ const abs = `${CWD}/huge.bin`;
86
+ mockFs.statMap.set(abs, { size: 60 * 1024 * 1024 });
87
+ // readFileSync is never reached
88
+ const out = expandFileMention("huge.bin", CWD);
89
+ expect(out).toEqual([]);
90
+ });
91
+
92
+ it("picks an 'unknown' code-fence language for files without an extension", () => {
93
+ const abs = `${CWD}/Makefile`;
94
+ mockFs.files.set(abs, "all:\n\techo ok\n");
95
+ mockFs.statMap.set(abs, { size: 16 });
96
+ const out = expandFileMention("Makefile", CWD).join("\n");
97
+ // .split(".").pop() on a name with no dot returns the whole name,
98
+ // which is the best we can do without a language map. We just
99
+ // assert a header + a closing fence are present.
100
+ expect(out).toContain("### File: Makefile");
101
+ expect(out).toMatch(/```[\w]*/);
102
+ expect(out).toContain("all:");
103
+ });
104
+
105
+ it("emits a read-failure note if the file stats OK but reads throw", () => {
106
+ const abs = `${CWD}/src/binary.ico`;
107
+ mockFs.statMap.set(abs, { size: 100 });
108
+ // Do NOT register contents — readFileSync will throw
109
+ const out = expandFileMention("src/binary.ico", CWD).join("\n");
110
+ expect(out).toContain("(file could not be read as UTF-8)");
111
+ });
112
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ loadDismissals,
4
+ saveDismissal,
5
+ activeDismissedIds,
6
+ DISMISSAL_TTL_MS,
7
+ } from "../dismissals";
8
+
9
+ type Store = { read: () => string | null; write: (v: string) => void };
10
+
11
+ function mockStore(initial: string | null = null): Store {
12
+ let v = initial;
13
+ return {
14
+ read: () => v,
15
+ write: (next) => {
16
+ v = next;
17
+ },
18
+ };
19
+ }
20
+
21
+ describe("dismissals", () => {
22
+ const NOW = 1_700_000_000_000;
23
+
24
+ it("returns empty when store is null", () => {
25
+ const store = mockStore();
26
+ const all = loadDismissals(store);
27
+ expect(all).toEqual({});
28
+ });
29
+
30
+ it("saves dismissals keyed by conversation + skill", () => {
31
+ const store = mockStore();
32
+ saveDismissal(store, "conv-1", "skill-a", NOW);
33
+ const all = loadDismissals(store);
34
+ expect(all["conv-1"]["skill-a"]).toBe(NOW);
35
+ });
36
+
37
+ it("activeDismissedIds excludes expired entries", () => {
38
+ const store = mockStore();
39
+ saveDismissal(store, "c1", "fresh", NOW);
40
+ saveDismissal(store, "c1", "old", NOW - DISMISSAL_TTL_MS - 1000);
41
+ const ids = activeDismissedIds(store, "c1", NOW);
42
+ expect(ids.has("fresh")).toBe(true);
43
+ expect(ids.has("old")).toBe(false);
44
+ });
45
+
46
+ it("returns empty set when conversation has no dismissals", () => {
47
+ const store = mockStore();
48
+ expect(activeDismissedIds(store, "never-seen", NOW).size).toBe(0);
49
+ });
50
+
51
+ it("silently tolerates store write errors", () => {
52
+ const store: Store = {
53
+ read: () => null,
54
+ write: () => {
55
+ throw new Error("quota");
56
+ },
57
+ };
58
+ expect(() => saveDismissal(store, "c1", "s1", NOW)).not.toThrow();
59
+ });
60
+
61
+ it("silently tolerates corrupt JSON on read", () => {
62
+ const store = mockStore("not-json");
63
+ expect(loadDismissals(store)).toEqual({});
64
+ });
65
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ CLAUDE_SDK_ALLOWED_TOOLS,
4
+ CLAUDE_SDK_SETTING_SOURCES,
5
+ CLAUDE_SDK_READ_ONLY_FS_TOOLS,
6
+ } from "@/lib/chat/engine";
7
+
8
+ describe("Claude SDK options (Phase 1a)", () => {
9
+ it("declares settingSources loading user and project config", () => {
10
+ expect(CLAUDE_SDK_SETTING_SOURCES).toEqual(["user", "project"]);
11
+ });
12
+
13
+ it("includes Skill, filesystem tools, Bash, and TodoWrite in allowedTools", () => {
14
+ expect(CLAUDE_SDK_ALLOWED_TOOLS).toEqual(
15
+ expect.arrayContaining([
16
+ "Skill",
17
+ "Read",
18
+ "Grep",
19
+ "Glob",
20
+ "Edit",
21
+ "Write",
22
+ "Bash",
23
+ "TodoWrite",
24
+ ])
25
+ );
26
+ });
27
+
28
+ it("does NOT include Task (subagent delegation replaced by Stagent primitives)", () => {
29
+ expect(CLAUDE_SDK_ALLOWED_TOOLS).not.toContain("Task");
30
+ });
31
+
32
+ it("declares Read, Grep, Glob as read-only filesystem tools", () => {
33
+ expect(CLAUDE_SDK_READ_ONLY_FS_TOOLS).toEqual(
34
+ new Set(["Read", "Grep", "Glob"])
35
+ );
36
+ });
37
+
38
+ it("does NOT treat Edit, Write, Bash, or TodoWrite as read-only", () => {
39
+ for (const tool of ["Edit", "Write", "Bash", "TodoWrite"]) {
40
+ expect(CLAUDE_SDK_READ_ONLY_FS_TOOLS.has(tool)).toBe(false);
41
+ }
42
+ });
43
+ });
44
+
45
+ import { canUseToolForTest } from "@/lib/chat/engine";
46
+
47
+ describe("canUseTool auto-allow policy for SDK filesystem tools", () => {
48
+ it("auto-allows Read without a permission request", async () => {
49
+ const result = await canUseToolForTest("Read", { file_path: "/tmp/x" });
50
+ expect(result.behavior).toBe("allow");
51
+ });
52
+
53
+ it("auto-allows Grep without a permission request", async () => {
54
+ const result = await canUseToolForTest("Grep", { pattern: "foo" });
55
+ expect(result.behavior).toBe("allow");
56
+ });
57
+
58
+ it("auto-allows Glob without a permission request", async () => {
59
+ const result = await canUseToolForTest("Glob", { pattern: "**/*.ts" });
60
+ expect(result.behavior).toBe("allow");
61
+ });
62
+
63
+ it("auto-allows Skill tool invocation", async () => {
64
+ const result = await canUseToolForTest("Skill", { skill: "code-reviewer" });
65
+ expect(result.behavior).toBe("allow");
66
+ });
67
+
68
+ it("does NOT auto-allow Edit (must go through permission flow)", async () => {
69
+ const result = await canUseToolForTest("Edit", { file_path: "/tmp/x", content: "y" });
70
+ expect(result.behavior).not.toBe("allow");
71
+ });
72
+
73
+ it("does NOT auto-allow Bash", async () => {
74
+ const result = await canUseToolForTest("Bash", { command: "ls" });
75
+ expect(result.behavior).not.toBe("allow");
76
+ });
77
+ });
78
+
79
+ describe("hooks excluded per Q2", () => {
80
+ it("does not declare a hooks field alongside settingSources", async () => {
81
+ const fs = await import("fs");
82
+ const path = await import("path");
83
+ const enginePath = path.resolve(__dirname, "../engine.ts");
84
+ const source = fs.readFileSync(enginePath, "utf8");
85
+ // Assert that within the query() options block, there is no `hooks:` field.
86
+ // This is a regex-level check because the options object is inline literals.
87
+ const optionsBlock = source.match(/query\(\s*\{[\s\S]*?\}\s*\)/)?.[0] ?? "";
88
+ expect(optionsBlock).toContain("settingSources");
89
+ expect(optionsBlock).not.toMatch(/\bhooks\s*:/);
90
+ });
91
+ });
92
+
93
+ describe("CLAUDE_SDK_* constants source-of-truth", () => {
94
+ it("exports CLAUDE_SDK_ALLOWED_TOOLS from runtime/claude-sdk", async () => {
95
+ const mod = await import("@/lib/agents/runtime/claude-sdk");
96
+ expect(mod.CLAUDE_SDK_ALLOWED_TOOLS).toEqual([
97
+ "Skill",
98
+ "Read",
99
+ "Grep",
100
+ "Glob",
101
+ "Edit",
102
+ "Write",
103
+ "Bash",
104
+ "TodoWrite",
105
+ ]);
106
+ });
107
+
108
+ it("exports CLAUDE_SDK_SETTING_SOURCES from runtime/claude-sdk", async () => {
109
+ const mod = await import("@/lib/agents/runtime/claude-sdk");
110
+ expect(mod.CLAUDE_SDK_SETTING_SOURCES).toEqual(["user", "project"]);
111
+ });
112
+
113
+ it("exports CLAUDE_SDK_READ_ONLY_FS_TOOLS from runtime/claude-sdk", async () => {
114
+ const mod = await import("@/lib/agents/runtime/claude-sdk");
115
+ expect(mod.CLAUDE_SDK_READ_ONLY_FS_TOOLS).toEqual(new Set(["Read", "Grep", "Glob"]));
116
+ });
117
+ });
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { db } from "@/lib/db";
3
+ import { conversations, chatMessages } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { randomUUID } from "crypto";
6
+ import { finalizeStreamingMessage } from "../reconcile";
7
+
8
+ function seedConversation(): string {
9
+ const id = randomUUID();
10
+ const now = new Date();
11
+ db.insert(conversations)
12
+ .values({
13
+ id,
14
+ runtimeId: "test-runtime",
15
+ status: "active",
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ })
19
+ .run();
20
+ return id;
21
+ }
22
+
23
+ function seedStreaming(convId: string, content: string): string {
24
+ const id = randomUUID();
25
+ db.insert(chatMessages)
26
+ .values({
27
+ id,
28
+ conversationId: convId,
29
+ role: "assistant",
30
+ content,
31
+ status: "streaming",
32
+ createdAt: new Date(),
33
+ })
34
+ .run();
35
+ return id;
36
+ }
37
+
38
+ describe("finalizeStreamingMessage", () => {
39
+ beforeEach(() => {
40
+ db.delete(chatMessages).run();
41
+ db.delete(conversations).run();
42
+ });
43
+
44
+ it("is a no-op when the message is already complete", async () => {
45
+ const convId = seedConversation();
46
+ const id = randomUUID();
47
+ db.insert(chatMessages)
48
+ .values({
49
+ id,
50
+ conversationId: convId,
51
+ role: "assistant",
52
+ content: "Already finished",
53
+ status: "complete",
54
+ createdAt: new Date(),
55
+ })
56
+ .run();
57
+
58
+ await finalizeStreamingMessage(id, "ignored salvage text");
59
+
60
+ const row = db
61
+ .select()
62
+ .from(chatMessages)
63
+ .where(eq(chatMessages.id, id))
64
+ .get();
65
+ expect(row?.status).toBe("complete");
66
+ expect(row?.content).toBe("Already finished");
67
+ });
68
+
69
+ it("salvages streaming row with substantial content as complete", async () => {
70
+ const convId = seedConversation();
71
+ const id = seedStreaming(convId, "");
72
+ const partialText =
73
+ "I searched the web and found three relevant articles about the topic. Here are the highlights of what I learned...";
74
+
75
+ await finalizeStreamingMessage(id, partialText);
76
+
77
+ const row = db
78
+ .select()
79
+ .from(chatMessages)
80
+ .where(eq(chatMessages.id, id))
81
+ .get();
82
+ expect(row?.status).toBe("complete");
83
+ expect(row?.content).toBe(partialText);
84
+ });
85
+
86
+ it("marks streaming row with no content as error with fallback string", async () => {
87
+ const convId = seedConversation();
88
+ const id = seedStreaming(convId, "");
89
+
90
+ await finalizeStreamingMessage(id, "");
91
+
92
+ const row = db
93
+ .select()
94
+ .from(chatMessages)
95
+ .where(eq(chatMessages.id, id))
96
+ .get();
97
+ expect(row?.status).toBe("error");
98
+ expect(row?.content).toMatch(/interrupted/i);
99
+ expect(row?.content.length).toBeGreaterThan(0);
100
+ });
101
+
102
+ it("marks streaming row with very short content as error, not complete", async () => {
103
+ const convId = seedConversation();
104
+ const id = seedStreaming(convId, "");
105
+
106
+ // 20 chars — not substantial enough to call "complete"
107
+ await finalizeStreamingMessage(id, "Just a short reply.");
108
+
109
+ const row = db
110
+ .select()
111
+ .from(chatMessages)
112
+ .where(eq(chatMessages.id, id))
113
+ .get();
114
+ expect(row?.status).toBe("error");
115
+ expect(row?.content).toBe("Just a short reply.");
116
+ });
117
+
118
+ it("marks streaming row with whitespace-only fullText as error with fallback", async () => {
119
+ const convId = seedConversation();
120
+ const id = seedStreaming(convId, "");
121
+
122
+ await finalizeStreamingMessage(id, " \n\n \t ");
123
+
124
+ const row = db
125
+ .select()
126
+ .from(chatMessages)
127
+ .where(eq(chatMessages.id, id))
128
+ .get();
129
+ expect(row?.status).toBe("error");
130
+ expect(row?.content).toMatch(/interrupted/i);
131
+ });
132
+
133
+ it("is a no-op when the message does not exist", async () => {
134
+ // Should not throw — defensive null check
135
+ await expect(
136
+ finalizeStreamingMessage("nonexistent-id", "some text"),
137
+ ).resolves.not.toThrow();
138
+ });
139
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { db } from "@/lib/db";
3
+ import { conversations, chatMessages } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { randomUUID } from "crypto";
6
+ import { reconcileStreamingMessages } from "../reconcile";
7
+
8
+ function seedConversation(): string {
9
+ const id = randomUUID();
10
+ const now = new Date();
11
+ db.insert(conversations)
12
+ .values({
13
+ id,
14
+ runtimeId: "test-runtime",
15
+ status: "active",
16
+ createdAt: now,
17
+ updatedAt: now,
18
+ })
19
+ .run();
20
+ return id;
21
+ }
22
+
23
+ function seedMessage(opts: {
24
+ conversationId: string;
25
+ status: "streaming" | "complete" | "error";
26
+ content: string;
27
+ createdAt: Date;
28
+ }): string {
29
+ const id = randomUUID();
30
+ db.insert(chatMessages)
31
+ .values({
32
+ id,
33
+ conversationId: opts.conversationId,
34
+ role: "assistant",
35
+ content: opts.content,
36
+ status: opts.status,
37
+ createdAt: opts.createdAt,
38
+ })
39
+ .run();
40
+ return id;
41
+ }
42
+
43
+ describe("reconcileStreamingMessages", () => {
44
+ beforeEach(() => {
45
+ // Isolate each test
46
+ db.delete(chatMessages).run();
47
+ db.delete(conversations).run();
48
+ });
49
+
50
+ it("sweeps a 20-min-old streaming row with empty content to error state with fallback", async () => {
51
+ const convId = seedConversation();
52
+ const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
53
+ const msgId = seedMessage({
54
+ conversationId: convId,
55
+ status: "streaming",
56
+ content: "",
57
+ createdAt: twentyMinAgo,
58
+ });
59
+
60
+ const swept = await reconcileStreamingMessages();
61
+
62
+ expect(swept).toBe(1);
63
+ const row = db
64
+ .select()
65
+ .from(chatMessages)
66
+ .where(eq(chatMessages.id, msgId))
67
+ .get();
68
+ expect(row?.status).toBe("error");
69
+ expect(row?.content).toMatch(/Interrupted/i);
70
+ expect(row?.content.length).toBeGreaterThan(0);
71
+ });
72
+
73
+ it("leaves a 30-second-old streaming row untouched", async () => {
74
+ const convId = seedConversation();
75
+ const thirtySecAgo = new Date(Date.now() - 30 * 1000);
76
+ const msgId = seedMessage({
77
+ conversationId: convId,
78
+ status: "streaming",
79
+ content: "",
80
+ createdAt: thirtySecAgo,
81
+ });
82
+
83
+ const swept = await reconcileStreamingMessages();
84
+
85
+ expect(swept).toBe(0);
86
+ const row = db
87
+ .select()
88
+ .from(chatMessages)
89
+ .where(eq(chatMessages.id, msgId))
90
+ .get();
91
+ expect(row?.status).toBe("streaming");
92
+ expect(row?.content).toBe("");
93
+ });
94
+
95
+ it("preserves partial content when sweeping old streaming row", async () => {
96
+ const convId = seedConversation();
97
+ const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
98
+ const msgId = seedMessage({
99
+ conversationId: convId,
100
+ status: "streaming",
101
+ content: "Here is what I found so",
102
+ createdAt: twentyMinAgo,
103
+ });
104
+
105
+ await reconcileStreamingMessages();
106
+
107
+ const row = db
108
+ .select()
109
+ .from(chatMessages)
110
+ .where(eq(chatMessages.id, msgId))
111
+ .get();
112
+ expect(row?.status).toBe("error");
113
+ expect(row?.content).toBe("Here is what I found so");
114
+ });
115
+
116
+ it("leaves complete messages untouched regardless of age", async () => {
117
+ const convId = seedConversation();
118
+ const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
119
+ const msgId = seedMessage({
120
+ conversationId: convId,
121
+ status: "complete",
122
+ content: "Finished response",
123
+ createdAt: twentyMinAgo,
124
+ });
125
+
126
+ const swept = await reconcileStreamingMessages();
127
+
128
+ expect(swept).toBe(0);
129
+ const row = db
130
+ .select()
131
+ .from(chatMessages)
132
+ .where(eq(chatMessages.id, msgId))
133
+ .get();
134
+ expect(row?.status).toBe("complete");
135
+ expect(row?.content).toBe("Finished response");
136
+ });
137
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { detectSkillConflicts } from "../skill-conflict";
3
+
4
+ describe("detectSkillConflicts", () => {
5
+ it("returns no conflicts for two unrelated skills", () => {
6
+ const a = { id: "a", name: "code-reviewer", content: "Always run ESLint before reviewing code." };
7
+ const b = { id: "b", name: "haiku-poet", content: "Use 5-7-5 syllable structure." };
8
+ expect(detectSkillConflicts(a, b)).toEqual([]);
9
+ });
10
+
11
+ it("flags directive divergence on a shared topic", () => {
12
+ const a = { id: "a", name: "tdd", content: "Always write the test first. Never write production code without a failing test." };
13
+ const b = { id: "b", name: "spike", content: "Never write tests during a spike. Prefer exploratory code." };
14
+ const conflicts = detectSkillConflicts(a, b);
15
+ expect(conflicts.length).toBeGreaterThan(0);
16
+ expect(conflicts[0]).toMatchObject({
17
+ skillA: "tdd",
18
+ skillB: "spike",
19
+ });
20
+ expect(conflicts[0].excerptA).toMatch(/test/i);
21
+ expect(conflicts[0].excerptB).toMatch(/test/i);
22
+ });
23
+
24
+ it("returns no conflicts when both skills agree on a topic", () => {
25
+ const a = { id: "a", name: "tdd", content: "Always write tests first." };
26
+ const b = { id: "b", name: "qa-strict", content: "Always write tests first and add coverage gates." };
27
+ expect(detectSkillConflicts(a, b)).toEqual([]);
28
+ });
29
+
30
+ it("ignores non-directive lines", () => {
31
+ const a = { id: "a", name: "x", content: "This skill is for documentation tasks." };
32
+ const b = { id: "b", name: "y", content: "Documentation is important context." };
33
+ expect(detectSkillConflicts(a, b)).toEqual([]);
34
+ });
35
+ });