@taskforcehq/taskforce 0.3.170 → 0.3.301

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 (698) hide show
  1. package/README.md +71 -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 +2227 -1188
  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 +68 -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 +47 -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 +243 -0
  269. package/dist/hooks/useTaskData.test.d.ts +1 -0
  270. package/dist/hooks/useTaskData.test.js +180 -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 +3299 -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-Bdq3T1Ts.js +1 -0
  489. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +1 -0
  490. package/dist/ui/assets/AnnotatedAttachmentWorkspace-jKUVo-PJ.js +3 -0
  491. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +1 -0
  492. package/dist/ui/assets/DocumentWorkspace-DuQa-uIC.js +250 -0
  493. package/dist/ui/assets/InitiativesModule-C8Pi2VTk.js +1 -0
  494. package/dist/ui/assets/PlansPage-B6BDECSi.js +1 -0
  495. package/dist/ui/assets/PlansPage-BlVC_lRq.css +1 -0
  496. package/dist/ui/assets/TaskSettings-BwB9NtmJ.css +1 -0
  497. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +8 -0
  498. package/dist/ui/assets/WorkflowsModule-b6TLuHwJ.js +5 -0
  499. package/dist/ui/assets/index-BiiF1cLQ.css +1 -0
  500. package/dist/ui/assets/index-xBWLPOEb.js +6 -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 +30 -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/deploy-prod.sh +41 -0
  610. package/scripts/deploy-staging.sh +23 -0
  611. package/scripts/migrate-planning-model.ts +233 -0
  612. package/scripts/migrate-task-data-assets.mjs +408 -0
  613. package/scripts/playwright-auth.sh +11 -0
  614. package/scripts/run-dev-proxy.sh +2 -0
  615. package/scripts/update-homebrew-formula.sh +2 -2
  616. package/scripts/with-esbuild.sh +11 -0
  617. package/src/resources/templates/WORKFLOW_V2_POLICY.md +1 -1
  618. package/src/resources/templates/workflow-sources/workflowDocs.mjs +62 -52
  619. package/src/resources/templates/workflows/collaborate.yaml +11 -5
  620. package/src/resources/templates/workflows/do.yaml +8 -2
  621. package/src/resources/templates/workflows/evaluate.yaml +13 -7
  622. package/src/resources/templates/workflows/execute.yaml +40 -23
  623. package/src/resources/templates/workflows/plan.yaml +15 -8
  624. package/src/resources/templates/workflows/review.yaml +14 -8
  625. package/dist/TaskforceScreenshot.d.ts +0 -11
  626. package/dist/TaskforceScreenshot.js +0 -113
  627. package/dist/compat/priorityCompat.d.ts +0 -2
  628. package/dist/compat/priorityCompat.js +0 -24
  629. package/dist/components/features/settings/TaxonomyManager 2.d.ts +0 -52
  630. package/dist/components/features/settings/TaxonomyManager 2.js +0 -75
  631. package/dist/components/task/TaskKanban 2.d.ts +0 -76
  632. package/dist/components/task/TaskKanban 2.js +0 -1004
  633. package/dist/components/task/TaskScreenshotSection.d.ts +0 -15
  634. package/dist/components/task/TaskScreenshotSection.js +0 -22
  635. package/dist/components/views/StandaloneLayout 2.d.ts +0 -8
  636. package/dist/components/views/StandaloneLayout 2.js +0 -1660
  637. package/dist/core/InitiativeTemplates.test.js +0 -50
  638. package/dist/core/ParentLifecycle.test.js +0 -98
  639. package/dist/migrations/clientMigrations.d.ts +0 -9
  640. package/dist/migrations/clientMigrations.js +0 -74
  641. package/dist/migrations/flags.d.ts +0 -6
  642. package/dist/migrations/flags.js +0 -8
  643. package/dist/migrations/taskMigrations.d.ts +0 -71
  644. package/dist/migrations/taskMigrations.js +0 -236
  645. package/dist/migrations/taskMigrations.test.js +0 -34
  646. package/dist/server/routes.billing.js +0 -991
  647. package/dist/server/routes.billing.test.js +0 -387
  648. package/dist/server/routes.sync.integration.test.js +0 -470
  649. package/dist/server/routes.sync.js +0 -1141
  650. package/dist/soak/syncSoakHarness.d.ts +0 -6
  651. package/dist/soak/syncSoakHarness.js +0 -10
  652. package/dist/ui/assets/index-0W5vyCpn.css +0 -1
  653. package/dist/ui/assets/index-MCZuNNfn.js +0 -23
  654. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  655. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +0 -5
  656. package/dist/ui/assets/vendor-react-DdIDsh7f.js +0 -9
  657. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +0 -3
  658. package/dist/utils/taskHierarchy.d.ts +0 -4
  659. package/dist/utils/taskHierarchy.js +0 -69
  660. package/dist/utils/taskHierarchy.test.js +0 -38
  661. package/dist/utils/taskParenting.d.ts +0 -3
  662. package/dist/utils/taskParenting.js +0 -24
  663. package/dist/utils/taskParenting.test.js +0 -21
  664. package/dist/utils/taskRelations.d.ts +0 -6
  665. package/dist/utils/taskRelations.js +0 -24
  666. package/dist/utils/taskRelations.test.js +0 -34
  667. package/scripts/check-compat-boundaries.js +0 -48
  668. /package/dist/{__tests__/planParser.test.d.ts → TaskforceCore.filter-persistence.test.d.ts} +0 -0
  669. /package/dist/{__tests__/releaseVersion.test.d.ts → TaskforceCore.routing.test.d.ts} +0 -0
  670. /package/dist/{__tests__/structuredCommentValidation.test.d.ts → annotatedAttachments/AnnotatedAttachmentSessionStore.test.d.ts} +0 -0
  671. /package/dist/{__tests__/structuredDocValidation.test.d.ts → annotatedAttachments/payload.test.d.ts} +0 -0
  672. /package/dist/{__tests__/templateEngine.test.d.ts → annotatedAttachments/types.js} +0 -0
  673. /package/dist/{__tests__/templates.test.d.ts → compat/workspaceSyncCompat.test.d.ts} +0 -0
  674. /package/dist/{__tests__/workflowDocumentFixtures.test.d.ts → components/features/AgentsModule.test.d.ts} +0 -0
  675. /package/dist/{__tests__/workflowExportMatrix.test.d.ts → components/features/AnnotatedAttachmentWorkspace.test.d.ts} +0 -0
  676. /package/dist/{__tests__/workflowExportSnapshots.test.d.ts → components/features/DocumentWorkspace.test.d.ts} +0 -0
  677. /package/dist/{__tests__/workflowGeneration.test.d.ts → components/features/settings/TaxonomyLibraryManager.test.d.ts} +0 -0
  678. /package/dist/{__tests__/workflowV2Policy.test.d.ts → components/task/TaskActionHeader.test.d.ts} +0 -0
  679. /package/dist/{core/InitiativeTemplates.test.d.ts → components/task/TaskKanban.planning-drop.test.d.ts} +0 -0
  680. /package/dist/{core/ParentLifecycle.test.d.ts → components/task/TaskStatusActions.test.d.ts} +0 -0
  681. /package/dist/{migrations/taskMigrations.test.d.ts → components/ui/TopNoticeLayer.test.d.ts} +0 -0
  682. /package/dist/{server/routes.billing.test.d.ts → components/views/AppShellHeader.test.d.ts} +0 -0
  683. /package/dist/{server/routes.sync.integration.test.d.ts → components/views/PlanComparisonPage.test.d.ts} +0 -0
  684. /package/dist/{utils/taskHierarchy.test.d.ts → components/views/chrome/WorkspaceChromeSections.test.d.ts} +0 -0
  685. /package/dist/{utils/taskParenting.test.d.ts → components/views/chrome/workspaceChrome.test.d.ts} +0 -0
  686. /package/dist/{utils/taskRelations.test.d.ts → components/views/panels/DocumentFilterToolbar.test.d.ts} +0 -0
  687. /package/dist/public/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  688. /package/dist/public/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  689. /package/dist/public/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  690. /package/dist/public/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  691. /package/dist/public/{favicon.png → archive/phoenix/favicon.png} +0 -0
  692. /package/dist/{__tests__ → services}/workflowExportSnapshots.test.js +0 -0
  693. /package/dist/{__tests__ → services}/workflowGeneration.test.js +0 -0
  694. /package/dist/ui/favicon/{android-chrome-192x192.png → archive/phoenix/android-chrome-192x192.png} +0 -0
  695. /package/dist/ui/favicon/{android-chrome-512x512.png → archive/phoenix/android-chrome-512x512.png} +0 -0
  696. /package/dist/ui/favicon/{favicon-16x16.png → archive/phoenix/favicon-16x16.png} +0 -0
  697. /package/dist/ui/favicon/{favicon-32x32.png → archive/phoenix/favicon-32x32.png} +0 -0
  698. /package/dist/ui/favicon/{favicon.png → archive/phoenix/favicon.png} +0 -0
