@taskforcehq/taskforce 0.3.305 → 0.3.306

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -0,0 +1,55 @@
1
+ export function hasOwnSyncField(source, field) {
2
+ return Boolean(source) && typeof source === 'object' && Object.prototype.hasOwnProperty.call(source, field);
3
+ }
4
+ export function normalizeSyncOptionalString(value) {
5
+ if (value === undefined || value === null)
6
+ return undefined;
7
+ const normalized = String(value).trim();
8
+ return normalized.length > 0 ? normalized : undefined;
9
+ }
10
+ export function normalizeSyncNullableString(value) {
11
+ if (value === undefined || value === null)
12
+ return null;
13
+ const normalized = String(value).trim();
14
+ return normalized.length > 0 ? normalized : null;
15
+ }
16
+ export function normalizeSyncOptionalId(value) {
17
+ return normalizeSyncOptionalString(value);
18
+ }
19
+ export function normalizeSyncNullableId(value) {
20
+ return normalizeSyncNullableString(value);
21
+ }
22
+ export function normalizeSyncNullableIsoString(value) {
23
+ return normalizeSyncNullableString(value);
24
+ }
25
+ export function normalizeSyncSameOriginAssetUrl(value) {
26
+ const normalized = normalizeSyncNullableString(value);
27
+ if (!normalized)
28
+ return null;
29
+ return normalized.startsWith('/api/taskforce/context/') ? normalized : null;
30
+ }
31
+ export function readSyncOptionalStringField(source, field) {
32
+ if (!hasOwnSyncField(source, field))
33
+ return undefined;
34
+ return normalizeSyncOptionalString(source[field]);
35
+ }
36
+ export function readSyncNullableStringField(source, field) {
37
+ if (!hasOwnSyncField(source, field))
38
+ return undefined;
39
+ return normalizeSyncNullableString(source[field]);
40
+ }
41
+ export function readSyncNullableIdField(source, field) {
42
+ if (!hasOwnSyncField(source, field))
43
+ return undefined;
44
+ return normalizeSyncNullableId(source[field]);
45
+ }
46
+ export function readSyncNullableIsoStringField(source, field) {
47
+ if (!hasOwnSyncField(source, field))
48
+ return undefined;
49
+ return normalizeSyncNullableIsoString(source[field]);
50
+ }
51
+ export function readSyncSameOriginAssetUrlField(source, field) {
52
+ if (!hasOwnSyncField(source, field))
53
+ return undefined;
54
+ return normalizeSyncSameOriginAssetUrl(source[field]);
55
+ }
@@ -0,0 +1,44 @@
1
+ import type { TaskItem } from '../types.js';
2
+ export interface SyncAiProfileLike {
3
+ id?: string | null;
4
+ workspaceId?: string | null;
5
+ profileToken?: string | null;
6
+ name?: string | null;
7
+ username?: string | null;
8
+ icon?: string | null;
9
+ color?: string | null;
10
+ avatarUrl?: string | null;
11
+ avatarSourceUrl?: string | null;
12
+ description?: string | null;
13
+ role?: string | null;
14
+ provider?: string | null;
15
+ model?: string | null;
16
+ surfaceType?: string | null;
17
+ seatScope?: string | null;
18
+ providerMetadata?: Record<string, unknown> | null;
19
+ archivedAt?: string | null;
20
+ archivedBy?: string | null;
21
+ archivedReason?: string | null;
22
+ mergedIntoProfileId?: string | null;
23
+ rosterStateUpdatedAt?: string | null;
24
+ createdAt?: string | null;
25
+ updatedAt?: string | null;
26
+ lastActiveAt?: string | null;
27
+ }
28
+ export interface SyncResourceAdapter<T> {
29
+ kind: string;
30
+ getId: (resource: T) => string;
31
+ getWatermark: (resource: T) => string;
32
+ getChangeKey: (resource: T) => string;
33
+ }
34
+ export interface SyncResourceBaselineState {
35
+ watermarks?: Map<string, string> | null;
36
+ changeKeys?: Map<string, string> | null;
37
+ }
38
+ export declare function getAiProfileSyncWatermark(profile: Partial<SyncAiProfileLike> | null | undefined): string;
39
+ export declare function getAiProfileSyncChangeKey(profile: Partial<SyncAiProfileLike> | null | undefined): string;
40
+ export declare const taskSyncResourceAdapter: SyncResourceAdapter<Partial<TaskItem> & {
41
+ isArchived?: boolean | null;
42
+ }>;
43
+ export declare const aiProfileSyncResourceAdapter: SyncResourceAdapter<Partial<SyncAiProfileLike>>;
44
+ export declare function shouldSyncResource<T>(adapter: SyncResourceAdapter<T>, resource: T, baseline?: SyncResourceBaselineState | null): boolean;
@@ -0,0 +1,77 @@
1
+ import { buildTaskSyncChangeKey } from './taskSyncChangeKey.js';
2
+ import { normalizeSyncNullableIsoString, normalizeSyncNullableString, normalizeSyncOptionalId, normalizeSyncSameOriginAssetUrl, } from './syncFieldSemantics.js';
3
+ function normalizeSyncTimestampMax(...values) {
4
+ return values.reduce((latest, value) => {
5
+ const normalized = normalizeSyncNullableIsoString(value) || '';
6
+ if (!latest)
7
+ return normalized;
8
+ if (!normalized)
9
+ return latest;
10
+ return normalized > latest ? normalized : latest;
11
+ }, '');
12
+ }
13
+ export function getAiProfileSyncWatermark(profile) {
14
+ const source = profile || {};
15
+ return normalizeSyncTimestampMax(source.createdAt, source.updatedAt, source.lastActiveAt, source.archivedAt, source.rosterStateUpdatedAt);
16
+ }
17
+ export function getAiProfileSyncChangeKey(profile) {
18
+ const source = profile || {};
19
+ return JSON.stringify({
20
+ id: String(source.id || '').trim(),
21
+ workspaceId: String(source.workspaceId || '').trim(),
22
+ profileToken: String(source.profileToken || '').trim(),
23
+ name: String(source.name || '').trim(),
24
+ username: String(source.username || '').trim(),
25
+ icon: String(source.icon || 'Bot').trim() || 'Bot',
26
+ color: String(source.color || '#8b5cf6').trim() || '#8b5cf6',
27
+ avatarUrl: normalizeSyncSameOriginAssetUrl(source.avatarUrl) || '',
28
+ avatarSourceUrl: normalizeSyncSameOriginAssetUrl(source.avatarSourceUrl) || '',
29
+ description: normalizeSyncNullableString(source.description) || '',
30
+ role: normalizeSyncNullableString(source.role) || '',
31
+ provider: normalizeSyncNullableString(source.provider) || '',
32
+ model: normalizeSyncNullableString(source.model) || '',
33
+ surfaceType: normalizeSyncNullableString(source.surfaceType) || '',
34
+ seatScope: normalizeSyncNullableString(source.seatScope) || '',
35
+ providerMetadata: source.providerMetadata && typeof source.providerMetadata === 'object' && !Array.isArray(source.providerMetadata)
36
+ ? source.providerMetadata
37
+ : null,
38
+ archivedAt: normalizeSyncNullableIsoString(source.archivedAt) || '',
39
+ archivedBy: normalizeSyncOptionalId(source.archivedBy) || '',
40
+ archivedReason: normalizeSyncNullableString(source.archivedReason) || '',
41
+ mergedIntoProfileId: normalizeSyncOptionalId(source.mergedIntoProfileId) || '',
42
+ rosterStateUpdatedAt: normalizeSyncNullableIsoString(source.rosterStateUpdatedAt) || '',
43
+ createdAt: normalizeSyncNullableIsoString(source.createdAt) || '',
44
+ updatedAt: normalizeSyncNullableIsoString(source.updatedAt) || '',
45
+ lastActiveAt: normalizeSyncNullableIsoString(source.lastActiveAt) || '',
46
+ });
47
+ }
48
+ export const taskSyncResourceAdapter = {
49
+ kind: 'task',
50
+ getId: (task) => String(task?.id || '').trim(),
51
+ getWatermark: (task) => String(task?.updatedAt || task?.createdAt || '').trim(),
52
+ getChangeKey: (task) => buildTaskSyncChangeKey(task, Boolean(task?.isArchived))
53
+ };
54
+ export const aiProfileSyncResourceAdapter = {
55
+ kind: 'ai-profile',
56
+ getId: (profile) => String(profile?.id || '').trim(),
57
+ getWatermark: (profile) => getAiProfileSyncWatermark(profile),
58
+ getChangeKey: (profile) => getAiProfileSyncChangeKey(profile)
59
+ };
60
+ export function shouldSyncResource(adapter, resource, baseline) {
61
+ const id = adapter.getId(resource);
62
+ if (!id)
63
+ return false;
64
+ const currentChangeKey = adapter.getChangeKey(resource);
65
+ const previousWatermarks = baseline?.watermarks;
66
+ if (!previousWatermarks || previousWatermarks.size === 0)
67
+ return true;
68
+ if (!previousWatermarks.has(id))
69
+ return true;
70
+ const previousChangeKeys = baseline?.changeKeys;
71
+ const previousChangeKey = previousChangeKeys?.get(id);
72
+ if (typeof previousChangeKey === 'string') {
73
+ return previousChangeKey !== currentChangeKey;
74
+ }
75
+ const previousWatermark = previousWatermarks.get(id) || '';
76
+ return previousWatermark !== currentChangeKey;
77
+ }
@@ -56,6 +56,8 @@ export interface WorkspacePushSyncResult {
56
56
  changeCount: number;
57
57
  deleteCount: number;
58
58
  currentTaskIds: Set<string>;
59
+ currentTaskWatermarks?: Map<string, string>;
60
+ currentTaskChangeKeys?: Map<string, string>;
59
61
  deleteTaskIds: Set<string>;
60
62
  currentInitiativeIds: Set<string>;
61
63
  currentInitiativeWatermarks: Map<string, string>;
@@ -63,6 +65,7 @@ export interface WorkspacePushSyncResult {
63
65
  currentWorkstreamWatermarks: Map<string, string>;
64
66
  currentAiProfileIds: Set<string>;
65
67
  currentAiProfileWatermarks: Map<string, string>;
68
+ currentAiProfileChangeKeys?: Map<string, string>;
66
69
  currentDocumentPaths: Set<string>;
67
70
  deleteDocumentPaths: Set<string>;
68
71
  currentDocumentWatermarks: Map<string, string>;
@@ -81,6 +84,7 @@ export interface WorkspacePushSyncResult {
81
84
  currentTaskEventWatermarks: Map<string, string>;
82
85
  /** Watermarks for every task upsert that was successfully pushed. */
83
86
  pushedWatermarks: Map<string, string>;
87
+ pushedTaskChangeKeys?: Map<string, string>;
84
88
  /** Server-emitted realtime mutation event ids for this push apply. */
85
89
  emittedEventIds: string[];
86
90
  /** Authoritative task snapshots returned immediately by the push route for incoming task upserts. */
@@ -106,6 +110,7 @@ export interface WorkspacePullSyncResult {
106
110
  success: boolean;
107
111
  kind?: WorkspacePullFailureKind;
108
112
  status?: number;
113
+ errorCode?: string;
109
114
  retryAfterMs?: number;
110
115
  transient?: boolean;
111
116
  failures?: string[];
@@ -15,6 +15,14 @@ function resolveWorkspaceAccessConflictMessage(data, fallbackStatus) {
15
15
  }
16
16
  return rawError || `Workspace provisioning failed (${fallbackStatus})`;
17
17
  }
18
+ function isMissingCloudWorkspaceSelection(data, status) {
19
+ if (status !== 400)
20
+ return false;
21
+ const code = String(data.code || '').trim().toUpperCase();
22
+ const rawError = String(data.error || '').trim().toLowerCase();
23
+ return code === 'WORKSPACE_ID_REQUIRED'
24
+ && rawError.includes('no cloud workspace is selected');
25
+ }
18
26
  export async function ensureCloudWorkspaceReadyForSyncService(input) {
19
27
  if (!input.cloudAuthConfigured)
20
28
  return { success: false, error: 'Cloud auth endpoint is not configured.' };
@@ -44,6 +52,39 @@ export async function ensureCloudWorkspaceReadyForSyncService(input) {
44
52
  error: String(data.error || 'Workspace sync rate limited. Please retry later.')
45
53
  };
46
54
  }
55
+ if (isMissingCloudWorkspaceSelection(data, handshakeResult.status)) {
56
+ const provisionResult = await provisionWorkspace(input.resolveCloudAuthUrl, {
57
+ workspaceId: input.workspaceId,
58
+ name: input.workspaceName || input.workspaceId
59
+ });
60
+ if (!provisionResult.ok) {
61
+ const provisionError = asObjectRecord(provisionResult.data);
62
+ return {
63
+ success: false,
64
+ statusCode: provisionResult.status,
65
+ transient: isTransientWorkspaceSyncStatus(provisionResult.status),
66
+ retryAfterMs: provisionResult.retryAfterMs,
67
+ error: resolveWorkspaceAccessConflictMessage(provisionError, provisionResult.status)
68
+ };
69
+ }
70
+ handshakeResult = await handshakeWorkspace(input.resolveCloudAuthUrl, input.workspaceId);
71
+ if (!handshakeResult.ok) {
72
+ const retryData = asObjectRecord(handshakeResult.data);
73
+ if (handshakeResult.status === 401) {
74
+ return { success: false, statusCode: 401, transient: false, error: String(retryData.error || 'Authentication required for workspace sync.') };
75
+ }
76
+ if (handshakeResult.status === 429) {
77
+ return { success: false, statusCode: 429, retryAfterMs: handshakeResult.retryAfterMs, transient: true, error: String(retryData.error || 'Workspace sync rate limited. Please retry later.') };
78
+ }
79
+ return {
80
+ success: false,
81
+ statusCode: handshakeResult.status,
82
+ transient: isTransientWorkspaceSyncStatus(handshakeResult.status),
83
+ error: String(retryData.error || `Workspace sync handshake failed (${handshakeResult.status})`)
84
+ };
85
+ }
86
+ return { success: true, provisioned: true };
87
+ }
47
88
  if (handshakeResult.status === 409 && data.code === 'WORKSPACE_ID_MISMATCH') {
48
89
  if (input.allowProvisionOnMismatch === false) {
49
90
  return {
@@ -173,6 +214,8 @@ export async function runWorkspacePushSyncService(input) {
173
214
  changeCount,
174
215
  deleteCount,
175
216
  currentTaskIds: input.payload.currentTaskIds,
217
+ currentTaskWatermarks: input.payload.currentTaskWatermarks ?? new Map(),
218
+ currentTaskChangeKeys: input.payload.currentTaskChangeKeys ?? new Map(),
176
219
  deleteTaskIds: input.payload.deleteTaskIds,
177
220
  currentInitiativeIds: input.payload.currentInitiativeIds,
178
221
  currentInitiativeWatermarks: input.payload.currentInitiativeWatermarks,
@@ -180,6 +223,7 @@ export async function runWorkspacePushSyncService(input) {
180
223
  currentWorkstreamWatermarks: input.payload.currentWorkstreamWatermarks,
181
224
  currentAiProfileIds: input.payload.currentAiProfileIds,
182
225
  currentAiProfileWatermarks: input.payload.currentAiProfileWatermarks,
226
+ currentAiProfileChangeKeys: input.payload.currentAiProfileChangeKeys ?? new Map(),
183
227
  currentDocumentPaths: input.payload.currentDocumentPaths,
184
228
  deleteDocumentPaths: input.payload.deleteDocumentPaths,
185
229
  currentDocumentWatermarks: input.payload.currentDocumentWatermarks,
@@ -197,6 +241,7 @@ export async function runWorkspacePushSyncService(input) {
197
241
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
198
242
  currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
199
243
  pushedWatermarks: new Map(),
244
+ pushedTaskChangeKeys: new Map(),
200
245
  emittedEventIds: [],
201
246
  appliedTaskUpserts: []
202
247
  };
@@ -231,6 +276,8 @@ export async function runWorkspacePushSyncService(input) {
231
276
  changeCount,
232
277
  deleteCount,
233
278
  currentTaskIds: input.payload.currentTaskIds,
279
+ currentTaskWatermarks: input.payload.currentTaskWatermarks ?? new Map(),
280
+ currentTaskChangeKeys: input.payload.currentTaskChangeKeys ?? new Map(),
234
281
  deleteTaskIds: input.payload.deleteTaskIds,
235
282
  currentInitiativeIds: input.payload.currentInitiativeIds,
236
283
  currentInitiativeWatermarks: input.payload.currentInitiativeWatermarks,
@@ -238,6 +285,7 @@ export async function runWorkspacePushSyncService(input) {
238
285
  currentWorkstreamWatermarks: input.payload.currentWorkstreamWatermarks,
239
286
  currentAiProfileIds: input.payload.currentAiProfileIds,
240
287
  currentAiProfileWatermarks: input.payload.currentAiProfileWatermarks,
288
+ currentAiProfileChangeKeys: input.payload.currentAiProfileChangeKeys ?? new Map(),
241
289
  currentDocumentPaths: input.payload.currentDocumentPaths,
242
290
  deleteDocumentPaths: input.payload.deleteDocumentPaths,
243
291
  currentDocumentWatermarks: input.payload.currentDocumentWatermarks,
@@ -255,6 +303,7 @@ export async function runWorkspacePushSyncService(input) {
255
303
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
256
304
  currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
257
305
  pushedWatermarks: input.payload.pushedWatermarks,
306
+ pushedTaskChangeKeys: input.payload.pushedTaskChangeKeys ?? new Map(),
258
307
  emittedEventIds,
259
308
  appliedTaskUpserts
260
309
  };
@@ -296,10 +345,13 @@ export async function runWorkspacePullSyncService(input) {
296
345
  }
297
346
  }
298
347
  if (!pullRes.ok) {
348
+ const pullErrorData = asObjectRecord(pullRes.data);
299
349
  return {
300
350
  success: false,
301
351
  kind: 'pull_http',
302
352
  status: pullRes.status,
353
+ errorCode: typeof pullErrorData.code === 'string' ? pullErrorData.code : undefined,
354
+ error: typeof pullErrorData.error === 'string' ? pullErrorData.error : undefined,
303
355
  retryAfterMs: pullRes.retryAfterMs,
304
356
  transient: isTransientWorkspaceSyncStatus(pullRes.status),
305
357
  cursor,
@@ -443,7 +443,7 @@ describe('syncService', () => {
443
443
  status: 200,
444
444
  data: {
445
445
  success: true,
446
- changes: [{ op: 'upsert', task: { id: 't-parent', title: 'Parent', status: 'done', updatedAt: '2026-03-15T20:27:08.000Z' } }],
446
+ changes: [{ op: 'upsert', task: { id: 'task-apply-error', title: 'Apply error', status: 'done', updatedAt: '2026-03-15T20:27:08.000Z' } }],
447
447
  nextCursor: null,
448
448
  hasMore: false
449
449
  }
@@ -452,7 +452,7 @@ describe('syncService', () => {
452
452
  ok: false,
453
453
  status: 400,
454
454
  failures: ['/api/taskforce/sync/workspace/apply-local:400'],
455
- data: { error: 'Parent tasks cannot be marked done manually; completion is derived from descendants' }
455
+ data: { error: 'Task apply rejected by local validation' }
456
456
  });
457
457
  const res = await runWorkspacePullSyncService({
458
458
  resolveCloudAuthUrl: mockResolveCloudAuthUrl,
@@ -463,7 +463,7 @@ describe('syncService', () => {
463
463
  expect(res.success).toBe(false);
464
464
  expect(res.kind).toBe('apply_all_candidates');
465
465
  expect(res.status).toBe(400);
466
- expect(res.error).toBe('Parent tasks cannot be marked done manually; completion is derived from descendants');
466
+ expect(res.error).toBe('Task apply rejected by local validation');
467
467
  });
468
468
  it('surfaces document decryption failures without failing pull', async () => {
469
469
  const previousKey = process.env.TASKFORCE_SYNC_KEY;
@@ -702,6 +702,60 @@ describe('syncService', () => {
702
702
  expect(res.statusCode).toBe(0);
703
703
  expect(res.transient).toBe(true);
704
704
  });
705
+ it('recovers from missing cloud workspace selection by provisioning and retrying handshake', async () => {
706
+ vi.mocked(handshakeWorkspace)
707
+ .mockResolvedValueOnce({
708
+ ok: false,
709
+ status: 400,
710
+ data: {
711
+ code: 'WORKSPACE_ID_REQUIRED',
712
+ error: 'No cloud workspace is selected. Complete workspace setup first.'
713
+ }
714
+ })
715
+ .mockResolvedValueOnce({
716
+ ok: true,
717
+ status: 200,
718
+ data: {}
719
+ });
720
+ vi.mocked(provisionWorkspace).mockResolvedValueOnce({
721
+ ok: true,
722
+ status: 200,
723
+ data: {
724
+ success: true,
725
+ existing: true
726
+ }
727
+ });
728
+ const res = await ensureCloudWorkspaceReadyForSyncService(baseHandshakeInput);
729
+ expect(res.success).toBe(true);
730
+ expect(res.provisioned).toBe(true);
731
+ expect(provisionWorkspace).toHaveBeenCalledWith(mockResolveCloudAuthUrl, {
732
+ workspaceId: 'ws-test',
733
+ name: 'Test Workspace'
734
+ });
735
+ expect(handshakeWorkspace).toHaveBeenCalledTimes(2);
736
+ });
737
+ it('surfaces provisioning failure when recovering missing cloud workspace selection', async () => {
738
+ vi.mocked(handshakeWorkspace).mockResolvedValueOnce({
739
+ ok: false,
740
+ status: 400,
741
+ data: {
742
+ code: 'WORKSPACE_ID_REQUIRED',
743
+ error: 'No cloud workspace is selected. Complete workspace setup first.'
744
+ }
745
+ });
746
+ vi.mocked(provisionWorkspace).mockResolvedValueOnce({
747
+ ok: false,
748
+ status: 409,
749
+ data: {
750
+ code: 'WORKSPACE_ID_ALREADY_EXISTS',
751
+ error: 'Workspace ID already exists'
752
+ }
753
+ });
754
+ const res = await ensureCloudWorkspaceReadyForSyncService(baseHandshakeInput);
755
+ expect(res.success).toBe(false);
756
+ expect(res.statusCode).toBe(409);
757
+ expect(res.error).toContain('already linked to another cloud account');
758
+ });
705
759
  it('returns failure when cloudAuthConfigured is false', async () => {
706
760
  const res = await ensureCloudWorkspaceReadyForSyncService({
707
761
  ...baseHandshakeInput,
@@ -0,0 +1,2 @@
1
+ import type { TaskItem } from '../types.js';
2
+ export declare function buildTaskSyncChangeKey(task: Partial<TaskItem> | null | undefined, archivedValue: boolean): string;
@@ -0,0 +1,143 @@
1
+ import { serializeTaskForSync } from './taskSyncPayload.js';
2
+ import { normalizeSyncNullableId, normalizeSyncNullableIsoString, normalizeSyncNullableString, normalizeSyncOptionalId, normalizeSyncOptionalString, } from './syncFieldSemantics.js';
3
+ function normalizeConflictTaxonomies(value) {
4
+ if (!value || typeof value !== 'object' || Array.isArray(value))
5
+ return {};
6
+ const entries = Object.entries(value)
7
+ .filter(([key]) => String(key || '').trim().length > 0)
8
+ .sort(([left], [right]) => left.localeCompare(right));
9
+ const normalized = {};
10
+ for (const [key, raw] of entries) {
11
+ if (Array.isArray(raw)) {
12
+ normalized[key] = raw.map((entry) => String(entry ?? '')).sort((left, right) => left.localeCompare(right));
13
+ continue;
14
+ }
15
+ if (raw === undefined || raw === null)
16
+ continue;
17
+ normalized[key] = String(raw);
18
+ }
19
+ return normalized;
20
+ }
21
+ function normalizeConflictChecklistItems(value) {
22
+ if (!Array.isArray(value))
23
+ return [];
24
+ return value
25
+ .map((item) => {
26
+ const entry = item && typeof item === 'object' ? item : {};
27
+ return {
28
+ id: String(entry.id || '').trim(),
29
+ title: String(entry.title || '').trim(),
30
+ isCompleted: Boolean(entry.isCompleted),
31
+ order: entry.order === undefined || entry.order === null ? '' : String(entry.order),
32
+ };
33
+ })
34
+ .sort((left, right) => {
35
+ const leftOrder = Number.parseInt(left.order, 10);
36
+ const rightOrder = Number.parseInt(right.order, 10);
37
+ if (Number.isFinite(leftOrder) && Number.isFinite(rightOrder) && leftOrder !== rightOrder) {
38
+ return leftOrder - rightOrder;
39
+ }
40
+ if (left.title !== right.title)
41
+ return left.title.localeCompare(right.title);
42
+ return left.id.localeCompare(right.id);
43
+ });
44
+ }
45
+ function normalizeSyncComments(value) {
46
+ if (!Array.isArray(value))
47
+ return [];
48
+ return value
49
+ .map((entry) => {
50
+ const comment = entry && typeof entry === 'object' ? entry : null;
51
+ return {
52
+ id: String(comment?.id || '').trim(),
53
+ author: String(comment?.author || '').trim(),
54
+ submittedAuthor: normalizeSyncOptionalString(comment?.submittedAuthor) || '',
55
+ text: String(comment?.text || ''),
56
+ timestamp: normalizeSyncNullableIsoString(comment?.timestamp) || '',
57
+ actor: comment?.actor ?? null,
58
+ };
59
+ })
60
+ .sort((left, right) => {
61
+ if (left.timestamp !== right.timestamp)
62
+ return left.timestamp.localeCompare(right.timestamp);
63
+ if (left.id !== right.id)
64
+ return left.id.localeCompare(right.id);
65
+ if (left.author !== right.author)
66
+ return left.author.localeCompare(right.author);
67
+ return left.text.localeCompare(right.text);
68
+ });
69
+ }
70
+ function normalizeSyncAttachments(value) {
71
+ if (!Array.isArray(value))
72
+ return [];
73
+ return value
74
+ .map((entry) => {
75
+ if (typeof entry === 'string') {
76
+ const path = entry.trim();
77
+ return {
78
+ path,
79
+ fsPath: '',
80
+ caption: '',
81
+ displayName: '',
82
+ originalFilename: '',
83
+ assetId: '',
84
+ referenceNumber: '',
85
+ taskId: '',
86
+ timestamp: '',
87
+ };
88
+ }
89
+ const attachment = entry && typeof entry === 'object' ? entry : null;
90
+ return {
91
+ path: String(attachment?.path || '').trim(),
92
+ fsPath: normalizeSyncOptionalString(attachment?.fsPath) || '',
93
+ caption: normalizeSyncOptionalString(attachment?.caption) || '',
94
+ displayName: normalizeSyncOptionalString(attachment?.displayName) || '',
95
+ originalFilename: normalizeSyncOptionalString(attachment?.originalFilename) || '',
96
+ assetId: normalizeSyncOptionalId(attachment?.assetId) || '',
97
+ referenceNumber: attachment?.referenceNumber === undefined || attachment.referenceNumber === null
98
+ ? ''
99
+ : String(attachment.referenceNumber),
100
+ taskId: normalizeSyncNullableId(attachment?.taskId) || '',
101
+ timestamp: normalizeSyncNullableIsoString(attachment?.timestamp) || '',
102
+ };
103
+ })
104
+ .sort((left, right) => {
105
+ if (left.path !== right.path)
106
+ return left.path.localeCompare(right.path);
107
+ if (left.assetId !== right.assetId)
108
+ return left.assetId.localeCompare(right.assetId);
109
+ if (left.timestamp !== right.timestamp)
110
+ return left.timestamp.localeCompare(right.timestamp);
111
+ return JSON.stringify(left).localeCompare(JSON.stringify(right));
112
+ });
113
+ }
114
+ export function buildTaskSyncChangeKey(task, archivedValue) {
115
+ const source = serializeTaskForSync(task);
116
+ return JSON.stringify({
117
+ id: source.id,
118
+ title: source.title,
119
+ description: normalizeSyncNullableString(source.description) || '',
120
+ category: source.category,
121
+ type: source.type,
122
+ priority: source.priority === undefined || source.priority === null ? '' : String(source.priority),
123
+ complexity: source.complexity === undefined || source.complexity === null ? '' : String(source.complexity),
124
+ status: source.status,
125
+ approach: normalizeSyncOptionalString(source.approach) || '',
126
+ canceledReason: normalizeSyncNullableString(source.canceledReason) || '',
127
+ createdAt: source.createdAt,
128
+ completedAt: normalizeSyncNullableIsoString(source.completedAt) || '',
129
+ taxonomies: normalizeConflictTaxonomies(source.taxonomies),
130
+ createdBy: normalizeSyncOptionalId(source.createdBy) || '',
131
+ assignee: normalizeSyncOptionalId(source.assignee) || '',
132
+ scheduledDate: normalizeSyncNullableString(source.scheduledDate) || '',
133
+ dueDate: normalizeSyncNullableString(source.dueDate) || '',
134
+ scheduledWeekKey: normalizeSyncNullableString(source.scheduledWeekKey) || '',
135
+ orderInDay: source.orderInDay === undefined || source.orderInDay === null ? '' : String(source.orderInDay),
136
+ workstreamId: normalizeSyncNullableId(source.workstreamId) || '',
137
+ referenceNumber: source.referenceNumber === undefined || source.referenceNumber === null ? '' : String(source.referenceNumber),
138
+ comments: normalizeSyncComments(source.comments),
139
+ attachments: normalizeSyncAttachments(source.attachments),
140
+ checklistItems: normalizeConflictChecklistItems(source.checklistItems),
141
+ isArchived: archivedValue ? '1' : '0'
142
+ });
143
+ }
@@ -1,16 +1,11 @@
1
+ import { normalizeSyncNullableString, normalizeSyncOptionalString, } from './syncFieldSemantics.js';
1
2
  function toOptionalString(value) {
2
- if (value === undefined || value === null)
3
- return undefined;
4
- const normalized = String(value).trim();
5
- return normalized.length > 0 ? normalized : undefined;
3
+ return normalizeSyncOptionalString(value);
6
4
  }
7
5
  function toNullableString(value) {
8
6
  if (value === undefined)
9
7
  return undefined;
10
- if (value === null)
11
- return null;
12
- const normalized = String(value).trim();
13
- return normalized.length > 0 ? normalized : null;
8
+ return normalizeSyncNullableString(value);
14
9
  }
15
10
  function toOptionalBoolean(value) {
16
11
  return typeof value === 'boolean' ? value : undefined;
@@ -1,4 +1,5 @@
1
1
  import { serializeTaskForSync } from './taskSyncPayload.js';
2
+ import { aiProfileSyncResourceAdapter, taskSyncResourceAdapter } from './syncResourceAdapters.js';
2
3
  export async function buildWorkspacePullFeed(input) {
3
4
  const buildStartedAtMs = Date.now();
4
5
  const { core, workspaceId, cursor, pageSize, encodeWorkspaceSyncCursor, contentSyncState, collaborationSyncState } = input;
@@ -7,7 +8,7 @@ export async function buildWorkspacePullFeed(input) {
7
8
  const taskRows = core.listTasksForSync(workspaceId, syncCursor, dbFetchLimit);
8
9
  const upsertEntries = taskRows
9
10
  .map((entry) => {
10
- const watermark = String(entry.task?.updatedAt || entry.task?.createdAt || '').trim();
11
+ const watermark = taskSyncResourceAdapter.getWatermark(entry.task);
11
12
  const taskId = String(entry.task?.id || '').trim();
12
13
  return {
13
14
  kind: 'upsert',
@@ -55,10 +56,10 @@ export async function buildWorkspacePullFeed(input) {
55
56
  workstream
56
57
  }))
57
58
  .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.workstream.id.length > 0);
58
- const aiProfileEntries = core.listAiProfiles(workspaceId)
59
+ const aiProfileEntries = core.listAiProfiles(workspaceId, { includeArchived: true })
59
60
  .map((profile) => ({
60
61
  kind: 'ai-profile',
61
- watermark: String(profile.updatedAt || profile.createdAt || '').trim(),
62
+ watermark: aiProfileSyncResourceAdapter.getWatermark(profile),
62
63
  sortId: `ai-profile:${String(profile.id || '').trim()}`,
63
64
  profile
64
65
  }))
@@ -126,6 +126,7 @@ describe('workspacePullFeed', () => {
126
126
  userId: 'user-1',
127
127
  email: 'person@example.com',
128
128
  displayName: 'Person',
129
+ avatarUrl: '/api/taskforce/context/users%2Fuser-1%2Fprofile%2Favatar%2Fheadshot.webp',
129
130
  role: 'owner',
130
131
  permissionMode: 'read-write',
131
132
  betaAccess: true,
@@ -167,6 +168,48 @@ describe('workspacePullFeed', () => {
167
168
  displayName: 'Person',
168
169
  })
169
170
  ]);
171
+ expect(result.workspaceMembers[0]).not.toHaveProperty('avatarUrl');
172
+ });
173
+ it('includes ai-profile upserts with avatar fields in the pull feed', async () => {
174
+ const core = createCoreMock();
175
+ core.listAiProfiles = () => [
176
+ {
177
+ id: 'ai-profile-1',
178
+ workspaceId: 'workspace-1',
179
+ profileToken: 'tfp_test',
180
+ name: 'Codex',
181
+ username: 'codex',
182
+ icon: 'Bot',
183
+ color: '#0ea5e9',
184
+ avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Favatar.webp',
185
+ avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fai-profile-1%2Favatar%2Fsource.webp',
186
+ createdAt: '2026-04-10T10:00:00.000Z',
187
+ updatedAt: '2026-04-10T10:05:00.000Z',
188
+ lastActiveAt: '2026-04-10T10:05:00.000Z'
189
+ }
190
+ ];
191
+ const result = await buildWorkspacePullFeed({
192
+ core,
193
+ workspaceId: 'workspace-1',
194
+ cursor: null,
195
+ pageSize: 10,
196
+ encodeWorkspaceSyncCursor: encodeCursor,
197
+ contentSyncState: createContentSyncStateMock(),
198
+ collaborationSyncState: createCollaborationSyncStateMock(),
199
+ });
200
+ expect(result.changes).toEqual([
201
+ expect.objectContaining({
202
+ op: 'ai-profile-upsert',
203
+ watermark: '2026-04-10T10:05:00.000Z',
204
+ profile: expect.objectContaining({
205
+ id: 'ai-profile-1',
206
+ avatarUrl: expect.stringContaining('/api/taskforce/context/'),
207
+ avatarSourceUrl: expect.stringContaining('/api/taskforce/context/')
208
+ })
209
+ })
210
+ ]);
211
+ expect(result.nextCursor).toBe('2026-04-10T10:05:00.000Z|ai-profile:ai-profile-1');
212
+ expect(result.hasMore).toBe(false);
170
213
  });
171
214
  it('hydrates document and asset payloads only for entries included in the current page', async () => {
172
215
  const core = createCoreMock();