@taskforcehq/taskforce 0.3.168

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 (492) hide show
  1. package/README.md +241 -0
  2. package/dist/Taskforce.d.ts +3 -0
  3. package/dist/Taskforce.js +3 -0
  4. package/dist/Taskforce.module.css +7587 -0
  5. package/dist/TaskforceCore.CategoryFallback.test.d.ts +1 -0
  6. package/dist/TaskforceCore.CategoryFallback.test.js +98 -0
  7. package/dist/TaskforceCore.d.ts +12 -0
  8. package/dist/TaskforceCore.js +729 -0
  9. package/dist/TaskforceCore.test.d.ts +1 -0
  10. package/dist/TaskforceCore.test.js +2132 -0
  11. package/dist/TaskforceScreenshot.d.ts +11 -0
  12. package/dist/TaskforceScreenshot.js +113 -0
  13. package/dist/TaskforceWidget.d.ts +6 -0
  14. package/dist/TaskforceWidget.js +101 -0
  15. package/dist/TaskforceWidget.test.d.ts +1 -0
  16. package/dist/TaskforceWidget.test.js +92 -0
  17. package/dist/UnsavedChangesRelaxed.test.d.ts +1 -0
  18. package/dist/UnsavedChangesRelaxed.test.js +16 -0
  19. package/dist/__tests__/localizationCatalogs.test.d.ts +1 -0
  20. package/dist/__tests__/localizationCatalogs.test.js +37 -0
  21. package/dist/__tests__/planParser.test.d.ts +1 -0
  22. package/dist/__tests__/planParser.test.js +183 -0
  23. package/dist/__tests__/releaseVersion.test.d.ts +1 -0
  24. package/dist/__tests__/releaseVersion.test.js +32 -0
  25. package/dist/__tests__/structuredCommentValidation.test.d.ts +1 -0
  26. package/dist/__tests__/structuredCommentValidation.test.js +31 -0
  27. package/dist/__tests__/structuredDocValidation.test.d.ts +1 -0
  28. package/dist/__tests__/structuredDocValidation.test.js +30 -0
  29. package/dist/__tests__/templateEngine.test.d.ts +1 -0
  30. package/dist/__tests__/templateEngine.test.js +150 -0
  31. package/dist/__tests__/templates.test.d.ts +1 -0
  32. package/dist/__tests__/templates.test.js +45 -0
  33. package/dist/__tests__/workflowDocumentFixtures.test.d.ts +1 -0
  34. package/dist/__tests__/workflowDocumentFixtures.test.js +78 -0
  35. package/dist/__tests__/workflowExportMatrix.test.d.ts +1 -0
  36. package/dist/__tests__/workflowExportMatrix.test.js +113 -0
  37. package/dist/__tests__/workflowExportSnapshots.test.d.ts +1 -0
  38. package/dist/__tests__/workflowExportSnapshots.test.js +28 -0
  39. package/dist/__tests__/workflowGeneration.test.d.ts +1 -0
  40. package/dist/__tests__/workflowGeneration.test.js +12 -0
  41. package/dist/__tests__/workflowSimulation.test.d.ts +1 -0
  42. package/dist/__tests__/workflowSimulation.test.js +113 -0
  43. package/dist/__tests__/workflowV2Policy.test.d.ts +1 -0
  44. package/dist/__tests__/workflowV2Policy.test.js +80 -0
  45. package/dist/api.d.ts +30 -0
  46. package/dist/api.js +85 -0
  47. package/dist/cli.d.ts +2 -0
  48. package/dist/cli.js +217 -0
  49. package/dist/compat/priorityCompat.d.ts +2 -0
  50. package/dist/compat/priorityCompat.js +24 -0
  51. package/dist/components/features/DocumentGenerateModal.d.ts +9 -0
  52. package/dist/components/features/DocumentGenerateModal.js +114 -0
  53. package/dist/components/features/DocumentIndex.d.ts +19 -0
  54. package/dist/components/features/DocumentIndex.js +51 -0
  55. package/dist/components/features/DocumentViewer.d.ts +9 -0
  56. package/dist/components/features/DocumentViewer.js +253 -0
  57. package/dist/components/features/DocumentWorkspace.d.ts +21 -0
  58. package/dist/components/features/DocumentWorkspace.js +88 -0
  59. package/dist/components/features/TaskSettings.d.ts +209 -0
  60. package/dist/components/features/TaskSettings.js +500 -0
  61. package/dist/components/features/TaskSettings.test.d.ts +1 -0
  62. package/dist/components/features/TaskSettings.test.js +308 -0
  63. package/dist/components/features/settings/CategoryManager.d.ts +19 -0
  64. package/dist/components/features/settings/CategoryManager.js +113 -0
  65. package/dist/components/features/settings/TaxonomyEditor.d.ts +8 -0
  66. package/dist/components/features/settings/TaxonomyEditor.js +98 -0
  67. package/dist/components/features/settings/TaxonomyManager 2.d.ts +52 -0
  68. package/dist/components/features/settings/TaxonomyManager 2.js +75 -0
  69. package/dist/components/features/settings/TaxonomyManager.d.ts +54 -0
  70. package/dist/components/features/settings/TaxonomyManager.js +77 -0
  71. package/dist/components/features/settings/TypeManager.d.ts +10 -0
  72. package/dist/components/features/settings/TypeManager.js +50 -0
  73. package/dist/components/features/settings/WorkflowEditorPrototype.d.ts +15 -0
  74. package/dist/components/features/settings/WorkflowEditorPrototype.js +245 -0
  75. package/dist/components/task/TaskActionHeader.d.ts +20 -0
  76. package/dist/components/task/TaskActionHeader.js +13 -0
  77. package/dist/components/task/TaskCard.d.ts +47 -0
  78. package/dist/components/task/TaskCard.js +326 -0
  79. package/dist/components/task/TaskCard.test.d.ts +1 -0
  80. package/dist/components/task/TaskCard.test.js +95 -0
  81. package/dist/components/task/TaskContextUpload.d.ts +12 -0
  82. package/dist/components/task/TaskContextUpload.js +393 -0
  83. package/dist/components/task/TaskContextUpload.test.d.ts +1 -0
  84. package/dist/components/task/TaskContextUpload.test.js +189 -0
  85. package/dist/components/task/TaskForm.d.ts +105 -0
  86. package/dist/components/task/TaskForm.js +241 -0
  87. package/dist/components/task/TaskForm.test.d.ts +1 -0
  88. package/dist/components/task/TaskForm.test.js +288 -0
  89. package/dist/components/task/TaskKanban 2.d.ts +76 -0
  90. package/dist/components/task/TaskKanban 2.js +1004 -0
  91. package/dist/components/task/TaskKanban.d.ts +76 -0
  92. package/dist/components/task/TaskKanban.empty-column-drop.test.d.ts +1 -0
  93. package/dist/components/task/TaskKanban.empty-column-drop.test.js +66 -0
  94. package/dist/components/task/TaskKanban.js +1026 -0
  95. package/dist/components/task/TaskKanban.test.d.ts +1 -0
  96. package/dist/components/task/TaskKanban.test.js +46 -0
  97. package/dist/components/task/TaskList.d.ts +74 -0
  98. package/dist/components/task/TaskList.js +38 -0
  99. package/dist/components/task/TaskList.test.d.ts +1 -0
  100. package/dist/components/task/TaskList.test.js +270 -0
  101. package/dist/components/task/TaskScreenshotSection.d.ts +15 -0
  102. package/dist/components/task/TaskScreenshotSection.js +22 -0
  103. package/dist/components/ui/LevelSelector.d.ts +13 -0
  104. package/dist/components/ui/LevelSelector.js +25 -0
  105. package/dist/components/ui/LevelSelector.test.d.ts +1 -0
  106. package/dist/components/ui/LevelSelector.test.js +55 -0
  107. package/dist/components/ui/Markdown.d.ts +6 -0
  108. package/dist/components/ui/Markdown.js +94 -0
  109. package/dist/components/ui/Markdown.test.d.ts +1 -0
  110. package/dist/components/ui/Markdown.test.js +38 -0
  111. package/dist/components/ui/Modal.d.ts +16 -0
  112. package/dist/components/ui/Modal.js +49 -0
  113. package/dist/components/ui/MultiSelectFilter.d.ts +10 -0
  114. package/dist/components/ui/MultiSelectFilter.js +71 -0
  115. package/dist/components/ui/MultiSelectFilter.test.d.ts +1 -0
  116. package/dist/components/ui/MultiSelectFilter.test.js +74 -0
  117. package/dist/components/ui/TaxonomyDropdown.d.ts +17 -0
  118. package/dist/components/ui/TaxonomyDropdown.js +145 -0
  119. package/dist/components/ui/TaxonomyDropdown.test.d.ts +1 -0
  120. package/dist/components/ui/TaxonomyDropdown.test.js +130 -0
  121. package/dist/components/views/StandaloneLayout 2.d.ts +8 -0
  122. package/dist/components/views/StandaloneLayout 2.js +1660 -0
  123. package/dist/components/views/StandaloneLayout.d.ts +8 -0
  124. package/dist/components/views/StandaloneLayout.js +2117 -0
  125. package/dist/components/views/WidgetView.d.ts +8 -0
  126. package/dist/components/views/WidgetView.js +221 -0
  127. package/dist/config/envSchema.d.ts +28 -0
  128. package/dist/config/envSchema.js +374 -0
  129. package/dist/config/envSchema.test.d.ts +1 -0
  130. package/dist/config/envSchema.test.js +97 -0
  131. package/dist/config/surfaceBase.d.ts +2 -0
  132. package/dist/config/surfaceBase.js +8 -0
  133. package/dist/core/BulkUpdateFieldsPreview.test.d.ts +1 -0
  134. package/dist/core/BulkUpdateFieldsPreview.test.js +62 -0
  135. package/dist/core/CompletedAt.test.d.ts +1 -0
  136. package/dist/core/CompletedAt.test.js +44 -0
  137. package/dist/core/CreatedBy.test.d.ts +1 -0
  138. package/dist/core/CreatedBy.test.js +60 -0
  139. package/dist/core/EntitlementsPolicy.test.d.ts +1 -0
  140. package/dist/core/EntitlementsPolicy.test.js +145 -0
  141. package/dist/core/InitiativeTemplates.test.d.ts +1 -0
  142. package/dist/core/InitiativeTemplates.test.js +50 -0
  143. package/dist/core/ParentLifecycle.test.d.ts +1 -0
  144. package/dist/core/ParentLifecycle.test.js +98 -0
  145. package/dist/core/ScheduleFields.test.d.ts +1 -0
  146. package/dist/core/ScheduleFields.test.js +197 -0
  147. package/dist/core/SetupState.test.d.ts +1 -0
  148. package/dist/core/SetupState.test.js +113 -0
  149. package/dist/core/SystemAdmin.test.d.ts +1 -0
  150. package/dist/core/SystemAdmin.test.js +295 -0
  151. package/dist/core/TaskTaxonomyValidation.test.d.ts +1 -0
  152. package/dist/core/TaskTaxonomyValidation.test.js +64 -0
  153. package/dist/core/Taskforce.d.ts +1063 -0
  154. package/dist/core/Taskforce.ids.test.d.ts +1 -0
  155. package/dist/core/Taskforce.ids.test.js +168 -0
  156. package/dist/core/Taskforce.js +5981 -0
  157. package/dist/core/Taskforce.realtimeMutation.test.d.ts +1 -0
  158. package/dist/core/Taskforce.realtimeMutation.test.js +76 -0
  159. package/dist/core/WorkspaceDelete.test.d.ts +1 -0
  160. package/dist/core/WorkspaceDelete.test.js +140 -0
  161. package/dist/core/WorkspacePermissions.test.d.ts +1 -0
  162. package/dist/core/WorkspacePermissions.test.js +151 -0
  163. package/dist/core/WorkspacePlanMode.test.d.ts +1 -0
  164. package/dist/core/WorkspacePlanMode.test.js +104 -0
  165. package/dist/hooks/authGuardPolicy.d.ts +26 -0
  166. package/dist/hooks/authGuardPolicy.js +32 -0
  167. package/dist/hooks/authGuardPolicy.test.d.ts +1 -0
  168. package/dist/hooks/authGuardPolicy.test.js +76 -0
  169. package/dist/hooks/useDraggable.d.ts +12 -0
  170. package/dist/hooks/useDraggable.js +65 -0
  171. package/dist/hooks/useRealtimeSync.d.ts +24 -0
  172. package/dist/hooks/useRealtimeSync.js +275 -0
  173. package/dist/hooks/useRealtimeSync.test.d.ts +1 -0
  174. package/dist/hooks/useRealtimeSync.test.js +284 -0
  175. package/dist/hooks/useSyncOrchestrator.d.ts +164 -0
  176. package/dist/hooks/useSyncOrchestrator.js +1427 -0
  177. package/dist/hooks/useSyncOrchestrator.retry-closure.test.d.ts +1 -0
  178. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +367 -0
  179. package/dist/hooks/useTaskforce.d.ts +727 -0
  180. package/dist/hooks/useTaskforce.js +4396 -0
  181. package/dist/hooks/useTaskforce.priority-filters.test.d.ts +1 -0
  182. package/dist/hooks/useTaskforce.priority-filters.test.js +33 -0
  183. package/dist/hooks/useWorkspaceSyncController.d.ts +80 -0
  184. package/dist/hooks/useWorkspaceSyncController.js +598 -0
  185. package/dist/hooks/useWorkspaceSyncController.test.d.ts +1 -0
  186. package/dist/hooks/useWorkspaceSyncController.test.js +236 -0
  187. package/dist/index.d.ts +12 -0
  188. package/dist/index.js +11 -0
  189. package/dist/localization/index.d.ts +8 -0
  190. package/dist/localization/index.js +81 -0
  191. package/dist/localization/locales/en-US.d.ts +151 -0
  192. package/dist/localization/locales/en-US.js +150 -0
  193. package/dist/localization/locales/es-419.d.ts +2 -0
  194. package/dist/localization/locales/es-419.js +150 -0
  195. package/dist/localization/locales/pt-BR.d.ts +2 -0
  196. package/dist/localization/locales/pt-BR.js +150 -0
  197. package/dist/main.d.ts +1 -0
  198. package/dist/main.js +33 -0
  199. package/dist/mcp/optionResolution.d.ts +14 -0
  200. package/dist/mcp/optionResolution.js +126 -0
  201. package/dist/mcp/optionResolution.test.d.ts +1 -0
  202. package/dist/mcp/optionResolution.test.js +39 -0
  203. package/dist/mcp/server.d.ts +1 -0
  204. package/dist/mcp/server.js +1793 -0
  205. package/dist/mcp/structuredCommentValidation.d.ts +6 -0
  206. package/dist/mcp/structuredCommentValidation.js +56 -0
  207. package/dist/mcp/structuredDocValidation.d.ts +6 -0
  208. package/dist/mcp/structuredDocValidation.js +75 -0
  209. package/dist/migrations/billingSchemaParity.test.d.ts +1 -0
  210. package/dist/migrations/billingSchemaParity.test.js +46 -0
  211. package/dist/migrations/clientMigrations.d.ts +9 -0
  212. package/dist/migrations/clientMigrations.js +74 -0
  213. package/dist/migrations/flags.d.ts +6 -0
  214. package/dist/migrations/flags.js +8 -0
  215. package/dist/migrations/taskMigrations.d.ts +71 -0
  216. package/dist/migrations/taskMigrations.js +236 -0
  217. package/dist/migrations/taskMigrations.test.d.ts +1 -0
  218. package/dist/migrations/taskMigrations.test.js +34 -0
  219. package/dist/migrations/taskSchemaMigrations.d.ts +11 -0
  220. package/dist/migrations/taskSchemaMigrations.js +453 -0
  221. package/dist/plugins/index.d.ts +10 -0
  222. package/dist/plugins/index.js +10 -0
  223. package/dist/plugins/vite.d.ts +19 -0
  224. package/dist/plugins/vite.js +271 -0
  225. package/dist/public/android-chrome-192x192.png +0 -0
  226. package/dist/public/android-chrome-512x512.png +0 -0
  227. package/dist/public/apple-touch-icon.png +0 -0
  228. package/dist/public/favicon-16x16.png +0 -0
  229. package/dist/public/favicon-32x32.png +0 -0
  230. package/dist/public/favicon.ico +0 -0
  231. package/dist/public/favicon.png +0 -0
  232. package/dist/public/site.webmanifest +1 -0
  233. package/dist/public/taskforce.png +0 -0
  234. package/dist/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  235. package/dist/resources/templates/environments/aider.yaml +14 -0
  236. package/dist/resources/templates/environments/antigravity.yaml +10 -0
  237. package/dist/resources/templates/environments/claude.yaml +10 -0
  238. package/dist/resources/templates/environments/cline.yaml +10 -0
  239. package/dist/resources/templates/environments/codex.yaml +13 -0
  240. package/dist/resources/templates/environments/copilot.yaml +10 -0
  241. package/dist/resources/templates/environments/cursor.yaml +9 -0
  242. package/dist/resources/templates/environments/gemini.yaml +9 -0
  243. package/dist/resources/templates/environments/windsurf.yaml +10 -0
  244. package/dist/resources/templates/workflow-sources/README.md +9 -0
  245. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  246. package/dist/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  247. package/dist/resources/templates/workflows/collaborate.yaml +104 -0
  248. package/dist/resources/templates/workflows/do.yaml +82 -0
  249. package/dist/resources/templates/workflows/evaluate.yaml +111 -0
  250. package/dist/resources/templates/workflows/execute.yaml +142 -0
  251. package/dist/resources/templates/workflows/plan.yaml +112 -0
  252. package/dist/resources/templates/workflows/review.yaml +109 -0
  253. package/dist/runtime/capabilities.d.ts +8 -0
  254. package/dist/runtime/capabilities.js +16 -0
  255. package/dist/security/virusScan.d.ts +16 -0
  256. package/dist/security/virusScan.js +83 -0
  257. package/dist/server/auth.d.ts +51 -0
  258. package/dist/server/auth.js +266 -0
  259. package/dist/server/auth.test.d.ts +1 -0
  260. package/dist/server/auth.test.js +111 -0
  261. package/dist/server/buildInfo.d.ts +7 -0
  262. package/dist/server/buildInfo.js +100 -0
  263. package/dist/server/buildInfo.test.d.ts +1 -0
  264. package/dist/server/buildInfo.test.js +29 -0
  265. package/dist/server/cors.d.ts +7 -0
  266. package/dist/server/cors.js +76 -0
  267. package/dist/server/cors.test.d.ts +1 -0
  268. package/dist/server/cors.test.js +71 -0
  269. package/dist/server/email.d.ts +28 -0
  270. package/dist/server/email.js +138 -0
  271. package/dist/server/email.test.d.ts +1 -0
  272. package/dist/server/email.test.js +113 -0
  273. package/dist/server/index.cookieProxy.test.d.ts +1 -0
  274. package/dist/server/index.cookieProxy.test.js +19 -0
  275. package/dist/server/index.d.ts +17 -0
  276. package/dist/server/index.js +757 -0
  277. package/dist/server/index.rateLimit.test.d.ts +1 -0
  278. package/dist/server/index.rateLimit.test.js +13 -0
  279. package/dist/server/index.test.d.ts +1 -0
  280. package/dist/server/index.test.js +19 -0
  281. package/dist/server/rateLimit.d.ts +29 -0
  282. package/dist/server/rateLimit.js +99 -0
  283. package/dist/server/rateLimit.test.d.ts +1 -0
  284. package/dist/server/rateLimit.test.js +107 -0
  285. package/dist/server/realtimeSync.auth.test.d.ts +1 -0
  286. package/dist/server/realtimeSync.auth.test.js +62 -0
  287. package/dist/server/realtimeSync.replay.test.d.ts +1 -0
  288. package/dist/server/realtimeSync.replay.test.js +192 -0
  289. package/dist/server/realtimeSync.workspaceIsolation.test.d.ts +1 -0
  290. package/dist/server/realtimeSync.workspaceIsolation.test.js +101 -0
  291. package/dist/server/realtimeSyncWs.d.ts +30 -0
  292. package/dist/server/realtimeSyncWs.js +257 -0
  293. package/dist/server/routes.billing.d.ts +15 -0
  294. package/dist/server/routes.billing.js +991 -0
  295. package/dist/server/routes.billing.test.d.ts +1 -0
  296. package/dist/server/routes.billing.test.js +387 -0
  297. package/dist/server/routes.d.ts +72 -0
  298. package/dist/server/routes.js +4820 -0
  299. package/dist/server/routes.sync.d.ts +17 -0
  300. package/dist/server/routes.sync.integration.test.d.ts +1 -0
  301. package/dist/server/routes.sync.integration.test.js +470 -0
  302. package/dist/server/routes.sync.js +1141 -0
  303. package/dist/server/routes.test.d.ts +1 -0
  304. package/dist/server/routes.test.js +4945 -0
  305. package/dist/server/runtimeMode.d.ts +4 -0
  306. package/dist/server/runtimeMode.js +25 -0
  307. package/dist/server/runtimeMode.test.d.ts +1 -0
  308. package/dist/server/runtimeMode.test.js +26 -0
  309. package/dist/server/securityHeaders.d.ts +3 -0
  310. package/dist/server/securityHeaders.js +25 -0
  311. package/dist/server/securityHeaders.test.d.ts +1 -0
  312. package/dist/server/securityHeaders.test.js +32 -0
  313. package/dist/server/smtpTransport.d.ts +21 -0
  314. package/dist/server/smtpTransport.js +11 -0
  315. package/dist/server/start.d.ts +1 -0
  316. package/dist/server/start.js +17 -0
  317. package/dist/services/formatTransformers.d.ts +29 -0
  318. package/dist/services/formatTransformers.js +56 -0
  319. package/dist/services/templateEngine.d.ts +46 -0
  320. package/dist/services/templateEngine.js +148 -0
  321. package/dist/shared/runtimeContract.d.ts +12 -0
  322. package/dist/shared/runtimeContract.js +23 -0
  323. package/dist/shared/runtimeContract.test.d.ts +1 -0
  324. package/dist/shared/runtimeContract.test.js +23 -0
  325. package/dist/soak/preflight.cli.d.ts +1 -0
  326. package/dist/soak/preflight.cli.js +28 -0
  327. package/dist/soak/preflight.d.ts +26 -0
  328. package/dist/soak/preflight.js +113 -0
  329. package/dist/soak/preflight.test.d.ts +1 -0
  330. package/dist/soak/preflight.test.js +39 -0
  331. package/dist/soak/syncSoakHarness.d.ts +6 -0
  332. package/dist/soak/syncSoakHarness.js +10 -0
  333. package/dist/storage/createDatabaseAdapter.d.ts +2 -0
  334. package/dist/storage/createDatabaseAdapter.js +9 -0
  335. package/dist/storage/databaseAdapter.d.ts +22 -0
  336. package/dist/storage/databaseAdapter.js +1 -0
  337. package/dist/storage/documentIntegrity.d.ts +28 -0
  338. package/dist/storage/documentIntegrity.js +103 -0
  339. package/dist/storage/documentPurge.d.ts +14 -0
  340. package/dist/storage/documentPurge.js +46 -0
  341. package/dist/storage/documentRegistry.d.ts +39 -0
  342. package/dist/storage/documentRegistry.js +115 -0
  343. package/dist/storage/integritySchedule.d.ts +17 -0
  344. package/dist/storage/integritySchedule.js +62 -0
  345. package/dist/storage/objectStorage.d.ts +20 -0
  346. package/dist/storage/objectStorage.js +15 -0
  347. package/dist/storage/objectStorageClient.d.ts +18 -0
  348. package/dist/storage/objectStorageClient.js +301 -0
  349. package/dist/storage/objectStorageConfig.d.ts +2 -0
  350. package/dist/storage/objectStorageConfig.js +45 -0
  351. package/dist/storage/postgresAdapter.d.ts +20 -0
  352. package/dist/storage/postgresAdapter.js +204 -0
  353. package/dist/storage/postgresWorker.d.ts +1 -0
  354. package/dist/storage/postgresWorker.js +110 -0
  355. package/dist/storage/providerConfig.d.ts +6 -0
  356. package/dist/storage/providerConfig.js +11 -0
  357. package/dist/storage/sqliteAdapter.d.ts +14 -0
  358. package/dist/storage/sqliteAdapter.js +28 -0
  359. package/dist/storage/taskAssetStore.d.ts +56 -0
  360. package/dist/storage/taskAssetStore.js +297 -0
  361. package/dist/sync/cloudSyncApi.d.ts +47 -0
  362. package/dist/sync/cloudSyncApi.js +138 -0
  363. package/dist/sync/cloudSyncApi.test.d.ts +1 -0
  364. package/dist/sync/cloudSyncApi.test.js +54 -0
  365. package/dist/sync/encryptionService.d.ts +11 -0
  366. package/dist/sync/encryptionService.js +61 -0
  367. package/dist/sync/encryptionService.test.d.ts +1 -0
  368. package/dist/sync/encryptionService.test.js +52 -0
  369. package/dist/sync/realtimeFeature.d.ts +6 -0
  370. package/dist/sync/realtimeFeature.js +20 -0
  371. package/dist/sync/realtimeFeature.test.d.ts +1 -0
  372. package/dist/sync/realtimeFeature.test.js +30 -0
  373. package/dist/sync/realtimeReconcile.d.ts +30 -0
  374. package/dist/sync/realtimeReconcile.js +90 -0
  375. package/dist/sync/realtimeReconcile.test.d.ts +1 -0
  376. package/dist/sync/realtimeReconcile.test.js +43 -0
  377. package/dist/sync/syncService.d.ts +96 -0
  378. package/dist/sync/syncService.js +423 -0
  379. package/dist/sync/syncService.test.d.ts +1 -0
  380. package/dist/sync/syncService.test.js +484 -0
  381. package/dist/sync/workspaceSyncModel.d.ts +61 -0
  382. package/dist/sync/workspaceSyncModel.js +136 -0
  383. package/dist/sync/workspaceSyncModel.test.d.ts +1 -0
  384. package/dist/sync/workspaceSyncModel.test.js +73 -0
  385. package/dist/test/setup.d.ts +1 -0
  386. package/dist/test/setup.js +15 -0
  387. package/dist/types.d.ts +157 -0
  388. package/dist/types.js +41 -0
  389. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  390. package/dist/ui/assets/index-0W5vyCpn.css +1 -0
  391. package/dist/ui/assets/index-MCZuNNfn.js +23 -0
  392. package/dist/ui/assets/logo_white_01-0Vx-iIIv.png +0 -0
  393. package/dist/ui/assets/taskforce-BxLPokNB.png +0 -0
  394. package/dist/ui/assets/vendor-dnd-BI1K77C1.js +5 -0
  395. package/dist/ui/assets/vendor-icons-ZFG3SGwo.js +1 -0
  396. package/dist/ui/assets/vendor-markdown-CKYkQHfj.js +29 -0
  397. package/dist/ui/assets/vendor-react-DdIDsh7f.js +9 -0
  398. package/dist/ui/assets/vendor-router-BtiNU7Dl.js +3 -0
  399. package/dist/ui/branding/logos/logo_black_01.png +0 -0
  400. package/dist/ui/branding/logos/logo_black_02.png +0 -0
  401. package/dist/ui/branding/logos/logo_white_01.png +0 -0
  402. package/dist/ui/branding/logos/logo_white_02.png +0 -0
  403. package/dist/ui/favicon/android-chrome-192x192.png +0 -0
  404. package/dist/ui/favicon/android-chrome-512x512.png +0 -0
  405. package/dist/ui/favicon/apple-touch-icon.png +0 -0
  406. package/dist/ui/favicon/favicon-16x16.png +0 -0
  407. package/dist/ui/favicon/favicon-32x32.png +0 -0
  408. package/dist/ui/favicon/favicon.ico +0 -0
  409. package/dist/ui/favicon/favicon.png +0 -0
  410. package/dist/ui/favicon/site.webmanifest +1 -0
  411. package/dist/ui/fonts/RussoOne-Regular.ttf +0 -0
  412. package/dist/ui/images/taskforce.png +0 -0
  413. package/dist/ui/index.html +19 -0
  414. package/dist/utils/checklistMarkdown.d.ts +9 -0
  415. package/dist/utils/checklistMarkdown.js +46 -0
  416. package/dist/utils/constants.d.ts +21 -0
  417. package/dist/utils/constants.icons.test.d.ts +1 -0
  418. package/dist/utils/constants.icons.test.js +40 -0
  419. package/dist/utils/constants.js +100 -0
  420. package/dist/utils/contextFiles.d.ts +8 -0
  421. package/dist/utils/contextFiles.js +54 -0
  422. package/dist/utils/formatting.d.ts +5 -0
  423. package/dist/utils/formatting.js +30 -0
  424. package/dist/utils/planParser.d.ts +22 -0
  425. package/dist/utils/planParser.js +151 -0
  426. package/dist/utils/taskDragZones.d.ts +8 -0
  427. package/dist/utils/taskDragZones.js +14 -0
  428. package/dist/utils/taskDragZones.test.d.ts +1 -0
  429. package/dist/utils/taskDragZones.test.js +14 -0
  430. package/dist/utils/taskHierarchy.d.ts +4 -0
  431. package/dist/utils/taskHierarchy.js +69 -0
  432. package/dist/utils/taskHierarchy.test.d.ts +1 -0
  433. package/dist/utils/taskHierarchy.test.js +38 -0
  434. package/dist/utils/taskParenting.d.ts +3 -0
  435. package/dist/utils/taskParenting.js +24 -0
  436. package/dist/utils/taskParenting.test.d.ts +1 -0
  437. package/dist/utils/taskParenting.test.js +21 -0
  438. package/dist/utils/taskRelations.d.ts +6 -0
  439. package/dist/utils/taskRelations.js +24 -0
  440. package/dist/utils/taskRelations.test.d.ts +1 -0
  441. package/dist/utils/taskRelations.test.js +34 -0
  442. package/dist/utils/taskUtils.d.ts +2 -0
  443. package/dist/utils/taskUtils.js +20 -0
  444. package/dist/utils/taskforceApiClient.d.ts +7 -0
  445. package/dist/utils/taskforceApiClient.js +42 -0
  446. package/dist/utils/workspaceIdentity.d.ts +8 -0
  447. package/dist/utils/workspaceIdentity.js +47 -0
  448. package/dist/utils/workspaceIdentity.test.d.ts +1 -0
  449. package/dist/utils/workspaceIdentity.test.js +32 -0
  450. package/dist/utils/workspaceLifecycle.d.ts +26 -0
  451. package/dist/utils/workspaceLifecycle.js +69 -0
  452. package/dist/utils/workspaceLifecycle.test.d.ts +1 -0
  453. package/dist/utils/workspaceLifecycle.test.js +64 -0
  454. package/package.json +133 -0
  455. package/scripts/check-compat-boundaries.js +48 -0
  456. package/scripts/check-env.ts +24 -0
  457. package/scripts/check-localization-strings.mjs +49 -0
  458. package/scripts/check-runtime-boundaries.mjs +44 -0
  459. package/scripts/debug-global.mjs +34 -0
  460. package/scripts/debug-server.mjs +36 -0
  461. package/scripts/export-sqlite-to-postgres.mjs +125 -0
  462. package/scripts/generate-env-example.ts +8 -0
  463. package/scripts/generate-workflow-templates.mjs +134 -0
  464. package/scripts/mcp-stdio-adapter.mjs +160 -0
  465. package/scripts/postinstall.js +60 -0
  466. package/scripts/release-version-lib.mjs +88 -0
  467. package/scripts/release-version.mjs +72 -0
  468. package/scripts/run-dev-proxy.sh +49 -0
  469. package/scripts/run-e2e-realtime.sh +16 -0
  470. package/scripts/run-smoke.sh +23 -0
  471. package/scripts/run-soak.sh +29 -0
  472. package/scripts/test-global.js +143 -0
  473. package/scripts/update-homebrew-formula.sh +38 -0
  474. package/src/resources/templates/WORKFLOW_V2_POLICY.md +55 -0
  475. package/src/resources/templates/environments/aider.yaml +14 -0
  476. package/src/resources/templates/environments/antigravity.yaml +10 -0
  477. package/src/resources/templates/environments/claude.yaml +10 -0
  478. package/src/resources/templates/environments/cline.yaml +10 -0
  479. package/src/resources/templates/environments/codex.yaml +13 -0
  480. package/src/resources/templates/environments/copilot.yaml +10 -0
  481. package/src/resources/templates/environments/cursor.yaml +9 -0
  482. package/src/resources/templates/environments/gemini.yaml +9 -0
  483. package/src/resources/templates/environments/windsurf.yaml +10 -0
  484. package/src/resources/templates/workflow-sources/README.md +9 -0
  485. package/src/resources/templates/workflow-sources/workflowDocs.mjs +520 -0
  486. package/src/resources/templates/workflow-sources/workflowEditorOverrides.json +1 -0
  487. package/src/resources/templates/workflows/collaborate.yaml +104 -0
  488. package/src/resources/templates/workflows/do.yaml +82 -0
  489. package/src/resources/templates/workflows/evaluate.yaml +111 -0
  490. package/src/resources/templates/workflows/execute.yaml +142 -0
  491. package/src/resources/templates/workflows/plan.yaml +112 -0
  492. package/src/resources/templates/workflows/review.yaml +109 -0
