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
@@ -49,7 +49,7 @@ describe("profile registry", () => {
49
49
  expect(general!.domain).toBe("work");
50
50
  });
51
51
 
52
- it("returns all 20 builtin profiles", () => {
52
+ it("returns all 21 builtin profiles", () => {
53
53
  const profiles = listProfiles().filter((p) => isBuiltin(p.id));
54
54
  const ids = profiles.map((p) => p.id);
55
55
 
@@ -68,14 +68,16 @@ describe("profile registry", () => {
68
68
  expect(ids).toContain("shopping-assistant");
69
69
  expect(ids).toContain("learning-coach");
70
70
  expect(ids).toContain("sweep");
71
- // 6 new business function profiles
71
+ // 6 business function profiles
72
72
  expect(ids).toContain("marketing-strategist");
73
73
  expect(ids).toContain("sales-researcher");
74
74
  expect(ids).toContain("customer-support-agent");
75
75
  expect(ids).toContain("financial-analyst");
76
76
  expect(ids).toContain("content-creator");
77
77
  expect(ids).toContain("operations-coordinator");
78
- expect(profiles.length).toBe(20);
78
+ // Clone lifecycle profile
79
+ expect(ids).toContain("upgrade-assistant");
80
+ expect(profiles.length).toBe(21);
79
81
  });
80
82
 
81
83
  it("getProfile returns undefined for unknown id", () => {
@@ -96,6 +98,53 @@ describe("profile registry", () => {
96
98
  expect(codeReviewer!.canUseToolPolicy!.autoApprove).toContain("Grep");
97
99
  });
98
100
 
101
+ it("preserves preferredRuntime from profile.yaml", async () => {
102
+ const originalHome = process.env.HOME;
103
+ const tempHome = fs.mkdtempSync(
104
+ path.join(os.tmpdir(), "registry-preferred-runtime-")
105
+ );
106
+
107
+ try {
108
+ process.env.HOME = tempHome;
109
+ vi.resetModules();
110
+
111
+ const profileId = `preferred-runtime-${Date.now()}`;
112
+ const profileDir = path.join(tempHome, ".claude", "skills", profileId);
113
+ fs.mkdirSync(profileDir, { recursive: true });
114
+ fs.writeFileSync(
115
+ path.join(profileDir, "profile.yaml"),
116
+ yaml.dump({
117
+ id: profileId,
118
+ name: "Preferred Runtime Test",
119
+ version: "1.0.0",
120
+ domain: "work",
121
+ tags: ["runtime"],
122
+ preferredRuntime: "openai-direct",
123
+ })
124
+ );
125
+ fs.writeFileSync(
126
+ path.join(profileDir, "SKILL.md"),
127
+ `---
128
+ name: ${profileId}
129
+ description: Preferred runtime test profile.
130
+ ---
131
+
132
+ Testing preferred runtime loading.
133
+ `
134
+ );
135
+
136
+ const registry = await import("../registry");
137
+ const loaded = registry.getProfile(profileId);
138
+
139
+ expect(loaded).toBeDefined();
140
+ expect(loaded?.preferredRuntime).toBe("openai-direct");
141
+ } finally {
142
+ process.env.HOME = originalHome;
143
+ fs.rmSync(tempHome, { recursive: true, force: true });
144
+ vi.resetModules();
145
+ }
146
+ });
147
+
99
148
  it("getProfileTags returns tag map", () => {
100
149
  const tagMap = getProfileTags();
101
150
  expect(tagMap.get("researcher")).toContain("research");
@@ -129,7 +178,7 @@ describe("profile registry", () => {
129
178
  (p) => p.domain === "personal"
130
179
  );
131
180
 
132
- expect(workProfiles.length).toBe(15); // general, code-reviewer, researcher, document-writer, project-manager, data-analyst, technical-writer, devops-engineer, sweep + 6 business profiles
181
+ expect(workProfiles.length).toBe(16); // general, code-reviewer, researcher, document-writer, project-manager, data-analyst, technical-writer, devops-engineer, sweep + 6 business profiles + upgrade-assistant
133
182
  expect(personalProfiles.length).toBe(5); // wealth-manager, travel-planner, health-fitness-coach, shopping-assistant, learning-coach
134
183
  });
135
184
 
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: upgrade-assistant
3
+ description: Guided interactive git merge of upstream stagent commits into the user's local instance branch
4
+ ---
5
+
6
+ You are the Upgrade Assistant for a stagent clone. Your job is to pull upstream commits from `origin/main` into the user's instance branch safely and interactively, surfacing merge conflicts in plain language so the user can decide how to resolve them.
7
+
8
+ ## Context for this upgrade
9
+
10
+ - **Instance branch:** `{{INSTANCE_BRANCH}}`
11
+ - **Upstream commits behind:** `{{COMMITS_BEHIND}}`
12
+ - **Data directory:** `{{DATA_DIR}}`
13
+ - **Working directory:** the current repo root
14
+
15
+ ## How to ask the user a question
16
+
17
+ **Never emit a question as plain text in the log.** The session UI cannot turn text into a reply input. Always invoke the `AskUserQuestion` tool — the user sees a structured prompt in the task view and the tool call returns with their answer so you can continue.
18
+
19
+ Two canonical shapes:
20
+
21
+ - **Free-form answer** (for "should I…" questions that don't have a fixed choice set):
22
+ ```
23
+ AskUserQuestion({ question: "Your main branch has 3 commits not in origin/main. Move them to `local` and reset main, or abort so you can review?" })
24
+ ```
25
+ The user types a reply and the tool returns `{ answer: "..."}`. Act on it.
26
+
27
+ - **Choice-based answer** (for merge-conflict resolution — always three canonical choices):
28
+ ```
29
+ AskUserQuestion({
30
+ question: "Conflict in src/app/page.tsx — which version do you want?",
31
+ options: [
32
+ { label: "Keep my version", description: "Use your changes; discard main's version" },
33
+ { label: "Take main's version", description: "Use main's changes; discard yours" },
34
+ { label: "Show me the diff", description: "Output the full conflict diff for manual review" }
35
+ ]
36
+ })
37
+ ```
38
+ The tool returns `{ answer: "Keep my version" }` (or one of the other labels). Run the matching `git checkout --ours` / `--theirs` / `git diff` command and continue.
39
+
40
+ If the answer is free-form prose, read it literally. Do not second-guess the user.
41
+
42
+ ## Crucial rules — read these before doing anything
43
+
44
+ 1. **Never modify `main` except by fast-forward.** After fetching, merge `origin/main` into local `main` with `--ff-only`. If that fast-forward fails, the user has local commits on `main` that aren't in `origin/main` — **invoke `AskUserQuestion`** asking whether to move them to `{{INSTANCE_BRANCH}}` or abort so they can review. Do not auto-resolve.
45
+
46
+ 2. **Never push any branch.** The pre-push hook blocks `{{INSTANCE_BRANCH}}` pushes, but you should not even attempt one. Your job ends at a local commit.
47
+
48
+ 3. **If any step fails, roll back.** On any error after the merge has begun, run `git merge --abort` and `git stash pop` (if you stashed earlier) before reporting the failure. Leave the working tree in the state the user started in.
49
+
50
+ 4. **Treat `local` identically to any named instance branch.** Users with a default single-clone setup have `{{INSTANCE_BRANCH}}=local`. Users running private domain clones have names like `wealth-mgr` or `investor-mgr`. The merge flow is identical in both cases.
51
+
52
+ 5. **Stop and ask on merge conflicts — always via `AskUserQuestion`.** Do not guess and do not emit the question as plain text. For each conflicted file, invoke `AskUserQuestion` with the three-choice payload shown above in "How to ask the user a question". Map the returned `answer` to the git command:
53
+ - **"Keep my version"** → `git checkout --ours <file>`
54
+ - **"Take main's version"** → `git checkout --theirs <file>`
55
+ - **"Show me the diff"** → `git diff <file>` and output the full conflict; then re-invoke `AskUserQuestion` so the user can pick one of the first two options after reviewing.
56
+ After all conflicts are resolved, `git add` the files and continue the merge.
57
+
58
+ ## Standard merge flow
59
+
60
+ Execute these steps in order. Report progress as you go so the live log view shows the user what's happening.
61
+
62
+ 1. **Pre-flight check.** Run `git status` to confirm the working tree state. If there's uncommitted work, tell the user you'll stash it first.
63
+
64
+ 2. **Stash any work-in-progress.** If the working tree is dirty, run `git stash push -m "upgrade-session auto-stash"`. Record that you stashed — you need to pop it at the end.
65
+
66
+ 3. **Fetch origin.** Run `git fetch origin main`. This is the only network operation. If it fails, report the error and stop.
67
+
68
+ 4. **Fast-forward main.** Run `git checkout main` then `git merge --ff-only origin/main`. If `--ff-only` fails, stop and ask the user (see Rule 1).
69
+
70
+ 5. **Return to the instance branch.** Run `git checkout {{INSTANCE_BRANCH}}`.
71
+
72
+ 6. **Merge main into the instance branch.** Run `git merge main`. If there are conflicts, git will report them. For each conflicted file, use the three-choice flow (Rule 5).
73
+
74
+ 7. **Complete the merge commit.** After conflicts are resolved (or if there were none), run `git commit` to finalize the merge if git hasn't already done so automatically.
75
+
76
+ 8. **Reinstall dependencies if package-lock.json changed.** Check `git diff HEAD~1 HEAD -- package-lock.json`. If there are changes, run `npm install`.
77
+
78
+ 9. **Pop the stash.** If you stashed in step 2, run `git stash pop`. Then immediately run `git status` to check for conflicts. If any file shows "both modified" or "Unmerged", the stash pop conflicted — resolve each file using the same three-choice flow (Rule 5), then `git add` the resolved files. After all stash conflicts are resolved, run `git stash drop` to remove the stash entry (a conflicted pop does NOT auto-drop the stash). Finally, run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no conflict markers remain in the working tree.
79
+
80
+ 10. **Report completion.** Tell the user the merge is complete, how many new commits landed, whether dependencies were reinstalled, and that they should restart the dev server to apply changes.
81
+
82
+ ## Aborting
83
+
84
+ If the user clicks "Abort" during the session, or if any step fails irrecoverably, run:
85
+ - `git merge --abort` (safe to run even if no merge is in progress — it'll just exit 0)
86
+ - `git checkout {{INSTANCE_BRANCH}}` (return the user to where they started)
87
+ - If you stashed in step 2: run `git stash pop`. Then run `git status` — if there are conflicts from the pop, resolve them using the three-choice flow (Rule 5), `git add` the files, and `git stash drop`. Run `grep -rn "<<<<<<< \|>>>>>>>" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.json" .` to verify no markers remain
88
+
89
+ Then report the abort and what state the repo is in.
90
+
91
+ ## Guidelines
92
+
93
+ - Be concise but explanatory. The user is watching a live log — they need enough context to understand what's happening, not a novel.
94
+ - Always name the file path when asking about a conflict.
95
+ - If the user sends a natural-language question mid-merge ("what do these changes do?"), answer it based on the diff before proceeding.
96
+ - Never use `git push`. Never use `git rebase`. Never use `git reset --hard`. Never delete branches. Never touch the remote.
97
+ - If `git status` shows files you didn't touch and the user didn't mention, bring them up before assuming they're safe to stash.
@@ -0,0 +1,36 @@
1
+ id: upgrade-assistant
2
+ name: Upgrade Assistant
3
+ version: "1.0.0"
4
+ domain: work
5
+ tags: [upgrade, git, merge, maintenance, instance]
6
+ supportedRuntimes: [claude-code, openai-codex-app-server]
7
+ preferredRuntime: claude-code
8
+
9
+ maxTurns: 40
10
+
11
+ author: stagent
12
+
13
+ # Bash tool allowlist — tight scope.
14
+ # The upgrade assistant should only ever run git and npm install, nothing else.
15
+ # Cross-reference: TDR-028 "self-upgrade via task pipeline, not chat tools"
16
+ # explains why this profile is the only safe surface for git shell access.
17
+ allowedTools:
18
+ - Bash(git fetch *)
19
+ - Bash(git status)
20
+ - Bash(git status *)
21
+ - Bash(git stash *)
22
+ - Bash(git checkout *)
23
+ - Bash(git merge *)
24
+ - Bash(git merge --abort)
25
+ - Bash(git commit *)
26
+ - Bash(git diff *)
27
+ - Bash(git rev-parse *)
28
+ - Bash(git log *)
29
+ - Bash(git branch *)
30
+ - Bash(npm install)
31
+ - Read
32
+ - Write
33
+ # Used for every decision point (merge conflicts, drifted main, etc.) —
34
+ # the agent must NEVER emit a question as plain text; always invoke this
35
+ # tool so the user can answer through the session UI.
36
+ - AskUserQuestion
@@ -0,0 +1,104 @@
1
+ import { readdirSync, readFileSync, statSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import { listProfiles } from "./registry";
5
+ import type { AgentProfile } from "./types";
6
+
7
+ /**
8
+ * Minimal YAML frontmatter parser — handles the `---\nkey: value\n---\n...`
9
+ * pattern used by SKILL.md files. Returns null if no frontmatter or no `name`.
10
+ */
11
+ function parseFrontmatter(content: string): Record<string, string> | null {
12
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
13
+ if (!match) return null;
14
+ const result: Record<string, string> = {};
15
+ for (const line of match[1].split("\n")) {
16
+ const colonIdx = line.indexOf(":");
17
+ if (colonIdx === -1) continue;
18
+ const key = line.slice(0, colonIdx).trim();
19
+ const value = line.slice(colonIdx + 1).trim();
20
+ if (key) result[key] = value;
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function loadFilesystemSkills(
26
+ skillsDir: string,
27
+ origin: "filesystem-project" | "filesystem-user",
28
+ projectRootDir: string | undefined
29
+ ): AgentProfile[] {
30
+ if (!existsSync(skillsDir)) return [];
31
+ const profiles: AgentProfile[] = [];
32
+ for (const entry of readdirSync(skillsDir)) {
33
+ const skillPath = join(skillsDir, entry);
34
+ try {
35
+ if (!statSync(skillPath).isDirectory()) continue;
36
+ const skillMdPath = join(skillPath, "SKILL.md");
37
+ if (!existsSync(skillMdPath)) continue;
38
+ const content = readFileSync(skillMdPath, "utf8");
39
+ const fm = parseFrontmatter(content);
40
+ if (!fm || !fm.name) {
41
+ console.warn(
42
+ `[listFusedProfiles] skipping ${skillMdPath}: missing name in frontmatter`
43
+ );
44
+ continue;
45
+ }
46
+ profiles.push({
47
+ id: fm.name,
48
+ name: fm.name,
49
+ description: fm.description ?? "",
50
+ domain: "skill",
51
+ tags: [],
52
+ systemPrompt: content,
53
+ skillMd: content,
54
+ allowedTools: [],
55
+ mcpServers: {},
56
+ supportedRuntimes: ["claude-code"],
57
+ origin,
58
+ scope: origin === "filesystem-project" ? "project" : "user",
59
+ readOnly: true,
60
+ projectDir: origin === "filesystem-project" ? projectRootDir : undefined,
61
+ } as AgentProfile);
62
+ } catch (err) {
63
+ console.warn(
64
+ `[listFusedProfiles] failed to load skill at ${skillPath}:`,
65
+ (err as Error).message
66
+ );
67
+ }
68
+ }
69
+ return profiles;
70
+ }
71
+
72
+ /**
73
+ * Lists every agent profile reachable from this Stagent instance, merging
74
+ * registry profiles with filesystem skills ("fused" view):
75
+ * 1. Registry profiles (builtins + user registry)
76
+ * 2. User filesystem skills at `~/.claude/skills/*\/SKILL.md` (or `userSkillsDir` override)
77
+ * 3. Project filesystem skills at `<projectDir>/.claude/skills/*\/SKILL.md`
78
+ * Dedupes by id — registry profiles win on collision (they're curated), then
79
+ * user skills win over project skills.
80
+ *
81
+ * @param projectDir Absolute path to the active project's working directory (project root)
82
+ * @param userSkillsDir Override for user skills dir (tests); defaults to `~/.claude/skills`
83
+ */
84
+ export async function listFusedProfiles(
85
+ projectDir: string | null | undefined,
86
+ userSkillsDir: string = join(homedir(), ".claude", "skills")
87
+ ): Promise<AgentProfile[]> {
88
+ const registry = listProfiles();
89
+ const registryIds = new Set(registry.map((p) => p.id));
90
+
91
+ const userSkills = loadFilesystemSkills(userSkillsDir, "filesystem-user", undefined).filter(
92
+ (p) => !registryIds.has(p.id)
93
+ );
94
+
95
+ const projectSkills = projectDir
96
+ ? loadFilesystemSkills(
97
+ join(projectDir, ".claude", "skills"),
98
+ "filesystem-project",
99
+ projectDir
100
+ ).filter((p) => !registryIds.has(p.id) && !userSkills.some((u) => u.id === p.id))
101
+ : [];
102
+
103
+ return [...registry, ...userSkills, ...projectSkills];
104
+ }
@@ -118,6 +118,22 @@ function ensureBuiltins(): void {
118
118
  changed = true;
119
119
  }
120
120
 
121
+ if (
122
+ source.preferredRuntime !== undefined &&
123
+ target.preferredRuntime !== source.preferredRuntime
124
+ ) {
125
+ target.preferredRuntime = source.preferredRuntime;
126
+ changed = true;
127
+ }
128
+
129
+ if (
130
+ source.capabilityOverrides !== undefined &&
131
+ target.capabilityOverrides === undefined
132
+ ) {
133
+ target.capabilityOverrides = source.capabilityOverrides;
134
+ changed = true;
135
+ }
136
+
121
137
  if (changed) {
122
138
  fs.writeFileSync(targetYaml, yaml.dump(target));
123
139
  }
@@ -206,7 +222,9 @@ function scanProfiles(): Map<string, AgentProfile> {
206
222
  tests: config.tests,
207
223
  importMeta: config.importMeta,
208
224
  supportedRuntimes: getSupportedRuntimes(config),
225
+ preferredRuntime: config.preferredRuntime,
209
226
  runtimeOverrides: config.runtimeOverrides,
227
+ capabilityOverrides: config.capabilityOverrides,
210
228
  origin,
211
229
  });
212
230
  } catch (err) {
@@ -31,7 +31,13 @@ export interface ProfileRuntimeCapabilityOverride {
31
31
  export type ProfileScope = "builtin" | "user" | "project";
32
32
 
33
33
  /** How a profile entered the system — distinct from scope (where it lives). */
34
- export type ProfileOrigin = "manual" | "environment" | "import" | "ai-assist";
34
+ export type ProfileOrigin =
35
+ | "manual"
36
+ | "environment"
37
+ | "import"
38
+ | "ai-assist"
39
+ | "filesystem-project"
40
+ | "filesystem-user";
35
41
 
36
42
  export interface AgentProfile {
37
43
  id: string;
@@ -1,15 +1,12 @@
1
1
  import { listProfiles, getProfile } from "./profiles/registry";
2
2
  import { profileSupportsRuntime } from "./profiles/compatibility";
3
- import {
4
- executeTaskWithRuntime,
5
- resumeTaskWithRuntime,
6
- } from "./runtime";
7
3
  import {
8
4
  DEFAULT_AGENT_RUNTIME,
9
5
  SUPPORTED_AGENT_RUNTIMES,
10
6
  type AgentRuntimeId,
11
7
  } from "./runtime/catalog";
12
8
  import type { RoutingPreference } from "@/lib/constants/settings";
9
+ import { resumeTaskExecution, startTaskExecution } from "./task-dispatch";
13
10
 
14
11
  // ── Keyword signal maps for runtime scoring ──────────────────────────
15
12
 
@@ -217,12 +214,12 @@ export async function executeTaskWithAgent(
217
214
  taskId: string,
218
215
  agentType: string | null | undefined = DEFAULT_AGENT_RUNTIME
219
216
  ): Promise<void> {
220
- return executeTaskWithRuntime(taskId, agentType);
217
+ return startTaskExecution(taskId, { requestedRuntimeId: agentType });
221
218
  }
222
219
 
223
220
  export async function resumeTaskWithAgent(
224
221
  taskId: string,
225
222
  agentType: string | null | undefined = DEFAULT_AGENT_RUNTIME
226
223
  ): Promise<void> {
227
- return resumeTaskWithRuntime(taskId, agentType);
224
+ return resumeTaskExecution(taskId, { requestedRuntimeId: agentType });
228
225
  }
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_AGENT_RUNTIME,
4
4
  getRuntimeCapabilities,
5
5
  getRuntimeCatalogEntry,
6
+ getRuntimeFeatures,
6
7
  listRuntimeCatalog,
7
8
  resolveAgentRuntime,
8
9
  } from "@/lib/agents/runtime/catalog";
@@ -46,4 +47,133 @@ describe("runtime catalog", () => {
46
47
  expect(result).toBe("claude-code");
47
48
  warnSpy.mockRestore();
48
49
  });
50
+
51
+ it("exposes LLM-surface features via getRuntimeFeatures", () => {
52
+ const features = getRuntimeFeatures("claude-code");
53
+ expect(features.hasNativeSkills).toBe(true);
54
+ expect(features.hasProgressiveDisclosure).toBe(true);
55
+ expect(features.autoLoadsInstructions).toBe("CLAUDE.md");
56
+ expect(features.stagentInjectsSkills).toBe(false);
57
+ });
58
+
59
+ it("marks Ollama as requiring Stagent-injected skills", () => {
60
+ const features = getRuntimeFeatures("ollama");
61
+ expect(features.hasNativeSkills).toBe(false);
62
+ expect(features.stagentInjectsSkills).toBe(true);
63
+ expect(features.autoLoadsInstructions).toBeNull();
64
+ });
65
+
66
+ it("declares Codex auto-loads AGENTS.md", () => {
67
+ expect(getRuntimeFeatures("openai-codex-app-server").autoLoadsInstructions).toBe("AGENTS.md");
68
+ });
69
+
70
+ it("every runtime declares every feature key (exhaustiveness guard)", () => {
71
+ const runtimes = listRuntimeCatalog();
72
+ const expectedKeys: Array<keyof ReturnType<typeof getRuntimeFeatures>> = [
73
+ "hasNativeSkills",
74
+ "hasProgressiveDisclosure",
75
+ "hasFilesystemTools",
76
+ "hasBash",
77
+ "hasTodoWrite",
78
+ "hasSubagentDelegation",
79
+ "hasHooks",
80
+ "autoLoadsInstructions",
81
+ "stagentInjectsSkills",
82
+ "supportsSkillComposition",
83
+ "maxActiveSkills",
84
+ ];
85
+
86
+ // Guard against the "list grows stale" failure mode: if a new key is added
87
+ // to RuntimeFeatures but not to expectedKeys above, this catches it.
88
+ expect(expectedKeys.length).toBe(Object.keys(getRuntimeFeatures()).length);
89
+
90
+ for (const runtime of runtimes) {
91
+ for (const key of expectedKeys) {
92
+ expect(
93
+ runtime.features,
94
+ `${runtime.id} missing feature "${key}"`
95
+ ).toHaveProperty(key);
96
+ }
97
+ }
98
+ });
99
+
100
+ it("feature matrix snapshot matches declared values", () => {
101
+ // Guard against silent regressions: the declared feature matrix must match
102
+ // this snapshot exactly. Update intentionally when flipping a capability flag
103
+ // (and reference the spec change in the commit message).
104
+ const snapshot = listRuntimeCatalog().reduce<Record<string, unknown>>((acc, r) => {
105
+ acc[r.id] = r.features;
106
+ return acc;
107
+ }, {});
108
+
109
+ expect(snapshot).toMatchInlineSnapshot(`
110
+ {
111
+ "anthropic-direct": {
112
+ "autoLoadsInstructions": null,
113
+ "hasBash": false,
114
+ "hasFilesystemTools": false,
115
+ "hasHooks": false,
116
+ "hasNativeSkills": false,
117
+ "hasProgressiveDisclosure": false,
118
+ "hasSubagentDelegation": false,
119
+ "hasTodoWrite": false,
120
+ "maxActiveSkills": 3,
121
+ "stagentInjectsSkills": false,
122
+ "supportsSkillComposition": true,
123
+ },
124
+ "claude-code": {
125
+ "autoLoadsInstructions": "CLAUDE.md",
126
+ "hasBash": true,
127
+ "hasFilesystemTools": true,
128
+ "hasHooks": false,
129
+ "hasNativeSkills": true,
130
+ "hasProgressiveDisclosure": true,
131
+ "hasSubagentDelegation": false,
132
+ "hasTodoWrite": true,
133
+ "maxActiveSkills": 3,
134
+ "stagentInjectsSkills": false,
135
+ "supportsSkillComposition": true,
136
+ },
137
+ "ollama": {
138
+ "autoLoadsInstructions": null,
139
+ "hasBash": false,
140
+ "hasFilesystemTools": false,
141
+ "hasHooks": false,
142
+ "hasNativeSkills": false,
143
+ "hasProgressiveDisclosure": false,
144
+ "hasSubagentDelegation": false,
145
+ "hasTodoWrite": false,
146
+ "maxActiveSkills": 1,
147
+ "stagentInjectsSkills": true,
148
+ "supportsSkillComposition": false,
149
+ },
150
+ "openai-codex-app-server": {
151
+ "autoLoadsInstructions": "AGENTS.md",
152
+ "hasBash": true,
153
+ "hasFilesystemTools": true,
154
+ "hasHooks": false,
155
+ "hasNativeSkills": true,
156
+ "hasProgressiveDisclosure": true,
157
+ "hasSubagentDelegation": false,
158
+ "hasTodoWrite": true,
159
+ "maxActiveSkills": 3,
160
+ "stagentInjectsSkills": false,
161
+ "supportsSkillComposition": true,
162
+ },
163
+ "openai-direct": {
164
+ "autoLoadsInstructions": null,
165
+ "hasBash": false,
166
+ "hasFilesystemTools": false,
167
+ "hasHooks": false,
168
+ "hasNativeSkills": false,
169
+ "hasProgressiveDisclosure": false,
170
+ "hasSubagentDelegation": false,
171
+ "hasTodoWrite": false,
172
+ "maxActiveSkills": 3,
173
+ "stagentInjectsSkills": false,
174
+ "supportsSkillComposition": true,
175
+ },
176
+ }
177
+ `);
178
+ });
49
179
  });