@taskforcehq/taskforce 0.3.169 → 0.3.300

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 (696) hide show
  1. package/README.md +52 -21
  2. package/dist/Taskforce.module.css +3394 -5981
  3. package/dist/TaskforceCore.CategoryFallback.test.js +5 -2
  4. package/dist/TaskforceCore.filter-persistence.test.js +402 -0
  5. package/dist/TaskforceCore.js +388 -71
  6. package/dist/TaskforceCore.routing.test.js +311 -0
  7. package/dist/TaskforceCore.test.js +3344 -615
  8. package/dist/TaskforceWidget.js +3 -2
  9. package/dist/__tests__/localizationCatalogs.test.js +48 -31
  10. package/dist/__tests__/workflowSimulation.test.js +2 -2
  11. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.d.ts +60 -0
  12. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +324 -0
  13. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.test.js +278 -0
  14. package/dist/annotatedAttachments/payload.d.ts +2 -0
  15. package/dist/annotatedAttachments/payload.js +54 -0
  16. package/dist/annotatedAttachments/payload.test.js +67 -0
  17. package/dist/annotatedAttachments/service.d.ts +49 -0
  18. package/dist/annotatedAttachments/service.js +121 -0
  19. package/dist/annotatedAttachments/types.d.ts +86 -0
  20. package/dist/cli.js +74 -25
  21. package/dist/compat/deploymentCompat.d.ts +12 -0
  22. package/dist/compat/deploymentCompat.js +43 -0
  23. package/dist/compat/workspaceSyncCompat.d.ts +6 -0
  24. package/dist/compat/workspaceSyncCompat.js +79 -0
  25. package/dist/compat/workspaceSyncCompat.test.js +87 -0
  26. package/dist/components/features/AgentsModule.d.ts +5 -0
  27. package/dist/components/features/AgentsModule.js +131 -0
  28. package/dist/components/features/AgentsModule.test.js +102 -0
  29. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +24 -0
  30. package/dist/components/features/AnnotatedAttachmentWorkspace.js +1942 -0
  31. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +2831 -0
  32. package/dist/components/features/DocumentGenerateModal.js +8 -16
  33. package/dist/components/features/DocumentIndex.d.ts +3 -4
  34. package/dist/components/features/DocumentIndex.js +22 -10
  35. package/dist/components/features/DocumentViewer.d.ts +6 -2
  36. package/dist/components/features/DocumentViewer.js +1000 -18
  37. package/dist/components/features/DocumentWorkspace.d.ts +40 -2
  38. package/dist/components/features/DocumentWorkspace.js +96 -16
  39. package/dist/components/features/DocumentWorkspace.test.js +1202 -0
  40. package/dist/components/features/InitiativesModule.d.ts +22 -0
  41. package/dist/components/features/InitiativesModule.js +33 -0
  42. package/dist/components/features/TaskSettings.d.ts +41 -32
  43. package/dist/components/features/TaskSettings.js +798 -313
  44. package/dist/components/features/TaskSettings.test.js +1221 -47
  45. package/dist/components/features/WorkflowsModule.d.ts +27 -0
  46. package/dist/components/features/WorkflowsModule.js +21 -0
  47. package/dist/components/features/documentWorkspaceFilters.d.ts +10 -0
  48. package/dist/components/features/documentWorkspaceFilters.js +11 -0
  49. package/dist/components/features/settings/CategoryManager.d.ts +2 -0
  50. package/dist/components/features/settings/CategoryManager.js +15 -8
  51. package/dist/components/features/settings/PriorityManager.d.ts +18 -0
  52. package/dist/components/features/settings/PriorityManager.js +51 -0
  53. package/dist/components/features/settings/TaxonomyEditor.d.ts +2 -1
  54. package/dist/components/features/settings/TaxonomyEditor.js +41 -18
  55. package/dist/components/features/settings/TaxonomyLibraryManager.d.ts +26 -0
  56. package/dist/components/features/settings/TaxonomyLibraryManager.js +301 -0
  57. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +141 -0
  58. package/dist/components/features/settings/TaxonomyManager.d.ts +35 -12
  59. package/dist/components/features/settings/TaxonomyManager.js +59 -27
  60. package/dist/components/features/settings/TypeManager.d.ts +11 -0
  61. package/dist/components/features/settings/TypeManager.js +48 -11
  62. package/dist/components/features/settings/{WorkflowEditorPrototype.d.ts → WorkflowEditor.d.ts} +2 -2
  63. package/dist/components/features/settings/{WorkflowEditorPrototype.js → WorkflowEditor.js} +3 -3
  64. package/dist/components/task/TaskActionHeader.d.ts +12 -2
  65. package/dist/components/task/TaskActionHeader.js +64 -8
  66. package/dist/components/task/TaskActionHeader.test.js +73 -0
  67. package/dist/components/task/TaskCard.d.ts +14 -16
  68. package/dist/components/task/TaskCard.js +144 -174
  69. package/dist/components/task/TaskCard.test.js +416 -64
  70. package/dist/components/task/TaskContextUpload.d.ts +6 -5
  71. package/dist/components/task/TaskContextUpload.js +199 -64
  72. package/dist/components/task/TaskContextUpload.test.js +331 -14
  73. package/dist/components/task/TaskForm.d.ts +23 -29
  74. package/dist/components/task/TaskForm.js +348 -169
  75. package/dist/components/task/TaskForm.test.js +419 -23
  76. package/dist/components/task/TaskKanban.d.ts +20 -21
  77. package/dist/components/task/TaskKanban.js +176 -316
  78. package/dist/components/task/TaskKanban.planning-drop.test.js +93 -0
  79. package/dist/components/task/TaskList.d.ts +22 -11
  80. package/dist/components/task/TaskList.js +98 -12
  81. package/dist/components/task/TaskList.test.js +206 -7
  82. package/dist/components/task/TaskReferenceBadge.d.ts +12 -0
  83. package/dist/components/task/TaskReferenceBadge.js +5 -0
  84. package/dist/components/task/TaskStatusActions.d.ts +12 -0
  85. package/dist/components/task/TaskStatusActions.js +18 -0
  86. package/dist/components/task/TaskStatusActions.test.js +45 -0
  87. package/dist/components/ui/ImageReferenceBadge.d.ts +12 -0
  88. package/dist/components/ui/ImageReferenceBadge.js +5 -0
  89. package/dist/components/ui/LevelSelector.d.ts +2 -1
  90. package/dist/components/ui/LevelSelector.js +2 -2
  91. package/dist/components/ui/Markdown.d.ts +1 -2
  92. package/dist/components/ui/Markdown.js +2 -13
  93. package/dist/components/ui/Modal.d.ts +3 -2
  94. package/dist/components/ui/Modal.js +4 -3
  95. package/dist/components/ui/MultiSelectFilter.d.ts +3 -1
  96. package/dist/components/ui/MultiSelectFilter.js +2 -2
  97. package/dist/components/ui/ReferenceBadgeButton.d.ts +13 -0
  98. package/dist/components/ui/ReferenceBadgeButton.js +6 -0
  99. package/dist/components/ui/TaxonomyDropdown.d.ts +7 -1
  100. package/dist/components/ui/TaxonomyDropdown.js +58 -23
  101. package/dist/components/ui/TopNoticeLayer.d.ts +7 -0
  102. package/dist/components/ui/TopNoticeLayer.js +14 -0
  103. package/dist/components/ui/TopNoticeLayer.test.js +25 -0
  104. package/dist/components/views/AppShellHeader.d.ts +13 -0
  105. package/dist/components/views/AppShellHeader.js +19 -0
  106. package/dist/components/views/AppShellHeader.test.js +21 -0
  107. package/dist/components/views/PlanComparisonPage.d.ts +73 -0
  108. package/dist/components/views/PlanComparisonPage.js +196 -0
  109. package/dist/components/views/PlanComparisonPage.test.js +251 -0
  110. package/dist/components/views/PlansPage.d.ts +14 -0
  111. package/dist/components/views/PlansPage.js +355 -0
  112. package/dist/components/views/StandaloneLayout.d.ts +2 -2
  113. package/dist/components/views/StandaloneLayout.js +2212 -1194
  114. package/dist/components/views/WidgetView.d.ts +2 -2
  115. package/dist/components/views/WidgetView.js +149 -43
  116. package/dist/components/views/chrome/WorkspaceChromeSections.d.ts +57 -0
  117. package/dist/components/views/chrome/WorkspaceChromeSections.js +38 -0
  118. package/dist/components/views/chrome/WorkspaceChromeSections.test.js +54 -0
  119. package/dist/components/views/chrome/workspaceChrome.d.ts +31 -0
  120. package/dist/components/views/chrome/workspaceChrome.js +108 -0
  121. package/dist/components/views/chrome/workspaceChrome.test.js +52 -0
  122. package/dist/components/views/panels/DocumentFilterToolbar.d.ts +10 -0
  123. package/dist/components/views/panels/DocumentFilterToolbar.js +7 -0
  124. package/dist/components/views/panels/DocumentFilterToolbar.test.js +19 -0
  125. package/dist/components/views/panels/FilterToolbar.d.ts +66 -0
  126. package/dist/components/views/panels/FilterToolbar.js +21 -0
  127. package/dist/components/views/panels/FilterToolbar.test.d.ts +1 -0
  128. package/dist/components/views/panels/FilterToolbar.test.js +45 -0
  129. package/dist/components/views/panels/FilterToolbarControls.d.ts +46 -0
  130. package/dist/components/views/panels/FilterToolbarControls.js +61 -0
  131. package/dist/components/views/panels/PlanningDrawer.d.ts +115 -0
  132. package/dist/components/views/panels/PlanningDrawer.js +220 -0
  133. package/dist/components/views/panels/ScheduleSidebar.d.ts +52 -0
  134. package/dist/components/views/panels/ScheduleSidebar.js +127 -0
  135. package/dist/components/views/panels/scheduleUtils.d.ts +14 -0
  136. package/dist/components/views/panels/scheduleUtils.js +79 -0
  137. package/dist/components/views/teamManagementAccess.d.ts +6 -0
  138. package/dist/components/views/teamManagementAccess.js +18 -0
  139. package/dist/components/views/teamManagementAccess.test.d.ts +1 -0
  140. package/dist/components/views/teamManagementAccess.test.js +44 -0
  141. package/dist/config/clientDeployment.d.ts +2 -0
  142. package/dist/config/clientDeployment.js +13 -0
  143. package/dist/config/cloudEnvironment.d.ts +15 -0
  144. package/dist/config/cloudEnvironment.js +60 -0
  145. package/dist/config/deployment.d.ts +20 -0
  146. package/dist/config/deployment.js +67 -0
  147. package/dist/config/deployment.test.d.ts +1 -0
  148. package/dist/config/deployment.test.js +88 -0
  149. package/dist/config/envSchema.js +36 -8
  150. package/dist/config/envSchema.test.js +28 -0
  151. package/dist/core/AiProfileService.d.ts +66 -0
  152. package/dist/core/AiProfileService.js +320 -0
  153. package/dist/core/AiProfileService.test.d.ts +1 -0
  154. package/dist/core/AiProfileService.test.js +164 -0
  155. package/dist/core/AiProfiles.test.d.ts +1 -0
  156. package/dist/core/AiProfiles.test.js +449 -0
  157. package/dist/core/AttachmentLinkService.d.ts +15 -0
  158. package/dist/core/AttachmentLinkService.js +157 -0
  159. package/dist/core/AuthTokenService.d.ts +44 -0
  160. package/dist/core/AuthTokenService.js +207 -0
  161. package/dist/core/CreatedBy.test.js +134 -6
  162. package/dist/core/DeletedTaskLifecycleService.d.ts +33 -0
  163. package/dist/core/DeletedTaskLifecycleService.js +158 -0
  164. package/dist/core/DeletedTaskTrash.test.d.ts +1 -0
  165. package/dist/core/DeletedTaskTrash.test.js +104 -0
  166. package/dist/core/EntitlementsPolicy.test.js +78 -5
  167. package/dist/core/GlobalSettingsService.d.ts +16 -0
  168. package/dist/core/GlobalSettingsService.js +370 -0
  169. package/dist/core/JsonBackupRuntime.test.d.ts +1 -0
  170. package/dist/core/JsonBackupRuntime.test.js +155 -0
  171. package/dist/core/McpTokenService.d.ts +118 -0
  172. package/dist/core/McpTokenService.js +921 -0
  173. package/dist/core/PlanEntitlementService.d.ts +201 -0
  174. package/dist/core/PlanEntitlementService.js +1442 -0
  175. package/dist/core/PlanVersionPolicy.test.d.ts +1 -0
  176. package/dist/core/PlanVersionPolicy.test.js +178 -0
  177. package/dist/core/PlanningEntities.test.d.ts +1 -0
  178. package/dist/core/PlanningEntities.test.js +95 -0
  179. package/dist/core/ScheduleFields.test.js +15 -19
  180. package/dist/core/SignupMonetizationSettings.test.d.ts +1 -0
  181. package/dist/core/SignupMonetizationSettings.test.js +159 -0
  182. package/dist/core/Stats.test.d.ts +1 -0
  183. package/dist/core/Stats.test.js +36 -0
  184. package/dist/core/SyncReconciliationService.d.ts +93 -0
  185. package/dist/core/SyncReconciliationService.js +406 -0
  186. package/dist/core/SystemAdmin.test.js +578 -13
  187. package/dist/core/TaskActivityService.d.ts +21 -0
  188. package/dist/core/TaskActivityService.js +130 -0
  189. package/dist/core/TaskAttachmentsCanonical.test.d.ts +1 -0
  190. package/dist/core/TaskAttachmentsCanonical.test.js +306 -0
  191. package/dist/core/TaskAuditTimeline.test.d.ts +1 -0
  192. package/dist/core/TaskAuditTimeline.test.js +132 -0
  193. package/dist/core/TaskTaxonomyValidation.test.js +338 -15
  194. package/dist/core/Taskforce.d.ts +930 -183
  195. package/dist/core/Taskforce.ids.test.js +252 -0
  196. package/dist/core/Taskforce.js +3848 -2532
  197. package/dist/core/Taskforce.mcpAuth.test.d.ts +1 -0
  198. package/dist/core/Taskforce.mcpAuth.test.js +343 -0
  199. package/dist/core/Taskforce.realtimeMutation.test.js +57 -2
  200. package/dist/core/TaxonomySettingsService.d.ts +18 -0
  201. package/dist/core/TaxonomySettingsService.js +162 -0
  202. package/dist/core/WorkspaceDelete.test.js +32 -13
  203. package/dist/core/WorkspaceLifecycleService.d.ts +11 -0
  204. package/dist/core/WorkspaceLifecycleService.js +88 -0
  205. package/dist/core/WorkspacePermissions.test.js +95 -0
  206. package/dist/core/WorkspacePlanMode.test.js +114 -1
  207. package/dist/core/WorkspacePreferenceService.d.ts +41 -0
  208. package/dist/core/WorkspacePreferenceService.js +246 -0
  209. package/dist/core/WorkspaceSyncSettings.test.d.ts +1 -0
  210. package/dist/core/WorkspaceSyncSettings.test.js +65 -0
  211. package/dist/core/__tests__/TaskAttachmentsCanonical.test.d.ts +1 -0
  212. package/dist/core/__tests__/TaskAttachmentsCanonical.test.js +212 -0
  213. package/dist/core/shared.d.ts +36 -0
  214. package/dist/core/shared.js +128 -0
  215. package/dist/core/types.d.ts +614 -0
  216. package/dist/core/types.js +27 -0
  217. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +56 -0
  218. package/dist/documentReviews/DocumentReviewCommentStore.js +230 -0
  219. package/dist/documentReviews/DocumentReviewSessionStore.d.ts +52 -0
  220. package/dist/documentReviews/DocumentReviewSessionStore.js +193 -0
  221. package/dist/documentReviews/DocumentReviewSessionStore.test.d.ts +1 -0
  222. package/dist/documentReviews/DocumentReviewSessionStore.test.js +127 -0
  223. package/dist/documentReviews/service.d.ts +68 -0
  224. package/dist/documentReviews/service.js +210 -0
  225. package/dist/documentReviews/types.d.ts +58 -0
  226. package/dist/documentReviews/types.js +1 -0
  227. package/dist/hooks/auth/useAuthGuardPolicy.d.ts +2 -0
  228. package/dist/hooks/auth/useAuthGuardPolicy.js +1 -0
  229. package/dist/hooks/tasks/useCurrentTaskRelations.d.ts +15 -0
  230. package/dist/hooks/tasks/useCurrentTaskRelations.js +37 -0
  231. package/dist/hooks/tasks/useTaskActions.d.ts +3 -0
  232. package/dist/hooks/tasks/useTaskActions.js +2 -0
  233. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +65 -0
  234. package/dist/hooks/tasks/useTaskEditorNavigation.js +235 -0
  235. package/dist/hooks/tasks/useTaskFilters.d.ts +2 -0
  236. package/dist/hooks/tasks/useTaskFilters.js +2 -0
  237. package/dist/hooks/tasks/useTaskFormActions.d.ts +58 -0
  238. package/dist/hooks/tasks/useTaskFormActions.js +365 -0
  239. package/dist/hooks/tasks/useTaskRelationships.d.ts +16 -0
  240. package/dist/hooks/tasks/useTaskRelationships.js +64 -0
  241. package/dist/hooks/ui/useResourceExport.d.ts +19 -0
  242. package/dist/hooks/ui/useResourceExport.js +84 -0
  243. package/dist/hooks/ui/useSettingsModel.d.ts +203 -0
  244. package/dist/hooks/ui/useSettingsModel.js +125 -0
  245. package/dist/hooks/ui/useSettingsPersistence.d.ts +36 -0
  246. package/dist/hooks/ui/useSettingsPersistence.js +254 -0
  247. package/dist/hooks/ui/useSettingsTaxonomyManagement.d.ts +119 -0
  248. package/dist/hooks/ui/useSettingsTaxonomyManagement.js +536 -0
  249. package/dist/hooks/ui/useUiNotice.d.ts +1 -0
  250. package/dist/hooks/ui/useUiNotice.js +1 -0
  251. package/dist/hooks/ui/useWorkflowTemplates.d.ts +37 -0
  252. package/dist/hooks/ui/useWorkflowTemplates.js +111 -0
  253. package/dist/hooks/ui/useZenMode.d.ts +5 -0
  254. package/dist/hooks/ui/useZenMode.js +32 -0
  255. package/dist/hooks/useFilterSort.d.ts +60 -0
  256. package/dist/hooks/useFilterSort.js +362 -0
  257. package/dist/hooks/useFilterSort.test.d.ts +1 -0
  258. package/dist/hooks/useFilterSort.test.js +333 -0
  259. package/dist/hooks/useRealtimeSync.d.ts +1 -0
  260. package/dist/hooks/useRealtimeSync.js +24 -9
  261. package/dist/hooks/useRealtimeSync.test.js +28 -1
  262. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.d.ts +1 -0
  263. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +34 -0
  264. package/dist/hooks/useSyncOrchestrator.d.ts +65 -103
  265. package/dist/hooks/useSyncOrchestrator.js +1538 -670
  266. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +700 -174
  267. package/dist/hooks/useTaskData.d.ts +35 -0
  268. package/dist/hooks/useTaskData.js +239 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +119 -0
  271. package/dist/hooks/useTaskMutations.d.ts +46 -0
  272. package/dist/hooks/useTaskMutations.js +472 -0
  273. package/dist/hooks/useTaskMutations.test.d.ts +1 -0
  274. package/dist/hooks/useTaskMutations.test.js +80 -0
  275. package/dist/hooks/useTaskforce.contract.test.d.ts +1 -0
  276. package/dist/hooks/useTaskforce.contract.test.js +7 -0
  277. package/dist/hooks/useTaskforce.d.ts +260 -295
  278. package/dist/hooks/useTaskforce.js +1954 -2623
  279. package/dist/hooks/useTaskforce.priority-filters.test.js +6 -11
  280. package/dist/hooks/useTaskforce.sync-behavior.test.d.ts +1 -0
  281. package/dist/hooks/useTaskforce.sync-behavior.test.js +1120 -0
  282. package/dist/hooks/useUiNotice.d.ts +16 -0
  283. package/dist/hooks/useUiNotice.js +49 -0
  284. package/dist/hooks/useWorkspaceSyncController.d.ts +23 -46
  285. package/dist/hooks/useWorkspaceSyncController.js +280 -461
  286. package/dist/hooks/useWorkspaceSyncController.test.js +106 -199
  287. package/dist/hooks/workspace/useTaskforceApiRouting.d.ts +8 -0
  288. package/dist/hooks/workspace/useTaskforceApiRouting.js +91 -0
  289. package/dist/hooks/workspace/useTaskforceApiRouting.test.d.ts +1 -0
  290. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +60 -0
  291. package/dist/hooks/workspace/useWorkspaceSyncController.d.ts +1 -0
  292. package/dist/hooks/workspace/useWorkspaceSyncController.js +1 -0
  293. package/dist/hooks/workspace/workspaceLocalState.d.ts +30 -0
  294. package/dist/hooks/workspace/workspaceLocalState.js +101 -0
  295. package/dist/localization/locales/en-US.d.ts +1 -8
  296. package/dist/localization/locales/en-US.js +4 -11
  297. package/dist/localization/locales/es-419.js +2 -9
  298. package/dist/localization/locales/pt-BR.js +2 -9
  299. package/dist/localization/localizationCatalogs.test.d.ts +1 -0
  300. package/dist/localization/localizationCatalogs.test.js +37 -0
  301. package/dist/main.d.ts +1 -0
  302. package/dist/main.js +9 -15
  303. package/dist/mcp/clientIntegrations.d.ts +33 -0
  304. package/dist/mcp/clientIntegrations.js +199 -0
  305. package/dist/mcp/clientIntegrations.test.d.ts +1 -0
  306. package/dist/mcp/clientIntegrations.test.js +110 -0
  307. package/dist/mcp/index.d.ts +2 -0
  308. package/dist/mcp/index.js +2 -0
  309. package/dist/mcp/optionResolution.test.js +6 -2
  310. package/dist/mcp/runtime.d.ts +120 -0
  311. package/dist/mcp/runtime.js +5668 -0
  312. package/dist/mcp/runtime.test.d.ts +1 -0
  313. package/dist/mcp/runtime.test.js +3293 -0
  314. package/dist/mcp/server.js +2 -1793
  315. package/dist/mcp/structuredCommentValidation.test.d.ts +1 -0
  316. package/dist/{__tests__ → mcp}/structuredCommentValidation.test.js +1 -1
  317. package/dist/mcp/structuredDocValidation.js +1 -2
  318. package/dist/mcp/structuredDocValidation.test.d.ts +1 -0
  319. package/dist/{__tests__ → mcp}/structuredDocValidation.test.js +9 -4
  320. package/dist/mcp/toolRegistry.d.ts +570 -0
  321. package/dist/mcp/toolRegistry.js +90 -0
  322. package/dist/mcp/toolRegistry.test.d.ts +1 -0
  323. package/dist/mcp/toolRegistry.test.js +30 -0
  324. package/dist/migrations/billingSchemaParity.test.js +111 -0
  325. package/dist/migrations/taskSchemaMigrations.d.ts +5 -0
  326. package/dist/migrations/taskSchemaMigrations.js +681 -39
  327. package/dist/migrations/taskSchemaMigrations.test.d.ts +1 -0
  328. package/dist/migrations/taskSchemaMigrations.test.js +210 -0
  329. package/dist/plugins/vite.js +13 -7
  330. package/dist/public/apple-touch-icon.png +0 -0
  331. package/dist/public/archive/phoenix/apple-touch-icon.png +0 -0
  332. package/dist/public/archive/phoenix/favicon.ico +0 -0
  333. package/dist/public/archive/phoenix/site.webmanifest +1 -0
  334. package/dist/public/favicon-96x96.png +0 -0
  335. package/dist/public/favicon.ico +0 -0
  336. package/dist/public/favicon.svg +17 -0
  337. package/dist/public/site.webmanifest +21 -1
  338. package/dist/public/web-app-manifest-192x192.png +0 -0
  339. package/dist/public/web-app-manifest-512x512.png +0 -0
  340. package/dist/releaseVersion.test.d.ts +1 -0
  341. package/dist/{__tests__/releaseVersion.test.js → releaseVersion.test.js} +1 -1
  342. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  343. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  344. package/dist/resources/templates/workflows/collaborate.yaml +11 -5
  345. package/dist/resources/templates/workflows/do.yaml +8 -2
  346. package/dist/resources/templates/workflows/evaluate.yaml +13 -7
  347. package/dist/resources/templates/workflows/execute.yaml +40 -23
  348. package/dist/resources/templates/workflows/plan.yaml +15 -8
  349. package/dist/resources/templates/workflows/review.yaml +14 -8
  350. package/dist/runtime/appGateDefinitions.d.ts +32 -0
  351. package/dist/runtime/appGateDefinitions.js +32 -0
  352. package/dist/runtime/appGates.d.ts +29 -0
  353. package/dist/runtime/appGates.js +53 -0
  354. package/dist/runtime/appGates.test.d.ts +1 -0
  355. package/dist/runtime/appGates.test.js +42 -0
  356. package/dist/runtime/migrationFlags.d.ts +1 -0
  357. package/dist/runtime/migrationFlags.js +1 -0
  358. package/dist/server/annotatedAttachmentsRoutes.test.d.ts +1 -0
  359. package/dist/server/annotatedAttachmentsRoutes.test.js +473 -0
  360. package/dist/server/auth.js +14 -2
  361. package/dist/server/auth.test.js +21 -1
  362. package/dist/server/cors.js +11 -5
  363. package/dist/server/cors.test.js +6 -0
  364. package/dist/server/documentReviewRoutes.test.d.ts +1 -0
  365. package/dist/server/documentReviewRoutes.test.js +157 -0
  366. package/dist/server/index.cookieProxy.test.js +46 -1
  367. package/dist/server/index.d.ts +20 -0
  368. package/dist/server/index.js +168 -41
  369. package/dist/server/index.rateLimit.test.js +6 -0
  370. package/dist/server/index.test.js +133 -2
  371. package/dist/server/mockStripe.d.ts +72 -0
  372. package/dist/server/mockStripe.js +357 -0
  373. package/dist/server/realtimeSyncWs.js +4 -0
  374. package/dist/server/routes/admin.d.ts +114 -0
  375. package/dist/server/routes/admin.js +2226 -0
  376. package/dist/server/routes/annotatedAttachments.d.ts +24 -0
  377. package/dist/server/routes/annotatedAttachments.js +235 -0
  378. package/dist/server/routes/auth.d.ts +61 -0
  379. package/dist/server/routes/auth.js +1296 -0
  380. package/dist/server/{routes.billing.d.ts → routes/billing.d.ts} +3 -2
  381. package/dist/server/routes/billing.js +2001 -0
  382. package/dist/server/routes/billing.test.d.ts +1 -0
  383. package/dist/server/routes/billing.test.js +1631 -0
  384. package/dist/server/routes/documentReviews.d.ts +11 -0
  385. package/dist/server/routes/documentReviews.js +191 -0
  386. package/dist/server/routes/documents.d.ts +118 -0
  387. package/dist/server/routes/documents.js +1113 -0
  388. package/dist/server/routes/resources.d.ts +25 -0
  389. package/dist/server/routes/resources.js +225 -0
  390. package/dist/server/routes/shared.d.ts +35 -0
  391. package/dist/server/routes/shared.js +305 -0
  392. package/dist/server/routes/shared.test.d.ts +1 -0
  393. package/dist/server/routes/shared.test.js +32 -0
  394. package/dist/server/{routes.sync.d.ts → routes/sync.d.ts} +5 -5
  395. package/dist/server/routes/sync.integration.test.d.ts +1 -0
  396. package/dist/server/routes/sync.integration.test.js +2003 -0
  397. package/dist/server/routes/sync.js +3143 -0
  398. package/dist/server/routes/tasks.d.ts +72 -0
  399. package/dist/server/routes/tasks.js +1252 -0
  400. package/dist/server/routes/workspaces.d.ts +32 -0
  401. package/dist/server/routes/workspaces.js +307 -0
  402. package/dist/server/routes.d.ts +8 -9
  403. package/dist/server/routes.js +968 -4092
  404. package/dist/server/routes.test.js +9043 -2415
  405. package/dist/server/runtimeMode.d.ts +1 -1
  406. package/dist/server/runtimeMode.js +3 -18
  407. package/dist/server/runtimeMode.test.js +3 -3
  408. package/dist/server/securityHeaders.d.ts +1 -1
  409. package/dist/server/securityHeaders.js +65 -6
  410. package/dist/server/securityHeaders.test.js +40 -2
  411. package/dist/services/templateEngine.test.d.ts +1 -0
  412. package/dist/{__tests__ → services}/templateEngine.test.js +1 -1
  413. package/dist/services/templates.test.d.ts +1 -0
  414. package/dist/{__tests__ → services}/templates.test.js +1 -1
  415. package/dist/services/workflowDocumentFixtures.test.d.ts +1 -0
  416. package/dist/{__tests__ → services}/workflowDocumentFixtures.test.js +4 -4
  417. package/dist/services/workflowExportMatrix.test.d.ts +1 -0
  418. package/dist/{__tests__ → services}/workflowExportMatrix.test.js +1 -1
  419. package/dist/services/workflowExportSnapshots.test.d.ts +1 -0
  420. package/dist/services/workflowGeneration.test.d.ts +1 -0
  421. package/dist/services/workflowSimulation.test.d.ts +1 -0
  422. package/dist/services/workflowSimulation.test.js +113 -0
  423. package/dist/services/workflowV2Policy.test.d.ts +1 -0
  424. package/dist/{__tests__ → services}/workflowV2Policy.test.js +2 -2
  425. package/dist/shared/aiProfileSurfaceType.d.ts +9 -0
  426. package/dist/shared/aiProfileSurfaceType.js +49 -0
  427. package/dist/shared/customTaxonomyLibrary.d.ts +11 -0
  428. package/dist/shared/customTaxonomyLibrary.js +276 -0
  429. package/dist/shared/customTaxonomyLibrary.test.d.ts +1 -0
  430. package/dist/shared/customTaxonomyLibrary.test.js +20 -0
  431. package/dist/shared/taxonomyCatalog.d.ts +19 -0
  432. package/dist/shared/taxonomyCatalog.js +36 -0
  433. package/dist/shared/taxonomyDefaults.d.ts +29 -0
  434. package/dist/shared/taxonomyDefaults.js +26 -0
  435. package/dist/shared/taxonomyLibrary.d.ts +15 -0
  436. package/dist/shared/taxonomyLibrary.js +98 -0
  437. package/dist/shared/taxonomyLibrary.test.d.ts +1 -0
  438. package/dist/shared/taxonomyLibrary.test.js +26 -0
  439. package/dist/shared/taxonomyState.d.ts +36 -0
  440. package/dist/shared/taxonomyState.js +73 -0
  441. package/dist/shared/taxonomyState.test.d.ts +1 -0
  442. package/dist/shared/taxonomyState.test.js +71 -0
  443. package/dist/shared/workflowDefaults.d.ts +9 -0
  444. package/dist/shared/workflowDefaults.js +11 -0
  445. package/dist/storage/documentIntegrity.d.ts +2 -0
  446. package/dist/storage/documentIntegrity.js +34 -17
  447. package/dist/storage/documentPurge.d.ts +2 -1
  448. package/dist/storage/documentPurge.js +35 -8
  449. package/dist/storage/index.d.ts +4 -0
  450. package/dist/storage/index.js +2 -0
  451. package/dist/storage/objectStorage.d.ts +1 -1
  452. package/dist/storage/objectStorageClient.d.ts +6 -4
  453. package/dist/storage/objectStorageClient.js +77 -29
  454. package/dist/storage/objectStorageClient.test.d.ts +1 -0
  455. package/dist/storage/objectStorageClient.test.js +57 -0
  456. package/dist/storage/objectStorageConfig.js +3 -1
  457. package/dist/storage/objectStorageConfig.test.d.ts +1 -0
  458. package/dist/storage/objectStorageConfig.test.js +38 -0
  459. package/dist/storage/postgresAdapter.d.ts +5 -0
  460. package/dist/storage/postgresAdapter.js +2 -2
  461. package/dist/storage/postgresAdapter.test.d.ts +1 -0
  462. package/dist/storage/postgresAdapter.test.js +29 -0
  463. package/dist/storage/taskAssetStore.d.ts +7 -4
  464. package/dist/storage/taskAssetStore.js +65 -60
  465. package/dist/storage/workspaceAssetStore.d.ts +122 -0
  466. package/dist/storage/workspaceAssetStore.js +496 -0
  467. package/dist/storage/workspaceAssetStore.test.d.ts +1 -0
  468. package/dist/storage/workspaceAssetStore.test.js +501 -0
  469. package/dist/sync/cloudSyncApi.d.ts +33 -2
  470. package/dist/sync/cloudSyncApi.js +37 -15
  471. package/dist/sync/cloudSyncApi.test.js +60 -3
  472. package/dist/sync/index.d.ts +2 -0
  473. package/dist/sync/index.js +3 -0
  474. package/dist/sync/syncService.d.ts +25 -0
  475. package/dist/sync/syncService.js +184 -16
  476. package/dist/sync/syncService.test.js +289 -25
  477. package/dist/sync/workspaceRepair.d.ts +114 -0
  478. package/dist/sync/workspaceRepair.js +566 -0
  479. package/dist/sync/workspaceSyncModel.d.ts +230 -2
  480. package/dist/sync/workspaceSyncModel.js +456 -17
  481. package/dist/sync/workspaceSyncModel.test.js +642 -0
  482. package/dist/sync/workspaceSyncState.d.ts +12 -0
  483. package/dist/sync/workspaceSyncState.js +12 -0
  484. package/dist/sync/workspaceSyncSurface.d.ts +33 -0
  485. package/dist/sync/workspaceSyncSurface.js +74 -0
  486. package/dist/types.d.ts +143 -18
  487. package/dist/types.js +9 -24
  488. package/dist/ui/assets/AgentsModule-TVmCgizS.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CAyhnPeR.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-BOCw5e4D.js +250 -0
  492. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  493. package/dist/ui/assets/InitiativesModule-CN0r02Oi.js +1 -0
  494. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  495. package/dist/ui/assets/PlansPage-FcQSNLZd.js +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-CY9R2zPB.js +5 -0
  499. package/dist/ui/assets/index-Bzqy5QGm.js +6 -0
  500. package/dist/ui/assets/index-DxRiI3tY.css +1 -0
  501. package/dist/ui/assets/vendor-dnd-DRzYolkg.js +5 -0
  502. package/dist/ui/assets/{vendor-icons-ZFG3SGwo.js → vendor-icons-QZyhEwgT.js} +1 -1
  503. package/dist/ui/assets/{vendor-markdown-CKYkQHfj.js → vendor-markdown-BUxTU7dS.js} +1 -1
  504. package/dist/ui/assets/vendor-react-CKJs5o3c.js +61 -0
  505. package/dist/ui/assets/vendor-router-BbWMxlnO.js +3 -0
  506. package/dist/ui/branding/logos/logo.png +0 -0
  507. package/dist/ui/branding/logos/logo_dark.svg +27 -0
  508. package/dist/ui/branding/logos/logo_favicon.svg +35 -0
  509. package/dist/ui/branding/logos/logo_light.svg +27 -0
  510. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  511. package/dist/ui/favicon/archive/phoenix/apple-touch-icon.png +0 -0
  512. package/dist/ui/favicon/archive/phoenix/favicon.ico +0 -0
  513. package/dist/ui/favicon/archive/phoenix/site.webmanifest +1 -0
  514. package/dist/ui/favicon/favicon-96x96.png +0 -0
  515. package/dist/ui/favicon/favicon.ico +0 -0
  516. package/dist/ui/favicon/favicon.svg +17 -0
  517. package/dist/ui/favicon/site.webmanifest +21 -1
  518. package/dist/ui/favicon/web-app-manifest-192x192.png +0 -0
  519. package/dist/ui/favicon/web-app-manifest-512x512.png +0 -0
  520. package/dist/ui/index.html +10 -7
  521. package/dist/ui/product/screen_01.png +0 -0
  522. package/dist/ui/product/screen_02.png +0 -0
  523. package/dist/ui/product/screen_03.png +0 -0
  524. package/dist/utils/annotatedAttachments.d.ts +20 -0
  525. package/dist/utils/annotatedAttachments.js +74 -0
  526. package/dist/utils/annotatedAttachments.test.d.ts +1 -0
  527. package/dist/utils/annotatedAttachments.test.js +108 -0
  528. package/dist/utils/assignees.d.ts +26 -0
  529. package/dist/utils/assignees.js +132 -0
  530. package/dist/utils/canonicalAssetKind.d.ts +16 -0
  531. package/dist/utils/canonicalAssetKind.js +39 -0
  532. package/dist/utils/canonicalAssetKind.test.d.ts +1 -0
  533. package/dist/utils/canonicalAssetKind.test.js +24 -0
  534. package/dist/utils/coalescedRefresh.d.ts +5 -0
  535. package/dist/utils/coalescedRefresh.js +16 -0
  536. package/dist/utils/coalescedRefresh.test.d.ts +1 -0
  537. package/dist/utils/coalescedRefresh.test.js +30 -0
  538. package/dist/utils/constants.d.ts +16 -3
  539. package/dist/utils/constants.icons.test.js +2 -1
  540. package/dist/utils/constants.js +70 -13
  541. package/dist/utils/contextAssetEvents.d.ts +7 -0
  542. package/dist/utils/contextAssetEvents.js +6 -0
  543. package/dist/utils/contextFiles.d.ts +14 -3
  544. package/dist/utils/contextFiles.js +40 -8
  545. package/dist/utils/documentManagerApi.d.ts +6 -0
  546. package/dist/utils/documentManagerApi.js +19 -0
  547. package/dist/utils/documentNames.d.ts +24 -0
  548. package/dist/utils/documentNames.js +91 -0
  549. package/dist/utils/documentReferences.d.ts +9 -0
  550. package/dist/utils/documentReferences.js +11 -0
  551. package/dist/utils/entityReferences.d.ts +9 -0
  552. package/dist/utils/entityReferences.js +32 -0
  553. package/dist/utils/imageReferences.d.ts +9 -0
  554. package/dist/utils/imageReferences.js +11 -0
  555. package/dist/utils/planParser.d.ts +2 -2
  556. package/dist/utils/planParser.js +11 -11
  557. package/dist/utils/planParser.test.d.ts +1 -0
  558. package/dist/{__tests__ → utils}/planParser.test.js +22 -22
  559. package/dist/utils/planningReferences.d.ts +17 -0
  560. package/dist/utils/planningReferences.js +30 -0
  561. package/dist/utils/planningReferences.test.d.ts +1 -0
  562. package/dist/utils/planningReferences.test.js +17 -0
  563. package/dist/utils/taskActivity.d.ts +9 -0
  564. package/dist/utils/taskActivity.js +147 -0
  565. package/dist/utils/taskActivity.test.d.ts +1 -0
  566. package/dist/utils/taskActivity.test.js +34 -0
  567. package/dist/utils/taskDraftDefaults.d.ts +21 -0
  568. package/dist/utils/taskDraftDefaults.js +40 -0
  569. package/dist/utils/taskDraftDefaults.test.d.ts +1 -0
  570. package/dist/utils/taskDraftDefaults.test.js +34 -0
  571. package/dist/utils/taskNormalization.d.ts +25 -0
  572. package/dist/utils/taskNormalization.js +161 -0
  573. package/dist/utils/taskNormalization.test.d.ts +1 -0
  574. package/dist/utils/taskNormalization.test.js +158 -0
  575. package/dist/utils/taskReferences.d.ts +5 -0
  576. package/dist/utils/taskReferences.js +11 -0
  577. package/dist/utils/taskforceApiClient.d.ts +2 -0
  578. package/dist/utils/taskforceApiClient.js +101 -3
  579. package/dist/utils/taskforceApiClient.test.d.ts +1 -0
  580. package/dist/utils/taskforceApiClient.test.js +44 -0
  581. package/dist/utils/taxonomyFilterOptions.d.ts +2 -0
  582. package/dist/utils/taxonomyFilterOptions.js +17 -0
  583. package/dist/utils/taxonomyFormDefaults.d.ts +3 -0
  584. package/dist/utils/taxonomyFormDefaults.js +28 -0
  585. package/dist/utils/taxonomyFormDefaults.test.d.ts +1 -0
  586. package/dist/utils/taxonomyFormDefaults.test.js +63 -0
  587. package/dist/utils/taxonomyLifecycle.d.ts +3 -0
  588. package/dist/utils/taxonomyLifecycle.js +17 -0
  589. package/dist/utils/taxonomySorting.d.ts +7 -0
  590. package/dist/utils/taxonomySorting.js +74 -0
  591. package/dist/utils/theme.d.ts +15 -0
  592. package/dist/utils/theme.js +23 -0
  593. package/dist/utils/uiNotice.d.ts +7 -0
  594. package/dist/utils/uiNotice.js +6 -0
  595. package/dist/utils/uiNotice.test.d.ts +1 -0
  596. package/dist/utils/uiNotice.test.js +18 -0
  597. package/dist/utils/workspaceIdentity.d.ts +14 -0
  598. package/dist/utils/workspaceIdentity.js +21 -0
  599. package/dist/utils/workspaceIdentity.test.js +30 -1
  600. package/dist/utils/workspaceSyncPresentation.d.ts +20 -0
  601. package/dist/utils/workspaceSyncPresentation.js +21 -0
  602. package/dist/utils/workspaceSyncPresentation.test.d.ts +1 -0
  603. package/dist/utils/workspaceSyncPresentation.test.js +36 -0
  604. package/package.json +28 -23
  605. package/scripts/bootstrap-playwright-auth.mjs +155 -0
  606. package/scripts/check-compat-boundaries.mjs +41 -0
  607. package/scripts/check-env.ts +9 -2
  608. package/scripts/debug-login.mjs +32 -0
  609. package/scripts/migrate-planning-model.ts +233 -0
  610. package/scripts/migrate-task-data-assets.mjs +408 -0
  611. package/scripts/playwright-auth.sh +11 -0
  612. package/scripts/run-dev-proxy.sh +2 -0
  613. package/scripts/update-homebrew-formula.sh +20 -3
  614. package/scripts/with-esbuild.sh +11 -0
  615. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  616. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  617. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  618. package/src/resources/templates/workflows/do.yaml +8 -2
  619. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  620. package/src/resources/templates/workflows/execute.yaml +40 -23
  621. package/src/resources/templates/workflows/plan.yaml +15 -8
  622. package/src/resources/templates/workflows/review.yaml +14 -8
  623. package/dist/TaskforceScreenshot.d.ts +0 -11
  624. package/dist/TaskforceScreenshot.js +0 -113
  625. package/dist/compat/priorityCompat.d.ts +0 -2
  626. package/dist/compat/priorityCompat.js +0 -24
  627. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  628. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  629. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  630. package/dist/components/task/TaskKanban 2.js +0 -1004
  631. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  632. package/dist/components/task/TaskScreenshotSection.js +0 -22
  633. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  634. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  635. package/dist/core/InitiativeTemplates.test.js +0 -50
  636. package/dist/core/ParentLifecycle.test.js +0 -98
  637. package/dist/migrations/clientMigrations.d.ts +0 -9
  638. package/dist/migrations/clientMigrations.js +0 -74
  639. package/dist/migrations/flags.d.ts +0 -6
  640. package/dist/migrations/flags.js +0 -8
  641. package/dist/migrations/taskMigrations.d.ts +0 -71
  642. package/dist/migrations/taskMigrations.js +0 -236
  643. package/dist/migrations/taskMigrations.test.js +0 -34
  644. package/dist/server/routes.billing.js +0 -991
  645. package/dist/server/routes.billing.test.js +0 -387
  646. package/dist/server/routes.sync.integration.test.js +0 -470
  647. package/dist/server/routes.sync.js +0 -1141
  648. package/dist/soak/syncSoakHarness.d.ts +0 -6
  649. package/dist/soak/syncSoakHarness.js +0 -10
  650. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  651. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  652. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  653. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  654. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  655. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  656. package/dist/utils/taskHierarchy.d.ts +0 -4
  657. package/dist/utils/taskHierarchy.js +0 -69
  658. package/dist/utils/taskHierarchy.test.js +0 -38
  659. package/dist/utils/taskParenting.d.ts +0 -3
  660. package/dist/utils/taskParenting.js +0 -24
  661. package/dist/utils/taskParenting.test.js +0 -21
  662. package/dist/utils/taskRelations.d.ts +0 -6
  663. package/dist/utils/taskRelations.js +0 -24
  664. package/dist/utils/taskRelations.test.js +0 -34
  665. package/scripts/check-compat-boundaries.js +0 -48
  666. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  667. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  668. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  669. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  670. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  671. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  672. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  673. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  677. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  678. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  679. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  680. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  681. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  682. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  683. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  684. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  685. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  686. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  687. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  688. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  689. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  690. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  691. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  692. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  693. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  694. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  695. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  696. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,3143 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createHash, randomUUID } from 'crypto';
