@taskforcehq/taskforce 0.3.305 → 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 (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. 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
  : '';
@@ -236,6 +319,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
236
319
  || path.startsWith('/api/taskforce/sync/')
237
320
  || path.startsWith('/api/taskforce/settings/mcp/');
238
321
  if (!isCloudHost && isCloudScopedPath) {
322
+ if (runtimeMode === 'cloud') {
323
+ return path;
324
+ }
239
325
  if (!runtimeConfigReady || runtimeConfigOverride.cloudAuthViaLocalProxy) {
240
326
  return path;
241
327
  }
@@ -263,7 +349,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
263
349
  if (!isCloudHost && isCloudScopedPath)
264
350
  return path;
265
351
  return path;
266
- }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost]);
352
+ }, [runtimeConfigReady, runtimeConfigOverride.cloudAuthViaLocalProxy, runtimeConfigOverride.workspaceMode, normalizedCloudAuthBaseUrl, normalizedApiBaseUrl, isCloudHost, runtimeMode]);
267
353
  const resolveWebSocketUrl = useCallback((path) => {
268
354
  const wsPath = String(path || '').trim() || '/taskforce-ws';
269
355
  const normalizedPath = wsPath.startsWith('/') ? wsPath : `/${wsPath}`;
@@ -313,7 +399,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
313
399
  }), [projectRoot, initialRuntimeMode]);
314
400
  const [mcpScriptPath, setMcpScriptPath] = useState('');
315
401
  const [tenantId, setTenantId] = useState('');
316
- const [runtimeMode, setRuntimeMode] = useState(() => initialRuntimeMode);
317
402
  const runtimeContractFallback = resolveRuntimeModeContract(runtimeMode);
318
403
  const workspaceMode = runtimeConfigOverride.workspaceMode === 'single-local' || runtimeConfigOverride.workspaceMode === 'multi-cloud'
319
404
  ? runtimeConfigOverride.workspaceMode
@@ -328,6 +413,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
328
413
  const [authWorkspaceId, setAuthWorkspaceId] = useState('');
329
414
  const [authUserEmail, setAuthUserEmail] = useState('');
330
415
  const [authUserDisplayName, setAuthUserDisplayName] = useState('');
416
+ const [availableAuthProviders, setAvailableAuthProviders] = useState([]);
331
417
  const [authUserAvatarUrl, setAuthUserAvatarUrl] = useState('');
332
418
  const [hasBetaAccess, setHasBetaAccess] = useState(true);
333
419
  const [hydratedWorkspaceRole, setHydratedWorkspaceRole] = useState(null);
@@ -358,6 +444,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
358
444
  const currentWorkspaceIdRef = useRef(currentWorkspaceId);
359
445
  const explicitWorkspaceSelectionRef = useRef(null);
360
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
+ })());
361
453
  const [workspaceTransitionEpoch, setWorkspaceTransitionEpoch] = useState(0);
362
454
  const bootstrapConfigAbortRef = useRef(null);
363
455
  const referenceDataAbortRef = useRef(null);
@@ -365,6 +457,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
365
457
  const assigneeOptionsAbortRef = useRef(null);
366
458
  const deletedTasksAbortRef = useRef(null);
367
459
  const planningEntitiesAbortRef = useRef(null);
460
+ const planningEntitiesRetryTimerRef = useRef(null);
368
461
  const commitCurrentWorkspaceId = useCallback((nextWorkspaceIdRaw, options) => {
369
462
  const nextWorkspaceId = String(nextWorkspaceIdRaw || '').trim();
370
463
  if (!nextWorkspaceId)
@@ -409,9 +502,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
409
502
  return workspaceTransitionEpochRef.current !== requestState.epoch;
410
503
  }, []);
411
504
  const isAbortError = useCallback((error) => {
412
- return error instanceof DOMException
413
- ? error.name === 'AbortError'
414
- : String(error?.name || '').toLowerCase() === 'aborterror';
505
+ return hasAbortErrorName(error) || isExpectedWorkspaceAbortReason(error);
415
506
  }, []);
