@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -16,21 +16,34 @@ function insertFreePriceMapping(core, planVersionId) {
16
16
  `).run(core.getTenantId(), planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
17
17
  }
18
18
  function seedPlanCatalog(core) {
19
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
20
- core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
19
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
20
+ core.upsertPlanCatalog({ planId: 'basic', displayName: 'Basic', status: 'active' });
21
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
21
22
  core.updatePlanVersion({
22
- planId: 'collab',
23
+ planId: 'basic',
24
+ versionNumber: 1,
25
+ seatLimit: null
26
+ });
27
+ core.updatePlanVersion({
28
+ planId: 'team-5',
23
29
  versionNumber: 1,
24
30
  seatLimit: 5
25
31
  });
26
32
  core.updatePlanVersionFeatures({
27
- planVersionId: 'starter-v1',
33
+ planVersionId: 'free-v1',
34
+ features: [
35
+ { featureKey: 'workspace.switching', access: 'enabled' }
36
+ ]
37
+ });
38
+ core.updatePlanVersionFeatures({
39
+ planVersionId: 'basic-v1',
28
40
  features: [
41
+ { featureKey: 'context.uploads', access: 'enabled' },
29
42
  { featureKey: 'workspace.switching', access: 'enabled' }
30
43
  ]
31
44
  });
32
45
  core.updatePlanVersionFeatures({
33
- planVersionId: 'collab-v1',
46
+ planVersionId: 'team-5-v1',
34
47
  features: [
35
48
  { featureKey: 'collaboration.invites', access: 'enabled' },
36
49
  { featureKey: 'collaboration.team_management', access: 'enabled' },
@@ -38,7 +51,7 @@ function seedPlanCatalog(core) {
38
51
  { featureKey: 'workspace.switching', access: 'enabled' }
39
52
  ]
40
53
  });
41
- insertFreePriceMapping(core, 'starter-v1');
54
+ insertFreePriceMapping(core, 'free-v1');
42
55
  }
43
56
  function seedPlans(core) {
44
57
  seedPlanCatalog(core);
@@ -46,7 +59,7 @@ function seedPlans(core) {
46
59
  billing: {
47
60
  onboardingPolicy: {
48
61
  mode: 'auto_grant_free',
49
- planVersionId: 'starter-v1'
62
+ planVersionId: 'free-v1'
50
63
  }
51
64
  }
52
65
  });
@@ -78,7 +91,7 @@ describe('TaskforceCore system admin model', () => {
78
91
  LIMIT 1
79
92
  `).get('default', created.userId);
80
93
  expect(row?.user_id).toBe(created.userId);
81
- expect(row?.tier).toBe('starter');
94
+ expect(row?.tier).toBe('free');
82
95
  expect(row?.entitlement_state).toBe('active');
83
96
  }