@@ -0,0 +1,1141 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { parseJsonBody } from './routes.js';
5
+ import { normalizeTaskDataRelativePath } from '../storage/objectStorageClient.js';
6
+ import { resolveSessionCookieAttributesForRequest } from './auth.js';
7
+ function decodeWorkspaceSyncCursor(raw) {
8
+ const value = String(raw || '').trim();
9
+ if (!value)
10
+ return null;
11
+ try {
12
+ const json = Buffer.from(value, 'base64url').toString('utf8');
13
+ const parsed = JSON.parse(json);
14
+ const u = String(parsed?.u || '').trim();
15
+ const id = String(parsed?.id || '').trim();
16
+ if (!u || !id)
17
+ return null;
18
+ if (!Number.isFinite(Date.parse(u)))
19
+ return null;
20
+ return { u, id };
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function encodeWorkspaceSyncCursor(cursor) {
27
+ if (!cursor)
28
+ return null;
29
+ return Buffer.from(JSON.stringify(cursor), 'utf8').toString('base64url');
30
+ }
31
+ export function registerSyncRoutes(deps) {
32
+ const { addRoute, core, context, auditAuthAction, resolveErrorStatusCode, requestWorkspaceId, resolveRequestAccess, isReservedWorkspaceId, createSessionTokenCookie } = deps;
33
+ const readPushIdempotency = (workspaceId, key) => {
34
+ if (typeof core.readPushIdempotency === 'function') {
35
+ return core.readPushIdempotency(workspaceId, key);
36
+ }
37
+ return null;
38
+ };
39
+ const writePushIdempotency = (workspaceId, key, payload) => {
40
+ if (typeof core.writePushIdempotency === 'function') {
41
+ core.writePushIdempotency(workspaceId, key, payload);
42
+ }
43
+ };
44
+ const recordSyncDiagnostic = (workspaceId, eventType, errorMessage, details) => {
45
+ try {
46
+ core.recordSyncEvent(workspaceId, {
47
+ eventType,
48
+ status: 'error',
49
+ errorMessage,
50
+ requestMs: 0
51
+ });
52
+ }
53
+ catch {
54
+ // diagnostics must never throw
55
+ }
56
+ if (details && Object.keys(details).length > 0) {
57
+ // eslint-disable-next-line no-console
58
+ console.warn('[Taskforce Sync Diagnostic]', { workspaceId, eventType, errorMessage, ...details });
59
+ }
60
+ };
61
+ const workspaceDocumentSnapshotCache = new Map();
62
+ const workspaceTaxonomySyncMeta = new Map();
63
+ const isMarkdownPath = (relativePath) => {
64
+ const lower = relativePath.toLowerCase();
65
+ return lower.endsWith('.md') || lower.endsWith('.markdown');
66
+ };
67
+ const normalizeWorkspaceSyncDocumentPath = (rawPath) => {
68
+ const value = String(rawPath || '').trim().replace(/\\/g, '/');
69
+ if (!value || value.includes('\0'))
70
+ return null;
71
+ if (value.startsWith('/'))
72
+ return null;
73
+ const normalized = path.posix.normalize(value);
74
+ if (!normalized || normalized.startsWith('../') || normalized.includes('/../') || normalized === '..')
75
+ return null;
76
+ if (!isMarkdownPath(normalized))
77
+ return null;
78
+ if (normalized.startsWith('docs/'))
79
+ return normalized;
80
+ const normalizedTaskData = normalizeTaskDataRelativePath(normalized);
81
+ if (!normalizedTaskData || !isMarkdownPath(normalizedTaskData))
82
+ return null;
83
+ return normalizedTaskData;
84
+ };
85
+ const listWorkspaceSyncDocumentsSnapshot = (basePath, workspaceId = 'default') => {
86
+ const collectFiles = (dirPath, baseRelative, out) => {
87
+ if (!fs.existsSync(dirPath))
88
+ return;
89
+ let entries = [];
90
+ try {
91
+ entries = fs.readdirSync(dirPath, { withFileTypes: true });
92
+ }
93
+ catch (error) {
94
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read sync document directory.', {
95
+ source: 'documents.collectFiles',
96
+ dirPath,
97
+ error: String(error?.message || error || '')
98
+ });
99
+ return;
100
+ }
101
+ for (const entry of entries) {
102
+ if (!entry || typeof entry.name !== 'string')
103
+ continue;
104
+ const absPath = path.join(dirPath, entry.name);
105
+ const relPath = path.posix.join(baseRelative, entry.name);
106
+ if (entry.isDirectory()) {
107
+ collectFiles(absPath, relPath, out);
108
+ continue;
109
+ }
110
+ if (!entry.isFile())
111
+ continue;
112
+ if (!isMarkdownPath(relPath))
113
+ continue;
114
+ const normalized = normalizeWorkspaceSyncDocumentPath(relPath);
115
+ if (!normalized)
116
+ continue;
117
+ out.push(normalized);
118
+ }
119
+ };
120
+ const candidatePaths = [];
121
+ collectFiles(path.join(basePath, '.taskforce', 'docs'), 'docs', candidatePaths);
122
+ collectFiles(path.join(basePath, '.taskforce', 'task-data'), 'task-data', candidatePaths);
123
+ const uniquePaths = Array.from(new Set(candidatePaths)).sort((a, b) => a.localeCompare(b));
124
+ const workspaceCache = workspaceDocumentSnapshotCache.get(workspaceId) || new Map();
125
+ workspaceDocumentSnapshotCache.set(workspaceId, workspaceCache);
126
+ const remainingPaths = new Set(uniquePaths);
127
+ const docs = [];
128
+ for (const relativePath of uniquePaths) {
129
+ const absPath = path.resolve(path.join(basePath, '.taskforce'), relativePath);
130
+ const safeRoot = path.resolve(path.join(basePath, '.taskforce'));
131
+ if (absPath !== safeRoot && !absPath.startsWith(`${safeRoot}${path.sep}`))
132
+ continue;
133
+ try {
134
+ const stat = fs.statSync(absPath);
135
+ if (!stat.isFile())
136
+ continue;
137
+ const cached = workspaceCache.get(relativePath);
138
+ if (cached
139
+ && cached.size === stat.size
140
+ && cached.mtimeMs === stat.mtimeMs
141
+ && cached.ctimeMs === stat.ctimeMs) {
142
+ docs.push({
143
+ path: relativePath,
144
+ updatedAt: cached.updatedAt,
145
+ content: cached.content
146
+ });
147
+ continue;
148
+ }
149
+ const content = fs.readFileSync(absPath, 'utf8');
150
+ // Hash the content so we can detect real changes regardless of mtime.
151
+ const contentHash = createHash('sha256').update(content).digest('hex');
152
+ const existing = core.getDocumentWatermark?.(workspaceId, relativePath);
153
+ let updatedAt;
154
+ if (existing && existing.contentHash === contentHash) {
155
+ // Content unchanged — use the stable stored timestamp.
156
+ updatedAt = existing.updatedAt;
157
+ }
158
+ else {
159
+ // New or changed content — seed from mtime, then persist.
160
+ updatedAt = Number.isFinite(stat.mtimeMs)
161
+ ? new Date(stat.mtimeMs).toISOString()
162
+ : new Date().toISOString();
163
+ try {
164
+ core.upsertDocumentWatermark?.(workspaceId, relativePath, contentHash, updatedAt);
165
+ }
166
+ catch (error) {
167
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to persist document watermark during snapshot.', {
168
+ source: 'documents.snapshot.upsertWatermark',
169
+ path: relativePath,
170
+ error: String(error?.message || error || '')
171
+ });
172
+ }
173
+ }
174
+ docs.push({
175
+ path: relativePath,
176
+ updatedAt,
177
+ content
178
+ });
179
+ workspaceCache.set(relativePath, {
180
+ path: relativePath,
181
+ mtimeMs: stat.mtimeMs,
182
+ ctimeMs: stat.ctimeMs,
183
+ size: stat.size,
184
+ updatedAt,
185
+ content
186
+ });
187
+ }
188
+ catch (error) {
189
+ recordSyncDiagnostic(workspaceId, 'pull', 'Failed to read sync document snapshot entry.', {
190
+ source: 'documents.snapshot.readEntry',
191
+ path: relativePath,
192
+ error: String(error?.message || error || '')
193
+ });
194
+ }
195
+ }
196
+ for (const cachedPath of Array.from(workspaceCache.keys())) {
197
+ if (!remainingPaths.has(cachedPath))
198
+ workspaceCache.delete(cachedPath);
199
+ }
200
+ return docs;
201
+ };
202
+ const writeWorkspaceSyncDocument = (basePath, doc, workspaceId = 'default') => {
203
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(doc.path);
204
+ if (!normalizedPath)
205
+ return;
206
+ const root = path.resolve(path.join(basePath, '.taskforce'));
207
+ const targetPath = path.resolve(root, normalizedPath);
208
+ if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
209
+ return;
210
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
211
+ const content = String(doc.content || '');
212
+ fs.writeFileSync(targetPath, content, 'utf8');
213
+ const updatedAt = String(doc.updatedAt || '').trim();
214
+ const watermarkMs = Date.parse(updatedAt);
215
+ if (Number.isFinite(watermarkMs) && watermarkMs > 0) {
216
+ const when = new Date(watermarkMs);
217
+ fs.utimesSync(targetPath, when, when);
218
+ }
219
+ // Persist content hash + timestamp to SQLite so the snapshot function uses a
220
+ // stable watermark instead of relying on non-deterministic filesystem mtime.
221
+ try {
222
+ const contentHash = createHash('sha256').update(content).digest('hex');
223
+ core.upsertDocumentWatermark?.(workspaceId, normalizedPath, contentHash, updatedAt || new Date().toISOString());
224
+ }
225
+ catch (error) {
226
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to persist document watermark after write.', {
227
+ source: 'documents.write.upsertWatermark',
228
+ path: normalizedPath,
229
+ error: String(error?.message || error || '')
230
+ });
231
+ }
232
+ workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
233
+ };
234
+ const deleteWorkspaceSyncDocument = (basePath, docPath, workspaceId = 'default') => {
235
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(docPath);
236
+ if (!normalizedPath)
237
+ return;
238
+ const root = path.resolve(path.join(basePath, '.taskforce'));
239
+ const targetPath = path.resolve(root, normalizedPath);
240
+ if (targetPath !== root && !targetPath.startsWith(`${root}${path.sep}`))
241
+ return;
242
+ try {
243
+ if (fs.existsSync(targetPath))
244
+ fs.unlinkSync(targetPath);
245
+ }
246
+ catch (error) {
247
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete workspace sync document.', {
248
+ source: 'documents.delete.file',
249
+ path: normalizedPath,
250
+ error: String(error?.message || error || '')
251
+ });
252
+ }
253
+ try {
254
+ core.deleteDocumentWatermark?.(workspaceId, normalizedPath);
255
+ }
256
+ catch (error) {
257
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete document watermark.', {
258
+ source: 'documents.delete.watermark',
259
+ path: normalizedPath,
260
+ error: String(error?.message || error || '')
261
+ });
262
+ }
263
+ workspaceDocumentSnapshotCache.get(workspaceId)?.delete(normalizedPath);
264
+ };
265
+ const resolveConflictOrdering = (incomingWatermark, existingWatermark, incomingTieBreak, existingTieBreak) => {
266
+ const incomingMs = Date.parse(String(incomingWatermark || '').trim());
267
+ const existingMs = Date.parse(String(existingWatermark || '').trim());
268
+ const incomingHasWatermark = Number.isFinite(incomingMs);
269
+ const existingHasWatermark = Number.isFinite(existingMs);
270
+ if (incomingHasWatermark && existingHasWatermark) {
271
+ if (incomingMs > existingMs)
272
+ return 1;
273
+ if (incomingMs < existingMs)
274
+ return -1;
275
+ }
276
+ else if (incomingHasWatermark && !existingHasWatermark) {
277
+ return 1;
278
+ }
279
+ else if (!incomingHasWatermark && existingHasWatermark) {
280
+ return -1;
281
+ }
282
+ return incomingTieBreak.localeCompare(existingTieBreak);
283
+ };
284
+ const dispatchWorkspaceSyncChanges = (workspaceId, incomingChanges) => {
285
+ const tasks = [];
286
+ const archived = [];
287
+ const deleteTaskIds = new Set();
288
+ const emittedEventIds = new Set();
289
+ const unsubscribeRealtime = typeof core.onRealtimeMutation === 'function'
290
+ ? core.onRealtimeMutation((event) => {
291
+ const eventWorkspaceId = String(event?.workspaceId || '').trim();
292
+ if (eventWorkspaceId !== workspaceId)
293
+ return;
294
+ const eventId = String(event?.eventId || '').trim();
295
+ if (eventId)
296
+ emittedEventIds.add(eventId);
297
+ })
298
+ : () => { };
299
+ let incomingTaxonomies = null;
300
+ let incomingTaxonomiesUpdatedAt = '';
301
+ const incomingDocuments = [];
302
+ const incomingDocumentDeletes = new Set();
303
+ try {
304
+ for (const change of incomingChanges) {
305
+ const raw = change;
306
+ const op = String(raw.op || '').trim().toLowerCase();
307
+ if (op === 'taxonomy-upsert') {
308
+ if (Array.isArray(raw.taxonomies)) {
309
+ incomingTaxonomies = raw.taxonomies;
310
+ incomingTaxonomiesUpdatedAt = String(raw.updatedAt || raw.watermark || '').trim();
311
+ }
312
+ continue;
313
+ }
314
+ if (op === 'document-upsert') {
315
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(raw.path);
316
+ if (normalizedPath) {
317
+ incomingDocuments.push({
318
+ path: normalizedPath,
319
+ updatedAt: String(raw.updatedAt || raw.watermark || '').trim(),
320
+ content: typeof raw.content === 'string' ? raw.content : ''
321
+ });
322
+ }
323
+ continue;
324
+ }
325
+ if (op === 'document-delete') {
326
+ const normalizedPath = normalizeWorkspaceSyncDocumentPath(raw.path);
327
+ if (normalizedPath)
328
+ incomingDocumentDeletes.add(normalizedPath);
329
+ continue;
330
+ }
331
+ if (op !== 'delete')
332
+ continue;
333
+ const task = raw.task;
334
+ const deleteTaskId = String(raw.taskId || raw.id || task?.id || '').trim();
335
+ if (deleteTaskId)
336
+ deleteTaskIds.add(deleteTaskId);
337
+ }
338
+ for (const change of incomingChanges) {
339
+ const raw = change;
340
+ const op = String(raw.op || '').trim().toLowerCase();
341
+ if (op === 'delete' || op === 'document-upsert' || op === 'document-delete')
342
+ continue;
343
+ const task = raw.task && typeof raw.task === 'object' ? raw.task : null;
344
+ if (!task)
345
+ continue;
346
+ const taskId = String(task.id || '').trim();
347
+ if (taskId && deleteTaskIds.has(taskId))
348
+ continue;
349
+ const markedArchived = raw.archived === true || task?.isArchived === true;
350
+ if (markedArchived)
351
+ archived.push(task);
352
+ else
353
+ tasks.push(task);
354
+ }
355
+ for (const taskId of deleteTaskIds) {
356
+ try {
357
+ if (typeof core.deleteTaskForSync === 'function') {
358
+ core.deleteTaskForSync(taskId, workspaceId, { persistBackup: false });
359
+ }
360
+ else {
361
+ core.deleteTask(taskId, workspaceId, { persistBackup: false });
362
+ }
363
+ }
364
+ catch (error) {
365
+ recordSyncDiagnostic(workspaceId, 'apply', 'Failed to delete task during sync apply.', {
366
+ source: 'apply.deleteTask',
367
+ taskId,
368
+ error: String(error?.message || error || '')
369
+ });
370
+ }
371
+ }
372
+ if (Array.isArray(incomingTaxonomies)) {
373
+ if (!incomingTaxonomiesUpdatedAt) {
374
+ core.updateTaxonomies(incomingTaxonomies);
375
+ const fallbackHash = createHash('sha256').update(JSON.stringify(incomingTaxonomies)).digest('hex');
376
+ workspaceTaxonomySyncMeta.set(workspaceId, {
377
+ updatedAt: new Date().toISOString(),
378
+ hash: fallbackHash
379
+ });
380
+ }
381
+ else {
382
+ const incomingTaxonomyHash = createHash('sha256').update(JSON.stringify(incomingTaxonomies)).digest('hex');
383
+ const currentMeta = workspaceTaxonomySyncMeta.get(workspaceId) || { updatedAt: '', hash: '' };
384
+ const compare = resolveConflictOrdering(incomingTaxonomiesUpdatedAt, currentMeta.updatedAt, incomingTaxonomyHash, currentMeta.hash);
385
+ if (compare >= 0) {
386
+ core.updateTaxonomies(incomingTaxonomies);
387
+ workspaceTaxonomySyncMeta.set(workspaceId, {
388
+ updatedAt: incomingTaxonomiesUpdatedAt,
389
+ hash: incomingTaxonomyHash
390
+ });
391
+ }
392
+ }
393
+ }
394
+ if (incomingDocuments.length > 0) {
395
+ for (const doc of incomingDocuments) {
396
+ const existingDoc = core.getDocumentWatermark?.(workspaceId, doc.path);
397
+ const incomingHash = createHash('sha256').update(String(doc.content || '')).digest('hex');
398
+ const existingUpdatedAt = String(existingDoc?.updatedAt || '').trim();
399
+ const existingHash = String(existingDoc?.contentHash || '').trim();
400
+ const compare = resolveConflictOrdering(doc.updatedAt, existingUpdatedAt, incomingHash, existingHash);
401
+ if (compare < 0)
402
+ continue;
403
+ if (compare === 0 && existingHash === incomingHash)
404
+ continue;
405
+ writeWorkspaceSyncDocument(core.getPaths().basePath, doc, workspaceId);
406
+ }
407
+ }
408
+ if (incomingDocumentDeletes.size > 0) {
409
+ for (const docPath of incomingDocumentDeletes) {
410
+ deleteWorkspaceSyncDocument(core.getPaths().basePath, docPath, workspaceId);
411
+ }
412
+ }
413
+ const result = core.applyWorkspaceSyncSnapshot({ tasks, archived }, workspaceId);
414
+ return { result, deleteCount: deleteTaskIds.size, emittedEventIds: Array.from(emittedEventIds) };
415
+ }
416
+ finally {
417
+ unsubscribeRealtime();
418
+ }
419
+ };
420
+ // GET /api/taskforce/sync/user-settings - Pull user-global sync payload for authenticated user
421
+ addRoute('GET', '/api/taskforce/sync/user-settings', async (req, res) => {
422
+ const authConfig = context.authConfig;
423
+ if (!authConfig || !authConfig.enabled) {
424
+ res.writeHead(400, { 'Content-Type': 'application/json' });
425
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
426
+ return;
427
+ }
428
+ const access = resolveRequestAccess(req, authConfig);
429
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
430
+ res.writeHead(401, { 'Content-Type': 'application/json' });
431
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
432
+ return;
433
+ }
434
+ const snapshot = core.getUserGlobalSyncSettings(access.userId);
435
+ res.writeHead(200, { 'Content-Type': 'application/json' });
436
+ res.end(JSON.stringify({
437
+ success: true,
438
+ settings: snapshot.settings,
439
+ updatedAt: snapshot.updatedAt
440
+ }));
441
+ });
442
+ // POST /api/taskforce/sync/user-settings - Push user-global sync payload for authenticated user
443
+ addRoute('POST', '/api/taskforce/sync/user-settings', async (req, res) => {
444
+ const authConfig = context.authConfig;
445
+ if (!authConfig || !authConfig.enabled) {
446
+ res.writeHead(400, { 'Content-Type': 'application/json' });
447
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
448
+ return;
449
+ }
450
+ const access = resolveRequestAccess(req, authConfig);
451
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
452
+ res.writeHead(401, { 'Content-Type': 'application/json' });
453
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
454
+ return;
455
+ }
456
+ const body = await parseJsonBody(req);
457
+ const incomingSettings = body?.settings;
458
+ const incomingUpdatedAt = typeof body?.updatedAt === 'string' ? body.updatedAt : undefined;
459
+ const result = core.upsertUserGlobalSyncSettings(access.userId, incomingSettings, incomingUpdatedAt);
460
+ res.writeHead(200, { 'Content-Type': 'application/json' });
461
+ res.end(JSON.stringify({
462
+ success: true,
463
+ settings: result.settings,
464
+ updatedAt: result.updatedAt,
465
+ applied: result.applied
466
+ }));
467
+ });
468
+ // POST /api/taskforce/sync/workspace/handshake - Enforce workspace-id match before workspace sync
469
+ addRoute('POST', '/api/taskforce/sync/workspace/handshake', async (req, res) => {
470
+ const authConfig = context.authConfig;
471
+ if (!authConfig || !authConfig.enabled) {
472
+ res.writeHead(400, { 'Content-Type': 'application/json' });
473
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
474
+ return;
475
+ }
476
+ const access = resolveRequestAccess(req, authConfig);
477
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
478
+ auditAuthAction(req, 'sync-workspace-handshake-unauthorized', {});
479
+ res.writeHead(401, { 'Content-Type': 'application/json' });
480
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
481
+ return;
482
+ }
483
+ const body = await parseJsonBody(req);
484
+ const localWorkspaceId = String(body?.workspaceId || '').trim();
485
+ if (!localWorkspaceId) {
486
+ auditAuthAction(req, 'sync-workspace-handshake-invalid', { code: 'WORKSPACE_ID_REQUIRED' });
487
+ res.writeHead(400, { 'Content-Type': 'application/json' });
488
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.' }));
489
+ return;
490
+ }
491
+ const cloudWorkspaceId = String(access.workspaceId || '').trim();
492
+ if (!cloudWorkspaceId) {
493
+ auditAuthAction(req, 'sync-workspace-handshake-missing-cloud-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
494
+ res.writeHead(400, { 'Content-Type': 'application/json' });
495
+ res.end(JSON.stringify({
496
+ success: false,
497
+ error: 'No cloud workspace is selected. Complete workspace setup first.',
498
+ code: 'WORKSPACE_ID_REQUIRED'
499
+ }));
500
+ return;
501
+ }
502
+ if (localWorkspaceId !== cloudWorkspaceId) {
503
+ auditAuthAction(req, 'sync-workspace-handshake-mismatch', {
504
+ localWorkspaceId,
505
+ cloudWorkspaceId
506
+ });
507
+ res.writeHead(409, { 'Content-Type': 'application/json' });
508
+ res.end(JSON.stringify({
509
+ success: false,
510
+ error: 'Workspace mismatch. Sync requires matching local and cloud workspace IDs.',
511
+ code: 'WORKSPACE_ID_MISMATCH',
512
+ localWorkspaceId,
513
+ cloudWorkspaceId
514
+ }));
515
+ return;
516
+ }
517
+ auditAuthAction(req, 'sync-workspace-handshake-success', { workspaceId: localWorkspaceId });
518
+ res.writeHead(200, { 'Content-Type': 'application/json' });
519
+ res.end(JSON.stringify({
520
+ success: true,
521
+ localWorkspaceId,
522
+ cloudWorkspaceId
523
+ }));
524
+ });
525
+ // POST /api/taskforce/sync/workspace/provision - Switch to existing cloud workspace that matches local workspace id
526
+ addRoute('POST', '/api/taskforce/sync/workspace/provision', async (req, res) => {
527
+ const authConfig = context.authConfig;
528
+ if (!authConfig || !authConfig.enabled) {
529
+ res.writeHead(400, { 'Content-Type': 'application/json' });
530
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
531
+ return;
532
+ }
533
+ const access = resolveRequestAccess(req, authConfig);
534
+ if (!access.authenticated || !access.sessionId || !access.userId || access.userId === 'anonymous') {
535
+ res.writeHead(401, { 'Content-Type': 'application/json' });
536
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
537
+ return;
538
+ }
539
+ const body = await parseJsonBody(req);
540
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
541
+ const requestedWorkspaceName = String(body?.name || requestedWorkspaceId || '').trim();
542
+ const requestedWorkspaceDescription = typeof body?.description === 'string' ? body.description : undefined;
543
+ if (!requestedWorkspaceId) {
544
+ res.writeHead(400, { 'Content-Type': 'application/json' });
545
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.' }));
546
+ return;
547
+ }
548
+ if (isReservedWorkspaceId(requestedWorkspaceId)) {
549
+ res.writeHead(400, { 'Content-Type': 'application/json' });
550
+ res.end(JSON.stringify({
551
+ success: false,
552
+ error: 'workspaceId "default" is reserved.',
553
+ code: 'WORKSPACE_ID_RESERVED'
554
+ }));
555
+ return;
556
+ }
557
+ if (!requestedWorkspaceName) {
558
+ res.writeHead(400, { 'Content-Type': 'application/json' });
559
+ res.end(JSON.stringify({ success: false, error: 'name is required.' }));
560
+ return;
561
+ }
562
+ const existing = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
563
+ if (existing) {
564
+ const cookie = createSessionTokenCookie(access.sessionId, authConfig, {
565
+ role: existing.role,
566
+ workspaceId: existing.id,
567
+ userId: access.userId,
568
+ ...resolveSessionCookieAttributesForRequest(req, authConfig)
569
+ });
570
+ res.setHeader('Set-Cookie', cookie);
571
+ res.writeHead(200, { 'Content-Type': 'application/json' });
572
+ res.end(JSON.stringify({
573
+ success: true,
574
+ workspace: existing,
575
+ existing: true
576
+ }));
577
+ return;
578
+ }
579
+ try {
580
+ const created = core.createWorkspaceForUser({
581
+ userId: access.userId,
582
+ name: requestedWorkspaceName,
583
+ description: requestedWorkspaceDescription,
584
+ role: 'owner',
585
+ preferredWorkspaceId: requestedWorkspaceId
586
+ });
587
+ const cookie = createSessionTokenCookie(access.sessionId, authConfig, {
588
+ role: created.role,
589
+ workspaceId: created.id,
590
+ userId: access.userId,
591
+ ...resolveSessionCookieAttributesForRequest(req, authConfig)
592
+ });
593
+ res.setHeader('Set-Cookie', cookie);
594
+ auditAuthAction(req, 'sync-workspace-provision-created', {
595
+ workspaceId: created.id,
596
+ workspaceName: created.name
597
+ });
598
+ res.writeHead(201, { 'Content-Type': 'application/json' });
599
+ res.end(JSON.stringify({
600
+ success: true,
601
+ workspace: created,
602
+ existing: false
603
+ }));
604
+ return;
605
+ }
606
+ catch (error) {
607
+ res.writeHead(resolveErrorStatusCode(error, 400), { 'Content-Type': 'application/json' });
608
+ res.end(JSON.stringify({
609
+ success: false,
610
+ error: String(error?.message || 'Unable to provision workspace for sync.'),
611
+ code: error?.code
612
+ }));
613
+ return;
614
+ }
615
+ });
616
+ // POST /api/taskforce/sync/workspace/bootstrap-push - Initial local snapshot push into authenticated cloud workspace
617
+ addRoute('POST', '/api/taskforce/sync/workspace/bootstrap-push', async (req, res) => {
618
+ const authConfig = context.authConfig;
619
+ if (!authConfig || !authConfig.enabled) {
620
+ res.writeHead(400, { 'Content-Type': 'application/json' });
621
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
622
+ return;
623
+ }
624
+ const access = resolveRequestAccess(req, authConfig);
625
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
626
+ auditAuthAction(req, 'sync-workspace-bootstrap-unauthorized', {});
627
+ res.writeHead(401, { 'Content-Type': 'application/json' });
628
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
629
+ return;
630
+ }
631
+ const body = await parseJsonBody(req);
632
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
633
+ const cloudWorkspaceId = String(access.workspaceId || '').trim();
634
+ if (!requestedWorkspaceId) {
635
+ auditAuthAction(req, 'sync-workspace-bootstrap-invalid', { code: 'WORKSPACE_ID_REQUIRED' });
636
+ res.writeHead(400, { 'Content-Type': 'application/json' });
637
+ res.end(JSON.stringify({ success: false, error: 'workspaceId is required.', code: 'WORKSPACE_ID_REQUIRED' }));
638
+ return;
639
+ }
640
+ if (!cloudWorkspaceId) {
641
+ auditAuthAction(req, 'sync-workspace-bootstrap-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
642
+ res.writeHead(400, { 'Content-Type': 'application/json' });
643
+ res.end(JSON.stringify({
644
+ success: false,
645
+ error: 'No workspace selected. Create or select a workspace first.',
646
+ code: 'WORKSPACE_ID_REQUIRED'
647
+ }));
648
+ return;
649
+ }
650
+ if (requestedWorkspaceId !== cloudWorkspaceId) {
651
+ auditAuthAction(req, 'sync-workspace-bootstrap-mismatch', {
652
+ requestedWorkspaceId,
653
+ cloudWorkspaceId
654
+ });
655
+ res.writeHead(409, { 'Content-Type': 'application/json' });
656
+ res.end(JSON.stringify({
657
+ success: false,
658
+ error: 'Workspace mismatch. Sync requires matching local and cloud workspace IDs.',
659
+ code: 'WORKSPACE_ID_MISMATCH',
660
+ localWorkspaceId: requestedWorkspaceId,
661
+ cloudWorkspaceId
662
+ }));
663
+ return;
664
+ }
665
+ const tasks = Array.isArray(body?.tasks) ? body.tasks : [];
666
+ const archived = Array.isArray(body?.archived) ? body.archived : [];
667
+ const incomingTaxonomies = Array.isArray(body?.taxonomies) ? body.taxonomies : null;
668
+ const incomingDocuments = Array.isArray(body?.documents) ? body.documents : [];
669
+ try {
670
+ if (incomingTaxonomies) {
671
+ core.updateTaxonomies(incomingTaxonomies);
672
+ }
673
+ for (const doc of incomingDocuments) {
674
+ const docEntry = {
675
+ path: String(doc?.path || '').trim(),
676
+ updatedAt: String(doc?.updatedAt || '').trim(),
677
+ content: typeof doc?.content === 'string' ? doc.content : ''
678
+ };
679
+ if (docEntry.path) {
680
+ writeWorkspaceSyncDocument(core.getPaths().basePath, docEntry, cloudWorkspaceId);
681
+ }
682
+ }
683
+ const result = core.applyWorkspaceSyncSnapshot({
684
+ tasks,
685
+ archived
686
+ }, cloudWorkspaceId);
687
+ res.writeHead(200, { 'Content-Type': 'application/json' });
688
+ res.end(JSON.stringify({
689
+ success: true,
690
+ workspaceId: cloudWorkspaceId,
691
+ result
692
+ }));
693
+ auditAuthAction(req, 'sync-workspace-bootstrap-success', {
694
+ workspaceId: cloudWorkspaceId,
695
+ taskCount: tasks.length,
696
+ archivedCount: archived.length,
697
+ taxonomyCount: incomingTaxonomies?.length ?? 0,
698
+ documentCount: incomingDocuments.length
699
+ });
700
+ }
701
+ catch (error) {
702
+ const statusCode = resolveErrorStatusCode(error, 400);
703
+ auditAuthAction(req, 'sync-workspace-bootstrap-failed', {
704
+ workspaceId: cloudWorkspaceId,
705
+ code: error?.code || null,
706
+ statusCode
707
+ });
708
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
709
+ res.end(JSON.stringify({
710
+ success: false,
711
+ error: String(error?.message || 'Failed to apply workspace snapshot'),
712
+ code: error?.code
713
+ }));
714
+ }
715
+ });
716
+ // GET /api/taskforce/sync/workspace/documents - Snapshot markdown documents for workspace sync payload/signature
717
+ addRoute('GET', '/api/taskforce/sync/workspace/documents', async (req, res) => {
718
+ const authConfig = context.authConfig;
719
+ if (authConfig?.runtimeMode === 'cloud') {
720
+ const access = resolveRequestAccess(req, authConfig);
721
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
722
+ res.writeHead(401, { 'Content-Type': 'application/json' });
723
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
724
+ return;
725
+ }
726
+ }
727
+ const basePath = core.getPaths().basePath;
728
+ const documents = listWorkspaceSyncDocumentsSnapshot(basePath, 'default');
729
+ const fingerprint = JSON.stringify(documents.map((doc) => `${doc.path}:${doc.updatedAt}:${doc.content.length}`));
730
+ res.writeHead(200, { 'Content-Type': 'application/json' });
731
+ res.end(JSON.stringify({
732
+ success: true,
733
+ documents,
734
+ fingerprint
735
+ }));
736
+ });
737
+ // GET /api/taskforce/sync/workspace/pull - Incremental workspace task deltas with cursor paging
738
+ addRoute('GET', '/api/taskforce/sync/workspace/pull', async (req, res) => {
739
+ const authConfig = context.authConfig;
740
+ if (!authConfig || !authConfig.enabled) {
741
+ res.writeHead(400, { 'Content-Type': 'application/json' });
742
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
743
+ return;
744
+ }
745
+ const access = resolveRequestAccess(req, authConfig);
746
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
747
+ auditAuthAction(req, 'sync-workspace-pull-unauthorized', {});
748
+ res.writeHead(401, { 'Content-Type': 'application/json' });
749
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
750
+ return;
751
+ }
752
+ const url = new URL(req.url || '/api/taskforce/sync/workspace/pull', 'http://localhost');
753
+ const requestedWorkspaceId = String(url.searchParams.get('workspaceId') || '').trim();
754
+ if (!requestedWorkspaceId) {
755
+ auditAuthAction(req, 'sync-workspace-pull-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
756
+ res.writeHead(400, { 'Content-Type': 'application/json' });
757
+ res.end(JSON.stringify({
758
+ success: false,
759
+ error: 'workspaceId is required.',
760
+ code: 'WORKSPACE_ID_REQUIRED',
761
+ workspaceId: null
762
+ }));
763
+ return;
764
+ }
765
+ const allowed = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
766
+ if (!allowed) {
767
+ auditAuthAction(req, 'sync-workspace-pull-forbidden', {
768
+ requestedWorkspaceId
769
+ });
770
+ res.writeHead(403, { 'Content-Type': 'application/json' });
771
+ res.end(JSON.stringify({
772
+ success: false,
773
+ error: 'You do not have access to this workspace.',
774
+ code: 'WORKSPACE_ACCESS_DENIED',
775
+ workspaceId: requestedWorkspaceId
776
+ }));
777
+ return;
778
+ }
779
+ const targetWorkspaceId = requestedWorkspaceId;
780
+ const cursor = decodeWorkspaceSyncCursor(url.searchParams.get('cursor'));
781
+ const requestedLimit = Number.parseInt(String(url.searchParams.get('limit') || ''), 10);
782
+ const pageSize = Number.isFinite(requestedLimit) && requestedLimit > 0
783
+ ? Math.max(1, Math.min(500, Math.floor(requestedLimit)))
784
+ : 200;
785
+ // Use watermark-indexed queries to avoid full table scans (Finding 12).
786
+ // Fetch up to pageSize * 2 tasks/deletes from DB since the cursor, then merge with
787
+ // small in-memory collections (taxonomy, documents) before slicing to pageSize.
788
+ const syncCursor = cursor ? { watermark: cursor.u, id: cursor.id } : null;
789
+ const dbFetchLimit = pageSize * 2 + 10;
790
+ const taskRows = typeof core.listTasksForSync === 'function'
791
+ ? core.listTasksForSync(targetWorkspaceId, syncCursor, dbFetchLimit)
792
+ : [
793
+ ...(core.listTasks(targetWorkspaceId)?.tasks || []).map((task) => ({ task, archived: false })),
794
+ ...(core.listArchive(targetWorkspaceId)?.archived || []).map((task) => ({ task, archived: true }))
795
+ ];
796
+ const upsertEntries = taskRows
797
+ .map((entry) => {
798
+ const watermark = String(entry.task?.updatedAt || entry.task?.createdAt || '').trim();
799
+ const taskId = String(entry.task?.id || '').trim();
800
+ return {
801
+ kind: 'upsert',
802
+ watermark,
803
+ sortId: taskId,
804
+ task: entry.task,
805
+ archived: Boolean(entry.archived)
806
+ };
807
+ })
808
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
809
+ const rawDeleteEntries = typeof core.listWorkspaceSyncDeletesSince === 'function'
810
+ ? core.listWorkspaceSyncDeletesSince(targetWorkspaceId, syncCursor, dbFetchLimit)
811
+ : (typeof core.listWorkspaceSyncDeletes === 'function'
812
+ ? core.listWorkspaceSyncDeletes(targetWorkspaceId)
813
+ : []);
814
+ const deleteEntries = rawDeleteEntries
815
+ .map((entry) => ({
816
+ kind: 'delete',
817
+ watermark: String(entry.deletedAt || '').trim(),
818
+ sortId: String(entry.taskId || '').trim(),
819
+ taskId: String(entry.taskId || '').trim()
820
+ }))
821
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.sortId.length > 0);
822
+ const taxonomySnapshot = (typeof core.getTaxonomiesSyncSnapshot === 'function'
823
+ ? core.getTaxonomiesSyncSnapshot()
824
+ : null);
825
+ const taxonomyEntries = (taxonomySnapshot
826
+ && Array.isArray(taxonomySnapshot.taxonomies)
827
+ && Number.isFinite(Date.parse(String(taxonomySnapshot.updatedAt || '').trim())))
828
+ ? [{
829
+ kind: 'taxonomy',
830
+ watermark: String(taxonomySnapshot.updatedAt || '').trim(),
831
+ sortId: '__taxonomy_config__',
832
+ taxonomies: taxonomySnapshot.taxonomies
833
+ }]
834
+ : [];
835
+ const documentEntries = listWorkspaceSyncDocumentsSnapshot(core.getPaths().basePath, targetWorkspaceId)
836
+ .map((doc) => ({
837
+ kind: 'document',
838
+ watermark: String(doc.updatedAt || '').trim(),
839
+ sortId: `doc:${String(doc.path || '').trim()}`,
840
+ path: String(doc.path || '').trim(),
841
+ content: String(doc.content || '')
842
+ }))
843
+ .filter((entry) => Number.isFinite(Date.parse(entry.watermark)) && entry.path.length > 0);
844
+ const all = [...upsertEntries, ...deleteEntries, ...taxonomyEntries, ...documentEntries];
845
+ const afterCursor = all.filter((entry) => {
846
+ if (!cursor)
847
+ return true;
848
+ const watermark = entry.watermark;
849
+ const sortId = entry.sortId;
850
+ if (watermark > cursor.u)
851
+ return true;
852
+ if (watermark < cursor.u)
853
+ return false;
854
+ return sortId > cursor.id;
855
+ });
856
+ afterCursor.sort((a, b) => {
857
+ if (a.watermark !== b.watermark)
858
+ return a.watermark.localeCompare(b.watermark);
859
+ return String(a.sortId || '').localeCompare(String(b.sortId || ''));
860
+ });
861
+ const page = afterCursor.slice(0, pageSize);
862
+ const hasMore = afterCursor.length > pageSize;
863
+ const last = page.length > 0 ? page[page.length - 1] : null;
864
+ const nextCursor = encodeWorkspaceSyncCursor(last
865
+ ? { u: last.watermark, id: String(last.sortId || '') }
866
+ : cursor);
867
+ res.writeHead(200, { 'Content-Type': 'application/json' });
868
+ res.end(JSON.stringify({
869
+ success: true,
870
+ workspaceId: targetWorkspaceId,
871
+ changes: page.map((entry) => (entry.kind === 'delete'
872
+ ? {
873
+ op: 'delete',
874
+ taskId: entry.taskId,
875
+ watermark: entry.watermark
876
+ }
877
+ : entry.kind === 'taxonomy'
878
+ ? {
879
+ op: 'taxonomy-upsert',
880
+ taxonomies: entry.taxonomies,
881
+ watermark: entry.watermark
882
+ }
883
+ : entry.kind === 'document'
884
+ ? {
885
+ op: 'document-upsert',
886
+ path: entry.path,
887
+ updatedAt: entry.watermark,
888
+ content: entry.content,
889
+ watermark: entry.watermark
890
+ }
891
+ : {
892
+ op: 'upsert',
893
+ task: entry.task,
894
+ archived: Boolean(entry.archived),
895
+ watermark: entry.watermark
896
+ })),
897
+ nextCursor,
898
+ hasMore
899
+ }));
900
+ auditAuthAction(req, 'sync-workspace-pull-success', {
901
+ workspaceId: targetWorkspaceId,
902
+ returnedChanges: page.length,
903
+ hasMore
904
+ });
905
+ });
906
+ // POST /api/taskforce/sync/workspace/apply-local - Apply pulled cloud deltas into local workspace store
907
+ addRoute('POST', '/api/taskforce/sync/workspace/apply-local', async (req, res) => {
908
+ const startedAtMs = Date.now();
909
+ const authConfig = context.authConfig;
910
+ if (authConfig?.runtimeMode === 'cloud') {
911
+ res.writeHead(400, { 'Content-Type': 'application/json' });
912
+ res.end(JSON.stringify({ success: false, error: 'Local apply endpoint is only available in local runtime.' }));
913
+ return;
914
+ }
915
+ const body = await parseJsonBody(req);
916
+ const bodyWorkspaceId = String(body?.workspaceId || '').trim();
917
+ const headerWorkspaceId = String(Array.isArray(req.headers['x-taskforce-workspace-id'])
918
+ ? req.headers['x-taskforce-workspace-id'][0]
919
+ : req.headers['x-taskforce-workspace-id'] || '').trim();
920
+ let workspaceId = bodyWorkspaceId || headerWorkspaceId || requestWorkspaceId(req);
921
+ if (authConfig?.enabled) {
922
+ const access = resolveRequestAccess(req, authConfig);
923
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
924
+ auditAuthAction(req, 'sync-workspace-apply-unauthorized', {});
925
+ res.writeHead(401, { 'Content-Type': 'application/json' });
926
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
927
+ return;
928
+ }
929
+ const accessWorkspaceId = String(access.workspaceId || '').trim();
930
+ if (accessWorkspaceId && accessWorkspaceId !== 'default') {
931
+ workspaceId = accessWorkspaceId;
932
+ }
933
+ }
934
+ if (!workspaceId || workspaceId === 'default' || workspaceId === 'null') {
935
+ res.writeHead(400, { 'Content-Type': 'application/json' });
936
+ res.end(JSON.stringify({
937
+ success: false,
938
+ error: 'Workspace ID required.',
939
+ code: 'WORKSPACE_ID_REQUIRED'
940
+ }));
941
+ return;
942
+ }
943
+ const incomingChanges = Array.isArray(body?.changes) ? body.changes : [];
944
+ try {
945
+ const { result } = dispatchWorkspaceSyncChanges(workspaceId, incomingChanges);
946
+ const durationMs = Date.now() - startedAtMs;
947
+ core.recordSyncEvent(workspaceId, {
948
+ eventType: 'apply',
949
+ status: 'success',
950
+ changeCount: incomingChanges.length,
951
+ requestMs: durationMs
952
+ });
953
+ res.writeHead(200, { 'Content-Type': 'application/json' });
954
+ res.end(JSON.stringify({
955
+ success: true,
956
+ workspaceId,
957
+ result,
958
+ durationMs
959
+ }));
960
+ }
961
+ catch (error) {
962
+ const statusCode = resolveErrorStatusCode(error, 400);
963
+ const durationMs = Date.now() - startedAtMs;
964
+ core.recordSyncEvent(workspaceId, {
965
+ eventType: 'apply',
966
+ status: 'error',
967
+ statusCode,
968
+ errorMessage: String(error?.message || 'Failed to apply local workspace deltas'),
969
+ changeCount: incomingChanges.length,
970
+ requestMs: durationMs
971
+ });
972
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
973
+ res.end(JSON.stringify({
974
+ success: false,
975
+ error: String(error?.message || 'Failed to apply local workspace deltas'),
976
+ code: error?.code,
977
+ durationMs
978
+ }));
979
+ }
980
+ });
981
+ // POST /api/taskforce/sync/workspace/push - Incremental workspace deltas from local to cloud
982
+ addRoute('POST', '/api/taskforce/sync/workspace/push', async (req, res) => {
983
+ const startedAtMs = Date.now();
984
+ const authConfig = context.authConfig;
985
+ if (!authConfig || !authConfig.enabled) {
986
+ res.writeHead(400, { 'Content-Type': 'application/json' });
987
+ res.end(JSON.stringify({ success: false, error: 'Sync is unavailable when auth is disabled.' }));
988
+ return;
989
+ }
990
+ const access = resolveRequestAccess(req, authConfig);
991
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous') {
992
+ auditAuthAction(req, 'sync-workspace-push-unauthorized', {});
993
+ res.writeHead(401, { 'Content-Type': 'application/json' });
994
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
995
+ return;
996
+ }
997
+ const body = await parseJsonBody(req);
998
+ const requestedWorkspaceId = String(body?.workspaceId || '').trim();
999
+ if (!requestedWorkspaceId) {
1000
+ auditAuthAction(req, 'sync-workspace-push-missing-workspace', { code: 'WORKSPACE_ID_REQUIRED' });
1001
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1002
+ res.end(JSON.stringify({
1003
+ success: false,
1004
+ error: 'workspaceId is required.',
1005
+ code: 'WORKSPACE_ID_REQUIRED',
1006
+ workspaceId: null,
1007
+ durationMs: Date.now() - startedAtMs
1008
+ }));
1009
+ return;
1010
+ }
1011
+ const allowed = core.listUserWorkspaces(access.userId).find((workspace) => workspace.id === requestedWorkspaceId);
1012
+ if (!allowed) {
1013
+ auditAuthAction(req, 'sync-workspace-push-forbidden', {
1014
+ requestedWorkspaceId
1015
+ });
1016
+ res.writeHead(403, { 'Content-Type': 'application/json' });
1017
+ res.end(JSON.stringify({
1018
+ success: false,
1019
+ error: 'You do not have access to this workspace.',
1020
+ code: 'WORKSPACE_ACCESS_DENIED',
1021
+ workspaceId: requestedWorkspaceId
1022
+ }));
1023
+ return;
1024
+ }
1025
+ const targetWorkspaceId = requestedWorkspaceId;
1026
+ const idempotencyKey = String(body?.idempotencyKey || '').trim();
1027
+ if (idempotencyKey) {
1028
+ const replay = readPushIdempotency(targetWorkspaceId, idempotencyKey);
1029
+ if (replay) {
1030
+ auditAuthAction(req, 'sync-workspace-push-idempotent-replay', {
1031
+ workspaceId: targetWorkspaceId,
1032
+ idempotencyKey
1033
+ });
1034
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1035
+ res.end(replay);
1036
+ return;
1037
+ }
1038
+ }
1039
+ const incomingChanges = Array.isArray(body?.changes) ? body.changes : [];
1040
+ try {
1041
+ const { result, deleteCount, emittedEventIds } = dispatchWorkspaceSyncChanges(targetWorkspaceId, incomingChanges);
1042
+ const durationMs = Date.now() - startedAtMs;
1043
+ const payload = JSON.stringify({
1044
+ success: true,
1045
+ workspaceId: targetWorkspaceId,
1046
+ idempotencyKey: idempotencyKey || null,
1047
+ emittedEventIds,
1048
+ result,
1049
+ durationMs
1050
+ });
1051
+ if (idempotencyKey) {
1052
+ writePushIdempotency(targetWorkspaceId, idempotencyKey, payload);
1053
+ }
1054
+ core.recordSyncEvent(targetWorkspaceId, {
1055
+ eventType: 'push',
1056
+ status: 'success',
1057
+ changeCount: incomingChanges.length,
1058
+ requestMs: durationMs
1059
+ });
1060
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1061
+ res.end(payload);
1062
+ auditAuthAction(req, 'sync-workspace-push-success', {
1063
+ workspaceId: targetWorkspaceId,
1064
+ idempotencyKey: idempotencyKey || null,
1065
+ changeCount: incomingChanges.length,
1066
+ deleteCount: deleteCount,
1067
+ durationMs
1068
+ });
1069
+ }
1070
+ catch (error) {
1071
+ const statusCode = resolveErrorStatusCode(error, 400);
1072
+ const durationMs = Date.now() - startedAtMs;
1073
+ core.recordSyncEvent(targetWorkspaceId, {
1074
+ eventType: 'push',
1075
+ status: 'error',
1076
+ statusCode,
1077
+ errorMessage: String(error?.message || 'Failed to apply workspace deltas'),
1078
+ changeCount: incomingChanges.length,
1079
+ requestMs: durationMs
1080
+ });
1081
+ auditAuthAction(req, 'sync-workspace-push-failed', {
1082
+ workspaceId: targetWorkspaceId,
1083
+ code: error?.code || null,
1084
+ statusCode,
1085
+ durationMs
1086
+ });
1087
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
1088
+ res.end(JSON.stringify({
1089
+ success: false,
1090
+ error: String(error?.message || 'Failed to apply workspace deltas'),
1091
+ code: error?.code,
1092
+ durationMs
1093
+ }));
1094
+ }
1095
+ });
1096
+ // GET /api/taskforce/sync/events - Query recent sync events (local runtime only)
1097
+ addRoute('GET', '/api/taskforce/sync/events', async (req, res) => {
1098
+ const authConfig = context.authConfig;
1099
+ const access = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
1100
+ if (access && (!access.authenticated || !access.userId || access.userId === 'anonymous')) {
1101
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1102
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1103
+ return;
1104
+ }
1105
+ const url = new URL(req.url || '/', 'http://localhost');
1106
+ const workspaceId = String(url.searchParams.get('workspace_id') || access?.workspaceId || '').trim() || 'default';
1107
+ const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit') || '100')));
1108
+ const events = core.listSyncEvents(workspaceId, limit);
1109
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1110
+ res.end(JSON.stringify({ success: true, events }));
1111
+ });
1112
+ // POST /api/taskforce/sync/events - Record a client-side sync event (pull results, handshake outcomes)
1113
+ addRoute('POST', '/api/taskforce/sync/events', async (req, res) => {
1114
+ const authConfig = context.authConfig;
1115
+ const access = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
1116
+ if (access && (!access.authenticated || !access.userId || access.userId === 'anonymous')) {
1117
+ res.writeHead(401, { 'Content-Type': 'application/json' });
1118
+ res.end(JSON.stringify({ success: false, error: 'Authentication required.' }));
1119
+ return;
1120
+ }
1121
+ const body = await parseJsonBody(req);
1122
+ const allowedEventTypes = ['pull', 'handshake', 'bootstrap', 'apply', 'push'];
1123
+ const eventType = String(body?.eventType || '').trim().toLowerCase();
1124
+ if (!allowedEventTypes.includes(eventType)) {
1125
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1126
+ res.end(JSON.stringify({ success: false, error: 'Invalid eventType.' }));
1127
+ return;
1128
+ }
1129
+ const workspaceId = String(body?.workspaceId || access?.workspaceId || '').trim() || 'default';
1130
+ core.recordSyncEvent(workspaceId, {
1131
+ eventType: eventType,
1132
+ status: String(body?.status || '') === 'error' ? 'error' : 'success',
1133
+ statusCode: typeof body?.statusCode === 'number' ? body.statusCode : undefined,
1134
+ errorMessage: typeof body?.errorMessage === 'string' ? body.errorMessage.slice(0, 500) : undefined,
1135
+ changeCount: typeof body?.changeCount === 'number' ? body.changeCount : undefined,
1136
+ requestMs: typeof body?.requestMs === 'number' ? body.requestMs : undefined
1137
+ });
1138
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1139
+ res.end(JSON.stringify({ success: true }));
1140
+ });
1141
+ }