stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,213 @@
1
+ # Spec C — Swarm Visibility
2
+
3
+ **Status:** Approved
4
+ **Created:** 2026-04-08
5
+ **Scope mode:** REDUCE
6
+ **Related:** [Schedule Orchestration (Spec A)](./2026-04-08-schedule-orchestration-design.md), [Chat SSE Resilience Hotfix (Spec B)](./2026-04-08-chat-sse-resilience-hotfix-design.md)
7
+
8
+ ## Context
9
+
10
+ Spec A introduces a global concurrency cap on scheduled agents. Power users running many schedules will observe queueing delays that they previously did not. Without a visible signal for "how busy is the swarm right now," they'll experience unexplained schedule lateness and file tickets.
11
+
12
+ This spec adds minimal, always-visible swarm-load signal to the app chrome, a saturation-only pre-chat banner, and small enhancements to the schedule list. It is deliberately small — REDUCE mode — to avoid overbuilding visibility infrastructure before we know what users actually need.
13
+
14
+ ## Goals
15
+
16
+ 1. Give users a passive, always-visible signal of swarm load state (quiet / working / saturated).
17
+ 2. Warn users *before* they send a chat message if the swarm is at capacity and their chat will queue behind running agents.
18
+ 3. Make the new concurrency-driven queueing visible on the schedule list.
19
+ 4. Rename "turns" to "agent steps" everywhere user-facing, to close the semantic gap between prompt-level "MAX N turns" hints and runtime-counted turns.
20
+
21
+ ## Non-goals (NOT in scope)
22
+
23
+ - **Activity feed route** (`/swarm/activity`) with event log, filters, pagination — future spec "Swarm Activity Feed"
24
+ - **`swarm_snapshots` time-series table** — future spec "Swarm Activity Feed"
25
+ - **Proactive push notifications for overload** — the indicator is always visible, no push needed
26
+ - **Bulk "pause all schedules" action** — users can pause individual schedules from existing pages
27
+ - **Efficiency scoring rings** / turn drift detection alerts — future spec "Schedule Observability"
28
+ - **New `busyness` StatusChip family** — rejected by design review; use custom primitive
29
+ - **Pre-chat banner in `working` state** — only render in `saturated` state to avoid anxiety copy
30
+ - **Traffic-light turn-budget badge** (`lastTurnCount / maxTurns` with color gradient) — leaks policy as warning on normal operation
31
+ - **Popover / Sheet for running schedules list** — hover tooltip + deep link to existing route is sufficient
32
+
33
+ ## Design
34
+
35
+ ### C.1 `GET /api/swarm-status` endpoint
36
+
37
+ New route at `src/app/api/swarm-status/route.ts`. Reads:
38
+
39
+ - `getAllExecutions()` from `src/lib/agents/execution-manager.ts:60` — filters to `sourceType === 'scheduled'` for running count and schedule metadata
40
+ - A count query on `tasks` table for `status='queued' AND source_type='scheduled'` — queued count
41
+ - `activeChatStreams.size` from Spec A's `src/lib/chat/active-streams.ts`
42
+
43
+ **Response shape:**
44
+
45
+ ```json
46
+ {
47
+ "runningScheduled": [
48
+ {
49
+ "scheduleId": "abc",
50
+ "name": "Portfolio Coach",
51
+ "startedAt": "2026-04-08T21:00:03Z",
52
+ "elapsedSec": 42,
53
+ "maxTurns": 500,
54
+ "currentTurns": 127
55
+ }
56
+ ],
57
+ "queuedScheduled": [
58
+ { "scheduleId": "def", "name": "News Sentinel", "queuedAt": "2026-04-08T21:00:14Z", "position": 1 }
59
+ ],
60
+ "chatStreamsActive": 0,
61
+ "loadState": "working"
62
+ }
63
+ ```
64
+
65
+ `loadState` is computed server-side:
66
+ - `quiet` — `runningScheduled.length === 0`
67
+ - `working` — `runningScheduled.length >= 1 && queuedScheduled.length === 0`
68
+ - `saturated` — `queuedScheduled.length > 0` (at-or-above cap)
69
+
70
+ No new DB state — the endpoint reads from in-memory execution map + one SQL count.
71
+
72
+ ### C.2 `<SwarmLoadIndicator />` component
73
+
74
+ **Placement:** top of `<SidebarContent>` in `src/components/shared/app-sidebar.tsx`, above the first NavGroup. Not the footer — per design review, the footer is already dense (UpgradeBadge, WorkspaceIndicator, AuthStatusDot, TrustTierBadge, theme toggle) and the sidebar-as-chrome pattern means aggregate system state belongs at the top, where nav groups live.
75
+
76
+ **Visual:** custom primitive (NOT a StatusChip family — semantic mismatch; StatusChip is "one entity, one state", swarm load is "aggregate cardinality"). Reuses badge tokens and the pulse animation pattern but renders as a thin one-line row.
77
+
78
+ **Three states** (not four — red is reserved for actual failures, not backpressure):
79
+
80
+ | State | Condition | Token | Label |
81
+ |---|---|---|---|
82
+ | Quiet | `loadState === 'quiet'` | `text-muted-foreground` | `Swarm quiet` |
83
+ | Working | `loadState === 'working'` | `text-status-running` (indigo, pulse) | `● 2 running` |
84
+ | Saturated | `loadState === 'saturated'` | `text-status-warning` (amber, pulse) | `● 3 running · 1 queued` |
85
+
86
+ **Hover tooltip:** lists up to 3 running schedules inline with elapsed time. Delivers ~80% of the "activity feed" value with zero new overlay state:
87
+
88
+ ```
89
+ Swarm · 2 running
90
+ ─────────────────
91
+ • portfolio-coach 2m
92
+ • launch-copy-chief 41s
93
+ ```
94
+
95
+ If there are more than 3 running, append `• +N more`.
96
+
97
+ **Click behavior:** the indicator is a `<Link>` to `/schedules?status=running` — deep-link to the existing schedules page with a filter. No popover, no sheet, no new overlay pattern.
98
+
99
+ **Polling:** every 8s via new `usePolling(url, intervalMs)` hook (C.5). Shared state is used by both the indicator and the pre-chat banner so there is no double-fetch.
100
+
101
+ **Accessibility:** `aria-live="polite"`, tooltip keyboard-focusable, text contrast meets Calm Ops baseline.
102
+
103
+ ### C.3 `<ChatOverloadBanner />` component
104
+
105
+ **Placement:** above `<ChatInput />` in `src/components/chat/chat-shell.tsx`.
106
+
107
+ **Render condition:** ONLY when `loadState === 'saturated'` (queue depth > 0). Not `working`. Anxiety copy on normal operation violates Calm Ops tone — "responses may be slower" with zero agency tells users a bad thing might happen and gives them no action.
108
+
109
+ **Visual:** surface-2 bordered banner, `rounded-lg`, amber accent matching the indicator's saturated state.
110
+
111
+ **Copy:** `"Swarm at capacity — your chat will queue behind {N} running agents."` where N is `runningScheduled.length`. One action: `[View Activity]` links to `/schedules?status=running`.
112
+
113
+ **Dismissal:** per conversation, stored in `sessionStorage` keyed by conversation ID. Re-appears if load state flips back to `saturated` after being `working`.
114
+
115
+ ### C.4 Schedule list row enhancements
116
+
117
+ Modify `src/components/schedules/schedule-list.tsx` (or equivalent):
118
+
119
+ 1. **Queue-depth badge (PR2a):** if a schedule has queued firings waiting for a slot, render `+{N} queued` as an `outline` Badge next to the schedule name. Almost free — reuses existing badge component. Addresses the power-user scenario where 10 schedules fire in a 5-min window and #4-10 queue silently.
120
+
121
+ 2. **"Near turn cap" outline badge:** rendered ONLY when `lastTurnCount / maxTurns >= 0.9`. No traffic-light gradient. Progressive disclosure — normal operation shows nothing. At ≥90%, shows a subtle outline badge: `Near step cap`.
122
+
123
+ ### C.5 `usePolling(url, intervalMs)` shared hook
124
+
125
+ New file `src/hooks/use-polling.ts`. Extracted from the pattern at `src/components/notifications/inbox-list.tsx:40-43`. Signature:
126
+
127
+ ```typescript
128
+ export function usePolling<T>(url: string, intervalMs: number): {
129
+ data: T | null;
130
+ error: Error | null;
131
+ loading: boolean;
132
+ };
133
+ ```
134
+
135
+ Fetches on mount, re-fetches every `intervalMs`. Handles unmount cleanup. Stable query key (URL) so multiple consumers of the same URL share state via module-level cache.
136
+
137
+ Used by: `<SwarmLoadIndicator />`, `<ChatOverloadBanner />`. Can be adopted by other components (inbox list, schedule detail sheet) in future cleanups.
138
+
139
+ ### C.6 UI rename: "turns" → "agent steps"
140
+
141
+ User-facing strings only. Keep `maxTurns` as the code/API identifier.
142
+
143
+ - `schedule-form.tsx` field label: "Max turns per firing" → "Max agent steps per run"
144
+ - Tooltip on field: "One step = one agent action (message, tool call, or sub-response). Most schedules use 50–500 steps; heavy research runs 2,000+."
145
+ - Tooltip on prompt field: "Note: writing 'MAX N turns' in your prompt is a hint to the model, not a runtime limit. Use Max agent steps below to enforce a budget."
146
+ - Inline calibration hint after prompt entry: "Schedules like this average ~{N} steps" (derived from `avgTurnsPerFiring` on similar schedules).
147
+ - Schedule list "Near step cap" badge (C.4)
148
+ - Notifications: "Schedule X used 812 / 800 agent steps" (formerly "turns")
149
+
150
+ ## Calm Ops compliance checklist
151
+
152
+ - [x] No backdrop-filter, rgba, glass, gradient
153
+ - [x] Running state uses `status-running` (indigo), NOT green (green is `status-completed`)
154
+ - [x] Saturated state uses `status-warning` (amber), NOT red (red is `status-failed`)
155
+ - [x] No new StatusChip family added (use custom `SwarmLoadIndicator`)
156
+ - [x] No popover/sheet overlay — tooltip + deep link only
157
+ - [x] Banner only renders when actionable (saturated state), not `working`
158
+ - [x] All radii ≤ `rounded-xl`
159
+ - [x] Polling pattern reuses existing template (`inbox-list.tsx`)
160
+ - [x] Any Sheet usage (N/A here) would need `px-6 pb-6` body padding
161
+
162
+ ## Tests
163
+
164
+ ### Unit / component
165
+ 1. `<SwarmLoadIndicator />` renders correct state (Quiet / Working / Saturated) for each input
166
+ 2. Tooltip shows running schedules; click navigates to `/schedules?status=running`
167
+ 3. `<ChatOverloadBanner />` renders ONLY in `saturated` state
168
+ 4. Dismissal persists in sessionStorage across re-mounts
169
+ 5. Queue-depth badge renders when schedule has queued firings
170
+ 6. "Near step cap" badge renders only at ≥90% ratio
171
+ 7. `usePolling` hook fetches on mount, re-fetches on interval, cleans up on unmount
172
+ 8. Multiple consumers of same URL share state (no duplicate fetches)
173
+
174
+ ### API
175
+ 9. `GET /api/swarm-status` returns correct shape with running/queued/chat counts
176
+ 10. `loadState` computed correctly at boundary conditions (0 running, cap-1 running, cap running, queue>0)
177
+
178
+ ### Accessibility
179
+ 11. Indicator has `aria-live="polite"`
180
+ 12. Tooltip is keyboard-focusable
181
+ 13. Contrast meets Calm Ops baseline (manual check)
182
+
183
+ ### Visual regression
184
+ 14. Screenshot sidebar in all 3 states; compare to Calm Ops tokens
185
+
186
+ ## Files touched
187
+
188
+ ### New
189
+ - `src/app/api/swarm-status/route.ts`
190
+ - `src/hooks/use-polling.ts`
191
+ - `src/components/shared/swarm-load-indicator.tsx`
192
+ - `src/components/chat/chat-overload-banner.tsx`
193
+
194
+ ### Modify
195
+ - `src/components/shared/app-sidebar.tsx` — mount `<SwarmLoadIndicator />` at top of SidebarContent
196
+ - `src/components/chat/chat-shell.tsx` — mount `<ChatOverloadBanner />` above ChatInput
197
+ - `src/components/schedules/schedule-list.tsx` (or equivalent) — queue-depth badge + near-cap badge
198
+ - `src/components/schedules/schedule-form.tsx` — rename + tooltips + calibration hint
199
+
200
+ ### Not modified (avoiding pollution)
201
+ - `src/lib/constants/status-families.ts` — NO new `busyness` family per design review
202
+
203
+ ## Dependencies on Spec A
204
+
205
+ - `<SwarmLoadIndicator />` reads `chatStreamsActive` from the in-memory `activeChatStreams` set created by Spec A (`src/lib/chat/active-streams.ts`). C can scaffold mid-A after A's interface is pinned.
206
+ - Queue-depth badge reads `tasks.status='queued' AND source_type='scheduled'` which exists today but is populated meaningfully only after Spec A's concurrency limiter lands.
207
+ - "Near step cap" badge reads `schedules.max_turns` column added by Spec A.
208
+
209
+ ## Ship plan
210
+
211
+ - No feature flag — UI is additive and safe.
212
+ - Scaffolding (API endpoint, hook, indicator component) can begin mid-A.
213
+ - Full ship after Spec A stabilizes and A's data-model migrations have landed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.9.5",
3
+ "version": "0.10.0",
4
4
  "description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
