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,94 @@
1
+ export async function registerNodeInstrumentation() {
2
+ try {
3
+ // Instance bootstrap — creates local branch, handles dev-mode gates, consent flow.
4
+ // Runs BEFORE other startup so instance config is available downstream.
5
+ // Safe in the canonical stagent dev repo thanks to STAGENT_DEV_MODE=true
6
+ // in .env.local plus the .git/stagent-dev-mode sentinel file.
7
+ const { ensureInstance } = await import("@/lib/instance/bootstrap");
8
+ const instanceResult = await ensureInstance();
9
+ if (instanceResult.skipped) {
10
+ console.log(`[instance] bootstrap skipped: ${instanceResult.skipped}`);
11
+ } else {
12
+ for (const step of instanceResult.steps) {
13
+ if (step.status === "failed") {
14
+ console.error(`[instance] ${step.step} failed: ${step.reason}`);
15
+ }
16
+ }
17
+ }
18
+
19
+ // Run pending Drizzle migrations (DROP TABLE, CREATE INDEX, etc.)
20
+ // that can't be handled by bootstrap's IF NOT EXISTS pattern.
21
+ // Runs here (not in db/index.ts) to avoid SQLITE_BUSY during next build.
22
+ await runPendingMigrations();
23
+
24
+ // Instance upgrade poller — hourly `git fetch` to detect upstream commits.
25
+ // Skipped in dev mode; lightweight; uses advisory lock to prevent overlap.
26
+ const { startUpgradePoller } = await import("@/lib/instance/upgrade-poller");
27
+ startUpgradePoller();
28
+
29
+ const { startScheduler } = await import("@/lib/schedules/scheduler");
30
+ startScheduler();
31
+
32
+ const { startChannelPoller } = await import("@/lib/channels/poller");
33
+ startChannelPoller();
34
+
35
+ const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
36
+ startAutoBackup();
37
+
38
+ // History retention cleanup — prunes old agent_logs and usage_ledger
39
+ startHistoryCleanup();
40
+
41
+ } catch (err) {
42
+ console.error("Instrumentation startup failed:", err);
43
+ }
44
+ }
45
+
46
+ async function startHistoryCleanup() {
47
+ const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000;
48
+ const RETENTION_DAYS = 365;
49
+
50
+ async function cleanup() {
51
+ const { db } = await import("@/lib/db");
52
+ const { agentLogs, usageLedger } = await import("@/lib/db/schema");
53
+ const { lt } = await import("drizzle-orm");
54
+
55
+ const cutoff = new Date(Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000);
56
+ db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
57
+ db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
58
+ }
59
+
60
+ cleanup().catch(() => {});
61
+ setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
62
+ }
63
+
64
+ async function runPendingMigrations() {
65
+ const { join } = await import("path");
66
+ const { existsSync } = await import("fs");
67
+ const { getAppRoot } = await import("@/lib/utils/app-root");
68
+
69
+ const appRoot = getAppRoot(import.meta.dirname, 1);
70
+ const migrationsDir = join(appRoot, "src", "lib", "db", "migrations");
71
+ if (!existsSync(migrationsDir)) return; // npx distribution — no migration files
72
+
73
+ const { sqlite } = await import("@/lib/db");
74
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
75
+ const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
76
+ const {
77
+ hasLegacyStagentTables,
78
+ hasMigrationHistory,
79
+ markAllMigrationsApplied,
80
+ bootstrapStagentDatabase,
81
+ } = await import("@/lib/db/bootstrap");
82
+
83
+ const needsLegacyRecovery =
84
+ hasLegacyStagentTables(sqlite) && !hasMigrationHistory(sqlite);
85
+
86
+ if (needsLegacyRecovery) {
87
+ bootstrapStagentDatabase(sqlite);
88
+ markAllMigrationsApplied(sqlite, migrationsDir);
89
+ console.log("[db] Recovered legacy database — all migrations stamped.");
90
+ } else {
91
+ const db = drizzle(sqlite);
92
+ migrate(db, { migrationsFolder: migrationsDir });
93
+ }
94
+ }
@@ -1,51 +1,7 @@
1
- export async function register() {
2
- // Only start background services on the server (not during build or edge)
3
- if (process.env.NEXT_RUNTIME === "nodejs") {
4
- try {
5
- // License manager — initialize from DB (creates default row if needed)
6
- const { licenseManager } = await import("@/lib/license/manager");
7
- licenseManager.initialize();
8
- licenseManager.startValidationTimer();
9
-
10
- const { startScheduler } = await import("@/lib/schedules/scheduler");
11
- startScheduler();
12
-
13
- const { startChannelPoller } = await import("@/lib/channels/poller");
14
- startChannelPoller();
15
-
16
- const { startAutoBackup } = await import("@/lib/snapshots/auto-backup");
17
- startAutoBackup();
18
-
19
- // History retention cleanup — prunes old agent_logs and usage_ledger
20
- // based on tier retention limit (Community: 30 days)
21
- startHistoryCleanup(licenseManager);
22
-
23
- // Telemetry batch flush (opt-in, every 5 minutes)
24
- const { startTelemetryFlush } = await import("@/lib/telemetry/queue");
25
- startTelemetryFlush();
26
- } catch (err) {
27
- console.error("Instrumentation startup failed:", err);
28
- }
29
- }
30
- }
31
-
32
- async function startHistoryCleanup(licenseManager: { getLimit: (r: "historyRetentionDays") => number }) {
33
- const CLEANUP_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours
34
-
35
- async function cleanup() {
36
- const retentionDays = licenseManager.getLimit("historyRetentionDays");
37
- if (!Number.isFinite(retentionDays)) return; // Unlimited retention
38
-
39
- const { db } = await import("@/lib/db");
40
- const { agentLogs, usageLedger } = await import("@/lib/db/schema");
41
- const { lt } = await import("drizzle-orm");
42
-
43
- const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000);
44
- db.delete(agentLogs).where(lt(agentLogs.timestamp, cutoff)).run();
45
- db.delete(usageLedger).where(lt(usageLedger.startedAt, cutoff)).run();
1
+ export function register() {
2
+ if (process.env.NEXT_RUNTIME !== "nodejs") {
3
+ return;
46
4
  }
47
5
 
48
- // Run once at startup, then daily
49
- cleanup().catch(() => {});
50
- setInterval(() => cleanup().catch(() => {}), CLEANUP_INTERVAL);
6
+ return require("./instrumentation-node").registerNodeInstrumentation();
51
7
  }
@@ -141,10 +141,16 @@ vi.mock("@/lib/agents/browser-mcp", () => ({
141
141
  isExaTool: vi.fn().mockReturnValue(false),
142
142
  isExaReadOnly: vi.fn().mockReturnValue(false),
143
143
  }));
144
+ vi.mock("@/lib/chat/stagent-tools", () => ({
145
+ createToolServer: vi.fn((_projectId?: string | null) => ({
146
+ asMcpServer: () => ({ __mockStagentServer: true }),
147
+ })),
148
+ }));
144
149
 
145
150
  // Static imports (works because vi.mock is hoisted)
146
151
  import { query } from "@anthropic-ai/claude-agent-sdk";
147
152
  import { executeClaudeTask, resumeClaudeTask } from "../claude-agent";
153
+ import { createToolServer } from "@/lib/chat/stagent-tools";
148
154
 
149
155
  const mockQuery = vi.mocked(query);
150
156
 
@@ -238,6 +244,72 @@ describe("executeClaudeTask", () => {
238
244
  expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
239
245
  });
240
246
 
247
+ it("A-stagent-1: injects stagent MCP server into query mcpServers", async () => {
248
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
249
+ mockQuery.mockReturnValue(
250
+ createMockStream([
251
+ { type: "result", result: "done" },
252
+ ]) as unknown as ReturnType<typeof query>
253
+ );
254
+
255
+ await executeClaudeTask("task-1");
256
+
257
+ const queryCall = mockQuery.mock.calls[0][0] as {
258
+ options: { mcpServers?: Record<string, unknown> };
259
+ };
260
+ expect(queryCall.options.mcpServers).toBeDefined();
261
+ expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
262
+ expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
263
+ });
264
+
265
+ it("A-stagent-2: prepends mcp__stagent__* when profile has allowedTools", async () => {
266
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
267
+ mockGetProfile.mockReturnValueOnce({
268
+ id: "restricted",
269
+ name: "Restricted",
270
+ systemPrompt: "",
271
+ allowedTools: ["Read", "Grep"],
272
+ });
273
+ mockQuery.mockReturnValue(
274
+ createMockStream([
275
+ { type: "result", result: "done" },
276
+ ]) as unknown as ReturnType<typeof query>
277
+ );
278
+
279
+ await executeClaudeTask("task-1");
280
+
281
+ const queryCall = mockQuery.mock.calls[0][0] as {
282
+ options: { allowedTools?: string[] };
283
+ };
284
+ expect(queryCall.options.allowedTools).toBeDefined();
285
+ expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
286
+ expect(queryCall.options.allowedTools).toContain("Read");
287
+ expect(queryCall.options.allowedTools).toContain("Grep");
288
+ // Duplicates not added when profile didn't already include the pattern
289
+ const stagentCount = queryCall.options.allowedTools!.filter(
290
+ (t) => t === "mcp__stagent__*"
291
+ ).length;
292
+ expect(stagentCount).toBe(1);
293
+ });
294
+
295
+ it("A-stagent-3: omits allowedTools when profile has none (preset defaults preserved)", async () => {
296
+ mockWhere.mockResolvedValueOnce([makeTask({ projectId: "proj-7" })]);
297
+ // Default mockGetProfile returns allowedTools: undefined, so ctx.payload.allowedTools
298
+ // will also be undefined — the query() call should NOT include an allowedTools option.
299
+ mockQuery.mockReturnValue(
300
+ createMockStream([
301
+ { type: "result", result: "done" },
302
+ ]) as unknown as ReturnType<typeof query>
303
+ );
304
+
305
+ await executeClaudeTask("task-1");
306
+
307
+ const queryCall = mockQuery.mock.calls[0][0] as {
308
+ options: { allowedTools?: string[] };
309
+ };
310
+ expect(queryCall.options.allowedTools).toBeUndefined();
311
+ });
312
+
241
313
  it("A3: captures sessionId from init message and re-calls setExecution", async () => {
242
314
  mockWhere.mockResolvedValueOnce([makeTask()]);
243
315
  mockQuery.mockReturnValue(
@@ -337,6 +409,51 @@ describe("executeClaudeTask", () => {
337
409
  expect(callOptions.maxTurns).toBeDefined();
338
410
  expect(callOptions.maxBudgetUsd).toBeDefined();
339
411
  });
412
+
413
+ it("A8: waits for learned-pattern extraction before final cleanup", async () => {
414
+ let resolveAnalysis: (() => void) | null = null;
415
+ mockWhere.mockResolvedValueOnce([makeTask()]);
416
+ mockQuery.mockReturnValue(
417
+ createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
418
+ );
419
+ mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
420
+ new Promise((resolve) => {
421
+ resolveAnalysis = () => resolve(null);
422
+ })
423
+ );
424
+
425
+ const runPromise = executeClaudeTask("task-1");
426
+ await vi.waitFor(() => {
427
+ expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
428
+ });
429
+
430
+ expect(mockRemoveExecution).not.toHaveBeenCalled();
431
+
432
+ resolveAnalysis?.();
433
+ await runPromise;
434
+
435
+ expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
436
+ });
437
+
438
+ it("A9: logs learned-pattern extraction failures without failing the task", async () => {
439
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
440
+ mockWhere.mockResolvedValueOnce([makeTask()]);
441
+ mockQuery.mockReturnValue(
442
+ createMockStream([{ type: "result", result: "done" }]) as unknown as ReturnType<typeof query>
443
+ );
444
+ mockAnalyzeForLearnedPatterns.mockRejectedValueOnce(new Error("extract failed"));
445
+
446
+ await executeClaudeTask("task-1");
447
+
448
+ expect(mockSet).toHaveBeenCalledWith(
449
+ expect.objectContaining({ status: "completed", result: "done" })
450
+ );
451
+ expect(errorSpy).toHaveBeenCalledWith(
452
+ "[self-improvement] pattern extraction failed:",
453
+ expect.any(Error)
454
+ );
455
+ errorSpy.mockRestore();
456
+ });
340
457
  });
341
458
 
342
459
  // ═══════════════════════════════════════════════════════════════════════
@@ -527,6 +644,88 @@ describe("resumeClaudeTask", () => {
527
644
  expect.objectContaining({ event: "error" })
528
645
  );
529
646
  });
