@taskforcehq/taskforce 0.3.304 → 0.3.306

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 (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -27,7 +27,7 @@
27
27
  * === SECTION: Navigation === Line ~3410 — setActiveTab, handleNavigation
28
28
  * === SECTION: Form === Line ~3489 — resetForm, handleEdit, handleSubmit, saveTask
29
29
  * === SECTION: Task CRUD === Line ~3691 — handleDelete, handleUpdateTask, handleToggle*
30
- * === SECTION: Task Relationships === Line ~4129 — parent/child link handlers
30
+ * === SECTION: Planning Links === Line ~4129 — workstream and initiative link handlers
31
31
  * === SECTION: Category/Type Mgmt === Line ~4462 — handleSaveCategory, handleRemoveCategory
32
32
  * === SECTION: Settings Model === Line ~4625 — settingsModel object
33
33
  * === SECTION: Return Value === Line ~4817 — final return object (public API shape)
@@ -43,6 +43,7 @@ import { DEFAULT_TYPE_VALUE } from '../shared/taxonomyDefaults.js';
43
43
  import { cloneDefaultWorkflowDefinitions } from '../shared/workflowDefaults.js';
44
44
  import { resolveClientDeploymentConfig } from '../config/clientDeployment';
45
45
  import { resolveTaskforceMcpBaseUrlFromBaseUrl } from '../config/cloudEnvironment';
46
+ import { buildBootstrapTraceId, withBootstrapTraceHeader } from '../utils/bootstrapTrace';
46
47
  import { createDefaultAssigneeOptions } from '../utils/assignees';
47
48
  import { persistLocalMutationActorUserId, readLocalMutationActorUserId } from '../utils/taskforceApiClient';
48
49
  import { useSyncOrchestrator } from './useSyncOrchestrator';
@@ -67,6 +68,13 @@ import { useTaskforceAuthBootstrap } from './auth/useTaskforceAuthBootstrap';
67
68
  import { deriveAuthGuardPolicy } from './auth/useAuthGuardPolicy';
68
69
  import { useTaskforceWorkspaceBootstrap } from './workspace/useTaskforceWorkspaceBootstrap';
69
70
  import { useDataVersionRefresh } from './sync/useDataVersionRefresh';
71
+ function upsertEntityById(items, nextItem) {
72
+ const existingIndex = items.findIndex((item) => item.id === nextItem.id);
73
+ if (existingIndex === -1) {
74
+ return [...items, nextItem];
75
+ }
76
+ return items.map((item, index) => (index === existingIndex ? nextItem : item));
77
+ }
70
78
  const ENV_DEPLOYMENT = resolveClientDeploymentConfig(import.meta.env);
71
79
  const DEFAULT_CLOUD_APP_ORIGIN = (() => {
72
80
  const configuredBase = ENV_DEPLOYMENT.baseUrl;
@@ -82,6 +90,75 @@ const DEFAULT_CLOUD_APP_ORIGIN = (() => {
82
90
  return 'https://app.taskforcehq.ai';
83
91
  })();
84
92
  const BOOTSTRAP_REQUEST_TIMEOUT_MS = 12_000;
93
+ const PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS = 20_000;
94
+ const EXPECTED_WORKSPACE_ABORT_REASONS = new Set([
95
+ 'external-abort',
96
+ 'superseded',
97
+ 'workspace-reset',
98
+ 'workspace-transition',
99
+ ]);
100
+ function hasAbortErrorName(error) {
101
+ if (typeof DOMException !== 'undefined' && error instanceof DOMException) {
102
+ return error.name === 'AbortError';
103
+ }
104
+ return String(error?.name || '').toLowerCase() === 'aborterror';
105
+ }
106
+ function isExpectedWorkspaceAbortReason(reason) {
107
+ return typeof reason === 'string' && EXPECTED_WORKSPACE_ABORT_REASONS.has(reason);
108
+ }
109
+ function isExpectedWorkspaceAbortFailure(error, signal) {
110
+ if (!signal.aborted)
111
+ return false;
112
+ if (error === signal.reason)
113
+ return true;
114
+ if (isExpectedWorkspaceAbortReason(error) || isExpectedWorkspaceAbortReason(signal.reason))
115
+ return true;
116
+ return hasAbortErrorName(error);
117
+ }
118
+ function createBootstrapTimeoutError(timeoutMs) {
119
+ const timeoutError = new Error(`Startup checks timed out after ${timeoutMs}ms.`);
120
+ timeoutError.name = 'BootstrapTimeoutError';
121
+ return timeoutError;
122
+ }
123
+ function sanitizePlanningBootstrapPayload(input) {
124
+ const payload = input && typeof input === 'object' ? input : {};
125
+ const initiatives = Array.isArray(payload.initiatives)
126
+ ? payload.initiatives.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
127
+ : [];
128
+ const workstreams = Array.isArray(payload.workstreams)
129
+ ? payload.workstreams.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
130
+ : [];
131
+ const workstreamTaskSummaries = Array.isArray(payload.workstreamTaskSummaries)
132
+ ? payload.workstreamTaskSummaries.flatMap((entry) => {
133
+ if (!entry || typeof entry !== 'object')
134
+ return [];
135
+ const normalizedWorkstreamId = String(entry.workstreamId || '').trim();
136
+ if (!normalizedWorkstreamId)
137
+ return [];
138
+ const rawTasks = Array.isArray(entry.tasks) ? entry.tasks : [];
139
+ const taskCount = Math.max(0, Number(entry.taskCount) || 0);
140
+ const completedTaskCount = Math.max(0, Number(entry.completedTaskCount) || 0);
141
+ return [{
142
+ workstreamId: normalizedWorkstreamId,
143
+ taskCount,
144
+ completedTaskCount,
145
+ tasks: rawTasks
146
+ .filter((task) => Boolean(task && typeof task === 'object' && typeof task.id === 'string'))
147
+ .map((task) => ({
148
+ id: String(task.id || '').trim(),
149
+ referenceNumber: typeof task.referenceNumber === 'number' ? task.referenceNumber : null,
150
+ title: String(task.title || '').trim() || 'Untitled Task',
151
+ status: typeof task.status === 'string' ? task.status : null,
152
+ })),
153
+ }];
154
+ })
155
+ : [];
156
+ return {
157
+ initiatives,
158
+ workstreams,
159
+ workstreamTaskSummaries,
160
+ };
161
+ }
85
162
  export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, onClose }) {
86
163
  const mergedConfig = { ...DEFAULT_CONFIG, ...config };
87
164
  const { categories, types, apiEndpoint, apiBaseUrl, cloudAuthBaseUrl, cloudMcpBaseUrl, wsBaseUrl, shortcut: configShortcut } = mergedConfig;
@@ -131,6 +208,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
131
208
  const workspaceMode = workspaceModeRaw === 'single-local' || workspaceModeRaw === 'multi-cloud'
132
209
  ? workspaceModeRaw
133
210
  : (runtimeModeHint === 'cloud' ? 'multi-cloud' : 'single-local');
211
+ setRuntimeMode(runtimeModeHint);
134
212
  setRuntimeConfigOverride({
135
213
  cloudEnvironment: typeof cfg.cloudEnvironment === 'string'
136
214
  ? String(cfg.cloudEnvironment).trim().toLowerCase()
@@ -175,10 +253,11 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
175
253
  const propApiBaseUrl = normalizeBaseUrl(apiBaseUrl);
176
254
  const propCloudAuthBaseUrl = normalizeBaseUrl(cloudAuthBaseUrl);
177
255
  const propCloudMcpBaseUrl = normalizeBaseUrl(cloudMcpBaseUrl);
178
- const explicitConfiguredApiBaseUrl = propApiBaseUrl || envApiBaseUrl || envBaseUrl;
179
256
  const runtimeResolvedApiBaseUrl = runtimeConfigOverride.apiBaseUrl || runtimeConfigOverride.baseUrl;
180
- const normalizedApiBaseUrl = explicitConfiguredApiBaseUrl
257
+ const envConfiguredApiBaseUrl = isLoopbackHost ? '' : (envApiBaseUrl || envBaseUrl);
258
+ const normalizedApiBaseUrl = propApiBaseUrl
181
259
  || runtimeResolvedApiBaseUrl
260
+ || envConfiguredApiBaseUrl
182
261
  || '';
183
262
  const explicitConfiguredCloudAuthBaseUrl = propCloudAuthBaseUrl || envCloudAuthBaseUrl || envBaseUrl;
184
263
  const runtimeResolvedCloudAuthBaseUrl = runtimeConfigOverride.cloudAuthBaseUrl || runtimeConfigOverride.baseUrl;
@@ -206,10 +285,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
206
285
  || normalizedCloudAuthBaseUrl
207
286
  || defaultCloudAuthBaseUrl).trim().replace(/\/+$/, '');
208
287
  const authOnlyCloudMode = !isCloudHost && Boolean(normalizedCloudAuthBaseUrl) && !normalizedApiBaseUrl;
209
- const shouldProbeCloudAuth = runtimeConfigReady && (Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl) || isCloudHost);
288
+ // Real cloud hosts must defer protected bootstrap calls immediately, even before
289
+ // runtime-config finishes, or the one-shot initial bootstrap can consume itself
290
+ // on unauthenticated /config, /taxonomy-state, and /planning/bootstrap reads.
291
+ const shouldProbeCloudAuth = isCloudHost || (runtimeConfigReady && Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl));
210
292
  const shouldGateProtectedApiCalls = shouldProbeCloudAuth && !authOnlyCloudMode;
211
293
  const cloudAuthConfigured = Boolean(normalizedCloudAuthBaseUrl || normalizedApiBaseUrl || isCloudHost);
212
294
  const initialRuntimeMode = isCloudHost ? 'cloud' : 'local';
295
+ const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
213
296
  const optimisticLocalAuthUserId = initialRuntimeMode === 'local' && isLoopbackHost && cloudAuthConfigured
214
297
  ? readLocalMutationActorUserId()
215
298
  : '';
@@ -235,8 +318,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
235
318
  || path.startsWith('/api/taskforce/billing/')
236
319
  || path.startsWith('/api/taskforce/sync/')
237
320
  || path.startsWith('/api/taskforce/settings/mcp/');
238
- if (!isCloudHost && isCloudScopedPath && (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy)) {
239
- return path;
321
+ if (!isCloudHost && isCloudScopedPath) {
322
+ if (runtimeMode === 'cloud') {
323
+ return path;
324
+ }
325
+ if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
326
+ return path;
327
+ }
328
+ if (runtimeConfigOverride.workspaceMode === 'multi-cloud') {
329
+ return path;
330
+ }
240
331
  }
241
332
  const base = normalizedCloudAuthBaseUrl || normalizedApiBaseUrl;
242
333
  if (base) {
@@ -258,7 +349,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
258
349
  if (!isCloudHost && isCloudScopedPath)
259
350
  return path;
260
351
  return path;
261
- }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
352
+ }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost, runtimeMode]);
262
353
  const resolveWebSocketUrl = useCallback((path) => {
263
354
  const wsPath = String(path || '').trim() || '/taskforce-ws';
264
355
  const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
@@ -308,7 +399,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
308
399
  }), [projectRoot, initialRuntimeMode]);
