@taskforcehq/taskforce 0.3.305 → 0.3.307

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 (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -3,6 +3,7 @@ import { Readable } from 'stream';
3
3
  import * as fs from 'fs';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
+ import { createHash } from 'crypto';
6
7
  import { TaskforceCore } from '../../core/Taskforce';
7
8
  import { createRoutes, matchRoute } from '../routes';
8
9
  import { createSessionTokenCookie } from '../auth';
@@ -110,6 +111,7 @@ describe('Server Routes Workspace Sync Integration', () => {
110
111
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-');
111
112
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
112
113
  core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
114
+ const basePath = core.getPaths().basePath;
113
115
  const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
114
116
  const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
115
117
  expect(match).toBeTruthy();
@@ -344,6 +346,8 @@ describe('Server Routes Workspace Sync Integration', () => {
344
346
  username: 'codex',
345
347
  icon: 'Bot',
346
348
  color: '#0ea5e9',
349
+ avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
350
+ avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp',
347
351
  description: 'Terminal-first coding agent',
348
352
  role: 'Implementer',
349
353
  provider: 'OpenAI',
@@ -369,7 +373,9 @@ describe('Server Routes Workspace Sync Integration', () => {
369
373
  role: 'Implementer',
370
374
  provider: 'OpenAI',
371
375
  model: 'gpt-5.4',
372
- surfaceType: 'coding_tool'
376
+ surfaceType: 'coding_tool',
377
+ avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Favatar.webp',
378
+ avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-sync-ai%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-codex%2Favatar%2Fsource.webp'
373
379
  })
374
380
  ]);
375
381
  });
@@ -1319,7 +1325,7 @@ describe('Server Routes Workspace Sync Integration', () => {
1319
1325
  })
1320
1326
  ]);
1321
1327
  });