84
97
  finally {
@@ -105,7 +118,7 @@ describe('TaskforceCore system admin model', () => {
105
118
  LIMIT 1
106
119
  `).get('default', created.userId);
107
120
  expect(row?.user_id).toBe(created.userId);
108
- expect(row?.tier).toBe('starter');
121
+ expect(row?.tier).toBe('free');
109
122
  expect(row?.entitlement_state).toBe('active');
110
123
  }
111
124
  finally {
@@ -118,14 +131,14 @@ describe('TaskforceCore system admin model', () => {
118
131
  const core = openCore(projectRoot, { seedDefaultPlans: true });
119
132
  try {
120
133
  core.createPlanVersion({
121
- planId: 'collab',
134
+ planId: 'team-5',
122
135
  versionNumber: 2,
123
136
  isDefault: true,
124
137
  seatLimit: 10,
125
138
  trialDays: 14
126
139
  });
127
140
  core.updatePlanVersionFeatures({
128
- planVersionId: 'collab-v2',
141
+ planVersionId: 'team-5-v2',
129
142
  features: [
130
143
  { featureKey: 'collaboration.invites', access: 'enabled' },
131
144
  { featureKey: 'collaboration.team_management', access: 'enabled' },
@@ -134,7 +147,7 @@ describe('TaskforceCore system admin model', () => {
134
147
  ]
135
148
  });
136
149
  core.createPlanVersion({
137
- planId: 'starter',
150
+ planId: 'free',
138
151
  versionNumber: 2,
139
152
  isDefault: false,
140
153
  seatLimit: null
@@ -143,7 +156,7 @@ describe('TaskforceCore system admin model', () => {
143
156
  billing: {
144
157
  onboardingPolicy: {
145
158
  mode: 'auto_start_trial',
146
- planVersionId: 'collab-v2'
159
+ planVersionId: 'team-5-v2'
147
160
  }
148
161
  }
149
162
  });
@@ -159,10 +172,10 @@ describe('TaskforceCore system admin model', () => {
159
172
  WHERE tenant_id = ? AND user_id = ?
160
173
  LIMIT 1
161
174
  `).get(core.getTenantId(), created.userId);
162
- expect(entitlement.planVersionId).toBe('collab-v2');
163
- expect(entitlement.planId).toBe('collab');
175
+ expect(entitlement.planVersionId).toBe('team-5-v2');
176
+ expect(entitlement.planId).toBe('team-5');
164
177
  expect(entitlement.state).toBe('trialing');
165
- expect(billing?.tier).toBe('collab');
178
+ expect(billing?.tier).toBe('team-5');
166
179
  expect(billing?.entitlement_state).toBe('trialing');
167
180
  }
168
181
  finally {
@@ -244,7 +257,7 @@ describe('TaskforceCore system admin model', () => {
244
257
  access = excluded.access,
245
258
  config_json = excluded.config_json,
246
259
  updated_at = excluded.updated_at
247
- `).run('default', 'collab-v1', 'documents.workspace_access', 'enabled', '{}', now, now);
260
+ `).run('default', 'team-5-v1', 'documents.workspace_access', 'enabled', '{}', now, now);
248
261
  }
249
262
  finally {
250
263
  core.close();
@@ -257,9 +270,9 @@ describe('TaskforceCore system admin model', () => {
257
270
  FROM plan_feature_matrix
258
271
  WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
259
272
  LIMIT 1
260
- `).get('default', 'collab-v1', 'documents.workspace_access');
273
+ `).get('default', 'team-5-v1', 'documents.workspace_access');
261
274
  expect(row).toBeUndefined();
262
- const snapshot = core.listPlanVersions('collab').find((version) => version.planVersionId === 'collab-v1');
275
+ const snapshot = core.listPlanVersions('team-5').find((version) => version.planVersionId === 'team-5-v1');
263
276
  expect(snapshot).toBeDefined();
264
277
  expect(snapshot.features['documents.workspace_access']).toBeUndefined();
265
278
  }
@@ -268,34 +281,120 @@ describe('TaskforceCore system admin model', () => {
268
281
  fs.rmSync(projectRoot, { recursive: true, force: true });
269
282
  }
270
283
  });
271
- it('assigns the explicitly selected plan version instead of the configured onboarding plan', () => {
284
+ it('keeps configured auto-grant onboarding plan when registration includes a selected plan version', () => {
272
285
  const projectRoot = makeProjectRoot();
273
286
  const core = openCore(projectRoot, { seedDefaultPlans: true });
274
287
  try {
275
- core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
276
288
  core.updatePlanVersion({
277
- planId: 'free',
289
+ planId: 'basic',
278
290
  versionNumber: 1,
279
- seatLimit: 1
291
+ seatLimit: null
280
292
  });
281
- insertFreePriceMapping(core, 'free-v1');
282
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'archived' });
283
293
  core.updateGlobalSettings({
284
294
  billing: {
285
295
  onboardingPolicy: {
286
296
  mode: 'auto_grant_free',
287
- planVersionId: 'starter-v1'
297
+ planVersionId: 'free-v1'
288
298
  }
289
299
  }
290
300
  });
291
301
  const created = core.registerPasswordUser({
292
302
  email: 'selected-plan@example.com',
293
303
  password: 'password123',
294
- planVersionId: 'free-v1'
304
+ planVersionId: 'basic-v1'
305
+ });
306
+ expect(created.planSelectionRequired).toBe(false);
307
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
308
+ planVersionId: 'free-v1',
309
+ planId: 'free',
310
+ state: 'active'
311
+ });
312
+ }
313
+ finally {
314
+ core.close();
315
+ fs.rmSync(projectRoot, { recursive: true, force: true });
316
+ }
317
+ });
318
+ it('uses the explicitly selected plan version when onboarding requires plan selection', () => {
319
+ const projectRoot = makeProjectRoot();
320
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
321
+ try {
322
+ core.updatePlanVersion({
323
+ planId: 'basic',
324
+ versionNumber: 1,
325
+ seatLimit: null
326
+ });
327
+ core.updateGlobalSettings({
328
+ billing: {
329
+ onboardingPolicy: {
330
+ mode: 'require_plan_selection',
331
+ planVersionId: null
332
+ }
333
+ }
334
+ });
335
+ const created = core.registerPasswordUser({
336
+ email: 'selected-plan-required@example.com',
337
+ password: 'password123',
338
+ planVersionId: 'basic-v1'
295
339
  });
296
340
  expect(created.planSelectionRequired).toBe(false);
297
341
  expect(core.getAccountEntitlement(created.userId)).toMatchObject({
342
+ planVersionId: 'basic-v1',
343
+ planId: 'basic',
344
+ state: 'checkout_pending'
345
+ });
346
+ }
347
+ finally {
348
+ core.close();
349
+ fs.rmSync(projectRoot, { recursive: true, force: true });
350
+ }
351
+ });
352
+ it('auto-heals stranded legacy personal free entitlements onto the active default free version', () => {
353
+ const projectRoot = makeProjectRoot();
354
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
355
+ try {
356
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
357
+ insertFreePriceMapping(core, 'free-v1');
358
+ core.createPlanVersion({
359
+ planId: 'free',
360
+ versionNumber: 2,
361
+ isDefault: true,
362
+ seatLimit: 1
363
+ });
364
+ insertFreePriceMapping(core, 'free-v2');
365
+ core.updateGlobalSettings({
366
+ billing: {
367
+ onboardingPolicy: {
368
+ mode: 'require_plan_selection',
369
+ planVersionId: null
370
+ }
371
+ }
372
+ });
373
+ const created = core.registerPasswordUser({
374
+ email: 'legacy-free@example.com',
375
+ password: 'password123',
376
+ planVersionId: 'free-v1'
377
+ });
378
+ core.updateAccountEntitlement({
379
+ accountId: created.userId,
298
380
  planVersionId: 'free-v1',
381
+ entitlementState: 'canceled',
382
+ actorUserId: created.userId,
383
+ actorRole: 'user',
384
+ reason: 'legacy-free-v1-canceled'
385
+ });
386
+ expect(core.getAccountAccessStatus(created.userId)).toMatchObject({
387
+ gate: 'ok',
388
+ hasEntitlement: true,
389
+ canAccessApp: true,
390
+ entitlement: {
391
+ planVersionId: 'free-v2',
392
+ planId: 'free',
393
+ state: 'active'
394
+ }
395
+ });
396
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
397
+ planVersionId: 'free-v2',
299
398
  planId: 'free',
300
399
  state: 'active'
301
400
  });
@@ -305,6 +404,61 @@ describe('TaskforceCore system admin model', () => {
305
404
  fs.rmSync(projectRoot, { recursive: true, force: true });
306
405
  }
307
406
  });
