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,142 @@
1
+ "use client";
2
+
3
+ /**
4
+ * `useSavedSearches` — client-side store for saved filter combinations
5
+ * surfaced in the chat mention popover and `⌘K` palette.
6
+ *
7
+ * Mirrors `use-pinned-entries.ts`: fetches once on mount, keeps an
8
+ * in-memory list, and writes back via PUT on every mutation (full-list
9
+ * replacement — see `src/app/api/settings/chat/saved-searches/route.ts`
10
+ * for design rationale).
11
+ */
12
+
13
+ import { useCallback, useEffect, useState } from "react";
14
+
15
+ export type SavedSearchSurface =
16
+ | "task"
17
+ | "project"
18
+ | "workflow"
19
+ | "document"
20
+ | "skill"
21
+ | "profile";
22
+
23
+ export interface SavedSearch {
24
+ id: string;
25
+ surface: SavedSearchSurface;
26
+ label: string;
27
+ filterInput: string;
28
+ createdAt: string;
29
+ }
30
+
31
+ interface UseSavedSearchesReturn {
32
+ searches: SavedSearch[];
33
+ loading: boolean;
34
+ save: (entry: Omit<SavedSearch, "id" | "createdAt">) => SavedSearch;
35
+ remove: (id: string) => void;
36
+ forSurface: (surface: SavedSearchSurface) => SavedSearch[];
37
+ /**
38
+ * Re-fetch from the server. Each `useSavedSearches()` consumer holds
39
+ * its own state — the chat popover and the ⌘K palette do not share a
40
+ * cache. Components that need to see edits made elsewhere (e.g. the
41
+ * palette opening after a save in the popover) call `refetch()` at
42
+ * the right moment to revalidate.
43
+ *
44
+ * See features/saved-search-polish-v1.md for the bug history.
45
+ */
46
+ refetch: () => Promise<void>;
47
+ rename: (id: string, label: string) => void;
48
+ }
49
+
50
+ export function useSavedSearches(): UseSavedSearchesReturn {
51
+ const [searches, setSearches] = useState<SavedSearch[]>([]);
52
+ const [loading, setLoading] = useState(true);
53
+
54
+ // Single fetch helper used by both the mount effect and `refetch`.
55
+ // Returns void so consumers can `await` revalidation if they want
56
+ // to wait for fresh data before continuing.
57
+ const fetchSearches = useCallback(async (): Promise<void> => {
58
+ try {
59
+ const r = await fetch("/api/settings/chat/saved-searches");
60
+ const data: { searches?: SavedSearch[] } = r.ok
61
+ ? await r.json()
62
+ : { searches: [] };
63
+ setSearches(data.searches ?? []);
64
+ } catch {
65
+ setSearches([]);
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ }, []);
70
+
71
+ useEffect(() => {
72
+ void fetchSearches();
73
+ }, [fetchSearches]);
74
+
75
+ const persist = useCallback(async (next: SavedSearch[]) => {
76
+ try {
77
+ await fetch("/api/settings/chat/saved-searches", {
78
+ method: "PUT",
79
+ headers: { "Content-Type": "application/json" },
80
+ body: JSON.stringify({ searches: next }),
81
+ });
82
+ } catch {
83
+ // Optimistic update already applied; server-sync failure silently
84
+ // swallowed. Matches the pins-hook contract.
85
+ }
86
+ }, []);
87
+
88
+ const save = useCallback(
89
+ (entry: Omit<SavedSearch, "id" | "createdAt">): SavedSearch => {
90
+ const full: SavedSearch = {
91
+ ...entry,
92
+ id:
93
+ typeof crypto !== "undefined" && "randomUUID" in crypto
94
+ ? crypto.randomUUID()
95
+ : `ss-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
96
+ createdAt: new Date().toISOString(),
97
+ };
98
+ setSearches((prev) => {
99
+ const next = [...prev, full];
100
+ void persist(next);
101
+ return next;
102
+ });
103
+ return full;
104
+ },
105
+ [persist]
106
+ );
107
+
108
+ const remove = useCallback(
109
+ (id: string) => {
110
+ setSearches((prev) => {
111
+ const next = prev.filter((s) => s.id !== id);
112
+ void persist(next);
113
+ return next;
114
+ });
115
+ },
116
+ [persist]
117
+ );
118
+
119
+ const forSurface = useCallback(
120
+ (surface: SavedSearchSurface) =>
121
+ searches.filter((s) => s.surface === surface),
122
+ [searches]
123
+ );
124
+
125
+ const refetch = useCallback(() => fetchSearches(), [fetchSearches]);
126
+
127
+ const rename = useCallback(
128
+ (id: string, label: string) => {
129
+ setSearches((prev) => {
130
+ const idx = prev.findIndex((s) => s.id === id);
131
+ if (idx === -1) return prev;
132
+ const next = prev.slice();
133
+ next[idx] = { ...next[idx], label };
134
+ void persist(next);
135
+ return next;
136
+ });
137
+ },
138
+ [persist]
139
+ );
140
+
141
+ return { searches, loading, save, remove, forSurface, refetch, rename };
142
+ }
@@ -0,0 +1,94 @@
1
+ export async function registerNodeInstrumentation() {
2
+ try {
3
+ // Instance bootstrap — creates local branch, handles dev-mode gates, consent flow.
4
+ // Runs BEFORE other startup so instance config is available downstream.
5
+ // Safe in the canonical stagent dev repo thanks to STAGENT_DEV_MODE=true
6
+ // in .env.local plus the .git/stagent-dev-mode sentinel file.
7
+ const { ensureInstance } = await import("@/lib/instance/bootstrap");
8
+ const instanceResult = await ensureInstance();
9
+ if (instanceResult.skipped) {
10
+ console.log(`[instance] bootstrap skipped: ${instanceResult.skipped}`);
11
+ } else {
12
+ for (const step of instanceResult.steps) {
13
+ if (step.status === "failed") {
14
+ console.error(`[instance] ${step.step} failed: ${step.reason}`);
15
+ }
16
+ }
17
+ }
18
+
19
+ // Run pending Drizzle migrations (DROP TABLE, CREATE INDEX, etc.)
20
+ // that can't be handled by bootstrap's IF NOT EXISTS pattern.
21
+ // Runs here (not in db/index.ts) to avoid SQLITE_BUSY during next build.
22
+ await runPendingMigrations();
23
+
24
+ // Instance upgrade poller — hourly `git fetch` to detect upstream commits.
25
+ // Skipped in dev mode; lightweight; uses advisory lock to prevent overlap.
26
+ const { startUpgradePoller } = await import("@/lib/instance/upgrade-poller");
27
+ startUpgradePoller();
28
+
29
+ const { startScheduler } = await import("@/lib/schedules/scheduler");
30
+ startScheduler();
31
+
32
+ const { startChannelPoller } = await import("@/lib/channels/poller");
33
+ startChannelPoller();
34
+
35
+ const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
36
+ startAutoBackup();
37
+
38
+ // History retention cleanup — prunes old agent_logs and usage_ledger
39
+ startHistoryCleanup();
40
+
41
+ } catch (err) {
42
+ console.error("Instrumentation startup failed:", err);
43
+ }
44
+ }
45
+
46
+ async function startHistoryCleanup() {
47
+ const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
48
+ const RETENTION_DAYS = 365;
49
+
50
+ async function cleanup() {
51
+ const { db } = await import("@/lib/db");
52
+ const { agentLogs, usageLedger } = await import("@/lib/db/schema");
53
+ const { lt } = await import("drizzle-orm");
54
+
55
+ const cutoff = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000);
56
+ db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
57
+ db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
58
+ }
59
+
60
+ cleanup().catch(() => {});
61
+ setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
62
+ }
63
+
64
+ async function runPendingMigrations() {
65
+ const { join } = await import("path");
66
+ const { existsSync } = await import("fs");
67
+ const { getAppRoot } = await import("@/lib/utils/app-root");
68
+
69
+ const appRoot = getAppRoot(import.meta.dirname, 1);
70
+ const migrationsDir = join(appRoot, "src", "lib", "db", "migrations");
71
+ if (!existsSync(migrationsDir)) return; // npx distribution — no migration files
72
+
73
+ const { sqlite } = await import("@/lib/db");
74
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
75
+ const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
76
+ const {
77
+ hasLegacyStagentTables,
78
+ hasMigrationHistory,
79
+ markAllMigrationsApplied,
80
+ bootstrapStagentDatabase,
81
+ } = await import("@/lib/db/bootstrap");
82
+
83
+ const needsLegacyRecovery =
84
+ hasLegacyStagentTables(sqlite) && !hasMigrationHistory(sqlite);
85
+
86
+ if (needsLegacyRecovery) {
87
+ bootstrapStagentDatabase(sqlite);
88
+ markAllMigrationsApplied(sqlite, migrationsDir);
89
+ console.log("[db] Recovered legacy database — all migrations stamped.");
90
+ } else {
91
+ const db = drizzle(sqlite);
92
+ migrate(db, { migrationsFolder: migrationsDir });
93
+ }
94
+ }
@@ -1,51 +1,7 @@
1
- export async function register() {
2
- // Only start background services on the server (not during build or edge)
3
- if (process.env.NEXT_RUNTIME === "nodejs") {
4
- try {
5
- // License manager — initialize from DB (creates default row if needed)
6
- const { licenseManager } = await import("@/lib/license/manager");
7
- licenseManager.initialize();
8
- licenseManager.startValidationTimer();
9
-
10
- const { startScheduler } = await import("@/lib/schedules/scheduler");
11
- startScheduler();
12
-
13
- const { startChannelPoller } = await import("@/lib/channels/poller");
14
- startChannelPoller();
15
-
16
- const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
17
- startAutoBackup();
18
-
19
- // History retention cleanup — prunes old agent_logs and usage_ledger
20
- // based on tier retention limit (Community: 30 days)
21
- startHistoryCleanup(licenseManager);
22
-
23
- // Telemetry batch flush (opt-in, every 5 minutes)
24
- const { startTelemetryFlush } = await import("@/lib/telemetry/queue");
25
- startTelemetryFlush();
26
- } catch (err) {
27
- console.error("Instrumentation startup failed:", err);
28
- }
29
- }
30
- }
31
-
32
- async function startHistoryCleanup(licenseManager: { getLimit: (r: "historyRetentionDays") => number }) {
33
- const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
34
-
35
- async function cleanup() {
36
- const retentionDays = licenseManager.getLimit("historyRetentionDays");
37
- if (!Number.isFinite(retentionDays)) return; // Unlimited retention
38
-
39
- const { db } = await import("@/lib/db");
40
- const { agentLogs, usageLedger } = await import("@/lib/db/schema");
41
- const { lt } = await import("drizzle-orm");
42
-
43
- const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
44
- db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
45
- db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
1
+ export function register() {
2
+ if (process.env.NEXT_RUNTIME !== "nodejs") {
3
+ return;
46
4
  }
47
5
 
48
- // Run once at startup, then daily
49
- cleanup().catch(() => {});
50
- setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
6
+ return require("./instrumentation-node").registerNodeInstrumentation();
51
7
  }
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ describe("claude-agent.ts SDK options parity with chat engine", () => {
6
+ const agentSource = fs.readFileSync(
7
+ path.resolve(__dirname, "../claude-agent.ts"),
8
+ "utf8",
9
+ );
10
+
11
+ // Split the source at the resumeClaudeTask function boundary. The first
12
+ // half contains executeClaudeTask's query() block; the second half contains
13
+ // resumeClaudeTask's query() block. This is more robust than a single
14
+ // regex over the whole file — a future edit that adds a stray `canUseTool`
15
+ // reference above either function won't cause a parity test to match the
16
+ // wrong query() call.
17
+ const resumeMarker = "export async function resumeClaudeTask";
18
+ const splitIndex = agentSource.indexOf(resumeMarker);
19
+ if (splitIndex === -1) {
20
+ throw new Error(
21
+ "claude-agent-sdk-options.test.ts: could not find `" + resumeMarker +
22
+ "` in claude-agent.ts — rename or refactor broke this test's assumptions",
23
+ );
24
+ }
25
+ const executeSection = agentSource.slice(0, splitIndex);
26
+ const resumeSection = agentSource.slice(splitIndex);
27
+
28
+ it("imports CLAUDE_SDK_ALLOWED_TOOLS from runtime/claude-sdk", () => {
29
+ expect(agentSource).toMatch(/CLAUDE_SDK_ALLOWED_TOOLS[\s\S]*runtime\/claude-sdk/);
30
+ });
31
+
32
+ it("imports CLAUDE_SDK_SETTING_SOURCES from runtime/claude-sdk", () => {
33
+ expect(agentSource).toMatch(/CLAUDE_SDK_SETTING_SOURCES[\s\S]*runtime\/claude-sdk/);
34
+ });
35
+
36
+ it("imports getFeaturesForModel to gate native-skill options", () => {
37
+ expect(agentSource).toMatch(/getFeaturesForModel/);
38
+ });
39
+
40
+ it("passes settingSources inside executeClaudeTask query() options", () => {
41
+ // The execute section must contain a query( call AND settingSources.
42
+ expect(executeSection).toMatch(/query\(/);
43
+ expect(executeSection).toContain("settingSources");
44
+ });
45
+
46
+ it("passes settingSources inside resumeClaudeTask query() options", () => {
47
+ expect(resumeSection).toMatch(/query\(/);
48
+ expect(resumeSection).toContain("settingSources");
49
+ });
50
+
51
+ it("hooks field is NOT present in either query() options block", () => {
52
+ for (const section of [executeSection, resumeSection]) {
53
+ expect(section).not.toMatch(/\bhooks\s*:/);
54
+ }
55
+ });
56
+ });
@@ -141,10 +141,16 @@ vi.mock("@/lib/agents/browser-mcp", () => ({
141
141
  isExaTool: vi.fn().mockReturnValue(false),
142
142
  isExaReadOnly: vi.fn().mockReturnValue(false),
143
143
  }));
144
+ vi.mock("@/lib/chat/stagent-tools", () => ({
145
+ createToolServer: vi.fn((_projectId?: string | null) => ({
146
+ asMcpServer: () => ({ __mockStagentServer: true }),
147
+ })),
148
+ }));
144
149
 
145
150
  // Static imports (works because vi.mock is hoisted)
146
151
  import { query } from "@anthropic-ai/claude-agent-sdk";
147
152
  import { executeClaudeTask, resumeClaudeTask } from "../claude-agent";
153
+ import { createToolServer } from "@/lib/chat/stagent-tools";
148
154
 
149
155
  const mockQuery = vi.mocked(query);
150
156
 
@@ -238,6 +244,85 @@ describe("executeClaudeTask", () => {
238
244
  expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
239
245
  });
240
246
 
247
+ it("A-stagent-1: injects stagent MCP server into query mcpServers", async () => {
248
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
249
+ mockQuery.mockReturnValue(
250
+ createMockStream([
251
+ { type: "result", result: "done" },
252
+ ]) as unknown as ReturnType<typeof query>
253
+ );
254
+
255
+ await executeClaudeTask("task-1");
256
+
257
+ const queryCall = mockQuery.mock.calls[0][0] as {
258
+ options: { mcpServers?: Record<string, unknown> };
259
+ };
260
+ expect(queryCall.options.mcpServers).toBeDefined();
261
+ expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
262
+ expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
263
+ });
264
+
265
+ it("A-stagent-2: prepends mcp__stagent__* when profile has allowedTools", async () => {
266
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
267
+ mockGetProfile.mockReturnValueOnce({
268
+ id: "restricted",
269
+ name: "Restricted",
270
+ systemPrompt: "",
271
+ allowedTools: ["Read", "Grep"],
272
+ });
273
+ mockQuery.mockReturnValue(
274
+ createMockStream([
275
+ { type: "result", result: "done" },
276
+ ]) as unknown as ReturnType<typeof query>
277
+ );
278
+
279
+ await executeClaudeTask("task-1");
280
+
281
+ const queryCall = mockQuery.mock.calls[0][0] as {
282
+ options: { allowedTools?: string[] };
283
+ };
284
+ expect(queryCall.options.allowedTools).toBeDefined();
285
+ expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
286
+ expect(queryCall.options.allowedTools).toContain("Read");
287
+ expect(queryCall.options.allowedTools).toContain("Grep");
288
+ // Duplicates not added when profile didn't already include the pattern
289
+ const stagentCount = queryCall.options.allowedTools!.filter(
290
+ (t) => t === "mcp__stagent__*"
291
+ ).length;
292
+ expect(stagentCount).toBe(1);
293
+ });
294
+
295
+ it("A-stagent-3: falls back to CLAUDE_SDK_ALLOWED_TOOLS when profile has none and runtime has native skills", async () => {
296
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
297
+ // Default mockGetProfile returns allowedTools: undefined. Task-runtime-skill-parity
298
+ // (Task 3) changed withStagentAllowedTools so the Phase 1a tool set (Skill,
299
+ // Read/Grep/Glob, Edit/Write/Bash, TodoWrite) is passed alongside mcp__stagent__*
300
+ // when the runtime has hasNativeSkills=true — which is the claude-code default.
301
+ mockQuery.mockReturnValue(
302
+ createMockStream([
303
+ { type: "result", result: "done" },
304
+ ]) as unknown as ReturnType<typeof query>
305
+ );
306
+
307
+ await executeClaudeTask("task-1");
308
+
309
+ const queryCall = mockQuery.mock.calls[0][0] as {
310
+ options: { allowedTools?: string[] };
311
+ };
312
+ expect(queryCall.options.allowedTools).toBeDefined();
313
+ expect(queryCall.options.allowedTools).toEqual([
314
+ "mcp__stagent__*",
315
+ "Skill",
316
+ "Read",
317
+ "Grep",
318
+ "Glob",
319
+ "Edit",
320
+ "Write",
321
+ "Bash",
322
+ "TodoWrite",
323
+ ]);
324
+ });
325
+
241
326
  it("A3: captures sessionId from init message and re-calls setExecution", async () => {
242
327
  mockWhere.mockResolvedValueOnce([makeTask()]);
243
328
  mockQuery.mockReturnValue(
@@ -337,6 +422,51 @@ describe("executeClaudeTask", () => {
337
422
  expect(callOptions.maxTurns).toBeDefined();
338
423
  expect(callOptions.maxBudgetUsd).toBeDefined();
339
424
  });
425
+
426
+ it("A8: waits for learned-pattern extraction before final cleanup", async () => {
427
+ let resolveAnalysis: (() => void) | null = null;
428
+ mockWhere.mockResolvedValueOnce([makeTask()]);
429
+ mockQuery.mockReturnValue(
430
+ createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
431
+ );
432
+ mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
433
+ new Promise((resolve) => {
434
+ resolveAnalysis = () => resolve(null);
435
+ })
436
+ );
437
+
438
+ const runPromise = executeClaudeTask("task-1");
439
+ await vi.waitFor(() => {
440
+ expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
441
+ });
442
+
443
+ expect(mockRemoveExecution).not.toHaveBeenCalled();
444
+
445
+ resolveAnalysis?.();
446
+ await runPromise;
447
+
448
+ expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
449
+ });
450
+
451
+ it("A9: logs learned-pattern extraction failures without failing the task", async () => {
452
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
453
+ mockWhere.mockResolvedValueOnce([makeTask()]);
454
+ mockQuery.mockReturnValue(
455
+ createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
456
+ );
457
+ mockAnalyzeForLearnedPatterns.mockRejectedValueOnce(new Error("extract failed"));
458
+
459
+ await executeClaudeTask("task-1");
460
+
461
+ expect(mockSet).toHaveBeenCalledWith(
462
+ expect.objectContaining({ status: "completed", result: "done" })
463
+ );
464
+ expect(errorSpy).toHaveBeenCalledWith(
465
+ "[self-improvement] pattern extraction failed:",
466
+ expect.any(Error)
467
+ );
468
+ errorSpy.mockRestore();
469
+ });
340
470
  });
341
471
 
342
472
  // ═══════════════════════════════════════════════════════════════════════
@@ -527,6 +657,88 @@ describe("resumeClaudeTask", () => {
527
657
  expect.objectContaining({ event: "error" })
528
658
  );
529
659
  });
660
+
661
+ it("C5: waits for learned-pattern extraction before final cleanup on resume", async () => {
662
+ let resolveAnalysis: (() => void) | null = null;
663
+ mockWhere.mockResolvedValueOnce([
664
+ makeTask({ sessionId: "sess-123", resumeCount: 0 }),
665
+ ]);
666
+ mockQuery.mockReturnValue(
667
+ createMockStream([{ type: "result", result: "resumed ok" }]) as unknown as ReturnType<typeof query>
668
+ );
669
+ mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
670
+ new Promise((resolve) => {
671
+ resolveAnalysis = () => resolve(null);
672
+ })
673
+ );
674
+
675
+ const runPromise = resumeClaudeTask("task-1");
676
+ await vi.waitFor(() => {
677
+ expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
678
+ });
679
+
680
+ expect(mockRemoveExecution).not.toHaveBeenCalled();
681
+
682
+ resolveAnalysis?.();
683
+ await runPromise;
684
+
685
+ expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
686
+ });
687
+
688
+ it("R-stagent-1: injects stagent MCP server into query mcpServers on resume", async () => {
689
+ mockWhere.mockResolvedValueOnce([
690
+ makeTask({
691
+ projectId: "proj-7",
692
+ sessionId: "session-abc",
693
+ resumeCount: 1,
694
+ }),
695
+ ]);
696
+ mockQuery.mockReturnValue(
697
+ createMockStream([
698
+ { type: "result", result: "resumed and done" },
699
+ ]) as unknown as ReturnType<typeof query>
700
+ );
701
+
702
+ await resumeClaudeTask("task-1");
703
+
704
+ const queryCall = mockQuery.mock.calls[0][0] as {
705
+ options: { mcpServers?: Record<string, unknown>; resume?: string };
706
+ };
707
+ expect(queryCall.options.resume).toBe("session-abc");
708
+ expect(queryCall.options.mcpServers).toBeDefined();
709
+ expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
710
+ expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
711
+ });
712
+
713
+ it("R-stagent-2: prepends mcp__stagent__* on resume when profile has allowedTools", async () => {
714
+ mockWhere.mockResolvedValueOnce([
715
+ makeTask({
716
+ projectId: "proj-7",
717
+ sessionId: "session-abc",
718
+ resumeCount: 1,
719
+ }),
720
+ ]);
721
+ mockGetProfile.mockReturnValueOnce({
722
+ id: "restricted",
723
+ name: "Restricted",
724
+ systemPrompt: "",
725
+ allowedTools: ["Read", "Grep"],
726
+ });
727
+ mockQuery.mockReturnValue(
728
+ createMockStream([
729
+ { type: "result", result: "resumed and done" },
730
+ ]) as unknown as ReturnType<typeof query>
731
+ );
732
+
733
+ await resumeClaudeTask("task-1");
734
+
735
+ const queryCall = mockQuery.mock.calls[0][0] as {
736
+ options: { allowedTools?: string[] };
737
+ };
738
+ expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
739
+ expect(queryCall.options.allowedTools).toContain("Read");
740
+ expect(queryCall.options.allowedTools![0]).toBe("mcp__stagent__*");
741
+ });
530
742
  });