1322
- it('lets the cloud document ref win during normal sync apply and records a diagnostic', async () => {
1328
+ it('lets the cloud document ref win during normal sync apply without recording a sync error', async () => {
1323
1329
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
1324
1330
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1325
1331
  core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
@@ -1381,26 +1387,17 @@ describe('Server Routes Workspace Sync Integration', () => {
1381
1387
  const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1382
1388
  const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1383
1389
  const asset = store.get('asset-doc-ref-drift-1', workspaceId);
1384
- const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1390
+ const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1391
+ && event.status === 'error'
1392
+ && event.errorMessage === 'Detected document reference number mismatch during normal sync.');
1385
1393
  expect(asset).toEqual(expect.objectContaining({
1386
1394
  assetId: 'asset-doc-ref-drift-1',
1387
1395
  referenceNumber: 18,
1388
1396
  version: 2
1389
1397
  }));
1390
- expect(syncEvents).toEqual(expect.arrayContaining([
1391
- expect.objectContaining({
1392
- eventType: 'apply',
1393
- status: 'error',
1394
- errorMessage: 'Detected document reference number mismatch during normal sync.',
1395
- details: expect.objectContaining({
1396
- assetId: 'asset-doc-ref-drift-1',
1397
- existingReferenceNumber: 12,
1398
- incomingReferenceNumber: 18
1399
- })
1400
- })
1401
- ]));
1398
+ expect(mismatchEvents).toHaveLength(0);
1402
1399
  });
1403
- it('lets the cloud image ref win during normal sync apply and records a diagnostic', async () => {
1400
+ it('lets the cloud image ref win during normal sync apply without recording a sync error', async () => {
1404
1401
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
1405
1402
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1406
1403
  core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
@@ -1461,23 +1458,94 @@ describe('Server Routes Workspace Sync Integration', () => {
1461
1458
  const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1462
1459
  const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1463
1460
  const asset = store.get('asset-image-ref-drift-1', workspaceId);
1464
- const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1461
+ const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1462
+ && event.status === 'error'
1463
+ && event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
1465
1464
  expect(asset).toEqual(expect.objectContaining({
1466
1465
  assetId: 'asset-image-ref-drift-1',
1467
1466
  referenceNumber: 18
1468
1467
  }));
1469
- expect(syncEvents).toEqual(expect.arrayContaining([
1470
- expect.objectContaining({
1471
- eventType: 'apply',
1472
- status: 'error',
1473
- errorMessage: 'Detected asset reference number mismatch during normal sync.',
1474
- details: expect.objectContaining({
1475
- assetId: 'asset-image-ref-drift-1',
1476
- existingReferenceNumber: 12,
1477
- incomingReferenceNumber: 18
1478
- })
1479
- })
1480
- ]));
1468
+ expect(mismatchEvents).toHaveLength(0);
1469
+ });
1470
+ it('converges unchanged image reference drift during normal sync so later pulls stop re-logging it', async () => {
1471
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-converge-');
1472
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1473
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1474
+ core.createTask({
1475
+ id: 'task-image-ref-converge-1',
1476
+ title: 'Task with converging synced image asset',
1477
+ status: 'task'
1478
+ }, {
1479
+ workspaceId,
1480
+ allowClientProvidedId: true
1481
+ });
1482
+ const basePath = core.getPaths().basePath;
1483
+ const relativeAssetPath = 'workspaces/workspace-local-active/assets/images/hero-converge.png';
1484
+ const absoluteAssetPath = path.join(basePath, relativeAssetPath);
1485
+ fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
1486
+ fs.writeFileSync(absoluteAssetPath, 'cloud-hero');
1487
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1488
+ seededStore.upsertAsset({
1489
+ assetId: 'asset-image-ref-converge-1',
1490
+ workspaceId,
1491
+ kind: 'image',
1492
+ originalFilename: 'hero-converge.png',
1493
+ mimeType: 'image/png',
1494
+ storageProvider: 'local',
1495
+ storageKey: relativeAssetPath,
1496
+ contentSha256: createHash('sha256').update('cloud-hero').digest('hex'),
1497
+ sizeBytes: Buffer.byteLength('cloud-hero'),
1498
+ referenceNumber: 12,
1499
+ createdAt: '2026-03-17T11:55:00.000Z',
1500
+ updatedAt: '2026-03-17T12:05:00.000Z'
1501
+ });
1502
+ seededStore.upsertLink({
1503
+ workspaceId,
1504
+ assetId: 'asset-image-ref-converge-1',
1505
+ taskId: 'task-image-ref-converge-1',
1506
+ role: 'image',
1507
+ updatedAt: '2026-03-17T11:55:00.000Z',
1508
+ deletedAt: null
1509
+ });
1510
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1511
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1512
+ expect(match).toBeTruthy();
1513
+ const body = {
1514
+ changes: [
1515
+ {
1516
+ op: 'asset-upsert',
1517
+ path: relativeAssetPath,
1518
+ updatedAt: '2026-03-17T12:05:00.000Z',
1519
+ contentBase64: Buffer.from('cloud-hero').toString('base64'),
1520
+ assetId: 'asset-image-ref-converge-1',
1521
+ kind: 'image',
1522
+ mimeType: 'image/png',
1523
+ referenceNumber: 18,
1524
+ taskId: 'task-image-ref-converge-1',
1525
+ originalFilename: 'hero-converge.png',
1526
+ linkRole: 'image'
1527
+ }
1528
+ ]
1529
+ };
1530
+ const firstReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
1531
+ const firstRes = createMockRes();
1532
+ await match?.handler(firstReq, firstRes, {});
1533
+ expect(firstRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1534
+ const secondReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', body, { 'x-taskforce-workspace-id': workspaceId });
1535
+ const secondRes = createMockRes();
1536
+ await match?.handler(secondReq, secondRes, {});
1537
+ expect(secondRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1538
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1539
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1540
+ const asset = store.get('asset-image-ref-converge-1', workspaceId);
1541
+ const mismatchEvents = reloadedCore.listSyncEvents(workspaceId, 20).filter((event) => event.eventType === 'apply'
1542
+ && event.status === 'error'
1543
+ && event.errorMessage === 'Detected asset reference number mismatch during normal sync.');
1544
+ expect(asset).toEqual(expect.objectContaining({
1545
+ assetId: 'asset-image-ref-converge-1',
1546
+ referenceNumber: 18
1547
+ }));
1548
+ expect(mismatchEvents).toHaveLength(0);
1481
1549
  });
1482
1550
  it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
1483
1551
  const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
@@ -1608,6 +1676,227 @@ describe('Server Routes Workspace Sync Integration', () => {
1608
1676
  })
1609
1677
  ]);
1610
1678
  });
1679
+ it('round-trips planning attachment links through pull and apply-local routes', async () => {
1680
+ const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-cloud-');
1681
+ const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-planning-link-local-');
1682
+ const cloudCore = new TaskforceCore({ projectRoot: cloudProjectRoot, storagePath: '.taskforce' });
1683
+ seedAuthenticatedWorkspace(cloudCore);
1684
+ cloudCore.createInitiative({
1685
+ id: 'initiative-cloud-link-roundtrip-1',
1686
+ title: 'Cloud initiative attachment target',
1687
+ createdAt: '2026-03-18T09:00:00.000Z',
1688
+ updatedAt: '2026-03-18T09:00:00.000Z'
1689
+ }, workspaceId);
1690
+ cloudCore.createWorkstream({
1691
+ id: 'workstream-cloud-link-roundtrip-1',
1692
+ initiativeId: 'initiative-cloud-link-roundtrip-1',
1693
+ title: 'Cloud workstream attachment target',
1694
+ createdAt: '2026-03-18T09:00:00.000Z',
1695
+ updatedAt: '2026-03-18T09:00:00.000Z'
1696
+ }, workspaceId);
1697
+ const cloudBasePath = cloudCore.getPaths().basePath;
1698
+ const cloudStore = new WorkspaceAssetStore(cloudCore.getDatabaseAdapter(), cloudCore.getTenantId());
1699
+ const markdownPath = `workspaces/${workspaceId}/assets/documents/asset-cloud-planning-link-plan.md`;
1700
+ const filePath = `workspaces/${workspaceId}/assets/files/asset-cloud-planning-link-notes.txt`;
1701
+ fs.mkdirSync(path.dirname(path.join(cloudBasePath, markdownPath)), { recursive: true });
1702
+ fs.mkdirSync(path.dirname(path.join(cloudBasePath, filePath)), { recursive: true });
1703
+ fs.writeFileSync(path.join(cloudBasePath, markdownPath), '# planning sync\n', 'utf8');
1704
+ fs.writeFileSync(path.join(cloudBasePath, filePath), 'planning attachment', 'utf8');
1705
+ cloudStore.upsertAsset({
1706
+ assetId: 'asset-cloud-planning-link-doc-1',
1707
+ workspaceId,
1708
+ kind: 'document',
1709
+ originalFilename: 'plan.md',
1710
+ mimeType: 'text/markdown',
1711
+ storageProvider: 'local',
1712
+ storageKey: markdownPath,
1713
+ contentSha256: 'planning-doc-roundtrip-hash',
1714
+ sizeBytes: '# planning sync\n'.length,
1715
+ documentId: 'doc-cloud-planning-link-1',
1716
+ version: 1,
1717
+ isLatest: true,
1718
+ createdAt: '2026-03-18T09:00:00.000Z',
1719
+ updatedAt: '2026-03-18T09:00:00.000Z'
1720
+ });
1721
+ cloudStore.upsertLink({
1722
+ workspaceId,
1723
+ assetId: 'asset-cloud-planning-link-doc-1',
1724
+ targetType: 'initiative',
1725
+ targetId: 'initiative-cloud-link-roundtrip-1',
1726
+ role: 'attachment',
1727
+ updatedAt: '2026-03-18T10:00:00.000Z'
1728
+ });
1729
+ cloudStore.upsertAsset({
1730
+ assetId: 'asset-cloud-planning-link-file-1',
1731
+ workspaceId,
1732
+ kind: 'file',
1733
+ originalFilename: 'notes.txt',
1734
+ mimeType: 'text/plain',
1735
+ storageProvider: 'local',
1736
+ storageKey: filePath,
1737
+ contentSha256: 'planning-file-roundtrip-hash',
1738
+ sizeBytes: 'planning attachment'.length,
1739
+ isLatest: true,
1740
+ createdAt: '2026-03-18T09:00:00.000Z',
1741
+ updatedAt: '2026-03-18T09:00:00.000Z'
1742
+ });
1743
+ cloudStore.upsertLink({
1744
+ workspaceId,
1745
+ assetId: 'asset-cloud-planning-link-file-1',
1746
+ targetType: 'workstream',
1747
+ targetId: 'workstream-cloud-link-roundtrip-1',
1748
+ role: 'attachment',
1749
+ updatedAt: '2026-03-18T10:00:01.000Z'
1750
+ });
1751
+ const cloudRoutes = createRoutes(cloudCore, { authConfig });
1752
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', cloudRoutes);
1753
+ expect(pullMatch).toBeTruthy();
1754
+ const cookie = createAuthCookie(workspaceId);
1755
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=50&cursor=eyJ1IjoiMjAyNi0wMy0xOFQwOTozMDowMC4wMDBaIiwiaWQiOiJzZWVkIn0', undefined, { cookie });
1756
+ const pullRes = createMockRes();
1757
+ await pullMatch?.handler(pullReq, pullRes, {});
1758
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1759
+ const pullBody = readJsonBody(pullRes);
1760
+ const pulledChanges = Array.isArray(pullBody?.changes) ? pullBody.changes : [];
1761
+ expect(pulledChanges).toEqual(expect.arrayContaining([
1762
+ expect.objectContaining({
1763
+ op: 'document-upsert',
1764
+ path: markdownPath,
1765
+ assetId: 'asset-cloud-planning-link-doc-1',
1766
+ linkTargetType: 'initiative',
1767
+ linkTargetId: 'initiative-cloud-link-roundtrip-1',
1768
+ originalFilename: 'plan.md',
1769
+ linkRole: 'attachment'
1770
+ }),
1771
+ expect.objectContaining({
1772
+ op: 'asset-upsert',
1773
+ path: filePath,
1774
+ assetId: 'asset-cloud-planning-link-file-1',
1775
+ linkTargetType: 'workstream',
1776
+ linkTargetId: 'workstream-cloud-link-roundtrip-1',
1777
+ originalFilename: 'notes.txt',
1778
+ linkRole: 'attachment'
1779
+ })
1780
+ ]));
1781
+ const localCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1782
+ localCore.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1783
+ localCore.createInitiative({
1784
+ id: 'initiative-cloud-link-roundtrip-1',
1785
+ title: 'Local initiative attachment target',
1786
+ createdAt: '2026-03-18T09:00:00.000Z',
1787
+ updatedAt: '2026-03-18T09:00:00.000Z'
1788
+ }, workspaceId);
1789
+ localCore.createWorkstream({
1790
+ id: 'workstream-cloud-link-roundtrip-1',
1791
+ initiativeId: 'initiative-cloud-link-roundtrip-1',
1792
+ title: 'Local workstream attachment target',
1793
+ createdAt: '2026-03-18T09:00:00.000Z',
1794
+ updatedAt: '2026-03-18T09:00:00.000Z'
1795
+ }, workspaceId);
1796
+ const localRoutes = createRoutes(localCore, { authConfig: { runtimeMode: 'local', enabled: false } });
1797
+ const applyMatch = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', localRoutes);
1798
+ expect(applyMatch).toBeTruthy();
1799
+ const applyReq = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', { changes: pulledChanges }, { 'x-taskforce-workspace-id': workspaceId });
1800
+ const applyRes = createMockRes();
1801
+ await applyMatch?.handler(applyReq, applyRes, {});
1802
+ expect(applyRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1803
+ const reloadedLocalCore = new TaskforceCore({ projectRoot: localProjectRoot, storagePath: '.taskforce' });
1804
+ const reloadedStore = new WorkspaceAssetStore(reloadedLocalCore.getDatabaseAdapter(), reloadedLocalCore.getTenantId());
1805
+ expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, markdownPath), 'utf8')).toBe('# planning sync\n');
1806
+ expect(fs.readFileSync(path.join(reloadedLocalCore.getPaths().basePath, filePath), 'utf8')).toBe('planning attachment');
1807
+ expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-doc-1', workspaceId)).toEqual([
1808
+ expect.objectContaining({
1809
+ targetType: 'initiative',
1810
+ targetId: 'initiative-cloud-link-roundtrip-1',
1811
+ taskId: null,
1812
+ role: 'attachment',
1813
+ deletedAt: null
1814
+ })
1815
+ ]);
1816
+ expect(reloadedStore.listLinksForAsset('asset-cloud-planning-link-file-1', workspaceId)).toEqual([
1817
+ expect.objectContaining({
1818
+ targetType: 'workstream',
1819
+ targetId: 'workstream-cloud-link-roundtrip-1',
1820
+ taskId: null,
1821
+ role: 'attachment',
1822
+ deletedAt: null
1823
+ })
1824
+ ]);
1825
+ });
1826
+ it('pushes document attachment links when only the link metadata changed locally', async () => {
1827
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-link-only-push-');
1828
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1829
+ seedAuthenticatedWorkspace(seedCore);
1830
+ seedCore.createTask({
1831
+ id: 'task-doc-link-only-push-1',
1832
+ title: 'Document link target',
1833
+ status: 'task'
1834
+ }, {
1835
+ workspaceId,
1836
+ allowClientProvidedId: true
1837
+ });
1838
+ const seedBasePath = seedCore.getPaths().basePath;
1839
+ const relativeDocPath = `workspaces/${workspaceId}/assets/documents/asset-doc-link-only-push-plan.md`;
1840
+ const absoluteDocPath = path.join(seedBasePath, relativeDocPath);
1841
+ const content = '# already synced\n';
1842
+ fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
1843
+ fs.writeFileSync(absoluteDocPath, content, 'utf8');
1844
+ const contentHash = createHash('sha256').update(content).digest('hex');
1845
+ seedCore.upsertDocumentWatermark(workspaceId, relativeDocPath, contentHash, '2026-03-18T13:30:00.000Z');
1846
+ const seedStore = new WorkspaceAssetStore(seedCore.getDatabaseAdapter(), seedCore.getTenantId());
1847
+ seedStore.upsertAsset({
1848
+ assetId: 'asset-doc-link-only-push-1',
1849
+ workspaceId,
1850
+ kind: 'document',
1851
+ originalFilename: 'plan.md',
1852
+ mimeType: 'text/markdown',
1853
+ storageProvider: 'local',
1854
+ storageKey: relativeDocPath,
1855
+ contentSha256: contentHash,
1856
+ sizeBytes: Buffer.byteLength(content, 'utf8'),
1857
+ documentId: 'doc-link-only-push-1',
1858
+ version: 1,
1859
+ isLatest: true,
1860
+ createdAt: '2026-03-18T13:30:00.000Z',
1861
+ updatedAt: '2026-03-18T13:30:00.000Z'
1862
+ });
1863
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1864
+ const routes = createRoutes(core, { authConfig });
1865
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1866
+ expect(pushMatch).toBeTruthy();
1867
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1868
+ workspaceId,
1869
+ idempotencyKey: 'doc-link-only-push-1',
1870
+ changes: [
1871
+ {
1872
+ op: 'document-upsert',
1873
+ path: relativeDocPath,
1874
+ updatedAt: '2026-03-18T14:45:00.000Z',
1875
+ content,
1876
+ assetId: 'asset-doc-link-only-push-1',
1877
+ documentId: 'doc-link-only-push-1',
1878
+ referenceNumber: 12,
1879
+ version: 1,
1880
+ taskId: 'task-doc-link-only-push-1',
1881
+ originalFilename: 'plan.md',
1882
+ linkRole: 'attachment'
1883
+ }
1884
+ ]
1885
+ }, { cookie: createAuthCookie(workspaceId) });
1886
+ const res = createMockRes();
1887
+ await pushMatch?.handler(req, res, {});
1888
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1889
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1890
+ const reloadedStore = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1891
+ expect(reloadedStore.listLinksForAsset('asset-doc-link-only-push-1', workspaceId)).toEqual([
1892
+ expect.objectContaining({
1893
+ taskId: 'task-doc-link-only-push-1',
1894
+ role: 'attachment',
1895
+ updatedAt: '2026-03-18T14:45:00.000Z',
1896
+ deletedAt: null
1897
+ })
1898
+ ]);
1899
+ });
1611
1900
  it('propagates document-delete through authenticated push and omits deleted doc from pull payload', async () => {
1612
1901
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-doc-delete-');
1613
1902
  const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
@@ -1992,9 +2281,9 @@ describe('Server Routes Workspace Sync Integration', () => {
1992
2281
  workspaceId,
1993
2282
  eventType: 'apply',
1994
2283
  status: 'error',
1995
- errorMessage: 'Detected asset reference number mismatch during normal sync.',
2284
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
1996
2285
  details: {
1997
- path: 'workspaces/workspace-local-active/assets/images/hero.png',
2286
+ taskId: 'task-1',
1998
2287
  existingReferenceNumber: 12,
1999
2288
  incomingReferenceNumber: 18
2000
2289
  }
@@ -2013,15 +2302,319 @@ describe('Server Routes Workspace Sync Integration', () => {
2013
2302
  expect.objectContaining({
2014
2303
  eventType: 'apply',
2015
2304
  status: 'error',
2016
- errorMessage: 'Detected asset reference number mismatch during normal sync.',
2305
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
2017
2306
  details: expect.objectContaining({
2018
- path: 'workspaces/workspace-local-active/assets/images/hero.png',
2307
+ taskId: 'task-1',
2019
2308
  existingReferenceNumber: 12,
2020
2309
  incomingReferenceNumber: 18
2021
2310
  })
2022
2311
  })
2023
2312
  ]));
2024
2313
  });
2314
+ it('repairs orphaned startup document and asset metadata and exposes the recovered metadata through snapshots', async () => {
2315
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-recover-');
2316
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2317
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2318
+ const basePath = core.getPaths().basePath;
2319
+ const relativeDocPath = 'docs/recovered-startup-note.md';
2320
+ const relativeAssetPath = `workspaces/${workspaceId}/assets/images/recovered-startup-image.png`;
2321
+ const absoluteDocPath = path.join(basePath, relativeDocPath);
2322
+ const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2323
+ const assetBuffer = Buffer.from('recovered-image-binary');
2324
+ fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
2325
+ fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2326
+ fs.writeFileSync(absoluteDocPath, '# repaired startup note\n', 'utf8');
2327
+ fs.writeFileSync(absoluteAssetPath, assetBuffer);
2328
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2329
+ const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2330
+ const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2331
+ const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2332
+ expect(documentsMatch).toBeTruthy();
2333
+ expect(assetsMatch).toBeTruthy();
2334
+ expect(repairMatch).toBeTruthy();
2335
+ const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2336
+ const beforeDocumentsRes = createMockRes();
2337
+ await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
2338
+ expect(beforeDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2339
+ const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
2340
+ const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
2341
+ .find((entry) => entry.path === relativeDocPath);
2342
+ expect(beforeDoc).toBeUndefined();
2343
+ const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2344
+ const beforeAssetsRes = createMockRes();
2345
+ await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2346
+ expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2347
+ const beforeAssetsBody = readJsonBody(beforeAssetsRes);
2348
+ expect(beforeAssetsBody.assets).toEqual([]);
2349
+ const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2350
+ const repairRes = createMockRes();
2351
+ await repairMatch?.handler(repairReq, repairRes, {});
2352
+ expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2353
+ expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2354
+ success: true,
2355
+ applied: 2,
2356
+ skipped: 0,
2357
+ failed: 0
2358
+ }));
2359
+ const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2360
+ const afterDocumentsRes = createMockRes();
2361
+ await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
2362
+ expect(afterDocumentsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2363
+ const afterDocumentsBody = readJsonBody(afterDocumentsRes);
2364
+ const repairedDoc = (Array.isArray(afterDocumentsBody?.documents) ? afterDocumentsBody.documents : [])
2365
+ .find((entry) => entry.path === relativeDocPath);
2366
+ expect(repairedDoc).toEqual(expect.objectContaining({
2367
+ path: relativeDocPath,
2368
+ content: '# repaired startup note\n',
2369
+ assetId: expect.stringMatching(/^asset-/),
2370
+ documentId: expect.stringMatching(/^doc-/),
2371
+ originalFilename: 'recovered-startup-note.md'
2372
+ }));
2373
+ const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2374
+ const afterAssetsRes = createMockRes();
2375
+ await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2376
+ expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2377
+ const afterAssetsBody = readJsonBody(afterAssetsRes);
2378
+ const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
2379
+ .find((entry) => entry.path === relativeAssetPath);
2380
+ expect(repairedAsset).toEqual(expect.objectContaining({
2381
+ path: relativeAssetPath,
2382
+ assetId: expect.stringMatching(/^asset-/),
2383
+ kind: 'image',
2384
+ mimeType: 'image/png',
2385
+ originalFilename: 'recovered-startup-image.png',
2386
+ linkRole: 'image',
2387
+ contentBase64: assetBuffer.toString('base64')
2388
+ }));
2389
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2390
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2391
+ expect(store.getByStorageKey(relativeDocPath, workspaceId)).toEqual(expect.objectContaining({
2392
+ storageKey: relativeDocPath,
2393
+ kind: 'document',
2394
+ mimeType: 'text/markdown',
2395
+ originalFilename: 'recovered-startup-note.md',
2396
+ documentId: expect.stringMatching(/^doc-/)
2397
+ }));
2398
+ expect(store.getByStorageKey(relativeAssetPath, workspaceId)).toEqual(expect.objectContaining({
2399
+ storageKey: relativeAssetPath,
2400
+ kind: 'image',
2401
+ mimeType: 'image/png',
2402
+ originalFilename: 'recovered-startup-image.png'
2403
+ }));
2404
+ });
2405
+ it('revives stale startup tombstones with the original metadata identifiers when local files are newer', async () => {
2406
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-revive-');
2407
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2408
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2409
+ const basePath = core.getPaths().basePath;
2410
+ const relativeDocPath = 'docs/revived-startup-note.md';
2411
+ const relativeAssetPath = `workspaces/${workspaceId}/assets/images/revived-startup-image.png`;
2412
+ const absoluteDocPath = path.join(basePath, relativeDocPath);
2413
+ const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2414
+ const revivedAt = new Date('2026-03-17T12:00:00.000Z');
2415
+ const deletedAt = '2026-03-10T08:00:00.000Z';
2416
+ const assetBuffer = Buffer.from('revived-image-binary');
2417
+ fs.mkdirSync(path.dirname(absoluteDocPath), { recursive: true });
2418
+ fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2419
+ fs.writeFileSync(absoluteDocPath, '# revived startup note\n', 'utf8');
2420
+ fs.writeFileSync(absoluteAssetPath, assetBuffer);
2421
+ fs.utimesSync(absoluteDocPath, revivedAt, revivedAt);
2422
+ fs.utimesSync(absoluteAssetPath, revivedAt, revivedAt);
2423
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
2424
+ seededStore.upsertAsset({
2425
+ assetId: 'asset-revived-doc-1',
2426
+ workspaceId,
2427
+ kind: 'document',
2428
+ originalFilename: 'revived-startup-note.md',
2429
+ mimeType: 'text/markdown',
2430
+ storageProvider: 'local',
2431
+ storageKey: relativeDocPath,
2432
+ contentSha256: 'deleted-doc-hash',
2433
+ sizeBytes: 1,
2434
+ documentId: 'doc-revived-startup-1',
2435
+ referenceNumber: 33,
2436
+ version: 4,
2437
+ createdAt: '2026-03-01T08:00:00.000Z',
2438
+ updatedAt: deletedAt,
2439
+ deletedAt,
2440
+ purgeAfter: '2026-06-01T08:00:00.000Z'
2441
+ });
2442
+ seededStore.upsertAsset({
2443
+ assetId: 'asset-revived-image-1',
2444
+ workspaceId,
2445
+ kind: 'image',
2446
+ originalFilename: 'revived-startup-image.png',
2447
+ mimeType: 'image/png',
2448
+ storageProvider: 'local',
2449
+ storageKey: relativeAssetPath,
2450
+ contentSha256: 'deleted-image-hash',
2451
+ sizeBytes: 1,
2452
+ referenceNumber: 21,
2453
+ createdAt: '2026-03-01T08:00:00.000Z',
2454
+ updatedAt: deletedAt,
2455
+ deletedAt,
2456
+ purgeAfter: '2026-06-01T08:00:00.000Z'
2457
+ });
2458
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2459
+ const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2460
+ const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2461
+ const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2462
+ expect(documentsMatch).toBeTruthy();
2463
+ expect(assetsMatch).toBeTruthy();
2464
+ expect(repairMatch).toBeTruthy();
2465
+ const beforeDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2466
+ const beforeDocumentsRes = createMockRes();
2467
+ await documentsMatch?.handler(beforeDocumentsReq, beforeDocumentsRes, {});
2468
+ const beforeDocumentsBody = readJsonBody(beforeDocumentsRes);
2469
+ const beforeDoc = (Array.isArray(beforeDocumentsBody?.documents) ? beforeDocumentsBody.documents : [])
2470
+ .find((entry) => entry.path === relativeDocPath);
2471
+ expect(beforeDoc).toBeUndefined();
2472
+ const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2473
+ const beforeAssetsRes = createMockRes();
2474
+ await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2475
+ expect(readJsonBody(beforeAssetsRes).assets).toEqual([]);
2476
+ const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2477
+ const repairRes = createMockRes();
2478
+ await repairMatch?.handler(repairReq, repairRes, {});
2479
+ expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2480
+ success: true,
2481
+ applied: 2,
2482
+ skipped: 0,
2483
+ failed: 0
2484
+ }));
2485
+ const afterDocumentsReq = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2486
+ const afterDocumentsRes = createMockRes();
2487
+ await documentsMatch?.handler(afterDocumentsReq, afterDocumentsRes, {});
2488
+ const repairedDoc = (Array.isArray(readJsonBody(afterDocumentsRes)?.documents) ? readJsonBody(afterDocumentsRes).documents : [])
2489
+ .find((entry) => entry.path === relativeDocPath);
2490
+ expect(repairedDoc).toEqual(expect.objectContaining({
2491
+ path: relativeDocPath,
2492
+ assetId: 'asset-revived-doc-1',
2493
+ documentId: 'doc-revived-startup-1',
2494
+ referenceNumber: 33,
2495
+ version: 5,
2496
+ originalFilename: 'revived-startup-note.md'
2497
+ }));
2498
+ const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2499
+ const afterAssetsRes = createMockRes();
2500
+ await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2501
+ const repairedAsset = (Array.isArray(readJsonBody(afterAssetsRes)?.assets) ? readJsonBody(afterAssetsRes).assets : [])
2502
+ .find((entry) => entry.path === relativeAssetPath);
2503
+ expect(repairedAsset).toEqual(expect.objectContaining({
2504
+ path: relativeAssetPath,
2505
+ assetId: 'asset-revived-image-1',
2506
+ referenceNumber: 21,
2507
+ kind: 'image',
2508
+ mimeType: 'image/png',
2509
+ originalFilename: 'revived-startup-image.png',
2510
+ contentBase64: assetBuffer.toString('base64')
2511
+ }));
2512
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2513
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2514
+ expect(store.get('asset-revived-doc-1', workspaceId)).toEqual(expect.objectContaining({
2515
+ assetId: 'asset-revived-doc-1',
2516
+ documentId: 'doc-revived-startup-1',
2517
+ referenceNumber: 33,
2518
+ version: 5,
2519
+ deletedAt: null
2520
+ }));
2521
+ expect(store.get('asset-revived-image-1', workspaceId)).toEqual(expect.objectContaining({
2522
+ assetId: 'asset-revived-image-1',
2523
+ referenceNumber: 21,
2524
+ deletedAt: null
2525
+ }));
2526
+ });
2527
+ it('removes stale startup asset links to missing tasks while preserving the asset snapshot', async () => {
2528
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-repair-startup-stale-link-');
2529
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2530
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2531
+ const basePath = core.getPaths().basePath;
2532
+ const relativeAssetPath = `workspaces/${workspaceId}/assets/files/stale-link-asset.txt`;
2533
+ const absoluteAssetPath = path.join(basePath, relativeAssetPath);
2534
+ const assetBuffer = Buffer.from('stale-link-asset');
2535
+ fs.mkdirSync(path.dirname(absoluteAssetPath), { recursive: true });
2536
+ fs.writeFileSync(absoluteAssetPath, assetBuffer);
2537
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
2538
+ seededStore.upsertAsset({
2539
+ assetId: 'asset-stale-link-1',
2540
+ workspaceId,
2541
+ kind: 'file',
2542
+ originalFilename: 'stale-link-asset.txt',
2543
+ mimeType: 'text/plain',
2544
+ storageProvider: 'local',
2545
+ storageKey: relativeAssetPath,
2546
+ contentSha256: createHash('sha256').update(assetBuffer).digest('hex'),
2547
+ sizeBytes: assetBuffer.length,
2548
+ createdAt: '2026-03-17T11:00:00.000Z',
2549
+ updatedAt: '2026-03-17T11:00:00.000Z'
2550
+ });
2551
+ seededStore.upsertLink({
2552
+ workspaceId,
2553
+ assetId: 'asset-stale-link-1',
2554
+ taskId: 'task-missing-link-1',
2555
+ role: 'attachment',
2556
+ updatedAt: '2026-03-17T11:00:00.000Z'
2557
+ });
2558
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2559
+ const assetsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/assets', routes);
2560
+ const repairMatch = matchRoute('POST', '/api/taskforce/sync/workspace/repair-startup', routes);
2561
+ expect(assetsMatch).toBeTruthy();
2562
+ expect(repairMatch).toBeTruthy();
2563
+ const beforeAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2564
+ const beforeAssetsRes = createMockRes();
2565
+ await assetsMatch?.handler(beforeAssetsReq, beforeAssetsRes, {});
2566
+ expect(beforeAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2567
+ const beforeAssetsBody = readJsonBody(beforeAssetsRes);
2568
+ const beforeAsset = (Array.isArray(beforeAssetsBody?.assets) ? beforeAssetsBody.assets : [])
2569
+ .find((entry) => entry.path === relativeAssetPath);
2570
+ expect(beforeAsset).toEqual(expect.objectContaining({
2571
+ path: relativeAssetPath,
2572
+ assetId: 'asset-stale-link-1',
2573
+ taskId: 'task-missing-link-1',
2574
+ linkRole: 'attachment',
2575
+ contentBase64: assetBuffer.toString('base64')
2576
+ }));
2577
+ const repairReq = createMockReq('POST', '/api/taskforce/sync/workspace/repair-startup', { workspaceId }, { 'x-taskforce-workspace-id': workspaceId });
2578
+ const repairRes = createMockRes();
2579
+ await repairMatch?.handler(repairReq, repairRes, {});
2580
+ expect(repairRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2581
+ expect(readJsonBody(repairRes)).toEqual(expect.objectContaining({
2582
+ success: true,
2583
+ applied: 1,
2584
+ skipped: 0,
2585
+ failed: 0
2586
+ }));
2587
+ const afterAssetsReq = createMockReq('GET', `/api/taskforce/sync/workspace/assets?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2588
+ const afterAssetsRes = createMockRes();
2589
+ await assetsMatch?.handler(afterAssetsReq, afterAssetsRes, {});
2590
+ expect(afterAssetsRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2591
+ const afterAssetsBody = readJsonBody(afterAssetsRes);
2592
+ const repairedAsset = (Array.isArray(afterAssetsBody?.assets) ? afterAssetsBody.assets : [])
2593
+ .find((entry) => entry.path === relativeAssetPath);
2594
+ expect(repairedAsset).toEqual(expect.objectContaining({
2595
+ path: relativeAssetPath,
2596
+ assetId: 'asset-stale-link-1',
2597
+ taskId: null,
2598
+ linkRole: 'attachment',
2599
+ contentBase64: assetBuffer.toString('base64')
2600
+ }));
2601
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2602
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
2603
+ expect(store.get('asset-stale-link-1', workspaceId)).toEqual(expect.objectContaining({
2604
+ assetId: 'asset-stale-link-1',
2605
+ storageKey: relativeAssetPath,
2606
+ deletedAt: null
2607
+ }));
2608
+ expect(store.listLinksForAsset('asset-stale-link-1', workspaceId)).toEqual([]);
2609
+ expect(store.listLinksForAsset('asset-stale-link-1', workspaceId, { includeDeleted: true })).toEqual([
2610
+ expect.objectContaining({
2611
+ assetId: 'asset-stale-link-1',
2612
+ taskId: 'task-missing-link-1',
2613
+ role: 'attachment',
2614
+ deletedAt: expect.any(String)
2615
+ })
2616
+ ]);
2617
+ });
2025
2618
  it('returns 400 on apply-local when workspaceId is null/empty/default', async () => {
2026
2619
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-wsmissing-');
2027
2620
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
@@ -2038,16 +2631,24 @@ describe('Server Routes Workspace Sync Integration', () => {
2038
2631
  it('keeps repeated document scans correct and invalidates cache when file content changes', async () => {
2039
2632
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-doc-cache-');
2040
2633
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2634
+ const basePath = core.getPaths().basePath;
2041
2635
  core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2042
- const syncRoot = core.getPaths().basePath;
2043
- const docsDir = path.join(syncRoot, 'docs');
2044
- fs.mkdirSync(docsDir, { recursive: true });
2045
- const relativeDocPath = 'docs/cache-target.md';
2046
- const absoluteDocPath = path.join(syncRoot, relativeDocPath);
2047
- fs.writeFileSync(absoluteDocPath, '# cache test\n', 'utf8');
2048
2636
  const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2637
+ const uploadMatch = matchRoute('POST', '/api/taskforce/context-upload', routes);
2049
2638
  const match = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2639
+ expect(uploadMatch).toBeTruthy();
2050
2640
  expect(match).toBeTruthy();
2641
+ const uploadReq = createMockReq('POST', '/api/taskforce/context-upload', {
2642
+ file: `data:text/markdown;base64,${Buffer.from('# cache test\n', 'utf8').toString('base64')}`,
2643
+ originalName: 'cache-target.md',
2644
+ workspaceId
2645
+ }, { 'x-taskforce-workspace-id': workspaceId });
2646
+ const uploadRes = createMockRes();
2647
+ await uploadMatch?.handler(uploadReq, uploadRes, {});
2648
+ expect(uploadRes.writeHead).toHaveBeenCalledWith(200, { 'Content-Type': 'application/json' });
2649
+ const uploadBody = JSON.parse(String(uploadRes.end.mock.calls.at(-1)?.[0] || '{}'));
2650
+ const relativeDocPath = String(uploadBody?.fsPath || '');
2651
+ const absoluteDocPath = path.join(basePath, relativeDocPath);
2051
2652
  const req1 = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${workspaceId}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2052
2653
  const res1 = createMockRes();
2053
2654
  await match?.handler(req1, res1, {});
@@ -2102,6 +2703,24 @@ describe('Server Routes Workspace Sync Integration', () => {
2102
2703
  expect(doc).toBeTruthy();
2103
2704
  expect(doc.content).toBe('# local attachment\n');
2104
2705
  });
2706
+ it('does not sync raw docs directory markdown files because shared document sync must be canonical and workspace-scoped', async () => {
2707
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-exclude-raw-docs-');
2708
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
2709
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
2710
+ const rawDocsDir = path.join(core.getPaths().basePath, 'docs');
2711
+ fs.mkdirSync(rawDocsDir, { recursive: true });
2712
+ fs.writeFileSync(path.join(rawDocsDir, 'legacy-shared.md'), '# legacy shared doc\n', 'utf8');
2713
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
2714
+ const documentsMatch = matchRoute('GET', '/api/taskforce/sync/workspace/documents', routes);
2715
+ expect(documentsMatch).toBeTruthy();
2716
+ const req = createMockReq('GET', `/api/taskforce/sync/workspace/documents?workspaceId=${encodeURIComponent(workspaceId)}`, undefined, { 'x-taskforce-workspace-id': workspaceId });
2717
+ const res = createMockRes();
2718
+ await documentsMatch?.handler(req, res, {});
2719
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
2720
+ const body = JSON.parse(String(res.end.mock.calls.at(-1)?.[0] || '{}'));
2721
+ const docs = Array.isArray(body?.documents) ? body.documents : [];
2722
+ expect(docs.some((entry) => entry.path === 'docs/legacy-shared.md')).toBe(false);
2723
+ });
2105
2724
  it('converges task conflicts deterministically for equal timestamps regardless of apply order', async () => {
2106
2725
  const applySequence = async (prefix, titlesInOrder) => {
2107
2726
  const projectRoot = createIsolatedProjectRoot(prefix);