407
+ it('does not auto-heal canceled accounts already on the current default free version', () => {
408
+ const projectRoot = makeProjectRoot();
409
+ const core = openCore(projectRoot, { seedDefaultPlans: true });
410
+ try {
411
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
412
+ insertFreePriceMapping(core, 'free-v1');
413
+ core.createPlanVersion({
414
+ planId: 'free',
415
+ versionNumber: 2,
416
+ isDefault: true,
417
+ seatLimit: 1
418
+ });
419
+ insertFreePriceMapping(core, 'free-v2');
420
+ core.updateGlobalSettings({
421
+ billing: {
422
+ onboardingPolicy: {
423
+ mode: 'require_plan_selection',
424
+ planVersionId: null
425
+ }
426
+ }
427
+ });
428
+ const created = core.registerPasswordUser({
429
+ email: 'current-default-free@example.com',
430
+ password: 'password123',
431
+ planVersionId: 'free-v2'
432
+ });
433
+ core.updateAccountEntitlement({
434
+ accountId: created.userId,
435
+ planVersionId: 'free-v2',
436
+ entitlementState: 'canceled',
437
+ actorUserId: created.userId,
438
+ actorRole: 'user',
439
+ reason: 'intentional-free-v2-cancel'
440
+ });
441
+ expect(core.getAccountAccessStatus(created.userId)).toMatchObject({
442
+ gate: 'missing_entitlement',
443
+ hasEntitlement: true,
444
+ canAccessApp: false,
445
+ entitlement: {
446
+ planVersionId: 'free-v2',
447
+ planId: 'free',
448
+ state: 'canceled'
449
+ }
450
+ });
451
+ expect(core.getAccountEntitlement(created.userId)).toMatchObject({
452
+ planVersionId: 'free-v2',
453
+ planId: 'free',
454
+ state: 'canceled'
455
+ });
456
+ }
457
+ finally {
458
+ core.close();
459
+ fs.rmSync(projectRoot, { recursive: true, force: true });
460
+ }
461
+ });
308
462
  it('treats accounts without an entitlement or pending-selection marker as missing entitlement', () => {
309
463
  const projectRoot = makeProjectRoot();
310
464
  const core = openCore(projectRoot, { seedDefaultPlans: true });
@@ -344,7 +498,7 @@ describe('TaskforceCore system admin model', () => {
344
498
  UPDATE plan_versions
345
499
  SET is_default = 0
346
500
  WHERE tenant_id = ? AND plan_id = ?
347
- `).run('default', 'starter');
501
+ `).run('default', 'free');
348
502
  }
349
503
  finally {
350
504
  initial.close();
@@ -357,10 +511,10 @@ describe('TaskforceCore system admin model', () => {
357
511
  FROM plan_versions
358
512
  WHERE tenant_id = ? AND plan_id = ? AND plan_version_id = ?
359
513
  LIMIT 1
360
- `).get('default', 'starter', 'starter-v1');
361
- expect(row?.plan_version_id).toBe('starter-v1');
514
+ `).get('default', 'free', 'free-v1');
515
+ expect(row?.plan_version_id).toBe('free-v1');
362
516
  expect(row?.is_default).toBe(1);
363
- expect(reopened.getRequiredDefaultPlanVersionId('starter')).toBe('starter-v1');
517
+ expect(reopened.getRequiredDefaultPlanVersionId('free')).toBe('free-v1');
364
518
  }
365
519
  finally {
366
520
  reopened.close();
@@ -405,7 +559,7 @@ describe('TaskforceCore system admin model', () => {
405
559
  UPDATE plan_versions
406
560
  SET seat_limit = NULL, updated_at = ?
407
561
  WHERE tenant_id = ? AND plan_version_id = ?
408
- `).run(now, 'default', 'collab-v1');
562
+ `).run(now, 'default', 'team-5-v1');
409
563
  db.prepare(`
410
564
  INSERT INTO plan_feature_matrix (tenant_id, plan_version_id, feature_key, access, config_json, created_at, updated_at)
411
565
  VALUES (?, ?, 'collaboration.seat_limit', 'limited', ?, ?, ?)
@@ -413,14 +567,14 @@ describe('TaskforceCore system admin model', () => {
413
567
  access = excluded.access,
414
568
  config_json = excluded.config_json,
415
569
  updated_at = excluded.updated_at
416
- `).run('default', 'collab-v1', JSON.stringify({ seat_limit: 7 }), now, now);
570
+ `).run('default', 'team-5-v1', JSON.stringify({ seat_limit: 7 }), now, now);
417
571
  }
418
572
  finally {
419
573
  initial.close();
420
574
  }
421
575
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
422
576
  try {
423
- const version = reopened.getPlanVersionFeatures('collab', 'collab-v1');
577
+ const version = reopened.getPlanVersionFeatures('team-5', 'team-5-v1');
424
578
  expect(version.seatLimit).toBe(7);
425
579
  expect(version.features['collaboration.seat_limit']?.config || {}).toEqual({});
426
580
  }
@@ -453,13 +607,15 @@ describe('TaskforceCore system admin model', () => {
453
607
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
454
608
  try {
455
609
  const db = reopened.db;
610
+ const normalizedPersonalVersionId = reopened.getDefaultPlanVersionIdByMode('personal');
611
+ const normalizedBillingMirror = reopened.getBillingMirrorProfileForPlanVersion(normalizedPersonalVersionId);
456
612
  const row = db.prepare(`
457
613
  SELECT tier, entitlement_state
458
614
  FROM user_billing
459
615
  WHERE tenant_id = ? AND user_id = ?
460
616
  LIMIT 1
461
617
  `).get('default', legacyUserId);
462
- expect(row?.tier).toBe('starter');
618
+ expect(row?.tier).toBe(normalizedBillingMirror.planId);
463
619
  expect(row?.entitlement_state).toBe('active');
464
620
  }
465
621
  finally {
@@ -490,11 +646,11 @@ describe('TaskforceCore system admin model', () => {
490
646
  db.prepare(`
491
647
  INSERT INTO plan_catalog (plan_id, tenant_id, display_name, status, created_at, updated_at)
492
648
  VALUES (?, ?, ?, 'active', ?, ?)
493
- `).run('enterprise', 'default', 'Enterprise', now, now);
649
+ `).run('agency', 'default', 'Agency', now, now);
494
650
  db.prepare(`
495
651
  INSERT INTO plan_versions (plan_version_id, tenant_id, plan_id, version_number, is_default, seat_limit, metadata_json, created_at, updated_at)
496
652
  VALUES (?, ?, ?, 1, 1, 25, '{}', ?, ?)
497
- `).run('enterprise-v1', 'default', 'enterprise', now, now);
653
+ `).run('agency-v1', 'default', 'agency', now, now);
498
654
  }
499
655
  finally {
500
656
  initial.close();
@@ -502,8 +658,8 @@ describe('TaskforceCore system admin model', () => {
502
658
  const reopened = openCore(projectRoot, { seedDefaultPlans: false });
503
659
  try {
504
660
  const plans = reopened.listPlanCatalog();
505
- expect(plans.map((plan) => plan.planId)).toEqual(['enterprise']);
506
- expect(reopened.getDefaultPlanVersionIdByMode('personal')).toBe('enterprise-v1');
661
+ expect(plans.map((plan) => plan.planId)).toEqual(['agency']);
662
+ expect(reopened.getDefaultPlanVersionIdByMode('personal')).toBe('agency-v1');
507
663
  }
508
664
  finally {
509
665
  reopened.close();
@@ -613,13 +769,13 @@ describe('TaskforceCore system admin model', () => {
613
769
  password: 'password123'
614
770
  });
615
771
  core.createPlanVersion({
616
- planId: 'starter',
772
+ planId: 'free',
617
773
  versionNumber: 2,
618
774
  isDefault: true
619
775
  });
620
776
  core.updateAccountEntitlement({
621
777
  accountId: member.userId,
622
- planVersionId: 'starter-v2',
778
+ planVersionId: 'free-v2',
623
779
  entitlementState: 'active',
624
780
  actorUserId: admin.userId,
625
781
  actorRole: 'system_admin'
@@ -741,7 +897,7 @@ describe('TaskforceCore system admin model', () => {
741
897
  ON CONFLICT (user_id) DO UPDATE SET
742
898
  stripe_customer_id = excluded.stripe_customer_id,
743
899
  updated_at = excluded.updated_at
744
- `).run(member.userId, 'default', 'cus_test_123', 'active', 'starter', now, now);
900
+ `).run(member.userId, 'default', 'cus_test_123', 'active', 'basic', now, now);
745
901
  expect(() => core.purgeSystemUser({
746
902
  targetUserId: member.userId,
747
903
  actorUserId: admin.userId,
@@ -758,16 +914,16 @@ describe('TaskforceCore system admin model', () => {
758
914
  const core = openCore(projectRoot, { seedDefaultPlans: true });
759
915
  try {
760
916
  const plan = core.upsertPlanCatalog({
761
- planId: 'enterprise',
762
- displayName: 'Enterprise',
917
+ planId: 'agency',
918
+ displayName: 'Agency',
763
919
  status: 'active'
764
920
  });
765
- expect(plan.planId).toBe('enterprise');
766
- const versions = core.listPlanVersions('enterprise');
921
+ expect(plan.planId).toBe('agency');
922
+ const versions = core.listPlanVersions('agency');
767
923
  expect(versions).toHaveLength(1);
768
924
  expect(versions[0]).toMatchObject({
769
- planVersionId: 'enterprise-v1',
770
- planId: 'enterprise',
925
+ planVersionId: 'agency-v1',
926
+ planId: 'agency',
771
927
  versionNumber: 1,
772
928
  isDefault: true,
773
929
  seatLimit: null
@@ -783,24 +939,24 @@ describe('TaskforceCore system admin model', () => {
783
939
  const core = openCore(projectRoot, { seedDefaultPlans: true });
784
940
  try {
785
941
  core.upsertPlanCatalog({
786
- planId: 'enterprise',
787
- displayName: 'Enterprise',
942
+ planId: 'agency',
943
+ displayName: 'Agency',
788
944
  status: 'active'
789
945
  });
790
946
  core.createPlanVersion({
791
- planId: 'enterprise',
947
+ planId: 'agency',
792
948
  versionNumber: 2,
793
949
  isDefault: true,
794
950
  seatLimit: 25
795
951
  });
796
952
  core.upsertPlanCatalog({
797
- planId: 'enterprise',
798
- displayName: 'Enterprise Plus',
953
+ planId: 'agency',
954
+ displayName: 'Agency Plus',
799
955
  status: 'archived'
800
956
  });
801
- const versions = core.listPlanVersions('enterprise');
802
- expect(versions.map((version) => version.planVersionId)).toEqual(['enterprise-v2', 'enterprise-v1']);
803
- expect(versions.find((version) => version.planVersionId === 'enterprise-v1')).toMatchObject({
957
+ const versions = core.listPlanVersions('agency');
958
+ expect(versions.map((version) => version.planVersionId)).toEqual(['agency-v2', 'agency-v1']);
959
+ expect(versions.find((version) => version.planVersionId === 'agency-v1')).toMatchObject({
804
960
  isDefault: false
805
961
  });
806
962
  }
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
2
- import type { Comment, TaskActivityItem, TaskActor, TaskEvent, TaskEventDetails } from './Taskforce.js';
2
+ import type { TaskActor } from '../shared/taskActor.js';
3
+ import type { Comment, TaskActivityItem, TaskEvent, TaskEventDetails } from './Taskforce.js';
3
4
  export interface TaskActivityServiceDeps {
4
5
  db: DatabaseAdapter;
5
6
  tenantId: string;
@@ -7,6 +8,7 @@ export interface TaskActivityServiceDeps {
7
8
  normalizeWorkspaceId: (workspaceId: string | undefined) => string;
8
9
  resolveWorkspaceActor: (workspaceId: string, actorRef: unknown) => TaskActor | null;
9
10
  }
11
+ export declare function listWorkspaceTaskEventsService(deps: TaskActivityServiceDeps, workspaceId: string): TaskEvent[];
10
12
  export declare function replaceTaskCommentsService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string, comments: Comment[]): void;
11
13
  export declare function listTaskEventsInternalService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string): TaskEvent[];
12
14
  export declare function buildTaskActivityService(comments: Comment[], taskEvents: TaskEvent[]): TaskActivityItem[];
@@ -19,3 +21,4 @@ export declare function addTaskEventService(deps: TaskActivityServiceDeps, input
19
21
  details?: TaskEventDetails;
20
22
  }): void;
21
23
  export declare function replaceTaskEventsService(deps: TaskActivityServiceDeps, taskId: string, workspaceId: string, taskEvents: TaskEvent[]): void;
24
+ export declare function upsertTaskEventForSyncService(deps: TaskActivityServiceDeps, event: TaskEvent): TaskEvent;
@@ -39,6 +39,17 @@ function mapTaskEventRow(deps, row) {
39
39
  actorProfile: actor === 'system' ? null : deps.resolveWorkspaceActor(workspaceId, actor)
40
40
  };
41
41
  }
42
+ export function listWorkspaceTaskEventsService(deps, workspaceId) {
43
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
44
+ const rows = deps.db.prepare(`
45
+ SELECT id, task_id, workspace_id, action, actor, actor_type, details_json, created_at
46
+ FROM task_events
47
+ WHERE tenant_id = ?
48
+ AND workspace_id = ?
49
+ ORDER BY created_at ASC, id ASC
50
+ `).all(deps.tenantId, normalizedWorkspaceId);
51
+ return rows.map((row) => mapTaskEventRow(deps, row));
52
+ }
42
53
  export function replaceTaskCommentsService(deps, taskId, workspaceId, comments) {
43
54
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
44
55
  deps.db.prepare('DELETE FROM comments WHERE tenant_id = ? AND workspace_id = ? AND task_id = ?')
@@ -128,3 +139,37 @@ export function replaceTaskEventsService(deps, taskId, workspaceId, taskEvents)
128
139
  insertTaskEvent.run(event.id, deps.tenantId, normalizedWorkspaceId, taskId, event.action, event.actor, event.actorType, JSON.stringify(normalizeTaskEventDetails(event.details)), event.createdAt);
129
140
  }
130
141
  }
142
+ export function upsertTaskEventForSyncService(deps, event) {
143
+ const workspaceId = deps.normalizeWorkspaceId(event.workspaceId);
144
+ const normalizedEvent = {
145
+ id: String(event.id || '').trim(),
146
+ taskId: String(event.taskId || '').trim(),
147
+ workspaceId,
148
+ action: String(event.action || '').trim(),
149
+ actor: String(event.actor || 'system').trim() || 'system',
150
+ actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
151
+ ? event.actorType
152
+ : 'system',
153
+ createdAt: String(event.createdAt || '').trim() || new Date().toISOString(),
154
+ details: normalizeTaskEventDetails(event.details)
155
+ };
156
+ deps.db.prepare(`
157
+ INSERT INTO task_events (
158
+ id, tenant_id, workspace_id, task_id, action, actor, actor_type, details_json, created_at
159
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
160
+ ON CONFLICT (id) DO UPDATE SET
161
+ workspace_id = excluded.workspace_id,
162
+ task_id = excluded.task_id,
163
+ action = excluded.action,
164
+ actor = excluded.actor,
165
+ actor_type = excluded.actor_type,
166
+ details_json = excluded.details_json,
167
+ created_at = excluded.created_at
168
+ `).run(normalizedEvent.id, deps.tenantId, workspaceId, normalizedEvent.taskId, normalizedEvent.action, normalizedEvent.actor, normalizedEvent.actorType, JSON.stringify(normalizedEvent.details), normalizedEvent.createdAt);
169
+ return {
170
+ ...normalizedEvent,
171
+ actorProfile: normalizedEvent.actor === 'system'
172
+ ? null
173
+ : deps.resolveWorkspaceActor(workspaceId, normalizedEvent.actor)
174
+ };
175
+ }
@@ -92,7 +92,7 @@ describe('TaskforceCore task audit timeline', () => {
92
92
  cleanup();
93
93
  }
94
94
  });
95
- it('suppresses task events for autosave-style updates when skipTaskEvents is true', () => {
95
+ it('suppresses task events when skipTaskEvents is explicitly true', () => {
96
96
  const { core, cleanup } = makeCore();
97
97
  try {
98
98
  const task = core.createTask({ title: 'Autosave task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
@@ -110,6 +110,57 @@ describe('TaskforceCore task audit timeline', () => {
110
110
  cleanup();
111
111
  }
112
112
  });
113
+ it('suppresses title and description autosave events while preserving the saved task content', () => {
114
+ const { core, cleanup } = makeCore();
115
+ try {
116
+ const task = core.createTask({ title: 'Autosave title task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
117
+ core.updateTask(task.id, {
118
+ title: 'Autosave title task updated',
119
+ description: 'Draft body'
120
+ }, 'workspace-a', {
121
+ actorRef: 'user',
122
+ saveSource: 'autosave'
123
+ });
124
+ const events = core.listTaskEvents(task.id, 'workspace-a');
125
+ const hydrated = core.getTask(task.id, 'workspace-a');
126
+ expect(events.map((event) => event.action)).toEqual(['task-created']);
127
+ expect(hydrated?.title).toBe('Autosave title task updated');
128
+ expect(hydrated?.description).toBe('Draft body');
129
+ expect(hydrated?.activity?.some((item) => item.type === 'event' && item.event.action === 'task-updated')).toBe(false);
130
+ }
131
+ finally {
132
+ cleanup();
133
+ }
134
+ });
135
+ it('keeps meaningful autosave updates in the activity feed while filtering noisy text changes', () => {
136
+ const { core, cleanup } = makeCore();
137
+ try {
138
+ const task = core.createTask({ title: 'Autosave assignee task', createdBy: 'user' }, { workspaceId: 'workspace-a', actorRef: 'user' });
139
+ core.updateTask(task.id, {
140
+ title: 'Autosave assignee task updated',
141
+ assignee: 'member-1'
142
+ }, 'workspace-a', {
143
+ actorRef: 'user',
144
+ saveSource: 'autosave'
145
+ });
146
+ const assignmentEvent = core.listTaskEvents(task.id, 'workspace-a').find((event) => event.action === 'task-updated' && event.details?.changes?.assignee);
147
+ expect(assignmentEvent).toEqual(expect.objectContaining({
148
+ action: 'task-updated',
149
+ details: {
150
+ changes: {
151
+ assignee: {
152
+ from: 'unassigned',
153
+ to: 'member-1',
154
+ },
155
+ },
156
+ },
157
+ }));
158
+ expect(assignmentEvent?.details?.changes?.title).toBeUndefined();
159
+ }
160
+ finally {
161
+ cleanup();
162
+ }
163
+ });
113
164
  it('captures assignment changes in the core task update event details', () => {
114
165
  const { core, cleanup } = makeCore();
115
166
  try {