531
743
 
532
744
  // ═══════════════════════════════════════════════════════════════════════
@@ -1,22 +1,10 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
-
3
- vi.mock("@/lib/license/manager", () => ({
4
- licenseManager: {
5
- getLimit: vi.fn().mockReturnValue(Infinity),
6
- getTier: vi.fn().mockReturnValue("scale"),
7
- },
8
- }));
9
-
10
- vi.mock("@/lib/license/notifications", () => ({
11
- createTierLimitNotification: vi.fn().mockResolvedValue(undefined),
12
- }));
1
+ import { describe, it, expect, beforeEach } from "vitest";
13
2
 
14
3
  import {
15
4
  getExecution,
16
5
  setExecution,
17
6
  removeExecution,
18
7
  getAllExecutions,
19
- ParallelLimitError,
20
8
  } from "@/lib/agents/execution-manager";
21
9
 
22
10
  function makeExecution(taskId: string) {
@@ -73,18 +61,4 @@ describe("execution-manager", () => {
73
61
  it("removing non-existent task does not throw", () => {
74
62
  expect(() => removeExecution("nonexistent")).not.toThrow();
75
63
  });
76
-
77
- it("throws ParallelLimitError when limit is reached", async () => {
78
- const { licenseManager } = await import("@/lib/license/manager");
79
- (licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(2);
80
- (licenseManager.getTier as ReturnType<typeof vi.fn>).mockReturnValue("community");
81
-
82
- setExecution("task-1", makeExecution("task-1"));
83
- setExecution("task-2", makeExecution("task-2"));
84
-
85
- expect(() => setExecution("task-3", makeExecution("task-3"))).toThrow(ParallelLimitError);
86
-
87
- // Restore unlimited for other tests
88
- (licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(Infinity);
89
- });
90
64
  });