309
400
  const [mcpScriptPath, setMcpScriptPath] = useState('');
310
401
  const [tenantId, setTenantId] = useState('');
311
- const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
312
402
  const runtimeContractFallback = resolveRuntimeModeContract(runtimeMode);
313
403
  const workspaceMode = runtimeConfigOverride.workspaceMode === 'single-local' || runtimeConfigOverride.workspaceMode === 'multi-cloud'
314
404
  ? runtimeConfigOverride.workspaceMode
@@ -323,6 +413,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
323
413
  const [authWorkspaceId, setAuthWorkspaceId] = useState('');
324
414
  const [authUserEmail, setAuthUserEmail] = useState('');
325
415
  const [authUserDisplayName, setAuthUserDisplayName] = useState('');
416
+ const [availableAuthProviders, setAvailableAuthProviders] = useState([]);
326
417
  const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
327
418
  const [hasBetaAccess, setHasBetaAccess] = useState(true);
328
419
  const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
@@ -353,6 +444,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
353
444
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
354
445
  const explicitWorkspaceSelectionRef = useRef(null);
355
446
  const workspaceTransitionEpochRef = useRef(0);
447
+ const bootstrapTraceSeedRef = useRef((() => {
448
+ const randomPart = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
449
+ ? crypto.randomUUID()
450
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
451
+ return randomPart.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 12) || 'session';
452
+ })());
356
453
  const [workspaceTransitionEpoch, setWorkspaceTransitionEpoch] = useState(0);
357
454
  const bootstrapConfigAbortRef = useRef(null);
358
455
  const referenceDataAbortRef = useRef(null);
@@ -360,6 +457,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
360
457
  const assigneeOptionsAbortRef = useRef(null);
361
458
  const deletedTasksAbortRef = useRef(null);
362
459
  const planningEntitiesAbortRef = useRef(null);
460
+ const planningEntitiesRetryTimerRef = useRef(null);
363
461
  const commitCurrentWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
364
462
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
365
463
  if (!nextWorkspaceId)
@@ -404,9 +502,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
404
502
  return workspaceTransitionEpochRef.current !== requestState.epoch;
405
503
  }, []);
406
504
  const isAbortError = useCallback((error) => {
407
- return error instanceof DOMException
408
- ? error.name === 'AbortError'
409
- : String(error?.name || '').toLowerCase() === 'aborterror';
505
+ return hasAbortErrorName(error) || isExpectedWorkspaceAbortReason(error);
410
506
  }, []);
411
507
  const abortWorkspaceScopedRequests = useCallback(() => {
412
508
  [
@@ -420,6 +516,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
420
516
  ref.current?.abort('workspace-transition');
421
517
  ref.current = null;
422
518
  });
519
+ if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
520
+ window.clearTimeout(planningEntitiesRetryTimerRef.current);
521
+ planningEntitiesRetryTimerRef.current = null;
522
+ }
423
523
  referenceDataRequestRef.current = null;
424
524
  }, []);
425
525
  // Local runtime APIs must stay available immediately on app start, even when cloud
@@ -499,6 +599,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
499
599
  const [archivedTasks, setArchivedTasks] = useState([]);
500
600
  const [initiatives, setInitiatives] = useState([]);
501
601
  const [workstreams, setWorkstreams] = useState([]);
602
+ const [planningBootstrapTaskSummaries, setPlanningBootstrapTaskSummaries] = useState([]);
502
603
  const [deletedTasks, setDeletedTasks] = useState([]);
503
604
  const [loadingTasks, setLoadingTasks] = useState(false);
504
605
  const [editingTaskId, setEditingTaskId] = useState(null);
@@ -540,6 +641,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
540
641
  const abortController = new AbortController();
541
642
  const externalSignal = options?.signal;
542
643
  const timeoutLabel = Number(timeoutMs) > 0 ? Number(timeoutMs) : BOOTSTRAP_REQUEST_TIMEOUT_MS;
644
+ const requestState = getWorkspaceRequestState();
645
+ const tracedInit = url.startsWith('/api/taskforce/')
646
+ ? withBootstrapTraceHeader(init, buildBootstrapTraceId({
647
+ workspaceId: requestState.workspaceId,
648
+ epoch: requestState.epoch,
649
+ seed: bootstrapTraceSeedRef.current,
650
+ }))
651
+ : init;
543
652
  const timerId = typeof window !== 'undefined'
