@taskforcehq/taskforce 0.3.304 → 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 (383) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +295 -89
  3. package/dist/TaskforceCore.js +378 -111
  4. package/dist/TaskforceCore.test.js +1221 -107
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +52 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  16. package/dist/components/context/ContextAttachmentManager.js +549 -0
  17. package/dist/components/features/AgentsModule.d.ts +10 -1
  18. package/dist/components/features/AgentsModule.js +260 -15
  19. package/dist/components/features/AgentsModule.test.js +255 -5
  20. package/dist/components/features/DocumentIndex.d.ts +1 -1
  21. package/dist/components/features/DocumentIndex.js +1 -1
  22. package/dist/components/features/DocumentViewer.d.ts +6 -2
  23. package/dist/components/features/DocumentViewer.js +90 -10
  24. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  25. package/dist/components/features/DocumentWorkspace.js +12 -155
  26. package/dist/components/features/DocumentWorkspace.test.js +226 -6
  27. package/dist/components/features/TaskSettings.js +29 -3
  28. package/dist/components/features/TaskSettings.test.js +42 -1
  29. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  30. package/dist/components/features/documentWorkspaceModel.js +57 -0
  31. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  32. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  33. package/dist/components/task/TaskCard.js +19 -28
  34. package/dist/components/task/TaskCard.test.js +38 -3
  35. package/dist/components/task/TaskContextUpload.js +4 -526
  36. package/dist/components/task/TaskForm.d.ts +8 -8
  37. package/dist/components/task/TaskForm.js +362 -230
  38. package/dist/components/task/TaskForm.test.js +558 -381
  39. package/dist/components/task/TaskKanban.d.ts +2 -1
  40. package/dist/components/task/TaskKanban.js +96 -14
  41. package/dist/components/task/TaskKanban.test.js +36 -0
  42. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  43. package/dist/components/ui/EditableAvatarButton.js +18 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  45. package/dist/components/views/PlanComparisonPage.js +70 -17
  46. package/dist/components/views/PlanComparisonPage.test.js +289 -41
  47. package/dist/components/views/PlansPage.js +239 -106
  48. package/dist/components/views/PlansPage.test.js +593 -42
  49. package/dist/components/views/StandaloneLayout.js +310 -554
  50. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  51. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  52. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  53. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  54. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  55. package/dist/components/views/planningScope.d.ts +20 -0
  56. package/dist/components/views/planningScope.js +25 -0
  57. package/dist/components/views/planningScope.test.d.ts +1 -0
  58. package/dist/components/views/planningScope.test.js +60 -0
  59. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +27 -14
  60. package/dist/components/views/standalone/modals/AccountHubModal.js +87 -15
  61. package/dist/components/views/standalone/modals/AccountHubModal.test.js +42 -2
  62. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  63. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  64. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  65. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  66. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  67. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  68. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  69. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  70. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  71. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  72. package/dist/config/envSchema.js +1 -1
  73. package/dist/core/AiProfileService.d.ts +45 -3
  74. package/dist/core/AiProfileService.js +774 -63
  75. package/dist/core/AiProfileService.test.js +22 -2
  76. package/dist/core/AiProfiles.test.js +761 -2
  77. package/dist/core/AttachmentLinkService.js +57 -15
  78. package/dist/core/AuthIdentityService.d.ts +107 -0
  79. package/dist/core/AuthIdentityService.js +284 -0
  80. package/dist/core/AuthTokenService.d.ts +4 -0
  81. package/dist/core/AuthTokenService.js +34 -6
  82. package/dist/core/AuthTokenService.test.d.ts +1 -0
  83. package/dist/core/AuthTokenService.test.js +78 -0
  84. package/dist/core/EntitlementsPolicy.test.js +77 -15
  85. package/dist/core/GlobalSettingsService.js +115 -6
  86. package/dist/core/PlanEntitlementService.d.ts +30 -3
  87. package/dist/core/PlanEntitlementService.js +454 -93
  88. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  89. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  90. package/dist/core/PlanVersionPolicy.test.js +54 -28
  91. package/dist/core/PlanningEntities.test.js +52 -0
  92. package/dist/core/SignupMonetizationSettings.test.js +241 -44
  93. package/dist/core/SyncReconciliationService.js +2 -65
  94. package/dist/core/SystemAdmin.test.js +244 -88
  95. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  96. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  97. package/dist/core/Taskforce.d.ts +228 -14
  98. package/dist/core/Taskforce.js +814 -201
  99. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  100. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  101. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  102. package/dist/core/WorkspaceLifecycleService.js +0 -5
  103. package/dist/core/WorkspacePermissions.test.js +25 -1
  104. package/dist/core/WorkspacePlanMode.test.js +128 -27
  105. package/dist/core/shared.d.ts +3 -1
  106. package/dist/core/shared.js +16 -1
  107. package/dist/core/types.d.ts +68 -2
  108. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  109. package/dist/hooks/sync/auth.js +3 -1
  110. package/dist/hooks/sync/bootstrap.js +2 -0
  111. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  112. package/dist/hooks/sync/controlPlane.js +78 -0
  113. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  114. package/dist/hooks/sync/controlPlane.test.js +121 -0
  115. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  116. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  117. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  118. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  119. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  120. package/dist/hooks/sync/orchestratorShared.js +68 -47
  121. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  122. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  123. package/dist/hooks/sync/pullLifecycle.js +24 -0
  124. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  125. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  126. package/dist/hooks/sync/recovery.d.ts +16 -2
  127. package/dist/hooks/sync/recovery.js +171 -53
  128. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  129. package/dist/hooks/sync/recovery.test.js +292 -0
  130. package/dist/hooks/sync/transfers.d.ts +16 -3
  131. package/dist/hooks/sync/transfers.js +186 -65
  132. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  133. package/dist/hooks/sync/transfers.test.js +396 -0
  134. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  135. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  136. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  137. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  138. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  139. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  140. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  141. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  142. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  143. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  144. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  145. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  146. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  147. package/dist/hooks/useSyncOrchestrator.d.ts +16 -6
  148. package/dist/hooks/useSyncOrchestrator.js +424 -175
  149. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +487 -10
  150. package/dist/hooks/useTaskData.js +8 -3
  151. package/dist/hooks/useTaskData.test.js +45 -0
  152. package/dist/hooks/useTaskMutations.d.ts +2 -1
  153. package/dist/hooks/useTaskMutations.js +12 -1
  154. package/dist/hooks/useTaskMutations.test.js +30 -1
  155. package/dist/hooks/useTaskforce.d.ts +65 -4
  156. package/dist/hooks/useTaskforce.js +564 -243
  157. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  158. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  159. package/dist/hooks/useTaskforce.sync-behavior.test.js +2454 -208
  160. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  161. package/dist/hooks/useWorkspaceSyncController.js +17 -2
  162. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  163. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  164. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  165. package/dist/localization/locales/en-US.d.ts +0 -1
  166. package/dist/localization/locales/en-US.js +0 -1
  167. package/dist/localization/locales/es-419.js +0 -1
  168. package/dist/localization/locales/pt-BR.js +0 -1
  169. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  170. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  171. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  172. package/dist/mcp/collaborationRegistrar.js +71 -0
  173. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  174. package/dist/mcp/documentAssetRegistrar.js +346 -0
  175. package/dist/mcp/runtime.d.ts +2 -0
  176. package/dist/mcp/runtime.js +2433 -3534
  177. package/dist/mcp/runtime.test.js +440 -2
  178. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  179. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  180. package/dist/mcp/toolCatalog.d.ts +35 -0
  181. package/dist/mcp/toolCatalog.js +3 -0
  182. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  183. package/dist/migrations/taskSchemaMigrations.js +171 -61
  184. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  185. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  186. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  187. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  188. package/dist/resources/templates/workflows/plan.yaml +1 -1
  189. package/dist/resources/templates/workflows/review.yaml +2 -2
  190. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  191. package/dist/server/auth/providers/apple.d.ts +33 -0
  192. package/dist/server/auth/providers/apple.js +82 -0
  193. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  194. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  195. package/dist/server/auth/providers/github.d.ts +26 -0
  196. package/dist/server/auth/providers/github.js +86 -0
  197. package/dist/server/auth/providers/google.d.ts +21 -0
  198. package/dist/server/auth/providers/google.js +56 -0
  199. package/dist/server/auth/providers/types.d.ts +21 -0
  200. package/dist/server/auth/providers/types.js +1 -0
  201. package/dist/server/auth.d.ts +2 -0
  202. package/dist/server/auth.js +6 -3
  203. package/dist/server/documentReviewRoutes.test.js +36 -3
  204. package/dist/server/index.cookieProxy.test.js +1 -0
  205. package/dist/server/index.d.ts +12 -0
  206. package/dist/server/index.js +120 -9
  207. package/dist/server/index.rateLimit.test.js +3 -0
  208. package/dist/server/index.test.js +106 -3
  209. package/dist/server/routes/admin.d.ts +4 -5
  210. package/dist/server/routes/admin.js +410 -80
  211. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  212. package/dist/server/routes/annotatedAttachments.js +1 -1
  213. package/dist/server/routes/auth.d.ts +16 -6
  214. package/dist/server/routes/auth.js +1015 -118
  215. package/dist/server/routes/authSupport.d.ts +30 -0
  216. package/dist/server/routes/authSupport.js +81 -0
  217. package/dist/server/routes/billing.d.ts +1 -1
  218. package/dist/server/routes/billing.js +276 -34
  219. package/dist/server/routes/billing.test.js +760 -52
  220. package/dist/server/routes/documentReviews.d.ts +1 -1
  221. package/dist/server/routes/documentReviews.js +1 -1
  222. package/dist/server/routes/documents.d.ts +5 -2
  223. package/dist/server/routes/documents.js +242 -74
  224. package/dist/server/routes/primitives.d.ts +11 -0
  225. package/dist/server/routes/primitives.js +73 -0
  226. package/dist/server/routes/resources.d.ts +1 -1
  227. package/dist/server/routes/resources.js +1 -1
  228. package/dist/server/routes/shared.d.ts +29 -3
  229. package/dist/server/routes/shared.js +69 -4
  230. package/dist/server/routes/sync.d.ts +1 -1
  231. package/dist/server/routes/sync.integration.test.js +437 -39
  232. package/dist/server/routes/sync.js +62 -606
  233. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  234. package/dist/server/routes/syncAuxRoutes.js +91 -0
  235. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  236. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  237. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  238. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  239. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  240. package/dist/server/routes/syncPushRoutes.js +212 -0
  241. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  242. package/dist/server/routes/syncRouteGuards.js +67 -0
  243. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  244. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  245. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  246. package/dist/server/routes/syncRouteTypes.js +1 -0
  247. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  248. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  249. package/dist/server/routes/tasks.d.ts +1 -1
  250. package/dist/server/routes/tasks.js +44 -2
  251. package/dist/server/routes/workspaces.d.ts +1 -1
  252. package/dist/server/routes/workspaces.js +37 -6
  253. package/dist/server/routes.d.ts +2 -9
  254. package/dist/server/routes.js +136 -165
  255. package/dist/server/routes.test.js +2457 -162
  256. package/dist/services/workflowExportSnapshots.test.js +2 -0
  257. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  258. package/dist/shared/aiProfileSeatScope.js +21 -0
  259. package/dist/shared/passwordPolicy.d.ts +13 -0
  260. package/dist/shared/passwordPolicy.js +84 -0
  261. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  262. package/dist/shared/passwordPolicy.test.js +34 -0
  263. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  264. package/dist/storage/workspaceAssetStore.js +132 -44
  265. package/dist/storage/workspaceAssetStore.test.js +93 -6
  266. package/dist/styles/fonts.css +70 -0
  267. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  268. package/dist/sync/collaborationSyncPayload.js +17 -11
  269. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  270. package/dist/sync/contentSyncState.d.ts +4 -8
  271. package/dist/sync/contentSyncState.js +33 -77
  272. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  273. package/dist/sync/syncApplyHandlers.js +28 -13
  274. package/dist/sync/syncApplyHandlers.test.js +72 -3
  275. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  276. package/dist/sync/syncFieldSemantics.js +55 -0
  277. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  278. package/dist/sync/syncResourceAdapters.js +77 -0
  279. package/dist/sync/syncService.d.ts +6 -0
  280. package/dist/sync/syncService.js +60 -0
  281. package/dist/sync/syncService.test.js +71 -3
  282. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  283. package/dist/sync/taskSyncChangeKey.js +143 -0
  284. package/dist/sync/taskSyncPayload.js +3 -8
  285. package/dist/sync/workspacePullFeed.js +4 -3
  286. package/dist/sync/workspacePullFeed.test.js +43 -0
  287. package/dist/sync/workspaceRepair.js +5 -2
  288. package/dist/sync/workspaceSyncModel.d.ts +48 -0
  289. package/dist/sync/workspaceSyncModel.js +195 -31
  290. package/dist/sync/workspaceSyncModel.test.js +325 -12
  291. package/dist/sync/workspaceSyncState.d.ts +19 -0
  292. package/dist/sync/workspaceSyncState.js +3 -1
  293. package/dist/sync/workspaceSyncSurface.js +3 -8
  294. package/dist/types.d.ts +17 -0
  295. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  296. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  297. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  298. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  299. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  300. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  301. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  302. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  303. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-9E6ParrY.js} +1 -1
  304. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  305. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  306. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  307. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  308. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  309. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  310. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +9 -0
  311. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  312. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  313. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  314. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-DuEd_65c.js} +1 -1
  315. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  316. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  317. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  318. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  319. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  320. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  321. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  322. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  323. package/dist/ui/fonts/Noir_medium.otf +0 -0
  324. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  325. package/dist/ui/fonts/Noir_regular.otf +0 -0
  326. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  327. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  328. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  329. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  330. package/dist/ui/index.html +3 -3
  331. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  332. package/dist/utils/assignees.d.ts +2 -0
  333. package/dist/utils/assignees.js +2 -0
  334. package/dist/utils/avatarUpload.d.ts +27 -0
  335. package/dist/utils/avatarUpload.js +132 -0
  336. package/dist/utils/avatarUpload.test.d.ts +1 -0
  337. package/dist/utils/avatarUpload.test.js +40 -0
  338. package/dist/utils/bootstrapTrace.d.ts +7 -0
  339. package/dist/utils/bootstrapTrace.js +29 -0
  340. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  341. package/dist/utils/commercialLifecyclePresentation.js +199 -0
  342. package/dist/utils/contextAssetEvents.d.ts +2 -0
  343. package/dist/utils/syncEventDetails.d.ts +3 -0
  344. package/dist/utils/syncEventDetails.js +64 -0
  345. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  346. package/dist/utils/syncEventDetails.test.js +23 -0
  347. package/dist/utils/syncEventPresentation.d.ts +15 -0
  348. package/dist/utils/syncEventPresentation.js +129 -0
  349. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  350. package/dist/utils/syncEventPresentation.test.js +64 -0
  351. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  352. package/dist/utils/syncStatusPresentation.js +217 -0
  353. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  354. package/dist/utils/syncStatusPresentation.test.js +164 -0
  355. package/dist/utils/taskActivity.d.ts +2 -0
  356. package/dist/utils/taskActivity.js +228 -22
  357. package/dist/utils/taskActivity.test.js +119 -8
  358. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  359. package/dist/utils/workspaceSyncPresentation.js +68 -2
  360. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  361. package/package.json +10 -2
  362. package/scripts/check-cloud-mcp.mjs +83 -0
  363. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  364. package/scripts/playwright-auth.sh +5 -1
  365. package/scripts/run-billing-performance-smoke.sh +24 -0
  366. package/scripts/run-e2e-realtime.sh +17 -3
  367. package/scripts/run-smoke.sh +17 -5
  368. package/scripts/run-soak.sh +17 -5
  369. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  370. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  371. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  372. package/src/resources/templates/workflows/plan.yaml +1 -1
  373. package/src/resources/templates/workflows/review.yaml +2 -2
  374. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +0 -1
  375. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  376. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  377. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  378. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  379. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  380. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  381. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
  382. package/dist/ui/assets/index-Ii0B0rTO.css +0 -1
  383. package/scripts/migrate-planning-model.ts +0 -233