416
507
  const abortWorkspaceScopedRequests = useCallback(() => {
417
508
  [
@@ -425,6 +516,10 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
425
516
  ref.current?.abort('workspace-transition');
426
517
  ref.current = null;
427
518
  });
519
+ if (planningEntitiesRetryTimerRef.current !== null && typeof window !== 'undefined') {
520
+ window.clearTimeout(planningEntitiesRetryTimerRef.current);
521
+ planningEntitiesRetryTimerRef.current = null;
522
+ }
428
523
  referenceDataRequestRef.current = null;
429
524
  }, []);
430
525
  // Local runtime APIs must stay available immediately on app start, even when cloud
@@ -504,6 +599,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
504
599
  const [archivedTasks, setArchivedTasks] = useState([]);
505
600
  const [initiatives, setInitiatives] = useState([]);
506
601
  const [workstreams, setWorkstreams] = useState([]);
602
+ const [planningBootstrapTaskSummaries, setPlanningBootstrapTaskSummaries] = useState([]);
507
603
  const [deletedTasks, setDeletedTasks] = useState([]);
508
604
  const [loadingTasks, setLoadingTasks] = useState(false);
509
605
  const [editingTaskId, setEditingTaskId] = useState(null);
@@ -545,6 +641,14 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
545
641
  const abortController = new AbortController();
546
642
  const externalSignal = options?.signal;
547
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;
548
652
  const timerId = typeof window !== 'undefined'
549
653
  ? window.setTimeout(() => abortController.abort('bootstrap-timeout'), timeoutLabel)
550
654
  : null;
@@ -561,18 +665,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
561
665
  }
562
666
  try {
563
667
  return await fetch(url, {
564
- ...(init || {}),
668
+ ...(tracedInit || {}),
565
669
  signal: abortController.signal
566
670
  });
567
671
  }
568
672
  catch (error) {
569
- const isAbortError = error instanceof DOMException
570
- ? error.name === 'AbortError'
571
- : String(error?.name || '').toLowerCase() === 'aborterror';
572
- if (isAbortError) {
573
- const timeoutError = new Error(`Startup checks timed out after ${timeoutLabel}ms.`);
574
- timeoutError.name = 'BootstrapTimeoutError';
575
- 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);
576
678
  }
577
679
  throw error;
578
680
  }
@@ -584,7 +686,50 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
584
686
  window.clearTimeout(timerId);
585
687
  }
586
688
  }
587
- }, []);
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
+ ]);
588
733
  const isBootstrapTimeoutError = useCallback((error) => {
589
734
  return String(error?.name || '') === 'BootstrapTimeoutError';
590
735
  }, []);
@@ -653,7 +798,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
653
798
  useEffect(() => {
654
799
  if (workspaceTransitionEpoch === 0)
655
800
  return;
656
- abortWorkspaceScopedRequests();
657
801
  referenceDataRequestRef.current = null;
658
802
  lastLoadedUiStateKeyRef.current = '';
659
803
  lastAssigneeOptionsLoadKeyRef.current = '';
@@ -668,8 +812,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
668
812
  setDeletedTasks([]);
669
813
  setTasks([]);
670
814
  setArchivedTasks([]);
671
- setInitiatives([]);
672
- setWorkstreams([]);
673
815
  setLoadingTasks(false);
674
816
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
675
817
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -679,7 +821,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
679
821
  window.clearTimeout(archiveBootstrapTimerRef.current);
680
822
  archiveBootstrapTimerRef.current = null;
681
823
  }
682
- }, [abortWorkspaceScopedRequests, setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
824
+ }, [setArchivedTasks, setLoadingTasks, setTasks, workspaceTransitionEpoch]);
683
825
  const fetchDeletedTasks = useCallback(async (isSilent = false, options) => {
684
826
  const ignoreAuthGuard = options?.ignoreAuthGuard === true;
685
827
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
@@ -727,7 +869,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
727
869
  }
728
870
  }
729
871
  catch (err) {
730
- if (isAbortError(err))
872
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
731
873
  return;
732
874
  console.error('[Taskforce] Failed to fetch deleted tasks:', err);
733
875
  }
@@ -751,37 +893,105 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
751
893
  if (!ignoreAuthGuard && (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls))
752
894
  return;
753
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
+ }
754
901
  const abortController = new AbortController();
755
902
  planningEntitiesAbortRef.current = abortController;