544
653
  ? window.setTimeout(() => abortController.abort('bootstrap-timeout'), timeoutLabel)
545
654
  : null;
@@ -556,18 +665,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
556
665
  }
557
666
  try {
558
667
  return await fetch(url, {
559
- ...(init || {}),
668
+ ...(tracedInit || {}),
560
669
  signal: abortController.signal
561
670
  });
562
671
  }
563
672
  catch (error) {
564
- const isAbortError = error instanceof DOMException
565
- ? error.name === 'AbortError'
566
- : String(error?.name || '').toLowerCase() === 'aborterror';
567
- if (isAbortError) {
568
- const timeoutError = new Error(`Startup checks timed out after ${timeoutLabel}ms.`);
569
- timeoutError.name = 'BootstrapTimeoutError';
570
- throw timeoutError;
673
+ if (externalSignal?.aborted && isExpectedWorkspaceAbortFailure(error, externalSignal)) {
674
+ throw externalSignal.reason || error;
675
+ }
676
+ if (hasAbortErrorName(error) || error === 'bootstrap-timeout') {
677
+ throw createBootstrapTimeoutError(timeoutLabel);
571
678
  }
572
679
  throw error;
573
680
  }
@@ -579,7 +686,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
579
686
  window.clearTimeout(timerId);
580
687
  }
581
688
  }
582
- }, []);
689
+ }, [getWorkspaceRequestState]);
690
+ const bootstrapState = useMemo(() => {
691
+ const authPending = !authSessionResolved;
692
+ const stalled = bootstrapPhase === 'stalled';
693
+ const appPending = authSessionResolved && (!configLoaded || workspaceBootstrapPending || stalled);
694
+ let subtitle = 'Checking account, workspace access, and setup status...';
695
+ if (bootstrapPhase === 'auth') {
696
+ subtitle = 'Verifying session...';
697
+ }
698
+ else if (bootstrapPhase === 'workspace') {
699
+ subtitle = 'Resolving workspace access...';
700
+ }
701
+ else if (bootstrapPhase === 'config') {
702
+ subtitle = 'Loading configuration...';
703
+ }
704
+ return {
705
+ phase: bootstrapPhase,
706
+ auth: {
707
+ resolved: authSessionResolved,
708
+ pending: authPending
709
+ },
710
+ config: {
711
+ loaded: configLoaded
712
+ },
713
+ workspace: {
714
+ pending: workspaceBootstrapPending
715
+ },
716
+ pending: authPending || appPending,
717
+ authPending,
718
+ appPending,
719
+ ready: authSessionResolved && configLoaded && !workspaceBootstrapPending && bootstrapPhase === 'ready',
720
+ stalled,
721
+ subtitle,
722
+ error: bootstrapError,
723
+ startedAt: bootstrapStartedAt
724
+ };
725
+ }, [
726
+ authSessionResolved,
727
+ bootstrapError,
728
+ bootstrapPhase,
729
+ bootstrapStartedAt,
730
+ configLoaded,
731
+ workspaceBootstrapPending
732
+ ]);
583
733
  const isBootstrapTimeoutError = useCallback((error) => {
584
734
  return String(error?.name || '') === 'BootstrapTimeoutError';
585
735
  }, []);
@@ -648,7 +798,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
648
798
  useEffect(() => {
649
799
  if (workspaceTransitionEpoch === 0)
650
800
  return;
651
- abortWorkspaceScopedRequests();
652
801
  referenceDataRequestRef.current = null;
653
802
  lastLoadedUiStateKeyRef.current = '';
654
803
  lastAssigneeOptionsLoadKeyRef.current = '';
@@ -663,8 +812,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
663
812
  setDeletedTasks([]);
664
813
  setTasks([]);
665
814
  setArchivedTasks([]);
666
- setInitiatives([]);
667
- setWorkstreams([]);
668
815
  setLoadingTasks(false);
669
816
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
670
817
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -674,7 +821,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
674
821
  window.clearTimeout(archiveBootstrapTimerRef.current);
675
822
  archiveBootstrapTimerRef.current = null;
676
823
  }
677
- }, [abortWorkspaceScopedRequests, setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
824
+ }, [setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
678
825
  const fetchDeletedTasks = useCallback(async (isSilent = false, options) => {
679
826
  const ignoreAuthGuard = options?.ignoreAuthGuard === true;
680
827
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
@@ -722,7 +869,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
722
869
  }
723
870
  }
724
871
  catch (err) {
725
- if (isAbortError(err))
872
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
726
873
  return;
727
874
  console.error('[Taskforce] Failed to fetch deleted tasks:', err);
728
875
  }
@@ -746,37 +893,105 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
746
893
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
747
894
  return;
748
895
  const requestState = getWorkspaceRequestState();
896
+ planningEntitiesAbortRef.current?.abort('superseded');
897
+ if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
898
+ window.clearTimeout(planningEntitiesRetryTimerRef.current);
899
+ planningEntitiesRetryTimerRef.current = null;
900
+ }
749
901
  const abortController = new AbortController();
750
902
  planningEntitiesAbortRef.current = abortController;
751
903
  try {
752
- const [initiativesRes, workstreamsRes] = await Promise.all([
753
- fetch('/api/taskforce/initiatives', { signal: abortController.signal }),
754
- fetch('/api/taskforce/workstreams', { signal: abortController.signal }),
755
- ]);
756
- if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
904
+ const planningTraceId = buildBootstrapTraceId({
905
+ workspaceId: requestState.workspaceId,
906
+ epoch: requestState.epoch,
907
+ seed: bootstrapTraceSeedRef.current,
908
+ });
909
+ const fetchPlanningRoute = async (url) => {
910
+ if (typeof window === 'undefined') {
911
+ return fetch(url, withBootstrapTraceHeader({ signal: abortController.signal }, planningTraceId));
912
+ }
913
+ const timeoutController = new AbortController();
914
+ const timerId = window.setTimeout(() => timeoutController.abort('planning-timeout'), PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS);
915
+ const onAbort = () => timeoutController.abort(abortController.signal.reason || 'external-abort');
916
+ if (abortController.signal.aborted) {
917
+ onAbort();
918
+ }
919
+ else {
920
+ abortController.signal.addEventListener('abort', onAbort, { once: true });
921
+ }
922
+ try {
923
+ return await fetch(url, withBootstrapTraceHeader({ signal: timeoutController.signal }, planningTraceId));
924
+ }
925
+ catch (error) {
926
+ if (abortController.signal.aborted)
927
+ throw abortController.signal.reason || error;
928
+ if (hasAbortErrorName(error) || error === 'planning-timeout') {
929
+ return null;
930
+ }
931
+ throw error;
932
+ }
933
+ finally {
934
+ window.clearTimeout(timerId);
935
+ abortController.signal.removeEventListener('abort', onAbort);
936
+ }
937
+ };
938
+ let bootstrapPayload = null;
939
+ const bootstrapRes = await fetchPlanningRoute('/api/taskforce/planning/bootstrap');
940
+ if (bootstrapRes?.status === 401) {
757
941
  handleUnauthorized();
758
942
  setInitiatives([]);
759
943
  setWorkstreams([]);
944
+ setPlanningBootstrapTaskSummaries([]);
760
945
  return;
761
946
  }
762
- const [initiativesData, workstreamsData] = await Promise.all([
763
- initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
764
- workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
765
- ]);
947
+ if (bootstrapRes?.ok) {
948
+ bootstrapPayload = sanitizePlanningBootstrapPayload(await bootstrapRes.json().catch(() => ({})));
949
+ }
950
+ else {
951
+ const [initiativesRes, workstreamsRes] = await Promise.all([
952
+ fetchPlanningRoute('/api/taskforce/initiatives'),
953
+ fetchPlanningRoute('/api/taskforce/workstreams'),
954
+ ]);
955
+ if (!initiativesRes || !workstreamsRes) {
956
+ throw new Error('Planning endpoints timed out during startup.');
957
+ }
958
+ if (initiativesRes.status === 401 || workstreamsRes.status === 401) {
959
+ handleUnauthorized();
960
+ setInitiatives([]);
961
+ setWorkstreams([]);
962
+ setPlanningBootstrapTaskSummaries([]);
963
+ return;
964
+ }
965
+ const [initiativesData, workstreamsData] = await Promise.all([
966
+ initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
967
+ workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
968
+ ]);
969
+ bootstrapPayload = sanitizePlanningBootstrapPayload({
970
+ initiatives: initiativesData,
971
+ workstreams: workstreamsData,
972
+ workstreamTaskSummaries: [],
973
+ });
974
+ }
766
975
  if (isWorkspaceRequestStale(requestState)) {
767
976
  return;
768
977
  }
769
- setInitiatives(Array.isArray(initiativesData)
770
- ? initiativesData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
771
- : []);
772
- setWorkstreams(Array.isArray(workstreamsData)
773
- ? workstreamsData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
774
- : []);
978
+ setInitiatives(bootstrapPayload.initiatives);
979
+ setWorkstreams(bootstrapPayload.workstreams);
980
+ setPlanningBootstrapTaskSummaries(bootstrapPayload.workstreamTaskSummaries);
775
981
  }