@@ -1,6 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'http';
2
2
  import type { TaskforceCore } from '../../core/Taskforce.js';
3
- import type { RouteHandler } from '../routes.js';
3
+ import { type RouteHandler } from './primitives.js';
4
4
  export interface DocumentReviewRouteDependencies {
5
5
  addRoute: (method: string, path: string, handler: RouteHandler) => void;
6
6
  core: TaskforceCore;
@@ -1,4 +1,4 @@
1
- import { parseJsonBody } from '../routes.js';
1
+ import { parseJsonBody } from './primitives.js';
2
2
  import { resolveErrorStatusCode } from './shared.js';
3
3
  function writeJson(res, statusCode, payload) {
4
4
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
@@ -15,6 +15,7 @@
15
15
  * GET /api/taskforce/context-metadata
16
16
  * POST /api/taskforce/context/purge-expired
17
17
  * GET /api/taskforce/documents
18
+ * POST /api/taskforce/documents/:assetId/attach
18
19
  * DELETE /api/taskforce/documents/:assetId
19
20
  * PATCH /api/taskforce/documents/:assetId
20
21
  * POST /api/taskforce/documents/save
@@ -28,7 +29,7 @@ import { ObjectStorageClient } from '../../storage/objectStorageClient.js';
28
29
  import type { DocumentRegistryEntry } from '../../storage/documentRegistry.js';
29
30
  import { TaskAssetStore } from '../../storage/taskAssetStore.js';
30
31
  import { WorkspaceAssetStore, type WorkspaceAssetEntry, type WorkspaceAssetKind } from '../../storage/workspaceAssetStore.js';
31
- import type { RouteContext, RouteHandler } from '../routes.js';
32
+ import { type RouteContext, type RouteHandler } from './primitives.js';
32
33
  type AssetMetadata = DocumentRegistryEntry & {
33
34
  quarantineStatus?: 'none' | 'quarantined' | 'released';
34
35
  workspaceId?: string;
@@ -51,7 +52,9 @@ export interface DocumentRouteDependencies {
51
52
  resolveEffectiveObjectStorageConfig: () => ResolvedObjectStorageConfig;
52
53
  getObjectStorageClient: () => ObjectStorageClient | null;
53
54
  resolveStorageWorkspaceId: (req: IncomingMessage, body?: any) => string | null;
54
- ensureContextUploadAllowed: (req: IncomingMessage, res: ServerResponse, workspaceId: string) => boolean;
55
+ ensureContextUploadAllowed: (req: IncomingMessage, res: ServerResponse, workspaceId: string, options?: {
56
+ requestedBytes?: number | null;
57
+ }) => boolean;
55
58
  parseStorageFolderSegments: (folderPathRaw: unknown) => string[] | null;
56
59
  buildCanonicalWorkspaceAssetRelativePath: (params: {
57
60
  workspaceIdRaw: unknown;
@@ -15,6 +15,7 @@
15
15
  * GET /api/taskforce/context-metadata
16
16
  * POST /api/taskforce/context/purge-expired
17
17
  * GET /api/taskforce/documents
18
+ * POST /api/taskforce/documents/:assetId/attach
18
19
  * DELETE /api/taskforce/documents/:assetId
19
20
  * PATCH /api/taskforce/documents/:assetId
20
21
  * POST /api/taskforce/documents/save
@@ -29,21 +30,122 @@ import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
29
30
  import { parsePlan } from '../../utils/planParser.js';
30
31
  import { getCanonicalDocumentName, getReadableDocumentName, isGeneratedDocumentName } from '../../utils/documentNames.js';
31
32
  import { requestRuntimeMode } from './shared.js';
32
- import { parseJsonBody } from '../routes.js';
33
+ import { parseJsonBody } from './primitives.js';
34
+ import { resolveDeploymentConfig } from '../../config/deployment.js';
35
+ function isUserProfileAvatarPath(relativePath) {
36
+ return /^users\/[^/]+\/profile\/avatar\/[^/]+$/i.test(String(relativePath || '').replace(/\\/g, '/'));
37
+ }
38
+ function isAiProfileAvatarPath(relativePath) {
39
+ return /^workspaces\/[^/]+\/assets\/images\/ai-profiles\/[^/]+\/avatar\/[^/]+$/i.test(String(relativePath || '').replace(/\\/g, '/'));
40
+ }
41
+ function resolveRequestOrigin(req) {
42
+ const host = String(req.headers.host || '').trim();
43
+ if (!host)
44
+ return '';
45
+ const proto = String(req.headers['x-forwarded-proto'] || '').trim().toLowerCase() === 'https'
46
+ ? 'https'
47
+ : (req.socket?.encrypted ? 'https' : 'http');
48
+ return `${proto}://${host.replace(/\/+$/, '')}`;
49
+ }
50
+ async function proxyCloudUserProfileAvatar(req, res, relativePath, contentTypeForExt) {
51
+ if (!isUserProfileAvatarPath(relativePath))
52
+ return false;
53
+ const deployment = resolveDeploymentConfig(process.env);
54
+ const cloudBaseUrl = String(deployment.cloudBaseUrl || deployment.cloudAuthBaseUrl || '').trim().replace(/\/+$/, '');
55
+ if (!cloudBaseUrl)
56
+ return false;
57
+ if (cloudBaseUrl === resolveRequestOrigin(req))
58
+ return false;
59
+ const requestUrl = new URL(req.url || '', 'http://localhost');
60
+ const upstreamUrl = `${cloudBaseUrl}/api/taskforce/context/${encodeURIComponent(relativePath)}${requestUrl.search || ''}`;
61
+ const headers = new Headers();
62
+ const cookie = String(req.headers.cookie || '').trim();
63
+ const accept = String(req.headers.accept || '').trim();
64
+ const userAgent = String(req.headers['user-agent'] || '').trim();
65
+ if (cookie)
66
+ headers.set('cookie', cookie);
67
+ if (accept)
68
+ headers.set('accept', accept);
69
+ if (userAgent)
70
+ headers.set('user-agent', userAgent);
71
+ headers.set('x-taskforce-origin-runtime', 'local-proxy');
72
+ const cfClientId = String(process.env.TASKFORCE_CF_ACCESS_CLIENT_ID || process.env.CF_ACCESS_CLIENT_ID || '').trim();
73
+ const cfClientSecret = String(process.env.TASKFORCE_CF_ACCESS_CLIENT_SECRET || process.env.CF_ACCESS_CLIENT_SECRET || '').trim();
74
+ if (cfClientId && cfClientSecret) {
75
+ headers.set('CF-Access-Client-Id', cfClientId);
76
+ headers.set('CF-Access-Client-Secret', cfClientSecret);
77
+ }
78
+ try {
79
+ const upstream = await fetch(upstreamUrl, {
80
+ method: 'GET',
81
+ headers
82
+ });
83
+ if (upstream.status === 404)
84
+ return false;
85
+ const outgoingHeaders = {};
86
+ for (const [key, value] of upstream.headers.entries()) {
87
+ const name = key.toLowerCase();
88
+ if (name === 'set-cookie' || name === 'content-length' || name === 'transfer-encoding')
89
+ continue;
90
+ outgoingHeaders[key] = value;
91
+ }
92
+ if (!outgoingHeaders['content-type']) {
93
+ outgoingHeaders['Content-Type'] = contentTypeForExt(path.extname(relativePath).toLowerCase());
94
+ }
95
+ res.writeHead(upstream.status, outgoingHeaders);
96
+ res.end(Buffer.from(await upstream.arrayBuffer()));
97
+ return true;
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
33
103
  export function registerDocumentRoutes(deps) {
34
104
  const { addRoute, core, context, requestWorkspaceId, ensureAdminRole, resolveEffectiveObjectStorageConfig, getObjectStorageClient, resolveStorageWorkspaceId, ensureContextUploadAllowed, parseStorageFolderSegments, buildCanonicalWorkspaceAssetRelativePath, inferAssetKindFromPath, appendStorageTelemetry, scanAndRecordDocument, buildTaskAttachmentPayload, workspaceAssetStore, taskAssetStore, normalizeDocumentPathInput, getAssetMetadata, getCanonicalAssetMetadata, documentRetentionDays, appendDocumentPurgeHistory, markAssetDeleted, isAssetDeleted, clearAssetDeleted, ensureWithinDir, sanitizeDownloadFilename, contentTypeForExt, inferInlineDocumentFromPath, resolveCanonicalDocumentRead, inferWorkspaceIdFromStoragePath, inferTaskIdFromStoragePath, } = deps;
105
+ const removeDocumentAttachmentFromTaskAttachments = (attachmentsRaw, asset) => {
106
+ const canonicalDocumentPath = `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`;
107
+ const legacyContextPath = `/api/taskforce/context/${encodeURIComponent(asset.storageKey)}`;
108
+ if (!Array.isArray(attachmentsRaw))
109
+ return [];
110
+ return attachmentsRaw.filter((attachment) => {
111
+ if (typeof attachment === 'string') {
112
+ const normalized = attachment.trim();
113
+ if (!normalized)
114
+ return false;
115
+ return normalized !== canonicalDocumentPath
116
+ && normalized !== legacyContextPath
117
+ && normalized !== asset.storageKey;
118
+ }
119
+ if (!attachment || typeof attachment !== 'object')
120
+ return false;
121
+ const candidate = attachment;
122
+ const candidatePath = String(candidate.path || '').trim();
123
+ const candidateFsPath = String(candidate.fsPath || '').trim();
124
+ const candidateAssetId = String(candidate.assetId || '').trim();
125
+ if (candidateAssetId && candidateAssetId === asset.assetId)
126
+ return false;
127
+ if (candidatePath === canonicalDocumentPath || candidatePath === legacyContextPath)
128
+ return false;
129
+ if (candidateFsPath && candidateFsPath === asset.storageKey)
130
+ return false;
131
+ return !!candidatePath;
132
+ });
133
+ };
35
134
  addRoute('POST', '/api/taskforce/context-upload/init', async (req, res) => {
36
135
  const resolvedObjectStorage = resolveEffectiveObjectStorageConfig();
37
136
  const objectStorageClient = getObjectStorageClient();
38
137
  const body = await parseJsonBody(req);
39
138
  const { originalName, mimeType, taskId, folderPath } = body || {};
40
139
  const workspaceId = resolveStorageWorkspaceId(req, body);
140
+ const requestedBytes = Number(body?.size);
41
141
  if (!workspaceId) {
42
142
  res.writeHead(400, { 'Content-Type': 'application/json' });
43
143
  res.end(JSON.stringify({ error: 'workspaceId is required and cannot be default for uploads.' }));
44
144
  return;
45
145
  }
46
- if (!ensureContextUploadAllowed(req, res, workspaceId))
146
+ if (!ensureContextUploadAllowed(req, res, workspaceId, {
147
+ requestedBytes: Number.isFinite(requestedBytes) ? requestedBytes : null
148
+ }))
47
149
  return;
48
150
  const folderSegments = parseStorageFolderSegments(folderPath);
49
151
  if (folderSegments === null) {
@@ -144,6 +246,10 @@ export function registerDocumentRoutes(deps) {
144
246
  res.end(JSON.stringify({ error: 'Uploaded file not found.' }));
145
247
  return;
146
248
  }
249
+ if (!ensureContextUploadAllowed(req, res, workspaceId, {
250
+ requestedBytes: object.body.length
251
+ }))
252
+ return;
147
253
  try {
148
254
  const taskId = typeof body?.taskId === 'string' && body.taskId.trim().length > 0
149
255
  ? body.taskId.trim()
@@ -191,8 +297,6 @@ export function registerDocumentRoutes(deps) {
191
297
  res.end(JSON.stringify({ error: 'workspaceId is required and cannot be default for uploads.' }));
192
298
  return;
193
299
  }
194
- if (!ensureContextUploadAllowed(req, res, workspaceId))
195
- return;
196
300
  const folderSegments = parseStorageFolderSegments(folderPath);
197
301
  if (folderSegments === null) {
198
302
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -233,6 +337,10 @@ export function registerDocumentRoutes(deps) {
233
337
  const actualFilename = `${safeBase}-${Date.now()}${extension}`;
234
338
  const { basePath, projectRoot } = core.getPaths();
235
339
  const buffer = Buffer.from(base64Data, 'base64');
340
+ if (!ensureContextUploadAllowed(req, res, workspaceId, {
341
+ requestedBytes: buffer.length
342
+ }))
343
+ return;
236
344
  const relativeToStorage = buildCanonicalWorkspaceAssetRelativePath({
237
345
  workspaceIdRaw: workspaceId,
238
346
  kind: mimeType.startsWith('image/') ? 'image' : inferAssetKindFromPath(actualFilename),
@@ -358,9 +466,14 @@ export function registerDocumentRoutes(deps) {
358
466
  return;
359
467
  }
360
468
  if (objectStorageClient && normalizedRelative && resolvedObjectStorage.provider === 'r2' && resolvedObjectStorage.r2) {
361
- if (shouldProxyInlineDocument) {
469
+ const shouldProxyInlineUserAvatar = isUserProfileAvatarPath(normalizedRelative);
470
+ const shouldProxyInlineAiAvatar = isAiProfileAvatarPath(normalizedRelative);
471
+ if (shouldProxyInlineDocument || shouldProxyInlineUserAvatar || shouldProxyInlineAiAvatar) {
362
472
  const object = await objectStorageClient.getObject(normalizedRelative);
363
473
  if (!object) {
474
+ if (shouldProxyInlineUserAvatar && await proxyCloudUserProfileAvatar(req, res, normalizedRelative, contentTypeForExt)) {
475
+ return;
476
+ }
364
477
  res.writeHead(404, { 'Content-Type': 'text/plain' });
365
478
  res.end('Context file not found');
366
479
  return;
@@ -385,6 +498,9 @@ export function registerDocumentRoutes(deps) {
385
498
  res.end(fs.readFileSync(filePath));
386
499
  return;
387
500
  }
501
+ if (normalizedRelative && await proxyCloudUserProfileAvatar(req, res, normalizedRelative, contentTypeForExt)) {
502
+ return;
503
+ }
388
504
  res.writeHead(404, { 'Content-Type': 'text/plain' });
389
505
  res.end('Context file not found');
390
506
  });
@@ -552,9 +668,6 @@ export function registerDocumentRoutes(deps) {
552
668
  });
553
669
  addRoute('GET', '/api/taskforce/documents', async (req, res) => {
554
670
  const workspaceId = requestWorkspaceId(req);
555
- const { basePath, projectRoot } = core.getPaths();
556
- const planningDocsDir = path.join(basePath, 'docs');
557
- const runtimeMode = context.authConfig?.runtimeMode === 'cloud' ? 'cloud' : 'local';
558
671
  function inferDocType(filename) {
559
672
  const base = path.basename(filename, path.extname(filename)).toLowerCase();
560
673
  if (base.includes('implementation-plan') || base.includes('implementation_plan'))
@@ -592,12 +705,9 @@ export function registerDocumentRoutes(deps) {
592
705
  : (taskId ? 1 : 0);
593
706
  const attachmentState = input.attachmentState === 'attached'
594
707
  || input.attachmentState === 'unattached'
595
- || input.attachmentState === 'standalone'
596
708
  ? (attachmentCount > 0 || taskId
597
709
  ? 'attached'
598
- : input.attachmentState === 'standalone'
599
- ? 'standalone'
600
- : 'unattached')
710
+ : 'unattached')
601
711
  : (attachmentCount > 0 || taskId ? 'attached' : 'unattached');
602
712
  return {
603
713
  id: input.id,
@@ -620,12 +730,6 @@ export function registerDocumentRoutes(deps) {
620
730
  attachmentState
621
731
  };
622
732
  }
623
- function safeRelative(fullPath) {
624
- const rel = path.relative(projectRoot, fullPath);
625
- if (rel.startsWith('..'))
626
- return null;
627
- return rel;
628
- }
629
733
  const documents = [];
630
734
  const seenDocumentPaths = new Set();
631
735
  const pushDocument = (doc) => {
@@ -636,6 +740,9 @@ export function registerDocumentRoutes(deps) {
636
740
  seenDocumentPaths.add(key);
637
741
  documents.push(normalizedDoc);
638
742
  };
743
+ // Shared document listing is canonical-document-only. Raw files under
744
+ // .taskforce/docs are intentionally excluded because they are not
745
+ // workspace-scoped managed documents.
639
746
  const canonicalDocuments = workspaceAssetStore?.listAllByWorkspace(workspaceId, {
640
747
  includeDeleted: false,
641
748
  kind: 'document'
@@ -651,8 +758,11 @@ export function registerDocumentRoutes(deps) {
651
758
  });
652
759
  const activeLinks = (workspaceAssetStore?.listLinksForAsset(asset.assetId, workspaceId) || [])
653
760
  .filter((link) => !link.deletedAt)
761
+ .filter((link) => link.targetType !== 'task' || (!!link.taskId && !!core.getTask(link.taskId, workspaceId)));
762
+ const activeTaskLinks = activeLinks
763
+ .filter((link) => link.targetType === 'task' && !!link.taskId)
654
764
  .filter((link) => !!core.getTask(link.taskId, workspaceId));
655
- const firstLink = activeLinks[0] || null;
765
+ const firstLink = activeTaskLinks[0] || null;
656
766
  const repairedLogicalName = isGeneratedDocumentName(asset.logicalName || null, asset.assetId || null)
657
767
  ? (String(firstLink?.displayName || '').trim()
658
768
  || repairedReadableName)
@@ -704,58 +814,6 @@ export function registerDocumentRoutes(deps) {
704
814
  attachmentState: activeLinks.length > 0 ? 'attached' : 'unattached',
705
815
  });
706
816
  }
707
- if (runtimeMode !== 'cloud') {
708
- const walkMarkdownFiles = (dir, visitor) => {
709
- let entries = [];
710
- try {
711
- entries = fs.readdirSync(dir, { withFileTypes: true });
712
- }
713
- catch {
714
- return;
715
- }
716
- for (const entry of entries) {
717
- const fullPath = path.join(dir, entry.name);
718
- if (entry.isDirectory()) {
719
- walkMarkdownFiles(fullPath, visitor);
720
- continue;
721
- }
722
- if (!entry.isFile() || path.extname(entry.name).toLowerCase() !== '.md') {
723
- continue;
724
- }
725
- let stat;
726
- try {
727
- stat = fs.statSync(fullPath);
728
- }
729
- catch {
730
- continue;
731
- }
732
- const relativePath = safeRelative(fullPath);
733
- if (!relativePath)
734
- continue;
735
- visitor({ relativePath, entryName: entry.name, stat });
736
- }
737
- };
738
- walkMarkdownFiles(planningDocsDir, ({ relativePath, entryName, stat }) => {
739
- pushDocument({
740
- id: relativePath,
741
- assetId: null,
742
- referenceNumber: null,
743
- referenceLabel: null,
744
- fsPath: relativePath,
745
- apiUrl: `/api/taskforce/context-link?path=${encodeURIComponent(relativePath)}`,
746
- taskId: null,
747
- displayName: inferTitle(entryName),
748
- originalFilename: entryName,
749
- title: inferTitle(entryName),
750
- docType: 'planning',
751
- sizeBytes: stat.size,
752
- updatedAt: stat.mtime.toISOString(),
753
- editable: true,
754
- attachmentCount: 0,
755
- attachmentState: 'standalone',
756
- });
757
- });
758
- }
759
817
  documents.sort((a, b) => {
760
818
  if (!a.updatedAt && !b.updatedAt)
761
819
  return 0;
@@ -768,9 +826,90 @@ export function registerDocumentRoutes(deps) {
768
826
  res.writeHead(200, { 'Content-Type': 'application/json' });
769
827
  res.end(JSON.stringify({ documents }));
770
828
  });
829
+ addRoute('POST', '/api/taskforce/documents/:assetId/attach', async (req, res, params) => {
830
+ const workspaceId = requestWorkspaceId(req);
831
+ const assetId = String(params.assetId || '').trim();
832
+ const body = await parseJsonBody(req);
833
+ const targetType = String(body?.targetType || 'task').trim();
834
+ const targetId = String(body?.targetId || '').trim();
835
+ if (!workspaceAssetStore || !assetId) {
836
+ res.writeHead(404, { 'Content-Type': 'application/json' });
837
+ res.end(JSON.stringify({ error: 'Document not found.' }));
838
+ return;
839
+ }
840
+ if (!targetId) {
841
+ res.writeHead(400, { 'Content-Type': 'application/json' });
842
+ res.end(JSON.stringify({ error: 'Missing required targetId.' }));
843
+ return;
844
+ }
845
+ if (targetType !== 'task') {
846
+ res.writeHead(400, { 'Content-Type': 'application/json' });
847
+ res.end(JSON.stringify({ error: 'Only task attachments are supported from the document manager right now.' }));
848
+ return;
849
+ }
850
+ const asset = workspaceAssetStore.get(assetId, workspaceId);
851
+ if (!asset || asset.kind !== 'document' || asset.deletedAt) {
852
+ res.writeHead(404, { 'Content-Type': 'application/json' });
853
+ res.end(JSON.stringify({ error: 'Document not found.' }));
854
+ return;
855
+ }
856
+ const task = core.getTask(targetId, workspaceId);
857
+ if (!task) {
858
+ res.writeHead(404, { 'Content-Type': 'application/json' });
859
+ res.end(JSON.stringify({ error: 'Task not found.' }));
860
+ return;
861
+ }
862
+ const existingLinks = workspaceAssetStore.listLinksForTarget('task', targetId, workspaceId)
863
+ .filter((link) => !link.deletedAt);
864
+ const alreadyAttached = existingLinks.some((link) => link.assetId === asset.assetId && link.role === 'attachment');
865
+ if (alreadyAttached) {
866
+ res.writeHead(200, { 'Content-Type': 'application/json' });
867
+ res.end(JSON.stringify({
868
+ success: true,
869
+ alreadyAttached: true,
870
+ targetType: 'task',
871
+ targetId,
872
+ attachmentCount: existingLinks.length,
873
+ }));
874
+ return;
875
+ }
876
+ const payload = buildTaskAttachmentPayload(asset.storageKey, {
877
+ caption: getCanonicalDocumentName({
878
+ assetId: asset.assetId,
879
+ fsPath: asset.storageKey,
880
+ logicalName: asset.logicalName || null,
881
+ title: asset.logicalName || asset.originalFilename,
882
+ originalFilename: asset.originalFilename,
883
+ }),
884
+ assetId: asset.assetId,
885
+ taskId: targetId,
886
+ });
887
+ if (!payload) {
888
+ res.writeHead(500, { 'Content-Type': 'application/json' });
889
+ res.end(JSON.stringify({ error: 'Failed to build attachment payload for this document.' }));
890
+ return;
891
+ }
892
+ core.updateTask(targetId, {
893
+ attachments: [...(Array.isArray(task.attachments) ? task.attachments : []), payload],
894
+ }, workspaceId, {
895
+ actorRef: 'user',
896
+ saveSource: 'manual',
897
+ });
898
+ core.notifyDocumentMetadataMutation(workspaceId, [asset.storageKey], 'upsert');
899
+ res.writeHead(200, { 'Content-Type': 'application/json' });
900
+ res.end(JSON.stringify({
901
+ success: true,
902
+ attached: true,
903
+ alreadyAttached: false,
904
+ targetType: 'task',
905
+ targetId,
906
+ }));
907
+ });
771
908
  addRoute('DELETE', '/api/taskforce/documents/:assetId', async (req, res, params) => {
772
909
  const workspaceId = requestWorkspaceId(req);
773
910
  const assetId = String(params.assetId || '').trim();
911
+ const body = await parseJsonBody(req);
912
+ const forceDetach = body?.forceDetach === true;
774
913
  if (!workspaceAssetStore || !assetId) {
775
914
  res.writeHead(404, { 'Content-Type': 'application/json' });
776
915
  res.end(JSON.stringify({ error: 'Document not found' }));
@@ -784,16 +923,44 @@ export function registerDocumentRoutes(deps) {
784
923
  }
785
924
  const activeLinks = workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId)
786
925
  .filter((link) => !link.deletedAt)
926
+ .filter((link) => link.targetType === 'task' && !!link.taskId)
787
927
  .filter((link) => !!core.getTask(link.taskId, workspaceId));
788
928
  if (activeLinks.length > 0) {
789
- res.writeHead(409, { 'Content-Type': 'application/json' });
790
- res.end(JSON.stringify({ error: 'Document is still attached to one or more tasks.' }));
791
- return;
929
+ if (!forceDetach) {
930
+ res.writeHead(409, { 'Content-Type': 'application/json' });
931
+ res.end(JSON.stringify({
932
+ error: 'Document is still attached to one or more tasks.',
933
+ activeAttachmentCount: activeLinks.length,
934
+ requiresForceDetach: true
935
+ }));
936
+ return;
937
+ }
938
+ for (const link of activeLinks) {
939
+ const taskId = link.taskId;
940
+ const task = core.getTask(taskId, workspaceId);
941
+ if (!task)
942
+ continue;
943
+ const updatedAttachments = removeDocumentAttachmentFromTaskAttachments(task.attachments, asset);
944
+ core.updateTask(taskId, { attachments: updatedAttachments }, workspaceId, {
945
+ actorRef: 'user',
946
+ saveSource: 'manual'
947
+ });
948
+ }
792
949
  }
793
950
  const nowIso = new Date().toISOString();
794
951
  const purgeAfter = new Date(Date.now() + documentRetentionDays * 24 * 60 * 60 * 1000).toISOString();
795
952
  const metadata = markAssetDeleted(asset.storageKey);
796
953
  workspaceAssetStore.markDeleted(asset.assetId, workspaceId, nowIso, purgeAfter);
954
+ const links = workspaceAssetStore.listLinksForAsset(asset.assetId, workspaceId, { includeDeleted: true });
955
+ for (const link of links) {
956
+ workspaceAssetStore.deleteLink({
957
+ workspaceId,
958
+ assetId: asset.assetId,
959
+ targetType: link.targetType,
960
+ targetId: link.targetId,
961
+ role: link.role
962
+ });
963
+ }
797
964
  appendStorageTelemetry('delete.soft', {
798
965
  path: asset.storageKey,
799
966
  status: 'soft-deleted',
@@ -803,6 +970,7 @@ export function registerDocumentRoutes(deps) {
803
970
  res.end(JSON.stringify({
804
971
  success: true,
805
972
  softDeleted: true,
973
+ detachedTaskCount: activeLinks.length,
806
974
  retentionDays: documentRetentionDays,
807
975
  purgeAfter: metadata?.purgeAfter || purgeAfter
808
976
  }));
@@ -0,0 +1,11 @@
1
+ import { IncomingMessage, ServerResponse } from 'http';
2
+ import type { TaskforceAuthConfig } from '../auth.js';
3
+ import type { ResolvedObjectStorageConfig } from '../../storage/objectStorage.js';
4
+ export type RouteHandler = (req: IncomingMessage, res: ServerResponse, params: Record<string, string>) => Promise<void>;
5
+ export interface RouteContext {
6
+ authConfig?: TaskforceAuthConfig;
7
+ objectStorage?: ResolvedObjectStorageConfig;
8
+ }
9
+ export declare function parseJsonBody(req: IncomingMessage, maxBytes?: number): Promise<any>;
10
+ export declare function parseFormBody(req: IncomingMessage, maxBytes?: number): Promise<Record<string, string>>;
11
+ export declare function escapeHtml(value: unknown): string;
@@ -0,0 +1,73 @@
1
+ const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024; // 1MB
2
+ function createHttpError(message, statusCode) {
3
+ const error = new Error(message);
4
+ error.statusCode = statusCode;
5
+ return error;
6
+ }
7
+ export async function parseJsonBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
8
+ const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
9
+ ? Math.floor(maxBytes)
10
+ : DEFAULT_MAX_JSON_BODY_BYTES;
11
+ return new Promise((resolve, reject) => {
12
+ let body = '';
13
+ let receivedBytes = 0;
14
+ req.on('data', chunk => {
15
+ receivedBytes += chunk.length;
16
+ if (receivedBytes > safeMaxBytes) {
17
+ req.pause();
18
+ reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
19
+ return;
20
+ }
21
+ body += chunk;
22
+ });
23
+ req.on('end', () => {
24
+ try {
25
+ resolve(body ? JSON.parse(body) : {});
26
+ }
27
+ catch {
28
+ reject(createHttpError('Invalid JSON', 400));
29
+ }
30
+ });
31
+ req.on('error', reject);
32
+ });
33
+ }
34
+ export async function parseFormBody(req, maxBytes = DEFAULT_MAX_JSON_BODY_BYTES) {
35
+ const safeMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0
36
+ ? Math.floor(maxBytes)
37
+ : DEFAULT_MAX_JSON_BODY_BYTES;
38
+ return new Promise((resolve, reject) => {
39
+ let body = '';
40
+ let receivedBytes = 0;
41
+ req.on('data', chunk => {
42
+ receivedBytes += chunk.length;
43
+ if (receivedBytes > safeMaxBytes) {
44
+ req.pause();
45
+ reject(createHttpError(`Payload too large (max ${safeMaxBytes} bytes)`, 413));
46
+ return;
47
+ }
48
+ body += chunk;
49
+ });
50
+ req.on('end', () => {
51
+ try {
52
+ const params = new URLSearchParams(body);
53
+ const parsed = {};
54
+ params.forEach((value, key) => {
55
+ parsed[String(key)] = String(value);
56
+ });
57
+ resolve(parsed);
58
+ }
59
+ catch {
60
+ reject(createHttpError('Invalid form body', 400));
61
+ }
62
+ });
63
+ req.on('error', reject);
64
+ });
65
+ }
66
+ export function escapeHtml(value) {
67
+ return String(value || '')
68
+ .replace(/&/g, '&amp;')
69
+ .replace(/</g, '&lt;')
70
+ .replace(/>/g, '&gt;')
71
+ .replace(/"/g, '&quot;')
72
+ .replace(/'/g, '&#39;');
73
+ }
@@ -17,7 +17,7 @@
17
17
  * GET /api/taskforce/environments
18
18
  */
19
19
  import type { TaskforceCore } from '../../core/Taskforce.js';
20
- import { type RouteHandler } from '../routes.js';
20
+ import { type RouteHandler } from './primitives.js';
21
21
  export interface ResourceRouteDependencies {
22
22
  addRoute: (method: string, path: string, handler: RouteHandler) => void;
23
23
  core: TaskforceCore;
@@ -19,7 +19,7 @@
19
19
  import * as fs from 'fs';
20
20
  import * as path from 'path';
21
21
  import { execFileSync } from 'child_process';
22
- import { parseJsonBody } from '../routes.js';
22
+ import { parseJsonBody } from './primitives.js';
23
23
  const WORKFLOW_EDITOR_LOCKED_FIELDS = {
24
24
  do: ['aliasesText', 'dispatchDefault', 'criticalToolsText', 'contentDefault'],
25
25
  execute: ['aliasesText', 'dispatchDefault', 'criticalToolsText', 'contentDefault']
@@ -8,9 +8,34 @@
8
8
  import { IncomingMessage, ServerResponse } from 'http';
9
9
  import { TaskforceCore } from '../../core/Taskforce.js';
10
10
  import type { McpAccessTokenAuthResult, McpOAuthAccessTokenAuthResult, McpAccessTokenScope } from '../../core/Taskforce.js';
11
- import type { RouteContext } from '../routes.js';
11
+ import type { RouteContext } from './primitives.js';
12
12
  import type { AiProfileBootstrapConfig } from '../../mcp/aiProfileBootstrap.js';
13
+ import type { AiProfileSeatScope } from '../../shared/aiProfileSeatScope.js';
13
14
  export declare function resolveErrorStatusCode(error: unknown, fallback?: number): number;
15
+ export declare const BOOTSTRAP_ROUTE_SLOW_THRESHOLD_MS = 1500;
16
+ export declare const BOOTSTRAP_ROUTE_SEVERE_THRESHOLD_MS = 5000;
17
+ export type ServerTimingMetric = {
18
+ name: string;
19
+ durationMs: number;
20
+ };
21
+ export declare function getRouteTimingNow(): number;
22
+ export declare function buildServerTimingMetric(name: string, durationMs: number): string;
23
+ export declare function setServerTimingHeader(res: ServerResponse, metrics: ServerTimingMetric[]): void;
24
+ export declare function serializeJsonWithTiming(payload: unknown, startedAt?: number): {
25
+ body: string;
26
+ byteLength: number;
27
+ durationMs: number;
28
+ };
29
+ export declare function requestBootstrapTraceId(req: IncomingMessage): string;
30
+ export declare function logSlowBootstrapRoute(input: {
31
+ route: string;
32
+ workspaceId: string;
33
+ durationMs: number;
34
+ traceId?: string | null;
35
+ thresholdMs?: number;
36
+ severeThresholdMs?: number;
37
+ details?: Record<string, unknown>;
38
+ }): void;
14
39
  export declare function buildCloudMcpRuntimeAuth(input: {
15
40
  req: IncomingMessage;
16
41
  auth: {
@@ -34,6 +59,7 @@ export declare function buildCloudMcpRuntimeAuth(input: {
34
59
  aiProfileProvider?: AiProfileBootstrapConfig['aiProfileProvider'];
35
60
  aiProfileModel?: AiProfileBootstrapConfig['aiProfileModel'];
36
61
  aiProfileSurfaceType?: AiProfileBootstrapConfig['aiProfileSurfaceType'];
62
+ aiProfileSeatScope?: AiProfileSeatScope | null;
37
63
  actorType?: 'local' | 'user' | 'token' | 'oauth';
38
64
  requestId?: string | null;
39
65
  ip?: string | null;
@@ -51,7 +77,7 @@ export declare function makeResolveAuthenticatedUserId(core: TaskforceCore, cont
51
77
  export declare function makeResolveAuthenticatedMutationUserId(core: TaskforceCore, context: RouteContext, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage) => string;
52
78
  export declare function makeEnsureAppAccess(core: TaskforceCore, context: RouteContext): (req: IncomingMessage, res: ServerResponse) => boolean;
53
79
  export declare function makeEnsureAdminRole(core: TaskforceCore, context: RouteContext): (req: IncomingMessage) => boolean;
54
- export declare function makeEnsureAuthenticatedUser(): (req: IncomingMessage) => boolean;
80
+ export declare function makeEnsureAuthenticatedUser(authConfig?: RouteContext['authConfig']): (req: IncomingMessage) => boolean;
55
81
  export declare function ensureOwnerRole(req: IncomingMessage): boolean;
56
82
  export declare function ensureWorkspaceAdminRole(req: IncomingMessage): boolean;
57
83
  export declare function makeResolveAdminRequestActor(core: TaskforceCore, context: RouteContext): (req: IncomingMessage) => {
@@ -60,6 +86,6 @@ export declare function makeResolveAdminRequestActor(core: TaskforceCore, contex
60
86
  systemRole: "system_admin" | "user";
61
87
  };
62
88
  export declare function makeAuditAdminAction(core: TaskforceCore, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
63
- export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
89
+ export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>, workspaceIdOverride?: string | null) => void;
64
90
  export declare function makeRequireCloudRuntime(context: RouteContext): (res: ServerResponse, feature: string) => boolean;
65
91
  export declare function makeEnsureCloudPlanData(context: RouteContext): (req: IncomingMessage, res: ServerResponse) => boolean;