647
+
648
+ it("C5: waits for learned-pattern extraction before final cleanup on resume", async () => {
649
+ let resolveAnalysis: (() => void) | null = null;
650
+ mockWhere.mockResolvedValueOnce([
651
+ makeTask({ sessionId: "sess-123", resumeCount: 0 }),
652
+ ]);
653
+ mockQuery.mockReturnValue(
654
+ createMockStream([{ type: "result", result: "resumed ok" }]) as unknown as ReturnType<typeof query>
655
+ );
656
+ mockAnalyzeForLearnedPatterns.mockReturnValueOnce(
657
+ new Promise((resolve) => {
658
+ resolveAnalysis = () => resolve(null);
659
+ })
660
+ );
661
+
662
+ const runPromise = resumeClaudeTask("task-1");
663
+ await vi.waitFor(() => {
664
+ expect(mockAnalyzeForLearnedPatterns).toHaveBeenCalledWith("task-1", "general");
665
+ });
666
+
667
+ expect(mockRemoveExecution).not.toHaveBeenCalled();
668
+
669
+ resolveAnalysis?.();
670
+ await runPromise;
671
+
672
+ expect(mockRemoveExecution).toHaveBeenCalledWith("task-1");
673
+ });
674
+
675
+ it("R-stagent-1: injects stagent MCP server into query mcpServers on resume", async () => {
676
+ mockWhere.mockResolvedValueOnce([
677
+ makeTask({
678
+ projectId: "proj-7",
679
+ sessionId: "session-abc",
680
+ resumeCount: 1,
681
+ }),
682
+ ]);
683
+ mockQuery.mockReturnValue(
684
+ createMockStream([
685
+ { type: "result", result: "resumed and done" },
686
+ ]) as unknown as ReturnType<typeof query>
687
+ );
688
+
689
+ await resumeClaudeTask("task-1");
690
+
691
+ const queryCall = mockQuery.mock.calls[0][0] as {
692
+ options: { mcpServers?: Record<string, unknown>; resume?: string };
693
+ };
694
+ expect(queryCall.options.resume).toBe("session-abc");
695
+ expect(queryCall.options.mcpServers).toBeDefined();
696
+ expect(queryCall.options.mcpServers!.stagent).toEqual({ __mockStagentServer: true });
697
+ expect(vi.mocked(createToolServer)).toHaveBeenCalledWith("proj-7");
698
+ });
699
+
700
+ it("R-stagent-2: prepends mcp__stagent__* on resume when profile has allowedTools", async () => {
701
+ mockWhere.mockResolvedValueOnce([
702
+ makeTask({
703
+ projectId: "proj-7",
704
+ sessionId: "session-abc",
705
+ resumeCount: 1,
706
+ }),
707
+ ]);
708
+ mockGetProfile.mockReturnValueOnce({
709
+ id: "restricted",
710
+ name: "Restricted",
711
+ systemPrompt: "",
712
+ allowedTools: ["Read", "Grep"],
713
+ });
714
+ mockQuery.mockReturnValue(
715
+ createMockStream([
716
+ { type: "result", result: "resumed and done" },
717
+ ]) as unknown as ReturnType<typeof query>
718
+ );
719
+
720
+ await resumeClaudeTask("task-1");
721
+
722
+ const queryCall = mockQuery.mock.calls[0][0] as {
723
+ options: { allowedTools?: string[] };
724
+ };
725
+ expect(queryCall.options.allowedTools).toContain("mcp__stagent__*");
726
+ expect(queryCall.options.allowedTools).toContain("Read");
727
+ expect(queryCall.options.allowedTools![0]).toBe("mcp__stagent__*");
728
+ });
530
729
  });