776
982
  catch (err) {
777
- if (isAbortError(err))
983
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
778
984
  return;
779
985
  console.error('[Taskforce] Failed to fetch planning entities:', err);
986
+ if (typeof window !== 'undefined'
987
+ && !isWorkspaceRequestStale(requestState)
988
+ && initiatives.length === 0
989
+ && workstreams.length === 0) {
990
+ planningEntitiesRetryTimerRef.current = window.setTimeout(() => {
991
+ planningEntitiesRetryTimerRef.current = null;
992
+ void fetchPlanningEntities(options);
993
+ }, 3000);
994
+ }
780
995
  }
781
996
  finally {
782
997
  if (planningEntitiesAbortRef.current === abortController) {
@@ -784,12 +999,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
784
999
  }
785
1000
  }
786
1001
  }, [
1002
+ BOOTSTRAP_REQUEST_TIMEOUT_MS,
1003
+ PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
1004
+ initiatives.length,
787
1005
  getWorkspaceRequestState,
788
1006
  handleUnauthorized,
789
1007
  isAbortError,
790
1008
  isWorkspaceRequestStale,
791
1009
  shouldDeferProtectedApiCalls,
792
1010
  shouldBlockProtectedApiCalls,
1011
+ workstreams.length,
793
1012
  ]);
