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
@@ -15,7 +15,12 @@ import {
15
15
  import { getProfile } from "./profiles/registry";
16
16
  import { resolveProfileRuntimePayload, type ResolvedProfileRuntimePayload } from "./profiles/compatibility";
17
17
  import type { CanUseToolPolicy } from "./profiles/types";
18
- import { buildClaudeSdkEnv } from "./runtime/claude-sdk";
18
+ import {
19
+ buildClaudeSdkEnv,
20
+ CLAUDE_SDK_ALLOWED_TOOLS,
21
+ CLAUDE_SDK_SETTING_SOURCES,
22
+ } from "./runtime/claude-sdk";
23
+ import { getFeaturesForModel } from "@/lib/chat/types";
19
24
  import { getActiveLearnedContext } from "./learned-context";
20
25
  import { getLaunchCwd, getWorkspaceContext } from "@/lib/environment/workspace-context";
21
26
  import { analyzeForLearnedPatterns } from "./pattern-extractor";
@@ -34,6 +39,97 @@ import {
34
39
  handleToolPermission,
35
40
  clearPermissionCache,
36
41
  } from "./tool-permissions";
42
+ import {
43
+ classifyTaskFailureReason,
44
+ toRetryableRuntimeLaunchError,
45
+ type RuntimeLaunchProgress,
46
+ } from "@/lib/agents/runtime/launch-failure";
47
+
48
+ // ─── Stagent MCP injection helpers ──────────────────────────────────────
49
+ //
50
+ // Shared by executeClaudeTask and resumeClaudeTask so the two runtime entry
51
+ // points cannot drift apart. The drift between chat engine injection and
52
+ // claude-code runtime injection is what produced the P0 bug this feature
53
+ // fixes — do not duplicate these patterns inline.
54
+
55
+ /**
56
+ * Merge the in-process stagent MCP server into a profile/browser/external
57
+ * MCP server map. Stagent is spread LAST so no upstream source can shadow
58
+ * the `stagent` key with its own server.
59
+ *
60
+ * `@/lib/chat/stagent-tools` is loaded via dynamic `import()` to avoid a
61
+ * circular-dependency crash: that module transitively pulls in the chat
62
+ * tools registry, which imports the runtime registry (`runtime/catalog`,
63
+ * `runtime/index`), which statically references `claudeRuntimeAdapter` —
64
+ * the very module this file is defined in. A static import here would
65
+ * crash with "Cannot access 'claudeRuntimeAdapter' before initialization"
66
+ * at module-load time. The dynamic import defers the stagent-tools module
67
+ * until `executeClaudeTask` / `resumeClaudeTask` actually run, by which
68
+ * time every module in the graph has finished initializing.
69
+ */
70
+ async function withStagentMcpServer(
71
+ profileServers: Record<string, unknown>,
72
+ browserServers: Record<string, unknown>,
73
+ externalServers: Record<string, unknown>,
74
+ projectId?: string | null,
75
+ ): Promise<Record<string, unknown>> {
76
+ const { createToolServer } = await import("@/lib/chat/stagent-tools");
77
+ const stagentServer = createToolServer(projectId).asMcpServer();
78
+ return {
79
+ ...profileServers,
80
+ ...browserServers,
81
+ ...externalServers,
82
+ stagent: stagentServer,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Prepend `mcp__stagent__*` to a profile's explicit allowedTools so the
88
+ * stagent tool registration survives the SDK preset filter. When the
89
+ * profile has no explicit allowlist and `includeSdkTools` is true, fall
90
+ * back to Phase 1a's CLAUDE_SDK_ALLOWED_TOOLS (Skill, Read/Grep/Glob,
91
+ * Edit/Write/Bash, TodoWrite) so task execution gets the same toolset as
92
+ * chat. Returns `undefined` only when the profile has no allowlist AND
93
+ * the caller does not want SDK tools added — letting the SDK fall
94
+ * through to claude_code preset defaults.
95
+ */
96
+ function withStagentAllowedTools(
97
+ profileAllowedTools: string[] | undefined,
98
+ includeSdkTools: boolean,
99
+ ): string[] | undefined {
100
+ // An empty `allowedTools: []` is treated the same as `undefined` — an
101
+ // empty array is almost never the profile author's intent (they'd get
102
+ // only `mcp__stagent__*` and nothing else). Require at least one tool
103
+ // name for the "profile has explicit list" branch.
104
+ if (profileAllowedTools && profileAllowedTools.length > 0) {
105
+ // Profile has explicit list — respect it. Only prepend stagent.
106
+ return Array.from(new Set(["mcp__stagent__*", ...profileAllowedTools]));
107
+ }
108
+ if (includeSdkTools) {
109
+ // No profile allowlist but runtime has native skills — pass the
110
+ // Phase 1a tool set alongside mcp__stagent__* + browser/external
111
+ // (callers merge their own browser/external patterns into this list).
112
+ return ["mcp__stagent__*", ...CLAUDE_SDK_ALLOWED_TOOLS];
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ /**
118
+ * Write an explicit failure_reason to tasks at terminal-state transitions.
119
+ * Called from handleExecutionError and the execute/resume functions on known
120
+ * error classes. Prefer this over reverse-engineering reasons from text via
121
+ * detectFailureReason in scheduler.ts, which is fragile to SDK message changes.
122
+ */
123
+ export async function writeTerminalFailureReason(
124
+ taskId: string,
125
+ error: unknown,
126
+ ): Promise<void> {
127
+ const reason = classifyTaskFailureReason(error);
128
+ await db
129
+ .update(tasks)
130
+ .set({ failureReason: reason, updatedAt: new Date() })
131
+ .where(eq(tasks.id, taskId));
132
+ }
37
133
 
38
134
  /** Typed representation of messages from the Agent SDK stream */
39
135
  interface AgentStreamMessage {
@@ -104,6 +200,14 @@ export async function finalizeTaskUsage(
104
200
  startedAt: state.startedAt,
105
201
  finishedAt: new Date(),
106
202
  });
203
+
204
+ await db
205
+ .update(tasks)
206
+ .set({
207
+ effectiveModelId: state.modelId ?? null,
208
+ updatedAt: new Date(),
209
+ })
210
+ .where(eq(tasks.id, state.taskId));
107
211
  }
108
212
 
109
213
  /**
@@ -116,7 +220,8 @@ async function processAgentStream(
116
220
  response: AsyncIterable<Record<string, unknown>>,
117
221
  abortController: AbortController,
118
222
  agentProfileId = "general",
119
- usageState: TaskUsageState
223
+ usageState: TaskUsageState,
224
+ launchProgress?: RuntimeLaunchProgress
120
225
  ): Promise<void> {
121
226
  let sessionId: string | null = null;
122
227
  let receivedResult = false;
@@ -179,8 +284,14 @@ async function processAgentStream(
179
284
  // Handle assistant messages (tool use starts)
180
285
  if (message.type === "assistant" && message.message?.content) {
181
286
  turnCount++;
287
+ if (launchProgress) {
288
+ launchProgress.hasTurnStarted = true;
289
+ }
182
290
  for (const block of message.message.content) {
183
291
  if (block.type === "tool_use") {
292
+ if (launchProgress) {
293
+ launchProgress.hasToolUse = true;
294
+ }
184
295
  // Track screenshot tool_use IDs for result interception
185
296
  const toolBlock = block as { type: string; id?: string; name?: string; input?: unknown };
186
297
  if (typeof toolBlock.name === "string" && SCREENSHOT_TOOL_NAMES.has(toolBlock.name) && typeof toolBlock.id === "string") {
@@ -249,6 +360,9 @@ async function processAgentStream(
249
360
  return;
250
361
  }
251
362
  receivedResult = true;
363
+ if (launchProgress) {
364
+ launchProgress.hasResult = true;
365
+ }
252
366
  const resultText =
253
367
  typeof message.result === "string"
254
368
  ? message.result
@@ -314,11 +428,14 @@ async function processAgentStream(
314
428
  ? `Agent exhausted its turn limit (${turnCount} turns used) without producing a final result. The task may need fewer sub-queries or a higher maxTurns setting.`
315
429
  : "Agent stream ended without producing a result";
316
430
 
431
+ const streamFailureReason = turnCount > 0 ? "turn_limit_exceeded" : "sdk_error";
432
+
317
433
  await db
318
434
  .update(tasks)
319
435
  .set({
320
436
  status: "failed",
321
437
  result: errorDetail,
438
+ failureReason: streamFailureReason,
322
439
  updatedAt: new Date(),
323
440
  })
324
441
  .where(eq(tasks.id, taskId));
@@ -417,6 +534,7 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
417
534
  const [task] = await db.select().from(tasks).where(eq(tasks.id, taskId));
418
535
  if (!task) throw new Error(`Task ${taskId} not found`);
419
536
  const usageState = createTaskUsageState(task);
537
+ const launchProgress: RuntimeLaunchProgress = {};
420
538
 
421
539
  const abortController = new AbortController();
422
540
  const agentProfileId = task.agentProfile ?? "general";
@@ -432,13 +550,44 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
432
550
  await prepareTaskOutputDirectory(taskId, { clearExisting: true });
433
551
  const ctx = await buildTaskQueryContext(task, agentProfileId);
434
552
 
435
- // Merge browser + external MCP servers when enabled globally
553
+ // Per-schedule override: if the task carries its own maxTurns (set by
554
+ // fireSchedule from schedules.maxTurns), it takes precedence over the
555
+ // profile default. This is the runtime-enforced budget cap.
556
+ const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
557
+
558
+ // Merge browser + external MCP servers, then inject the in-process
559
+ // stagent server via the shared helper (see withStagentMcpServer above).
560
+ // The helper is async because it dynamically imports @/lib/chat/stagent-tools
561
+ // to break a module-load cycle with the runtime registry.
436
562
  const [browserServers, externalServers] = await Promise.all([
437
563
  getBrowserMcpServers(),
438
564
  getExternalMcpServers(),
439
565
  ]);
440
- const profileMcpServers = ctx.payload?.mcpServers ?? {};
441
- const mergedMcpServers = { ...profileMcpServers, ...browserServers, ...externalServers };
566
+ const mergedMcpServers = await withStagentMcpServer(
567
+ ctx.payload?.mcpServers ?? {},
568
+ browserServers,
569
+ externalServers,
570
+ task.projectId,
571
+ );
572
+ // Capability gate: only pass settingSources + CLAUDE_SDK tools when the
573
+ // runtime is claude-code (or a future runtime with hasNativeSkills).
574
+ // Anthropic-direct and OpenAI-direct task runtimes don't understand
575
+ // these SDK-specific options. Tasks do not carry a model field yet —
576
+ // an empty string falls through to the claude-code default in
577
+ // getFeaturesForModel, so the gate opens by default for the primary
578
+ // claude-code use case. Task 4's resume path follows the same pattern.
579
+ const runtimeFeatures = getFeaturesForModel("");
580
+ const includeSdkNativeTools = runtimeFeatures.hasNativeSkills;
581
+
582
+ // allowedTools merged via shared helper. When the profile has no explicit
583
+ // allowlist AND the runtime has native skills, we fall back to Phase 1a's
584
+ // CLAUDE_SDK_ALLOWED_TOOLS (Skill, Read/Grep/Glob, Edit/Write/Bash,
585
+ // TodoWrite) so task execution matches chat. Computed once so the
586
+ // conditional spread below does not invoke the helper twice.
587
+ const mergedAllowedTools = withStagentAllowedTools(
588
+ ctx.payload?.allowedTools,
589
+ includeSdkNativeTools,
590
+ );
442
591
 
443
592
  const authEnv = await getAuthEnv();
444
593
  const response = query({
@@ -452,11 +601,16 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
452
601
  systemPrompt: ctx.systemInstructions
453
602
  ? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
454
603
  : { type: "preset" as const, preset: "claude_code" as const },
455
- // F9: Bounded turn limit from profile or default
456
- maxTurns: ctx.maxTurns,
604
+ // F9: Bounded turn limit from profile or default; per-schedule override wins
605
+ maxTurns: effectiveMaxTurns,
457
606
  // F4: Per-execution budget cap — use task-specific override if set
458
607
  maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
459
- ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
608
+ ...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
609
+ // Phase 1a parity: load user + project settings (.claude/skills,
610
+ // CLAUDE.md, .claude/rules/*.md) when the runtime supports it.
611
+ ...(includeSdkNativeTools && {
612
+ settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
613
+ }),
460
614
  ...(Object.keys(mergedMcpServers).length > 0 && {
461
615
  mcpServers: mergedMcpServers,
462
616
  }),
@@ -476,14 +630,24 @@ export async function executeClaudeTask(taskId: string): Promise<void> {
476
630
  response as AsyncIterable<Record<string, unknown>>,
477
631
  abortController,
478
632
  agentProfileId,
479
- usageState
633
+ usageState,
634
+ launchProgress
480
635
  );
481
636
 
482
- // Fire-and-forget pattern extraction for self-improvement
483
- analyzeForLearnedPatterns(taskId, agentProfileId).catch((err) => {
637
+ try {
638
+ await analyzeForLearnedPatterns(taskId, agentProfileId);
639
+ } catch (err) {
484
640
  console.error("[self-improvement] pattern extraction failed:", err);
485
- });
641
+ }
486
642
  } catch (error: unknown) {
643
+ const retryableLaunchError = toRetryableRuntimeLaunchError({
644
+ runtimeId: "claude-code",
645
+ error,
646
+ progress: launchProgress,
647
+ });
648
+ if (retryableLaunchError) {
649
+ throw retryableLaunchError;
650
+ }
487
651
  await handleExecutionError(
488
652
  taskId,
489
653
  task.title,
@@ -545,13 +709,37 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
545
709
  await prepareTaskOutputDirectory(taskId);
546
710
  const ctx = await buildTaskQueryContext(task, profileId);
547
711
 
548
- // Merge browser + external MCP servers when enabled globally
712
+ // Per-schedule override: if the task carries its own maxTurns (set by
713
+ // fireSchedule from schedules.maxTurns), it takes precedence over the
714
+ // profile default. This is the runtime-enforced budget cap.
715
+ const effectiveMaxTurns = task.maxTurns ?? ctx.maxTurns;
716
+
717
+ // Merge browser + external MCP servers, then inject the in-process
718
+ // stagent server via the shared helper (see withStagentMcpServer).
719
+ // Async for the same cycle-breaking reason as executeClaudeTask above.
549
720
  const [browserServers, externalServers] = await Promise.all([
550
721
  getBrowserMcpServers(),
551
722
  getExternalMcpServers(),
552
723
  ]);
553
- const profileMcpServers = ctx.payload?.mcpServers ?? {};
554
- const mergedMcpServers = { ...profileMcpServers, ...browserServers, ...externalServers };
724
+ const mergedMcpServers = await withStagentMcpServer(
725
+ ctx.payload?.mcpServers ?? {},
726
+ browserServers,
727
+ externalServers,
728
+ task.projectId,
729
+ );
730
+ // Capability gate: same logic as executeClaudeTask. Resumed tasks must
731
+ // get the same SDK options as their original run so skills that were
732
+ // visible on first execution remain visible after a resume. `task.model`
733
+ // does not exist on the tasks schema — pass "" which resolves to the
734
+ // claude-code default (hasNativeSkills: true) for every current task
735
+ // flow. See features/task-runtime-skill-parity.md Task 4.
736
+ const runtimeFeatures = getFeaturesForModel("");
737
+ const includeSdkNativeTools = runtimeFeatures.hasNativeSkills;
738
+
739
+ const mergedAllowedTools = withStagentAllowedTools(
740
+ ctx.payload?.allowedTools,
741
+ includeSdkNativeTools,
742
+ );
555
743
 
556
744
  const authEnv = await getAuthEnv();
557
745
  const response = query({
@@ -566,11 +754,15 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
566
754
  systemPrompt: ctx.systemInstructions
567
755
  ? { type: "preset" as const, preset: "claude_code" as const, append: ctx.systemInstructions }
568
756
  : { type: "preset" as const, preset: "claude_code" as const },
569
- // F9: Bounded turn limit from profile or default
570
- maxTurns: ctx.maxTurns,
757
+ // F9: Bounded turn limit from profile or default; per-schedule override wins
758
+ maxTurns: effectiveMaxTurns,
571
759
  // F4: Per-execution budget cap — use task-specific override if set
572
760
  maxBudgetUsd: task.maxBudgetUsd ?? DEFAULT_MAX_BUDGET_USD,
573
- ...(ctx.payload?.allowedTools && { allowedTools: ctx.payload.allowedTools }),
761
+ ...(mergedAllowedTools && { allowedTools: mergedAllowedTools }),
762
+ // Phase 1a parity: match executeClaudeTask — see Task 3 rationale.
763
+ ...(includeSdkNativeTools && {
764
+ settingSources: [...CLAUDE_SDK_SETTING_SOURCES],
765
+ }),
574
766
  ...(Object.keys(mergedMcpServers).length > 0 && {
575
767
  mcpServers: mergedMcpServers,
576
768
  }),
@@ -593,10 +785,11 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
593
785
  usageState
594
786
  );
595
787
 
596
- // Fire-and-forget pattern extraction for self-improvement
597
- analyzeForLearnedPatterns(taskId, profileId).catch((err) => {
788
+ try {
789
+ await analyzeForLearnedPatterns(taskId, profileId);
790
+ } catch (err) {
598
791
  console.error("[self-improvement] pattern extraction failed:", err);
599
- });
792
+ }
600
793
  } catch (error: unknown) {
601
794
  const errorMessage =
602
795
  error instanceof Error ? error.message : String(error);
@@ -612,6 +805,7 @@ export async function resumeClaudeTask(taskId: string): Promise<void> {
612
805
  status: "failed",
613
806
  result: "Session expired — re-queue for fresh start",
614
807
  sessionId: null,
808
+ failureReason: "auth_failed",
615
809
  updatedAt: new Date(),
616
810
  })
617
811
  .where(eq(tasks.id, taskId));
@@ -667,11 +861,13 @@ async function handleExecutionError(
667
861
  return;
668
862
  }
669
863
 
864
+ const failureReason = classifyTaskFailureReason(error);
670
865
  await db
671
866
  .update(tasks)
672
867
  .set({
673
868
  status: "failed",
674
869
  result: errorMessage,
870
+ failureReason,
675
871
  updatedAt: new Date(),
676
872
  })
677
873
  .where(eq(tasks.id, taskId));
@@ -1,6 +1,3 @@
1
- import { licenseManager } from "@/lib/license/manager";
2
- import { createTierLimitNotification } from "@/lib/license/notifications";
3
-
4
1
  interface RunningExecution {
5
2
  abortController: AbortController;
6
3
  sessionId: string | null;
@@ -17,42 +14,10 @@ export function getExecution(taskId: string): RunningExecution | undefined {
17
14
  return executions.get(taskId);
18
15
  }
19
16
 
20
- /**
21
- * Register a running execution. Checks the parallel workflow limit
22
- * for the current tier before allowing the execution to proceed.
23
- *
24
- * @throws {ParallelLimitError} if the concurrent execution limit is reached
25
- */
26
17
  export function setExecution(taskId: string, execution: RunningExecution): void {
27
- const limit = licenseManager.getLimit("parallelWorkflows");
28
- const currentCount = executions.size;
29
-
30
- if (Number.isFinite(limit) && currentCount >= limit) {
31
- const tier = licenseManager.getTier();
32
- // Fire-and-forget notification
33
- createTierLimitNotification("parallelWorkflows", currentCount, limit, taskId).catch(() => {});
34
- throw new ParallelLimitError(currentCount, limit, tier);
35
- }
36
-
37
18
  executions.set(taskId, execution);
38
19
  }
39
20
 
40
- export class ParallelLimitError extends Error {
41
- public readonly current: number;
42
- public readonly limit: number;
43
- public readonly tier: string;
44
-
45
- constructor(current: number, limit: number, tier: string) {
46
- super(
47
- `Parallel workflow limit reached (${current}/${limit}) on ${tier} tier. Wait for a running task to complete or upgrade.`
48
- );
49
- this.name = "ParallelLimitError";
50
- this.current = current;
51
- this.limit = limit;
52
- this.tier = tier;
53
- }
54
- }
55
-
56
21
  export function removeExecution(taskId: string): void {
57
22
  executions.delete(taskId);
58
23
  }
@@ -125,8 +125,8 @@ export async function processHandoffs(): Promise<void> {
125
125
 
126
126
  // Fire-and-forget task execution
127
127
  try {
128
- const { executeTaskWithRuntime } = await import("@/lib/agents/runtime");
129
- executeTaskWithRuntime(taskId).catch((err) => {
128
+ const { startTaskExecution } = await import("@/lib/agents/task-dispatch");
129
+ startTaskExecution(taskId).catch((err) => {
130
130
  console.error(`[handoff] task execution failed for message ${msg.id}:`, err);
131
131
  });
132
132
  } catch (err) {
@@ -5,9 +5,6 @@ import type { LearnedContextRow } from "@/lib/db/schema";
5
5
  import { runMetaCompletion } from "./runtime/claude";
6
6
  import { getSettingSync } from "@/lib/settings/helpers";
7
7
  import { SETTINGS_KEYS } from "@/lib/constants/settings";
8
- import { checkLimit } from "@/lib/license/limit-check";
9
- import { getContextVersionCount } from "@/lib/license/limit-queries";
10
- import { createTierLimitNotification } from "@/lib/license/notifications";
11
8
 
12
9
  const DEFAULT_CONTEXT_CHAR_LIMIT = 8_000;
13
10
  const SUMMARIZATION_RATIO = 0.75;
@@ -95,15 +92,6 @@ export async function proposeContextAddition(
95
92
  additions: string,
96
93
  options?: { silent?: boolean }
97
94
  ): Promise<string> {
98
- // Tier limit check — context version cap per profile
99
- const versionCount = getContextVersionCount(profileId);
100
- const limitResult = checkLimit("contextVersions", versionCount);
101
- if (!limitResult.allowed) {
102
- createTierLimitNotification("contextVersions", versionCount, limitResult.limit, taskId).catch(() => {});
103
- throw new Error(
104
- `Context version limit reached (${versionCount}/${limitResult.limit}). Upgrade to unlock more capacity.`
105
- );
106
- }
107
95
 
108
96
  const version = getNextVersion(profileId);
109
97
  const notificationId = options?.silent ? null : crypto.randomUUID();
@@ -181,6 +181,7 @@ export async function batchApproveProposals(
181
181
  await import("./learned-context");
182
182
 
183
183
  let approved = 0;
184
+ const touchedProfileIds = new Set<string>();
184
185
  for (const rowId of proposalRowIds) {
185
186
  const [row] = db
186
187
  .select()
@@ -229,16 +230,28 @@ export async function batchApproveProposals(
229
230
  }
230
231
 
231
232
  approved++;
232
-
233
- const sizeInfo = checkContextSize(row.profileId);
234
- if (sizeInfo.needsSummarization) {
235
- await summarizeContext(row.profileId);
236
- }
233
+ touchedProfileIds.add(row.profileId);
237
234
  }
238
235
 
239
236
  // Mark the batch notification as responded
240
237
  await markBatchNotificationResponded(proposalRowIds, "approved");
241
238
 
239
+ const profilesNeedingSummarization = [...touchedProfileIds].filter(
240
+ (profileId) => checkContextSize(profileId).needsSummarization
241
+ );
242
+ void Promise.allSettled(
243
+ profilesNeedingSummarization.map(async (profileId) => {
244
+ try {
245
+ await summarizeContext(profileId);
246
+ } catch (error) {
247
+ console.error(
248
+ "[learning-session] Failed to summarize approved context batch:",
249
+ error
250
+ );
251
+ }
252
+ })
253
+ );
254
+
242
255
  return approved;
243
256
  }
244
257
 
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { listFusedProfiles } from "@/lib/agents/profiles/list-fused-profiles";
6
+
7
+ describe("listFusedProfiles", () => {
8
+ let projectDir: string;
9
+ let userSkillsDir: string;
10
+
11
+ beforeEach(() => {
12
+ projectDir = mkdtempSync(join(tmpdir(), "stagent-skills-"));
13
+ userSkillsDir = mkdtempSync(join(tmpdir(), "stagent-user-skills-"));
14
+ mkdirSync(join(projectDir, ".claude", "skills"), { recursive: true });
15
+ });
16
+
17
+ afterEach(() => {
18
+ rmSync(projectDir, { recursive: true, force: true });
19
+ rmSync(userSkillsDir, { recursive: true, force: true });
20
+ });
21
+
22
+ function writeSkill(baseDir: string, name: string, frontmatter: string) {
23
+ mkdirSync(join(baseDir, name), { recursive: true });
24
+ writeFileSync(
25
+ join(baseDir, name, "SKILL.md"),
26
+ `---\n${frontmatter}\n---\n\nbody for ${name}\n`
27
+ );
28
+ }
29
+
30
+ it("returns registry profiles when no filesystem skills exist", async () => {
31
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
32
+ // Should contain at least one registry profile (builtin)
33
+ expect(result.length).toBeGreaterThan(0);
34
+ expect(result.every((p) => typeof p.id === "string")).toBe(true);
35
+ });
36
+
37
+ it("surfaces a project .claude/skills/<name> entry", async () => {
38
+ writeSkill(
39
+ join(projectDir, ".claude", "skills"),
40
+ "my-project-skill",
41
+ `name: my-project-skill\ndescription: Test project skill`
42
+ );
43
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
44
+ expect(result.some((p) => p.id === "my-project-skill")).toBe(true);
45
+ const skill = result.find((p) => p.id === "my-project-skill")!;
46
+ expect(skill.name).toBe("my-project-skill");
47
+ expect(skill.description).toBe("Test project skill");
48
+ expect(skill.origin).toBe("filesystem-project");
49
+ });
50
+
51
+ it("sets projectDir to the project root (not the skills subdirectory) on filesystem-project entries", async () => {
52
+ writeSkill(
53
+ join(projectDir, ".claude", "skills"),
54
+ "my-scoped-skill",
55
+ `name: my-scoped-skill\ndescription: Scoped`
56
+ );
57
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
58
+ const skill = result.find((p) => p.id === "my-scoped-skill")!;
59
+ expect(skill.projectDir).toBe(projectDir);
60
+ // Negative: must not be the .claude/skills subdirectory
61
+ expect(skill.projectDir).not.toContain(".claude/skills");
62
+ });
63
+
64
+ it("surfaces a user ~/.claude/skills/<name> entry", async () => {
65
+ writeSkill(
66
+ userSkillsDir,
67
+ "my-user-skill",
68
+ `name: my-user-skill\ndescription: Test user skill`
69
+ );
70
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
71
+ expect(result.some((p) => p.id === "my-user-skill")).toBe(true);
72
+ expect(
73
+ result.find((p) => p.id === "my-user-skill")!.origin
74
+ ).toBe("filesystem-user");
75
+ });
76
+
77
+ it("dedupes by id — registry profile wins over filesystem skill with same id", async () => {
78
+ // "general" is a known builtin registry profile id; write a filesystem
79
+ // skill with the same id to force a collision.
80
+ writeSkill(
81
+ join(projectDir, ".claude", "skills"),
82
+ "general",
83
+ `name: general\ndescription: This should be overridden by registry`
84
+ );
85
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
86
+ const entries = result.filter((p) => p.id === "general");
87
+ expect(entries).toHaveLength(1);
88
+ // Registry description should win (not the filesystem-overridden one)
89
+ expect(entries[0].description).not.toBe("This should be overridden by registry");
90
+ });
91
+
92
+ it("logs and skips a malformed SKILL.md (no name field in frontmatter)", async () => {
93
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
94
+ writeSkill(
95
+ join(projectDir, ".claude", "skills"),
96
+ "broken-skill",
97
+ `description: Missing name field — broken`
98
+ );
99
+ const result = await listFusedProfiles(projectDir, userSkillsDir);
100
+ expect(result.some((p) => p.id === "broken-skill")).toBe(false);
101
+ expect(warnSpy).toHaveBeenCalled();
102
+ warnSpy.mockRestore();
103
+ });
104
+
105
+ it("returns an empty-safe result when projectDir does not exist", async () => {
106
+ const result = await listFusedProfiles("/nonexistent/path", userSkillsDir);
107
+ // Should still return registry + user skills, no throw
108
+ expect(Array.isArray(result)).toBe(true);
109
+ });
110
+ });