@@ -0,0 +1,2003 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest';
2
+ import { Readable } from 'stream';
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { TaskforceCore } from '../../core/Taskforce';
7
+ import { createRoutes, matchRoute } from '../routes';
8
+ import { createSessionTokenCookie } from '../auth';
9
+ import { WorkspaceAssetStore } from '../../storage/workspaceAssetStore';
10
+ function createMockReq(method, url, body, headers) {
11
+ const req = new Readable();
12
+ req._read = () => { };
13
+ req.push(body ? JSON.stringify(body) : null);
14
+ req.push(null);
15
+ req.method = method;
16
+ req.url = url;
17
+ req.headers = headers || {};
18
+ return req;
19
+ }
20
+ function createMockRes() {
21
+ return {
22
+ writeHead: vi.fn(),
23
+ end: vi.fn()
24
+ };
25
+ }
26
+ describe('Server Routes Workspace Sync Integration', () => {
27
+ const tempDirs = [];
28
+ const originalHome = process.env.HOME;
29
+ const workspaceId = 'workspace-local-active';
30
+ const authConfig = {
31
+ runtimeMode: 'cloud',
32
+ enabled: true,
33
+ requiredForApi: true,
34
+ excludedPaths: ['/api/taskforce/health', '/api/taskforce/version', '/api/taskforce/auth'],
35
+ defaultRole: 'member',
36
+ defaultWorkspaceId: '',
37
+ sessionCookieName: 'taskforce_session',
38
+ sessionSecret: 'integration-test-secret',
39
+ sessionTtlSeconds: 60 * 60,
40
+ sessionCookieSameSite: 'None',
41
+ sessionCookieSecure: true
42
+ };
43
+ const createAuthCookie = (overrideWorkspaceId) => createSessionTokenCookie('session-sync-integration', authConfig, {
44
+ role: 'owner',
45
+ workspaceId: overrideWorkspaceId || workspaceId,
46
+ userId: 'integration-user'
47
+ });
48
+ const readJsonBody = (res) => JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
49
+ const createIsolatedProjectRoot = (prefix) => {
50
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
51
+ tempDirs.push(projectRoot);
52
+ process.env.HOME = projectRoot;
53
+ return projectRoot;
54
+ };
55
+ const buildSoftwareTaxonomyState = () => ({
56
+ categories: [
57
+ { value: 'default', label: 'General', icon: 'Inbox' },
58
+ { value: 'product', label: 'Product', icon: 'Layers', color: 'blue-500' }
59
+ ],
60
+ types: [
61
+ { value: 'feature', label: 'Feature', icon: 'Star', color: 'teal-500', status: 'active' },
62
+ { value: 'bug', label: 'Bug', icon: 'Bug', color: 'red-500', status: 'active' },
63
+ { value: 'chore', label: 'Chore', icon: 'Zap', color: 'amber-500', status: 'active' },
64
+ { value: 'refactor', label: 'Refactor', icon: 'Code', color: 'blue-500', status: 'active' },
65
+ { value: 'documentation', label: 'Docs', icon: 'Book', color: 'sky-500', status: 'active' }
66
+ ],
67
+ priorities: [
68
+ { value: 1, label: 'Low', color: 'amber-200', icon: 'ArrowDown' },
69
+ { value: 2, label: 'Medium', color: 'yellow-500', icon: 'Minus' },
70
+ { value: 3, label: 'High', color: 'orange-500', icon: 'ArrowUp' },
71
+ { value: 4, label: 'Critical', color: 'red-500', icon: 'AlertTriangle' }
72
+ ],
73
+ taxonomies: []
74
+ });
75
+ const seedAuthenticatedWorkspace = (core, options) => {
76
+ const userId = String(options?.userId || 'integration-user');
77
+ const targetWorkspaceId = String(options?.workspaceId || workspaceId);
78
+ const now = new Date().toISOString();
79
+ core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
80
+ core.db.prepare(`
81
+ INSERT OR IGNORE INTO billing_price_mappings (
82
+ tenant_id, plan_version_id, billing_interval, stripe_price_id, pricing_type, active, unit_amount, currency, created_at, updated_at
83
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
84
+ `).run('default', 'starter-v1', 'month', 'free:starter-v1:month', 'free', 1, 0, null, now, now);
85
+ core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
86
+ core.db.prepare('INSERT OR IGNORE INTO users (id, tenant_id, email, created_at, updated_at) VALUES (?, ?, ?, ?, ?)').run(userId, 'default', `${userId}@test.com`, now, now);
87
+ core.updateAccountEntitlement({
88
+ accountId: userId,
89
+ planVersionId: 'starter-v1',
90
+ actorUserId: 'system',
91
+ actorRole: 'system_admin'
92
+ });
93
+ core.createWorkspaceForUser({
94
+ userId,
95
+ name: options?.name || 'Local Active Workspace',
96
+ role: 'owner',
97
+ preferredWorkspaceId: targetWorkspaceId
98
+ });
99
+ };
100
+ afterEach(() => {
101
+ process.env.HOME = originalHome;
102
+ while (tempDirs.length > 0) {
103
+ const dir = tempDirs.pop();
104
+ if (!dir)
105
+ continue;
106
+ fs.rmSync(dir, { recursive: true, force: true });
107
+ }
108
+ });
109
+ it('persists cloud-pulled tasks into local sqlite via apply-local route', async () => {
110
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-');
111
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
112
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
113
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
114
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
115
+ expect(match).toBeTruthy();
116
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
117
+ changes: [
118
+ {
119
+ op: 'upsert',
120
+ archived: false,
121
+ task: {
122
+ id: 'task-cloud-1',
123
+ title: 'Task from cloud',
124
+ status: 'in-progress',
125
+ canceledReason: null
126
+ }
127
+ }
128
+ ]
129
+ }, { 'x-taskforce-workspace-id': workspaceId });
130
+ const res = createMockRes();
131
+ await match?.handler(req, res, {});
132
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
133
+ // Reload from disk to assert persistence, not just in-memory state.
134
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
135
+ const persisted = reloadedCore.listTasks(workspaceId);
136
+ const task = persisted.tasks.find((item) => item.id === 'task-cloud-1');
137
+ const defaultWorkspaceTasks = reloadedCore.listTasks('default').tasks;
138
+ expect(task).toBeTruthy();
139
+ expect(task?.title).toBe('Task from cloud');
140
+ expect(task?.status).toBe('in-progress');
141
+ expect(defaultWorkspaceTasks.find((item) => item.id === 'task-cloud-1')).toBeUndefined();
142
+ });
143
+ it('persists synced comments via apply-local route', async () => {
144
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-comments-');
145
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
146
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
147
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
148
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
149
+ expect(match).toBeTruthy();
150
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
151
+ changes: [
152
+ {
153
+ op: 'upsert',
154
+ archived: false,
155
+ task: {
156
+ id: 'task-cloud-comments-1',
157
+ title: 'Task with synced comments',
158
+ status: 'in-progress',
159
+ updatedAt: '2026-03-16T12:00:00.000Z',
160
+ comments: [
161
+ {
162
+ id: 'comment-cloud-1',
163
+ author: 'user',
164
+ text: 'Cloud comment',
165
+ timestamp: '2026-03-16T12:00:00.000Z'
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ ]
171
+ }, { 'x-taskforce-workspace-id': workspaceId });
172
+ const res = createMockRes();
173
+ await match?.handler(req, res, {});
174
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
175
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
176
+ const persisted = reloadedCore.getTask('task-cloud-comments-1', workspaceId);
177
+ expect(persisted?.comments).toEqual([
178
+ expect.objectContaining({
179
+ id: 'comment-cloud-1',
180
+ author: 'user',
181
+ text: 'Cloud comment',
182
+ timestamp: '2026-03-16T12:00:00.000Z'
183
+ })
184
+ ]);
185
+ });
186
+ it('persists unattached annotated attachment sessions via apply-local route', async () => {
187
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-annotated-unattached-');
188
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
189
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
190
+ const assetStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
191
+ assetStore.upsertAsset({
192
+ assetId: 'asset-image-sync-1',
193
+ workspaceId,
194
+ kind: 'image',
195
+ originalFilename: 'synced-image.png',
196
+ mimeType: 'image/png',
197
+ storageProvider: 'local',
198
+ storageKey: 'workspaces/workspace-local-active/assets/images/asset-image-sync-1-synced-image.png',
199
+ contentSha256: 'seed-hash-sync-image',
200
+ sizeBytes: 42,
201
+ createdAt: '2026-03-30T20:00:00.000Z',
202
+ updatedAt: '2026-03-30T20:00:00.000Z'
203
+ });
204
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
205
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
206
+ expect(match).toBeTruthy();
207
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
208
+ changes: [
209
+ {
210
+ op: 'annotated-attachment-session-upsert',
211
+ session: {
212
+ id: 'session-unattached-apply-1',
213
+ workspaceId,
214
+ taskId: '',
215
+ baseImageAssetId: 'asset-image-sync-1',
216
+ title: 'Synced loose notes',
217
+ globalInstruction: 'Review this image',
218
+ annotations: [
219
+ { id: 'annotation-1', kind: 'pin', order: 0, instruction: 'Look here', x: 0.2, y: 0.3 }
220
+ ],
221
+ createdByActorId: null,
222
+ updatedByActorId: null,
223
+ createdAt: '2026-03-30T20:00:00.000Z',
224
+ updatedAt: '2026-03-30T20:05:00.000Z',
225
+ deletedAt: null
226
+ }
227
+ }
228
+ ]
229
+ }, { 'x-taskforce-workspace-id': workspaceId });
230
+ const res = createMockRes();
231
+ await match?.handler(req, res, {});
232
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
233
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
234
+ expect(reloadedCore.listAnnotatedAttachmentSessions({
235
+ workspaceId,
236
+ taskId: '',
237
+ imageAssetId: 'asset-image-sync-1'
238
+ })).toEqual([
239
+ expect.objectContaining({
240
+ id: 'session-unattached-apply-1',
241
+ taskId: '',
242
+ baseImageAssetId: 'asset-image-sync-1',
243
+ title: 'Synced loose notes'
244
+ })
245
+ ]);
246
+ });
247
+ it('returns 500 when annotated attachment sync persistence fails during apply-local', async () => {
248
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-annotated-failure-');
249
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
250
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
251
+ vi.spyOn(core, 'upsertAnnotatedAttachmentSessionForSync').mockImplementation(() => {
252
+ throw new Error('annotated sync failure');
253
+ });
254
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
255
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
256
+ expect(match).toBeTruthy();
257
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
258
+ changes: [
259
+ {
260
+ op: 'annotated-attachment-session-upsert',
261
+ session: {
262
+ id: 'session-fail-1',
263
+ workspaceId,
264
+ taskId: '',
265
+ baseImageAssetId: 'asset-image-sync-1',
266
+ title: 'Broken notes',
267
+ globalInstruction: null,
268
+ annotations: [],
269
+ createdByActorId: null,
270
+ updatedByActorId: null,
271
+ createdAt: '2026-03-30T20:00:00.000Z',
272
+ updatedAt: '2026-03-30T20:05:00.000Z',
273
+ deletedAt: null
274
+ }
275
+ }
276
+ ]
277
+ }, { 'x-taskforce-workspace-id': workspaceId });
278
+ const res = createMockRes();
279
+ await match?.handler(req, res, {});
280
+ expect(res.writeHead).toHaveBeenCalledWith(500, expect.any(Object));
281
+ expect(readJsonBody(res)).toEqual(expect.objectContaining({
282
+ error: expect.stringContaining('Failed to persist annotated attachment session session-fail-1 during sync apply')
283
+ }));
284
+ });
285
+ it('persists synced ai profiles via apply-local route', async () => {
286
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-profiles-');
287
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
288
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
289
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
290
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
291
+ expect(match).toBeTruthy();
292
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
293
+ changes: [
294
+ {
295
+ op: 'ai-profile-upsert',
296
+ profile: {
297
+ id: 'ai-profile-codex',
298
+ workspaceId,
299
+ profileToken: 'tfp_codex_sync',
300
+ name: 'Codex',
301
+ username: 'codex',
302
+ icon: 'Bot',
303
+ color: '#0ea5e9',
304
+ surfaceType: 'coding_tool',
305
+ createdAt: '2026-03-22T10:00:00.000Z',
306
+ updatedAt: '2026-03-22T10:05:00.000Z',
307
+ lastActiveAt: '2026-03-22T10:05:00.000Z'
308
+ }
309
+ }
310
+ ]
311
+ }, { 'x-taskforce-workspace-id': workspaceId });
312
+ const res = createMockRes();
313
+ await match?.handler(req, res, {});
314
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
315
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
316
+ expect(reloadedCore.listAiProfiles(workspaceId)).toEqual([
317
+ expect.objectContaining({
318
+ id: 'ai-profile-codex',
319
+ name: 'Codex',
320
+ username: 'codex',
321
+ surfaceType: 'coding_tool'
322
+ })
323
+ ]);
324
+ });
325
+ it('rejects synced ai profiles with an invalid surfaceType via apply-local route', async () => {
326
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-profiles-invalid-surface-');
327
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
328
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
329
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
330
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
331
+ expect(match).toBeTruthy();
332
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
333
+ changes: [
334
+ {
335
+ op: 'ai-profile-upsert',
336
+ profile: {
337
+ id: 'ai-profile-invalid',
338
+ workspaceId,
339
+ profileToken: 'tfp_invalid_surface',
340
+ name: 'OpenClaw Agent',
341
+ username: 'openclaw-agent',
342
+ icon: 'Bot',
343
+ color: '#64748b',
344
+ surfaceType: 'desktop_app',
345
+ createdAt: '2026-03-22T10:00:00.000Z',
346
+ updatedAt: '2026-03-22T10:05:00.000Z',
347
+ lastActiveAt: '2026-03-22T10:05:00.000Z'
348
+ }
349
+ }
350
+ ]
351
+ }, { 'x-taskforce-workspace-id': workspaceId });
352
+ const res = createMockRes();
353
+ await match?.handler(req, res, {});
354
+ expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
355
+ expect(readJsonBody(res)).toEqual(expect.objectContaining({
356
+ code: 'AI_PROFILE_SURFACE_TYPE_INVALID',
357
+ error: expect.stringContaining('surfaceType')
358
+ }));
359
+ });
360
+ it('applies tasks whose assignee resolves after ai profile sync', async () => {
361
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-ai-assignee-');
362
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
363
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
364
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
365
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
366
+ expect(match).toBeTruthy();
367
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
368
+ changes: [
369
+ {
370
+ op: 'ai-profile-upsert',
371
+ profile: {
372
+ id: 'ai-profile-codex',
373
+ workspaceId,
374
+ profileToken: 'tfp_codex_sync',
375
+ name: 'Codex',
376
+ username: 'codex',
377
+ icon: 'Bot',
378
+ color: '#0ea5e9',
379
+ surfaceType: 'coding_tool',
380
+ createdAt: '2026-03-22T10:00:00.000Z',
381
+ updatedAt: '2026-03-22T10:05:00.000Z',
382
+ lastActiveAt: '2026-03-22T10:05:00.000Z'
383
+ }
384
+ },
385
+ {
386
+ op: 'upsert',
387
+ archived: false,
388
+ task: {
389
+ id: 'task-ai-assignee-1',
390
+ title: 'Task assigned to synced AI',
391
+ status: 'task',
392
+ assignee: 'ai-profile-codex'
393
+ }
394
+ }
395
+ ]
396
+ }, { 'x-taskforce-workspace-id': workspaceId });
397
+ const res = createMockRes();
398
+ await match?.handler(req, res, {});
399
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
400
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
401
+ const task = reloadedCore.getTask('task-ai-assignee-1', workspaceId);
402
+ expect(task?.assignee).toBe('ai-profile-codex');
403
+ expect(task?.assigneeActor).toEqual(expect.objectContaining({
404
+ id: 'ai-profile-codex',
405
+ kind: 'ai',
406
+ label: 'Codex'
407
+ }));
408
+ });
409
+ it('applies tasks whose assignee resolves to a workspace member', async () => {
410
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-human-assignee-');
411
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
412
+ seedAuthenticatedWorkspace(core, { workspaceId, userId: 'member-1', name: 'Local Active Workspace' });
413
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
414
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
415
+ expect(match).toBeTruthy();
416
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
417
+ changes: [
418
+ {
419
+ op: 'upsert',
420
+ archived: false,
421
+ task: {
422
+ id: 'task-human-assignee-1',
423
+ title: 'Task assigned to workspace member',
424
+ status: 'task',
425
+ assignee: 'member-1'
426
+ }
427
+ }
428
+ ]
429
+ }, { 'x-taskforce-workspace-id': workspaceId });
430
+ const res = createMockRes();
431
+ await match?.handler(req, res, {});
432
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
433
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
434
+ const task = reloadedCore.getTask('task-human-assignee-1', workspaceId);
435
+ expect(task?.assignee).toBe('member-1');
436
+ expect(task?.assigneeActor).toEqual(expect.objectContaining({
437
+ id: 'member-1',
438
+ kind: 'human'
439
+ }));
440
+ });
441
+ it('preserves unresolved human assignees during apply-local sync', async () => {
442
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-unresolved-human-assignee-');
443
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
444
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
445
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
446
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
447
+ expect(match).toBeTruthy();
448
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
449
+ changes: [
450
+ {
451
+ op: 'upsert',
452
+ archived: false,
453
+ task: {
454
+ id: 'task-unresolved-human-assignee-1',
455
+ title: 'Task assigned to remote human',
456
+ status: 'task',
457
+ assignee: 'user-019c91d9-c143-771d-b0bf-36e318cfb353'
458
+ }
459
+ }
460
+ ]
461
+ }, { 'x-taskforce-workspace-id': workspaceId });
462
+ const res = createMockRes();
463
+ await match?.handler(req, res, {});
464
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
465
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
466
+ const task = reloadedCore.getTask('task-unresolved-human-assignee-1', workspaceId);
467
+ expect(task?.assignee).toBe('user-019c91d9-c143-771d-b0bf-36e318cfb353');
468
+ expect(task?.assigneeActor).toEqual(expect.objectContaining({
469
+ id: 'user-019c91d9-c143-771d-b0bf-36e318cfb353',
470
+ kind: 'human',
471
+ meta: expect.objectContaining({ unresolved: true })
472
+ }));
473
+ });
474
+ it('still rejects unresolved ai assignees during apply-local sync', async () => {
475
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-unresolved-ai-assignee-');
476
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
477
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
478
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
479
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
480
+ expect(match).toBeTruthy();
481
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
482
+ changes: [
483
+ {
484
+ op: 'upsert',
485
+ archived: false,
486
+ task: {
487
+ id: 'task-unresolved-ai-assignee-1',
488
+ title: 'Task assigned to missing AI profile',
489
+ status: 'task',
490
+ assignee: 'ai-profile-missing'
491
+ }
492
+ }
493
+ ]
494
+ }, { 'x-taskforce-workspace-id': workspaceId });
495
+ const res = createMockRes();
496
+ await match?.handler(req, res, {});
497
+ expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
498
+ expect(res.end).toHaveBeenCalledWith(expect.stringContaining('references unresolved assignee ai-profile-missing'));
499
+ });
500
+ it('persists archived cloud-pulled tasks into local sqlite archive via apply-local route', async () => {
501
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-archived-');
502
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
503
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
504
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
505
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
506
+ expect(match).toBeTruthy();
507
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
508
+ changes: [
509
+ {
510
+ op: 'upsert',
511
+ archived: true,
512
+ task: {
513
+ id: 'task-cloud-archived-1',
514
+ title: 'Archived from cloud',
515
+ status: 'done',
516
+ isArchived: true
517
+ }
518
+ }
519
+ ]
520
+ }, { 'x-taskforce-workspace-id': workspaceId });
521
+ const res = createMockRes();
522
+ await match?.handler(req, res, {});
523
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
524
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
525
+ const persistedArchive = reloadedCore.listArchive(workspaceId);
526
+ const archivedTask = persistedArchive.archived.find((item) => item.id === 'task-cloud-archived-1');
527
+ const defaultWorkspaceArchive = reloadedCore.listArchive('default').archived;
528
+ expect(archivedTask).toBeTruthy();
529
+ expect(archivedTask?.title).toBe('Archived from cloud');
530
+ expect(archivedTask?.isArchived).toBe(true);
531
+ expect(defaultWorkspaceArchive.find((item) => item.id === 'task-cloud-archived-1')).toBeUndefined();
532
+ });
533
+ it('persists archived task workstream links into local sqlite archive via apply-local route', async () => {
534
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-archived-workstream-');
535
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
536
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
537
+ core.createInitiative({
538
+ id: 'initiative-archived-sync-1',
539
+ title: 'Archived synced initiative',
540
+ createdAt: '2026-03-15T00:00:00.000Z',
541
+ updatedAt: '2026-03-15T00:00:00.000Z'
542
+ }, workspaceId);
543
+ core.createWorkstream({
544
+ id: 'workstream-archived-sync-1',
545
+ initiativeId: 'initiative-archived-sync-1',
546
+ title: 'Archived synced workstream',
547
+ createdAt: '2026-03-15T00:00:00.000Z',
548
+ updatedAt: '2026-03-15T00:00:00.000Z'
549
+ }, workspaceId);
550
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
551
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
552
+ expect(match).toBeTruthy();
553
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
554
+ changes: [
555
+ {
556
+ op: 'upsert',
557
+ archived: true,
558
+ task: {
559
+ id: 'task-cloud-archived-linked-1',
560
+ title: 'Archived linked task',
561
+ status: 'done',
562
+ workstreamId: 'workstream-archived-sync-1',
563
+ isArchived: true,
564
+ updatedAt: '2026-03-16T00:27:08.000Z'
565
+ }
566
+ }
567
+ ]
568
+ }, { 'x-taskforce-workspace-id': workspaceId });
569
+ const res = createMockRes();
570
+ await match?.handler(req, res, {});
571
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
572
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
573
+ const persistedArchive = reloadedCore.listArchive(workspaceId);
574
+ const archivedTask = persistedArchive.archived.find((item) => item.id === 'task-cloud-archived-linked-1');
575
+ expect(archivedTask).toBeTruthy();
576
+ expect(archivedTask?.isArchived).toBe(true);
577
+ expect(archivedTask?.workstreamId).toBe('workstream-archived-sync-1');
578
+ });
579
+ it('applies multiple terminal sync updates in one batch', async () => {
580
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-terminal-status-');
581
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
582
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
583
+ core.createTask({
584
+ id: 'task-terminal-1',
585
+ title: 'Terminal task 1',
586
+ status: 'task',
587
+ createdAt: '2026-03-15T00:00:00.000Z',
588
+ updatedAt: '2026-03-15T00:00:00.000Z'
589
+ }, {
590
+ workspaceId,
591
+ allowClientProvidedId: true
592
+ });
593
+ core.createTask({
594
+ id: 'task-terminal-2',
595
+ title: 'Terminal task 2',
596
+ status: 'task',
597
+ createdAt: '2026-03-15T00:00:00.000Z',
598
+ updatedAt: '2026-03-15T00:00:00.000Z'
599
+ }, {
600
+ workspaceId,
601
+ allowClientProvidedId: true
602
+ });
603
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
604
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
605
+ expect(match).toBeTruthy();
606
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
607
+ changes: [
608
+ {
609
+ op: 'upsert',
610
+ archived: false,
611
+ task: {
612
+ id: 'task-terminal-1',
613
+ title: 'Terminal task 1',
614
+ status: 'done',
615
+ updatedAt: '2026-03-16T00:27:08.000Z'
616
+ }
617
+ },
618
+ {
619
+ op: 'upsert',
620
+ archived: false,
621
+ task: {
622
+ id: 'task-terminal-2',
623
+ title: 'Terminal task 2',
624
+ status: 'done',
625
+ updatedAt: '2026-03-16T00:27:08.000Z'
626
+ }
627
+ }
628
+ ]
629
+ }, { 'x-taskforce-workspace-id': workspaceId });
630
+ const res = createMockRes();
631
+ await match?.handler(req, res, {});
632
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
633
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
634
+ const first = reloadedCore.getTask('task-terminal-1', workspaceId);
635
+ const second = reloadedCore.getTask('task-terminal-2', workspaceId);
636
+ expect(first?.status).toBe('done');
637
+ expect(second?.status).toBe('done');
638
+ });
639
+ it('preserves task workstream links when applying a synced snapshot', async () => {
640
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-workstream-link-');
641
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
642
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
643
+ core.createInitiative({
644
+ id: 'initiative-sync-1',
645
+ title: 'Synced initiative',
646
+ createdAt: '2026-03-15T00:00:00.000Z',
647
+ updatedAt: '2026-03-15T00:00:00.000Z'
648
+ }, workspaceId);
649
+ core.createWorkstream({
650
+ id: 'workstream-sync-1',
651
+ initiativeId: 'initiative-sync-1',
652
+ title: 'Synced workstream',
653
+ createdAt: '2026-03-15T00:00:00.000Z',
654
+ updatedAt: '2026-03-15T00:00:00.000Z'
655
+ }, workspaceId);
656
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
657
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
658
+ expect(match).toBeTruthy();
659
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
660
+ changes: [
661
+ {
662
+ op: 'upsert',
663
+ archived: false,
664
+ task: {
665
+ id: 'task-workstream-linked-1',
666
+ title: 'Linked task',
667
+ status: 'task',
668
+ workstreamId: 'workstream-sync-1',
669
+ updatedAt: '2026-03-16T00:27:08.000Z'
670
+ }
671
+ }
672
+ ]
673
+ }, { 'x-taskforce-workspace-id': workspaceId });
674
+ const res = createMockRes();
675
+ await match?.handler(req, res, {});
676
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
677
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
678
+ const task = reloadedCore.getTask('task-workstream-linked-1', workspaceId);
679
+ expect(task?.workstreamId).toBe('workstream-sync-1');
680
+ });
681
+ it('persists checklist items when applying a synced snapshot locally', async () => {
682
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-checklist-apply-');
683
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
684
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
685
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
686
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
687
+ expect(match).toBeTruthy();
688
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
689
+ changes: [
690
+ {
691
+ op: 'upsert',
692
+ archived: false,
693
+ task: {
694
+ id: 'task-checklist-sync-1',
695
+ title: 'Checklist sync task',
696
+ status: 'task',
697
+ updatedAt: '2026-03-16T00:27:08.000Z',
698
+ checklistItems: [
699
+ {
700
+ id: 'checklist-sync-1',
701
+ title: 'First synced item',
702
+ isCompleted: false,
703
+ order: 0,
704
+ createdAt: '2026-03-16T00:00:00.000Z',
705
+ updatedAt: '2026-03-16T00:27:08.000Z',
706
+ },
707
+ {
708
+ id: 'checklist-sync-2',
709
+ title: 'Second synced item',
710
+ isCompleted: true,
711
+ order: 1,
712
+ createdAt: '2026-03-16T00:00:00.000Z',
713
+ updatedAt: '2026-03-16T00:27:08.000Z',
714
+ }
715
+ ]
716
+ }
717
+ }
718
+ ]
719
+ }, { 'x-taskforce-workspace-id': workspaceId });
720
+ const res = createMockRes();
721
+ await match?.handler(req, res, {});
722
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
723
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
724
+ const task = reloadedCore.getTask('task-checklist-sync-1', workspaceId);
725
+ expect(task?.checklistItems?.map((item) => item.title)).toEqual([
726
+ 'First synced item',
727
+ 'Second synced item',
728
+ ]);
729
+ expect(task?.checklistItems?.map((item) => item.isCompleted)).toEqual([false, true]);
730
+ });
731
+ it('serves synced workstream links back through the live cloud tasks route after push', async () => {
732
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-workstream-route-');
733
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
734
+ seedAuthenticatedWorkspace(core);
735
+ core.createInitiative({
736
+ id: 'initiative-cloud-1',
737
+ title: 'Cloud initiative',
738
+ createdAt: '2026-03-15T00:00:00.000Z',
739
+ updatedAt: '2026-03-15T00:00:00.000Z'
740
+ }, workspaceId);
741
+ core.createWorkstream({
742
+ id: 'workstream-cloud-1',
743
+ initiativeId: 'initiative-cloud-1',
744
+ title: 'Cloud workstream',
745
+ createdAt: '2026-03-15T00:00:00.000Z',
746
+ updatedAt: '2026-03-15T00:00:00.000Z'
747
+ }, workspaceId);
748
+ const routes = createRoutes(core, { authConfig });
749
+ const cookie = createAuthCookie(workspaceId);
750
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
751
+ expect(pushMatch).toBeTruthy();
752
+ const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
753
+ workspaceId,
754
+ idempotencyKey: 'push-cloud-workstream-link-1',
755
+ changes: [
756
+ {
757
+ op: 'upsert',
758
+ archived: false,
759
+ task: {
760
+ id: 'task-cloud-workstream-1',
761
+ title: 'Cloud linked task',
762
+ status: 'task',
763
+ workstreamId: 'workstream-cloud-1',
764
+ updatedAt: '2026-03-16T00:27:08.000Z'
765
+ }
766
+ }
767
+ ]
768
+ }, { cookie });
769
+ const pushRes = createMockRes();
770
+ await pushMatch?.handler(pushReq, pushRes, {});
771
+ expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
772
+ const tasksMatch = matchRoute('GET', '/api/taskforce/tasks', routes);
773
+ expect(tasksMatch).toBeTruthy();
774
+ const tasksReq = createMockReq('GET', `/api/taskforce/tasks?workspaceId=${workspaceId}`, undefined, { cookie });
775
+ const tasksRes = createMockRes();
776
+ await tasksMatch?.handler(tasksReq, tasksRes, {});
777
+ expect(tasksRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
778
+ const payload = readJsonBody(tasksRes);
779
+ const task = Array.isArray(payload?.tasks)
780
+ ? payload.tasks.find((entry) => entry?.id === 'task-cloud-workstream-1')
781
+ : null;
782
+ expect(task).toBeTruthy();
783
+ expect(task?.workstreamId).toBe('workstream-cloud-1');
784
+ });
785
+ it('prefers incoming workstream links when timestamps tie but task relationships differ', async () => {
786
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-workstream-tie-');
787
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
788
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
789
+ core.createInitiative({
790
+ id: 'initiative-sync-2',
791
+ title: 'Tie-break initiative',
792
+ createdAt: '2026-03-15T00:00:00.000Z',
793
+ updatedAt: '2026-03-15T00:00:00.000Z'
794
+ }, workspaceId);
795
+ core.createWorkstream({
796
+ id: 'workstream-sync-2',
797
+ initiativeId: 'initiative-sync-2',
798
+ title: 'Tie-break workstream',
799
+ createdAt: '2026-03-15T00:00:00.000Z',
800
+ updatedAt: '2026-03-15T00:00:00.000Z'
801
+ }, workspaceId);
802
+ core.createTask({
803
+ id: 'task-workstream-linked-2',
804
+ title: 'Linked task',
805
+ status: 'task',
806
+ updatedAt: '2026-03-16T00:27:08.000Z',
807
+ createdAt: '2026-03-16T00:00:00.000Z'
808
+ }, {
809
+ workspaceId,
810
+ allowClientProvidedId: true,
811
+ syncUpdatedAt: '2026-03-16T00:27:08.000Z',
812
+ });
813
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
814
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
815
+ expect(match).toBeTruthy();
816
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
817
+ changes: [
818
+ {
819
+ op: 'upsert',
820
+ archived: false,
821
+ task: {
822
+ id: 'task-workstream-linked-2',
823
+ title: 'Linked task',
824
+ status: 'task',
825
+ workstreamId: 'workstream-sync-2',
826
+ updatedAt: '2026-03-16T00:27:08.000Z',
827
+ createdAt: '2026-03-16T00:00:00.000Z'
828
+ }
829
+ }
830
+ ]
831
+ }, { 'x-taskforce-workspace-id': workspaceId });
832
+ const res = createMockRes();
833
+ await match?.handler(req, res, {});
834
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
835
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
836
+ const task = reloadedCore.getTask('task-workstream-linked-2', workspaceId);
837
+ expect(task?.workstreamId).toBe('workstream-sync-2');
838
+ });
839
+ it('prefers incoming checklist changes when timestamps tie but checklist content differs', async () => {
840
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-checklist-tie-');
841
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
842
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
843
+ core.createTask({
844
+ id: 'task-checklist-sync-2',
845
+ title: 'Checklist tie-break task',
846
+ status: 'task',
847
+ updatedAt: '2026-03-16T00:27:08.000Z',
848
+ createdAt: '2026-03-16T00:00:00.000Z',
849
+ checklistItems: [
850
+ {
851
+ id: 'checklist-existing-1',
852
+ taskId: 'task-checklist-sync-2',
853
+ title: 'Old local item',
854
+ isCompleted: false,
855
+ order: 0,
856
+ createdAt: '2026-03-16T00:00:00.000Z',
857
+ updatedAt: '2026-03-16T00:27:08.000Z',
858
+ }
859
+ ]
860
+ }, { workspaceId, allowClientProvidedId: true });
861
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
862
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
863
+ expect(match).toBeTruthy();
864
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
865
+ changes: [
866
+ {
867
+ op: 'upsert',
868
+ archived: false,
869
+ task: {
870
+ id: 'task-checklist-sync-2',
871
+ title: 'Checklist tie-break task',
872
+ status: 'task',
873
+ updatedAt: '2026-03-16T00:27:08.000Z',
874
+ checklistItems: [
875
+ {
876
+ id: 'checklist-incoming-1',
877
+ title: 'Incoming synced item',
878
+ isCompleted: true,
879
+ order: 0,
880
+ createdAt: '2026-03-16T00:00:00.000Z',
881
+ updatedAt: '2026-03-16T00:27:08.000Z',
882
+ }
883
+ ]
884
+ }
885
+ }
886
+ ]
887
+ }, { 'x-taskforce-workspace-id': workspaceId });
888
+ const res = createMockRes();
889
+ await match?.handler(req, res, {});
890
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
891
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
892
+ const task = reloadedCore.getTask('task-checklist-sync-2', workspaceId);
893
+ expect(task?.checklistItems?.map((item) => item.title)).toEqual(['Incoming synced item']);
894
+ expect(task?.checklistItems?.map((item) => item.isCompleted)).toEqual([true]);
895
+ });
896
+ it('does not let an older pulled snapshot revert a newer local in-progress update', async () => {
897
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-newer-local-status-');
898
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
899
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
900
+ core.createTask({
901
+ id: 'task-status-1',
902
+ title: 'Task status',
903
+ status: 'task',
904
+ createdAt: '2026-03-16T00:00:00.000Z'
905
+ }, {
906
+ workspaceId,
907
+ allowClientProvidedId: true
908
+ });
909
+ const locallyUpdated = core.updateTask('task-status-1', {
910
+ status: 'in-progress'
911
+ }, workspaceId, { persistBackup: false });
912
+ expect(locallyUpdated?.status).toBe('in-progress');
913
+ expect(String(locallyUpdated?.updatedAt || '')).not.toBe('');
914
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
915
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
916
+ expect(match).toBeTruthy();
917
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
918
+ changes: [
919
+ {
920
+ op: 'upsert',
921
+ archived: false,
922
+ task: {
923
+ id: 'task-status-1',
924
+ title: 'Task status',
925
+ status: 'task',
926
+ updatedAt: '2026-03-16T00:00:01.000Z'
927
+ }
928
+ }
929
+ ]
930
+ }, { 'x-taskforce-workspace-id': workspaceId });
931
+ const res = createMockRes();
932
+ await match?.handler(req, res, {});
933
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
934
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
935
+ const persisted = reloadedCore.getTask('task-status-1', workspaceId);
936
+ expect(persisted?.status).toBe('in-progress');
937
+ expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse('2026-03-16T00:00:01.000Z'));
938
+ });
939
+ it('applies cloud delete operations to local sqlite across reload via apply-local route', async () => {
940
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-delete-');
941
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
942
+ seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
943
+ seedCore.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
944
+ const seeded = seedCore.createTask({
945
+ title: 'Will be deleted by cloud sync',
946
+ category: 'Default',
947
+ type: 'feature',
948
+ priority: 2,
949
+ status: 'task'
950
+ }, { workspaceId });
951
+ expect(seeded.id).toBeTruthy();
952
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
953
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
954
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
955
+ expect(match).toBeTruthy();
956
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
957
+ changes: [
958
+ { op: 'delete', taskId: seeded.id }
959
+ ]
960
+ }, { 'x-taskforce-workspace-id': workspaceId });
961
+ const res = createMockRes();
962
+ await match?.handler(req, res, {});
963
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
964
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
965
+ const persisted = reloadedCore.listTasks(workspaceId);
966
+ const deletedTask = persisted.tasks.find((item) => item.id === seeded.id);
967
+ expect(deletedTask).toBeUndefined();
968
+ });
969
+ it('keeps task deleted when apply-local receives same-task upsert and delete in one batch', async () => {
970
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-delete-wins-');
971
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
972
+ seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
973
+ seedCore.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
974
+ const seeded = seedCore.createTask({
975
+ title: 'Seed task for delete-wins',
976
+ category: 'Default',
977
+ type: 'feature',
978
+ priority: 2,
979
+ status: 'task'
980
+ }, { workspaceId });
981
+ expect(seeded.id).toBeTruthy();
982
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
983
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
984
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
985
+ expect(match).toBeTruthy();
986
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
987
+ changes: [
988
+ {
989
+ op: 'upsert',
990
+ archived: false,
991
+ task: {
992
+ id: seeded.id,
993
+ title: 'Stale cloud upsert'
994
+ }
995
+ },
996
+ { op: 'delete', taskId: seeded.id }
997
+ ]
998
+ }, { 'x-taskforce-workspace-id': workspaceId });
999
+ const res = createMockRes();
1000
+ await match?.handler(req, res, {});
1001
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1002
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1003
+ const persisted = reloadedCore.listTasks(workspaceId);
1004
+ const shouldRemainDeleted = persisted.tasks.find((item) => item.id === seeded.id);
1005
+ expect(shouldRemainDeleted).toBeUndefined();
1006
+ });
1007
+ it('applies document-delete operations by deleting file and clearing watermark index', async () => {
1008
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-delete-');
1009
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1010
+ seedCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1011
+ const syncRoot = seedCore.getPaths().basePath;
1012
+ const docsDir = path.join(syncRoot, 'docs');
1013
+ fs.mkdirSync(docsDir, { recursive: true });
1014
+ const relativeDocPath = 'docs/sync-delete.md';
1015
+ const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1016
+ fs.writeFileSync(absoluteDocPath, '# sync delete\n', 'utf8');
1017
+ seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
1018
+ expect(seedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeTruthy();
1019
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1020
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1021
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1022
+ expect(match).toBeTruthy();
1023
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1024
+ changes: [
1025
+ { op: 'document-delete', path: relativeDocPath }
1026
+ ]
1027
+ }, { 'x-taskforce-workspace-id': workspaceId });
1028
+ const res = createMockRes();
1029
+ await match?.handler(req, res, {});
1030
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1031
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1032
+ expect(fs.existsSync(absoluteDocPath)).toBe(false);
1033
+ expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
1034
+ });
1035
+ it('persists canonical markdown asset metadata across reload via apply-local route', async () => {
1036
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-asset-');
1037
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1038
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1039
+ core.createTask({
1040
+ id: 'task-doc-asset-1',
1041
+ title: 'Task with synced markdown asset',
1042
+ status: 'task'
1043
+ }, {
1044
+ workspaceId,
1045
+ allowClientProvidedId: true
1046
+ });
1047
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1048
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1049
+ expect(match).toBeTruthy();
1050
+ const relativeDocPath = 'docs/phase1-sync.md';
1051
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1052
+ changes: [
1053
+ {
1054
+ op: 'document-upsert',
1055
+ path: relativeDocPath,
1056
+ updatedAt: '2026-03-17T12:00:00.000Z',
1057
+ content: '# canonical markdown\n',
1058
+ assetId: 'asset-doc-sync-1',
1059
+ documentId: 'doc-sync-1',
1060
+ version: 3,
1061
+ taskId: 'task-doc-asset-1',
1062
+ originalFilename: 'phase1-sync.md',
1063
+ linkRole: 'attachment'
1064
+ }
1065
+ ]
1066
+ }, { 'x-taskforce-workspace-id': workspaceId });
1067
+ const res = createMockRes();
1068
+ await match?.handler(req, res, {});
1069
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1070
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1071
+ const absoluteDocPath = path.join(reloadedCore.getPaths().basePath, relativeDocPath);
1072
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1073
+ const asset = store.get('asset-doc-sync-1', workspaceId);
1074
+ const links = store.listLinksForAsset('asset-doc-sync-1', workspaceId);
1075
+ expect(fs.readFileSync(absoluteDocPath, 'utf8')).toBe('# canonical markdown\n');
1076
+ expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toEqual({
1077
+ updatedAt: '2026-03-17T12:00:00.000Z',
1078
+ contentHash: expect.any(String)
1079
+ });
1080
+ expect(asset).toEqual(expect.objectContaining({
1081
+ assetId: 'asset-doc-sync-1',
1082
+ workspaceId,
1083
+ kind: 'document',
1084
+ storageProvider: 'local',
1085
+ storageKey: relativeDocPath,
1086
+ documentId: 'doc-sync-1',
1087
+ version: 3,
1088
+ originalFilename: 'phase1-sync.md',
1089
+ mimeType: 'text/markdown',
1090
+ deletedAt: null
1091
+ }));
1092
+ expect(links).toEqual([
1093
+ expect.objectContaining({
1094
+ assetId: 'asset-doc-sync-1',
1095
+ workspaceId,
1096
+ taskId: 'task-doc-asset-1',
1097
+ role: 'attachment',
1098
+ deletedAt: null
1099
+ })
1100
+ ]);
1101
+ });
1102
+ it('retries document asset reference allocation during apply-local when persistence hits a collision', async () => {
1103
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-asset-collision-');
1104
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1105
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1106
+ core.createTask({
1107
+ id: 'task-doc-asset-collision-1',
1108
+ title: 'Task with collided synced markdown asset',
1109
+ status: 'task'
1110
+ }, {
1111
+ workspaceId,
1112
+ allowClientProvidedId: true
1113
+ });
1114
+ const db = core.getDatabaseAdapter();
1115
+ const seededStore = new WorkspaceAssetStore(db, core.getTenantId());
1116
+ seededStore.upsertAsset({
1117
+ assetId: 'asset-doc-existing-collision',
1118
+ workspaceId,
1119
+ kind: 'document',
1120
+ originalFilename: 'existing.md',
1121
+ mimeType: 'text/markdown',
1122
+ storageProvider: 'local',
1123
+ storageKey: 'docs/existing-collision.md',
1124
+ contentSha256: 'sha-existing-collision',
1125
+ sizeBytes: 10,
1126
+ referenceNumber: 7,
1127
+ createdAt: '2026-03-17T11:55:00.000Z',
1128
+ updatedAt: '2026-03-17T11:55:00.000Z'
1129
+ });
1130
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1131
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1132
+ expect(match).toBeTruthy();
1133
+ const relativeDocPath = 'docs/phase1-sync-collision.md';
1134
+ const originalPrepare = db.prepare.bind(db);
1135
+ let forcedCollision = false;
1136
+ vi.spyOn(db, 'prepare').mockImplementation((...args) => {
1137
+ const [sql] = args;
1138
+ const statement = originalPrepare(...args);
1139
+ if (typeof sql !== 'string') {
1140
+ return statement;
1141
+ }
1142
+ if (!sql.includes('INSERT INTO workspace_assets')) {
1143
+ return statement;
1144
+ }
1145
+ return {
1146
+ ...statement,
1147
+ run: (...args) => {
1148
+ const storageKeyArg = args[8];
1149
+ const referenceNumberArg = args[12];
1150
+ if (!forcedCollision
1151
+ && storageKeyArg === relativeDocPath
1152
+ && referenceNumberArg === 8) {
1153
+ forcedCollision = true;
1154
+ throw new Error('UNIQUE constraint failed: workspace_assets.tenant_id, workspace_assets.workspace_id, workspace_assets.kind, workspace_assets.reference_number');
1155
+ }
1156
+ return statement.run(...args);
1157
+ }
1158
+ };
1159
+ });
1160
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1161
+ changes: [
1162
+ {
1163
+ op: 'document-upsert',
1164
+ path: relativeDocPath,
1165
+ updatedAt: '2026-03-17T12:00:00.000Z',
1166
+ content: '# canonical markdown after collision\n',
1167
+ assetId: 'asset-doc-sync-collision-1',
1168
+ documentId: 'doc-sync-collision-1',
1169
+ referenceNumber: 8,
1170
+ version: 3,
1171
+ taskId: 'task-doc-asset-collision-1',
1172
+ originalFilename: 'phase1-sync-collision.md',
1173
+ linkRole: 'attachment'
1174
+ }
1175
+ ]
1176
+ }, { 'x-taskforce-workspace-id': workspaceId });
1177
+ const res = createMockRes();
1178
+ await match?.handler(req, res, {});
1179
+ expect(forcedCollision).toBe(true);
1180
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1181
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1182
+ const absoluteDocPath = path.join(reloadedCore.getPaths().basePath, relativeDocPath);
1183
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1184
+ const asset = store.get('asset-doc-sync-collision-1', workspaceId);
1185
+ const links = store.listLinksForAsset('asset-doc-sync-collision-1', workspaceId);
1186
+ const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1187
+ expect(fs.readFileSync(absoluteDocPath, 'utf8')).toBe('# canonical markdown after collision\n');
1188
+ expect(asset).toEqual(expect.objectContaining({
1189
+ assetId: 'asset-doc-sync-collision-1',
1190
+ workspaceId,
1191
+ kind: 'document',
1192
+ storageKey: relativeDocPath,
1193
+ documentId: 'doc-sync-collision-1',
1194
+ version: 3,
1195
+ referenceNumber: 9,
1196
+ deletedAt: null
1197
+ }));
1198
+ expect(links).toEqual([
1199
+ expect.objectContaining({
1200
+ assetId: 'asset-doc-sync-collision-1',
1201
+ workspaceId,
1202
+ taskId: 'task-doc-asset-collision-1',
1203
+ role: 'attachment',
1204
+ deletedAt: null
1205
+ })
1206
+ ]);
1207
+ expect(syncEvents.some((event) => event.errorMessage === 'Failed to persist document watermark after write.')).toBe(false);
1208
+ });
1209
+ it('persists canonical binary assets across reload via apply-local route', async () => {
1210
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-binary-asset-');
1211
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1212
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1213
+ core.createTask({
1214
+ id: 'task-binary-asset-1',
1215
+ title: 'Task with synced binary asset',
1216
+ status: 'task'
1217
+ }, {
1218
+ workspaceId,
1219
+ allowClientProvidedId: true
1220
+ });
1221
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1222
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1223
+ expect(match).toBeTruthy();
1224
+ const relativeAssetPath = 'workspaces/workspace-local-active/assets/images/asset-binary-sync-1-synced-image.png';
1225
+ const assetBuffer = Buffer.from('binary-image-content');
1226
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1227
+ changes: [
1228
+ {
1229
+ op: 'asset-upsert',
1230
+ path: relativeAssetPath,
1231
+ updatedAt: '2026-03-17T12:05:00.000Z',
1232
+ contentBase64: assetBuffer.toString('base64'),
1233
+ assetId: 'asset-binary-sync-1',
1234
+ kind: 'image',
1235
+ mimeType: 'image/png',
1236
+ taskId: 'task-binary-asset-1',
1237
+ originalFilename: 'synced-image.png',
1238
+ linkRole: 'image'
1239
+ }
1240
+ ]
1241
+ }, { 'x-taskforce-workspace-id': workspaceId });
1242
+ const res = createMockRes();
1243
+ await match?.handler(req, res, {});
1244
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1245
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1246
+ const absoluteAssetPath = path.join(reloadedCore.getPaths().basePath, relativeAssetPath);
1247
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1248
+ const asset = store.get('asset-binary-sync-1', workspaceId);
1249
+ const links = store.listLinksForAsset('asset-binary-sync-1', workspaceId);
1250
+ expect(fs.readFileSync(absoluteAssetPath)).toEqual(assetBuffer);
1251
+ expect(asset).toEqual(expect.objectContaining({
1252
+ assetId: 'asset-binary-sync-1',
1253
+ workspaceId,
1254
+ kind: 'image',
1255
+ storageProvider: 'local',
1256
+ storageKey: relativeAssetPath,
1257
+ originalFilename: 'synced-image.png',
1258
+ mimeType: 'image/png',
1259
+ deletedAt: null
1260
+ }));
1261
+ expect(links).toEqual([
1262
+ expect.objectContaining({
1263
+ assetId: 'asset-binary-sync-1',
1264
+ workspaceId,
1265
+ taskId: 'task-binary-asset-1',
1266
+ role: 'image',
1267
+ deletedAt: null
1268
+ })
1269
+ ]);
1270
+ });
1271
+ it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
1272
+ const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
1273
+ const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-local-');
1274
+ const cloudCore = new TaskforceCore({ projectRoot: cloudProjectRoot, storagePath: '.taskforce' });
1275
+ seedAuthenticatedWorkspace(cloudCore);
1276
+ cloudCore.createTask({
1277
+ id: 'task-cloud-link-roundtrip-1',
1278
+ title: 'Cloud attachment source',
1279
+ status: 'task'
1280
+ }, {
1281
+ workspaceId,
1282
+ allowClientProvidedId: true
1283
+ });
1284
+ const cloudBasePath = cloudCore.getPaths().basePath;
1285
+ const cloudStore = new WorkspaceAssetStore(cloudCore.getDatabaseAdapter(), cloudCore.getTenantId());
1286
+ const markdownPath = `workspaces/${workspaceId}/assets/documents/asset-cloud-link-roundtrip-plan.md`;
1287
+ const filePath = `workspaces/${workspaceId}/assets/files/asset-cloud-link-roundtrip-notes.txt`;
1288
+ fs.mkdirSync(path.dirname(path.join(cloudBasePath, markdownPath)), { recursive: true });
1289
+ fs.mkdirSync(path.dirname(path.join(cloudBasePath, filePath)), { recursive: true });
1290
+ fs.writeFileSync(path.join(cloudBasePath, markdownPath), '# synced from cloud\n', 'utf8');
1291
+ fs.writeFileSync(path.join(cloudBasePath, filePath), 'cloud text attachment', 'utf8');
1292
+ cloudStore.upsertAsset({
1293
+ assetId: 'asset-cloud-link-roundtrip-doc-1',
1294
+ workspaceId,
1295
+ kind: 'document',
1296
+ originalFilename: 'plan.md',
1297
+ mimeType: 'text/markdown',
1298
+ storageProvider: 'local',
1299
+ storageKey: markdownPath,
1300
+ contentSha256: 'doc-roundtrip-hash',
1301
+ sizeBytes: '# synced from cloud\n'.length,
1302
+ documentId: 'doc-cloud-link-roundtrip-1',
1303
+ version: 1,
1304
+ isLatest: true,
1305
+ createdAt: '2026-03-18T09:00:00.000Z',
1306
+ updatedAt: '2026-03-18T09:00:00.000Z'
1307
+ });
1308
+ cloudStore.upsertLink({
1309
+ workspaceId,
1310
+ assetId: 'asset-cloud-link-roundtrip-doc-1',
1311
+ taskId: 'task-cloud-link-roundtrip-1',
1312
+ role: 'attachment',
1313
+ updatedAt: '2026-03-18T10:00:00.000Z'
1314
+ });
1315
+ cloudStore.upsertAsset({
1316
+ assetId: 'asset-cloud-link-roundtrip-file-1',
1317
+ workspaceId,
1318
+ kind: 'file',
1319
+ originalFilename: 'notes.txt',
1320
+ mimeType: 'text/plain',
1321
+ storageProvider: 'local',
1322
+ storageKey: filePath,
1323
+ contentSha256: 'file-roundtrip-hash',
1324
+ sizeBytes: 'cloud text attachment'.length,
1325
+ isLatest: true,
1326
+ createdAt: '2026-03-18T09:00:00.000Z',
1327
+ updatedAt: '2026-03-18T09:00:00.000Z'
1328
+ });
1329
+ cloudStore.upsertLink({
1330
+ workspaceId,
1331
+ assetId: 'asset-cloud-link-roundtrip-file-1',
1332
+ taskId: 'task-cloud-link-roundtrip-1',
1333
+ role: 'attachment',
1334
+ updatedAt: '2026-03-18T10:00:01.000Z'
1335
+ });
1336
+ const cloudRoutes = createRoutes(cloudCore, { authConfig });
1337
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', cloudRoutes);
1338
+ expect(pullMatch).toBeTruthy();
1339
+ const cookie = createAuthCookie(workspaceId);
1340
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=50&cursor=eyJ1IjoiMjAyNi0wMy0xOFQwOTozMDowMC4wMDBaIiwiaWQiOiJzZWVkIn0', undefined, { cookie });
1341
+ const pullRes = createMockRes();
1342
+ await pullMatch?.handler(pullReq, pullRes, {});
1343
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1344
+ const pullBody = readJsonBody(pullRes);
1345
+ const pulledChanges = Array.isArray(pullBody?.changes) ? pullBody.changes : [];
1346
+ expect(pulledChanges).toEqual(expect.arrayContaining([
1347
+ expect.objectContaining({
1348
+ op: 'document-upsert',
1349
+ path: markdownPath,
1350
+ assetId: 'asset-cloud-link-roundtrip-doc-1',
1351
+ taskId: 'task-cloud-link-roundtrip-1',
1352
+ originalFilename: 'plan.md',
1353
+ linkRole: 'attachment'
1354
+ }),
1355
+ expect.objectContaining({
1356
+ op: 'asset-upsert',
1357
+ path: filePath,
1358
+ assetId: 'asset-cloud-link-roundtrip-file-1',
1359
+ taskId: 'task-cloud-link-roundtrip-1',
1360
+ originalFilename: 'notes.txt',
1361
+ linkRole: 'attachment'
1362
+ })
1363
+ ]));
1364
+ const localCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1365
+ localCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1366
+ localCore.createTask({
1367
+ id: 'task-cloud-link-roundtrip-1',
1368
+ title: 'Local attachment target',
1369
+ status: 'task'
1370
+ }, {
1371
+ workspaceId,
1372
+ allowClientProvidedId: true
1373
+ });
1374
+ const localRoutes = createRoutes(localCore, { authConfig: { runtimeMode: 'local', enabled: false } });
1375
+ const applyMatch = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', localRoutes);
1376
+ expect(applyMatch).toBeTruthy();
1377
+ const applyReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: pulledChanges }, { 'x-taskforce-workspace-id': workspaceId });
1378
+ const applyRes = createMockRes();
1379
+ await applyMatch?.handler(applyReq, applyRes, {});
1380
+ expect(applyRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1381
+ const reloadedLocalCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1382
+ const reloadedStore = new WorkspaceAssetStore(reloadedLocalCore.getDatabaseAdapter(), reloadedLocalCore.getTenantId());
1383
+ expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, markdownPath), 'utf8')).toBe('# synced from cloud\n');
1384
+ expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, filePath), 'utf8')).toBe('cloud text attachment');
1385
+ expect(reloadedStore.listLinksForAsset('asset-cloud-link-roundtrip-doc-1', workspaceId)).toEqual([
1386
+ expect.objectContaining({
1387
+ taskId: 'task-cloud-link-roundtrip-1',
1388
+ role: 'attachment',
1389
+ deletedAt: null
1390
+ })
1391
+ ]);
1392
+ expect(reloadedStore.listLinksForAsset('asset-cloud-link-roundtrip-file-1', workspaceId)).toEqual([
1393
+ expect.objectContaining({
1394
+ taskId: 'task-cloud-link-roundtrip-1',
1395
+ role: 'attachment',
1396
+ deletedAt: null
1397
+ })
1398
+ ]);
1399
+ });
1400
+ it('propagates document-delete through authenticated push and omits deleted doc from pull payload', async () => {
1401
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-doc-delete-');
1402
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1403
+ seedAuthenticatedWorkspace(seedCore);
1404
+ const syncRoot = seedCore.getPaths().basePath;
1405
+ const docsDir = path.join(syncRoot, 'docs');
1406
+ fs.mkdirSync(docsDir, { recursive: true });
1407
+ const relativeDocPath = 'docs/sync-delete-push.md';
1408
+ const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1409
+ fs.writeFileSync(absoluteDocPath, '# from local\n', 'utf8');
1410
+ seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
1411
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1412
+ const routes = createRoutes(core, { authConfig });
1413
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1414
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1415
+ expect(pushMatch).toBeTruthy();
1416
+ expect(pullMatch).toBeTruthy();
1417
+ const cookie = createAuthCookie(workspaceId);
1418
+ const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1419
+ workspaceId,
1420
+ idempotencyKey: 'doc-delete-integration-1',
1421
+ changes: [
1422
+ { op: 'document-delete', path: relativeDocPath }
1423
+ ]
1424
+ }, { cookie });
1425
+ const pushRes = createMockRes();
1426
+ await pushMatch?.handler(pushReq, pushRes, {});
1427
+ expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1428
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1429
+ expect(fs.existsSync(absoluteDocPath)).toBe(false);
1430
+ expect(reloadedCore.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
1431
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1432
+ const pullRes = createMockRes();
1433
+ await pullMatch?.handler(pullReq, pullRes, {});
1434
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1435
+ const pullBody = readJsonBody(pullRes);
1436
+ const documentUpserts = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1437
+ .filter((change) => String(change?.op || '').toLowerCase() === 'document-upsert');
1438
+ expect(documentUpserts.find((change) => change.path === relativeDocPath)).toBeUndefined();
1439
+ });
1440
+ it('round-trips task comments through authenticated push and pull payloads', async () => {
1441
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-comments-');
1442
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1443
+ seedAuthenticatedWorkspace(seedCore);
1444
+ const created = seedCore.createTask({
1445
+ title: 'Comment sync task',
1446
+ status: 'task'
1447
+ }, { workspaceId });
1448
+ seedCore.addComment(created.id, 'Synced activity comment', 'user', workspaceId);
1449
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1450
+ const routes = createRoutes(core, { authConfig });
1451
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1452
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1453
+ expect(pushMatch).toBeTruthy();
1454
+ expect(pullMatch).toBeTruthy();
1455
+ const cookie = createAuthCookie(workspaceId);
1456
+ const currentTask = core.getTask(created.id, workspaceId);
1457
+ expect(currentTask?.comments).toHaveLength(1);
1458
+ expect(currentTask?.comments?.[0]?.submittedAuthor).toBe('user');
1459
+ const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1460
+ workspaceId,
1461
+ idempotencyKey: 'comment-roundtrip-1',
1462
+ changes: [
1463
+ {
1464
+ op: 'upsert',
1465
+ archived: false,
1466
+ task: currentTask
1467
+ }
1468
+ ]
1469
+ }, { cookie });
1470
+ const pushRes = createMockRes();
1471
+ await pushMatch?.handler(pushReq, pushRes, {});
1472
+ expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1473
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1474
+ const pullRes = createMockRes();
1475
+ await pullMatch?.handler(pullReq, pullRes, {});
1476
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1477
+ const pullBody = readJsonBody(pullRes);
1478
+ const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1479
+ .find((change) => change?.op === 'upsert' && change?.task?.id === created.id);
1480
+ expect(upsert?.task?.comments).toEqual([
1481
+ expect.objectContaining({
1482
+ id: expect.any(String),
1483
+ author: 'user',
1484
+ submittedAuthor: 'user',
1485
+ text: 'Synced activity comment',
1486
+ timestamp: expect.any(String)
1487
+ })
1488
+ ]);
1489
+ });
1490
+ it('keeps document deleted when push batch includes upsert and delete for same path', async () => {
1491
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-doc-delete-wins-');
1492
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1493
+ seedAuthenticatedWorkspace(seedCore);
1494
+ const syncRoot = seedCore.getPaths().basePath;
1495
+ const docsDir = path.join(syncRoot, 'docs');
1496
+ fs.mkdirSync(docsDir, { recursive: true });
1497
+ const relativeDocPath = 'docs/sync-delete-wins.md';
1498
+ const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1499
+ fs.writeFileSync(absoluteDocPath, '# old\n', 'utf8');
1500
+ seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, 'seed-hash', '2026-02-23T00:00:00.000Z');
1501
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1502
+ const routes = createRoutes(core, { authConfig });
1503
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1504
+ expect(pushMatch).toBeTruthy();
1505
+ const cookie = createAuthCookie(workspaceId);
1506
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1507
+ workspaceId,
1508
+ idempotencyKey: 'doc-delete-integration-2',
1509
+ changes: [
1510
+ {
1511
+ op: 'document-upsert',
1512
+ path: relativeDocPath,
1513
+ updatedAt: '2026-02-23T10:00:00.000Z',
1514
+ content: '# stale upsert'
1515
+ },
1516
+ {
1517
+ op: 'document-delete',
1518
+ path: relativeDocPath
1519
+ }
1520
+ ]
1521
+ }, { cookie });
1522
+ const res = createMockRes();
1523
+ await pushMatch?.handler(req, res, {});
1524
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1525
+ expect(fs.existsSync(absoluteDocPath)).toBe(false);
1526
+ expect(core.getDocumentWatermark(workspaceId, relativeDocPath)).toBeNull();
1527
+ });
1528
+ it('propagates binary asset deletes through authenticated push and persists canonical tombstones', async () => {
1529
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-asset-delete-');
1530
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1531
+ seedAuthenticatedWorkspace(seedCore);
1532
+ seedCore.createTask({
1533
+ id: 'task-asset-delete-1',
1534
+ title: 'Asset delete task',
1535
+ status: 'task'
1536
+ }, {
1537
+ workspaceId,
1538
+ allowClientProvidedId: true
1539
+ });
1540
+ const relativeAssetPath = 'workspaces/workspace-local-active/assets/files/asset-delete-auth-1-delete-me.bin';
1541
+ const absoluteAssetPath = path.join(seedCore.getPaths().basePath, relativeAssetPath);
1542
+ fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
1543
+ fs.writeFileSync(absoluteAssetPath, Buffer.from('delete-me'));
1544
+ const seededStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
1545
+ seededStore.upsertAsset({
1546
+ assetId: 'asset-delete-auth-1',
1547
+ workspaceId,
1548
+ kind: 'file',
1549
+ originalFilename: 'delete-me.bin',
1550
+ mimeType: 'application/octet-stream',
1551
+ storageProvider: 'local',
1552
+ storageKey: relativeAssetPath,
1553
+ contentSha256: 'seed-hash',
1554
+ sizeBytes: 9,
1555
+ createdAt: '2026-03-17T11:00:00.000Z',
1556
+ updatedAt: '2026-03-17T11:00:00.000Z'
1557
+ });
1558
+ seededStore.upsertLink({
1559
+ workspaceId,
1560
+ assetId: 'asset-delete-auth-1',
1561
+ taskId: 'task-asset-delete-1',
1562
+ role: 'attachment',
1563
+ updatedAt: '2026-03-17T11:00:00.000Z'
1564
+ });
1565
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1566
+ const routes = createRoutes(core, { authConfig });
1567
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1568
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1569
+ expect(pushMatch).toBeTruthy();
1570
+ expect(pullMatch).toBeTruthy();
1571
+ const cookie = createAuthCookie(workspaceId);
1572
+ const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1573
+ workspaceId,
1574
+ idempotencyKey: 'asset-delete-integration-1',
1575
+ changes: [
1576
+ {
1577
+ op: 'asset-delete',
1578
+ path: relativeAssetPath,
1579
+ assetId: 'asset-delete-auth-1'
1580
+ }
1581
+ ]
1582
+ }, { cookie });
1583
+ const pushRes = createMockRes();
1584
+ await pushMatch?.handler(pushReq, pushRes, {});
1585
+ expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1586
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1587
+ const reloadedStore = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1588
+ const deletedAsset = reloadedStore.get('asset-delete-auth-1', workspaceId);
1589
+ const deletedLinks = reloadedStore.listLinksForAsset('asset-delete-auth-1', workspaceId, { includeDeleted: true });
1590
+ expect(fs.existsSync(absoluteAssetPath)).toBe(false);
1591
+ expect(deletedAsset?.deletedAt).toEqual(expect.any(String));
1592
+ expect(deletedLinks).toEqual([
1593
+ expect.objectContaining({
1594
+ assetId: 'asset-delete-auth-1',
1595
+ workspaceId,
1596
+ taskId: 'task-asset-delete-1',
1597
+ role: 'attachment',
1598
+ deletedAt: expect.any(String)
1599
+ })
1600
+ ]);
1601
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1602
+ const pullRes = createMockRes();
1603
+ await pullMatch?.handler(pullReq, pullRes, {});
1604
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1605
+ const pullBody = readJsonBody(pullRes);
1606
+ const assetUpserts = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1607
+ .filter((change) => String(change?.op || '').toLowerCase() === 'asset-upsert');
1608
+ const assetDeletes = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1609
+ .filter((change) => String(change?.op || '').toLowerCase() === 'asset-delete');
1610
+ expect(assetUpserts.find((change) => change.path === relativeAssetPath)).toBeUndefined();
1611
+ expect(assetDeletes).toContainEqual(expect.objectContaining({
1612
+ op: 'asset-delete',
1613
+ path: relativeAssetPath,
1614
+ assetId: 'asset-delete-auth-1'
1615
+ }));
1616
+ });
1617
+ it('includes unattached annotated attachment sessions in the authenticated sync snapshot', async () => {
1618
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-annotated-snapshot-');
1619
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1620
+ seedAuthenticatedWorkspace(seedCore);
1621
+ const seededStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
1622
+ seededStore.upsertAsset({
1623
+ assetId: 'asset-image-unattached-1',
1624
+ workspaceId,
1625
+ kind: 'image',
1626
+ originalFilename: 'unattached.png',
1627
+ mimeType: 'image/png',
1628
+ storageProvider: 'local',
1629
+ storageKey: 'workspaces/workspace-local-active/assets/images/asset-image-unattached-1-unattached.png',
1630
+ contentSha256: 'seed-hash-unattached',
1631
+ sizeBytes: 42,
1632
+ createdAt: '2026-03-30T20:00:00.000Z',
1633
+ updatedAt: '2026-03-30T20:00:00.000Z'
1634
+ });
1635
+ seedCore.createAnnotatedAttachmentSession({
1636
+ workspaceId,
1637
+ baseImageAssetId: 'asset-image-unattached-1',
1638
+ title: 'Loose image notes',
1639
+ annotations: [
1640
+ { id: 'annotation-1', kind: 'pin', order: 0, instruction: 'Check this', x: 0.2, y: 0.3 },
1641
+ ],
1642
+ });
1643
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1644
+ const routes = createRoutes(core, { authConfig });
1645
+ const snapshotMatch = matchRoute('GET', '/api/taskforce/sync/workspace/annotated-attachments', routes);
1646
+ expect(snapshotMatch).toBeTruthy();
1647
+ const cookie = createAuthCookie(workspaceId);
1648
+ const req = createMockReq('GET', '/api/taskforce/sync/workspace/annotated-attachments?workspaceId=workspace-local-active', undefined, { cookie });
1649
+ const res = createMockRes();
1650
+ await snapshotMatch?.handler(req, res, {});
1651
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1652
+ expect(readJsonBody(res).sessions).toEqual(expect.arrayContaining([
1653
+ expect.objectContaining({
1654
+ baseImageAssetId: 'asset-image-unattached-1',
1655
+ taskId: '',
1656
+ title: 'Loose image notes'
1657
+ })
1658
+ ]));
1659
+ });
1660
+ it('returns 401 on apply-local when auth is enabled but absent', async () => {
1661
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-auth-');
1662
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1663
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1664
+ // Enable auth this time as we are testing it
1665
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: true } });
1666
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1667
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: [] }, { 'x-taskforce-workspace-id': workspaceId }
1668
+ // No auth cookie or session
1669
+ );
1670
+ const res = createMockRes();
1671
+ await match?.handler(req, res, {});
1672
+ expect(res.writeHead).toHaveBeenCalledWith(401, expect.any(Object));
1673
+ });
1674
+ it('round-trips sync event details through the sync events API', async () => {
1675
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-events-details-');
1676
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1677
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1678
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1679
+ const postMatch = matchRoute('POST', '/api/taskforce/sync/events', routes);
1680
+ expect(postMatch).toBeTruthy();
1681
+ const postReq = createMockReq('POST', '/api/taskforce/sync/events', {
1682
+ workspaceId,
1683
+ eventType: 'apply',
1684
+ status: 'error',
1685
+ errorMessage: 'Detected asset reference number mismatch during normal sync.',
1686
+ details: {
1687
+ path: 'workspaces/workspace-local-active/assets/images/hero.png',
1688
+ existingReferenceNumber: 12,
1689
+ incomingReferenceNumber: 18
1690
+ }
1691
+ });
1692
+ const postRes = createMockRes();
1693
+ await postMatch?.handler(postReq, postRes, {});
1694
+ expect(postRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1695
+ const getMatch = matchRoute('GET', '/api/taskforce/sync/events', routes);
1696
+ expect(getMatch).toBeTruthy();
1697
+ const getReq = createMockReq('GET', `/api/taskforce/sync/events?workspace_id=${workspaceId}&limit=5`);
1698
+ const getRes = createMockRes();
1699
+ await getMatch?.handler(getReq, getRes, {});
1700
+ expect(getRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1701
+ const payload = JSON.parse(String(getRes.end.mock.calls[0]?.[0] || '{}'));
1702
+ expect(payload.events).toEqual(expect.arrayContaining([
1703
+ expect.objectContaining({
1704
+ eventType: 'apply',
1705
+ status: 'error',
1706
+ errorMessage: 'Detected asset reference number mismatch during normal sync.',
1707
+ details: expect.objectContaining({
1708
+ path: 'workspaces/workspace-local-active/assets/images/hero.png',
1709
+ existingReferenceNumber: 12,
1710
+ incomingReferenceNumber: 18
1711
+ })
1712
+ })
1713
+ ]));
1714
+ });
1715
+ it('returns 400 on apply-local when workspaceId is null/empty/default', async () => {
1716
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-wsmissing-');
1717
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1718
+ // Use false for auth to isolate workspaceId testing
1719
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1720
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1721
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: [] }
1722
+ // Omit custom headers, and the body does not have workspaceId
1723
+ );
1724
+ const res = createMockRes();
1725
+ await match?.handler(req, res, {});
1726
+ expect(res.writeHead).toHaveBeenCalledWith(400, expect.any(Object));
1727
+ });
1728
+ it('keeps repeated document scans correct and invalidates cache when file content changes', async () => {
1729
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-cache-');
1730
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1731
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1732
+ const syncRoot = core.getPaths().basePath;
1733
+ const docsDir = path.join(syncRoot, 'docs');
1734
+ fs.mkdirSync(docsDir, { recursive: true });
1735
+ const relativeDocPath = 'docs/cache-target.md';
1736
+ const absoluteDocPath = path.join(syncRoot, relativeDocPath);
1737
+ fs.writeFileSync(absoluteDocPath, '# cache test\n', 'utf8');
1738
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1739
+ const match = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
1740
+ expect(match).toBeTruthy();
1741
+ const req1 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
1742
+ const res1 = createMockRes();
1743
+ await match?.handler(req1, res1, {});
1744
+ expect(res1.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1745
+ const body1 = JSON.parse(String(res1.end.mock.calls.at(-1)?.[0] || '{}'));
1746
+ const req2 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
1747
+ const res2 = createMockRes();
1748
+ await match?.handler(req2, res2, {});
1749
+ expect(res2.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1750
+ const body2 = JSON.parse(String(res2.end.mock.calls.at(-1)?.[0] || '{}'));
1751
+ const firstDoc1 = (Array.isArray(body1?.documents) ? body1.documents : []).find((doc) => doc.path === relativeDocPath);
1752
+ const firstDoc2 = (Array.isArray(body2?.documents) ? body2.documents : []).find((doc) => doc.path === relativeDocPath);
1753
+ expect(firstDoc1?.content).toBe('# cache test\n');
1754
+ expect(firstDoc2?.content).toBe('# cache test\n');
1755
+ expect(firstDoc2?.updatedAt).toBe(firstDoc1?.updatedAt);
1756
+ fs.writeFileSync(absoluteDocPath, '# cache test changed\n', 'utf8');
1757
+ const req3 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
1758
+ const res3 = createMockRes();
1759
+ await match?.handler(req3, res3, {});
1760
+ expect(res3.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1761
+ const body3 = JSON.parse(String(res3.end.mock.calls.at(-1)?.[0] || '{}'));
1762
+ const firstDoc3 = (Array.isArray(body3?.documents) ? body3.documents : []).find((doc) => doc.path === relativeDocPath);
1763
+ expect(firstDoc3?.content).toBe('# cache test changed\n');
1764
+ });
1765
+ it('includes locally uploaded canonical markdown attachments in the workspace document snapshot', async () => {
1766
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-canonical-doc-');
1767
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1768
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1769
+ const task = core.createTask({ title: 'Task with markdown attachment' }, { workspaceId });
1770
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1771
+ const uploadMatch = matchRoute('POST', '/api/taskforce/context-upload', routes);
1772
+ expect(uploadMatch).toBeTruthy();
1773
+ const uploadReq = createMockReq('POST', '/api/taskforce/context-upload', {
1774
+ file: `data:text/markdown;base64,${Buffer.from('# local attachment\n', 'utf8').toString('base64')}`,
1775
+ originalName: 'local-notes.md',
1776
+ taskId: task.id
1777
+ }, { 'x-taskforce-workspace-id': workspaceId });
1778
+ const uploadRes = createMockRes();
1779
+ await uploadMatch?.handler(uploadReq, uploadRes, {});
1780
+ expect(uploadRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
1781
+ const uploadBody = JSON.parse(String(uploadRes.end.mock.calls.at(-1)?.[0] || '{}'));
1782
+ expect(String(uploadBody?.fsPath || '')).toContain(`workspaces/${workspaceId}/assets/documents/`);
1783
+ const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
1784
+ expect(documentsMatch).toBeTruthy();
1785
+ const req = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
1786
+ const res = createMockRes();
1787
+ await documentsMatch?.handler(req, res, {});
1788
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1789
+ const body = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
1790
+ const relativeDocPath = String(uploadBody?.fsPath || '');
1791
+ const doc = (Array.isArray(body?.documents) ? body.documents : []).find((entry) => entry.path === relativeDocPath);
1792
+ expect(doc).toBeTruthy();
1793
+ expect(doc.content).toBe('# local attachment\n');
1794
+ });
1795
+ it('converges task conflicts deterministically for equal timestamps regardless of apply order', async () => {
1796
+ const applySequence = async (prefix, titlesInOrder) => {
1797
+ const projectRoot = createIsolatedProjectRoot(prefix);
1798
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1799
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1800
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1801
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1802
+ expect(match).toBeTruthy();
1803
+ for (const title of titlesInOrder) {
1804
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1805
+ changes: [{
1806
+ op: 'upsert',
1807
+ archived: false,
1808
+ task: {
1809
+ id: 'task-concurrent-1',
1810
+ title,
1811
+ status: 'task',
1812
+ createdAt: '2026-02-27T00:00:00.000Z',
1813
+ updatedAt: '2026-02-27T00:00:00.000Z'
1814
+ }
1815
+ }]
1816
+ }, { 'x-taskforce-workspace-id': workspaceId });
1817
+ const res = createMockRes();
1818
+ await match?.handler(req, res, {});
1819
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1820
+ }
1821
+ return core.getTask('task-concurrent-1', workspaceId)?.title || null;
1822
+ };
1823
+ const orderAB = await applySequence('taskforce-sync-conflict-task-ab-', ['alpha-title', 'zeta-title']);
1824
+ const orderBA = await applySequence('taskforce-sync-conflict-task-ba-', ['zeta-title', 'alpha-title']);
1825
+ expect(orderAB).toBeTruthy();
1826
+ expect(orderBA).toBe(orderAB);
1827
+ });
1828
+ it('converges document metadata conflicts deterministically for equal timestamps regardless of apply order', async () => {
1829
+ const applySequence = async (prefix, contentsInOrder) => {
1830
+ const projectRoot = createIsolatedProjectRoot(prefix);
1831
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1832
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1833
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1834
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1835
+ expect(match).toBeTruthy();
1836
+ const relativeDocPath = 'docs/conflict-doc.md';
1837
+ const absoluteDocPath = path.join(core.getPaths().basePath, relativeDocPath);
1838
+ for (const content of contentsInOrder) {
1839
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1840
+ changes: [{
1841
+ op: 'document-upsert',
1842
+ path: relativeDocPath,
1843
+ updatedAt: '2026-02-27T00:00:00.000Z',
1844
+ content
1845
+ }]
1846
+ }, { 'x-taskforce-workspace-id': workspaceId });
1847
+ const res = createMockRes();
1848
+ await match?.handler(req, res, {});
1849
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1850
+ }
1851
+ return fs.existsSync(absoluteDocPath) ? fs.readFileSync(absoluteDocPath, 'utf8') : '';
1852
+ };
1853
+ const orderAB = await applySequence('taskforce-sync-conflict-doc-ab-', ['doc-content-alpha', 'doc-content-zeta']);
1854
+ const orderBA = await applySequence('taskforce-sync-conflict-doc-ba-', ['doc-content-zeta', 'doc-content-alpha']);
1855
+ expect(orderAB).toBeTruthy();
1856
+ expect(orderBA).toBe(orderAB);
1857
+ });
1858
+ it('returns 200 with skipped count when updateTask throws during apply-local (regression for Bug 1)', async () => {
1859
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-updatethrow-');
1860
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1861
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1862
+ // Create the task first so the apply-local path takes the updateTask branch
1863
+ core.createTask({
1864
+ id: 'task-update-throws',
1865
+ title: 'Original title',
1866
+ status: 'task'
1867
+ }, {
1868
+ workspaceId,
1869
+ allowClientProvidedId: true
1870
+ });
1871
+ // Stub updateTask to throw on this specific task id
1872
+ const originalUpdateTask = core.updateTask.bind(core);
1873
+ vi.spyOn(core, 'updateTask').mockImplementationOnce((...args) => {
1874
+ if (args[0] === 'task-update-throws') {
1875
+ throw new Error('Rule violation: cannot update task');
1876
+ }
1877
+ return originalUpdateTask(...args);
1878
+ });
1879
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1880
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1881
+ expect(match).toBeTruthy();
1882
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1883
+ changes: [
1884
+ {
1885
+ op: 'upsert',
1886
+ archived: false,
1887
+ task: {
1888
+ id: 'task-update-throws',
1889
+ title: 'Updated title from cloud',
1890
+ status: 'in-progress'
1891
+ }
1892
+ }
1893
+ ]
1894
+ }, { 'x-taskforce-workspace-id': workspaceId });
1895
+ const res = createMockRes();
1896
+ await match?.handler(req, res, {});
1897
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1898
+ const body = readJsonBody(res);
1899
+ expect(body.result.skipped).toBeGreaterThanOrEqual(1);
1900
+ });
1901
+ it('preserves seeded first-class taxonomy state when apply-local receives a partial taxonomy payload', async () => {
1902
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-taxonomy-partial-');
1903
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1904
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1905
+ core.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
1906
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1907
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1908
+ expect(match).toBeTruthy();
1909
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1910
+ changes: [
1911
+ {
1912
+ op: 'taxonomy-upsert',
1913
+ updatedAt: '2026-03-30T17:05:00.000Z',
1914
+ taxonomyState: {
1915
+ categories: [{ value: 'default', label: 'General', icon: 'Inbox' }],
1916
+ priorities: [
1917
+ { value: 1, label: 'Low', color: 'teal-500', icon: 'ArrowDown' },
1918
+ { value: 2, label: 'Medium', color: 'blue-500', icon: 'Minus' },
1919
+ { value: 3, label: 'High', color: 'amber-500', icon: 'ArrowUp' }
1920
+ ],
1921
+ taxonomies: []
1922
+ }
1923
+ }
1924
+ ]
1925
+ }, { 'x-taskforce-workspace-id': workspaceId });
1926
+ const res = createMockRes();
1927
+ await match?.handler(req, res, {});
1928
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1929
+ const state = core.getTaxonomyState(workspaceId);
1930
+ expect((state.types || []).map((type) => type.value)).toEqual([
1931
+ 'feature',
1932
+ 'bug',
1933
+ 'chore',
1934
+ 'refactor',
1935
+ 'documentation'
1936
+ ]);
1937
+ expect((state.priorities || []).map((priority) => priority.value)).toEqual([1, 2, 3, 4]);
1938
+ });
1939
+ it('preserves seeded first-class taxonomy state when bootstrap-push receives a partial taxonomy payload', async () => {
1940
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-bootstrap-taxonomy-partial-');
1941
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1942
+ seedAuthenticatedWorkspace(core);
1943
+ core.updateTaxonomyState(buildSoftwareTaxonomyState(), workspaceId);
1944
+ const routes = createRoutes(core, { authConfig });
1945
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/bootstrap-push', routes);
1946
+ expect(match).toBeTruthy();
1947
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/bootstrap-push', {
1948
+ workspaceId,
1949
+ tasks: [],
1950
+ archived: [],
1951
+ taxonomyState: {
1952
+ categories: [{ value: 'default', label: 'General', icon: 'Inbox' }],
1953
+ priorities: [
1954
+ { value: 1, label: 'Low', color: 'teal-500', icon: 'ArrowDown' },
1955
+ { value: 2, label: 'Medium', color: 'blue-500', icon: 'Minus' },
1956
+ { value: 3, label: 'High', color: 'amber-500', icon: 'ArrowUp' }
1957
+ ],
1958
+ taxonomies: []
1959
+ }
1960
+ }, { cookie: createAuthCookie(workspaceId) });
1961
+ const res = createMockRes();
1962
+ await match?.handler(req, res, {});
1963
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1964
+ const state = core.getTaxonomyState(workspaceId);
1965
+ expect((state.types || []).map((type) => type.value)).toEqual([
1966
+ 'feature',
1967
+ 'bug',
1968
+ 'chore',
1969
+ 'refactor',
1970
+ 'documentation'
1971
+ ]);
1972
+ expect((state.priorities || []).map((priority) => priority.value)).toEqual([1, 2, 3, 4]);
1973
+ });
1974
+ it('converges taxonomy conflicts deterministically for equal timestamps regardless of apply order', async () => {
1975
+ const applySequence = async (prefix, taxonomiesInOrder) => {
1976
+ const projectRoot = createIsolatedProjectRoot(prefix);
1977
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1978
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1979
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1980
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1981
+ expect(match).toBeTruthy();
1982
+ for (const taxonomies of taxonomiesInOrder) {
1983
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1984
+ changes: [{
1985
+ op: 'taxonomy-upsert',
1986
+ updatedAt: '2026-02-27T00:00:00.000Z',
1987
+ taxonomies
1988
+ }]
1989
+ }, { 'x-taskforce-workspace-id': workspaceId });
1990
+ const res = createMockRes();
1991
+ await match?.handler(req, res, {});
1992
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1993
+ }
1994
+ return core.getTaxonomies();
1995
+ };
1996
+ const alphaTaxonomies = [{ id: 'custom', label: 'Alpha', options: [], widgetType: 'select', isSystem: false, multiSelect: false }];
1997
+ const zetaTaxonomies = [{ id: 'custom', label: 'Zeta', options: [], widgetType: 'select', isSystem: false, multiSelect: false }];
1998
+ const orderAB = await applySequence('taskforce-sync-conflict-tax-ab-', [alphaTaxonomies, zetaTaxonomies]);
1999
+ const orderBA = await applySequence('taskforce-sync-conflict-tax-ba-', [zetaTaxonomies, alphaTaxonomies]);
2000
+ expect(Array.isArray(orderAB)).toBe(true);
2001
+ expect(orderBA).toEqual(orderAB);
2002
+ });
2003
+ });