756
903
  try {
757
- const [initiativesRes, workstreamsRes] = await Promise.all([
758
- fetch('/api/taskforce/initiatives', { signal: abortController.signal }),
759
- fetch('/api/taskforce/workstreams', { signal: abortController.signal }),
760
- ]);
761
- 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) {
762
941
  handleUnauthorized();
763
942
  setInitiatives([]);
764
943
  setWorkstreams([]);
944
+ setPlanningBootstrapTaskSummaries([]);
765
945
  return;
766
946
  }
767
- const [initiativesData, workstreamsData] = await Promise.all([
768
- initiativesRes.ok ? initiativesRes.json().catch(() => []) : [],
769
- workstreamsRes.ok ? workstreamsRes.json().catch(() => []) : [],
770
- ]);
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
+ }
771
975
  if (isWorkspaceRequestStale(requestState)) {
772
976
  return;
773
977
  }
774
- setInitiatives(Array.isArray(initiativesData)
775
- ? initiativesData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
776
- : []);
777
- setWorkstreams(Array.isArray(workstreamsData)
778
- ? workstreamsData.filter((item) => Boolean(item && typeof item === 'object' && typeof item.id === 'string'))
779
- : []);
978
+ setInitiatives(bootstrapPayload.initiatives);
979
+ setWorkstreams(bootstrapPayload.workstreams);
980
+ setPlanningBootstrapTaskSummaries(bootstrapPayload.workstreamTaskSummaries);
780
981
  }
781
982
  catch (err) {
782
- if (isAbortError(err))
983
+ if (isExpectedWorkspaceAbortFailure(err, abortController.signal) || isAbortError(err))
783
984
  return;
784
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
+ }
785
995
  }
786
996
  finally {
787
997
  if (planningEntitiesAbortRef.current === abortController) {
@@ -789,12 +999,16 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
789
999
  }
790
1000
  }
791
1001
  }, [
1002
+ BOOTSTRAP_REQUEST_TIMEOUT_MS,
1003
+ PLANNING_BOOTSTRAP_REQUEST_TIMEOUT_MS,
1004
+ initiatives.length,
792
1005
  getWorkspaceRequestState,
793
1006
  handleUnauthorized,
794
1007
  isAbortError,
795
1008
  isWorkspaceRequestStale,
796
1009
  shouldDeferProtectedApiCalls,
797
1010
  shouldBlockProtectedApiCalls,
1011
+ workstreams.length,
798
1012
  ]);
799
1013
  const fetchTasks = useCallback(async (isSilent = false, options) => {
800
1014
  await fetchTasksBase(isSilent, options);
@@ -1323,7 +1537,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1323
1537
  }
1324
1538
  }
1325
1539
  }, [fetchWithTimeout, isBootstrapTimeoutError, markBootstrapStalled]);