4
+ import { parseJsonBody } from '../routes.js';
5
+ import { normalizeTaskDataRelativePath, ObjectStorageClient } from '../../storage/objectStorageClient.js';
6
+ import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore.js';
7
+ import { resolveSessionCookieAttributesForRequest } from '../auth.js';
8
+ import { decryptDocument, ENCRYPTION_PREFIX } from '../../sync/encryptionService.js';
9
+ import { isAiProfileSurfaceType } from '../../shared/aiProfileSurfaceType.js';
10
+ import { compareWorkspaceMutationOrdering, getTaskMutationTieBreak, getTaskMutationWatermark } from '../../sync/workspaceSyncSurface.js';
11
+ import { applyWorkspaceRepair } from '../../sync/workspaceRepair.js';
12
+ function decodeWorkspaceSyncCursor(raw) {
13
+ const value = String(raw || '').trim();
14
+ if (!value)
15
+ return null;
16
+ try {
17
+ const json = Buffer.from(value, 'base64url').toString('utf8');
18
+ const parsed = JSON.parse(json);
19
+ const u = String(parsed?.u || '').trim();
20
+ const id = String(parsed?.id || '').trim();
21
+ if (!u || !id)
22
+ return null;
23
+ if (!Number.isFinite(Date.parse(u)))
24
+ return null;
25
+ return { u, id };
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ function encodeWorkspaceSyncCursor(cursor) {
32
+ if (!cursor)
33
+ return null;
34
+ return Buffer.from(JSON.stringify(cursor), 'utf8').toString('base64url');
35
+ }
36
+ function createSyncRouteError(message, details, options) {
37
+ const error = new Error(message);
38
+ error.code = options?.code || 'SYNC_APPLY_FAILED';
39
+ error.statusCode = options?.statusCode || 500;
40
+ error.details = details;
41
+ return error;
42
+ }
43
+ function normalizeIncomingAiProfileSurfaceType(value, context) {
44
+ if (value === undefined || value === null)
45
+ return null;
46
+ const normalized = String(value || '').trim();
47
+ if (!normalized)
48
+ return null;
49
+ if (!isAiProfileSurfaceType(normalized)) {
50
+ throw createSyncRouteError(`Invalid ai profile surfaceType "${normalized}"`, {
51
+ ...context,
52
+ surfaceType: normalized
53
+ }, { code: 'AI_PROFILE_SURFACE_TYPE_INVALID', statusCode: 400 });
54
+ }
55
+ return normalized;
56
+ }
57
+ export function registerSyncRoutes(deps) {
58
+ const { addRoute, core, context, auditAuthAction, resolveErrorStatusCode, requestWorkspaceId, resolveRequestAccess, isReservedWorkspaceId, createSessionTokenCookie } = deps;
59
+ const readPushIdempotency = (workspaceId, key) => {
60
+ return core.readPushIdempotency(workspaceId, key);
61
+ };
62
+ const workspaceAssetStore = typeof core.getDatabaseAdapter === 'function' && typeof core.getTenantId === 'function'
63
+ ? new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId())
64
+ : null;
65
+ const resolveEffectiveObjectStorageConfig = () => {
66
+ const base = context.objectStorage || { provider: 'local' };
67
+ return base;
68
+ };
69
+ const getObjectStorageClient = () => {
70
+ const resolved = resolveEffectiveObjectStorageConfig();
71
+ return resolved.provider === 'r2' ? new ObjectStorageClient(resolved) : null;
72
+ };
73
+ const writePushIdempotency = (workspaceId, key, payload) => {
74
+ core.writePushIdempotency(workspaceId, key, payload);
75
+ };
76
+ const recordSyncDiagnostic = (workspaceId, eventType, errorMessage, details) => {
77
+ try {
78
+ core.recordSyncEvent(workspaceId, {
79
+ eventType,
80
+ status: 'error',
81
+ errorMessage,
82
+ requestMs: 0,
83
+ details
84
+ });
85
+ }
86
+ catch {
87
+ // diagnostics must never throw
88
+ }
89
+ if (details && Object.keys(details).length > 0) {
90
+ // eslint-disable-next-line no-console
91
+ console.warn('[Taskforce Sync Diagnostic]', { workspaceId, eventType, errorMessage, ...details });
92
+ }
93
+ };
94
+ const validateTaskActorAssignments = (workspaceId, tasks, archived) => {
95
+ const entries = [...tasks, ...archived];
96
+ if (entries.length === 0)
97
+ return;
98
+ for (const task of entries) {
99
+ const assignee = String(task.assignee || '').trim();
100
+ if (!assignee || assignee === 'unassigned')
101
+ continue;
102
+ const actor = core.resolveWorkspaceActor(workspaceId, assignee);
103
+ if (actor && actor.meta?.unresolved !== true)
104
+ continue;
105
+ const unresolvedKind = actor?.kind === 'ai' ? 'ai' : 'human';
106
+ if (unresolvedKind === 'human')
107
+ continue;
108
+ throw createSyncRouteError(`Task ${String(task.id || '').trim() || '(unknown task)'} references unresolved assignee ${assignee}.`, {
109
+ source: 'apply.assigneeValidation',
110
+ workspaceId,
111
+ taskId: String(task.id || '').trim() || null,
112
+ assignee
113
+ }, { code: 'TASK_ACTOR_RESOLUTION_FAILED', statusCode: 400 });
114
+ }
115
+ };
116
+ const assertSyncedAiProfilesPersisted = (workspaceId, incomingAiProfiles) => {
117
+ if (incomingAiProfiles.size === 0)
118
+ return;
119
+ const persistedIds = new Set(core.listAiProfiles(workspaceId)
120
+ .map((profile) => String(profile.id || '').trim())
121
+ .filter(Boolean));
122
+ for (const profile of incomingAiProfiles.values()) {
123
+ if (persistedIds.has(profile.id))
124
+ continue;
125
+ throw createSyncRouteError(`AI profile ${profile.id} was not persisted during sync apply.`, {
126
+ source: 'apply.aiProfileIntegrityCheck',
127
+ workspaceId,
128
+ profileId: profile.id,
129
+ expectedAiProfileCount: incomingAiProfiles.size,
130
+ persistedAiProfileCount: persistedIds.size
131
+ }, { code: 'AI_PROFILE_SYNC_APPLY_FAILED', statusCode: 500 });
132
+ }
133
+ };
134
+ const normalizeWorkspaceMemberSyncSnapshot = (workspaceId, raw) => {
135
+ const userId = String(raw?.userId || '').trim();
136
+ const email = String(raw?.email || '').trim().toLowerCase();
137
+ if (!userId || !email)
138
+ return null;
139
+ return {
140
+ userId,
141
+ workspaceId: String(raw?.workspaceId || workspaceId).trim() || workspaceId,
142
+ email,
143
+ displayName: typeof raw?.displayName === 'string' ? raw.displayName.trim() || null : null,
144
+ role: String(raw?.role || 'member').trim() || 'member',
145
+ permissionMode: String(raw?.permissionMode || 'read-write').trim() || 'read-write',
146
+ betaAccess: raw?.betaAccess !== false,
147
+ status: String(raw?.status || 'active').trim() || 'active',
148
+ disabled: raw?.disabled === true,
149
+ createdAt: typeof raw?.createdAt === 'string' ? raw.createdAt.trim() || null : null,
150
+ updatedAt: typeof raw?.updatedAt === 'string' ? raw.updatedAt.trim() || null : null,
151
+ emailVerifiedAt: typeof raw?.emailVerifiedAt === 'string' ? raw.emailVerifiedAt.trim() || null : null
152
+ };
153
+ };
154
+ const workspaceDocumentSnapshotCache = new Map();
155
+ const workspaceTaxonomySyncMeta = new Map();
156
+ const isMarkdownPath = (relativePath) => {
157
+ const lower = relativePath.toLowerCase();
158
+ return lower.endsWith('.md') || lower.endsWith('.markdown');
159
+ };
160
+ const normalizeWorkspaceSyncDocumentPath = (rawPath) => {
161
+ const value = String(rawPath || '').trim().replace(/\\/g, '/');
162
+ if (!value || value.includes('\0'))
163
+ return null;
164
+ if (value.startsWith('/'))
165
+ return null;
166
+ const normalized = path.posix.normalize(value);
167
+ if (!normalized || normalized.startsWith('../') || normalized.includes('/../') || normalized === '..')
168
+ return null;
169
+ if (!isMarkdownPath(normalized))
170
+ return null;
171
+ if (/^workspaces\/[^/]+\/assets\/documents\//.test(normalized))
172
+ return normalized;
173
+ if (normalized.startsWith('docs/'))
174
+ return normalized;
175
+ return null;
176
+ };
177
+ const resolveCanonicalAssetSyncLinkState = (asset, workspaceId) => {
178
+ if (!workspaceAssetStore) {
179
+ return {
180
+ primaryLink: null,
181
+ effectiveUpdatedAt: String(asset.updatedAt || asset.createdAt || '').trim() || new Date().toISOString()
182
+ };
183
+ }
184
+ const links = workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId, { includeDeleted: true });
185
+ const activeLinks = links.filter((entry) => !entry.deletedAt);
186
+ const primaryLink = activeLinks[0] || null;
187
+ let effectiveUpdatedAt = String(asset.updatedAt || asset.createdAt || '').trim() || new Date().toISOString();
188
+ for (const link of links) {
189
+ const linkUpdatedAt = String(link.updatedAt || link.createdAt || '').trim();
190
+ if (linkUpdatedAt && linkUpdatedAt > effectiveUpdatedAt)
191
+ effectiveUpdatedAt = linkUpdatedAt;
192
+ }
193
+ return { primaryLink, effectiveUpdatedAt };
194
+ };
195
+ const getCanonicalSyncDocumentMetadata = (relativePath, workspaceId) => {
196
+ if (!workspaceAssetStore)
197
+ return {};
198
+ const asset = workspaceAssetStore.getByStorageKey(relativePath, workspaceId);
199
+ if (!asset || asset.kind !== 'document')
200
+ return {};
201
+ const { primaryLink } = resolveCanonicalAssetSyncLinkState(asset, workspaceId);
202
+ return {
203
+ assetId: asset.assetId,
204
+ documentId: asset.documentId,
205
+ referenceNumber: asset.referenceNumber,
206
+ version: asset.version,
207
+ taskId: primaryLink?.taskId || null,
208
+ logicalName: asset.logicalName || null,
209
+ caption: primaryLink?.displayName || null,
210
+ originalFilename: asset.originalFilename || path.basename(relativePath),
211
+ linkRole: primaryLink?.role === 'reference' ? 'reference' : 'attachment'
212
+ };
213
+ };
214
+ const hasDocumentSyncMetadataDrift = (workspaceId, doc, options) => {
215
+ if (!workspaceAssetStore)
216
+ return false;
217
+ const existingAsset = workspaceAssetStore.getByStorageKey(doc.path, workspaceId);
218
+ if (!existingAsset || existingAsset.kind !== 'document')
219
+ return false;
220
+ const { primaryLink } = resolveCanonicalAssetSyncLinkState(existingAsset, workspaceId);
221
+ const normalizedIncomingReference = Number.isFinite(Number(doc.referenceNumber))
222
+ ? Math.max(1, Math.floor(Number(doc.referenceNumber)))
223
+ : null;
224
+ const normalizedIncomingVersion = Number.isFinite(Number(doc.version))
225
+ ? Math.max(1, Math.floor(Number(doc.version)))
226
+ : null;
227
+ const normalizedIncomingDocumentId = typeof doc.documentId === 'string' && doc.documentId.trim().length > 0
228
+ ? doc.documentId.trim()
229
+ : null;
230
+ const normalizedIncomingLogicalName = typeof doc.logicalName === 'string' && doc.logicalName.trim().length > 0
231
+ ? doc.logicalName.trim()
232
+ : null;
233
+ const normalizedIncomingCaption = typeof doc.caption === 'string' && doc.caption.trim().length > 0
234
+ ? doc.caption.trim()
235
+ : null;
236
+ const normalizedIncomingFilename = typeof doc.originalFilename === 'string' && doc.originalFilename.trim().length > 0
237
+ ? doc.originalFilename.trim()
238
+ : null;
239
+ const normalizedIncomingTaskId = typeof doc.taskId === 'string' && doc.taskId.trim().length > 0
240
+ ? doc.taskId.trim()
241
+ : null;
242
+ const normalizedIncomingLinkRole = doc.linkRole === 'reference' ? 'reference' : 'attachment';
243
+ const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
244
+ if (!allowReferenceNumberReassignment
245
+ && existingAsset.referenceNumber
246
+ && normalizedIncomingReference
247
+ && existingAsset.referenceNumber !== normalizedIncomingReference) {
248
+ recordSyncDiagnostic(workspaceId, 'apply', 'Detected document reference number mismatch during normal sync.', {
249
+ source: 'documents.referenceNumber.mismatch',
250
+ path: doc.path,
251
+ assetId: existingAsset.assetId,
252
+ existingReferenceNumber: existingAsset.referenceNumber,
253
+ incomingReferenceNumber: normalizedIncomingReference
254
+ });
255
+ }
256
+ return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
257
+ || existingAsset.version !== normalizedIncomingVersion
258
+ || (existingAsset.documentId || null) !== normalizedIncomingDocumentId
259
+ || (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
260
+ || (existingAsset.originalFilename || null) !== normalizedIncomingFilename
261
+ || (primaryLink?.taskId || null) !== normalizedIncomingTaskId
262
+ || (primaryLink?.displayName || null) !== normalizedIncomingCaption
263
+ || ((primaryLink?.role === 'reference' ? 'reference' : 'attachment')) !== normalizedIncomingLinkRole;
264
+ };
265
+ const hasBinaryAssetSyncMetadataDrift = (workspaceId, asset, options) => {
266
+ if (!workspaceAssetStore)
267
+ return false;
268
+ const existingAsset = workspaceAssetStore.getByStorageKey(asset.path, workspaceId);
269
+ if (!existingAsset || (existingAsset.kind !== 'image' && existingAsset.kind !== 'file'))
270
+ return false;
271
+ const { primaryLink } = resolveCanonicalAssetSyncLinkState(existingAsset, workspaceId);
272
+ const normalizedIncomingReference = Number.isFinite(Number(asset.referenceNumber))
273
+ ? Math.max(1, Math.floor(Number(asset.referenceNumber)))
274
+ : null;
275
+ const normalizedIncomingLogicalName = typeof asset.logicalName === 'string' && asset.logicalName.trim().length > 0
276
+ ? asset.logicalName.trim()
277
+ : null;
278
+ const normalizedIncomingCaption = typeof asset.caption === 'string' && asset.caption.trim().length > 0
279
+ ? asset.caption.trim()
280
+ : null;
281
+ const normalizedIncomingFilename = typeof asset.originalFilename === 'string' && asset.originalFilename.trim().length > 0
282
+ ? asset.originalFilename.trim()
283
+ : null;
284
+ const normalizedIncomingTaskId = typeof asset.taskId === 'string' && asset.taskId.trim().length > 0
285
+ ? asset.taskId.trim()
286
+ : null;
287
+ const normalizedIncomingLinkRole = asset.linkRole === 'reference'
288
+ ? 'reference'
289
+ : asset.kind === 'image'
290
+ ? 'image'
291
+ : 'attachment';
292
+ const allowReferenceNumberReassignment = options?.allowReferenceNumberReassignment === true;
293
+ if (!allowReferenceNumberReassignment
294
+ && existingAsset.referenceNumber
295
+ && normalizedIncomingReference
296
+ && existingAsset.referenceNumber !== normalizedIncomingReference) {
297
+ recordSyncDiagnostic(workspaceId, 'apply', 'Detected asset reference number mismatch during normal sync.', {
298
+ source: 'assets.referenceNumber.mismatch',
299
+ path: asset.path,
300
+ assetId: existingAsset.assetId,
301
+ existingReferenceNumber: existingAsset.referenceNumber,
302
+ incomingReferenceNumber: normalizedIncomingReference
303
+ });
304
+ }
305
+ return (allowReferenceNumberReassignment && existingAsset.referenceNumber !== normalizedIncomingReference)
306
+ || (existingAsset.logicalName || null) !== normalizedIncomingLogicalName
307
+ || (existingAsset.originalFilename || null) !== normalizedIncomingFilename
308
+ || (primaryLink?.taskId || null) !== normalizedIncomingTaskId
309
+ || (primaryLink?.displayName || null) !== normalizedIncomingCaption
310
+ || ((primaryLink?.role === 'reference'
311
+ ? 'reference'
312
+ : existingAsset.kind === 'image'
313
+ ? 'image'
314
+ : 'attachment')) !== normalizedIncomingLinkRole;
315
+ };
316
+ const reconcileWorkspaceSyncLinks = (input) => {
317
+ if (!workspaceAssetStore)
318
+ return;
319
+ const workspaceId = String(input.workspaceId || '').trim() || 'default';
320
+ const assetId = String(input.assetId || '').trim();
321
+ if (!assetId)
322
+ return;
323
+ const desiredTaskId = String(input.taskId || '').trim();
324
+ const desiredRole = input.role === 'reference'
325
+ ? 'reference'
326
+ : input.role === 'image'
327
+ ? 'image'
328
+ : 'attachment';
329
+ const linkUpdatedAt = String(input.updatedAt || '').trim() || new Date().toISOString();
330
+ const existingLinks = workspaceAssetStore.listLinksForAsset(assetId, workspaceId, { includeDeleted: true });
331
+ if (desiredTaskId) {
332
+ workspaceAssetStore.upsertLink({
333
+ workspaceId,
334
+ assetId,
335
+ taskId: desiredTaskId,
336
+ role: desiredRole,
337
+ displayName: typeof input.displayName === 'string' && input.displayName.trim().length > 0 ? input.displayName.trim() : null,
338
+ updatedAt: linkUpdatedAt,
339
+ deletedAt: null
340
+ });
341
+ }
342
+ // Only clean up stale links when we have an explicit incoming taskId. If the incoming
343
+ // asset-upsert carries no taskId, there is no authoritative association to enforce, so
344
+ // preserve all existing links rather than wiping them.
345
+ if (desiredTaskId) {
346
+ for (const link of existingLinks) {
347
+ if (link.taskId === desiredTaskId && link.role === desiredRole)
348
+ continue;
349
+ if (link.deletedAt)
350
+ continue;
351
+ workspaceAssetStore.deleteLink({
352
+ workspaceId,
353
+ assetId,
354
+ taskId: link.taskId,
355
+ role: link.role
356
+ });
357
+ }
358
+ }
359
+ };
360
+ const listWorkspaceSyncDocumentDeletesSnapshot = (workspaceId = 'default') => {
361
+ if (!workspaceAssetStore)
362
+ return [];
363
+ const entries = workspaceAssetStore.listAllByWorkspace(workspaceId, {
364
+ includeDeleted: true,
365
+ kind: 'document'
366
+ })
367
+ .filter((asset) => typeof asset.deletedAt === 'string' && asset.deletedAt.trim().length > 0)
368
+ .map((asset) => {
369
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(asset.storageKey);
370
+ if (!normalizedPath)
371
+ return null;
372
+ return {
373
+ path: normalizedPath,
374
+ deletedAt: String(asset.deletedAt || '').trim(),
375
+ assetId: asset.assetId
376
+ };
377
+ });
378
+ return entries.filter((entry) => Boolean(entry && entry.path && entry.deletedAt));
379
+ };
380
+ const listWorkspaceSyncDocumentReviewSessionsSnapshot = (workspaceId = 'default') => {
381
+ return core.listDocumentReviewSessionsForSync({ workspaceId, includeDeleted: true })
382
+ .map((session) => ({
383
+ id: session.id,
384
+ assetId: session.assetId,
385
+ documentId: session.documentId,
386
+ documentVersion: session.documentVersion,
387
+ title: session.title,
388
+ status: session.status,
389
+ comments: session.comments,
390
+ createdByActorId: session.createdByActorId,
391
+ updatedByActorId: session.updatedByActorId,
392
+ createdAt: session.createdAt,
393
+ updatedAt: session.updatedAt,
394
+ deletedAt: session.deletedAt,
395
+ }))
396
+ .filter((session) => session.id.length > 0 && session.assetId.length > 0);
397
+ };
398
+ const listWorkspaceSyncDocumentReviewCommentsSnapshot = (workspaceId = 'default') => {
399
+ return core.listDocumentReviewCommentsForSync({ workspaceId, includeDeleted: true })
400
+ .map((comment) => ({
401
+ id: comment.id,
402
+ sessionId: comment.sessionId,
403
+ body: comment.body,
404
+ anchor: comment.anchor,
405
+ order: comment.order,
406
+ authorActorId: comment.authorActorId,
407
+ createdAt: comment.createdAt,
408
+ updatedAt: comment.updatedAt,
409
+ deletedAt: comment.deletedAt,
410
+ }))
411
+ .filter((comment) => comment.id.length > 0 && comment.sessionId.length > 0);
412
+ };
413
+ const listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot = (workspaceId = 'default') => {
414
+ return core.listAnnotatedAttachmentSessionsForSync({ workspaceId, includeDeleted: true })
415
+ .map((session) => ({
416
+ id: session.id,
417
+ workspaceId: session.workspaceId,
418
+ taskId: session.taskId,
419
+ baseImageAssetId: session.baseImageAssetId,
420
+ title: session.title,
421
+ globalInstruction: session.globalInstruction,
422
+ annotations: session.annotations,
423
+ createdByActorId: session.createdByActorId,
424
+ updatedByActorId: session.updatedByActorId,
425
+ createdAt: session.createdAt,
426
+ updatedAt: session.updatedAt,
427
+ deletedAt: session.deletedAt,
428
+ }))
429
+ .filter((session) => (session.id.length > 0
430
+ && session.workspaceId.length > 0
431
+ && session.baseImageAssetId.length > 0));
432
+ };
433
+ const listWorkspaceSyncDocumentsSnapshot = async (basePath, workspaceId = 'default') => {
434
+ const root = path.resolve(basePath);
435
+ const objectStorageClient = getObjectStorageClient();
436
+ const candidatePaths = new Set();
437
+ const docsRoot = path.join(root, 'docs');
438
+ const collectMarkdownPaths = (directory) => {
439
+ if (!fs.existsSync(directory))
440
+ return;
441
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
442
+ for (const entry of entries) {
443
+ const absolutePath = path.join(directory, entry.name);
444
+ if (entry.isDirectory()) {
445
+ collectMarkdownPaths(absolutePath);
446
+ continue;
447
+ }
448
+ if (!entry.isFile())
449
+ continue;
450
+ const relativeFromRoot = path.relative(root, absolutePath).replace(/\\/g, '/');
451
+ const normalized = normalizeWorkspaceSyncDocumentPath(relativeFromRoot);
452
+ if (normalized)
453
+ candidatePaths.add(normalized);
454
+ }
455
+ };
456
+ collectMarkdownPaths(docsRoot);
457
+ if (workspaceAssetStore) {
458
+ const canonicalAssets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
459
+ includeDeleted: false,
460
+ kind: 'document'
461
+ });
462
+ for (const asset of canonicalAssets) {
463
+ const normalized = normalizeWorkspaceSyncDocumentPath(asset.storageKey);
464
+ if (normalized)
465
+ candidatePaths.add(normalized);
466
+ }
467
+ }
468
+ const uniquePaths = Array.from(candidatePaths).sort((a, b) => a.localeCompare(b));
469
+ const workspaceCache = workspaceDocumentSnapshotCache.get(workspaceId) || new Map();
470
+ workspaceDocumentSnapshotCache.set(workspaceId, workspaceCache);
471
+ const remainingPaths = new Set(uniquePaths);
472
+ const docs = [];
473
+ for (const relativePath of uniquePaths) {
474
+ try {
475
+ const canonicalAsset = workspaceAssetStore?.getByStorageKey(relativePath, workspaceId);
476
+ let stat = null;
477
+ let content = null;
478
+ let updatedAt = '';
479
+ if (canonicalAsset?.kind === 'document') {
480
+ updatedAt = resolveCanonicalAssetSyncLinkState(canonicalAsset, workspaceId).effectiveUpdatedAt;
481
+ if (canonicalAsset.storageProvider === 'r2' && objectStorageClient) {
482
+ const object = await objectStorageClient.getObject(relativePath);
483
+ if (!object?.body)
484
+ continue;
485
+ content = object.body.toString('utf8');
486
+ }
487
+ else {
488
+ const absPath = path.resolve(path.join(root, relativePath));
489
+ if (absPath !== root && !absPath.startsWith(`${root}${path.sep}`))
490
+ continue;
491
+ if (!fs.existsSync(absPath))
492
+ continue;
493
+ stat = fs.statSync(absPath);
494
+ if (!stat.isFile())
495
+ continue;
496
+ content = fs.readFileSync(absPath, 'utf8');
497
+ }
498
+ }
499
+ else {
500
+ const absPath = path.resolve(path.join(basePath, relativePath));
501
+ const safeRoot = path.resolve(basePath);
502
+ if (absPath !== safeRoot && !absPath.startsWith(`${safeRoot}${path.sep}`))
503
+ continue;
504
+ stat = fs.statSync(absPath);
505
+ if (!stat.isFile())
506
+ continue;
507
+ content = fs.readFileSync(absPath, 'utf8');
508
+ }
509
+ if (content == null)
510
+ continue;
511
+ const cached = workspaceCache.get(relativePath);
512
+ if (cached
513
+ && cached.size === Buffer.byteLength(content, 'utf8')
514
+ && (canonicalAsset
515
+ ? cached.updatedAt === updatedAt
516
+ : (stat != null && cached.mtimeMs === stat.mtimeMs && cached.ctimeMs === stat.ctimeMs))) {
517
+ docs.push({
518
+ path: relativePath,
519
+ updatedAt: cached.updatedAt,
520
+ content: cached.content,
521
+ ...getCanonicalSyncDocumentMetadata(relativePath, workspaceId)
522
+ });
523
+ continue;
524
+ }
525
+ if (!canonicalAsset) {
526
+ const contentHash = createHash('sha256').update(content).digest('hex');
527
+ const existing = core.getDocumentWatermark(workspaceId, relativePath);
528
+ if (existing && existing.contentHash === contentHash) {
529
+ updatedAt = existing.updatedAt;
530
+ }
531
+ else {
532
+ updatedAt = stat && Number.isFinite(stat.mtimeMs)
533
+ ? new Date(stat.mtimeMs).toISOString()
534
+ : new Date().toISOString();
535
+ try {
536
+ core.upsertDocumentWatermark(workspaceId, relativePath, contentHash, updatedAt);
537
+ }
538
+ catch (error) {
539
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to persist document watermark during snapshot.', {
540
+ source: 'documents.snapshot.upsertWatermark',
541
+ path: relativePath,
542
+ error: String(error?.message || error || '')
543
+ });
544
+ }
545
+ }
546
+ }
547
+ docs.push({
548
+ path: relativePath,
549
+ updatedAt,
550
+ content,
551
+ ...getCanonicalSyncDocumentMetadata(relativePath, workspaceId)
552
+ });
553
+ workspaceCache.set(relativePath, {
554
+ path: relativePath,
555
+ mtimeMs: stat?.mtimeMs ?? 0,
556
+ ctimeMs: stat?.ctimeMs ?? 0,
557
+ size: Buffer.byteLength(content, 'utf8'),
558
+ updatedAt,
559
+ content
560
+ });
561
+ }
562
+ catch (error) {
563
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read sync document snapshot entry.', {
564
+ source: 'documents.snapshot.readEntry',
565
+ path: relativePath,
566
+ error: String(error?.message || error || '')
567
+ });
568
+ }
569
+ }
570
+ for (const cachedPath of Array.from(workspaceCache.keys())) {
571
+ if (!remainingPaths.has(cachedPath))
572
+ workspaceCache.delete(cachedPath);
573
+ }
574
+ return docs;
575
+ };
576
+ const listWorkspaceSyncBinaryAssetsSnapshot = async (basePath, workspaceId = 'default') => {
577
+ if (!workspaceAssetStore)
578
+ return [];
579
+ const root = path.resolve(basePath);
580
+ const objectStorageClient = getObjectStorageClient();
581
+ const assets = workspaceAssetStore.listAllByWorkspace(workspaceId, {
582
+ includeDeleted: false
583
+ });
584
+ const snapshots = [];
585
+ for (const asset of assets) {
586
+ if (asset.kind !== 'image' && asset.kind !== 'file')
587
+ continue;
588
+ // External references are not syncable binary payloads.
589
+ if (asset.storageProvider === 'external')
590
+ continue;
591
+ const storageKey = String(asset.storageKey || '').trim();
592
+ if (!storageKey)
593
+ continue;
594
+ // Skip legacy task-data assets — they cannot be received on the other end
595
+ if (!normalizeTaskDataRelativePath(storageKey))
596
+ continue;
597
+ try {
598
+ let buffer = null;
599
+ if (asset.storageProvider === 'r2' && objectStorageClient) {
600
+ const object = await objectStorageClient.getObject(storageKey);
601
+ buffer = object?.body || null;
602
+ }
603
+ else {
604
+ const absolutePath = path.resolve(root, storageKey);
605
+ if (absolutePath !== root && !absolutePath.startsWith(`${root}${path.sep}`))
606
+ continue;
607
+ if (!fs.existsSync(absolutePath)) {
608
+ recordSyncDiagnostic(workspaceId, 'push', 'Asset registered in DB but file missing from disk — skipped from sync payload.', {
609
+ source: 'assets.snapshot.missingFile',
610
+ assetId: asset.assetId,
611
+ path: storageKey
612
+ });
613
+ continue;
614
+ }
615
+ buffer = fs.readFileSync(absolutePath);
616
+ }
617
+ if (!buffer) {
618
+ recordSyncDiagnostic(workspaceId, 'push', 'Asset registered in DB but content could not be read — skipped from sync payload.', {
619
+ source: 'assets.snapshot.emptyBuffer',
620
+ assetId: asset.assetId,
621
+ path: storageKey,
622
+ storageProvider: asset.storageProvider
623
+ });
624
+ continue;
625
+ }
626
+ const { primaryLink, effectiveUpdatedAt } = resolveCanonicalAssetSyncLinkState(asset, workspaceId);
627
+ snapshots.push({
628
+ path: storageKey,
629
+ updatedAt: effectiveUpdatedAt,
630
+ contentBase64: buffer.toString('base64'),
631
+ assetId: asset.assetId,
632
+ kind: asset.kind,
633
+ mimeType: asset.mimeType || 'application/octet-stream',
634
+ referenceNumber: asset.referenceNumber,
635
+ taskId: primaryLink?.taskId || null,
636
+ logicalName: asset.logicalName || null,
637
+ caption: primaryLink?.displayName || null,
638
+ originalFilename: asset.originalFilename || path.basename(storageKey),
639
+ linkRole: primaryLink?.role || (asset.kind === 'image' ? 'image' : 'attachment')
640
+ });
641
+ }
642
+ catch (error) {
643
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read binary asset snapshot entry.', {
644
+ source: 'assets.snapshot.readEntry',
645
+ path: storageKey,
646
+ error: String(error?.message || error || '')
647
+ });
648
+ }
649
+ }
650
+ return snapshots;
651
+ };
652
+ const listWorkspaceSyncBinaryAssetDeletesSnapshot = (workspaceId = 'default') => {
653
+ if (!workspaceAssetStore)
654
+ return [];
655
+ const entries = workspaceAssetStore.listAllByWorkspace(workspaceId, {
656
+ includeDeleted: true
657
+ })
658
+ .filter((asset) => ((asset.kind === 'image' || asset.kind === 'file')
659
+ && asset.storageProvider !== 'external'
660
+ && typeof asset.deletedAt === 'string'
661
+ && asset.deletedAt.trim().length > 0))
662
+ .map((asset) => {
663
+ const normalizedPath = normalizeTaskDataRelativePath(asset.storageKey);
664
+ if (!normalizedPath)
665
+ return null;
666
+ return {
667
+ path: normalizedPath,
668
+ deletedAt: String(asset.deletedAt || '').trim(),
669
+ assetId: asset.assetId
670
+ };
671
+ });
672
+ return entries.filter((entry) => Boolean(entry && entry.path && entry.deletedAt));
673
+ };
674
+ const writeWorkspaceSyncDocument = async (basePath, doc, workspaceId = 'default', options) => {
675
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(doc.path);
676
+ if (!normalizedPath)
677
+ return;
678
+ const root = path.resolve(basePath);
679
+ const targetPath = path.resolve(root, normalizedPath);
680
+ if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
681
+ return;
682
+ const objectStorageClient = getObjectStorageClient();
683
+ const content = String(doc.content || '');
684
+ if (objectStorageClient) {
685
+ try {
686
+ await objectStorageClient.putObject(normalizedPath, Buffer.from(content, 'utf8'), 'text/markdown');
687
+ }
688
+ catch (error) {
689
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to write document to object storage.', {
690
+ source: 'documents.write.putObject',
691
+ path: normalizedPath,
692
+ error: String(error?.message || error || '')
693
+ });
694
+ return;
695
+ }
696
+ }
697
+ else {
698
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
699
+ fs.writeFileSync(targetPath, content, 'utf8');
700
+ }
701
+ const updatedAt = String(doc.updatedAt || '').trim();
702
+ const watermarkMs = Date.parse(updatedAt);
703
+ if (!objectStorageClient && Number.isFinite(watermarkMs) && watermarkMs > 0) {
704
+ const when = new Date(watermarkMs);
705
+ fs.utimesSync(targetPath, when, when);
706
+ }
707
+ // Persist content hash + timestamp to SQLite so the snapshot function uses a
708
+ // stable watermark instead of relying on non-deterministic filesystem mtime.
709
+ try {
710
+ const contentHash = createHash('sha256').update(content).digest('hex');
711
+ core.upsertDocumentWatermark(workspaceId, normalizedPath, contentHash, updatedAt || new Date().toISOString());
712
+ if (workspaceAssetStore) {
713
+ const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
714
+ const effectiveUpdatedAt = updatedAt || new Date().toISOString();
715
+ const originalFilename = String(doc.originalFilename || existing?.originalFilename || path.basename(normalizedPath) || 'document.md').trim() || 'document.md';
716
+ const asset = workspaceAssetStore.upsertAsset({
717
+ assetId: String(doc.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
718
+ workspaceId,
719
+ kind: 'document',
720
+ logicalName: typeof doc.logicalName === 'string' && doc.logicalName.trim().length > 0
721
+ ? doc.logicalName.trim()
722
+ : existing?.logicalName || null,
723
+ originalFilename,
724
+ mimeType: existing?.mimeType || 'text/markdown',
725
+ storageProvider: objectStorageClient ? 'r2' : 'local',
726
+ storageKey: normalizedPath,
727
+ contentSha256: contentHash,
728
+ sizeBytes: Buffer.byteLength(content, 'utf8'),
729
+ documentId: String(doc.documentId || existing?.documentId || `doc-${randomUUID().replace(/-/g, '')}`),
730
+ referenceNumber: Number.isFinite(Number(doc.referenceNumber))
731
+ ? Math.max(1, Math.floor(Number(doc.referenceNumber)))
732
+ : existing?.referenceNumber || null,
733
+ version: Number.isFinite(Number(doc.version))
734
+ ? Math.max(1, Math.floor(Number(doc.version)))
735
+ : Math.max(1, Number(existing?.version || 0) + 1),
736
+ isLatest: true,
737
+ scanStatus: existing?.scanStatus || 'clean',
738
+ scanEngine: existing?.scanEngine || null,
739
+ scanDetails: existing?.scanDetails || null,
740
+ createdByUserId: existing?.createdByUserId || null,
741
+ createdAt: existing?.createdAt || effectiveUpdatedAt,
742
+ updatedAt: effectiveUpdatedAt,
743
+ deletedAt: null,
744
+ purgeAfter: null,
745
+ allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
746
+ });
747
+ const taskId = String(doc.taskId || '').trim();
748
+ reconcileWorkspaceSyncLinks({
749
+ workspaceId,
750
+ assetId: asset.assetId,
751
+ taskId,
752
+ displayName: typeof doc.caption === 'string' && doc.caption.trim().length > 0 ? doc.caption.trim() : null,
753
+ role: doc.linkRole === 'reference' ? 'reference' : 'attachment',
754
+ updatedAt: effectiveUpdatedAt
755
+ });
756
+ }
757
+ }
758
+ catch (error) {
759
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to persist document watermark after write.', {
760
+ source: 'documents.write.upsertWatermark',
761
+ path: normalizedPath,
762
+ error: String(error?.message || error || '')
763
+ });
764
+ }
765
+ workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
766
+ };
767
+ const writeWorkspaceSyncBinaryAsset = async (basePath, asset, workspaceId = 'default', options) => {
768
+ const normalizedPath = normalizeTaskDataRelativePath(asset.path);
769
+ if (!normalizedPath)
770
+ return;
771
+ const root = path.resolve(basePath);
772
+ const targetPath = path.resolve(root, normalizedPath);
773
+ if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
774
+ return;
775
+ const objectStorageClient = getObjectStorageClient();
776
+ const buffer = Buffer.from(String(asset.contentBase64 || ''), 'base64');
777
+ if (objectStorageClient) {
778
+ try {
779
+ await objectStorageClient.putObject(normalizedPath, buffer, String(asset.mimeType || 'application/octet-stream').trim() || 'application/octet-stream');
780
+ }
781
+ catch (error) {
782
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to write binary asset to object storage.', {
783
+ source: 'assets.write.putObject',
784
+ path: normalizedPath,
785
+ error: String(error?.message || error || '')
786
+ });
787
+ return;
788
+ }
789
+ }
790
+ else {
791
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
792
+ fs.writeFileSync(targetPath, buffer);
793
+ }
794
+ const updatedAt = String(asset.updatedAt || '').trim();
795
+ const watermarkMs = Date.parse(updatedAt);
796
+ if (!objectStorageClient && Number.isFinite(watermarkMs) && watermarkMs > 0) {
797
+ const when = new Date(watermarkMs);
798
+ fs.utimesSync(targetPath, when, when);
799
+ }
800
+ if (!workspaceAssetStore)
801
+ return;
802
+ try {
803
+ const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
804
+ const effectiveUpdatedAt = updatedAt || new Date().toISOString();
805
+ const assetRecord = workspaceAssetStore.upsertAsset({
806
+ assetId: String(asset.assetId || existing?.assetId || `asset-${randomUUID().replace(/-/g, '')}`),
807
+ workspaceId,
808
+ kind: asset.kind === 'image' ? 'image' : 'file',
809
+ logicalName: typeof asset.logicalName === 'string' && asset.logicalName.trim().length > 0
810
+ ? asset.logicalName.trim()
811
+ : existing?.logicalName || null,
812
+ originalFilename: String(asset.originalFilename || existing?.originalFilename || path.basename(normalizedPath) || 'asset').trim() || 'asset',
813
+ mimeType: String(asset.mimeType || existing?.mimeType || 'application/octet-stream').trim() || 'application/octet-stream',
814
+ storageProvider: objectStorageClient ? 'r2' : 'local',
815
+ storageKey: normalizedPath,
816
+ contentSha256: createHash('sha256').update(buffer).digest('hex'),
817
+ sizeBytes: buffer.length,
818
+ documentId: null,
819
+ referenceNumber: Number.isFinite(Number(asset.referenceNumber))
820
+ ? Math.max(1, Math.floor(Number(asset.referenceNumber)))
821
+ : existing?.referenceNumber || null,
822
+ version: null,
823
+ isLatest: true,
824
+ scanStatus: existing?.scanStatus || 'clean',
825
+ scanEngine: existing?.scanEngine || null,
826
+ scanDetails: existing?.scanDetails || null,
827
+ createdByUserId: existing?.createdByUserId || null,
828
+ createdAt: existing?.createdAt || effectiveUpdatedAt,
829
+ updatedAt: effectiveUpdatedAt,
830
+ deletedAt: null,
831
+ purgeAfter: null,
832
+ allowReferenceNumberReassignment: options?.allowReferenceNumberReassignment === true
833
+ });
834
+ const taskId = String(asset.taskId || '').trim();
835
+ reconcileWorkspaceSyncLinks({
836
+ workspaceId,
837
+ assetId: assetRecord.assetId,
838
+ taskId,
839
+ displayName: typeof asset.caption === 'string' && asset.caption.trim().length > 0 ? asset.caption.trim() : null,
840
+ role: asset.linkRole === 'reference' ? 'reference' : asset.kind === 'image' ? 'image' : 'attachment',
841
+ updatedAt: effectiveUpdatedAt
842
+ });
843
+ }
844
+ catch (error) {
845
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to persist binary asset metadata after write.', {
846
+ source: 'assets.write.upsertAsset',
847
+ path: normalizedPath,
848
+ assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
849
+ kind: asset.kind,
850
+ error: String(error?.message || error || '')
851
+ });
852
+ }
853
+ };
854
+ const deleteWorkspaceSyncBinaryAsset = (basePath, assetPath, workspaceId = 'default') => {
855
+ const normalizedPath = normalizeTaskDataRelativePath(assetPath);
856
+ if (!normalizedPath)
857
+ return;
858
+ const root = path.resolve(basePath);
859
+ const targetPath = path.resolve(root, normalizedPath);
860
+ const objectStorageClient = getObjectStorageClient();
861
+ try {
862
+ if (objectStorageClient) {
863
+ void objectStorageClient.deleteObject(normalizedPath);
864
+ }
865
+ else if (targetPath === root || targetPath.startsWith(`${root}${path.sep}`)) {
866
+ if (fs.existsSync(targetPath))
867
+ fs.unlinkSync(targetPath);
868
+ }
869
+ }
870
+ catch (error) {
871
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete workspace sync binary asset.', {
872
+ source: 'assets.delete.file',
873
+ path: normalizedPath,
874
+ error: String(error?.message || error || '')
875
+ });
876
+ }
877
+ if (workspaceAssetStore) {
878
+ const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
879
+ if (existing) {
880
+ const deletedAt = new Date().toISOString();
881
+ workspaceAssetStore.markDeleted(existing.assetId, workspaceId, deletedAt);
882
+ if (existing.kind === 'image') {
883
+ core.deleteAnnotatedAttachmentSessionsForImage({
884
+ workspaceId,
885
+ imageAssetId: existing.assetId,
886
+ deletedAt,
887
+ });
888
+ }
889
+ const links = workspaceAssetStore.listLinksForAsset(existing.assetId, workspaceId, { includeDeleted: true });
890
+ for (const link of links) {
891
+ workspaceAssetStore.deleteLink({
892
+ workspaceId,
893
+ assetId: existing.assetId,
894
+ taskId: link.taskId,
895
+ role: link.role
896
+ });
897
+ }
898
+ }
899
+ }
900
+ };
901
+ const deleteWorkspaceSyncDocument = (basePath, docPath, workspaceId = 'default') => {
902
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(docPath);
903
+ if (!normalizedPath)
904
+ return;
905
+ const root = path.resolve(basePath);
906
+ const targetPath = path.resolve(root, normalizedPath);
907
+ if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
908
+ return;
909
+ const objectStorageClient = getObjectStorageClient();
910
+ try {
911
+ if (objectStorageClient) {
912
+ void objectStorageClient.deleteObject(normalizedPath);
913
+ }
914
+ else if (fs.existsSync(targetPath)) {
915
+ fs.unlinkSync(targetPath);
916
+ }
917
+ }
918
+ catch (error) {
919
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete workspace sync document.', {
920
+ source: 'documents.delete.file',
921
+ path: normalizedPath,
922
+ error: String(error?.message || error || '')
923
+ });
924
+ }
925
+ try {
926
+ core.deleteDocumentWatermark(workspaceId, normalizedPath);
927
+ if (workspaceAssetStore) {
928
+ const existing = workspaceAssetStore.getByStorageKey(normalizedPath, workspaceId);
929
+ if (existing) {
930
+ const deletedAt = new Date().toISOString();
931
+ workspaceAssetStore.markDeleted(existing.assetId, workspaceId, deletedAt);
932
+ if (existing.kind === 'image') {
933
+ core.deleteAnnotatedAttachmentSessionsForImage({
934
+ workspaceId,
935
+ imageAssetId: existing.assetId,
936
+ deletedAt,
937
+ });
938
+ }
939
+ const links = workspaceAssetStore.listLinksForAsset(existing.assetId, workspaceId, { includeDeleted: true });
940
+ for (const link of links) {
941
+ workspaceAssetStore.deleteLink({
942
+ workspaceId,
943
+ assetId: existing.assetId,
944
+ taskId: link.taskId,
945
+ role: link.role
946
+ });
947
+ }
948
+ }
949
+ }
950
+ }
951
+ catch (error) {
952
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document watermark.', {
953
+ source: 'documents.delete.watermark',
954
+ path: normalizedPath,
955
+ error: String(error?.message || error || '')
956
+ });
957
+ }
958
+ workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
959
+ };
960
+ const resolveConflictOrdering = (incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak) => (compareWorkspaceMutationOrdering(incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak));
961
+ const dispatchWorkspaceSyncChanges = async (workspaceId, incomingChanges, options) => {
962
+ const tasks = [];
963
+ const archived = [];
964
+ const incomingTaskDeletes = new Map();
965
+ const emittedEventIds = new Set();
966
+ const unsubscribeRealtime = core.onRealtimeMutation((event) => {
967
+ const eventWorkspaceId = String(event?.workspaceId || '').trim();
968
+ if (eventWorkspaceId !== workspaceId)
969
+ return;
970
+ const eventId = String(event?.eventId || '').trim();
971
+ if (eventId)
972
+ emittedEventIds.add(eventId);
973
+ });
974
+ let incomingTaxonomies = null;
975
+ let incomingTaxonomyState = null;
976
+ let incomingTaxonomiesUpdatedAt = '';
977
+ const incomingInitiatives = new Map();
978
+ const incomingWorkstreams = new Map();
979
+ const incomingAiProfiles = new Map();
980
+ const incomingDocuments = [];
981
+ const incomingDocumentDeletes = new Map();
982
+ const incomingDocumentReviewSessions = new Map();
983
+ const incomingDocumentReviewSessionDeletes = new Map();
984
+ const incomingDocumentReviewComments = new Map();
985
+ const incomingDocumentReviewCommentDeletes = new Map();
986
+ const incomingAnnotatedAttachmentSessions = new Map();
987
+ const incomingAnnotatedAttachmentSessionDeletes = new Map();
988
+ const incomingAssets = [];
989
+ const incomingAssetDeletes = new Map();
990
+ try {
991
+ for (const change of incomingChanges) {
992
+ const raw = change;
993
+ const op = String(raw.op || '').trim().toLowerCase();
994
+ if (op === 'taxonomy-upsert') {
995
+ if (raw.taxonomyState && typeof raw.taxonomyState === 'object' && !Array.isArray(raw.taxonomyState)) {
996
+ incomingTaxonomyState = raw.taxonomyState;
997
+ incomingTaxonomiesUpdatedAt = String(raw.updatedAt || raw.watermark || '').trim();
998
+ }
999
+ else if (Array.isArray(raw.taxonomies)) {
1000
+ incomingTaxonomies = raw.taxonomies;
1001
+ incomingTaxonomiesUpdatedAt = String(raw.updatedAt || raw.watermark || '').trim();
1002
+ }
1003
+ continue;
1004
+ }
1005
+ if (op === 'initiative-upsert') {
1006
+ const initiative = raw.initiative && typeof raw.initiative === 'object'
1007
+ ? raw.initiative
1008
+ : null;
1009
+ const initiativeId = String(initiative?.id || '').trim();
1010
+ if (initiativeId) {
1011
+ incomingInitiatives.set(initiativeId, {
1012
+ id: initiativeId,
1013
+ referenceNumber: Number.isFinite(Number(initiative?.referenceNumber))
1014
+ ? Math.max(1, Math.floor(Number(initiative?.referenceNumber)))
1015
+ : null,
1016
+ referenceLabel: typeof initiative?.referenceLabel === 'string' ? initiative.referenceLabel.trim() : undefined,
1017
+ title: String(initiative?.title || '').trim(),
1018
+ description: typeof initiative?.description === 'string' ? initiative.description : null,
1019
+ ownerId: typeof initiative?.ownerId === 'string' ? initiative.ownerId.trim() : null,
1020
+ createdAt: String(initiative?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1021
+ updatedAt: String(initiative?.updatedAt || raw.updatedAt || raw.watermark || initiative?.createdAt || '').trim(),
1022
+ order: Number.isFinite(Number(initiative?.order)) ? Math.floor(Number(initiative?.order)) : null,
1023
+ isArchived: Boolean(initiative?.isArchived)
1024
+ });
1025
+ }
1026
+ continue;
1027
+ }
1028
+ if (op === 'workstream-upsert') {
1029
+ const workstream = raw.workstream && typeof raw.workstream === 'object'
1030
+ ? raw.workstream
1031
+ : null;
1032
+ const workstreamId = String(workstream?.id || '').trim();
1033
+ if (workstreamId) {
1034
+ incomingWorkstreams.set(workstreamId, {
1035
+ id: workstreamId,
1036
+ referenceNumber: Number.isFinite(Number(workstream?.referenceNumber))
1037
+ ? Math.max(1, Math.floor(Number(workstream?.referenceNumber)))
1038
+ : null,
1039
+ referenceLabel: typeof workstream?.referenceLabel === 'string' ? workstream.referenceLabel.trim() : undefined,
1040
+ initiativeId: typeof workstream?.initiativeId === 'string' && workstream.initiativeId.trim().length > 0
1041
+ ? workstream.initiativeId.trim()
1042
+ : null,
1043
+ title: String(workstream?.title || '').trim(),
1044
+ description: typeof workstream?.description === 'string' ? workstream.description : null,
1045
+ ownerId: typeof workstream?.ownerId === 'string' ? workstream.ownerId.trim() : null,
1046
+ createdAt: String(workstream?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1047
+ updatedAt: String(workstream?.updatedAt || raw.updatedAt || raw.watermark || workstream?.createdAt || '').trim(),
1048
+ order: Number.isFinite(Number(workstream?.order)) ? Math.floor(Number(workstream?.order)) : null,
1049
+ isArchived: Boolean(workstream?.isArchived)
1050
+ });
1051
+ }
1052
+ continue;
1053
+ }
1054
+ if (op === 'ai-profile-upsert') {
1055
+ const profile = raw.profile && typeof raw.profile === 'object'
1056
+ ? raw.profile
1057
+ : null;
1058
+ const profileId = String(profile?.id || '').trim();
1059
+ if (profileId) {
1060
+ const profileWorkspaceId = String(profile?.workspaceId || '').trim();
1061
+ if (profileWorkspaceId && profileWorkspaceId !== workspaceId) {
1062
+ recordSyncDiagnostic(workspaceId, 'apply', `Rejected ai-profile-upsert for profile ${profileId}: workspace mismatch (expected ${workspaceId}, got ${profileWorkspaceId}).`, {
1063
+ source: 'apply.aiProfileWorkspaceMismatch',
1064
+ workspaceId,
1065
+ profileId,
1066
+ expectedWorkspaceId: workspaceId,
1067
+ receivedWorkspaceId: profileWorkspaceId
1068
+ });
1069
+ continue;
1070
+ }
1071
+ incomingAiProfiles.set(profileId, {
1072
+ id: profileId,
1073
+ workspaceId,
1074
+ profileToken: String(profile?.profileToken || '').trim(),
1075
+ name: String(profile?.name || '').trim(),
1076
+ username: String(profile?.username || '').trim(),
1077
+ icon: String(profile?.icon || 'Bot').trim() || 'Bot',
1078
+ color: String(profile?.color || '#8b5cf6').trim() || '#8b5cf6',
1079
+ surfaceType: normalizeIncomingAiProfileSurfaceType(profile?.surfaceType, {
1080
+ source: 'apply.aiProfileUpsert',
1081
+ profileId,
1082
+ workspaceId
1083
+ }),
1084
+ providerMetadata: profile?.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
1085
+ ? profile.providerMetadata
1086
+ : null,
1087
+ createdAt: String(profile?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1088
+ updatedAt: String(profile?.updatedAt || raw.updatedAt || raw.watermark || '').trim(),
1089
+ lastActiveAt: typeof profile?.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
1090
+ ? profile.lastActiveAt.trim()
1091
+ : null
1092
+ });
1093
+ }
1094
+ continue;
1095
+ }
1096
+ if (op === 'document-upsert') {
1097
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(raw.path);
1098
+ if (normalizedPath) {
1099
+ incomingDocuments.push({
1100
+ path: normalizedPath,
1101
+ updatedAt: String(raw.updatedAt || raw.watermark || '').trim(),
1102
+ content: typeof raw.content === 'string' ? raw.content : '',
1103
+ assetId: typeof raw.assetId === 'string' ? raw.assetId.trim() : undefined,
1104
+ documentId: typeof raw.documentId === 'string' ? raw.documentId.trim() : null,
1105
+ referenceNumber: Number.isFinite(Number(raw.referenceNumber))
1106
+ ? Math.max(1, Math.floor(Number(raw.referenceNumber)))
1107
+ : null,
1108
+ version: Number.isFinite(Number(raw.version)) ? Math.max(1, Math.floor(Number(raw.version))) : null,
1109
+ taskId: typeof raw.taskId === 'string' ? raw.taskId.trim() : null,
1110
+ logicalName: typeof raw.logicalName === 'string' ? raw.logicalName.trim() : null,
1111
+ caption: typeof raw.caption === 'string' ? raw.caption.trim() : null,
1112
+ originalFilename: typeof raw.originalFilename === 'string' ? raw.originalFilename.trim() : null,
1113
+ linkRole: raw.linkRole === 'reference' ? 'reference' : 'attachment'
1114
+ });
1115
+ }
1116
+ continue;
1117
+ }
1118
+ if (op === 'document-delete') {
1119
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(raw.path);
1120
+ if (normalizedPath) {
1121
+ incomingDocumentDeletes.set(normalizedPath, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1122
+ }
1123
+ continue;
1124
+ }
1125
+ if (op === 'asset-upsert') {
1126
+ const normalizedPath = normalizeTaskDataRelativePath(String(raw.path || '').trim());
1127
+ if (normalizedPath) {
1128
+ incomingAssets.push({
1129
+ path: normalizedPath,
1130
+ updatedAt: String(raw.updatedAt || raw.watermark || '').trim(),
1131
+ contentBase64: typeof raw.contentBase64 === 'string' ? raw.contentBase64 : '',
1132
+ assetId: typeof raw.assetId === 'string' ? raw.assetId.trim() : undefined,
1133
+ kind: raw.kind === 'image' ? 'image' : 'file',
1134
+ mimeType: typeof raw.mimeType === 'string' ? raw.mimeType.trim() : 'application/octet-stream',
1135
+ referenceNumber: Number.isFinite(Number(raw.referenceNumber))
1136
+ ? Math.max(1, Math.floor(Number(raw.referenceNumber)))
1137
+ : null,
1138
+ taskId: typeof raw.taskId === 'string' ? raw.taskId.trim() : null,
1139
+ logicalName: typeof raw.logicalName === 'string' ? raw.logicalName.trim() : null,
1140
+ caption: typeof raw.caption === 'string' ? raw.caption.trim() : null,
1141
+ originalFilename: typeof raw.originalFilename === 'string' ? raw.originalFilename.trim() : null,
1142
+ linkRole: raw.linkRole === 'reference' ? 'reference' : raw.linkRole === 'image' ? 'image' : 'attachment'
1143
+ });
1144
+ }
1145
+ continue;
1146
+ }
1147
+ if (op === 'asset-delete') {
1148
+ const normalizedPath = normalizeTaskDataRelativePath(String(raw.path || '').trim());
1149
+ if (normalizedPath) {
1150
+ incomingAssetDeletes.set(normalizedPath, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1151
+ }
1152
+ continue;
1153
+ }
1154
+ if (op === 'document-review-session-upsert') {
1155
+ const session = raw.session && typeof raw.session === 'object'
1156
+ ? raw.session
1157
+ : null;
1158
+ const sessionId = String(session?.id || '').trim();
1159
+ const assetId = String(session?.assetId || '').trim();
1160
+ if (sessionId && assetId) {
1161
+ incomingDocumentReviewSessions.set(sessionId, {
1162
+ id: sessionId,
1163
+ assetId,
1164
+ documentId: typeof session?.documentId === 'string' ? session.documentId.trim() : null,
1165
+ documentVersion: Number.isFinite(Number(session?.documentVersion)) ? Math.max(1, Math.floor(Number(session?.documentVersion))) : null,
1166
+ title: typeof session?.title === 'string' ? session.title.trim() : null,
1167
+ status: session?.status === 'resolved' ? 'resolved' : 'open',
1168
+ comments: Array.isArray(session?.comments) ? session.comments : [],
1169
+ createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
1170
+ updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
1171
+ createdAt: String(session?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1172
+ updatedAt: String(session?.updatedAt || raw.updatedAt || raw.watermark || session?.createdAt || '').trim(),
1173
+ deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null,
1174
+ });
1175
+ }
1176
+ continue;
1177
+ }
1178
+ if (op === 'document-review-session-delete') {
1179
+ const sessionId = String(raw.sessionId || raw.id || '').trim();
1180
+ if (sessionId) {
1181
+ incomingDocumentReviewSessionDeletes.set(sessionId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1182
+ }
1183
+ continue;
1184
+ }
1185
+ if (op === 'document-review-comment-upsert') {
1186
+ const comment = raw.comment && typeof raw.comment === 'object'
1187
+ ? raw.comment
1188
+ : null;
1189
+ const commentId = String(comment?.id || '').trim();
1190
+ const sessionId = String(comment?.sessionId || '').trim();
1191
+ if (commentId && sessionId) {
1192
+ incomingDocumentReviewComments.set(commentId, {
1193
+ id: commentId,
1194
+ sessionId,
1195
+ body: typeof comment?.body === 'string' ? comment.body : '',
1196
+ anchor: comment?.anchor ?? null,
1197
+ order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment?.order))) : 0,
1198
+ authorActorId: typeof comment?.authorActorId === 'string' ? comment.authorActorId.trim() : null,
1199
+ createdAt: String(comment?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1200
+ updatedAt: String(comment?.updatedAt || raw.updatedAt || raw.watermark || comment?.createdAt || '').trim(),
1201
+ deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
1202
+ });
1203
+ }
1204
+ continue;
1205
+ }
1206
+ if (op === 'document-review-comment-delete') {
1207
+ const commentId = String(raw.commentId || raw.id || '').trim();
1208
+ if (commentId) {
1209
+ incomingDocumentReviewCommentDeletes.set(commentId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1210
+ }
1211
+ continue;
1212
+ }
1213
+ if (op === 'annotated-attachment-session-upsert') {
1214
+ const session = raw.session && typeof raw.session === 'object'
1215
+ ? raw.session
1216
+ : null;
1217
+ const sessionId = String(session?.id || '').trim();
1218
+ const sessionWorkspaceId = String(session?.workspaceId || '').trim();
1219
+ const taskId = String(session?.taskId || '').trim();
1220
+ const baseImageAssetId = String(session?.baseImageAssetId || '').trim();
1221
+ if (sessionId
1222
+ && sessionWorkspaceId === workspaceId
1223
+ && baseImageAssetId) {
1224
+ incomingAnnotatedAttachmentSessions.set(sessionId, {
1225
+ id: sessionId,
1226
+ workspaceId: sessionWorkspaceId,
1227
+ taskId,
1228
+ baseImageAssetId,
1229
+ title: typeof session?.title === 'string' ? session.title.trim() : null,
1230
+ globalInstruction: typeof session?.globalInstruction === 'string' ? session.globalInstruction.trim() : null,
1231
+ annotations: Array.isArray(session?.annotations) ? session.annotations : [],
1232
+ createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
1233
+ updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
1234
+ createdAt: String(session?.createdAt || raw.updatedAt || raw.watermark || '').trim(),
1235
+ updatedAt: String(session?.updatedAt || raw.updatedAt || raw.watermark || session?.createdAt || '').trim(),
1236
+ deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null,
1237
+ });
1238
+ }
1239
+ continue;
1240
+ }
1241
+ if (op === 'annotated-attachment-session-delete') {
1242
+ const sessionId = String(raw.sessionId || raw.id || '').trim();
1243
+ if (sessionId) {
1244
+ incomingAnnotatedAttachmentSessionDeletes.set(sessionId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1245
+ }
1246
+ continue;
1247
+ }
1248
+ if (op !== 'delete')
1249
+ continue;
1250
+ const task = raw.task;
1251
+ const deleteTaskId = String(raw.taskId || raw.id || task?.id || '').trim();
1252
+ if (deleteTaskId) {
1253
+ incomingTaskDeletes.set(deleteTaskId, String(raw.deletedAt || raw.watermark || new Date().toISOString()).trim());
1254
+ }
1255
+ }
1256
+ for (const change of incomingChanges) {
1257
+ const raw = change;
1258
+ const op = String(raw.op || '').trim().toLowerCase();
1259
+ if (op === 'delete'
1260
+ || op === 'initiative-upsert'
1261
+ || op === 'workstream-upsert'
1262
+ || op === 'document-upsert'
1263
+ || op === 'document-delete'
1264
+ || op === 'asset-upsert'
1265
+ || op === 'asset-delete'
1266
+ || op === 'document-review-session-upsert'
1267
+ || op === 'document-review-session-delete'
1268
+ || op === 'document-review-comment-upsert'
1269
+ || op === 'document-review-comment-delete'
1270
+ || op === 'annotated-attachment-session-upsert'
1271
+ || op === 'annotated-attachment-session-delete')
1272
+ continue;
1273
+ const task = raw.task && typeof raw.task === 'object' ? raw.task : null;
1274
+ if (!task)
1275
+ continue;
1276
+ const taskId = String(task.id || '').trim();
1277
+ const deleteWatermark = taskId ? String(incomingTaskDeletes.get(taskId) || '').trim() : '';
1278
+ if (taskId && deleteWatermark) {
1279
+ const compareAgainstDelete = resolveConflictOrdering(deleteWatermark, String(task.updatedAt || task.createdAt || '').trim(), `delete:${taskId}`, getTaskMutationTieBreak(task, raw.archived === true || task?.isArchived === true));
1280
+ if (compareAgainstDelete >= 0)
1281
+ continue;
1282
+ }
1283
+ const markedArchived = raw.archived === true || task?.isArchived === true;
1284
+ if (markedArchived)
1285
+ archived.push(task);
1286
+ else
1287
+ tasks.push(task);
1288
+ }
1289
+ for (const [taskId, deleteWatermark] of incomingTaskDeletes.entries()) {
1290
+ try {
1291
+ const existingTask = core.getTask(taskId, workspaceId);
1292
+ if (existingTask) {
1293
+ const compareAgainstExisting = resolveConflictOrdering(deleteWatermark, getTaskMutationWatermark(existingTask), `delete:${taskId}`, getTaskMutationTieBreak(existingTask, Boolean(existingTask.isArchived)));
1294
+ if (compareAgainstExisting < 0)
1295
+ continue;
1296
+ }
1297
+ core.deleteTaskForSync(taskId, workspaceId, { persistBackup: false });
1298
+ }
1299
+ catch (error) {
1300
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete task during sync apply.', {
1301
+ source: 'apply.deleteTask',
1302
+ taskId,
1303
+ error: String(error?.message || error || '')
1304
+ });
1305
+ }
1306
+ }
1307
+ if (incomingInitiatives.size > 0) {
1308
+ for (const initiative of incomingInitiatives.values()) {
1309
+ try {
1310
+ core.upsertInitiativeForSync({
1311
+ id: initiative.id,
1312
+ referenceNumber: initiative.referenceNumber,
1313
+ referenceLabel: initiative.referenceLabel,
1314
+ title: initiative.title,
1315
+ description: initiative.description,
1316
+ ownerId: initiative.ownerId,
1317
+ createdAt: initiative.createdAt,
1318
+ updatedAt: initiative.updatedAt,
1319
+ order: initiative.order,
1320
+ isArchived: initiative.isArchived
1321
+ }, workspaceId);
1322
+ }
1323
+ catch (error) {
1324
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert initiative during sync apply.', {
1325
+ source: 'apply.initiativeUpsert',
1326
+ initiativeId: initiative.id,
1327
+ error: String(error?.message || error || '')
1328
+ });
1329
+ throw error;
1330
+ }
1331
+ }
1332
+ }
1333
+ if (incomingWorkstreams.size > 0) {
1334
+ for (const workstream of incomingWorkstreams.values()) {
1335
+ try {
1336
+ core.upsertWorkstreamForSync({
1337
+ id: workstream.id,
1338
+ referenceNumber: workstream.referenceNumber,
1339
+ referenceLabel: workstream.referenceLabel,
1340
+ initiativeId: workstream.initiativeId,
1341
+ title: workstream.title,
1342
+ description: workstream.description,
1343
+ ownerId: workstream.ownerId,
1344
+ createdAt: workstream.createdAt,
1345
+ updatedAt: workstream.updatedAt,
1346
+ order: workstream.order,
1347
+ isArchived: workstream.isArchived
1348
+ }, workspaceId);
1349
+ }
1350
+ catch (error) {
1351
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert workstream during sync apply.', {
1352
+ source: 'apply.workstreamUpsert',
1353
+ workstreamId: workstream.id,
1354
+ error: String(error?.message || error || '')
1355
+ });
1356
+ throw error;
1357
+ }
1358
+ }
1359
+ }
1360
+ if (incomingTaxonomyState || Array.isArray(incomingTaxonomies)) {
1361
+ const nextTaxonomyState = incomingTaxonomyState || { taxonomies: incomingTaxonomies || [] };
1362
+ if (!incomingTaxonomiesUpdatedAt) {
1363
+ if (typeof core.updateTaxonomyState === 'function') {
1364
+ core.updateTaxonomyState(nextTaxonomyState, workspaceId);
1365
+ }
1366
+ else {
1367
+ core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
1368
+ }
1369
+ const fallbackHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
1370
+ workspaceTaxonomySyncMeta.set(workspaceId, {
1371
+ updatedAt: new Date().toISOString(),
1372
+ hash: fallbackHash
1373
+ });
1374
+ }
1375
+ else {
1376
+ const incomingTaxonomyHash = createHash('sha256').update(JSON.stringify(nextTaxonomyState)).digest('hex');
1377
+ const currentMeta = workspaceTaxonomySyncMeta.get(workspaceId) || { updatedAt: '', hash: '' };
1378
+ const compare = resolveConflictOrdering(incomingTaxonomiesUpdatedAt, currentMeta.updatedAt, incomingTaxonomyHash, currentMeta.hash);
1379
+ if (compare >= 0) {
1380
+ if (typeof core.updateTaxonomyState === 'function') {
1381
+ core.updateTaxonomyState(nextTaxonomyState, workspaceId);
1382
+ }
1383
+ else {
1384
+ core.updateTaxonomies(Array.isArray(incomingTaxonomies) ? incomingTaxonomies : []);
1385
+ }
1386
+ workspaceTaxonomySyncMeta.set(workspaceId, {
1387
+ updatedAt: incomingTaxonomiesUpdatedAt,
1388
+ hash: incomingTaxonomyHash
1389
+ });
1390
+ }
1391
+ }
1392
+ }
1393
+ if (incomingAiProfiles.size > 0) {
1394
+ for (const profile of incomingAiProfiles.values()) {
1395
+ try {
1396
+ core.upsertAiProfileForSync({
1397
+ id: profile.id,
1398
+ workspaceId: workspaceId,
1399
+ profileToken: profile.profileToken,
1400
+ name: profile.name,
1401
+ username: profile.username,
1402
+ icon: profile.icon,
1403
+ color: profile.color,
1404
+ surfaceType: profile.surfaceType,
1405
+ providerMetadata: profile.providerMetadata,
1406
+ createdAt: profile.createdAt,
1407
+ updatedAt: profile.updatedAt,
1408
+ lastActiveAt: profile.lastActiveAt
1409
+ });
1410
+ }
1411
+ catch (error) {
1412
+ const details = {
1413
+ source: 'apply.aiProfileUpsert',
1414
+ workspaceId,
1415
+ profileId: profile.id,
1416
+ error: String(error?.message || error || ''),
1417
+ errorCode: String(error?.code || '').trim() || null
1418
+ };
1419
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert AI profile during sync apply.', details);
1420
+ throw createSyncRouteError(`Failed to persist AI profile ${profile.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'AI_PROFILE_SYNC_APPLY_FAILED', statusCode: 500 });
1421
+ }
1422
+ }
1423
+ assertSyncedAiProfilesPersisted(workspaceId, incomingAiProfiles);
1424
+ }
1425
+ if (Array.isArray(options?.workspaceMembers)) {
1426
+ core.replaceWorkspaceUsersForSync({
1427
+ workspaceId,
1428
+ users: options.workspaceMembers
1429
+ .filter((member) => String(member?.workspaceId || workspaceId).trim() === workspaceId)
1430
+ .map((member) => ({
1431
+ userId: member.userId,
1432
+ email: member.email,
1433
+ displayName: member.displayName,
1434
+ role: member.role,
1435
+ permissionMode: member.permissionMode,
1436
+ betaAccess: member.betaAccess,
1437
+ status: member.status,
1438
+ disabled: member.disabled,
1439
+ createdAt: member.createdAt,
1440
+ updatedAt: member.updatedAt,
1441
+ emailVerifiedAt: member.emailVerifiedAt
1442
+ }))
1443
+ });
1444
+ }
1445
+ validateTaskActorAssignments(workspaceId, tasks, archived);
1446
+ if (incomingDocuments.length > 0) {
1447
+ for (const doc of incomingDocuments) {
1448
+ try {
1449
+ const existingDoc = core.getDocumentWatermark(workspaceId, doc.path);
1450
+ const incomingHash = createHash('sha256').update(String(doc.content || '')).digest('hex');
1451
+ const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
1452
+ const existingHash = String(existingDoc?.contentHash || '').trim();
1453
+ const compare = resolveConflictOrdering(doc.updatedAt, existingUpdatedAt, incomingHash, existingHash);
1454
+ const metadataDrift = hasDocumentSyncMetadataDrift(workspaceId, doc, options);
1455
+ if (compare < 0)
1456
+ continue;
1457
+ if (compare === 0 && existingHash === incomingHash && !metadataDrift)
1458
+ continue;
1459
+ await writeWorkspaceSyncDocument(core.getPaths().basePath, doc, workspaceId, options);
1460
+ }
1461
+ catch (error) {
1462
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced document change.', {
1463
+ source: 'documents.apply.write',
1464
+ path: doc.path,
1465
+ assetId: typeof doc.assetId === 'string' ? doc.assetId : null,
1466
+ error: String(error?.message || error || '')
1467
+ });
1468
+ }
1469
+ }
1470
+ }
1471
+ if (incomingDocumentDeletes.size > 0) {
1472
+ for (const [docPath, deletedAt] of incomingDocumentDeletes.entries()) {
1473
+ const existingDoc = core.getDocumentWatermark(workspaceId, docPath);
1474
+ const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
1475
+ const existingHash = String(existingDoc?.contentHash || '').trim();
1476
+ const compare = resolveConflictOrdering(deletedAt, existingUpdatedAt, `delete:${docPath}`, existingHash);
1477
+ if (existingDoc && compare < 0)
1478
+ continue;
1479
+ deleteWorkspaceSyncDocument(core.getPaths().basePath, docPath, workspaceId);
1480
+ }
1481
+ }
1482
+ if (incomingAssets.length > 0) {
1483
+ for (const asset of incomingAssets) {
1484
+ try {
1485
+ const existing = workspaceAssetStore?.getByStorageKey(asset.path, workspaceId);
1486
+ const incomingHash = createHash('sha256').update(Buffer.from(String(asset.contentBase64 || ''), 'base64')).digest('hex');
1487
+ const existingUpdatedAt = String(existing?.updatedAt || '').trim();
1488
+ const existingHash = String(existing?.contentSha256 || '').trim();
1489
+ const assetPath = path.resolve(core.getPaths().basePath, asset.path);
1490
+ const localFileMissing = !getObjectStorageClient() && !fs.existsSync(assetPath);
1491
+ const compare = resolveConflictOrdering(asset.updatedAt, existingUpdatedAt, incomingHash, existingHash);
1492
+ const metadataDrift = hasBinaryAssetSyncMetadataDrift(workspaceId, asset, options);
1493
+ if (compare < 0)
1494
+ continue;
1495
+ if (compare === 0 && existingHash === incomingHash && !localFileMissing && !metadataDrift)
1496
+ continue;
1497
+ await writeWorkspaceSyncBinaryAsset(core.getPaths().basePath, asset, workspaceId, options);
1498
+ }
1499
+ catch (error) {
1500
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to apply synced asset change.', {
1501
+ source: 'assets.apply.write',
1502
+ path: asset.path,
1503
+ assetId: typeof asset.assetId === 'string' ? asset.assetId : null,
1504
+ kind: asset.kind,
1505
+ error: String(error?.message || error || '')
1506
+ });
1507
+ }
1508
+ }
1509
+ }
1510
+ if (incomingAssetDeletes.size > 0) {
1511
+ for (const [assetPath, deletedAt] of incomingAssetDeletes.entries()) {
1512
+ const existing = workspaceAssetStore?.getByStorageKey(assetPath, workspaceId);
1513
+ const existingUpdatedAt = String(existing?.updatedAt || '').trim();
1514
+ const existingHash = String(existing?.contentSha256 || '').trim();
1515
+ const compare = resolveConflictOrdering(deletedAt, existingUpdatedAt, `delete:${assetPath}`, existingHash);
1516
+ if (existing && compare < 0)
1517
+ continue;
1518
+ deleteWorkspaceSyncBinaryAsset(core.getPaths().basePath, assetPath, workspaceId);
1519
+ }
1520
+ }
1521
+ if (incomingDocumentReviewSessions.size > 0) {
1522
+ for (const session of incomingDocumentReviewSessions.values()) {
1523
+ try {
1524
+ const existing = core.getDocumentReviewSession({
1525
+ workspaceId,
1526
+ sessionId: session.id,
1527
+ includeDeleted: true
1528
+ });
1529
+ const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.comments || [])).digest('hex');
1530
+ const existingTieBreak = existing
1531
+ ? createHash('sha256').update(JSON.stringify(existing.comments || [])).digest('hex')
1532
+ : '';
1533
+ const compare = resolveConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
1534
+ if (compare < 0)
1535
+ continue;
1536
+ core.upsertDocumentReviewSessionForSync({
1537
+ id: session.id,
1538
+ tenantId: core.getTenantId(),
1539
+ workspaceId,
1540
+ assetId: session.assetId,
1541
+ documentId: session.documentId,
1542
+ documentVersion: session.documentVersion,
1543
+ title: session.title,
1544
+ status: session.status,
1545
+ comments: [],
1546
+ createdByActorId: session.createdByActorId,
1547
+ updatedByActorId: session.updatedByActorId,
1548
+ createdAt: session.createdAt,
1549
+ updatedAt: session.updatedAt,
1550
+ deletedAt: session.deletedAt,
1551
+ });
1552
+ }
1553
+ catch (error) {
1554
+ const details = {
1555
+ source: 'apply.documentReviewUpsert',
1556
+ sessionId: session.id,
1557
+ error: String(error?.message || error || ''),
1558
+ errorCode: String(error?.code || '').trim() || null
1559
+ };
1560
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review session during sync apply.', details);
1561
+ throw createSyncRouteError(`Failed to persist document review session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
1562
+ }
1563
+ }
1564
+ }
1565
+ if (incomingDocumentReviewComments.size > 0) {
1566
+ for (const comment of incomingDocumentReviewComments.values()) {
1567
+ try {
1568
+ const existingComments = core.listDocumentReviewCommentsForSync({
1569
+ workspaceId,
1570
+ includeDeleted: true
1571
+ });
1572
+ const existing = existingComments.find((entry) => entry.id === comment.id) || null;
1573
+ const incomingTieBreak = createHash('sha256')
1574
+ .update(JSON.stringify({ body: comment.body, anchor: comment.anchor, deletedAt: comment.deletedAt }))
1575
+ .digest('hex');
1576
+ const existingTieBreak = existing
1577
+ ? createHash('sha256')
1578
+ .update(JSON.stringify({ body: existing.body, anchor: existing.anchor, deletedAt: existing.deletedAt }))
1579
+ .digest('hex')
1580
+ : '';
1581
+ const compare = resolveConflictOrdering(comment.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
1582
+ if (compare < 0)
1583
+ continue;
1584
+ core.upsertDocumentReviewCommentForSync({
1585
+ id: comment.id,
1586
+ tenantId: core.getTenantId(),
1587
+ workspaceId,
1588
+ sessionId: comment.sessionId,
1589
+ body: comment.body,
1590
+ anchor: comment.anchor,
1591
+ order: comment.order,
1592
+ authorActorId: comment.authorActorId,
1593
+ createdAt: comment.createdAt,
1594
+ updatedAt: comment.updatedAt,
1595
+ deletedAt: comment.deletedAt,
1596
+ });
1597
+ }
1598
+ catch (error) {
1599
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert document review comment during sync apply.', {
1600
+ source: 'apply.documentReviewCommentUpsert',
1601
+ commentId: comment.id,
1602
+ error: String(error?.message || error || '')
1603
+ });
1604
+ throw error;
1605
+ }
1606
+ }
1607
+ }
1608
+ if (incomingDocumentReviewSessionDeletes.size > 0) {
1609
+ for (const [sessionId, deletedAt] of incomingDocumentReviewSessionDeletes.entries()) {
1610
+ try {
1611
+ const existing = core.getDocumentReviewSession({
1612
+ workspaceId,
1613
+ sessionId,
1614
+ includeDeleted: true
1615
+ });
1616
+ const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
1617
+ if (existing && compare < 0)
1618
+ continue;
1619
+ core.deleteDocumentReviewSessionForSync({ workspaceId, sessionId, deletedAt });
1620
+ }
1621
+ catch (error) {
1622
+ const details = {
1623
+ source: 'apply.documentReviewDelete',
1624
+ sessionId,
1625
+ error: String(error?.message || error || ''),
1626
+ errorCode: String(error?.code || '').trim() || null
1627
+ };
1628
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review session during sync apply.', details);
1629
+ throw createSyncRouteError(`Failed to delete document review session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'DOCUMENT_REVIEW_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
1630
+ }
1631
+ }
1632
+ }
1633
+ if (incomingDocumentReviewCommentDeletes.size > 0) {
1634
+ for (const [commentId, deletedAt] of incomingDocumentReviewCommentDeletes.entries()) {
1635
+ try {
1636
+ const existing = core.listDocumentReviewCommentsForSync({
1637
+ workspaceId,
1638
+ includeDeleted: true
1639
+ }).find((entry) => entry.id === commentId) || null;
1640
+ const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${commentId}`, String(existing?.id || '').trim());
1641
+ if (existing && compare < 0)
1642
+ continue;
1643
+ core.deleteDocumentReviewCommentForSync({ workspaceId, commentId, deletedAt });
1644
+ }
1645
+ catch (error) {
1646
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document review comment during sync apply.', {
1647
+ source: 'apply.documentReviewCommentDelete',
1648
+ commentId,
1649
+ error: String(error?.message || error || '')
1650
+ });
1651
+ throw error;
1652
+ }
1653
+ }
1654
+ }
1655
+ if (incomingAnnotatedAttachmentSessions.size > 0) {
1656
+ for (const session of incomingAnnotatedAttachmentSessions.values()) {
1657
+ try {
1658
+ const existing = core.getAnnotatedAttachmentSession({
1659
+ workspaceId,
1660
+ sessionId: session.id,
1661
+ includeDeleted: true
1662
+ });
1663
+ const incomingTieBreak = createHash('sha256').update(JSON.stringify(session.annotations || [])).digest('hex');
1664
+ const existingTieBreak = existing
1665
+ ? createHash('sha256').update(JSON.stringify(existing.annotations || [])).digest('hex')
1666
+ : '';
1667
+ const compare = resolveConflictOrdering(session.updatedAt, String(existing?.updatedAt || '').trim(), incomingTieBreak, existingTieBreak);
1668
+ if (compare < 0)
1669
+ continue;
1670
+ core.upsertAnnotatedAttachmentSessionForSync({
1671
+ id: session.id,
1672
+ tenantId: core.getTenantId(),
1673
+ workspaceId,
1674
+ taskId: session.taskId,
1675
+ baseImageAssetId: session.baseImageAssetId,
1676
+ title: session.title,
1677
+ globalInstruction: session.globalInstruction,
1678
+ annotations: session.annotations,
1679
+ createdByActorId: session.createdByActorId,
1680
+ updatedByActorId: session.updatedByActorId,
1681
+ createdAt: session.createdAt,
1682
+ updatedAt: session.updatedAt,
1683
+ deletedAt: session.deletedAt,
1684
+ });
1685
+ }
1686
+ catch (error) {
1687
+ const details = {
1688
+ source: 'apply.annotatedAttachmentUpsert',
1689
+ sessionId: session.id,
1690
+ error: String(error?.message || error || ''),
1691
+ errorCode: String(error?.code || '').trim() || null
1692
+ };
1693
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to upsert annotated attachment session during sync apply.', details);
1694
+ throw createSyncRouteError(`Failed to persist annotated attachment session ${session.id} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_APPLY_FAILED', statusCode: 500 });
1695
+ }
1696
+ }
1697
+ }
1698
+ if (incomingAnnotatedAttachmentSessionDeletes.size > 0) {
1699
+ for (const [sessionId, deletedAt] of incomingAnnotatedAttachmentSessionDeletes.entries()) {
1700
+ try {
1701
+ const existing = core.getAnnotatedAttachmentSession({
1702
+ workspaceId,
1703
+ sessionId,
1704
+ includeDeleted: true
1705
+ });
1706
+ const compare = resolveConflictOrdering(deletedAt, String(existing?.updatedAt || '').trim(), `delete:${sessionId}`, String(existing?.id || '').trim());
1707
+ if (existing && compare < 0)
1708
+ continue;
1709
+ core.deleteAnnotatedAttachmentSessionForSync({ workspaceId, sessionId, deletedAt });
1710
+ }
1711
+ catch (error) {
1712
+ const details = {
1713
+ source: 'apply.annotatedAttachmentDelete',
1714
+ sessionId,
1715
+ error: String(error?.message || error || ''),
1716
+ errorCode: String(error?.code || '').trim() || null
1717
+ };
1718
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete annotated attachment session during sync apply.', details);
1719
+ throw createSyncRouteError(`Failed to delete annotated attachment session ${sessionId} during sync apply: ${String(error?.message || error || 'Unknown error')}`, details, { code: 'ANNOTATED_ATTACHMENT_SESSION_SYNC_DELETE_FAILED', statusCode: 500 });
1720
+ }
1721
+ }
1722
+ }
1723
+ if (incomingDocuments.length > 0 || incomingAssets.length > 0) {
1724
+ core.notifyDocumentMetadataMutation(workspaceId, [
1725
+ ...incomingDocuments.map((entry) => entry.path),
1726
+ ...incomingAssets.map((entry) => entry.path)
1727
+ ], 'upsert');
1728
+ }
1729
+ if (incomingDocumentDeletes.size > 0 || incomingAssetDeletes.size > 0) {
1730
+ core.notifyDocumentMetadataMutation(workspaceId, [
1731
+ ...Array.from(incomingDocumentDeletes.keys()),
1732
+ ...Array.from(incomingAssetDeletes.keys())
1733
+ ], 'delete');
1734
+ }
1735
+ const result = core.applyWorkspaceSyncSnapshot({ tasks, archived }, workspaceId);
1736
+ return { result, deleteCount: incomingTaskDeletes.size, emittedEventIds: Array.from(emittedEventIds) };
1737
+ }
1738
+ finally {
1739
+ unsubscribeRealtime();
1740
+ }
1741
+ };
1742
+ // GET /api/taskforce/sync/user-settings - Pull user-global sync payload for authenticated user
1743
+ addRoute('GET', '/api/taskforce/sync/user-settings', async (req, res) => {
1744
+ const authConfig = context.authConfig;
1745
+ if (!authConfig || !authConfig.enabled) {
1746
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1747
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
1748
+ return;
1749
+ }
1750
+ const access = resolveRequestAccess(req, authConfig);
1751
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
1752
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1753
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1754
+ return;
1755
+ }
1756
+ const snapshot = core.getUserGlobalSyncSettings(access.userId);
1757
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1758
+ res.end(JSON.stringify({
1759
+ success: true,
1760
+ settings: snapshot.settings,
1761
+ updatedAt: snapshot.updatedAt
1762
+ }));
1763
+ });
1764
+ // POST /api/taskforce/sync/user-settings - Push user-global sync payload for authenticated user
1765
+ addRoute('POST', '/api/taskforce/sync/user-settings', async (req, res) => {
1766
+ const authConfig = context.authConfig;
1767
+ if (!authConfig || !authConfig.enabled) {
1768
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1769
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
1770
+ return;
1771
+ }
1772
+ const access = resolveRequestAccess(req, authConfig);
1773
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
1774
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1775
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1776
+ return;
1777
+ }
1778
+ const body = await parseJsonBody(req, 50 * 1024 * 1024);
1779
+ const incomingSettings = body?.settings;
1780
+ const incomingUpdatedAt = typeof body?.updatedAt === 'string' ? body.updatedAt : undefined;
1781
+ const result = core.upsertUserGlobalSyncSettings(access.userId, incomingSettings, incomingUpdatedAt);
1782
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1783
+ res.end(JSON.stringify({
1784
+ success: true,
1785
+ settings: result.settings,
1786
+ updatedAt: result.updatedAt,
1787
+ applied: result.applied
1788
+ }));
1789
+ });
1790
+ // POST /api/taskforce/sync/workspace/handshake - Enforce workspace-id match before workspace sync
1791
+ addRoute('POST', '/api/taskforce/sync/workspace/handshake', async (req, res) => {
1792
+ const authConfig = context.authConfig;
1793
+ if (!authConfig || !authConfig.enabled) {
1794
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1795
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
1796
+ return;
1797
+ }
1798
+ const access = resolveRequestAccess(req, authConfig);
1799
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
1800
+ auditAuthAction(req, 'sync-workspace-handshake-unauthorized', {});
1801
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1802
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1803
+ return;
1804
+ }
1805
+ const body = await parseJsonBody(req);
1806
+ const localWorkspaceId = String(body?.workspaceId || '').trim();
1807
+ if (!localWorkspaceId) {
1808
+ auditAuthAction(req, 'sync-workspace-handshake-invalid', { code: 'WORKSPACE_ID_REQUIRED' });
1809
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1810
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.' }));
1811
+ return;
1812
+ }
1813
+ const cloudWorkspaceId = String(access.workspaceId || '').trim();
1814
+ if (!cloudWorkspaceId) {
1815
+ auditAuthAction(req, 'sync-workspace-handshake-missing-cloud-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
1816
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1817
+ res.end(JSON.stringify({
1818
+ success: false,
1819
+ error: 'No cloud workspace is selected. Complete workspace setup first.',
1820
+ code: 'WORKSPACE_ID_REQUIRED'
1821
+ }));
1822
+ return;
1823
+ }
1824
+ if (localWorkspaceId !== cloudWorkspaceId) {
1825
+ auditAuthAction(req, 'sync-workspace-handshake-mismatch', {
1826
+ localWorkspaceId,
1827
+ cloudWorkspaceId
1828
+ });
1829
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1830
+ res.end(JSON.stringify({
1831
+ success: false,
1832
+ error: 'Workspace mismatch. Sync requires matching local and cloud workspace IDs.',
1833
+ code: 'WORKSPACE_ID_MISMATCH',
1834
+ localWorkspaceId,
1835
+ cloudWorkspaceId
1836
+ }));
1837
+ return;
1838
+ }
1839
+ auditAuthAction(req, 'sync-workspace-handshake-success', { workspaceId: localWorkspaceId });
1840
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1841
+ res.end(JSON.stringify({
1842
+ success: true,
1843
+ localWorkspaceId,
1844
+ cloudWorkspaceId
1845
+ }));
1846
+ });
1847
+ // POST /api/taskforce/sync/workspace/provision - Switch to existing cloud workspace that matches local workspace id
1848
+ addRoute('POST', '/api/taskforce/sync/workspace/provision', async (req, res) => {
1849
+ const authConfig = context.authConfig;
1850
+ if (!authConfig || !authConfig.enabled) {
1851
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1852
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
1853
+ return;
1854
+ }
1855
+ const access = resolveRequestAccess(req, authConfig);
1856
+ if (!access.authenticated || !access.sessionId || !access.userId || access.userId === 'anonymous') {
1857
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1858
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1859
+ return;
1860
+ }
1861
+ const body = await parseJsonBody(req);
1862
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
1863
+ const requestedWorkspaceName = String(body?.name || requestedWorkspaceId || '').trim();
1864
+ const requestedWorkspaceDescription = typeof body?.description === 'string' ? body.description : undefined;
1865
+ if (!requestedWorkspaceId) {
1866
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1867
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.' }));
1868
+ return;
1869
+ }
1870
+ if (isReservedWorkspaceId(requestedWorkspaceId)) {
1871
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1872
+ res.end(JSON.stringify({
1873
+ success: false,
1874
+ error: 'workspaceId "default" is reserved.',
1875
+ code: 'WORKSPACE_ID_RESERVED'
1876
+ }));
1877
+ return;
1878
+ }
1879
+ if (!requestedWorkspaceName) {
1880
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1881
+ res.end(JSON.stringify({ success: false, error: 'name is required.' }));
1882
+ return;
1883
+ }
1884
+ const existing = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
1885
+ if (existing) {
1886
+ const cookie = createSessionTokenCookie(access.sessionId, authConfig, {
1887
+ role: existing.role,
1888
+ workspaceId: existing.id,
1889
+ userId: access.userId,
1890
+ ...resolveSessionCookieAttributesForRequest(req, authConfig)
1891
+ });
1892
+ res.setHeader('Set-Cookie', cookie);
1893
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1894
+ res.end(JSON.stringify({
1895
+ success: true,
1896
+ workspace: existing,
1897
+ existing: true
1898
+ }));
1899
+ return;
1900
+ }
1901
+ try {
1902
+ const created = core.createWorkspaceForUser({
1903
+ userId: access.userId,
1904
+ name: requestedWorkspaceName,
1905
+ description: requestedWorkspaceDescription,
1906
+ role: 'owner',
1907
+ preferredWorkspaceId: requestedWorkspaceId
1908
+ });
1909
+ const cookie = createSessionTokenCookie(access.sessionId, authConfig, {
1910
+ role: created.role,
1911
+ workspaceId: created.id,
1912
+ userId: access.userId,
1913
+ ...resolveSessionCookieAttributesForRequest(req, authConfig)
1914
+ });
1915
+ res.setHeader('Set-Cookie', cookie);
1916
+ auditAuthAction(req, 'sync-workspace-provision-created', {
1917
+ workspaceId: created.id,
1918
+ workspaceName: created.name
1919
+ });
1920
+ res.writeHead(201, { 'Content-Type': 'application/json' });
1921
+ res.end(JSON.stringify({
1922
+ success: true,
1923
+ workspace: created,
1924
+ existing: false
1925
+ }));
1926
+ return;
1927
+ }
1928
+ catch (error) {
1929
+ res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
1930
+ res.end(JSON.stringify({
1931
+ success: false,
1932
+ error: String(error?.message || 'Unable to provision workspace for sync.'),
1933
+ code: error?.code
1934
+ }));
1935
+ return;
1936
+ }
1937
+ });
1938
+ // POST /api/taskforce/sync/workspace/bootstrap-push - Initial local snapshot push into authenticated cloud workspace
1939
+ addRoute('POST', '/api/taskforce/sync/workspace/bootstrap-push', async (req, res) => {
1940
+ const authConfig = context.authConfig;
1941
+ if (!authConfig || !authConfig.enabled) {
1942
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1943
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
1944
+ return;
1945
+ }
1946
+ const access = resolveRequestAccess(req, authConfig);
1947
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
1948
+ auditAuthAction(req, 'sync-workspace-bootstrap-unauthorized', {});
1949
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1950
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1951
+ return;
1952
+ }
1953
+ const body = await parseJsonBody(req, 50 * 1024 * 1024);
1954
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
1955
+ const cloudWorkspaceId = String(access.workspaceId || '').trim();
1956
+ if (!requestedWorkspaceId) {
1957
+ auditAuthAction(req, 'sync-workspace-bootstrap-invalid', { code: 'WORKSPACE_ID_REQUIRED' });
1958
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1959
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.', code: 'WORKSPACE_ID_REQUIRED' }));
1960
+ return;
1961
+ }
1962
+ if (!cloudWorkspaceId) {
1963
+ auditAuthAction(req, 'sync-workspace-bootstrap-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
1964
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1965
+ res.end(JSON.stringify({
1966
+ success: false,
1967
+ error: 'No workspace selected. Create or select a workspace first.',
1968
+ code: 'WORKSPACE_ID_REQUIRED'
1969
+ }));
1970
+ return;
1971
+ }
1972
+ if (requestedWorkspaceId !== cloudWorkspaceId) {
1973
+ auditAuthAction(req, 'sync-workspace-bootstrap-mismatch', {
1974
+ requestedWorkspaceId,
1975
+ cloudWorkspaceId
1976
+ });
1977
+ res.writeHead(409, { 'Content-Type': 'application/json' });
1978
+ res.end(JSON.stringify({
1979
+ success: false,
1980
+ error: 'Workspace mismatch. Sync requires matching local and cloud workspace IDs.',
1981
+ code: 'WORKSPACE_ID_MISMATCH',
1982
+ localWorkspaceId: requestedWorkspaceId,
1983
+ cloudWorkspaceId
1984
+ }));
1985
+ return;
1986
+ }
1987
+ const tasks = Array.isArray(body?.tasks) ? body.tasks : [];
1988
+ const archived = Array.isArray(body?.archived) ? body.archived : [];
1989
+ const incomingInitiatives = Array.isArray(body?.initiatives) ? body.initiatives : [];
1990
+ const incomingWorkstreams = Array.isArray(body?.workstreams) ? body.workstreams : [];
1991
+ const incomingTaxonomies = Array.isArray(body?.taxonomies) ? body.taxonomies : null;
1992
+ const incomingTaxonomyState = body?.taxonomyState && typeof body.taxonomyState === 'object' && !Array.isArray(body.taxonomyState)
1993
+ ? body.taxonomyState
1994
+ : null;
1995
+ const incomingAiProfiles = Array.isArray(body?.aiProfiles) ? body.aiProfiles : [];
1996
+ const incomingDocuments = Array.isArray(body?.documents) ? body.documents : [];
1997
+ const incomingDocumentReviewSessions = Array.isArray(body?.documentReviewSessions) ? body.documentReviewSessions : [];
1998
+ const incomingDocumentReviewComments = Array.isArray(body?.documentReviewComments) ? body.documentReviewComments : [];
1999
+ const incomingAnnotatedAttachmentSessions = Array.isArray(body?.annotatedAttachmentSessions) ? body.annotatedAttachmentSessions : [];
2000
+ const incomingAssets = Array.isArray(body?.assets) ? body.assets : [];
2001
+ try {
2002
+ const bootstrapChanges = [
2003
+ ...((incomingTaxonomyState || incomingTaxonomies) ? [{
2004
+ op: 'taxonomy-upsert',
2005
+ taxonomies: incomingTaxonomies,
2006
+ taxonomyState: incomingTaxonomyState || undefined,
2007
+ updatedAt: new Date().toISOString()
2008
+ }] : []),
2009
+ ...incomingInitiatives
2010
+ .map((initiative) => ({
2011
+ op: 'initiative-upsert',
2012
+ initiative: {
2013
+ id: String(initiative?.id || '').trim(),
2014
+ referenceNumber: Number.isFinite(Number(initiative?.referenceNumber))
2015
+ ? Math.max(1, Math.floor(Number(initiative.referenceNumber)))
2016
+ : null,
2017
+ referenceLabel: typeof initiative?.referenceLabel === 'string' ? initiative.referenceLabel.trim() : undefined,
2018
+ title: String(initiative?.title || '').trim(),
2019
+ description: typeof initiative?.description === 'string' ? initiative.description : null,
2020
+ ownerId: typeof initiative?.ownerId === 'string' ? initiative.ownerId.trim() : null,
2021
+ createdAt: String(initiative?.createdAt || '').trim(),
2022
+ updatedAt: String(initiative?.updatedAt || initiative?.createdAt || '').trim(),
2023
+ order: Number.isFinite(Number(initiative?.order)) ? Math.floor(Number(initiative.order)) : null,
2024
+ isArchived: Boolean(initiative?.isArchived)
2025
+ }
2026
+ }))
2027
+ .filter((change) => change.initiative.id.length > 0),
2028
+ ...incomingWorkstreams
2029
+ .map((workstream) => ({
2030
+ op: 'workstream-upsert',
2031
+ workstream: {
2032
+ id: String(workstream?.id || '').trim(),
2033
+ referenceNumber: Number.isFinite(Number(workstream?.referenceNumber))
2034
+ ? Math.max(1, Math.floor(Number(workstream.referenceNumber)))
2035
+ : null,
2036
+ referenceLabel: typeof workstream?.referenceLabel === 'string' ? workstream.referenceLabel.trim() : undefined,
2037
+ initiativeId: typeof workstream?.initiativeId === 'string' ? workstream.initiativeId.trim() : null,
2038
+ title: String(workstream?.title || '').trim(),
2039
+ description: typeof workstream?.description === 'string' ? workstream.description : null,
2040
+ ownerId: typeof workstream?.ownerId === 'string' ? workstream.ownerId.trim() : null,
2041
+ createdAt: String(workstream?.createdAt || '').trim(),
2042
+ updatedAt: String(workstream?.updatedAt || workstream?.createdAt || '').trim(),
2043
+ order: Number.isFinite(Number(workstream?.order)) ? Math.floor(Number(workstream.order)) : null,
2044
+ isArchived: Boolean(workstream?.isArchived)
2045
+ }
2046
+ }))
2047
+ .filter((change) => change.workstream.id.length > 0),
2048
+ ...incomingAiProfiles
2049
+ .map((profile) => ({
2050
+ op: 'ai-profile-upsert',
2051
+ profile: {
2052
+ id: String(profile?.id || '').trim(),
2053
+ workspaceId: cloudWorkspaceId,
2054
+ profileToken: String(profile?.profileToken || '').trim(),
2055
+ name: String(profile?.name || '').trim(),
2056
+ username: String(profile?.username || '').trim(),
2057
+ icon: String(profile?.icon || 'Bot').trim() || 'Bot',
2058
+ color: String(profile?.color || '#8b5cf6').trim() || '#8b5cf6',
2059
+ surfaceType: normalizeIncomingAiProfileSurfaceType(profile?.surfaceType, {
2060
+ source: 'push.aiProfileUpsert',
2061
+ profileId: String(profile?.id || '').trim(),
2062
+ workspaceId: cloudWorkspaceId
2063
+ }),
2064
+ providerMetadata: profile?.providerMetadata && typeof profile.providerMetadata === 'object' && !Array.isArray(profile.providerMetadata)
2065
+ ? profile.providerMetadata
2066
+ : null,
2067
+ createdAt: String(profile?.createdAt || '').trim(),
2068
+ updatedAt: String(profile?.updatedAt || profile?.createdAt || '').trim(),
2069
+ lastActiveAt: typeof profile?.lastActiveAt === 'string' && profile.lastActiveAt.trim().length > 0
2070
+ ? profile.lastActiveAt.trim()
2071
+ : null
2072
+ }
2073
+ }))
2074
+ .filter((change) => change.profile.id.length > 0),
2075
+ ...incomingDocuments
2076
+ .map((doc) => ({
2077
+ op: 'document-upsert',
2078
+ path: String(doc?.path || '').trim(),
2079
+ updatedAt: String(doc?.updatedAt || '').trim(),
2080
+ content: typeof doc?.content === 'string' ? doc.content : '',
2081
+ assetId: typeof doc?.assetId === 'string' ? doc.assetId.trim() : undefined,
2082
+ documentId: typeof doc?.documentId === 'string' ? doc.documentId.trim() : null,
2083
+ referenceNumber: Number.isFinite(Number(doc?.referenceNumber)) ? Math.max(1, Math.floor(Number(doc.referenceNumber))) : null,
2084
+ version: Number.isFinite(Number(doc?.version)) ? Math.max(1, Math.floor(Number(doc.version))) : null,
2085
+ taskId: typeof doc?.taskId === 'string' ? doc.taskId.trim() : null,
2086
+ logicalName: typeof doc?.logicalName === 'string' ? doc.logicalName.trim() : null,
2087
+ caption: typeof doc?.caption === 'string' ? doc.caption.trim() : null,
2088
+ originalFilename: typeof doc?.originalFilename === 'string' ? doc.originalFilename.trim() : null,
2089
+ linkRole: (doc?.linkRole === 'reference' ? 'reference' : 'attachment')
2090
+ }))
2091
+ .filter((doc) => doc.path.length > 0),
2092
+ ...incomingAssets
2093
+ .map((asset) => ({
2094
+ op: 'asset-upsert',
2095
+ path: String(asset?.path || '').trim(),
2096
+ updatedAt: String(asset?.updatedAt || '').trim(),
2097
+ contentBase64: typeof asset?.contentBase64 === 'string' ? asset.contentBase64 : '',
2098
+ assetId: typeof asset?.assetId === 'string' ? asset.assetId.trim() : undefined,
2099
+ kind: asset?.kind === 'image' ? 'image' : 'file',
2100
+ mimeType: typeof asset?.mimeType === 'string' ? asset.mimeType.trim() : 'application/octet-stream',
2101
+ referenceNumber: Number.isFinite(Number(asset?.referenceNumber)) ? Math.max(1, Math.floor(Number(asset.referenceNumber))) : null,
2102
+ taskId: typeof asset?.taskId === 'string' ? asset.taskId.trim() : null,
2103
+ logicalName: typeof asset?.logicalName === 'string' ? asset.logicalName.trim() : null,
2104
+ caption: typeof asset?.caption === 'string' ? asset.caption.trim() : null,
2105
+ originalFilename: typeof asset?.originalFilename === 'string' ? asset.originalFilename.trim() : null,
2106
+ linkRole: asset?.linkRole === 'reference'
2107
+ ? 'reference'
2108
+ : asset?.kind === 'image'
2109
+ ? 'image'
2110
+ : 'attachment'
2111
+ }))
2112
+ .filter((asset) => asset.path.length > 0 && asset.contentBase64.length > 0),
2113
+ ...incomingDocumentReviewSessions
2114
+ .map((session) => ({
2115
+ op: 'document-review-session-upsert',
2116
+ session: {
2117
+ id: String(session?.id || '').trim(),
2118
+ assetId: String(session?.assetId || '').trim(),
2119
+ documentId: typeof session?.documentId === 'string' ? session.documentId.trim() : null,
2120
+ documentVersion: Number.isFinite(Number(session?.documentVersion)) ? Math.max(1, Math.floor(Number(session.documentVersion))) : null,
2121
+ title: typeof session?.title === 'string' ? session.title.trim() : null,
2122
+ status: session?.status === 'resolved' ? 'resolved' : 'open',
2123
+ comments: [],
2124
+ createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
2125
+ updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
2126
+ createdAt: String(session?.createdAt || '').trim(),
2127
+ updatedAt: String(session?.updatedAt || session?.createdAt || '').trim(),
2128
+ deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null
2129
+ }
2130
+ }))
2131
+ .filter((change) => change.session.id.length > 0 && change.session.assetId.length > 0),
2132
+ ...(incomingDocumentReviewComments.length > 0
2133
+ ? incomingDocumentReviewComments
2134
+ : incomingDocumentReviewSessions.flatMap((session) => {
2135
+ const sessionId = String(session?.id || '').trim();
2136
+ const comments = Array.isArray(session?.comments) ? session.comments : [];
2137
+ return comments.map((comment) => ({ sessionId, comment }));
2138
+ }))
2139
+ .map((entry) => {
2140
+ const isLegacyNested = 'comment' in entry;
2141
+ const comment = (isLegacyNested ? entry.comment : entry) && typeof (isLegacyNested ? entry.comment : entry) === 'object'
2142
+ ? (isLegacyNested ? entry.comment : entry)
2143
+ : null;
2144
+ const sessionId = isLegacyNested
2145
+ ? String(entry.sessionId || '').trim()
2146
+ : String(comment?.sessionId || '').trim();
2147
+ return {
2148
+ op: 'document-review-comment-upsert',
2149
+ comment: {
2150
+ id: String(comment?.id || '').trim(),
2151
+ sessionId,
2152
+ body: typeof comment?.body === 'string' ? comment.body : '',
2153
+ anchor: comment?.anchor ?? null,
2154
+ order: Number.isFinite(Number(comment?.order)) ? Math.max(0, Math.floor(Number(comment?.order))) : 0,
2155
+ authorActorId: typeof comment?.authorActorId === 'string' ? comment.authorActorId.trim() : null,
2156
+ createdAt: String(comment?.createdAt || '').trim(),
2157
+ updatedAt: String(comment?.updatedAt || comment?.createdAt || '').trim(),
2158
+ deletedAt: typeof comment?.deletedAt === 'string' && comment.deletedAt.trim().length > 0 ? comment.deletedAt.trim() : null,
2159
+ }
2160
+ };
2161
+ })
2162
+ .filter((change) => change.comment.id.length > 0 && change.comment.sessionId.length > 0),
2163
+ ...incomingAnnotatedAttachmentSessions
2164
+ .map((session) => ({
2165
+ op: 'annotated-attachment-session-upsert',
2166
+ session: {
2167
+ id: String(session?.id || '').trim(),
2168
+ workspaceId: cloudWorkspaceId,
2169
+ taskId: typeof session?.taskId === 'string' ? session.taskId.trim() : '',
2170
+ baseImageAssetId: typeof session?.baseImageAssetId === 'string' ? session.baseImageAssetId.trim() : '',
2171
+ title: typeof session?.title === 'string' ? session.title.trim() : null,
2172
+ globalInstruction: typeof session?.globalInstruction === 'string' ? session.globalInstruction.trim() : null,
2173
+ annotations: Array.isArray(session?.annotations) ? session.annotations : [],
2174
+ createdByActorId: typeof session?.createdByActorId === 'string' ? session.createdByActorId.trim() : null,
2175
+ updatedByActorId: typeof session?.updatedByActorId === 'string' ? session.updatedByActorId.trim() : null,
2176
+ createdAt: String(session?.createdAt || '').trim(),
2177
+ updatedAt: String(session?.updatedAt || session?.createdAt || '').trim(),
2178
+ deletedAt: typeof session?.deletedAt === 'string' && session.deletedAt.trim().length > 0 ? session.deletedAt.trim() : null
2179
+ }
2180
+ }))
2181
+ .filter((change) => (change.session.id.length > 0
2182
+ && change.session.workspaceId.length > 0
2183
+ && change.session.baseImageAssetId.length > 0)),
2184
+ ...tasks.map((task) => ({ op: 'upsert', archived: false, task })),
2185
+ ...archived.map((task) => ({ op: 'upsert', archived: true, task }))
2186
+ ];
2187
+ const { result } = await dispatchWorkspaceSyncChanges(cloudWorkspaceId, bootstrapChanges);
2188
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2189
+ res.end(JSON.stringify({
2190
+ success: true,
2191
+ workspaceId: cloudWorkspaceId,
2192
+ result
2193
+ }));
2194
+ auditAuthAction(req, 'sync-workspace-bootstrap-success', {
2195
+ workspaceId: cloudWorkspaceId,
2196
+ taskCount: tasks.length,
2197
+ archivedCount: archived.length,
2198
+ taxonomyCount: incomingTaxonomies?.length ?? 0,
2199
+ aiProfileCount: incomingAiProfiles.length,
2200
+ documentCount: incomingDocuments.length,
2201
+ documentReviewSessionCount: incomingDocumentReviewSessions.length,
2202
+ documentReviewCommentCount: incomingDocumentReviewComments.length,
2203
+ annotatedAttachmentSessionCount: incomingAnnotatedAttachmentSessions.length,
2204
+ assetCount: incomingAssets.length
2205
+ });
2206
+ }
2207
+ catch (error) {
2208
+ const statusCode = resolveErrorStatusCode(error, 400);
2209
+ auditAuthAction(req, 'sync-workspace-bootstrap-failed', {
2210
+ workspaceId: cloudWorkspaceId,
2211
+ code: error?.code || null,
2212
+ statusCode,
2213
+ details: error?.details || null
2214
+ });
2215
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2216
+ res.end(JSON.stringify({
2217
+ success: false,
2218
+ error: String(error?.message || 'Failed to apply workspace snapshot'),
2219
+ code: error?.code,
2220
+ details: error?.details || undefined
2221
+ }));
2222
+ }
2223
+ });
2224
+ // GET /api/taskforce/sync/workspace/documents - Snapshot markdown documents only.
2225
+ // Binary image/file assets travel through the sibling /assets snapshot and asset-* sync ops.
2226
+ addRoute('GET', '/api/taskforce/sync/workspace/documents', async (req, res) => {
2227
+ const authConfig = context.authConfig;
2228
+ if (authConfig?.runtimeMode === 'cloud') {
2229
+ const access = resolveRequestAccess(req, authConfig);
2230
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2231
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2232
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2233
+ return;
2234
+ }
2235
+ }
2236
+ const workspaceId = requestWorkspaceId(req) || 'default';
2237
+ const basePath = core.getPaths().basePath;
2238
+ const documents = await listWorkspaceSyncDocumentsSnapshot(basePath, workspaceId);
2239
+ const fingerprint = JSON.stringify(documents.map((doc) => [
2240
+ doc.path,
2241
+ doc.updatedAt,
2242
+ doc.content.length,
2243
+ doc.assetId || '',
2244
+ doc.documentId || '',
2245
+ String(doc.version ?? ''),
2246
+ doc.taskId || '',
2247
+ doc.linkRole || ''
2248
+ ].join(':')));
2249
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2250
+ res.end(JSON.stringify({
2251
+ success: true,
2252
+ documents,
2253
+ fingerprint
2254
+ }));
2255
+ });
2256
+ // GET /api/taskforce/sync/workspace/ai-profiles - Snapshot workspace AI profiles for local->cloud sync.
2257
+ addRoute('GET', '/api/taskforce/sync/workspace/ai-profiles', async (req, res) => {
2258
+ const authConfig = context.authConfig;
2259
+ if (authConfig?.runtimeMode === 'cloud') {
2260
+ const access = resolveRequestAccess(req, authConfig);
2261
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2262
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2263
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2264
+ return;
2265
+ }
2266
+ }
2267
+ const workspaceId = requestWorkspaceId(req) || 'default';
2268
+ const aiProfiles = core.listAiProfiles(workspaceId);
2269
+ const fingerprint = JSON.stringify(aiProfiles.map((profile) => [
2270
+ profile.id,
2271
+ profile.updatedAt,
2272
+ profile.name,
2273
+ profile.username,
2274
+ profile.profileToken,
2275
+ profile.surfaceType ?? ''
2276
+ ].join(':')));
2277
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2278
+ res.end(JSON.stringify({
2279
+ success: true,
2280
+ aiProfiles,
2281
+ fingerprint
2282
+ }));
2283
+ });
2284
+ // GET /api/taskforce/sync/workspace/document-reviews - Snapshot document review sessions for local->cloud sync.
2285
+ addRoute('GET', '/api/taskforce/sync/workspace/document-reviews', async (req, res) => {
2286
+ const authConfig = context.authConfig;
2287
+ if (authConfig?.runtimeMode === 'cloud') {
2288
+ const access = resolveRequestAccess(req, authConfig);
2289
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2290
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2291
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2292
+ return;
2293
+ }
2294
+ }
2295
+ const workspaceId = requestWorkspaceId(req) || 'default';
2296
+ const sessions = listWorkspaceSyncDocumentReviewSessionsSnapshot(workspaceId);
2297
+ const comments = listWorkspaceSyncDocumentReviewCommentsSnapshot(workspaceId);
2298
+ const fingerprint = JSON.stringify([
2299
+ ...sessions.map((session) => [
2300
+ 'session',
2301
+ session.id,
2302
+ session.updatedAt,
2303
+ session.status,
2304
+ session.comments.length
2305
+ ].join(':')),
2306
+ ...comments.map((comment) => [
2307
+ 'comment',
2308
+ comment.id,
2309
+ comment.sessionId,
2310
+ comment.updatedAt,
2311
+ comment.deletedAt || ''
2312
+ ].join(':'))
2313
+ ]);
2314
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2315
+ res.end(JSON.stringify({
2316
+ success: true,
2317
+ sessions,
2318
+ comments,
2319
+ fingerprint
2320
+ }));
2321
+ });
2322
+ // GET /api/taskforce/sync/workspace/annotated-attachments - Snapshot Image Notes sessions for local->cloud sync.
2323
+ addRoute('GET', '/api/taskforce/sync/workspace/annotated-attachments', async (req, res) => {
2324
+ const authConfig = context.authConfig;
2325
+ if (authConfig?.runtimeMode === 'cloud') {
2326
+ const access = resolveRequestAccess(req, authConfig);
2327
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2328
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2329
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2330
+ return;
2331
+ }
2332
+ }
2333
+ const workspaceId = requestWorkspaceId(req) || 'default';
2334
+ const sessions = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(workspaceId);
2335
+ const fingerprint = JSON.stringify(sessions.map((session) => [
2336
+ session.id,
2337
+ session.updatedAt,
2338
+ session.taskId,
2339
+ session.baseImageAssetId,
2340
+ session.annotations.length,
2341
+ session.deletedAt || ''
2342
+ ].join(':')));
2343
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2344
+ res.end(JSON.stringify({
2345
+ success: true,
2346
+ sessions,
2347
+ fingerprint
2348
+ }));
2349
+ });
2350
+ // GET /api/taskforce/sync/workspace/assets - Snapshot non-markdown local assets.
2351
+ // Markdown remains on the /documents snapshot so document encryption/version semantics stay isolated.
2352
+ addRoute('GET', '/api/taskforce/sync/workspace/assets', async (req, res) => {
2353
+ const authConfig = context.authConfig;
2354
+ if (authConfig?.runtimeMode === 'cloud') {
2355
+ const access = resolveRequestAccess(req, authConfig);
2356
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2357
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2358
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2359
+ return;
2360
+ }
2361
+ }
2362
+ const workspaceId = requestWorkspaceId(req) || 'default';
2363
+ const basePath = core.getPaths().basePath;
2364
+ const assets = await listWorkspaceSyncBinaryAssetsSnapshot(basePath, workspaceId);
2365
+ const fingerprint = JSON.stringify(assets.map((asset) => [
2366
+ asset.path,
2367
+ asset.updatedAt,
2368
+ asset.contentBase64.length,
2369
+ asset.assetId || '',
2370
+ asset.kind,
2371
+ asset.mimeType,
2372
+ asset.taskId || '',
2373
+ asset.originalFilename || '',
2374
+ asset.linkRole || ''
2375
+ ].join(':')));
2376
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2377
+ res.end(JSON.stringify({
2378
+ success: true,
2379
+ assets,
2380
+ fingerprint
2381
+ }));
2382
+ });
2383
+ // GET /api/taskforce/sync/workspace/pull - Incremental workspace task deltas with cursor paging
2384
+ addRoute('GET', '/api/taskforce/sync/workspace/pull', async (req, res) => {
2385
+ const authConfig = context.authConfig;
2386
+ if (!authConfig || !authConfig.enabled) {
2387
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2388
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
2389
+ return;
2390
+ }
2391
+ const access = resolveRequestAccess(req, authConfig);
2392
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2393
+ auditAuthAction(req, 'sync-workspace-pull-unauthorized', {});
2394
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2395
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2396
+ return;
2397
+ }
2398
+ const url = new URL(req.url || '/api/taskforce/sync/workspace/pull', 'http://localhost');
2399
+ const requestedWorkspaceId = String(url.searchParams.get('workspaceId') || '').trim();
2400
+ if (!requestedWorkspaceId) {
2401
+ auditAuthAction(req, 'sync-workspace-pull-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
2402
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2403
+ res.end(JSON.stringify({
2404
+ success: false,
2405
+ error: 'workspaceId is required.',
2406
+ code: 'WORKSPACE_ID_REQUIRED',
2407
+ workspaceId: null
2408
+ }));
2409
+ return;
2410
+ }
2411
+ const allowed = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
2412
+ if (!allowed) {
2413
+ auditAuthAction(req, 'sync-workspace-pull-forbidden', {
2414
+ requestedWorkspaceId
2415
+ });
2416
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2417
+ res.end(JSON.stringify({
2418
+ success: false,
2419
+ error: 'You do not have access to this workspace.',
2420
+ code: 'WORKSPACE_ACCESS_DENIED',
2421
+ workspaceId: requestedWorkspaceId
2422
+ }));
2423
+ return;
2424
+ }
2425
+ const targetWorkspaceId = requestedWorkspaceId;
2426
+ const cursor = decodeWorkspaceSyncCursor(url.searchParams.get('cursor'));
2427
+ const requestedLimit = Number.parseInt(String(url.searchParams.get('limit') || ''), 10);
2428
+ const pageSize = Number.isFinite(requestedLimit) && requestedLimit > 0
2429
+ ? Math.max(1, Math.min(500, Math.floor(requestedLimit)))
2430
+ : 200;
2431
+ // Use watermark-indexed queries to avoid full table scans (Finding 12).
2432
+ // Fetch up to pageSize * 2 tasks/deletes from DB since the cursor, then merge with
2433
+ // small in-memory collections (taxonomy, documents) before slicing to pageSize.
2434
+ const syncCursor = cursor ? { watermark: cursor.u, id: cursor.id } : null;
2435
+ const dbFetchLimit = pageSize * 2 + 10;
2436
+ const taskRows = core.listTasksForSync(targetWorkspaceId, syncCursor, dbFetchLimit);
2437
+ const upsertEntries = taskRows
2438
+ .map((entry) => {
2439
+ const watermark = String(entry.task?.updatedAt || entry.task?.createdAt || '').trim();
2440
+ const taskId = String(entry.task?.id || '').trim();
2441
+ return {
2442
+ kind: 'upsert',
2443
+ watermark,
2444
+ sortId: taskId,
2445
+ task: entry.task,
2446
+ archived: Boolean(entry.archived)
2447
+ };
2448
+ })
2449
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
2450
+ const rawDeleteEntries = core.listWorkspaceSyncDeletesSince(targetWorkspaceId, syncCursor, dbFetchLimit);
2451
+ const deleteEntries = rawDeleteEntries
2452
+ .map((entry) => ({
2453
+ kind: 'delete',
2454
+ watermark: String(entry.deletedAt || '').trim(),
2455
+ sortId: String(entry.taskId || '').trim(),
2456
+ taskId: String(entry.taskId || '').trim()
2457
+ }))
2458
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
2459
+ const taxonomySnapshot = core.getTaxonomiesSyncSnapshot(targetWorkspaceId);
2460
+ const taxonomyEntries = (taxonomySnapshot
2461
+ && Array.isArray(taxonomySnapshot.taxonomies)
2462
+ && Number.isFinite(Date.parse(String(taxonomySnapshot.updatedAt || '').trim())))
2463
+ ? [{
2464
+ kind: 'taxonomy',
2465
+ watermark: String(taxonomySnapshot.updatedAt || '').trim(),
2466
+ sortId: '__taxonomy_config__',
2467
+ taxonomies: taxonomySnapshot.taxonomies,
2468
+ taxonomyState: taxonomySnapshot.taxonomyState || null
2469
+ }]
2470
+ : [];
2471
+ const initiativeEntries = core.listInitiatives(targetWorkspaceId)
2472
+ .map((initiative) => ({
2473
+ kind: 'initiative',
2474
+ watermark: String(initiative.updatedAt || initiative.createdAt || '').trim(),
2475
+ sortId: `initiative:${String(initiative.id || '').trim()}`,
2476
+ initiative
2477
+ }))
2478
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.initiative.id.length > 0);
2479
+ const workstreamEntries = core.listWorkstreams(targetWorkspaceId)
2480
+ .map((workstream) => ({
2481
+ kind: 'workstream',
2482
+ watermark: String(workstream.updatedAt || workstream.createdAt || '').trim(),
2483
+ sortId: `workstream:${String(workstream.id || '').trim()}`,
2484
+ workstream
2485
+ }))
2486
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.workstream.id.length > 0);
2487
+ const aiProfileEntries = core.listAiProfiles(targetWorkspaceId)
2488
+ .map((profile) => ({
2489
+ kind: 'ai-profile',
2490
+ watermark: String(profile.updatedAt || profile.createdAt || '').trim(),
2491
+ sortId: `ai-profile:${String(profile.id || '').trim()}`,
2492
+ profile
2493
+ }))
2494
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
2495
+ const documentEntries = (await listWorkspaceSyncDocumentsSnapshot(core.getPaths().basePath, targetWorkspaceId))
2496
+ .map((doc) => ({
2497
+ kind: 'document',
2498
+ watermark: String(doc.updatedAt || '').trim(),
2499
+ sortId: `doc:${String(doc.path || '').trim()}`,
2500
+ path: String(doc.path || '').trim(),
2501
+ content: String(doc.content || ''),
2502
+ assetId: doc.assetId,
2503
+ documentId: doc.documentId,
2504
+ referenceNumber: doc.referenceNumber,
2505
+ version: doc.version,
2506
+ taskId: doc.taskId,
2507
+ logicalName: doc.logicalName,
2508
+ caption: doc.caption,
2509
+ originalFilename: doc.originalFilename,
2510
+ linkRole: doc.linkRole
2511
+ }))
2512
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
2513
+ const documentDeleteEntries = listWorkspaceSyncDocumentDeletesSnapshot(targetWorkspaceId)
2514
+ .map((doc) => ({
2515
+ kind: 'document-delete',
2516
+ watermark: String(doc.deletedAt || '').trim(),
2517
+ sortId: `doc-delete:${String(doc.path || '').trim()}:${String(doc.assetId || '').trim()}`,
2518
+ path: String(doc.path || '').trim(),
2519
+ assetId: typeof doc.assetId === 'string' && doc.assetId.trim().length > 0 ? doc.assetId.trim() : undefined
2520
+ }))
2521
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
2522
+ const documentReviewSessionEntries = listWorkspaceSyncDocumentReviewSessionsSnapshot(targetWorkspaceId)
2523
+ .filter((session) => !session.deletedAt)
2524
+ .map((session) => ({
2525
+ kind: 'document-review-session',
2526
+ watermark: String(session.updatedAt || session.createdAt || '').trim(),
2527
+ sortId: `document-review-session:${String(session.id || '').trim()}`,
2528
+ session
2529
+ }))
2530
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.assetId.length > 0);
2531
+ const documentReviewSessionDeleteEntries = listWorkspaceSyncDocumentReviewSessionsSnapshot(targetWorkspaceId)
2532
+ .filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
2533
+ .map((session) => ({
2534
+ kind: 'document-review-session-delete',
2535
+ watermark: String(session.deletedAt || session.updatedAt || '').trim(),
2536
+ sortId: `document-review-session-delete:${String(session.id || '').trim()}`,
2537
+ sessionId: String(session.id || '').trim()
2538
+ }))
2539
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
2540
+ const documentReviewCommentEntries = listWorkspaceSyncDocumentReviewCommentsSnapshot(targetWorkspaceId)
2541
+ .filter((comment) => !comment.deletedAt)
2542
+ .map((comment) => ({
2543
+ kind: 'document-review-comment',
2544
+ watermark: String(comment.updatedAt || comment.createdAt || '').trim(),
2545
+ sortId: `document-review-comment:${String(comment.id || '').trim()}`,
2546
+ comment
2547
+ }))
2548
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.comment.id.length > 0 && entry.comment.sessionId.length > 0);
2549
+ const documentReviewCommentDeleteEntries = listWorkspaceSyncDocumentReviewCommentsSnapshot(targetWorkspaceId)
2550
+ .filter((comment) => typeof comment.deletedAt === 'string' && comment.deletedAt.trim().length > 0)
2551
+ .map((comment) => ({
2552
+ kind: 'document-review-comment-delete',
2553
+ watermark: String(comment.deletedAt || comment.updatedAt || '').trim(),
2554
+ sortId: `document-review-comment-delete:${String(comment.id || '').trim()}`,
2555
+ commentId: String(comment.id || '').trim()
2556
+ }))
2557
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.commentId.length > 0);
2558
+ const annotatedAttachmentSessionEntries = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(targetWorkspaceId)
2559
+ .filter((session) => !session.deletedAt)
2560
+ .map((session) => ({
2561
+ kind: 'annotated-attachment-session',
2562
+ watermark: String(session.updatedAt || session.createdAt || '').trim(),
2563
+ sortId: `annotated-attachment-session:${String(session.id || '').trim()}`,
2564
+ session
2565
+ }))
2566
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.session.id.length > 0 && entry.session.baseImageAssetId.length > 0);
2567
+ const annotatedAttachmentSessionDeleteEntries = listWorkspaceSyncAnnotatedAttachmentSessionsSnapshot(targetWorkspaceId)
2568
+ .filter((session) => typeof session.deletedAt === 'string' && session.deletedAt.trim().length > 0)
2569
+ .map((session) => ({
2570
+ kind: 'annotated-attachment-session-delete',
2571
+ watermark: String(session.deletedAt || session.updatedAt || '').trim(),
2572
+ sortId: `annotated-attachment-session-delete:${String(session.id || '').trim()}`,
2573
+ sessionId: String(session.id || '').trim()
2574
+ }))
2575
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sessionId.length > 0);
2576
+ const assetEntries = (await listWorkspaceSyncBinaryAssetsSnapshot(core.getPaths().basePath, targetWorkspaceId))
2577
+ .map((asset) => ({
2578
+ kind: 'asset',
2579
+ watermark: String(asset.updatedAt || '').trim(),
2580
+ sortId: `asset:${String(asset.path || '').trim()}`,
2581
+ path: String(asset.path || '').trim(),
2582
+ contentBase64: String(asset.contentBase64 || ''),
2583
+ assetId: asset.assetId,
2584
+ kindValue: asset.kind,
2585
+ mimeType: asset.mimeType,
2586
+ referenceNumber: asset.referenceNumber,
2587
+ taskId: asset.taskId,
2588
+ logicalName: asset.logicalName,
2589
+ caption: asset.caption,
2590
+ originalFilename: asset.originalFilename,
2591
+ linkRole: asset.linkRole
2592
+ }))
2593
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0 && entry.contentBase64.length > 0);
2594
+ const assetDeleteEntries = listWorkspaceSyncBinaryAssetDeletesSnapshot(targetWorkspaceId)
2595
+ .map((asset) => ({
2596
+ kind: 'asset-delete',
2597
+ watermark: String(asset.deletedAt || '').trim(),
2598
+ sortId: `asset-delete:${String(asset.path || '').trim()}:${String(asset.assetId || '').trim()}`,
2599
+ path: String(asset.path || '').trim(),
2600
+ assetId: typeof asset.assetId === 'string' && asset.assetId.trim().length > 0 ? asset.assetId.trim() : undefined
2601
+ }))
2602
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
2603
+ const all = [
2604
+ ...upsertEntries,
2605
+ ...deleteEntries,
2606
+ ...taxonomyEntries,
2607
+ ...initiativeEntries,
2608
+ ...workstreamEntries,
2609
+ ...aiProfileEntries,
2610
+ ...documentEntries,
2611
+ ...documentDeleteEntries,
2612
+ ...documentReviewSessionEntries,
2613
+ ...documentReviewSessionDeleteEntries,
2614
+ ...documentReviewCommentEntries,
2615
+ ...documentReviewCommentDeleteEntries,
2616
+ ...annotatedAttachmentSessionEntries,
2617
+ ...annotatedAttachmentSessionDeleteEntries,
2618
+ ...assetEntries,
2619
+ ...assetDeleteEntries
2620
+ ];
2621
+ const afterCursor = all.filter((entry) => {
2622
+ if (!cursor)
2623
+ return true;
2624
+ const watermark = entry.watermark;
2625
+ const sortId = entry.sortId;
2626
+ if (watermark > cursor.u)
2627
+ return true;
2628
+ if (watermark < cursor.u)
2629
+ return false;
2630
+ return sortId > cursor.id;
2631
+ });
2632
+ afterCursor.sort((a, b) => {
2633
+ if (a.watermark !== b.watermark)
2634
+ return a.watermark.localeCompare(b.watermark);
2635
+ return String(a.sortId || '').localeCompare(String(b.sortId || ''));
2636
+ });
2637
+ const page = afterCursor.slice(0, pageSize);
2638
+ const hasMore = afterCursor.length > pageSize;
2639
+ const last = page.length > 0 ? page[page.length - 1] : null;
2640
+ const nextCursor = encodeWorkspaceSyncCursor(last
2641
+ ? { u: last.watermark, id: String(last.sortId || '') }
2642
+ : cursor);
2643
+ const workspaceMembers = core.listWorkspaceUsers(targetWorkspaceId).map((member) => ({
2644
+ userId: member.userId,
2645
+ workspaceId: targetWorkspaceId,
2646
+ email: member.email,
2647
+ displayName: member.displayName || null,
2648
+ role: member.role,
2649
+ permissionMode: member.permissionMode,
2650
+ betaAccess: member.betaAccess,
2651
+ status: member.status,
2652
+ disabled: member.disabled,
2653
+ createdAt: member.createdAt || null,
2654
+ updatedAt: member.updatedAt || null,
2655
+ emailVerifiedAt: member.emailVerifiedAt || null
2656
+ }));
2657
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2658
+ res.end(JSON.stringify({
2659
+ success: true,
2660
+ workspaceId: targetWorkspaceId,
2661
+ workspaceMembers,
2662
+ changes: page.map((entry) => (entry.kind === 'delete'
2663
+ ? {
2664
+ op: 'delete',
2665
+ taskId: entry.taskId,
2666
+ deletedAt: entry.watermark,
2667
+ watermark: entry.watermark
2668
+ }
2669
+ : entry.kind === 'taxonomy'
2670
+ ? {
2671
+ op: 'taxonomy-upsert',
2672
+ taxonomies: entry.taxonomies,
2673
+ taxonomyState: entry.taxonomyState,
2674
+ watermark: entry.watermark
2675
+ }
2676
+ : entry.kind === 'initiative'
2677
+ ? {
2678
+ op: 'initiative-upsert',
2679
+ initiative: entry.initiative,
2680
+ updatedAt: entry.watermark,
2681
+ watermark: entry.watermark
2682
+ }
2683
+ : entry.kind === 'workstream'
2684
+ ? {
2685
+ op: 'workstream-upsert',
2686
+ workstream: entry.workstream,
2687
+ updatedAt: entry.watermark,
2688
+ watermark: entry.watermark
2689
+ }
2690
+ : entry.kind === 'ai-profile'
2691
+ ? {
2692
+ op: 'ai-profile-upsert',
2693
+ profile: entry.profile,
2694
+ updatedAt: entry.watermark,
2695
+ watermark: entry.watermark
2696
+ }
2697
+ : entry.kind === 'document-review-session'
2698
+ ? {
2699
+ op: 'document-review-session-upsert',
2700
+ session: entry.session,
2701
+ updatedAt: entry.watermark,
2702
+ watermark: entry.watermark
2703
+ }
2704
+ : entry.kind === 'document-review-session-delete'
2705
+ ? {
2706
+ op: 'document-review-session-delete',
2707
+ sessionId: entry.sessionId,
2708
+ deletedAt: entry.watermark,
2709
+ watermark: entry.watermark
2710
+ }
2711
+ : entry.kind === 'document-review-comment'
2712
+ ? {
2713
+ op: 'document-review-comment-upsert',
2714
+ comment: entry.comment,
2715
+ updatedAt: entry.watermark,
2716
+ watermark: entry.watermark
2717
+ }
2718
+ : entry.kind === 'document-review-comment-delete'
2719
+ ? {
2720
+ op: 'document-review-comment-delete',
2721
+ commentId: entry.commentId,
2722
+ deletedAt: entry.watermark,
2723
+ watermark: entry.watermark
2724
+ }
2725
+ : entry.kind === 'annotated-attachment-session'
2726
+ ? {
2727
+ op: 'annotated-attachment-session-upsert',
2728
+ session: entry.session,
2729
+ updatedAt: entry.watermark,
2730
+ watermark: entry.watermark
2731
+ }
2732
+ : entry.kind === 'annotated-attachment-session-delete'
2733
+ ? {
2734
+ op: 'annotated-attachment-session-delete',
2735
+ sessionId: entry.sessionId,
2736
+ deletedAt: entry.watermark,
2737
+ watermark: entry.watermark
2738
+ }
2739
+ // Keep markdown and binary assets on separate ops so the protocol stays explicit:
2740
+ // document-* carries markdown content + document identity, asset-* carries image/file bytes.
2741
+ : entry.kind === 'document'
2742
+ ? {
2743
+ op: 'document-upsert',
2744
+ path: entry.path,
2745
+ updatedAt: entry.watermark,
2746
+ content: entry.content,
2747
+ assetId: entry.assetId,
2748
+ documentId: entry.documentId,
2749
+ referenceNumber: entry.referenceNumber,
2750
+ version: entry.version,
2751
+ taskId: entry.taskId,
2752
+ logicalName: entry.logicalName,
2753
+ caption: entry.caption,
2754
+ originalFilename: entry.originalFilename,
2755
+ linkRole: entry.linkRole,
2756
+ watermark: entry.watermark
2757
+ }
2758
+ : entry.kind === 'document-delete'
2759
+ ? {
2760
+ op: 'document-delete',
2761
+ path: entry.path,
2762
+ assetId: entry.assetId,
2763
+ deletedAt: entry.watermark,
2764
+ watermark: entry.watermark
2765
+ }
2766
+ : entry.kind === 'asset'
2767
+ ? {
2768
+ op: 'asset-upsert',
2769
+ path: entry.path,
2770
+ updatedAt: entry.watermark,
2771
+ contentBase64: entry.contentBase64,
2772
+ assetId: entry.assetId,
2773
+ kind: entry.kindValue,
2774
+ mimeType: entry.mimeType,
2775
+ referenceNumber: entry.referenceNumber,
2776
+ taskId: entry.taskId,
2777
+ logicalName: entry.logicalName,
2778
+ caption: entry.caption,
2779
+ originalFilename: entry.originalFilename,
2780
+ linkRole: entry.linkRole,
2781
+ watermark: entry.watermark
2782
+ }
2783
+ : entry.kind === 'asset-delete'
2784
+ ? {
2785
+ op: 'asset-delete',
2786
+ path: entry.path,
2787
+ assetId: entry.assetId,
2788
+ deletedAt: entry.watermark,
2789
+ watermark: entry.watermark
2790
+ }
2791
+ : {
2792
+ op: 'upsert',
2793
+ task: entry.task,
2794
+ archived: Boolean(entry.archived),
2795
+ watermark: entry.watermark
2796
+ })),
2797
+ nextCursor,
2798
+ hasMore
2799
+ }));
2800
+ auditAuthAction(req, 'sync-workspace-pull-success', {
2801
+ workspaceId: targetWorkspaceId,
2802
+ returnedChanges: page.length,
2803
+ hasMore
2804
+ });
2805
+ });
2806
+ // POST /api/taskforce/sync/workspace/apply-local - Apply pulled cloud deltas into local workspace store
2807
+ addRoute('POST', '/api/taskforce/sync/workspace/apply-local', async (req, res) => {
2808
+ const startedAtMs = Date.now();
2809
+ const authConfig = context.authConfig;
2810
+ if (authConfig?.runtimeMode === 'cloud') {
2811
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2812
+ res.end(JSON.stringify({ success: false, error: 'Local apply endpoint is only available in local runtime.' }));
2813
+ return;
2814
+ }
2815
+ const body = await parseJsonBody(req, 50 * 1024 * 1024);
2816
+ const bodyWorkspaceId = String(body?.workspaceId || '').trim();
2817
+ const headerWorkspaceId = String(Array.isArray(req.headers['x-taskforce-workspace-id'])
2818
+ ? req.headers['x-taskforce-workspace-id'][0]
2819
+ : req.headers['x-taskforce-workspace-id'] || '').trim();
2820
+ let workspaceId = bodyWorkspaceId || headerWorkspaceId || requestWorkspaceId(req);
2821
+ if (authConfig?.enabled) {
2822
+ const access = resolveRequestAccess(req, authConfig);
2823
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2824
+ auditAuthAction(req, 'sync-workspace-apply-unauthorized', {});
2825
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2826
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2827
+ return;
2828
+ }
2829
+ if (workspaceId && workspaceId !== 'default' && workspaceId !== 'null') {
2830
+ const allowed = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === workspaceId);
2831
+ if (!allowed) {
2832
+ auditAuthAction(req, 'sync-workspace-apply-forbidden', {
2833
+ requestedWorkspaceId: workspaceId
2834
+ });
2835
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2836
+ res.end(JSON.stringify({
2837
+ success: false,
2838
+ error: 'You do not have access to this workspace.',
2839
+ code: 'WORKSPACE_ACCESS_DENIED',
2840
+ workspaceId
2841
+ }));
2842
+ return;
2843
+ }
2844
+ }
2845
+ else {
2846
+ const accessWorkspaceId = String(access.workspaceId || '').trim();
2847
+ if (accessWorkspaceId && accessWorkspaceId !== 'default') {
2848
+ workspaceId = accessWorkspaceId;
2849
+ }
2850
+ }
2851
+ }
2852
+ if (!workspaceId || workspaceId === 'default' || workspaceId === 'null') {
2853
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2854
+ res.end(JSON.stringify({
2855
+ success: false,
2856
+ error: 'Workspace ID required.',
2857
+ code: 'WORKSPACE_ID_REQUIRED'
2858
+ }));
2859
+ return;
2860
+ }
2861
+ const incomingChanges = Array.isArray(body?.changes) ? body.changes : [];
2862
+ const workspaceMembers = Array.isArray(body?.workspaceMembers)
2863
+ ? body.workspaceMembers
2864
+ .map((entry) => normalizeWorkspaceMemberSyncSnapshot(workspaceId, entry && typeof entry === 'object' ? entry : null))
2865
+ .filter((entry) => Boolean(entry))
2866
+ : undefined;
2867
+ const allowReferenceNumberReassignment = body?.repair === true;
2868
+ try {
2869
+ const { result, emittedEventIds } = await dispatchWorkspaceSyncChanges(workspaceId, incomingChanges, {
2870
+ allowReferenceNumberReassignment,
2871
+ ...(Array.isArray(workspaceMembers) ? { workspaceMembers } : {})
2872
+ });
2873
+ const durationMs = Date.now() - startedAtMs;
2874
+ core.recordSyncEvent(workspaceId, {
2875
+ eventType: 'apply',
2876
+ status: 'success',
2877
+ changeCount: incomingChanges.length,
2878
+ requestMs: durationMs
2879
+ });
2880
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2881
+ res.end(JSON.stringify({
2882
+ success: true,
2883
+ workspaceId,
2884
+ result,
2885
+ emittedEventIds,
2886
+ durationMs
2887
+ }));
2888
+ }
2889
+ catch (error) {
2890
+ const statusCode = resolveErrorStatusCode(error, 400);
2891
+ const durationMs = Date.now() - startedAtMs;
2892
+ core.recordSyncEvent(workspaceId, {
2893
+ eventType: 'apply',
2894
+ status: 'error',
2895
+ statusCode,
2896
+ errorMessage: String(error?.message || 'Failed to apply local workspace deltas'),
2897
+ changeCount: incomingChanges.length,
2898
+ requestMs: durationMs
2899
+ });
2900
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
2901
+ res.end(JSON.stringify({
2902
+ success: false,
2903
+ error: String(error?.message || 'Failed to apply local workspace deltas'),
2904
+ code: error?.code,
2905
+ details: error?.details || undefined,
2906
+ durationMs
2907
+ }));
2908
+ }
2909
+ });
2910
+ // POST /api/taskforce/sync/workspace/push - Incremental workspace deltas from local to cloud
2911
+ addRoute('POST', '/api/taskforce/sync/workspace/push', async (req, res) => {
2912
+ const startedAtMs = Date.now();
2913
+ const authConfig = context.authConfig;
2914
+ if (!authConfig || !authConfig.enabled) {
2915
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2916
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
2917
+ return;
2918
+ }
2919
+ const access = resolveRequestAccess(req, authConfig);
2920
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
2921
+ auditAuthAction(req, 'sync-workspace-push-unauthorized', {});
2922
+ res.writeHead(401, { 'Content-Type': 'application/json' });
2923
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
2924
+ return;
2925
+ }
2926
+ const body = await parseJsonBody(req, 50 * 1024 * 1024);
2927
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
2928
+ if (!requestedWorkspaceId) {
2929
+ auditAuthAction(req, 'sync-workspace-push-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
2930
+ res.writeHead(400, { 'Content-Type': 'application/json' });
2931
+ res.end(JSON.stringify({
2932
+ success: false,
2933
+ error: 'workspaceId is required.',
2934
+ code: 'WORKSPACE_ID_REQUIRED',
2935
+ workspaceId: null,
2936
+ durationMs: Date.now() - startedAtMs
2937
+ }));
2938
+ return;
2939
+ }
2940
+ const allowed = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
2941
+ if (!allowed) {
2942
+ auditAuthAction(req, 'sync-workspace-push-forbidden', {
2943
+ requestedWorkspaceId
2944
+ });
2945
+ res.writeHead(403, { 'Content-Type': 'application/json' });
2946
+ res.end(JSON.stringify({
2947
+ success: false,
2948
+ error: 'You do not have access to this workspace.',
2949
+ code: 'WORKSPACE_ACCESS_DENIED',
2950
+ workspaceId: requestedWorkspaceId
2951
+ }));
2952
+ return;
2953
+ }
2954
+ const targetWorkspaceId = requestedWorkspaceId;
2955
+ const idempotencyKey = String(body?.idempotencyKey || '').trim();
2956
+ if (idempotencyKey) {
2957
+ const replay = readPushIdempotency(targetWorkspaceId, idempotencyKey);
2958
+ if (replay) {
2959
+ auditAuthAction(req, 'sync-workspace-push-idempotent-replay', {
2960
+ workspaceId: targetWorkspaceId,
2961
+ idempotencyKey
2962
+ });
2963
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2964
+ res.end(replay);
2965
+ return;
2966
+ }
2967
+ }
2968
+ let incomingChanges = Array.isArray(body?.changes) ? body.changes : [];
2969
+ const allowReferenceNumberReassignment = body?.repair === true;
2970
+ // Decrypt asset and document content if TASKFORCE_SYNC_KEY is configured on this server.
2971
+ // The local runtime encrypts content before pushing; the cloud must decrypt before writing to storage.
2972
+ if (typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
2973
+ const syncKey = process.env.TASKFORCE_SYNC_KEY;
2974
+ const decryptedChanges = [];
2975
+ for (const change of incomingChanges) {
2976
+ if (change.op === 'asset-upsert' && typeof change.contentBase64 === 'string' && change.contentBase64.startsWith(ENCRYPTION_PREFIX)) {
2977
+ try {
2978
+ decryptedChanges.push({ ...change, contentBase64: await decryptDocument(change.contentBase64, syncKey) });
2979
+ }
2980
+ catch {
2981
+ decryptedChanges.push(change);
2982
+ }
2983
+ }
2984
+ else if (change.op === 'document-upsert' && typeof change.content === 'string' && change.content.startsWith(ENCRYPTION_PREFIX)) {
2985
+ try {
2986
+ decryptedChanges.push({ ...change, content: await decryptDocument(change.content, syncKey) });
2987
+ }
2988
+ catch {
2989
+ decryptedChanges.push(change);
2990
+ }
2991
+ }
2992
+ else {
2993
+ decryptedChanges.push(change);
2994
+ }
2995
+ }
2996
+ incomingChanges = decryptedChanges;
2997
+ }
2998
+ try {
2999
+ const { result, deleteCount, emittedEventIds } = await dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges, {
3000
+ allowReferenceNumberReassignment
3001
+ });
3002
+ const durationMs = Date.now() - startedAtMs;
3003
+ const payload = JSON.stringify({
3004
+ success: true,
3005
+ workspaceId: targetWorkspaceId,
3006
+ idempotencyKey: idempotencyKey || null,
3007
+ emittedEventIds,
3008
+ result,
3009
+ durationMs
3010
+ });
3011
+ if (idempotencyKey) {
3012
+ writePushIdempotency(targetWorkspaceId, idempotencyKey, payload);
3013
+ }
3014
+ core.recordSyncEvent(targetWorkspaceId, {
3015
+ eventType: 'push',
3016
+ status: 'success',
3017
+ changeCount: incomingChanges.length,
3018
+ requestMs: durationMs
3019
+ });
3020
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3021
+ res.end(payload);
3022
+ auditAuthAction(req, 'sync-workspace-push-success', {
3023
+ workspaceId: targetWorkspaceId,
3024
+ idempotencyKey: idempotencyKey || null,
3025
+ changeCount: incomingChanges.length,
3026
+ deleteCount: deleteCount,
3027
+ durationMs
3028
+ });
3029
+ }
3030
+ catch (error) {
3031
+ const statusCode = resolveErrorStatusCode(error, 400);
3032
+ const durationMs = Date.now() - startedAtMs;
3033
+ core.recordSyncEvent(targetWorkspaceId, {
3034
+ eventType: 'push',
3035
+ status: 'error',
3036
+ statusCode,
3037
+ errorMessage: String(error?.message || 'Failed to apply workspace deltas'),
3038
+ changeCount: incomingChanges.length,
3039
+ requestMs: durationMs
3040
+ });
3041
+ auditAuthAction(req, 'sync-workspace-push-failed', {
3042
+ workspaceId: targetWorkspaceId,
3043
+ code: error?.code || null,
3044
+ statusCode,
3045
+ details: error?.details || null,
3046
+ durationMs
3047
+ });
3048
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
3049
+ res.end(JSON.stringify({
3050
+ success: false,
3051
+ error: String(error?.message || 'Failed to apply workspace deltas'),
3052
+ code: error?.code,
3053
+ details: error?.details || undefined,
3054
+ durationMs
3055
+ }));
3056
+ }
3057
+ });
3058
+ // GET /api/taskforce/sync/events - Query recent sync events (local runtime only)
3059
+ addRoute('GET', '/api/taskforce/sync/events', async (req, res) => {
3060
+ const authConfig = context.authConfig;
3061
+ const access = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
3062
+ if (access && (!access.authenticated || !access.userId || access.userId === 'anonymous')) {
3063
+ res.writeHead(401, { 'Content-Type': 'application/json' });
3064
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
3065
+ return;
3066
+ }
3067
+ const url = new URL(req.url || '/', 'http://localhost');
3068
+ const workspaceId = String(url.searchParams.get('workspace_id') || access?.workspaceId || '').trim() || 'default';
3069
+ const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit') || '100')));
3070
+ const events = core.listSyncEvents(workspaceId, limit);
3071
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3072
+ res.end(JSON.stringify({ success: true, events }));
3073
+ });
3074
+ // POST /api/taskforce/sync/events - Record a client-side sync event (pull results, handshake outcomes)
3075
+ addRoute('POST', '/api/taskforce/sync/events', async (req, res) => {
3076
+ const authConfig = context.authConfig;
3077
+ const access = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
3078
+ if (access && (!access.authenticated || !access.userId || access.userId === 'anonymous')) {
3079
+ res.writeHead(401, { 'Content-Type': 'application/json' });
3080
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
3081
+ return;
3082
+ }
3083
+ const body = await parseJsonBody(req);
3084
+ const allowedEventTypes = ['pull', 'handshake', 'bootstrap', 'apply', 'push'];
3085
+ const eventType = String(body?.eventType || '').trim().toLowerCase();
3086
+ if (!allowedEventTypes.includes(eventType)) {
3087
+ res.writeHead(400, { 'Content-Type': 'application/json' });
3088
+ res.end(JSON.stringify({ success: false, error: 'Invalid eventType.' }));
3089
+ return;
3090
+ }
3091
+ const workspaceId = String(body?.workspaceId || access?.workspaceId || '').trim() || 'default';
3092
+ core.recordSyncEvent(workspaceId, {
3093
+ eventType: eventType,
3094
+ status: String(body?.status || '') === 'error' ? 'error' : 'success',
3095
+ statusCode: typeof body?.statusCode === 'number' ? body.statusCode : undefined,
3096
+ errorMessage: typeof body?.errorMessage === 'string' ? body.errorMessage.slice(0, 500) : undefined,
3097
+ changeCount: typeof body?.changeCount === 'number' ? body.changeCount : undefined,
3098
+ requestMs: typeof body?.requestMs === 'number' ? body.requestMs : undefined,
3099
+ details: body?.details && typeof body.details === 'object' && !Array.isArray(body.details)
3100
+ ? body.details
3101
+ : undefined
3102
+ });
3103
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3104
+ res.end(JSON.stringify({ success: true }));
3105
+ });
3106
+ // POST /api/taskforce/sync/workspace/repair-startup - Auto-repair missing asset/document
3107
+ // metadata on workspace open. Only safe fixups: re-registers DB records for files that
3108
+ // exist on disk but have no (or a stale deleted) metadata entry. Never deletes files.
3109
+ // Called once by the sync orchestrator when the workspace becomes active.
3110
+ addRoute('POST', '/api/taskforce/sync/workspace/repair-startup', async (req, res) => {
3111
+ const authConfig = context.authConfig;
3112
+ const access = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
3113
+ if (access && (!access.authenticated || !access.userId || access.userId === 'anonymous')) {
3114
+ res.writeHead(401, { 'Content-Type': 'application/json' });
3115
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
3116
+ return;
3117
+ }
3118
+ const body = await parseJsonBody(req);
3119
+ const workspaceId = String(body?.workspaceId || requestWorkspaceId(req) || '').trim() || 'default';
3120
+ if (!workspaceAssetStore) {
3121
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3122
+ res.end(JSON.stringify({ success: true, applied: 0, skipped: 0, failed: 0, detail: 'Asset store unavailable.' }));
3123
+ return;
3124
+ }
3125
+ const { basePath } = core.getPaths();
3126
+ const result = applyWorkspaceRepair({
3127
+ workspaceId,
3128
+ basePath,
3129
+ tasks: core.listTasks(workspaceId)?.tasks || [],
3130
+ archivedTasks: core.listArchive(workspaceId)?.archived || [],
3131
+ deletedTasks: core.listWorkspaceSyncDeletes(workspaceId) || [],
3132
+ workspaceAssetStore,
3133
+ upsertDocumentWatermark: (targetWorkspaceId, docPath, contentHash, updatedAt) => {
3134
+ core.upsertDocumentWatermark(targetWorkspaceId, docPath, contentHash, updatedAt);
3135
+ }
3136
+ });
3137
+ const applied = result.actionsApplied.filter((a) => a.status === 'applied').length;
3138
+ const failed = result.actionsApplied.filter((a) => a.status === 'failed').length;
3139
+ const skipped = result.actionsApplied.filter((a) => a.status === 'skipped').length;
3140
+ res.writeHead(200, { 'Content-Type': 'application/json' });
3141
+ res.end(JSON.stringify({ success: true, applied, skipped, failed }));
3142
+ });
3143
+ }