794
1013
  const fetchTasks = useCallback(async (isSilent = false, options) => {
795
1014
  await fetchTasksBase(isSilent, options);
@@ -815,7 +1034,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
815
1034
  const setUserGlobalSyncErrorRef = useRef(() => undefined);
816
1035
  const checkAuthSessionImplRef = useRef(async () => false);
817
1036
  const checkAuthSession = useCallback((options) => checkAuthSessionImplRef.current(options), []);
818
- const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
1037
+ const { userGlobalSyncStatus, setUserGlobalSyncStatus, userGlobalSyncError, setUserGlobalSyncError, workspaceLastPullAt, workspaceLastPushAt, workspaceLastErrorAt, workspaceLastErrorMessage, workspaceLastSuccessfulSyncAt, workspaceCloudSyncEnabled, workspaceSyncPhase, workspaceSyncSetupIntent, workspaceSyncStatus, workspaceSyncSummary, workspaceSyncRecommendedAction, workspaceSyncBusy, workspaceSyncPendingChanges, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, persistWorkspaceSyncPatch, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, syncUserGlobalSettings, buildWorkspaceSyncSignature, pushWorkspaceChangesToCloud, saveWorkspaceCloudSyncSettings, retryUserGlobalSettingsSync, retryWorkspaceCloudSync, resetWorkspaceSyncCursorAndPull, getWorkspaceSyncDiagnostics } = useSyncOrchestrator({
819
1038
  currentWorkspaceId,
820
1039
  cloudAuthConfigured,
821
1040
  runtimeMode,
@@ -828,6 +1047,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
828
1047
  realtimeSyncEnabled,
829
1048
  tasks,
830
1049
  archivedTasks,
1050
+ deletedTasks,
831
1051
  initiatives,
832
1052
  workstreams,
833
1053
  taxonomies,
@@ -1317,7 +1537,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1317
1537
  }
1318
1538
  }
1319
1539
  }, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
1320
- const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1540
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks, runCloudBootstrap } = useTaskforceWorkspaceBootstrap({
1321
1541
  shouldDeferProtectedApiCalls,
1322
1542
  shouldBlockProtectedApiCalls,
1323
1543
  getWorkspaceRequestState,
@@ -1357,6 +1577,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1357
1577
  authSessionResolved,
1358
1578
  isAuthenticated,
1359
1579
  authUserId,
1580
+ authUserAvatarUrl,
1360
1581
  setHydratedWorkspaceRole,
1361
1582
  setAssigneeOptions,
1362
1583
  setAssigneeOptionsLoaded,
@@ -1382,6 +1603,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1382
1603
  showArchive,
1383
1604
  taskScope,
1384
1605
  settingsSection,
1606
+ workspaceBootstrapPending,
1385
1607
  fetchTasks,
1386
1608
  fetchArchive,
1387
1609
  fetchPlanningEntities,
@@ -1397,12 +1619,35 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1397
1619
  useEffect(() => {
1398
1620
  if (!configLoaded || workspaceBootstrapPending)
1399
1621
  return;
1400
- const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}`;
1622
+ const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}:${authUserAvatarUrl || 'no-avatar'}`;
1401
1623
  if (assigneeOptionsLoaded && lastAssigneeOptionsLoadKeyRef.current === loadKey)
1402
1624
  return;
1403
1625
  lastAssigneeOptionsLoadKeyRef.current = loadKey;
1404
1626
  void fetchAssigneeOptions();
1405
- }, [assigneeOptionsLoaded, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
1627
+ }, [assigneeOptionsLoaded, authUserAvatarUrl, configLoaded, currentWorkspaceId, fetchAssigneeOptions, isAuthenticated, runtimeMode, workspaceBootstrapPending]);
1628
+ useEffect(() => {
1629
+ if (!authSessionResolved || !isAuthenticated)
1630
+ return;
1631
+ const normalizedAuthUserId = String(authUserId || '').trim();
1632
+ if (!normalizedAuthUserId || normalizedAuthUserId === 'anonymous')
1633
+ return;
1634
+ const nextAvatarUrl = String(authUserAvatarUrl || '').trim() || null;
1635
+ setAssigneeOptions((previous) => {
1636
+ let changed = false;
1637
+ const next = previous.map((option) => {
1638
+ if (option.kind !== 'member' || String(option.value || '').trim() !== normalizedAuthUserId)
1639
+ return option;
1640
+ const currentAvatarUrl = typeof option.avatarUrl === 'string' && option.avatarUrl.trim().length > 0
1641
+ ? option.avatarUrl.trim()
1642
+ : null;
1643
+ if (currentAvatarUrl === nextAvatarUrl)
1644
+ return option;
1645
+ changed = true;
1646
+ return { ...option, avatarUrl: nextAvatarUrl };
1647
+ });
1648
+ return changed ? next : previous;
1649
+ });
1650
+ }, [authSessionResolved, authUserAvatarUrl, authUserId, isAuthenticated]);
1406
1651
  const currentWorkspaceRole = useMemo(() => {
1407
1652
  if (workspaceSwitchingEnabled) {
1408
1653
  const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
@@ -1417,44 +1662,86 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1417
1662
  }, [isAuthenticated]);
1418
1663
  const continueAfterCommercialOnboarding = useCallback(async () => {
1419
1664
  setBootstrapError(null);
1420
- markBootstrapPhase('auth');
1421
- const authenticated = await checkAuthSession({ force: true });
1422
- if (!authenticated) {
1423
- await fetchConfig({ ignoreAuthGuard: true });
1424
- return { success: false, destination: 'login', error: 'Sign in required.', code: 'AUTH_REQUIRED' };
1665
+ const result = await runCloudBootstrap({
1666
+ reason: 'post-plans',
1667
+ ignoreAuthGuard: true,
1668
+ tasksSilent: true,
1669
+ includeDeferredArchive: false
1670
+ });
1671
+ if (!result.authenticated) {
1672
+ return { success: false, destination: 'login', error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
1425
1673
  }
1426
- if (workspaceSwitchingEnabled) {
1427
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1674
+ if (!result.success) {
1675
+ return {
1676
+ success: false,
1677
+ destination: 'app',
1678
+ error: result.error || 'Unable to resolve workspace after plan selection.',
1679
+ code: result.code
1680
+ };
1681
+ }
1682
+ if (result.workspaceSetupRequired) {
1683
+ return { success: true, destination: 'setup' };
1684
+ }
1685
+ return { success: true, destination: 'app' };
1686
+ }, [
1687
+ runCloudBootstrap
1688
+ ]);
1689
+ const resolvePostAuthCloudEntry = useCallback(async (options) => {
1690
+ const reason = options?.reason || 'retry';
1691
+ const preferredWorkspaceId = typeof options?.preferredWorkspaceId === 'string' ? options.preferredWorkspaceId.trim() : '';
1692
+ const allowCommercialGate = options?.allowCommercialGate === true;
1693
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
1694
+ const authenticated = await checkAuthSession({ force: true });
1695
+ if (!authenticated) {
1696
+ await fetchConfig({ ignoreAuthGuard: true });
1697
+ return { success: false, error: 'Sign in required.', code: 'AUTH_REQUIRED' };
1698
+ }
1699
+ const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1428
1700
  if (!workspaceResolution.success) {
1429
1701
  await fetchConfig({ ignoreAuthGuard: true });
1430
1702
  return {
1431
1703
  success: false,
1432
- destination: 'app',
1433
- error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
1704
+ error: workspaceResolution.error || 'Unable to resolve workspace access.',
1434
1705
  code: workspaceResolution.code
1435
1706
  };
1436
1707
  }
1437
1708
  if (workspaceResolution.workspaceSetupRequired) {
1438
1709
  await fetchConfig({ ignoreAuthGuard: true });
1439
- return { success: true, destination: 'setup' };
1710
+ return { success: true, workspaceSetupRequired: true };
1440
1711
  }
1441
1712
  }
1442
- await fetchBootstrapConfig();
1443
- await Promise.all([
1444
- fetchTasks(true, { ignoreAuthGuard: true }),
1445
- fetchReferenceData(),
1446
- fetchPlanningEntities()
1447
- ]);
1448
- return { success: true, destination: 'app' };
1713
+ const result = await runCloudBootstrap({
1714
+ reason,
1715
+ ignoreAuthGuard: true,
1716
+ tasksSilent: true,
1717
+ includeDeferredArchive: false
1718
+ });
1719
+ if (!result.authenticated) {
1720
+ return { success: false, error: result.error || 'Sign in required.', code: result.code || 'AUTH_REQUIRED' };
1721
+ }
1722
+ if (!result.success) {
1723
+ return {
1724
+ success: false,
1725
+ error: result.error || 'Unable to resolve workspace access.',
1726
+ code: result.code
1727
+ };
1728
+ }
1729
+ if (result.workspaceSetupRequired) {
1730
+ if (allowCommercialGate) {
1731
+ return { success: true, workspaceSetupRequired: false };
1732
+ }
1733
+ return { success: true, workspaceSetupRequired: true };
1734
+ }
1735
+ if (!allowCommercialGate) {
1736
+ void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1737
+ }
1738
+ return { success: true, workspaceSetupRequired: false };
1449
1739
  }, [
1450
1740
  checkAuthSession,
1451
- fetchBootstrapConfig,
1452
1741
  fetchConfig,
1453
- fetchPlanningEntities,
1454
- fetchReferenceData,
1455
- fetchTasks,
1456
- markBootstrapPhase,
1457
1742
  resolveWorkspaceAfterAuth,
1743
+ runCloudBootstrap,
1744
+ syncUserGlobalSettings,
1458
1745
  workspaceSwitchingEnabled
1459
1746
  ]);
1460
1747
  const createWorkspace = useCallback(async (name, description) => {
@@ -1615,30 +1902,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1615
1902
  return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
1616
1903
  }
1617
1904
  setError('');
1618
- await checkAuthSession({ force: true });
1619
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1620
- if (!workspaceResolution.success) {
1621
- return {
1622
- success: false,
1623
- error: workspaceResolution.error || 'Unable to resolve workspace after sign in.',
1624
- code: workspaceResolution.code
1625
- };
1626
- }
1627
- if (workspaceResolution.workspaceSetupRequired) {
1628
- await fetchConfig({ ignoreAuthGuard: true });
1629
- return { success: true, workspaceSetupRequired: true };
1905
+ const entry = await resolvePostAuthCloudEntry();
1906
+ if (!entry.success) {
1907
+ return { success: false, error: entry.error || 'Unable to resolve workspace after sign in.', code: entry.code };
1630
1908
  }
1631
- await Promise.all([
1632
- fetchConfig({ ignoreAuthGuard: true }),
1633
- fetchTasks(true, { ignoreAuthGuard: true })
1634
- ]);
1635
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1636
- return { success: true };
1909
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1637
1910
  }
1638
1911
  catch {
1639
1912
  return { success: false, error: 'Sign in failed.' };
1640
1913
  }
1641
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
1914
+ }, [resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
1915
+ /**
1916
+ * Initiates an OAuth login (or invite-claim) flow by redirecting the browser
1917
+ * to the provider's authorisation URL via the server-side start route.
1918
+ *
1919
+ * @param provider - 'google' | 'github' | 'apple'
1920
+ * @param returnTo - Optional relative path to redirect back to after auth (defaults to '/').
1921
+ * @param inviteToken - Optional invite token; when provided the flow uses `invite_claim` mode.
1922
+ */
1923
+ const beginOAuthLogin = useCallback((provider, returnTo, inviteToken) => {
1924
+ const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
1925
+ ? returnTo
1926
+ : '/';
1927
+ let path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?return_to=${encodeURIComponent(sanitizedReturnTo)}`;
1928
+ if (inviteToken) {
1929
+ path += `&invite_token=${encodeURIComponent(inviteToken)}`;
1930
+ }
1931
+ window.location.href = resolveCloudAuthUrl(path);
1932
+ }, [resolveCloudAuthUrl]);
1642
1933
  const registerWithCredentials = useCallback(async (email, password, options) => {
1643
1934
  if (!cloudAuthConfigured) {
1644
1935
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1688,32 +1979,42 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1688
1979
  const verificationRequired = Boolean(data?.verificationRequired);
1689
1980
  if (!verificationRequired) {
1690
1981
  setError('');
1691
- await checkAuthSession({ force: true });
1692
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1693
- if (!workspaceResolution.success) {
1694
- return {
1695
- success: false,
1696
- error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
1697
- code: workspaceResolution.code
1698
- };
1699
- }
1700
1982
  const hasCommercialOnboardingGate = Boolean(planSelectionRequired
1701
1983
  || checkoutPending
1702
1984
  || commercialState === 'pending_plan_selection'
1703
1985
  || commercialState === 'checkout_pending');
1704
- if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
1705
- await fetchConfig({ ignoreAuthGuard: true });
1986
+ if (hasCommercialOnboardingGate) {
1987
+ const authenticated = await checkAuthSession({ force: true });
1988
+ if (!authenticated) {
1989
+ return {
1990
+ success: false,
1991
+ error: 'Unable to resolve workspace after registration.',
1992
+ code: 'AUTH_REQUIRED'
1993
+ };
1994
+ }
1995
+ return {
1996
+ success: true,
1997
+ workspaceSetupRequired: false,
1998
+ planSelectionRequired,
1999
+ checkoutPending,
2000
+ commercialState,
2001
+ verificationRequired,
2002
+ verificationToken: typeof data?.verificationToken === 'string' ? data.verificationToken : undefined,
2003
+ emailSent: data?.emailSent !== false,
2004
+ emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
2005
+ };
1706
2006
  }
1707
- else {
1708
- await Promise.all([
1709
- fetchConfig({ ignoreAuthGuard: true }),
1710
- fetchTasks(true, { ignoreAuthGuard: true })
1711
- ]);
1712
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
2007
+ const entry = await resolvePostAuthCloudEntry({ allowCommercialGate: false });
2008
+ if (!entry.success) {
2009
+ return {
2010
+ success: false,
2011
+ error: entry.error || 'Unable to resolve workspace after registration.',
2012
+ code: entry.code
2013
+ };
1713
2014
  }
1714
2015
  return {
1715
2016
  success: true,
1716
- workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
2017
+ workspaceSetupRequired: entry.workspaceSetupRequired === true,
1717
2018
  planSelectionRequired,
1718
2019
  checkoutPending,
1719
2020
  commercialState,
@@ -1738,7 +2039,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1738
2039
  catch {
1739
2040
  return { success: false, error: 'Create account failed.' };
1740
2041
  }
1741
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2042
+ }, [checkAuthSession, resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
1742
2043
  const updateCurrentUserProfile = useCallback(async (input) => {
1743
2044
  if (!cloudAuthConfigured) {
1744
2045
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1800,6 +2101,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1800
2101
  success: true,
1801
2102
  verificationToken: data?.verificationToken ?? null,
1802
2103
  emailSent: data?.emailSent !== false,
2104
+ deliveryAttempted: data?.deliveryAttempted === true
2105
+ || (typeof data?.verificationToken === 'string' && data.verificationToken.trim().length > 0),
1803
2106
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1804
2107
  };
1805
2108
  }
@@ -1853,6 +2156,8 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1853
2156
  success: true,
1854
2157
  resetToken: data?.resetToken ?? null,
1855
2158
  emailSent: data?.emailSent !== false,
2159
+ deliveryAttempted: data?.deliveryAttempted === true
2160
+ || (typeof data?.resetToken === 'string' && data.resetToken.trim().length > 0),
1856
2161
  emailError: typeof data?.emailError === 'string' ? data.emailError : undefined
1857
2162
  };
1858
2163
  }
@@ -1915,7 +2220,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1915
2220
  email: typeof data?.email === 'string' ? data.email : undefined,
1916
2221
  workspaceId: typeof data?.workspaceId === 'string' ? data.workspaceId : undefined,
1917
2222
  inviteeKind: data?.inviteeKind === 'existing_user' ? 'existing_user' : 'new_user',
1918
- passwordRequired: data?.passwordRequired === true
2223
+ passwordRequired: data?.passwordRequired === true,
2224
+ inviteeState: data?.inviteeState === 'existing_account' ? 'existing_account' : 'pending_setup',
2225
+ availableMethods: Array.isArray(data?.availableMethods) ? data.availableMethods : []
1919
2226
  };
1920
2227
  }
1921
2228
  catch {
@@ -1943,30 +2250,30 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1943
2250
  return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
1944
2251
  }
1945
2252
  setError('');
1946
- await checkAuthSession({ force: true });
1947
2253
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
1948
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1949
- if (!workspaceResolution.success) {
1950
- return {
1951
- success: false,
1952
- error: workspaceResolution.error || 'Unable to resolve workspace after invite acceptance.',
1953
- code: workspaceResolution.code
1954
- };
2254
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
2255
+ const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
2256
+ if (!switched.success) {
2257
+ return {
2258
+ success: false,
2259
+ error: switched.error || 'Unable to resolve workspace after invite acceptance.',
2260
+ code: switched.code
2261
+ };
2262
+ }
1955
2263
  }
1956
- if (workspaceResolution.workspaceSetupRequired) {
1957
- return { success: true, workspaceSetupRequired: true };
2264
+ const entry = await resolvePostAuthCloudEntry();
2265
+ if (!entry.success) {
2266
+ return { success: false, error: entry.error || 'Unable to resolve workspace after invite acceptance.', code: entry.code };
1958
2267
  }
1959
- await Promise.all([
1960
- fetchConfig({ ignoreAuthGuard: true }),
1961
- fetchTasks(true, { ignoreAuthGuard: true })
1962
- ]);
1963
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1964
- return { success: true };
2268
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2269
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2270
+ }
2271
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1965
2272
  }
1966
2273
  catch {
1967
2274
  return { success: false, error: 'Invite acceptance failed.' };
1968
2275
  }
1969
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2276
+ }, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
1970
2277
  const joinInviteWithToken = useCallback(async (token) => {
1971
2278
  if (!cloudAuthConfigured) {
1972
2279
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1987,30 +2294,93 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1987
2294
  return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
1988
2295
  }
1989
2296
  setError('');
1990
- await checkAuthSession();
1991
2297
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
1992
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1993
- if (!workspaceResolution.success) {
1994
- return {
1995
- success: false,
1996
- error: workspaceResolution.error || 'Unable to resolve workspace after invite join.',
1997
- code: workspaceResolution.code
1998
- };
2298
+ if (preferredWorkspaceId && workspaceSwitchingEnabled) {
2299
+ const switched = await switchWorkspace(preferredWorkspaceId, { hydrate: false });
2300
+ if (!switched.success) {
2301
+ return {
2302
+ success: false,
2303
+ error: switched.error || 'Unable to resolve workspace after invite join.',
2304
+ code: switched.code
2305
+ };
2306
+ }
1999
2307
  }
2000
- if (workspaceResolution.workspaceSetupRequired) {
2001
- return { success: true, workspaceSetupRequired: true };
2308
+ const entry = await resolvePostAuthCloudEntry();
2309
+ if (!entry.success) {
2310
+ return { success: false, error: entry.error || 'Unable to resolve workspace after invite join.', code: entry.code };
2002
2311
  }
2003
- await Promise.all([
2004
- fetchConfig({ ignoreAuthGuard: true }),
2005
- fetchTasks(true, { ignoreAuthGuard: true })
2006
- ]);
2007
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
2008
- return { success: true };
2312
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2313
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2314
+ }
2315
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
2009
2316
  }
2010
2317
  catch {
2011
2318
  return { success: false, error: 'Invite join failed.' };
2012
2319
  }
2013
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2320
+ }, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
2321
+ // ─── Login method management ────────────────────────────────────────────────
2322
+ const fetchLoginMethods = useCallback(async () => {
2323
+ if (!cloudAuthConfigured)
2324
+ return { success: false, error: 'Cloud auth not configured.' };
2325
+ try {
2326
+ const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods'), {
2327
+ credentials: 'include'
2328
+ });
2329
+ const data = await res.json().catch(() => ({}));
2330
+ if (!res.ok || !data?.success) {
2331
+ return { success: false, error: data?.error || 'Failed to fetch login methods.' };
2332
+ }
2333
+ return { success: true, methods: Array.isArray(data?.methods) ? data.methods : [] };
2334
+ }
2335
+ catch {
2336
+ return { success: false, error: 'Failed to fetch login methods.' };
2337
+ }
2338
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2339
+ const unlinkLoginMethod = useCallback(async (provider) => {
2340
+ if (!cloudAuthConfigured)
2341
+ return { success: false, error: 'Cloud auth not configured.' };
2342
+ try {
2343
+ const res = await fetch(resolveCloudAuthUrl(`/api/taskforce/account/login-methods/${encodeURIComponent(provider)}`), {
2344
+ method: 'DELETE',
2345
+ credentials: 'include'
2346
+ });
2347
+ const data = await res.json().catch(() => ({}));
2348
+ if (!res.ok || !data?.success) {
2349
+ return { success: false, error: data?.error || 'Unlink failed.', code: data?.code };
2350
+ }
2351
+ return { success: true };
2352
+ }
2353
+ catch {
2354
+ return { success: false, error: 'Unlink failed.' };
2355
+ }
2356
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2357
+ const addPasswordToAccount = useCallback(async (password) => {
2358
+ if (!cloudAuthConfigured)
2359
+ return { success: false, error: 'Cloud auth not configured.' };
2360
+ try {
2361
+ const res = await fetch(resolveCloudAuthUrl('/api/taskforce/account/login-methods/password'), {
2362
+ method: 'POST',
2363
+ headers: { 'Content-Type': 'application/json' },
2364
+ credentials: 'include',
2365
+ body: JSON.stringify({ password })
2366
+ });
2367
+ const data = await res.json().catch(() => ({}));
2368
+ if (!res.ok || !data?.success) {
2369
+ return { success: false, error: data?.error || 'Failed to add password.', code: data?.code };
2370
+ }
2371
+ return { success: true };
2372
+ }
2373
+ catch {
2374
+ return { success: false, error: 'Failed to add password.' };
2375
+ }
2376
+ }, [resolveCloudAuthUrl, cloudAuthConfigured]);
2377
+ const beginOAuthLink = useCallback((provider, returnTo) => {
2378
+ const sanitizedReturnTo = returnTo && /^\/[A-Za-z0-9_\-./?=&%]*$/.test(returnTo)
2379
+ ? returnTo
2380
+ : '/';
2381
+ const path = `/api/taskforce/auth/oauth/${encodeURIComponent(provider)}/start?flow=link&return_to=${encodeURIComponent(sanitizedReturnTo)}`;
2382
+ window.location.href = resolveCloudAuthUrl(path);
2383
+ }, [resolveCloudAuthUrl]);
2014
2384
  const resetCloudLogoutClientState = useCallback(() => {
2015
2385
  abortWorkspaceScopedRequests();
2016
2386
  referenceDataRequestRef.current = null;
@@ -2029,6 +2399,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2029
2399
  setArchivedTasks([]);
2030
2400
  setInitiatives([]);
2031
2401
  setWorkstreams([]);
2402
+ setPlanningBootstrapTaskSummaries([]);
2032
2403
  setLoadingTasks(false);
2033
2404
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
2034
2405
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -2139,6 +2510,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2139
2510
  throw new Error(data?.error || `Failed to create initiative (${res.status})`);
2140
2511
  }
2141
2512
  await fetchPlanningEntities();
2513
+ setInitiatives((current) => upsertEntityById(current, data));
2142
2514
  return data;
2143
2515
  }, [fetchPlanningEntities]);
2144
2516
  const updateInitiative = useCallback(async (id, payload) => {
@@ -2152,6 +2524,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2152
2524
  throw new Error(data?.error || `Failed to update initiative (${res.status})`);
2153
2525
  }
2154
2526
  await fetchPlanningEntities();
2527
+ setInitiatives((current) => upsertEntityById(current, data));
2155
2528
  return data;
2156
2529
  }, [fetchPlanningEntities]);
2157
2530
  const archiveInitiative = useCallback(async (id) => {
@@ -2163,6 +2536,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2163
2536
  throw new Error(data?.error || `Failed to archive initiative (${res.status})`);
2164
2537
  }
2165
2538
  await fetchPlanningEntities();
2539
+ setInitiatives((current) => upsertEntityById(current, data));
2166
2540
  return data;
2167
2541
  }, [fetchPlanningEntities]);
2168
2542
  const unarchiveInitiative = useCallback(async (id) => {
@@ -2174,6 +2548,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2174
2548
  throw new Error(data?.error || `Failed to unarchive initiative (${res.status})`);
2175
2549
  }
2176
2550
  await fetchPlanningEntities();
2551
+ setInitiatives((current) => upsertEntityById(current, data));
2177
2552
  return data;
2178
2553
  }, [fetchPlanningEntities]);
2179
2554
  const createWorkstream = useCallback(async (payload) => {
@@ -2187,6 +2562,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2187
2562
  throw new Error(data?.error || `Failed to create workstream (${res.status})`);
2188
2563
  }
2189
2564
  await fetchPlanningEntities();
2565
+ setWorkstreams((current) => upsertEntityById(current, data));
2190
2566
  return data;
2191
2567
  }, [fetchPlanningEntities]);
2192
2568
  const updateWorkstream = useCallback(async (id, payload) => {
@@ -2200,6 +2576,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2200
2576
  throw new Error(data?.error || `Failed to update workstream (${res.status})`);
2201
2577
  }
2202
2578
  await fetchPlanningEntities();
2579
+ setWorkstreams((current) => upsertEntityById(current, data));
2203
2580
  return data;
2204
2581
  }, [fetchPlanningEntities]);
2205
2582
  const archiveWorkstream = useCallback(async (id) => {
@@ -2211,6 +2588,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2211
2588
  throw new Error(data?.error || `Failed to archive workstream (${res.status})`);
2212
2589
  }
2213
2590
  await fetchPlanningEntities();
2591
+ setWorkstreams((current) => upsertEntityById(current, data));
2214
2592
  return data;
2215
2593
  }, [fetchPlanningEntities]);
2216
2594
  const unarchiveWorkstream = useCallback(async (id) => {
@@ -2222,6 +2600,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2222
2600
  throw new Error(data?.error || `Failed to unarchive workstream (${res.status})`);
2223
2601
  }
2224
2602
  await fetchPlanningEntities();
2603
+ setWorkstreams((current) => upsertEntityById(current, data));
2225
2604
  return data;
2226
2605
  }, [fetchPlanningEntities]);
2227
2606
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
@@ -2238,6 +2617,38 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2238
2617
  return;
2239
2618
  void checkAuthSession();
2240
2619
  }, [runtimeConfigReady, checkAuthSession]);
2620
+ // Fetch available OAuth providers once we know the auth endpoint.
2621
+ // Uses a short timeout so an unreachable auth server doesn't hold open
2622
+ // browser connections and slow down subsequent requests on the same origin.
2623
+ useEffect(() => {
2624
+ if (!cloudAuthConfigured)
2625
+ return;
2626
+ let cancelled = false;
2627
+ const controller = new AbortController();
2628
+ const timerId = typeof window !== 'undefined'
2629
+ ? window.setTimeout(() => controller.abort(), 5000)
2630
+ : null;
2631
+ fetch(resolveCloudAuthUrl('/api/taskforce/auth/providers'), {
2632
+ credentials: 'include',
2633
+ signal: controller.signal
2634
+ })
2635
+ .then(r => r.ok ? r.json() : null)
2636
+ .then(data => {
2637
+ if (!cancelled && Array.isArray(data?.providers)) {
2638
+ setAvailableAuthProviders(data.providers);
2639
+ }
2640
+ })
2641
+ .catch(() => { })
2642
+ .finally(() => {
2643
+ if (timerId !== null && typeof window !== 'undefined') {
2644
+ window.clearTimeout(timerId);
2645
+ }
2646
+ });
2647
+ return () => {
2648
+ cancelled = true;
2649
+ controller.abort();
2650
+ };
2651
+ }, [cloudAuthConfigured, resolveCloudAuthUrl]);
2241
2652
  // Keep local runtime auth state aligned with cloud session changes
2242
2653
  // (e.g. user signs in/out in another tab or cloud-hosted app).
2243
2654
  useEffect(() => {
@@ -2261,109 +2672,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2261
2672
  window.clearInterval(intervalId);
2262
2673
  };
2263
2674
  }, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
2264
- useEffect(() => {
2265
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2266
- return;
2267
- if (hasBootstrappedDataRef.current)
2268
- return;
2269
- hasBootstrappedDataRef.current = true;
2270
- void (async () => {
2271
- if (runtimeMode === 'cloud' && authSessionResolved && isAuthenticated) {
2272
- const workspaceResolution = await resolveWorkspaceAfterAuth();
2273
- if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
2274
- await fetchBootstrapConfig();
2275
- void fetchReferenceData();
2276
- return;
2277
- }
2278
- }
2279
- await fetchBootstrapConfig();
2280
- await Promise.all([
2281
- (async () => {
2282
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2283
- await fetchTasks(false);
2284
- logBootstrapDebug('bootstrap_tasks_loaded', {
2285
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2286
- });
2287
- })(),
2288
- fetchReferenceData(),
2289
- fetchPlanningEntities()
2290
- ]);
2291
- if (typeof window !== 'undefined') {
2292
- if (archiveBootstrapTimerRef.current !== null) {
2293
- window.clearTimeout(archiveBootstrapTimerRef.current);
2294
- }
2295
- archiveBootstrapTimerRef.current = window.setTimeout(() => {
2296
- archiveBootstrapTimerRef.current = null;
2297
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2298
- void fetchArchive(true).then(() => {
2299
- logBootstrapDebug('bootstrap_archive_loaded_deferred', {
2300
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2301
- });
2302
- });
2303
- }, 1500);
2304
- }
2305
- })();
2306
- }, [
2307
- shouldDeferProtectedApiCalls,
2308
- shouldBlockProtectedApiCalls,
2309
- runtimeMode,
2310
- authSessionResolved,
2311
- isAuthenticated,
2312
- resolveWorkspaceAfterAuth,
2313
- fetchBootstrapConfig,
2314
- fetchTasks,
2315
- fetchArchive,
2316
- fetchReferenceData,
2317
- fetchPlanningEntities
2318
- ]);
2319
- useEffect(() => {
2320
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2321
- return;
2322
- if (workspaceBootstrapPending)
2323
- return;
2324
- if (workspaceSelectionScope === WORKSPACE_BOOTSTRAP_SCOPE)
2325
- return;
2326
- if (!hasBootstrappedDataRef.current)
2327
- return;
2328
- const loadKey = `${runtimeMode}:${currentWorkspaceId}:${workspaceSelectionScope}`;
2329
- if (lastConfigRefreshKeyRef.current === loadKey)
2330
- return;
2331
- lastConfigRefreshKeyRef.current = loadKey;
2332
- void fetchConfig();
2333
- }, [
2334
- currentWorkspaceId,
2335
- shouldDeferProtectedApiCalls,
2336
- shouldBlockProtectedApiCalls,
2337
- workspaceBootstrapPending,
2338
- runtimeMode,
2339
- workspaceSelectionScope,
2340
- fetchConfig
2341
- ]);
2342
- useEffect(() => {
2343
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2344
- return;
2345
- if (workspaceBootstrapPending)
2346
- return;
2347
- const loadKey = currentWorkspaceId;
2348
- if (lastWorkspaceSyncStateLoadKeyRef.current === loadKey)
2349
- return;
2350
- lastWorkspaceSyncStateLoadKeyRef.current = loadKey;
2351
- void loadWorkspaceSyncState();
2352
- }, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls, currentWorkspaceId, loadWorkspaceSyncState, workspaceBootstrapPending]);
2353
- useEffect(() => {
2354
- if (runtimeMode !== 'cloud' || !authSessionResolved || !isAuthenticated) {
2355
- if (runtimeMode !== 'cloud') {
2356
- setAvailableWorkspaces([]);
2357
- }
2358
- workspaceListHydratedRef.current = false;
2359
- return;
2360
- }
2361
- if (workspaceBootstrapPending)
2362
- return;
2363
- if (!hasBootstrappedDataRef.current || workspaceListHydratedRef.current)
2364
- return;
2365
- fetchWorkspaces();
2366
- }, [runtimeMode, authSessionResolved, isAuthenticated, fetchWorkspaces, workspaceBootstrapPending]);
2367
2675
  // Report task count to parent
2368
2676
  useEffect(() => {
2369
2677
  if (onTaskCountChange) {
@@ -2529,6 +2837,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2529
2837
  return;
2530
2838
  lastTaskSurfaceLoadKeyRef.current = loadKey;
2531
2839
  fetchTasks();
2840
+ fetchPlanningEntities();
2532
2841
  if (showArchive || taskScope === 'archived') {
2533
2842
  fetchArchive(true);
2534
2843
  }
@@ -2555,6 +2864,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2555
2864
  showArchive,
2556
2865
  taskScope,
2557
2866
  fetchTasks,
2867
+ fetchPlanningEntities,
2558
2868
  fetchArchive,
2559
2869
  fetchDeletedTasks,
2560
2870
  fetchConfig,
@@ -2723,6 +3033,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2723
3033
  pushWorkspaceChangesToCloud,
2724
3034
  workspacePendingSignatureRef,
2725
3035
  workspaceDeletedTaskIdsRef,
3036
+ workspaceDeletedTaskWatermarksRef,
2726
3037
  });
2727
3038
  const { autoSaveState, flushAutoSave, scheduleWarningPrompt, handleSubmit, confirmScheduleWarning, cancelScheduleWarning, handleAddComment } = useTaskFormActions({
2728
3039
  activeCategories,
@@ -2992,6 +3303,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2992
3303
  workspaceLastErrorMessage,
2993
3304
  workspaceLastSuccessfulSyncAt,
2994
3305
  workspaceSyncPhase,
3306
+ workspaceSyncSetupIntent,
2995
3307
  workspaceSyncStatus,
2996
3308
  workspaceSyncSummary,
2997
3309
  workspaceSyncRecommendedAction,
@@ -3011,6 +3323,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3011
3323
  workspaceCloudSyncEnabled,
3012
3324
  saveWorkspaceCloudSyncSettings,
3013
3325
  applyWorkspaceSyncStateSnapshot,
3326
+ bootstrapState,
3014
3327
  authSessionResolved,
3015
3328
  workspaceBootstrapPending,
3016
3329
  bootstrapPhase,
@@ -3027,6 +3340,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3027
3340
  deleteWorkspace,
3028
3341
  switchWorkspace,
3029
3342
  loginWithCredentials,
3343
+ beginOAuthLogin,
3344
+ availableAuthProviders,
3345
+ fetchLoginMethods,
3346
+ unlinkLoginMethod,
3347
+ addPasswordToAccount,
3348
+ beginOAuthLink,
3030
3349
  registerWithCredentials,
3031
3350
  updateCurrentUserProfile,
3032
3351
  requestEmailVerification,
@@ -3059,6 +3378,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3059
3378
  archivedTasks,
3060
3379
  initiatives,
3061
3380
  workstreams,
3381
+ planningBootstrapTaskSummaries,
3062
3382
  deletedTasks,
3063
3383
  activeCategories,
3064
3384
  activeTypes,
@@ -3081,6 +3401,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3081
3401
  setFilterAssignees,
3082
3402
  filterTaxonomies,
3083
3403
  setFilterTaxonomies,
3404
+ hasInitedFilters,
3084
3405
  sortBy,
3085
3406
  setSortBy,
3086
3407
  sortOrder,