1326
- const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks } = useTaskforceWorkspaceBootstrap({
1540
+ const { fetchReferenceData, fetchBootstrapConfig, fetchConfig, fetchWorkspaces, fetchAssigneeOptions, switchWorkspace, resolveWorkspaceAfterAuth, retryBootstrapChecks, runCloudBootstrap } = useTaskforceWorkspaceBootstrap({
1327
1541
  shouldDeferProtectedApiCalls,
1328
1542
  shouldBlockProtectedApiCalls,
1329
1543
  getWorkspaceRequestState,
@@ -1363,6 +1577,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1363
1577
  authSessionResolved,
1364
1578
  isAuthenticated,
1365
1579
  authUserId,
1580
+ authUserAvatarUrl,
1366
1581
  setHydratedWorkspaceRole,
1367
1582
  setAssigneeOptions,
1368
1583
  setAssigneeOptionsLoaded,
@@ -1388,6 +1603,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1388
1603
  showArchive,
1389
1604
  taskScope,
1390
1605
  settingsSection,
1606
+ workspaceBootstrapPending,
1391
1607
  fetchTasks,
1392
1608
  fetchArchive,
1393
1609
  fetchPlanningEntities,
@@ -1403,12 +1619,35 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1403
1619
  useEffect(() => {
1404
1620
  if (!configLoaded || workspaceBootstrapPending)
1405
1621
  return;
1406
- const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}`;
1622
+ const loadKey = `${runtimeMode}:${isAuthenticated ? 'auth' : 'guest'}:${currentWorkspaceId}:${authUserAvatarUrl || 'no-avatar'}`;
1407
1623
  if (assigneeOptionsLoaded && lastAssigneeOptionsLoadKeyRef.current === loadKey)
1408
1624
  return;
1409
1625
  lastAssigneeOptionsLoadKeyRef.current = loadKey;
1410
1626
  void fetchAssigneeOptions();
1411
- }, [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]);
1412
1651
  const currentWorkspaceRole = useMemo(() => {
1413
1652
  if (workspaceSwitchingEnabled) {
1414
1653
  const current = availableWorkspaces.find((workspace) => workspace.id === currentWorkspaceId);
@@ -1423,44 +1662,86 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1423
1662
  }, [isAuthenticated]);
1424
1663
  const continueAfterCommercialOnboarding = useCallback(async () => {
1425
1664
  setBootstrapError(null);
1426
- markBootstrapPhase('auth');
1427
- const authenticated = await checkAuthSession({ force: true });
1428
- if (!authenticated) {
1429
- await fetchConfig({ ignoreAuthGuard: true });
1430
- 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' };
1431
1673
  }
1432
- if (workspaceSwitchingEnabled) {
1433
- 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 });
1434
1700
  if (!workspaceResolution.success) {
1435
1701
  await fetchConfig({ ignoreAuthGuard: true });
1436
1702
  return {
1437
1703
  success: false,
1438
- destination: 'app',
1439
- error: workspaceResolution.error || 'Unable to resolve workspace after plan selection.',
1704
+ error: workspaceResolution.error || 'Unable to resolve workspace access.',
1440
1705
  code: workspaceResolution.code
1441
1706
  };
1442
1707
  }
1443
1708
  if (workspaceResolution.workspaceSetupRequired) {
1444
1709
  await fetchConfig({ ignoreAuthGuard: true });
1445
- return { success: true, destination: 'setup' };
1710
+ return { success: true, workspaceSetupRequired: true };
1446
1711
  }
1447
1712
  }
1448
- await fetchBootstrapConfig();
1449
- await Promise.all([
1450
- fetchTasks(true, { ignoreAuthGuard: true }),
1451
- fetchReferenceData(),
1452
- fetchPlanningEntities()
1453
- ]);
1454
- 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 };
1455
1739
  }, [
1456
1740
  checkAuthSession,
1457
- fetchBootstrapConfig,
1458
1741
  fetchConfig,
1459
- fetchPlanningEntities,
1460
- fetchReferenceData,
1461
- fetchTasks,
1462
- markBootstrapPhase,
1463
1742
  resolveWorkspaceAfterAuth,
1743
+ runCloudBootstrap,
1744
+ syncUserGlobalSettings,
1464
1745
  workspaceSwitchingEnabled
1465
1746
  ]);
1466
1747
  const createWorkspace = useCallback(async (name, description) => {
@@ -1621,30 +1902,34 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1621
1902
  return { success: false, error: data?.error || 'Sign in failed.', code: data?.code };
1622
1903
  }
1623
1904
  setError('');
1624
- await checkAuthSession({ force: true });
1625
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1626
- if (!workspaceResolution.success) {
1627
- return {
1628
- success: false,
1629
- error: workspaceResolution.error || 'Unable to resolve workspace after sign in.',
1630
- code: workspaceResolution.code
1631
- };
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 };
1632
1908
  }
1633
- if (workspaceResolution.workspaceSetupRequired) {
1634
- await fetchConfig({ ignoreAuthGuard: true });
1635
- return { success: true, workspaceSetupRequired: true };
1636
- }
1637
- await Promise.all([
1638
- fetchConfig({ ignoreAuthGuard: true }),
1639
- fetchTasks(true, { ignoreAuthGuard: true })
1640
- ]);
1641
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1642
- return { success: true };
1909
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1643
1910
  }
1644
1911
  catch {
1645
1912
  return { success: false, error: 'Sign in failed.' };
1646
1913
  }
1647
- }, [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]);
1648
1933
  const registerWithCredentials = useCallback(async (email, password, options) => {
1649
1934
  if (!cloudAuthConfigured) {
1650
1935
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1694,32 +1979,42 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1694
1979
  const verificationRequired = Boolean(data?.verificationRequired);
1695
1980
  if (!verificationRequired) {
1696
1981
  setError('');
1697
- await checkAuthSession({ force: true });
1698
- const workspaceResolution = await resolveWorkspaceAfterAuth();
1699
- if (!workspaceResolution.success) {
1700
- return {
1701
- success: false,
1702
- error: workspaceResolution.error || 'Unable to resolve workspace after registration.',
1703
- code: workspaceResolution.code
1704
- };
1705
- }
1706
1982
  const hasCommercialOnboardingGate = Boolean(planSelectionRequired
1707
1983
  || checkoutPending
1708
1984
  || commercialState === 'pending_plan_selection'
1709
1985
  || commercialState === 'checkout_pending');
1710
- if (workspaceResolution.workspaceSetupRequired || hasCommercialOnboardingGate) {
1711
- 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
+ };
1712
2006
  }
1713
- else {
1714
- await Promise.all([
1715
- fetchConfig({ ignoreAuthGuard: true }),
1716
- fetchTasks(true, { ignoreAuthGuard: true })
1717
- ]);
1718
- 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
+ };
1719
2014
  }
1720
2015
  return {
1721
2016
  success: true,
1722
- workspaceSetupRequired: workspaceResolution.workspaceSetupRequired,
2017
+ workspaceSetupRequired: entry.workspaceSetupRequired === true,
1723
2018
  planSelectionRequired,
1724
2019
  checkoutPending,
1725
2020
  commercialState,
@@ -1744,7 +2039,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1744
2039
  catch {
1745
2040
  return { success: false, error: 'Create account failed.' };
1746
2041
  }
1747
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2042
+ }, [checkAuthSession, resolveCloudAuthUrl, cloudAuthConfigured, resolvePostAuthCloudEntry]);
1748
2043
  const updateCurrentUserProfile = useCallback(async (input) => {
1749
2044
  if (!cloudAuthConfigured) {
1750
2045
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1925,7 +2220,9 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1925
2220
  email: typeof data?.email === 'string' ? data.email : undefined,
1926
2221
  workspaceId: typeof data?.workspaceId === 'string' ? data.workspaceId : undefined,
1927
2222
  inviteeKind: data?.inviteeKind === 'existing_user' ? 'existing_user' : 'new_user',
1928
- 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 : []
1929
2226
  };
1930
2227
  }
1931
2228
  catch {
@@ -1953,30 +2250,30 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1953
2250
  return { success: false, error: data?.error || 'Invite acceptance failed.', code: data?.code };
1954
2251
  }
1955
2252
  setError('');
1956
- await checkAuthSession({ force: true });
1957
2253
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
1958
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
1959
- if (!workspaceResolution.success) {
1960
- return {
1961
- success: false,
1962
- error: workspaceResolution.error || 'Unable to resolve workspace after invite acceptance.',
1963
- code: workspaceResolution.code
1964
- };
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
+ }
1965
2263
  }
1966
- if (workspaceResolution.workspaceSetupRequired) {
1967
- 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 };
1968
2267
  }
1969
- await Promise.all([
1970
- fetchConfig({ ignoreAuthGuard: true }),
1971
- fetchTasks(true, { ignoreAuthGuard: true })
1972
- ]);
1973
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
1974
- return { success: true };
2268
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2269
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2270
+ }
2271
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
1975
2272
  }
1976
2273
  catch {
1977
2274
  return { success: false, error: 'Invite acceptance failed.' };
1978
2275
  }
1979
- }, [checkAuthSession, fetchConfig, fetchTasks, resolveWorkspaceAfterAuth, syncUserGlobalSettings, resolveCloudAuthUrl, cloudAuthConfigured]);
2276
+ }, [commitCurrentWorkspaceId, resolveCloudAuthUrl, cloudAuthConfigured, currentWorkspaceIdRef, resolvePostAuthCloudEntry, switchWorkspace, workspaceSwitchingEnabled]);
1980
2277
  const joinInviteWithToken = useCallback(async (token) => {
1981
2278
  if (!cloudAuthConfigured) {
1982
2279
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -1997,30 +2294,93 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
1997
2294
  return { success: false, error: data?.error || 'Invite join failed.', code: data?.code };
1998
2295
  }
1999
2296
  setError('');
2000
- await checkAuthSession({ force: true });
2001
2297
  const preferredWorkspaceId = typeof data?.workspaceId === 'string' ? data.workspaceId : null;
2002
- const workspaceResolution = await resolveWorkspaceAfterAuth({ preferredWorkspaceId });
2003
- if (!workspaceResolution.success) {
2004
- return {
2005
- success: false,
2006
- error: workspaceResolution.error || 'Unable to resolve workspace after invite join.',
2007
- code: workspaceResolution.code
2008
- };
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
+ }
2009
2307
  }
2010
- if (workspaceResolution.workspaceSetupRequired) {
2011
- 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 };
2012
2311
  }
2013
- await Promise.all([
2014
- fetchConfig({ ignoreAuthGuard: true }),
2015
- fetchTasks(true, { ignoreAuthGuard: true })
2016
- ]);
2017
- void syncUserGlobalSettings({ preferCloudOnFirstSync: true });
2018
- return { success: true };
2312
+ if (preferredWorkspaceId && currentWorkspaceIdRef.current !== preferredWorkspaceId) {
2313
+ commitCurrentWorkspaceId(preferredWorkspaceId, { clearExplicitSelection: false });
2314
+ }
2315
+ return { success: true, workspaceSetupRequired: entry.workspaceSetupRequired === true };
2019
2316
  }
2020
2317
  catch {
2021
2318
  return { success: false, error: 'Invite join failed.' };
2022
2319
  }
2023
- }, [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]);
2024
2384
  const resetCloudLogoutClientState = useCallback(() => {
2025
2385
  abortWorkspaceScopedRequests();
2026
2386
  referenceDataRequestRef.current = null;
@@ -2039,6 +2399,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2039
2399
  setArchivedTasks([]);
2040
2400
  setInitiatives([]);
2041
2401
  setWorkstreams([]);
2402
+ setPlanningBootstrapTaskSummaries([]);
2042
2403
  setLoadingTasks(false);
2043
2404
  if (cloudRealtimeFollowUpRefreshTimerRef.current !== null && typeof window !== 'undefined') {
2044
2405
  window.clearTimeout(cloudRealtimeFollowUpRefreshTimerRef.current);
@@ -2149,6 +2510,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2149
2510
  throw new Error(data?.error || `Failed to create initiative (${res.status})`);
2150
2511
  }
2151
2512
  await fetchPlanningEntities();
2513
+ setInitiatives((current) => upsertEntityById(current, data));
2152
2514
  return data;
2153
2515
  }, [fetchPlanningEntities]);
2154
2516
  const updateInitiative = useCallback(async (id, payload) => {
@@ -2162,6 +2524,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2162
2524
  throw new Error(data?.error || `Failed to update initiative (${res.status})`);
2163
2525
  }
2164
2526
  await fetchPlanningEntities();
2527
+ setInitiatives((current) => upsertEntityById(current, data));
2165
2528
  return data;
2166
2529
  }, [fetchPlanningEntities]);
2167
2530
  const archiveInitiative = useCallback(async (id) => {
@@ -2173,6 +2536,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2173
2536
  throw new Error(data?.error || `Failed to archive initiative (${res.status})`);
2174
2537
  }
2175
2538
  await fetchPlanningEntities();
2539
+ setInitiatives((current) => upsertEntityById(current, data));
2176
2540
  return data;
2177
2541
  }, [fetchPlanningEntities]);
2178
2542
  const unarchiveInitiative = useCallback(async (id) => {
@@ -2184,6 +2548,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2184
2548
  throw new Error(data?.error || `Failed to unarchive initiative (${res.status})`);
2185
2549
  }
2186
2550
  await fetchPlanningEntities();
2551
+ setInitiatives((current) => upsertEntityById(current, data));
2187
2552
  return data;
2188
2553
  }, [fetchPlanningEntities]);
2189
2554
  const createWorkstream = useCallback(async (payload) => {
@@ -2197,6 +2562,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2197
2562
  throw new Error(data?.error || `Failed to create workstream (${res.status})`);
2198
2563
  }
2199
2564
  await fetchPlanningEntities();
2565
+ setWorkstreams((current) => upsertEntityById(current, data));
2200
2566
  return data;
2201
2567
  }, [fetchPlanningEntities]);
2202
2568
  const updateWorkstream = useCallback(async (id, payload) => {
@@ -2210,6 +2576,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2210
2576
  throw new Error(data?.error || `Failed to update workstream (${res.status})`);
2211
2577
  }
2212
2578
  await fetchPlanningEntities();
2579
+ setWorkstreams((current) => upsertEntityById(current, data));
2213
2580
  return data;
2214
2581
  }, [fetchPlanningEntities]);
2215
2582
  const archiveWorkstream = useCallback(async (id) => {
@@ -2221,6 +2588,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2221
2588
  throw new Error(data?.error || `Failed to archive workstream (${res.status})`);
2222
2589
  }
2223
2590
  await fetchPlanningEntities();
2591
+ setWorkstreams((current) => upsertEntityById(current, data));
2224
2592
  return data;
2225
2593
  }, [fetchPlanningEntities]);
2226
2594
  const unarchiveWorkstream = useCallback(async (id) => {
@@ -2232,6 +2600,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2232
2600
  throw new Error(data?.error || `Failed to unarchive workstream (${res.status})`);
2233
2601
  }
2234
2602
  await fetchPlanningEntities();
2603
+ setWorkstreams((current) => upsertEntityById(current, data));
2235
2604
  return data;
2236
2605
  }, [fetchPlanningEntities]);
2237
2606
  // Initialize auth once, then bootstrap protected data once auth gates are settled.
@@ -2248,6 +2617,38 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2248
2617
  return;
2249
2618
  void checkAuthSession();
2250
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]);
2251
2652
  // Keep local runtime auth state aligned with cloud session changes
2252
2653
  // (e.g. user signs in/out in another tab or cloud-hosted app).
2253
2654
  useEffect(() => {
@@ -2271,109 +2672,6 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2271
2672
  window.clearInterval(intervalId);
2272
2673
  };
2273
2674
  }, [runtimeMode, shouldProbeCloudAuth, checkAuthSession]);
2274
- useEffect(() => {
2275
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2276
- return;
2277
- if (hasBootstrappedDataRef.current)
2278
- return;
2279
- hasBootstrappedDataRef.current = true;
2280
- void (async () => {
2281
- if (runtimeMode === 'cloud' && authSessionResolved && isAuthenticated) {
2282
- const workspaceResolution = await resolveWorkspaceAfterAuth();
2283
- if (!workspaceResolution.success || workspaceResolution.workspaceSetupRequired) {
2284
- await fetchBootstrapConfig();
2285
- void fetchReferenceData();
2286
- return;
2287
- }
2288
- }
2289
- await fetchBootstrapConfig();
2290
- await Promise.all([
2291
- (async () => {
2292
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2293
- await fetchTasks(false);
2294
- logBootstrapDebug('bootstrap_tasks_loaded', {
2295
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2296
- });
2297
- })(),
2298
- fetchReferenceData(),
2299
- fetchPlanningEntities()
2300
- ]);
2301
- if (typeof window !== 'undefined') {
2302
- if (archiveBootstrapTimerRef.current !== null) {
2303
- window.clearTimeout(archiveBootstrapTimerRef.current);
2304
- }
2305
- archiveBootstrapTimerRef.current = window.setTimeout(() => {
2306
- archiveBootstrapTimerRef.current = null;
2307
- const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
2308
- void fetchArchive(true).then(() => {
2309
- logBootstrapDebug('bootstrap_archive_loaded_deferred', {
2310
- durationMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt)
2311
- });
2312
- });
2313
- }, 1500);
2314
- }
2315
- })();
2316
- }, [
2317
- shouldDeferProtectedApiCalls,
2318
- shouldBlockProtectedApiCalls,
2319
- runtimeMode,
2320
- authSessionResolved,
2321
- isAuthenticated,
2322
- resolveWorkspaceAfterAuth,
2323
- fetchBootstrapConfig,
2324
- fetchTasks,
2325
- fetchArchive,
2326
- fetchReferenceData,
2327
- fetchPlanningEntities
2328
- ]);
2329
- useEffect(() => {
2330
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2331
- return;
2332
- if (workspaceBootstrapPending)
2333
- return;
2334
- if (workspaceSelectionScope === WORKSPACE_BOOTSTRAP_SCOPE)
2335
- return;
2336
- if (!hasBootstrappedDataRef.current)
2337
- return;
2338
- const loadKey = `${runtimeMode}:${currentWorkspaceId}:${workspaceSelectionScope}`;
2339
- if (lastConfigRefreshKeyRef.current === loadKey)
2340
- return;
2341
- lastConfigRefreshKeyRef.current = loadKey;
2342
- void fetchConfig();
2343
- }, [
2344
- currentWorkspaceId,
2345
- shouldDeferProtectedApiCalls,
2346
- shouldBlockProtectedApiCalls,
2347
- workspaceBootstrapPending,
2348
- runtimeMode,
2349
- workspaceSelectionScope,
2350
- fetchConfig
2351
- ]);
2352
- useEffect(() => {
2353
- if (shouldDeferProtectedApiCalls || shouldBlockProtectedApiCalls)
2354
- return;
2355
- if (workspaceBootstrapPending)
2356
- return;
2357
- const loadKey = currentWorkspaceId;
2358
- if (lastWorkspaceSyncStateLoadKeyRef.current === loadKey)
2359
- return;
2360
- lastWorkspaceSyncStateLoadKeyRef.current = loadKey;
2361
- void loadWorkspaceSyncState();
2362
- }, [shouldDeferProtectedApiCalls, shouldBlockProtectedApiCalls, currentWorkspaceId, loadWorkspaceSyncState, workspaceBootstrapPending]);
2363
- useEffect(() => {
2364
- if (runtimeMode !== 'cloud' || !authSessionResolved || !isAuthenticated) {
2365
- if (runtimeMode !== 'cloud') {
2366
- setAvailableWorkspaces([]);
2367
- }
2368
- workspaceListHydratedRef.current = false;
2369
- return;
2370
- }
2371
- if (workspaceBootstrapPending)
2372
- return;
2373
- if (!hasBootstrappedDataRef.current || workspaceListHydratedRef.current)
2374
- return;
2375
- fetchWorkspaces();
2376
- }, [runtimeMode, authSessionResolved, isAuthenticated, fetchWorkspaces, workspaceBootstrapPending]);
2377
2675
  // Report task count to parent
2378
2676
  useEffect(() => {
2379
2677
  if (onTaskCountChange) {
@@ -2539,6 +2837,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2539
2837
  return;
2540
2838
  lastTaskSurfaceLoadKeyRef.current = loadKey;
2541
2839
  fetchTasks();
2840
+ fetchPlanningEntities();
2542
2841
  if (showArchive || taskScope === 'archived') {
2543
2842
  fetchArchive(true);
2544
2843
  }
@@ -2565,6 +2864,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
2565
2864
  showArchive,
2566
2865
  taskScope,
2567
2866
  fetchTasks,
2867
+ fetchPlanningEntities,
2568
2868
  fetchArchive,
2569
2869
  fetchDeletedTasks,
2570
2870
  fetchConfig,
@@ -3023,6 +3323,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3023
3323
  workspaceCloudSyncEnabled,
3024
3324
  saveWorkspaceCloudSyncSettings,
3025
3325
  applyWorkspaceSyncStateSnapshot,
3326
+ bootstrapState,
3026
3327
  authSessionResolved,
3027
3328
  workspaceBootstrapPending,
3028
3329
  bootstrapPhase,
@@ -3039,6 +3340,12 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3039
3340
  deleteWorkspace,
3040
3341
  switchWorkspace,
3041
3342
  loginWithCredentials,
3343
+ beginOAuthLogin,
3344
+ availableAuthProviders,
3345
+ fetchLoginMethods,
3346
+ unlinkLoginMethod,
3347
+ addPasswordToAccount,
3348
+ beginOAuthLink,
3042
3349
  registerWithCredentials,
3043
3350
  updateCurrentUserProfile,
3044
3351
  requestEmailVerification,
@@ -3071,6 +3378,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3071
3378
  archivedTasks,
3072
3379
  initiatives,
3073
3380
  workstreams,
3381
+ planningBootstrapTaskSummaries,
3074
3382
  deletedTasks,
3075
3383
  activeCategories,
3076
3384
  activeTypes,
@@ -3093,6 +3401,7 @@ export function useTaskforce({ config = {}, initialTaskId, onTaskCountChange, on
3093
3401
  setFilterAssignees,
3094
3402
  filterTaxonomies,
3095
3403
  setFilterTaxonomies,
3404
+ hasInitedFilters,
3096
3405
  sortBy,
3097
3406
  setSortBy,
3098
3407
  sortOrder,