5
5
  "keywords": [
6
6
  "ai",
@@ -67,7 +67,6 @@
67
67
  "@dnd-kit/sortable": "^10.0.0",
68
68
  "@dnd-kit/utilities": "^3.2.2",
69
69
  "@hookform/resolvers": "^5.2.2",
70
- "@supabase/supabase-js": "^2.101.1",
71
70
  "@tailwindcss/postcss": "^4",
72
71
  "@tailwindcss/typography": "^0.5",
73
72
  "@tanstack/react-table": "^8.21.3",
@@ -97,6 +96,7 @@
97
96
  "react-markdown": "^10.1.0",
98
97
  "recharts": "^3.8.1",
99
98
  "remark-gfm": "^4.0.1",
99
+ "semver": "^7.7.4",
100
100
  "sharp": "^0.34.5",
101
101
  "smol-toml": "^1.6.1",
102
102
  "sonner": "^2.0.7",
@@ -116,6 +116,7 @@
116
116
  "@types/js-yaml": "^4.0.9",
117
117
  "@types/react": "^19",
118
118
  "@types/react-dom": "^19",
119
+ "@types/semver": "^7.7.1",
119
120
  "@types/sharp": "^0.31.1",
120
121
  "@vitejs/plugin-react": "^5.1.4",
121
122
  "@vitest/coverage-v8": "^4.0.18",
@@ -0,0 +1,15 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ describe("instrumentation register()", () => {
4
+ afterEach(() => {
5
+ vi.unstubAllEnvs();
6
+ });
7
+
8
+ it("ensureInstance is importable from the bootstrap module and returns a skipped result in dev mode", async () => {
9
+ vi.stubEnv("STAGENT_DEV_MODE", "true");
10
+ const { ensureInstance } = await import("@/lib/instance/bootstrap");
11
+ const result = await ensureInstance();
12
+ expect(result.skipped).toBe("dev_mode_env");
13
+ expect(result.steps).toEqual([]);
14
+ });
15
+ });
@@ -1,8 +1,6 @@
1
1
  import { Suspense } from "react";
2
2
  import { PageShell } from "@/components/shared/page-shell";
3
3
  import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard";
4
- import { AnalyticsGateCard } from "@/components/analytics/analytics-gate-card";
5
- import { licenseManager } from "@/lib/license/manager";
6
4
  import {
7
5
  getOutcomeCounts,
8
6
  getSuccessRateTrend,
@@ -14,16 +12,13 @@ import {
14
12
  export const dynamic = "force-dynamic";
15
13
 
16
14
  function AnalyticsContent() {
17
- const tier = licenseManager.getTierFromDb();
18
- const isAllowed = tier !== "community";
19
-
20
15
  const outcomes = getOutcomeCounts(30);
21
16
  const successTrend = getSuccessRateTrend(30);
22
17
  const costTrend = getCostPerOutcomeTrend(30);
23
18
  const leaderboard = getProfileLeaderboard(30);
24
19
  const hoursSaved = getEstimatedHoursSaved(30);
25
20
 
26
- const dashboard = (
21
+ return (
27
22
  <AnalyticsDashboard
28
23
  outcomes={outcomes}
29
24
  successTrend={successTrend}
@@ -32,21 +27,6 @@ function AnalyticsContent() {
32
27
  hoursSaved={hoursSaved}
33
28
  />
34
29
  );
35
-
36
- if (isAllowed) return dashboard;
37
-
38
- return (
39
- <div className="relative">
40
- {/* Blurred dashboard preview */}
41
- <div className="opacity-20 pointer-events-none select-none blur-[2px]" aria-hidden>
42
- {dashboard}
43
- </div>
44
- {/* Upgrade CTA */}
45
- <div className="absolute inset-0 flex items-start justify-center pt-16">
46
- <AnalyticsGateCard />
47
- </div>
48
- </div>
49
- );
50
30
  }
51
31
 
52
32
  export default function AnalyticsPage() {
@@ -1,6 +1,7 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { getConversation, getMessages } from "@/lib/data/chat";
3
3
  import { sendMessage } from "@/lib/chat/engine";
4
+ import { recordTermination } from "@/lib/chat/stream-telemetry";
4
5
 
5
6
  /**
6
7
  * GET /api/chat/conversations/[id]/messages?after=xxx&limit=100
@@ -58,6 +59,7 @@ export async function POST(
58
59
 
59
60
  // Bridge the async generator to an SSE ReadableStream
60
61
  const encoder = new TextEncoder();
62
+ const streamStartedAt = Date.now();
61
63
  const stream = new ReadableStream({
62
64
  async start(controller) {
63
65
  const keepalive = setInterval(() => {
@@ -94,9 +96,28 @@ export async function POST(
94
96
  );
95
97
  } finally {
96
98
  clearInterval(keepalive);
97
- controller.close();
99
+ try {
100
+ controller.close();
101
+ } catch {
102
+ // Stream may already be closed by peer; safe to ignore
103
+ }
98
104
  }
99
105
  },
106
+ // Fires when the client disconnects mid-stream (browser tab closed,
107
+ // user navigated away, AbortController.abort() fired on the fetch).
108
+ // The engine's own `req.signal` abort already records
109
+ // `stream.aborted.signal` in its catch path — this cancel callback
110
+ // only fires when the ReadableStream is torn down independently,
111
+ // so record it as a distinct `stream.aborted.client` code.
112
+ cancel(reason) {
113
+ recordTermination({
114
+ reason: "stream.aborted.client",
115
+ conversationId: id,
116
+ messageId: null,
117
+ durationMs: Date.now() - streamStartedAt,
118
+ error: reason ? String(reason).slice(0, 200) : undefined,
119
+ });
120
+ },
100
121
  });
101
122
 
102
123
  return new Response(stream, {
@@ -0,0 +1,65 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ readTerminations,
4
+ countTerminations,
5
+ } from "@/lib/chat/stream-telemetry";
6
+
7
+ /**
8
+ * GET /api/diagnostics/chat-streams
9
+ *
10
+ * Dev-only diagnostics endpoint that reports how chat SSE streams have
11
+ * terminated in the current server process. Returns counts by reason code
12
+ * plus the most recent N events.
13
+ *
14
+ * Query params:
15
+ * ?windowMinutes=N — restrict counts to the last N minutes (default: all)
16
+ * ?limit=N — cap on recent events returned (default: 50, max: 500)
17
+ *
18
+ * Response shape:
19
+ * {
20
+ * windowMinutes: number | null,
21
+ * totalEvents: number,
22
+ * counts: Record<TerminationReason, number>,
23
+ * recent: TerminationEvent[]
24
+ * }
25
+ *
26
+ * Gated behind NODE_ENV !== production to match the data/clear and
27
+ * data/seed routes. This is for maintainer inspection, not end users.
28
+ *
29
+ * See src/lib/chat/stream-telemetry.ts for the ring buffer + reason code
30
+ * definitions. See features/chat-stream-resilience-telemetry.md for the
31
+ * motivation (verify-before-building telemetry for a mid-stream refresh
32
+ * bug reported by a sibling repo that doesn't reproduce here).
33
+ */
34
+ export async function GET(req: NextRequest) {
35
+ if (process.env.NODE_ENV === "production") {
36
+ return NextResponse.json(
37
+ { error: "Diagnostics disabled in production" },
38
+ { status: 403 }
39
+ );
40
+ }
41
+
42
+ const { searchParams } = req.nextUrl;
43
+ const windowMinutesRaw = searchParams.get("windowMinutes");
44
+ const limitRaw = searchParams.get("limit");
45
+
46
+ const windowMinutes =
47
+ windowMinutesRaw !== null ? Math.max(0, parseInt(windowMinutesRaw, 10) || 0) : null;
48
+ const windowMs = windowMinutes !== null ? windowMinutes * 60 * 1000 : 0;
49
+
50
+ const limit = Math.min(
51
+ 500,
52
+ limitRaw !== null ? Math.max(1, parseInt(limitRaw, 10) || 50) : 50
53
+ );
54
+
55
+ const all = readTerminations();
56
+ const recent = all.slice(-limit).reverse(); // newest first
57
+ const counts = countTerminations(windowMs);
58
+
59
+ return NextResponse.json({
60
+ windowMinutes,
61
+ totalEvents: all.length,
62
+ counts,
63
+ recent,
64
+ });
65
+ }
@@ -0,0 +1,41 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ getInstanceConfig,
4
+ getGuardrails,
5
+ getUpgradeState,
6
+ } from "@/lib/instance/settings";
7
+ import { isDevMode } from "@/lib/instance/detect";
8
+
9
+ /**
10
+ * GET /api/instance/config
11
+ *
12
+ * Returns the full instance state: config, guardrails, and upgrade state
13
+ * in a single response. Used by the Settings → Instance section and by
14
+ * the upgrade pre-flight modal.
15
+ *
16
+ * When running on the canonical dev repo (STAGENT_DEV_MODE=true or the
17
+ * .git/stagent-dev-mode sentinel), returns `{ devMode: true }` with null
18
+ * payloads. This prevents stale instance rows written during prior testing
19
+ * from surfacing in the UI as if the dev repo were a real instance.
20
+ */
21
+ export async function GET() {
22
+ try {
23
+ if (isDevMode()) {
24
+ return NextResponse.json({
25
+ devMode: true,
26
+ config: null,
27
+ guardrails: null,
28
+ upgrade: null,
29
+ });
30
+ }
31
+ return NextResponse.json({
32
+ devMode: false,
33
+ config: getInstanceConfig(),
34
+ guardrails: getGuardrails(),
35
+ upgrade: getUpgradeState(),
36
+ });
37
+ } catch (err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ return NextResponse.json({ error: message }, { status: 500 });
40
+ }
41
+ }
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from "next/server";
2
+ import { ensureInstance } from "@/lib/instance/bootstrap";
3
+ import {
4
+ getInstanceConfig,
5
+ getGuardrails,
6
+ getUpgradeState,
7
+ } from "@/lib/instance/settings";
8
+
9
+ /**
10
+ * POST /api/instance/init
11
+ *
12
+ * Idempotent manual re-run of the instance bootstrap. Useful when the
13
+ * initial boot-time run failed (permission error, git not installed),
14
+ * or when the user wants to re-apply guardrails after changing consent
15
+ * via the Settings → Instance UI.
16
+ *
17
+ * Returns the current instance config + guardrails + upgrade state after
18
+ * the re-run so the Settings → Instance section can refresh its display
19
+ * without a second request.
20
+ */
21
+ export async function POST() {
22
+ try {
23
+ const result = await ensureInstance();
24
+ return NextResponse.json({
25
+ ensureResult: result,
26
+ config: getInstanceConfig(),
27
+ guardrails: getGuardrails(),
28
+ upgrade: getUpgradeState(),
29
+ });
30
+ } catch (err) {
31
+ const message = err instanceof Error ? err.message : String(err);
32
+ return NextResponse.json({ error: message }, { status: 500 });
33
+ }
34
+ }
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from "next/server";
2
+ import { tick } from "@/lib/instance/upgrade-poller";
3
+
4
+ /**
5
+ * POST /api/instance/upgrade/check
6
+ *
7
+ * Force-run the upgrade availability poller. Rate-limited to one call per
8
+ * ~5 minutes via the same lock file the scheduled poller uses. Returns the
9
+ * new UpgradeState on success, or a skipped reason if the lock was held or
10
+ * dev-mode was active.
11
+ */
12
+ export async function POST() {
13
+ try {
14
+ const result = await tick();
15
+ if (result.updated) {
16
+ return NextResponse.json({ ok: true, state: result.updated });
17
+ }
18
+ return NextResponse.json(
19
+ { ok: false, skipped: result.skipped, error: result.error },
20
+ { status: 202 }
21
+ );
22
+ } catch (err) {
23
+ const message = err instanceof Error ? err.message : String(err);
24
+ return NextResponse.json({ error: message }, { status: 500 });
25
+ }
26
+ }
@@ -0,0 +1,96 @@
1
+ import { NextResponse } from "next/server";
2
+ import { randomUUID } from "crypto";
3
+ import { db } from "@/lib/db";
4
+ import { tasks } from "@/lib/db/schema";
5
+ import {
6
+ getInstanceConfig,
7
+ getUpgradeState,
8
+ setUpgradeState,
9
+ } from "@/lib/instance/settings";
10
+
11
+ /**
12
+ * POST /api/instance/upgrade
13
+ *
14
+ * Spawns an upgrade task with the `upgrade-assistant` agent profile. Returns
15
+ * 202 Accepted with the task id; the client then navigates to the upgrade
16
+ * session view to watch streaming progress and respond to conflict prompts.
17
+ *
18
+ * The task description includes the instance context (branch name, commits
19
+ * behind, data directory) as template variables that the profile's SKILL.md
20
+ * references. The claude-agent runtime interpolates them when building the
21
+ * system prompt.
22
+ *
23
+ * Fire-and-forget per TDR-001: the route returns immediately; task execution
24
+ * runs in the background through the existing execution-manager pipeline.
25
+ */
26
+ export async function POST() {
27
+ try {
28
+ const config = getInstanceConfig();
29
+ if (!config) {
30
+ return NextResponse.json(
31
+ { error: "Instance not yet initialized — run POST /api/instance/init first" },
32
+ { status: 409 }
33
+ );
34
+ }
35
+
36
+ const upgrade = getUpgradeState();
37
+ if (!upgrade.upgradeAvailable) {
38
+ return NextResponse.json(
39
+ { error: "No upgrade available", upgradeState: upgrade },
40
+ { status: 409 }
41
+ );
42
+ }
43
+
44
+ const branchName = config.branchName;
45
+ const commitsBehind = upgrade.commitsBehind;
46
+ const dataDir = process.env.STAGENT_DATA_DIR ?? "~/.stagent";
47
+
48
+ const description = [
49
+ `Upgrade instance branch \`${branchName}\` with ${commitsBehind} upstream commit(s) from origin/main.`,
50
+ "",
51
+ "Context for the upgrade-assistant profile:",
52
+ `- INSTANCE_BRANCH=${branchName}`,
53
+ `- COMMITS_BEHIND=${commitsBehind}`,
54
+ `- DATA_DIR=${dataDir}`,
55
+ "",
56
+ "Follow the standard merge flow defined in SKILL.md. Stop and ask the user on any merge conflict. Abort and roll back on any failure. Do not push any branch.",
57
+ ].join("\n");
58
+
59
+ const id = randomUUID();
60
+ const now = new Date();
61
+
62
+ db.insert(tasks)
63
+ .values({
64
+ id,
65
+ title: `Upgrade ${branchName} — ${commitsBehind} upstream commit${commitsBehind === 1 ? "" : "s"}`,
66
+ description,
67
+ projectId: null,
68
+ priority: 1,
69
+ assignedAgent: null,
70
+ agentProfile: "upgrade-assistant",
71
+ sourceType: "manual",
72
+ status: "planned",
73
+ createdAt: now,
74
+ updatedAt: now,
75
+ })
76
+ .run();
77
+
78
+ // Record which task id owns this upgrade so the UI can deep-link to it,
79
+ // and optimistically clear the pending-count so the sidebar badge and
80
+ // settings card reflect the user's intent immediately. If the merge task
81
+ // fails or is cancelled, the next scheduled poll (or a manual "Check for
82
+ // upgrades") will restore the real count by re-running git rev-list.
83
+ await setUpgradeState({
84
+ ...upgrade,
85
+ lastUpgradeTaskId: id,
86
+ commitsBehind: 0,
87
+ upgradeAvailable: false,
88
+ lastSuccessfulUpgradeAt: Math.floor(Date.now() / 1000),
89
+ });
90
+
91
+ return NextResponse.json({ taskId: id }, { status: 202 });
92
+ } catch (err) {
93
+ const message = err instanceof Error ? err.message : String(err);
94
+ return NextResponse.json({ error: message }, { status: 500 });
95
+ }
96
+ }
@@ -0,0 +1,35 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getUpgradeState } from "@/lib/instance/settings";
3
+ import { isDevMode } from "@/lib/instance/detect";
4
+
5
+ /**
6
+ * GET /api/instance/upgrade/status
7
+ *
8
+ * Returns the current UpgradeState for client components that need to poll
9
+ * (e.g. the upgrade modal pre-flight). Server Components should read directly
10
+ * from settings per TDR-004 rather than calling this route.
11
+ *
12
+ * When running on the canonical dev repo, returns a synthetic state with
13
+ * `devMode: true` and `upgradeAvailable: false` so the sidebar upgrade
14
+ * button never renders on main.
15
+ */
16
+ export async function GET() {
17
+ try {
18
+ if (isDevMode()) {
19
+ return NextResponse.json({
20
+ devMode: true,
21
+ lastPolledAt: null,
22
+ upgradeAvailable: false,
23
+ commitsBehind: 0,
24
+ lastSuccessfulUpgradeAt: null,
25
+ pollFailureCount: 0,
26
+ lastPollError: null,
27
+ });
28
+ }
29
+ const state = getUpgradeState();
30
+ return NextResponse.json({ devMode: false, ...state });
31
+ } catch (err) {
32
+ const message = err instanceof Error ? err.message : String(err);
33
+ return NextResponse.json({ error: message }, { status: 500 });
34
+ }
35
+ }