531
730
 
532
731
  // ═══════════════════════════════════════════════════════════════════════
@@ -1,22 +1,10 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
-
3
- vi.mock("@/lib/license/manager", () => ({
4
- licenseManager: {
5
- getLimit: vi.fn().mockReturnValue(Infinity),
6
- getTier: vi.fn().mockReturnValue("scale"),
7
- },
8
- }));
9
-
10
- vi.mock("@/lib/license/notifications", () => ({
11
- createTierLimitNotification: vi.fn().mockResolvedValue(undefined),
12
- }));
1
+ import { describe, it, expect, beforeEach } from "vitest";
13
2
 
14
3
  import {
15
4
  getExecution,
16
5
  setExecution,
17
6
  removeExecution,
18
7
  getAllExecutions,
19
- ParallelLimitError,
20
8
  } from "@/lib/agents/execution-manager";
21
9
 
22
10
  function makeExecution(taskId: string) {
@@ -73,18 +61,4 @@ describe("execution-manager", () => {
73
61
  it("removing non-existent task does not throw", () => {
74
62
  expect(() => removeExecution("nonexistent")).not.toThrow();
75
63
  });
76
-
77
- it("throws ParallelLimitError when limit is reached", async () => {
78
- const { licenseManager } = await import("@/lib/license/manager");
79
- (licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(2);
80
- (licenseManager.getTier as ReturnType<typeof vi.fn>).mockReturnValue("community");
81
-
82
- setExecution("task-1", makeExecution("task-1"));
83
- setExecution("task-2", makeExecution("task-2"));
84
-
85
- expect(() => setExecution("task-3", makeExecution("task-3"))).toThrow(ParallelLimitError);
86
-
87
- // Restore unlimited for other tests
88
- (licenseManager.getLimit as ReturnType<typeof vi.fn>).mockReturnValue(Infinity);
89
- });
90
64
  });
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { db } from "@/lib/db";
3
+ import { tasks, projects } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { randomUUID } from "crypto";
6
+ import { writeTerminalFailureReason } from "../claude-agent";
7
+
8
+ function seedRunningTask(): string {
9
+ const pid = randomUUID();
10
+ const tid = randomUUID();
11
+ const now = new Date();
12
+ db.insert(projects)
13
+ .values({ id: pid, name: "p", status: "active", createdAt: now, updatedAt: now })
14
+ .run();
15
+ db.insert(tasks)
16
+ .values({
17
+ id: tid,
18
+ projectId: pid,
19
+ title: "t",
20
+ status: "running",
21
+ priority: 2,
22
+ resumeCount: 0,
23
+ createdAt: now,
24
+ updatedAt: now,
25
+ })
26
+ .run();
27
+ return tid;
28
+ }
29
+
30
+ describe("writeTerminalFailureReason", () => {
31
+ beforeEach(() => {
32
+ db.delete(tasks).run();
33
+ db.delete(projects).run();
34
+ });
35
+
36
+ it("writes 'turn_limit_exceeded' on turn limit errors", async () => {
37
+ const tid = seedRunningTask();
38
+ await writeTerminalFailureReason(
39
+ tid,
40
+ new Error("Agent exhausted its turn limit (42 turns used)"),
41
+ );
42
+ const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
43
+ expect(row?.failureReason).toBe("turn_limit_exceeded");
44
+ });
45
+
46
+ it("writes 'aborted' on AbortError", async () => {
47
+ const tid = seedRunningTask();
48
+ const err = new Error("aborted");
49
+ err.name = "AbortError";
50
+ await writeTerminalFailureReason(tid, err);
51
+ const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
52
+ expect(row?.failureReason).toBe("aborted");
53
+ });
54
+
55
+ it("writes 'sdk_error' for unknown errors", async () => {
56
+ const tid = seedRunningTask();
57
+ await writeTerminalFailureReason(tid, new Error("something weird"));
58
+ const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
59
+ expect(row?.failureReason).toBe("sdk_error");
60
+ });
61
+
62
+ it("writes 'rate_limited' on 429 errors", async () => {
63
+ const tid = seedRunningTask();
64
+ await writeTerminalFailureReason(tid, new Error("HTTP 429 rate limit"));
65
+ const row = db.select().from(tasks).where(eq(tasks.id, tid)).get();
66
+ expect(row?.failureReason).toBe("rate_limited");
67
+ });
68
+ });
@@ -88,17 +88,6 @@ vi.mock("@/lib/constants/settings", () => ({
88
88
  },
89
89
  }));
90
90
 
91
- vi.mock("@/lib/license/limit-check", () => ({
92
- checkLimit: vi.fn().mockReturnValue({ allowed: true, current: 0, limit: 10, tier: "community", requiredTier: "community" }),
93
- }));
94
-
95
- vi.mock("@/lib/license/limit-queries", () => ({
96
- getContextVersionCount: vi.fn().mockReturnValue(0),
97
- }));
98
-
99
- vi.mock("@/lib/license/notifications", () => ({
100
- createTierLimitNotification: vi.fn().mockResolvedValue(undefined),
101
- }));
102
91
 
103
92
  // ─── Import under test ────────────────────────────────────────────────
104
93
 
@@ -0,0 +1,158 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ mockAll,
5
+ mockLimit,
6
+ mockOrderBy,
7
+ mockWhere,
8
+ mockFrom,
9
+ mockSelect,
10
+ mockValues,
11
+ mockInsert,
12
+ mockSetWhere,
13
+ mockSet,
14
+ mockUpdate,
15
+ mockGetActiveLearnedContext,
16
+ mockCheckContextSize,
17
+ mockSummarizeContext,
18
+ } = vi.hoisted(() => {
19
+ const mockAll = vi.fn();
20
+ const mockLimit = vi.fn().mockReturnValue({ all: mockAll });
21
+ const mockOrderBy = vi.fn().mockReturnValue({ limit: mockLimit });
22
+ const mockWhere = vi.fn().mockReturnValue({ all: mockAll, orderBy: mockOrderBy });
23
+ const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
24
+ const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
25
+ const mockValues = vi.fn().mockResolvedValue(undefined);
26
+ const mockInsert = vi.fn().mockReturnValue({ values: mockValues });
27
+ const mockSetWhere = vi.fn().mockResolvedValue(undefined);
28
+ const mockSet = vi.fn().mockReturnValue({ where: mockSetWhere });
29
+ const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
30
+ return {
31
+ mockAll,
32
+ mockLimit,
33
+ mockOrderBy,
34
+ mockWhere,
35
+ mockFrom,
36
+ mockSelect,
37
+ mockValues,
38
+ mockInsert,
39
+ mockSetWhere,
40
+ mockSet,
41
+ mockUpdate,
42
+ mockGetActiveLearnedContext: vi.fn(),
43
+ mockCheckContextSize: vi.fn(),
44
+ mockSummarizeContext: vi.fn(),
45
+ };
46
+ });
47
+
48
+ vi.mock("@/lib/db", () => ({
49
+ db: {
50
+ select: mockSelect,
51
+ insert: mockInsert,
52
+ update: mockUpdate,
53
+ },
54
+ }));
55
+
56
+ vi.mock("@/lib/db/schema", () => ({
57
+ learnedContext: {
58
+ id: "id",
59
+ profileId: "profile_id",
60
+ changeType: "change_type",
61
+ version: "version",
62
+ },
63
+ notifications: {
64
+ id: "id",
65
+ toolInput: "tool_input",
66
+ type: "type",
67
+ response: "response",
68
+ },
69
+ tasks: {
70
+ workflowId: "workflow_id",
71
+ id: "id",
72
+ },
73
+ }));
74
+
75
+ vi.mock("drizzle-orm", () => ({
76
+ eq: vi.fn((_col: string, val: unknown) => ({ val })),
77
+ and: vi.fn((...conditions: unknown[]) => conditions),
78
+ desc: vi.fn((col: string) => ({ desc: col })),
79
+ isNull: vi.fn((col: string) => ({ isNull: col })),
80
+ }));
81
+
82
+ vi.mock("../learned-context", () => ({
83
+ getActiveLearnedContext: mockGetActiveLearnedContext,
84
+ checkContextSize: mockCheckContextSize,
85
+ summarizeContext: mockSummarizeContext,
86
+ }));
87
+
88
+ import { batchApproveProposals } from "../learning-session";
89
+
90
+ describe("batchApproveProposals", () => {
91
+ beforeEach(() => {
92
+ vi.clearAllMocks();
93
+ mockSelect.mockReturnValue({ from: mockFrom });
94
+ mockFrom.mockReturnValue({ where: mockWhere });
95
+ mockWhere.mockReturnValue({ all: mockAll, orderBy: mockOrderBy });
96
+ mockOrderBy.mockReturnValue({ limit: mockLimit });
97
+ mockLimit.mockReturnValue({ all: mockAll });
98
+ mockInsert.mockReturnValue({ values: mockValues });
99
+ mockUpdate.mockReturnValue({ set: mockSet });
100
+ mockSet.mockReturnValue({ where: mockSetWhere });
101
+ mockValues.mockResolvedValue(undefined);
102
+ mockSetWhere.mockResolvedValue(undefined);
103
+ mockGetActiveLearnedContext.mockReturnValue("Existing context");
104
+ mockCheckContextSize.mockReturnValue({
105
+ currentSize: 9000,
106
+ limit: 8000,
107
+ needsSummarization: true,
108
+ });
109
+ mockSummarizeContext.mockResolvedValue(undefined);
110
+ });
111
+
112
+ it("resolves without waiting for summarization and checks each profile once", async () => {
113
+ let releaseSummaries: (() => void) | null = null;
114
+ mockSummarizeContext.mockReturnValueOnce(
115
+ new Promise<void>((resolve) => {
116
+ releaseSummaries = resolve;
117
+ })
118
+ );
119
+
120
+ mockAll
121
+ .mockReturnValueOnce([
122
+ {
123
+ id: "proposal-1",
124
+ profileId: "general",
125
+ proposedAdditions: "First addition",
126
+ sourceTaskId: "task-1",
127
+ proposalNotificationId: null,
128
+ },
129
+ ])
130
+ .mockReturnValueOnce([{ version: 2 }])
131
+ .mockReturnValueOnce([
132
+ {
133
+ id: "proposal-2",
134
+ profileId: "general",
135
+ proposedAdditions: "Second addition",
136
+ sourceTaskId: "task-2",
137
+ proposalNotificationId: null,
138
+ },
139
+ ])
140
+ .mockReturnValueOnce([{ version: 3 }])
141
+ .mockReturnValueOnce([
142
+ {
143
+ id: "batch-notif-1",
144
+ toolInput: JSON.stringify({ proposalIds: ["proposal-1", "proposal-2"] }),
145
+ },
146
+ ]);
147
+
148
+ const result = await batchApproveProposals(["proposal-1", "proposal-2"]);
149
+
150
+ expect(result).toBe(2);
151
+ expect(mockCheckContextSize).toHaveBeenCalledTimes(1);
152
+ expect(mockCheckContextSize).toHaveBeenCalledWith("general");
153
+ expect(mockSummarizeContext).toHaveBeenCalledTimes(1);
154
+ expect(mockSummarizeContext).toHaveBeenCalledWith("general");
155
+
156
+ releaseSummaries?.();
157
+ });
158
+ });