@taskforcehq/taskforce 0.3.305 → 0.3.307

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (440) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +603 -107
  3. package/dist/TaskforceCore.js +216 -43
  4. package/dist/TaskforceCore.test.js +611 -28
  5. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  8. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  10. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  11. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  12. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  13. package/dist/compat/workspaceSyncCompat.js +46 -2
  14. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  15. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  16. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  17. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  18. package/dist/components/context/ContextAttachmentManager.js +561 -0
  19. package/dist/components/features/AgentsModule.d.ts +14 -1
  20. package/dist/components/features/AgentsModule.js +366 -18
  21. package/dist/components/features/AgentsModule.test.js +507 -8
  22. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  23. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  24. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  25. package/dist/components/features/DocumentIndex.d.ts +2 -2
  26. package/dist/components/features/DocumentIndex.js +3 -4
  27. package/dist/components/features/DocumentViewer.d.ts +7 -1
  28. package/dist/components/features/DocumentViewer.js +62 -4
  29. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  30. package/dist/components/features/DocumentWorkspace.js +15 -4
  31. package/dist/components/features/DocumentWorkspace.test.js +51 -1
  32. package/dist/components/features/TaskSettings.d.ts +2 -0
  33. package/dist/components/features/TaskSettings.js +83 -78
  34. package/dist/components/features/TaskSettings.test.js +171 -82
  35. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  36. package/dist/components/task/TaskActionHeader.js +11 -5
  37. package/dist/components/task/TaskActionHeader.test.js +10 -0
  38. package/dist/components/task/TaskCard.js +24 -29
  39. package/dist/components/task/TaskCard.test.js +38 -3
  40. package/dist/components/task/TaskContextUpload.js +4 -526
  41. package/dist/components/task/TaskContextUpload.test.js +3 -3
  42. package/dist/components/task/TaskForm.js +61 -116
  43. package/dist/components/task/TaskForm.test.js +110 -8
  44. package/dist/components/task/TaskKanban.d.ts +2 -1
  45. package/dist/components/task/TaskKanban.js +94 -21
  46. package/dist/components/task/TaskKanban.test.js +36 -0
  47. package/dist/components/task/TaskList.js +5 -1
  48. package/dist/components/task/TaskList.test.js +4 -0
  49. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  50. package/dist/components/ui/EditableAvatarButton.js +18 -0
  51. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  52. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  53. package/dist/components/views/AppShellHeader.d.ts +3 -1
  54. package/dist/components/views/AppShellHeader.js +27 -11
  55. package/dist/components/views/AppShellHeader.test.js +18 -1
  56. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  57. package/dist/components/views/PlanComparisonPage.js +78 -14
  58. package/dist/components/views/PlanComparisonPage.test.js +257 -0
  59. package/dist/components/views/PlansPage.js +127 -36
  60. package/dist/components/views/PlansPage.test.js +294 -12
  61. package/dist/components/views/StandaloneLayout.js +529 -592
  62. package/dist/components/views/WidgetView.js +10 -8
  63. package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
  64. package/dist/components/views/panels/PlanningDrawer.js +270 -55
  65. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  66. package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
  67. package/dist/components/views/planningScope.d.ts +20 -0
  68. package/dist/components/views/planningScope.js +25 -0
  69. package/dist/components/views/planningScope.test.d.ts +1 -0
  70. package/dist/components/views/planningScope.test.js +60 -0
  71. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  72. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  73. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  74. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  75. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  76. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  77. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  78. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  79. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  80. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  81. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  82. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  83. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  84. package/dist/config/envSchema.js +1 -0
  85. package/dist/core/AiProfileService.d.ts +46 -2
  86. package/dist/core/AiProfileService.js +1073 -82
  87. package/dist/core/AiProfileService.test.js +19 -1
  88. package/dist/core/AiProfiles.test.js +1249 -44
  89. package/dist/core/AttachmentLinkService.d.ts +3 -0
  90. package/dist/core/AttachmentLinkService.js +110 -23
  91. package/dist/core/AuthIdentityService.d.ts +107 -0
  92. package/dist/core/AuthIdentityService.js +284 -0
  93. package/dist/core/AuthTokenService.d.ts +4 -0
  94. package/dist/core/AuthTokenService.js +34 -6
  95. package/dist/core/AuthTokenService.test.js +3 -3
  96. package/dist/core/EntitlementsPolicy.test.js +2 -2
  97. package/dist/core/GlobalSettingsService.js +127 -6
  98. package/dist/core/McpTokenService.d.ts +12 -0
  99. package/dist/core/McpTokenService.js +44 -1
  100. package/dist/core/PlanEntitlementService.d.ts +23 -2
  101. package/dist/core/PlanEntitlementService.js +343 -57
  102. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  103. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  104. package/dist/core/PlanVersionPolicy.test.js +35 -2
  105. package/dist/core/PlanningEntities.test.js +418 -4
  106. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  107. package/dist/core/SyncReconciliationService.js +2 -65
  108. package/dist/core/SystemAdmin.test.js +42 -42
  109. package/dist/core/TaskActivityService.d.ts +28 -1
  110. package/dist/core/TaskActivityService.js +227 -46
  111. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  112. package/dist/core/TaskActivitySync.test.js +90 -0
  113. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  114. package/dist/core/TaskAuditTimeline.test.js +96 -0
  115. package/dist/core/Taskforce.d.ts +415 -40
  116. package/dist/core/Taskforce.ids.test.js +58 -0
  117. package/dist/core/Taskforce.js +1543 -266
  118. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  119. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  120. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  121. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  122. package/dist/core/WorkspaceLifecycleService.js +0 -5
  123. package/dist/core/WorkspacePermissions.test.js +25 -1
  124. package/dist/core/WorkspacePlanMode.test.js +117 -16
  125. package/dist/core/shared.d.ts +1 -1
  126. package/dist/core/shared.js +5 -1
  127. package/dist/core/types.d.ts +75 -2
  128. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
  129. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  130. package/dist/hooks/bootstrapTimeouts.js +8 -0
  131. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  132. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  133. package/dist/hooks/sync/bootstrap.js +2 -0
  134. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  135. package/dist/hooks/sync/controlPlane.js +78 -0
  136. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  137. package/dist/hooks/sync/controlPlane.test.js +121 -0
  138. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  139. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  140. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  141. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  142. package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
  143. package/dist/hooks/sync/orchestratorShared.js +129 -48
  144. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  145. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  146. package/dist/hooks/sync/pullLifecycle.js +24 -0
  147. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  148. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  149. package/dist/hooks/sync/recovery.d.ts +16 -2
  150. package/dist/hooks/sync/recovery.js +179 -53
  151. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  152. package/dist/hooks/sync/recovery.test.js +292 -0
  153. package/dist/hooks/sync/transfers.d.ts +16 -3
  154. package/dist/hooks/sync/transfers.js +210 -66
  155. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  156. package/dist/hooks/sync/transfers.test.js +928 -0
  157. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  158. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  159. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  160. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  161. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  162. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  163. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  164. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  165. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  166. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  167. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  168. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  169. package/dist/hooks/useFilterSort.js +13 -13
  170. package/dist/hooks/useFilterSort.test.js +87 -1
  171. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  172. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  173. package/dist/hooks/useSyncOrchestrator.js +409 -183
  174. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  175. package/dist/hooks/useTaskData.js +8 -3
  176. package/dist/hooks/useTaskData.test.js +45 -0
  177. package/dist/hooks/useTaskforce.d.ts +145 -52
  178. package/dist/hooks/useTaskforce.js +592 -253
  179. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  180. package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
  181. package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
  182. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  183. package/dist/hooks/useWorkspaceSyncController.js +13 -2
  184. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  185. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  186. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
  187. package/dist/localization/locales/en-US.d.ts +0 -1
  188. package/dist/localization/locales/en-US.js +0 -1
  189. package/dist/localization/locales/es-419.js +0 -1
  190. package/dist/localization/locales/pt-BR.js +0 -1
  191. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  192. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  193. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  194. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  195. package/dist/mcp/clientIntegrations.d.ts +1 -0
  196. package/dist/mcp/clientIntegrations.js +25 -0
  197. package/dist/mcp/clientIntegrations.test.js +14 -3
  198. package/dist/mcp/clientProfileDefaults.js +3 -3
  199. package/dist/mcp/collaborationRegistrar.js +7 -7
  200. package/dist/mcp/documentAssetRegistrar.js +116 -10
  201. package/dist/mcp/runtime.d.ts +3 -0
  202. package/dist/mcp/runtime.js +1099 -185
  203. package/dist/mcp/runtime.test.js +1491 -140
  204. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  205. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  206. package/dist/mcp/toolRegistry.d.ts +56 -0
  207. package/dist/mcp/toolRegistry.js +8 -0
  208. package/dist/mcp/toolRegistry.test.js +4 -0
  209. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  210. package/dist/migrations/taskSchemaMigrations.js +192 -61
  211. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  212. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  213. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  214. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  215. package/dist/resources/templates/workflows/plan.yaml +1 -1
  216. package/dist/resources/templates/workflows/review.yaml +2 -2
  217. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  218. package/dist/server/aiProfileAvatarAssets.js +319 -0
  219. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  220. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  221. package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
  222. package/dist/server/auth/providers/apple.d.ts +33 -0
  223. package/dist/server/auth/providers/apple.js +82 -0
  224. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  225. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  226. package/dist/server/auth/providers/github.d.ts +26 -0
  227. package/dist/server/auth/providers/github.js +86 -0
  228. package/dist/server/auth/providers/google.d.ts +21 -0
  229. package/dist/server/auth/providers/google.js +56 -0
  230. package/dist/server/auth/providers/types.d.ts +21 -0
  231. package/dist/server/auth/providers/types.js +1 -0
  232. package/dist/server/auth.d.ts +2 -0
  233. package/dist/server/auth.js +6 -3
  234. package/dist/server/documentReviewRoutes.test.js +36 -3
  235. package/dist/server/index.cookieProxy.test.js +1 -0
  236. package/dist/server/index.d.ts +12 -0
  237. package/dist/server/index.js +124 -9
  238. package/dist/server/index.rateLimit.test.js +3 -0
  239. package/dist/server/index.test.js +111 -3
  240. package/dist/server/routes/admin.d.ts +3 -0
  241. package/dist/server/routes/admin.js +459 -28
  242. package/dist/server/routes/annotatedAttachments.js +51 -0
  243. package/dist/server/routes/auth.d.ts +26 -2
  244. package/dist/server/routes/auth.js +1170 -71
  245. package/dist/server/routes/authSupport.d.ts +5 -1
  246. package/dist/server/routes/authSupport.js +13 -2
  247. package/dist/server/routes/billing.js +372 -35
  248. package/dist/server/routes/billing.test.js +595 -12
  249. package/dist/server/routes/documents.d.ts +1 -0
  250. package/dist/server/routes/documents.js +182 -70
  251. package/dist/server/routes/shared.d.ts +28 -2
  252. package/dist/server/routes/shared.js +69 -14
  253. package/dist/server/routes/sync.integration.test.js +658 -39
  254. package/dist/server/routes/sync.js +74 -608
  255. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  256. package/dist/server/routes/syncAuxRoutes.js +91 -0
  257. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  258. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  259. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  260. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  261. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  262. package/dist/server/routes/syncPushRoutes.js +212 -0
  263. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  264. package/dist/server/routes/syncRouteGuards.js +67 -0
  265. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  266. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  267. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  268. package/dist/server/routes/syncRouteTypes.js +1 -0
  269. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  270. package/dist/server/routes/syncSnapshotRoutes.js +182 -0
  271. package/dist/server/routes/tasks.js +55 -10
  272. package/dist/server/routes/workspaces.js +36 -5
  273. package/dist/server/routes.js +147 -11
  274. package/dist/server/routes.test.js +2704 -183
  275. package/dist/services/workflowExportSnapshots.test.js +2 -0
  276. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  277. package/dist/shared/aiProfileSeatScope.js +21 -0
  278. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  279. package/dist/shared/aiProfileSurfaceType.js +10 -8
  280. package/dist/shared/passwordPolicy.d.ts +13 -0
  281. package/dist/shared/passwordPolicy.js +84 -0
  282. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  283. package/dist/shared/passwordPolicy.test.js +34 -0
  284. package/dist/storage/objectStorage.d.ts +1 -0
  285. package/dist/storage/objectStorage.js +2 -1
  286. package/dist/storage/objectStorageClient.d.ts +1 -0
  287. package/dist/storage/objectStorageClient.js +41 -8
  288. package/dist/storage/objectStorageClient.test.js +26 -1
  289. package/dist/storage/objectStorageConfig.js +3 -1
  290. package/dist/storage/objectStorageConfig.test.js +13 -1
  291. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  292. package/dist/storage/workspaceAssetStore.js +132 -44
  293. package/dist/storage/workspaceAssetStore.test.js +93 -6
  294. package/dist/styles/fonts.css +70 -0
  295. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  296. package/dist/sync/collaborationSyncPayload.js +19 -11
  297. package/dist/sync/collaborationSyncPayload.test.js +14 -1
  298. package/dist/sync/collaborationSyncState.d.ts +3 -3
  299. package/dist/sync/collaborationSyncState.js +19 -12
  300. package/dist/sync/contentSyncPayload.d.ts +5 -1
  301. package/dist/sync/contentSyncPayload.js +9 -0
  302. package/dist/sync/contentSyncPayload.test.js +39 -0
  303. package/dist/sync/contentSyncState.d.ts +9 -9
  304. package/dist/sync/contentSyncState.js +248 -107
  305. package/dist/sync/planningSyncPayload.js +5 -0
  306. package/dist/sync/planningSyncPayload.test.js +24 -0
  307. package/dist/sync/syncApplyHandlers.d.ts +11 -4
  308. package/dist/sync/syncApplyHandlers.js +115 -42
  309. package/dist/sync/syncApplyHandlers.test.js +251 -4
  310. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  311. package/dist/sync/syncFieldSemantics.js +55 -0
  312. package/dist/sync/syncResourceAdapters.d.ts +46 -0
  313. package/dist/sync/syncResourceAdapters.js +79 -0
  314. package/dist/sync/syncService.d.ts +6 -0
  315. package/dist/sync/syncService.js +65 -1
  316. package/dist/sync/syncService.test.js +96 -3
  317. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  318. package/dist/sync/taskSyncChangeKey.js +143 -0
  319. package/dist/sync/taskSyncPayload.js +3 -8
  320. package/dist/sync/workspacePullFeed.d.ts +4 -0
  321. package/dist/sync/workspacePullFeed.js +132 -72
  322. package/dist/sync/workspacePullFeed.test.js +92 -0
  323. package/dist/sync/workspaceRepair.js +5 -2
  324. package/dist/sync/workspaceSyncModel.d.ts +73 -6
  325. package/dist/sync/workspaceSyncModel.js +342 -73
  326. package/dist/sync/workspaceSyncModel.test.js +559 -14
  327. package/dist/sync/workspaceSyncState.d.ts +17 -0
  328. package/dist/sync/workspaceSyncState.js +2 -1
  329. package/dist/sync/workspaceSyncSurface.js +3 -8
  330. package/dist/types.d.ts +50 -2
  331. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  332. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  333. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  334. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  335. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  336. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  337. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  338. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  339. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  340. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  341. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  342. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  343. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
  344. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  345. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  346. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  347. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  348. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  349. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  350. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  351. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  352. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  353. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  354. package/dist/ui/assets/index-607WPo_X.js +4 -0
  355. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  356. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
  357. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  358. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  359. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  360. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  361. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  362. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  363. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  364. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  365. package/dist/ui/fonts/Noir_medium.otf +0 -0
  366. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  367. package/dist/ui/fonts/Noir_regular.otf +0 -0
  368. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  369. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  370. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  371. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  372. package/dist/ui/index.html +3 -3
  373. package/dist/ui/product/agent manager.png +0 -0
  374. package/dist/ui/product/agent_manager_02.png +0 -0
  375. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  376. package/dist/utils/aiProfileEvents.d.ts +7 -0
  377. package/dist/utils/aiProfileEvents.js +6 -0
  378. package/dist/utils/assignees.d.ts +8 -0
  379. package/dist/utils/assignees.js +10 -1
  380. package/dist/utils/assignees.test.d.ts +1 -0
  381. package/dist/utils/assignees.test.js +25 -0
  382. package/dist/utils/avatarDisplay.d.ts +1 -0
  383. package/dist/utils/avatarDisplay.js +15 -0
  384. package/dist/utils/avatarUpload.d.ts +27 -0
  385. package/dist/utils/avatarUpload.js +132 -0
  386. package/dist/utils/avatarUpload.test.d.ts +1 -0
  387. package/dist/utils/avatarUpload.test.js +40 -0
  388. package/dist/utils/bootstrapTrace.d.ts +7 -0
  389. package/dist/utils/bootstrapTrace.js +29 -0
  390. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  391. package/dist/utils/contextAssetEvents.d.ts +2 -0
  392. package/dist/utils/contextFiles.d.ts +3 -1
  393. package/dist/utils/contextFiles.js +2 -1
  394. package/dist/utils/syncEventDetails.d.ts +3 -0
  395. package/dist/utils/syncEventDetails.js +64 -0
  396. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  397. package/dist/utils/syncEventDetails.test.js +23 -0
  398. package/dist/utils/syncEventPresentation.d.ts +15 -0
  399. package/dist/utils/syncEventPresentation.js +129 -0
  400. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  401. package/dist/utils/syncEventPresentation.test.js +64 -0
  402. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  403. package/dist/utils/syncStatusPresentation.js +217 -0
  404. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  405. package/dist/utils/syncStatusPresentation.test.js +164 -0
  406. package/dist/utils/taskActivity.d.ts +9 -1
  407. package/dist/utils/taskActivity.js +290 -22
  408. package/dist/utils/taskActivity.test.js +141 -8
  409. package/dist/utils/taskSearch.d.ts +22 -0
  410. package/dist/utils/taskSearch.js +87 -0
  411. package/dist/utils/taskSearch.test.d.ts +1 -0
  412. package/dist/utils/taskSearch.test.js +69 -0
  413. package/dist/utils/taskUtils.js +10 -3
  414. package/dist/utils/taskUtils.test.d.ts +1 -0
  415. package/dist/utils/taskUtils.test.js +19 -0
  416. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  417. package/dist/utils/workspaceSyncPresentation.js +68 -2
  418. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  419. package/package.json +3 -2
  420. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  421. package/scripts/run-e2e-realtime.sh +17 -3
  422. package/scripts/run-smoke.sh +17 -5
  423. package/scripts/run-soak.sh +17 -5
  424. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  425. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  426. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  427. package/src/resources/templates/workflows/plan.yaml +1 -1
  428. package/src/resources/templates/workflows/review.yaml +2 -2
  429. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  430. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
  431. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  432. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  433. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  434. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  435. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  436. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  437. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
  438. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  439. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  440. package/scripts/migrate-planning-model.ts +0 -233
@@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { CallToolRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
- import { randomUUID } from 'node:crypto';
6
+ import { createHash, randomUUID } from 'node:crypto';
7
7
  import * as os from 'node:os';
8
8
  import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce.js'; // Note .js extension for ESM
9
9
  import { COMPLEXITY_OPTIONS, resolveNumberOption, resolveStringOption, STATUS_OPTIONS } from './optionResolution.js';
@@ -15,20 +15,40 @@ import { validateStructuredDocForAttach } from './structuredDocValidation.js';
15
15
  import { validateStructuredCommentForAdd } from './structuredCommentValidation.js';
16
16
  import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
17
17
  import { resolveObjectStorageConfigFromEnv } from '../storage/objectStorageConfig.js';
18
- import { ObjectStorageClient } from '../storage/objectStorageClient.js';
18
+ import { ObjectStorageClient, normalizeTaskDataRelativePath } from '../storage/objectStorageClient.js';
19
19
  import { WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
20
+ import { resolveVirusScanConfigFromEnv, scanBufferForThreats } from '../security/virusScan.js';
20
21
  import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, resolveAppGateAccess, } from '../runtime/appGates.js';
21
22
  import { getDocumentReferenceLabel, parseDocumentReference } from '../utils/documentReferences.js';
22
23
  import { getImageReferenceLabel, parseImageReference } from '../utils/imageReferences.js';
23
24
  import { inferCanonicalAssetKind, isValidCanonicalDocumentAsset } from '../utils/canonicalAssetKind.js';
24
25
  import { getTaskReferenceLabel, shouldUseProvisionalTaskReferences } from '../utils/taskReferences.js';
25
26
  import { parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
26
- import { formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
27
+ import { parseAiProfileSeatScope } from '../shared/aiProfileSeatScope.js';
28
+ import { formatInitiativeReference, formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
27
29
  import { MCP_TOOL_PERMISSIONS, MCP_WRITE_TOOL_NAMES } from './toolRegistry.js';
28
30
  import { registerAdminWorkspaceTools } from './adminWorkspaceRegistrar.js';
29
31
  import { registerCollaborationTools } from './collaborationRegistrar.js';
30
32
  import { registerDocumentAssetTools } from './documentAssetRegistrar.js';
31
33
  import { registerTaskPlanningTools } from './taskPlanningRegistrar.js';
34
+ function readEnvValue(name) {
35
+ const value = String(process.env[name] || '').trim();
36
+ return value || null;
37
+ }
38
+ function buildStdioAuthFromEnv() {
39
+ return {
40
+ aiProfileToken: readEnvValue('TASKFORCE_AI_PROFILE_TOKEN'),
41
+ aiProfileName: readEnvValue('TASKFORCE_AI_NAME'),
42
+ aiProfileIcon: readEnvValue('TASKFORCE_AI_ICON'),
43
+ aiProfileColor: readEnvValue('TASKFORCE_AI_COLOR'),
44
+ aiProfileDescription: readEnvValue('TASKFORCE_AI_DESCRIPTION'),
45
+ aiProfileRole: readEnvValue('TASKFORCE_AI_ROLE'),
46
+ aiProfileProvider: readEnvValue('TASKFORCE_AI_PROVIDER'),
47
+ aiProfileModel: readEnvValue('TASKFORCE_AI_MODEL'),
48
+ aiProfileSurfaceType: parseAiProfileSurfaceType(readEnvValue('TASKFORCE_AI_SURFACE_TYPE')),
49
+ aiProfileSeatScope: parseAiProfileSeatScope(readEnvValue('TASKFORCE_AI_SEAT_SCOPE')),
50
+ };
51
+ }
32
52
  export function isMcpWriteToolName(name) {
33
53
  return MCP_WRITE_TOOL_NAMES.has(String(name || '').trim());
34
54
  }
@@ -80,7 +100,7 @@ function resolveRecoverableToolError(error) {
80
100
  code: 'AI_PROFILE_REQUIRED',
81
101
  nextTool: 'resolve_profile',
82
102
  suggestedArgs: {},
83
- retryHint: 'Call get_current_profile to inspect binding state first, or call resolve_profile to bind an AI identity. Only pass profileToken on later mutating calls if your client is stateless.'
103
+ retryHint: 'Call get_current_profile to inspect binding state first, or call resolve_profile to bind an AI identity. Store the returned profileToken only in private agent/client state and pass it on later mutating calls only if your client is stateless.'
84
104
  };
85
105
  }
86
106
  if (/^Invalid AI profile token\./i.test(message)) {
@@ -89,7 +109,7 @@ function resolveRecoverableToolError(error) {
89
109
  code: 'INVALID_AI_PROFILE_TOKEN',
90
110
  nextTool: 'resolve_profile',
91
111
  suggestedArgs: {},
92
- retryHint: 'Call get_current_profile to confirm whether ambient identity is already bound. If not, call resolve_profile again to obtain a fresh profileToken, then retry the original mutating tool call.'
112
+ retryHint: 'Call get_current_profile to confirm whether ambient identity is already bound. If not, call resolve_profile again to obtain a fresh profileToken for private agent/client storage, then retry the original mutating tool call once.'
93
113
  };
94
114
  }
95
115
  if (error instanceof TaskforceRuleError) {
@@ -102,18 +122,42 @@ function resolveRecoverableToolError(error) {
102
122
  message,
103
123
  code,
104
124
  ...(details !== undefined ? { details } : {}),
125
+ ...(code === 'AI_PROFILE_RETIRED'
126
+ ? {
127
+ nextTool: 'resolve_profile',
128
+ suggestedArgs: {},
129
+ retryHint: 'Call resolve_profile to intentionally reactivate the retired AI identity, then retry the original mutating tool call.'
130
+ }
131
+ : {}),
105
132
  ...(code === 'TAXONOMY_NEEDS_CONFIRMATION' ? { needs_confirmation: true } : {})
106
133
  };
107
134
  }
108
135
  }
109
136
  if (!message)
110
137
  return null;
138
+ const transient = classifyTransientMcpError(error);
139
+ if (transient) {
140
+ return {
141
+ message,
142
+ code: transient.code,
143
+ kind: transient.kind,
144
+ retryable: true,
145
+ details: {
146
+ kind: transient.kind,
147
+ retryable: true
148
+ },
149
+ retryHint: transient.retryHint
150
+ };
151
+ }
111
152
  const safePatterns = [
112
153
  /^Invalid /i,
113
154
  / is required$/i,
114
- /^Task .+ not found$/i,
155
+ /^attachments must be an array$/i,
156
+ /^Task .+ not found/i,
157
+ /^Initiative .+ not found/i,
158
+ /^Workstream .+ not found/i,
115
159
  /^Image .+ not found in workspace .+$/i,
116
- /^Attachment .+ not found on task .+$/i,
160
+ /^Attachment .+ not found on (task|initiative|workstream) .+$/i,
117
161
  /^Attachment is quarantined$/i,
118
162
  /^Attachment scan pending$/i,
119
163
  /^Attachment is not readable as text$/i,
@@ -133,6 +177,53 @@ function resolveRecoverableToolError(error) {
133
177
  }
134
178
  return null;
135
179
  }
180
+ function collectErrorMessages(error) {
181
+ const messages = [];
182
+ let current = error;
183
+ const seen = new Set();
184
+ while (current && !seen.has(current)) {
185
+ seen.add(current);
186
+ if (current instanceof Error && current.message) {
187
+ messages.push(String(current.message));
188
+ current = current.cause;
189
+ continue;
190
+ }
191
+ if (typeof current === 'object') {
192
+ const record = current;
193
+ if (typeof record.message === 'string')
194
+ messages.push(record.message);
195
+ if (typeof record.code === 'string')
196
+ messages.push(record.code);
197
+ current = record.cause;
198
+ continue;
199
+ }
200
+ break;
201
+ }
202
+ return messages;
203
+ }
204
+ function classifyTransientMcpError(error) {
205
+ const combined = collectErrorMessages(error).join(' ').trim();
206
+ if (!combined)
207
+ return null;
208
+ if (/(fetch failed|ECONNRESET|ECONNREFUSED|EPIPE|socket hang up|UND_ERR_|network error|request aborted|connection terminated)/i.test(combined)) {
209
+ return {
210
+ kind: 'transport_failure',
211
+ code: 'MCP_TRANSPORT_FAILURE',
212
+ retryHint: 'The tool transport failed before Taskforce could return a stable tool response. Retry the same read once; if it repeats, inspect the cloud MCP route logs with this requestId.'
213
+ };
214
+ }
215
+ if (/(timed out|timeout|ETIMEDOUT|query canceled|statement timeout|database timeout|postgres worker)/i.test(combined)) {
216
+ return {
217
+ kind: 'backend_timeout',
218
+ code: 'MCP_BACKEND_TIMEOUT',
219
+ retryHint: 'Taskforce reached the backend but the read timed out. Retry the same read after a short delay; repeated failures point to backend saturation rather than a missing document or attachment.'
220
+ };
221
+ }
222
+ return null;
223
+ }
224
+ function delay(ms) {
225
+ return new Promise((resolve) => setTimeout(resolve, ms));
226
+ }
136
227
  function inferMimeTypeFromPath(filePath) {
137
228
  const ext = path.extname(filePath).toLowerCase();
138
229
  const contentTypes = {
@@ -152,6 +243,22 @@ function inferMimeTypeFromPath(filePath) {
152
243
  function inferAssetKind(filePath, mimeType) {
153
244
  return inferCanonicalAssetKind(filePath, mimeType);
154
245
  }
246
+ function scanVerdictToCanonicalStatus(verdict) {
247
+ if (verdict === 'pending')
248
+ return 'pending';
249
+ if (verdict === 'infected')
250
+ return 'infected';
251
+ if (verdict === 'error')
252
+ return 'failed';
253
+ return 'clean';
254
+ }
255
+ function inferWorkspaceIdFromStoragePath(pathValue) {
256
+ const normalized = String(pathValue || '').trim().replace(/\\/g, '/');
257
+ const match = normalized.match(/^workspaces\/([^/]+)\//);
258
+ if (match && match[1])
259
+ return match[1];
260
+ return 'default';
261
+ }
155
262
  function isTextReadableMimeType(mimeTypeRaw) {
156
263
  const mimeType = String(mimeTypeRaw || '').trim().toLowerCase();
157
264
  if (!mimeType)
@@ -205,6 +312,8 @@ export function buildRecoverableToolErrorResult(error, requestId) {
205
312
  error: resolved.message,
206
313
  message: resolved.message,
207
314
  ...(resolved.code ? { code: resolved.code } : {}),
315
+ ...(resolved.kind ? { kind: resolved.kind } : {}),
316
+ ...(resolved.retryable ? { retryable: true } : {}),
208
317
  ...(resolved.details !== undefined ? { details: resolved.details } : {}),
209
318
  ...(resolved.needs_confirmation ? { needs_confirmation: true } : {}),
210
319
  ...(resolved.nextTool ? { nextTool: resolved.nextTool } : {}),
@@ -227,11 +336,11 @@ export function createTaskforceMcpServer(options = {}) {
227
336
  const allowedScopes = Array.isArray(options.auth?.scopes)
228
337
  ? Array.from(new Set(options.auth.scopes))
229
338
  : null;
339
+ const configuredAiProfileSeatScope = runtimeMode === 'cloud' ? 'cloud_metered' : 'local_unmetered';
230
340
  if (logStartup) {
231
341
  console.error(`[Taskforce MCP] Starting for project: ${PROJECT_NAME} at ${PROJECT_ROOT}`);
232
342
  }
233
- const db = resolveDatabaseConfigFromEnv();
234
- const tenantId = String(options.tenantId || process.env.TASKFORCE_TENANT_ID || 'default').trim() || 'default';
343
+ const tenantId = String(options.tenantId || options.core?.getTenantId() || process.env.TASKFORCE_TENANT_ID || 'default').trim() || 'default';
235
344
  const MCP_SERVER_VERSION = (() => {
236
345
  if (options.version)
237
346
  return String(options.version).trim() || '0.0.0-dev';
@@ -246,14 +355,17 @@ export function createTaskforceMcpServer(options = {}) {
246
355
  return '0.0.0-dev';
247
356
  }
248
357
  })();
249
- const core = new TaskforceCore({
250
- projectRoot: PROJECT_ROOT,
251
- tenantId,
252
- database: {
253
- provider: db.provider,
254
- connectionString: db.connectionString
255
- }
256
- });
358
+ const core = options.core || (() => {
359
+ const db = resolveDatabaseConfigFromEnv();
360
+ return new TaskforceCore({
361
+ projectRoot: PROJECT_ROOT,
362
+ tenantId,
363
+ database: {
364
+ provider: db.provider,
365
+ connectionString: db.connectionString
366
+ }
367
+ });
368
+ })();
257
369
  const workspaceFromEnv = String(options.workspaceId || process.env.TASKFORCE_WORKSPACE_ID || '').trim();
258
370
  let ACTIVE_WORKSPACE_ID = '';
259
371
  const workspaceFromEnvValid = workspaceFromEnv && workspaceFromEnv.toLowerCase() !== 'default'
@@ -333,7 +445,7 @@ export function createTaskforceMcpServer(options = {}) {
333
445
  },
334
446
  tools: {},
335
447
  },
336
- instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: Before mutating Taskforce data, ensure Taskforce knows which AI profile you are acting as. The preferred path is ambient session binding: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes. If no AI identity is currently bound, call resolve_profile with your name. Reuse the returned profileToken across sessions, especially for stateless clients.\n\nStateless client pattern: call resolve_profile, save the returned profileToken, and include profileToken on later mutating tool calls only when your MCP client does not preserve session identity between calls.\n\nTo inspect your current attribution state before writes, call get_current_profile. It reports whether identity is bound, how it was resolved, and the suggested args to use with list_tasks.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, coding_tool for the coding surface or terminal tool itself, chat_app for a conversational app identity, ide for an IDE-native copilot identity, and other only when none of those fit. If you are unsure and you are registering a named collaborator such as a teammate-style AI persona, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use get_annotated_attachment_sessions to list saved sessions, then use read_annotated_attachment_payload for the structured annotation payload.\n\nFor documents, use search_documents first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_document, then read_document_content to inspect the document body. For a lower-friction review flow, prefer review_document; otherwise use get_document_review_sessions or create_document_review_session, then add_document_review_comment to record review feedback.`,
448
+ instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: Before mutating Taskforce data, ensure Taskforce knows which AI profile you are acting as. The preferred path is ambient session binding: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes. If no AI identity is currently bound, call resolve_profile with your name. Reuse the returned profileToken across sessions, especially for stateless clients.\n\nStateless client pattern: call resolve_profile, save the returned profileToken only in private agent/client state, and include profileToken on later mutating tool calls only when your MCP client does not preserve session identity between calls. Never write profileToken values to repos, task comments, task attachments, logs, generated docs, or shared output.\n\nTo inspect your current attribution state before writes, call get_current_profile. It reports whether identity is bound, how it was resolved, and the suggested args to use with list_tasks.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, cli for terminal-native coding agents, coding_tool for coding surfaces that are not specifically CLI or IDE-native, chat_app for a conversational app identity, and ide for an IDE-native copilot identity. If you are unsure or none of those fit, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use get_annotated_attachment_sessions to list saved sessions, then use read_annotated_attachment_payload for the structured annotation payload. If an image reference is being used for UI, placement, or visual correction feedback, treat the saved annotation payload as the source of truth and do not infer intent from the screenshot alone.\n\nFor documents, use search_documents first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_document, then read_document_content to inspect the document body. For a lower-friction review flow, prefer review_document; otherwise use get_document_review_sessions or create_document_review_session, then add_document_review_comment to record review feedback.`,
337
449
  });
338
450
  const TOOL_PERMISSIONS = MCP_TOOL_PERMISSIONS;
339
451
  const RESOURCE_PERMISSIONS = {
@@ -562,6 +674,7 @@ export function createTaskforceMcpServer(options = {}) {
562
674
  provider: typeof actor.meta?.provider === 'string' ? actor.meta.provider : null,
563
675
  model: typeof actor.meta?.model === 'string' ? actor.meta.model : null,
564
676
  surfaceType: typeof actor.meta?.surfaceType === 'string' ? actor.meta.surfaceType : null,
677
+ seatScope: parseAiProfileSeatScope(actor.meta?.seatScope),
565
678
  } : {}),
566
679
  ...(activeAiProfileId && actor.id === activeAiProfileId ? { isCurrentProfile: true } : {})
567
680
  }))
@@ -581,6 +694,7 @@ export function createTaskforceMcpServer(options = {}) {
581
694
  provider: profile.provider ?? null,
582
695
  model: profile.model ?? null,
583
696
  surfaceType: profile.surfaceType ?? null,
697
+ seatScope: profile.seatScope ?? null,
584
698
  lastActiveAt: profile.lastActiveAt ?? null,
585
699
  };
586
700
  }
@@ -642,7 +756,7 @@ export function createTaskforceMcpServer(options = {}) {
642
756
  }
643
757
  const previousStatus = String(task?.status || 'task').trim() || 'task';
644
758
  const previousAssignee = String(task?.assignee || '').trim() || 'unassigned';
645
- if (previousStatus === 'done' || previousStatus === 'review' || previousStatus === 'cancelled') {
759
+ if (previousStatus === 'done' || previousStatus === 'cancelled') {
646
760
  throw new Error(`Task ${formatTaskIdentifier(task)} is already ${previousStatus} and must be reopened explicitly before active work resumes.`);
647
761
  }
648
762
  if (previousAssignee !== 'unassigned' && previousAssignee !== currentAiAssignee) {
@@ -679,6 +793,26 @@ export function createTaskforceMcpServer(options = {}) {
679
793
  return auditActorUserId;
680
794
  return undefined;
681
795
  }
796
+ function isStableMcpCommentActorRef(value) {
797
+ const normalized = String(value || '').trim().toLowerCase();
798
+ if (!normalized)
799
+ return false;
800
+ return normalized === 'user'
801
+ || normalized.startsWith('user-')
802
+ || normalized.startsWith('ai-profile-');
803
+ }
804
+ function resolveMcpCommentAuthorInput(value) {
805
+ const raw = String(value || '').trim();
806
+ const normalized = raw.toLowerCase();
807
+ const activeActor = getActiveMcpActorRef();
808
+ if (!raw || normalized === 'system')
809
+ return activeActor;
810
+ if (isStableMcpCommentActorRef(raw))
811
+ return raw;
812
+ if (activeActor !== 'system')
813
+ return activeActor;
814
+ return raw;
815
+ }
682
816
  function resolveProfileTokenFromArgs(args) {
683
817
  if (!args || typeof args !== 'object' || Array.isArray(args))
684
818
  return null;
@@ -692,7 +826,7 @@ export function createTaskforceMcpServer(options = {}) {
692
826
  }
693
827
  function buildConfiguredAiProfileResolveInput(includeToken = true) {
694
828
  const configuredSurfaceType = parseAiProfileSurfaceType(configuredAiProfileSurfaceType);
695
- return buildResolveAiProfileInputFromBootstrap(ACTIVE_WORKSPACE_ID, {
829
+ const baseInput = buildResolveAiProfileInputFromBootstrap(ACTIVE_WORKSPACE_ID, {
696
830
  aiProfileToken: configuredAiProfileToken,
697
831
  aiProfileName: configuredAiProfileName,
698
832
  aiProfileIcon: configuredAiProfileIcon,
@@ -703,6 +837,51 @@ export function createTaskforceMcpServer(options = {}) {
703
837
  aiProfileModel: configuredAiProfileModel,
704
838
  aiProfileSurfaceType: configuredSurfaceType,
705
839
  }, includeToken);
840
+ if (!baseInput)
841
+ return null;
842
+ return baseInput;
843
+ }
844
+ function normalizeConfiguredAiProfileUsername(value, fallbackName) {
845
+ const source = String(value || fallbackName || '').trim().toLowerCase();
846
+ const normalized = source
847
+ .replace(/[^a-z0-9]+/g, '-')
848
+ .replace(/-+/g, '-')
849
+ .replace(/^-+|-+$/g, '')
850
+ .slice(0, 48);
851
+ return normalized || 'ai';
852
+ }
853
+ function findArchivedConfiguredAiProfileCandidate() {
854
+ if (configuredAiProfileToken) {
855
+ const profile = core.getAiProfileByToken({
856
+ workspaceId: ACTIVE_WORKSPACE_ID,
857
+ profileToken: configuredAiProfileToken,
858
+ includeArchived: true,
859
+ seatScope: configuredAiProfileSeatScope,
860
+ });
861
+ if (profile?.archivedAt)
862
+ return profile;
863
+ }
864
+ const configuredName = String(configuredAiProfileName || '').trim();
865
+ if (!configuredName)
866
+ return null;
867
+ const normalizedUsername = normalizeConfiguredAiProfileUsername(undefined, configuredName).toLowerCase();
868
+ return core.listAiProfiles(ACTIVE_WORKSPACE_ID, { includeArchived: true }).find((profile) => {
869
+ if (!profile.archivedAt)
870
+ return false;
871
+ if ((profile.seatScope ?? null) !== configuredAiProfileSeatScope)
872
+ return false;
873
+ const profileName = String(profile.name || '').trim().toLowerCase();
874
+ const profileUsername = String(profile.username || '').trim().toLowerCase();
875
+ return profileName === configuredName.toLowerCase() || profileUsername === normalizedUsername;
876
+ }) || null;
877
+ }
878
+ function throwArchivedConfiguredAiProfileError(profile) {
879
+ throw new TaskforceRuleError(`AI profile "${profile.name}" has been retired from this workspace. Call resolve_profile to reactivate it before retrying.`, 409, 'AI_PROFILE_RETIRED', {
880
+ workspaceId: ACTIVE_WORKSPACE_ID,
881
+ profileId: profile.id,
882
+ profileToken: profile.profileToken || null,
883
+ archivedReason: profile.archivedReason || null,
884
+ });
706
885
  }
707
886
  function ensureResolvedProfileFromConnection(defaults) {
708
887
  if (activeAiProfileId)
@@ -710,10 +889,16 @@ export function createTaskforceMcpServer(options = {}) {
710
889
  const allowCreateFromName = defaults?.allowCreateFromName === true;
711
890
  const configuredResolveInput = buildConfiguredAiProfileResolveInput(true);
712
891
  const configuredResolveFallbackInput = buildConfiguredAiProfileResolveInput(false);
892
+ const archivedConfiguredProfile = findArchivedConfiguredAiProfileCandidate();
893
+ if (archivedConfiguredProfile) {
894
+ throwArchivedConfiguredAiProfileError(archivedConfiguredProfile);
895
+ }
713
896
  if (configuredAiProfileToken) {
714
897
  if (configuredResolveInput) {
715
898
  try {
716
- const resolved = core.resolveAiProfile(configuredResolveInput);
899
+ const resolved = core.resolveAiProfile(configuredResolveInput, {
900
+ trustedSeatScope: configuredAiProfileSeatScope,
901
+ });
717
902
  activeAiProfileId = resolved.profile.id;
718
903
  activeAiBindingSource = 'connection_bootstrap';
719
904
  return;
@@ -725,6 +910,7 @@ export function createTaskforceMcpServer(options = {}) {
725
910
  const profile = core.getAiProfileByToken({
726
911
  workspaceId: ACTIVE_WORKSPACE_ID,
727
912
  profileToken: configuredAiProfileToken,
913
+ seatScope: configuredAiProfileSeatScope,
728
914
  });
729
915
  if (profile) {
730
916
  activeAiProfileId = profile.id;
@@ -736,7 +922,9 @@ export function createTaskforceMcpServer(options = {}) {
736
922
  }
737
923
  }
738
924
  if (allowCreateFromName && configuredResolveFallbackInput) {
739
- const resolved = core.resolveAiProfile(configuredResolveFallbackInput);
925
+ const resolved = core.resolveAiProfile(configuredResolveFallbackInput, {
926
+ trustedSeatScope: configuredAiProfileSeatScope,
927
+ });
740
928
  activeAiProfileId = resolved.profile.id;
741
929
  activeAiBindingSource = 'connection_bootstrap';
742
930
  }
@@ -756,6 +944,7 @@ export function createTaskforceMcpServer(options = {}) {
756
944
  const profile = core.getAiProfileByToken({
757
945
  workspaceId: ACTIVE_WORKSPACE_ID,
758
946
  profileToken,
947
+ seatScope: configuredAiProfileSeatScope,
759
948
  });
760
949
  if (profile) {
761
950
  activeAiProfileId = profile.id;
@@ -764,7 +953,9 @@ export function createTaskforceMcpServer(options = {}) {
764
953
  }
765
954
  const configuredResolveInput = buildConfiguredAiProfileResolveInput(false);
766
955
  if (configuredResolveInput) {
767
- const resolved = core.resolveAiProfile(configuredResolveInput);
956
+ const resolved = core.resolveAiProfile(configuredResolveInput, {
957
+ trustedSeatScope: configuredAiProfileSeatScope,
958
+ });
768
959
  activeAiProfileId = resolved.profile.id;
769
960
  activeAiBindingSource = 'connection_bootstrap';
770
961
  return;
@@ -878,8 +1069,8 @@ export function createTaskforceMcpServer(options = {}) {
878
1069
  };
879
1070
  }
880
1071
  function createAnnotatedAttachmentAnnotationFromArgs(args, order) {
881
- const kind = normalizeAnnotatedAttachmentKind(args.kind);
882
- const instruction = normalizeOptionalString(args.instruction);
1072
+ const kind = normalizeAnnotatedAttachmentKind(args.kind ?? args.type);
1073
+ const instruction = normalizeOptionalString(args.instruction ?? args.text);
883
1074
  if (!instruction) {
884
1075
  throw new Error('instruction is required');
885
1076
  }
@@ -1039,6 +1230,61 @@ export function createTaskforceMcpServer(options = {}) {
1039
1230
  ...details,
1040
1231
  });
1041
1232
  }
1233
+ const burstyQaReadToolNames = new Set([
1234
+ 'get_initiative',
1235
+ 'get_workstream',
1236
+ 'list_initiatives',
1237
+ 'list_workstreams',
1238
+ 'get_workstream_attachments',
1239
+ 'get_initiative_attachments',
1240
+ 'get_document',
1241
+ 'read_document_content',
1242
+ ]);
1243
+ const burstyQaReadRetryDelaysMs = [125, 350];
1244
+ async function executeMcpToolWithReadRetry(toolName, args, handler) {
1245
+ const startedAtMs = Date.now();
1246
+ let attempt = 0;
1247
+ for (;;) {
1248
+ try {
1249
+ const result = await handler(args);
1250
+ if (burstyQaReadToolNames.has(toolName)) {
1251
+ logMcpReadObservation(toolName, startedAtMs, {
1252
+ attempts: attempt + 1,
1253
+ retried: attempt > 0,
1254
+ });
1255
+ }
1256
+ return result;
1257
+ }
1258
+ catch (error) {
1259
+ const transient = classifyTransientMcpError(error);
1260
+ if (!burstyQaReadToolNames.has(toolName) || !transient || attempt >= burstyQaReadRetryDelaysMs.length) {
1261
+ if (burstyQaReadToolNames.has(toolName) && transient) {
1262
+ logMcpStructuredEvent('warn', 'tool_read_retry_exhausted', {
1263
+ toolName,
1264
+ attempts: attempt + 1,
1265
+ durationMs: Date.now() - startedAtMs,
1266
+ errorKind: transient.kind,
1267
+ errorCode: transient.code,
1268
+ });
1269
+ }
1270
+ throw error;
1271
+ }
1272
+ const delayMs = burstyQaReadRetryDelaysMs[attempt];
1273
+ attempt += 1;
1274
+ logMcpStructuredEvent('warn', 'tool_read_retry', {
1275
+ toolName,
1276
+ attempt,
1277
+ nextAttempt: attempt + 1,
1278
+ delayMs,
1279
+ durationMs: Date.now() - startedAtMs,
1280
+ errorKind: transient.kind,
1281
+ errorCode: transient.code,
1282
+ args: sanitizeToolArgsForLog(args),
1283
+ });
1284
+ await delay(delayMs);
1285
+ }
1286
+ }
1287
+ }
1042
1288
  /**
1043
1289
  * Helpers
1044
1290
  */
@@ -1224,6 +1470,21 @@ export function createTaskforceMcpServer(options = {}) {
1224
1470
  const d = String(date.getDate()).padStart(2, '0');
1225
1471
  return `${y}-${m}-${d}`;
1226
1472
  }
1473
+ function dateOnlyToIsoWeekKey(dateOnly) {
1474
+ const parsed = parseDateOnlyLocal(dateOnly);
1475
+ if (!parsed)
1476
+ return null;
1477
+ const weekDate = new Date(Date.UTC(parsed.getFullYear(), parsed.getMonth(), parsed.getDate()));
1478
+ const day = weekDate.getUTCDay() || 7;
1479
+ weekDate.setUTCDate(weekDate.getUTCDate() + 4 - day);
1480
+ const year = weekDate.getUTCFullYear();
1481
+ const yearStart = new Date(Date.UTC(year, 0, 1));
1482
+ const week = Math.ceil((((weekDate.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
1483
+ return `${year}-W${String(week).padStart(2, '0')}`;
1484
+ }
1485
+ function currentIsoWeekKeyLocal() {
1486
+ return dateOnlyToIsoWeekKey(formatDateOnlyLocal(new Date())) || '';
1487
+ }
1227
1488
  function addDaysDateOnly(dateOnly, days) {
1228
1489
  const parsed = parseDateOnlyLocal(dateOnly);
1229
1490
  if (!parsed)
@@ -1518,6 +1779,61 @@ export function createTaskforceMcpServer(options = {}) {
1518
1779
  workstreams,
1519
1780
  };
1520
1781
  }
1782
+ function buildPlanningWriteResponseHydrationFallbackDetails(targetType, entity, context, error) {
1783
+ const transient = classifyTransientMcpError(error);
1784
+ if (!transient)
1785
+ throw error;
1786
+ const referenceLabel = targetType === 'initiative'
1787
+ ? (typeof entity?.referenceLabel === 'string' ? entity.referenceLabel : formatInitiativeReference(entity))
1788
+ : (typeof entity?.referenceLabel === 'string' ? entity.referenceLabel : formatWorkstreamReference(entity));
1789
+ logMcpStructuredEvent('warn', 'planning_write_response_hydration_fallback', {
1790
+ context,
1791
+ targetType,
1792
+ targetId: entity?.id || null,
1793
+ referenceLabel,
1794
+ errorKind: transient.kind,
1795
+ errorCode: transient.code,
1796
+ message: error instanceof Error ? error.message : String(error),
1797
+ });
1798
+ return {
1799
+ fallback: true,
1800
+ reason: transient.kind,
1801
+ code: transient.code,
1802
+ retryable: true,
1803
+ message: 'The write completed, but Taskforce skipped the richer response hydration after a transient read timeout.',
1804
+ nextTool: targetType === 'initiative' ? 'get_initiative' : 'get_workstream',
1805
+ suggestedArgs: {
1806
+ id: referenceLabel || entity?.id || '',
1807
+ },
1808
+ };
1809
+ }
1810
+ function buildPostWriteInitiativeResourcePayload(initiative, context) {
1811
+ try {
1812
+ return buildInitiativeResourcePayload(initiative);
1813
+ }
1814
+ catch (error) {
1815
+ return {
1816
+ initiative: buildPlanningInitiativeSummary(initiative),
1817
+ workstreamCount: 0,
1818
+ taskCount: 0,
1819
+ workstreams: [],
1820
+ responseHydration: buildPlanningWriteResponseHydrationFallbackDetails('initiative', initiative, context, error),
1821
+ };
1822
+ }
1823
+ }
1824
+ function buildPostWriteWorkstreamResourcePayload(workstream, context) {
1825
+ try {
1826
+ return buildWorkstreamResourcePayload(workstream);
1827
+ }
1828
+ catch (error) {
1829
+ return {
1830
+ workstream: buildPlanningWorkstreamSummary(workstream),
1831
+ taskCount: 0,
1832
+ tasks: [],
1833
+ responseHydration: buildPlanningWriteResponseHydrationFallbackDetails('workstream', workstream, context, error),
1834
+ };
1835
+ }
1836
+ }
1521
1837
  function buildInitiativeLookupFailureMessage(initiativeIdOrReference) {
1522
1838
  const normalizedValue = String(initiativeIdOrReference || '').trim() || 'initiative';
1523
1839
  return `Initiative ${normalizedValue} not found in workspace ${ACTIVE_WORKSPACE_ID}.`;
@@ -1809,61 +2125,333 @@ export function createTaskforceMcpServer(options = {}) {
1809
2125
  tools,
1810
2126
  };
1811
2127
  });
1812
- /**
1813
- * Tool Execution
1814
- */
2128
+ const resolvePlanningAttachmentTarget = (targetType, inputId) => {
2129
+ if (targetType === 'initiative') {
2130
+ const { id, initiative } = resolveInitiativeByIdentifierOrThrow(inputId);
2131
+ return {
2132
+ type: targetType,
2133
+ id,
2134
+ entity: initiative,
2135
+ referenceLabel: typeof initiative.referenceLabel === 'string' ? initiative.referenceLabel : formatInitiativeReference(initiative),
2136
+ };
2137
+ }
2138
+ const { id, workstream } = resolveWorkstreamByIdentifierOrThrow(inputId);
2139
+ return {
2140
+ type: targetType,
2141
+ id,
2142
+ entity: workstream,
2143
+ referenceLabel: typeof workstream.referenceLabel === 'string' ? workstream.referenceLabel : formatWorkstreamReference(workstream),
2144
+ };
2145
+ };
2146
+ const formatPlanningAttachmentTarget = (target) => {
2147
+ const reference = String(target.referenceLabel || '').trim();
2148
+ if (reference)
2149
+ return reference;
2150
+ const title = String(target.entity?.title || '').trim();
2151
+ if (title)
2152
+ return `"${title}"`;
2153
+ return target.id;
2154
+ };
2155
+ const listPlanningAttachmentDescriptors = (target) => (taskAttachmentHelpers.listTaskAttachmentDescriptors({
2156
+ id: target.id,
2157
+ attachments: Array.isArray(target.entity?.attachments) ? target.entity.attachments : [],
2158
+ }));
2159
+ const updatePlanningAttachmentTarget = (targetType, targetId, attachments) => (targetType === 'initiative'
2160
+ ? core.updateInitiative(targetId, { attachments }, ACTIVE_WORKSPACE_ID)
2161
+ : core.updateWorkstream(targetId, { attachments }, ACTIVE_WORKSPACE_ID));
2162
+ const notifyPlanningAttachmentMutation = async (targetType, targetId, entity) => {
2163
+ if (targetType === 'initiative') {
2164
+ await notifyResourceMutation({
2165
+ includeInitiatives: true,
2166
+ initiativeIds: [targetId],
2167
+ });
2168
+ return;
2169
+ }
2170
+ await notifyResourceMutation({
2171
+ includeWorkstreams: true,
2172
+ workstreamIds: [targetId],
2173
+ ...(entity?.initiativeId ? { includeInitiatives: true, initiativeIds: [entity.initiativeId] } : {}),
2174
+ });
2175
+ };
2176
+ const buildPlanningAttachmentListPayload = (targetType, target) => ({
2177
+ [`${targetType}Id`]: target.id,
2178
+ referenceLabel: target.referenceLabel,
2179
+ attachments: listPlanningAttachmentDescriptors(target),
2180
+ });
2181
+ const readPlanningAttachment = async (targetType, args) => {
2182
+ const inputId = targetType === 'initiative' ? args.initiativeId : args.workstreamId;
2183
+ const { attachmentId, format = "text", maxChars = 200000 } = args;
2184
+ const target = resolvePlanningAttachmentTarget(targetType, inputId);
2185
+ const attachments = listPlanningAttachmentDescriptors(target);
2186
+ const attachment = attachments.find((entry) => entry.attachmentId === attachmentId);
2187
+ if (!attachment) {
2188
+ throw new Error(`Attachment ${attachmentId} not found on ${targetType} ${String(inputId || '').trim() || target.id}`);
2189
+ }
2190
+ if (format === "metadata") {
2191
+ return {
2192
+ content: [{
2193
+ type: "text",
2194
+ text: JSON.stringify({
2195
+ [`${targetType}Id`]: target.id,
2196
+ attachment,
2197
+ format: "metadata"
2198
+ }, null, 2)
2199
+ }],
2200
+ };
2201
+ }
2202
+ if (format === "download") {
2203
+ return {
2204
+ content: [{
2205
+ type: "text",
2206
+ text: JSON.stringify({
2207
+ [`${targetType}Id`]: target.id,
2208
+ attachment,
2209
+ format: "download",
2210
+ downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
2211
+ reason: attachment.source === 'external' ? 'external_reference' : 'requested_download'
2212
+ }, null, 2)
2213
+ }],
2214
+ };
2215
+ }
2216
+ if (!attachment.readableAsText) {
2217
+ return {
2218
+ content: [{
2219
+ type: "text",
2220
+ text: JSON.stringify({
2221
+ [`${targetType}Id`]: target.id,
2222
+ attachment,
2223
+ format: "download",
2224
+ downloadUrl: canonicalAssetHelpers.buildAttachmentDownloadUrl(attachment),
2225
+ reason: attachment.source === 'external' ? 'external_reference' : 'binary_file'
2226
+ }, null, 2)
2227
+ }],
2228
+ };
2229
+ }
2230
+ const resolvedMaxChars = Math.max(1, Math.floor(Number(maxChars || 200000)));
2231
+ const buffer = await canonicalAssetHelpers.readCanonicalAttachmentBuffer(target.id, attachment, targetType);
2232
+ const content = buffer.toString('utf8');
2233
+ if (content.length > resolvedMaxChars) {
2234
+ throw new Error('Attachment exceeds maxChars limit for inline read');
2235
+ }
2236
+ return {
2237
+ content: [{
2238
+ type: "text",
2239
+ text: JSON.stringify({
2240
+ [`${targetType}Id`]: target.id,
2241
+ attachment,
2242
+ format: "text",
2243
+ truncated: false,
2244
+ content
2245
+ }, null, 2)
2246
+ }],
2247
+ };
2248
+ };
2249
+ const savePlanningAttachment = async (targetType, args) => {
2250
+ const { id: inputId, filename, content, sourcePath, caption, overwrite = false } = args;
2251
+ const target = resolvePlanningAttachmentTarget(targetType, inputId);
2252
+ if (!filename || typeof filename !== 'string')
2253
+ throw new Error("filename is required");
2254
+ const hasContent = typeof content === 'string';
2255
+ const hasSourcePath = typeof sourcePath === 'string' && sourcePath.trim().length > 0;
2256
+ if (hasContent === hasSourcePath) {
2257
+ throw new Error("Provide exactly one of: content or sourcePath");
2258
+ }
2259
+ const now = new Date().toISOString();
2260
+ const safeCaption = typeof caption === 'string' && caption.trim().length > 0
2261
+ ? caption.trim()
2262
+ : undefined;
2263
+ const safeFilename = sanitizeFilenameForPath(filename);
2264
+ const { projectRoot, basePath } = core.getPaths();
2265
+ const targetForOverwrite = {
2266
+ id: target.id,
2267
+ attachments: Array.isArray(target.entity.attachments) ? target.entity.attachments : [],
2268
+ };
2269
+ const overwriteTarget = overwrite === true
2270
+ ? canonicalAssetHelpers.resolveCanonicalOverwriteTarget(targetForOverwrite, safeFilename, taskAttachmentHelpers.describeTaskAttachment)
2271
+ : null;
2272
+ const canonicalAsset = overwriteTarget
2273
+ ? null
2274
+ : canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
2275
+ workspaceIdRaw: ACTIVE_WORKSPACE_ID,
2276
+ kind: inferAssetKind(safeFilename, inferMimeTypeFromPath(safeFilename)),
2277
+ filename: safeFilename
2278
+ });
2279
+ const destinationPath = overwriteTarget
2280
+ ? overwriteTarget.absolutePath
2281
+ : path.join(basePath, canonicalAsset.relativePath);
2282
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
2283
+ if (hasContent) {
2284
+ fs.writeFileSync(destinationPath, content, 'utf8');
2285
+ }
2286
+ else {
2287
+ const trimmedSource = sourcePath.trim();
2288
+ const sourceResolved = path.isAbsolute(trimmedSource)
2289
+ ? path.resolve(trimmedSource)
2290
+ : path.resolve(projectRoot, trimmedSource);
2291
+ if (!fs.existsSync(sourceResolved)) {
2292
+ throw new Error(`File not found: ${trimmedSource}`);
2293
+ }
2294
+ const sourceReal = fs.realpathSync(sourceResolved);
2295
+ const projectRootReal = fs.realpathSync(projectRoot);
2296
+ if (!isApprovedAttachmentSourcePath(sourceReal, projectRootReal)) {
2297
+ throw new Error("sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root");
2298
+ }
2299
+ if (!fs.statSync(sourceReal).isFile()) {
2300
+ throw new Error(`Path is not a file: ${trimmedSource}`);
2301
+ }
2302
+ fs.copyFileSync(sourceReal, destinationPath);
2303
+ }
2304
+ const relativePath = overwriteTarget
2305
+ ? overwriteTarget.relativePath
2306
+ : toPosixPath(path.relative(projectRoot, destinationPath));
2307
+ if (relativePath.endsWith('.md')) {
2308
+ const fileContent = fs.readFileSync(destinationPath, 'utf8');
2309
+ const validation = validateStructuredDocForAttach(relativePath, fileContent);
2310
+ if (!validation.ok) {
2311
+ throw new Error(validation.message);
2312
+ }
2313
+ }
2314
+ const isMarkdownDocument = relativePath.toLowerCase().endsWith('.md') || relativePath.toLowerCase().endsWith('.markdown');
2315
+ const canonicalName = safeCaption || overwriteTarget?.asset.logicalName || safeFilename;
2316
+ const persistedCaption = isMarkdownDocument
2317
+ ? canonicalName
2318
+ : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
2319
+ const asset = await canonicalAssetHelpers.ensureCanonicalAttachment(relativePath, targetType, target.id, persistedCaption, now, 'attachment', {
2320
+ originalFilename: safeFilename,
2321
+ logicalName: isMarkdownDocument ? canonicalName : safeFilename,
2322
+ ...(overwriteTarget
2323
+ ? {
2324
+ assetId: overwriteTarget.asset.assetId,
2325
+ documentId: overwriteTarget.asset.documentId,
2326
+ createdAt: overwriteTarget.asset.createdAt,
2327
+ createdByUserId: overwriteTarget.asset.createdByUserId,
2328
+ scanStatus: overwriteTarget.asset.scanStatus,
2329
+ scanEngine: overwriteTarget.asset.scanEngine,
2330
+ scanDetails: overwriteTarget.asset.scanDetails,
2331
+ }
2332
+ : {})
2333
+ });
2334
+ const attachmentReferenceLabel = asset?.kind === 'image'
2335
+ ? getImageReferenceLabel(asset)
2336
+ : (asset && isValidCanonicalDocumentAsset(asset) ? getDocumentReferenceLabel(asset) : null);
2337
+ const attachment = {
2338
+ path: overwriteTarget?.attachment.path || (asset?.kind === 'document' && isValidCanonicalDocumentAsset(asset) && asset.assetId
2339
+ ? `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`
2340
+ : `/api/taskforce/context/${encodeURIComponent((canonicalAsset || { relativePath: overwriteTarget?.descriptor.fsPath || '' }).relativePath)}`),
2341
+ fsPath: overwriteTarget?.attachment.fsPath || canonicalAsset?.relativePath || overwriteTarget?.descriptor.fsPath,
2342
+ caption: isMarkdownDocument ? canonicalName : persistedCaption,
2343
+ displayName: isMarkdownDocument ? canonicalName : safeFilename,
2344
+ originalFilename: safeFilename,
2345
+ ...(asset?.assetId ? { assetId: asset.assetId } : {}),
2346
+ ...(typeof asset?.referenceNumber === 'number' ? { referenceNumber: asset.referenceNumber } : {}),
2347
+ ...(attachmentReferenceLabel ? { referenceLabel: attachmentReferenceLabel } : {}),
2348
+ timestamp: now
2349
+ };
2350
+ const existing = Array.isArray(target.entity.attachments)
2351
+ ? target.entity.attachments
2352
+ : [];
2353
+ if (overwriteTarget) {
2354
+ const updatedAttachments = existing.map((item, index) => (index === overwriteTarget.index
2355
+ ? attachment
2356
+ : item));
2357
+ updatePlanningAttachmentTarget(targetType, target.id, updatedAttachments);
2358
+ recordMcpWriteAudit(`save_${targetType}_attachment`, { [`${targetType}Id`]: target.id, path: attachment.fsPath, overwrite: true, attachmentId: asset?.assetId || overwriteTarget.descriptor.attachmentId });
2359
+ await notifyPlanningAttachmentMutation(targetType, target.id, target.entity);
2360
+ return {
2361
+ content: [{ type: "text", text: `${targetType[0].toUpperCase()}${targetType.slice(1)} attachment overwritten for ${formatPlanningAttachmentTarget(target)}\n${JSON.stringify(attachment, null, 2)}` }],
2362
+ };
2363
+ }
2364
+ const duplicate = existing.some((item) => {
2365
+ if (typeof item === 'string') {
2366
+ return item === attachment.path || item === attachment.fsPath;
2367
+ }
2368
+ if (!item || typeof item !== 'object')
2369
+ return false;
2370
+ if (item.path === attachment.path)
2371
+ return true;
2372
+ if (item.fsPath === attachment.fsPath)
2373
+ return true;
2374
+ return false;
2375
+ });
2376
+ if (duplicate) {
2377
+ return {
2378
+ content: [{ type: "text", text: `${targetType[0].toUpperCase()}${targetType.slice(1)} attachment saved for ${formatPlanningAttachmentTarget(target)}\n${JSON.stringify(attachment, null, 2)}\nAlready attached.` }],
2379
+ };
2380
+ }
2381
+ updatePlanningAttachmentTarget(targetType, target.id, [...existing, attachment]);
2382
+ recordMcpWriteAudit(`save_${targetType}_attachment`, { [`${targetType}Id`]: target.id, path: attachment.fsPath });
2383
+ await notifyPlanningAttachmentMutation(targetType, target.id, target.entity);
2384
+ return {
2385
+ content: [{ type: "text", text: `${targetType[0].toUpperCase()}${targetType.slice(1)} attachment saved and attached to ${formatPlanningAttachmentTarget(target)}\n${JSON.stringify(attachment, null, 2)}` }],
2386
+ };
2387
+ };
2388
+ const deletePlanningAttachment = async (targetType, args) => {
2389
+ const inputId = targetType === 'initiative' ? args.initiativeId : args.workstreamId;
2390
+ const { attachmentId } = args;
2391
+ if (!attachmentId || typeof attachmentId !== 'string')
2392
+ throw new Error('attachmentId is required');
2393
+ const target = resolvePlanningAttachmentTarget(targetType, inputId);
2394
+ const existing = Array.isArray(target.entity.attachments) ? target.entity.attachments : [];
2395
+ const descriptors = listPlanningAttachmentDescriptors(target);
2396
+ const targetIndex = descriptors.findIndex((entry) => entry.attachmentId === attachmentId);
2397
+ if (targetIndex < 0) {
2398
+ throw new Error(`Attachment ${attachmentId} not found on ${targetType} ${String(inputId || '').trim() || target.id}`);
2399
+ }
2400
+ const targetDescriptor = descriptors[targetIndex];
2401
+ const updatedAttachments = existing.filter((_, index) => index !== targetIndex);
2402
+ updatePlanningAttachmentTarget(targetType, target.id, updatedAttachments);
2403
+ let canonicalAssetDeleted = false;
2404
+ if (targetDescriptor.source === 'canonical' && targetDescriptor.assetId && workspaceAssetStore) {
2405
+ workspaceAssetStore.deleteLink({
2406
+ workspaceId: ACTIVE_WORKSPACE_ID,
2407
+ assetId: targetDescriptor.assetId,
2408
+ targetType,
2409
+ targetId: target.id,
2410
+ role: 'attachment',
2411
+ });
2412
+ const remainingLinks = workspaceAssetStore.listLinksForAsset(targetDescriptor.assetId, ACTIVE_WORKSPACE_ID);
2413
+ if (remainingLinks.length === 0) {
2414
+ workspaceAssetStore.markDeleted(targetDescriptor.assetId, ACTIVE_WORKSPACE_ID, new Date().toISOString(), null);
2415
+ canonicalAssetDeleted = true;
2416
+ }
2417
+ }
2418
+ recordMcpWriteAudit(`delete_${targetType}_attachment`, {
2419
+ [`${targetType}Id`]: target.id,
2420
+ attachmentId,
2421
+ canonicalAssetDeleted,
2422
+ });
2423
+ await notifyPlanningAttachmentMutation(targetType, target.id, target.entity);
2424
+ return {
2425
+ content: [{
2426
+ type: "text",
2427
+ text: JSON.stringify({
2428
+ success: true,
2429
+ [`${targetType}Id`]: target.id,
2430
+ attachmentId,
2431
+ canonicalAssetDeleted
2432
+ }, null, 2)
2433
+ }],
2434
+ };
2435
+ };
1815
2436
  const executeRegisteredTool = async (name, args) => {
1816
2437
  switch (name) {
1817
2438
  case "list_tasks": {
1818
2439
  const startedAtMs = Date.now();
1819
2440
  const { format = "markdown", category, status, openOnly = false, assignee, priority, type, sort = "created", order = "desc", limit } = args;
1820
- let data = core.listTasks(ACTIVE_WORKSPACE_ID);
1821
- let tasks = data.tasks;
1822
- const openStatuses = new Set(["task", "on-hold", "in-progress", "review"]);
1823
- // Filtering
1824
- if (category)
1825
- tasks = tasks.filter(t => t.category === category);
1826
- if (status)
1827
- tasks = tasks.filter(t => t.status === status);
1828
- if (openOnly)
1829
- tasks = tasks.filter(t => openStatuses.has(t.status));
1830
- if (assignee)
1831
- tasks = tasks.filter(t => (t.assignee || 'unassigned') === assignee);
1832
- if (priority !== undefined)
1833
- tasks = tasks.filter(t => t.priority === Number(priority));
1834
- if (type)
1835
- tasks = tasks.filter(t => t.type === type);
1836
- // Sorting
1837
- tasks.sort((a, b) => {
1838
- let aVal, bVal;
1839
- if (sort === "priority") {
1840
- aVal = a.priority;
1841
- bVal = b.priority;
1842
- }
1843
- else if (sort === "updated") {
1844
- aVal = a.updatedAt || a.createdAt;
1845
- bVal = b.updatedAt || b.createdAt;
1846
- }
1847
- else {
1848
- aVal = a.createdAt;
1849
- bVal = b.createdAt;
1850
- }
1851
- const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
1852
- return order === "asc" ? cmp : -cmp;
2441
+ const slimTasks = core.listTasksSlim(ACTIVE_WORKSPACE_ID, {
2442
+ category,
2443
+ status,
2444
+ openOnly: Boolean(openOnly),
2445
+ assignee,
2446
+ priority: priority !== undefined ? Number(priority) : undefined,
2447
+ type,
2448
+ sort,
2449
+ order,
2450
+ limit: typeof limit === 'number' ? limit : (limit ? Number(limit) : undefined),
1853
2451
  });
1854
- // Limiting
1855
- if (limit && limit > 0) {
1856
- tasks = tasks.slice(0, limit);
1857
- }
1858
- // Strip verbose detail fields — descriptions, comments, events, and derived
1859
- // activity are omitted from list responses to keep context window usage
1860
- // manageable. Use get_task to retrieve full details for a specific task.
1861
- const slimTasks = tasks.map(({ description, comments, taskEvents, activity, createdByActor, assigneeActor, attachments, checklistItems, ...rest }) => rest);
1862
2452
  logMcpReadObservation('list_tasks', startedAtMs, {
1863
2453
  format,
1864
- totalTasks: data.tasks.length,
1865
2454
  returnedTasks: slimTasks.length,
1866
- filtered: slimTasks.length !== data.tasks.length,
1867
2455
  openOnly,
1868
2456
  hasAssigneeFilter: Boolean(assignee),
1869
2457
  hasStatusFilter: Boolean(status),
@@ -1895,9 +2483,20 @@ export function createTaskforceMcpServer(options = {}) {
1895
2483
  model,
1896
2484
  surfaceType: typeof surfaceType === 'string' || surfaceType === null ? surfaceType : undefined,
1897
2485
  profileToken: typeof profileToken === 'string' ? profileToken : null
2486
+ }, {
2487
+ trustedSeatScope: configuredAiProfileSeatScope,
1898
2488
  });
1899
2489
  activeAiProfileId = resolved.profile.id;
1900
2490
  activeAiBindingSource = 'session';
2491
+ if (auditActorType === 'oauth' && auditTokenId && resolved.profile.profileToken) {
2492
+ core.bindMcpOAuthConnectorGrantAiProfile({
2493
+ grantId: auditTokenId,
2494
+ workspaceId: ACTIVE_WORKSPACE_ID,
2495
+ profileId: resolved.profile.id,
2496
+ profileToken: resolved.profile.profileToken,
2497
+ profileName: resolved.profile.name,
2498
+ });
2499
+ }
1901
2500
  recordMcpWriteAudit('resolve_profile', {
1902
2501
  profileId: resolved.profile.id,
1903
2502
  created: resolved.created
@@ -1921,6 +2520,7 @@ export function createTaskforceMcpServer(options = {}) {
1921
2520
  provider: resolved.profile.provider ?? null,
1922
2521
  model: resolved.profile.model ?? null,
1923
2522
  surfaceType: resolved.profile.surfaceType ?? null,
2523
+ seatScope: resolved.profile.seatScope ?? null,
1924
2524
  lastActiveAt: resolved.profile.lastActiveAt
1925
2525
  },
1926
2526
  ...(resolved.duplicateWarning && { duplicateWarning: resolved.duplicateWarning })
@@ -1951,6 +2551,7 @@ export function createTaskforceMcpServer(options = {}) {
1951
2551
  profile = core.getAiProfileByToken({
1952
2552
  workspaceId: ACTIVE_WORKSPACE_ID,
1953
2553
  profileToken: profileToken.trim(),
2554
+ seatScope: configuredAiProfileSeatScope,
1954
2555
  });
1955
2556
  if (!profile) {
1956
2557
  throw new Error('Invalid AI profile token. Call resolve_profile again and retry.');
@@ -2013,12 +2614,15 @@ export function createTaskforceMcpServer(options = {}) {
2013
2614
  };
2014
2615
  }
2015
2616
  case "create_initiative": {
2016
- const { title, description, ownerId, order } = args;
2617
+ const { title, description, ownerId, order, attachments } = args;
2618
+ if (attachments !== undefined && !Array.isArray(attachments))
2619
+ throw new Error('attachments must be an array');
2017
2620
  const created = core.createInitiative({
2018
2621
  title,
2019
2622
  description,
2020
2623
  ownerId: resolvePlanningOwnerInput(ownerId),
2021
2624
  order,
2625
+ ...(attachments !== undefined ? { attachments } : {}),
2022
2626
  }, ACTIVE_WORKSPACE_ID);
2023
2627
  recordMcpWriteAudit('create_initiative', { initiativeId: created.id });
2024
2628
  await notifyResourceMutation({
@@ -2026,11 +2630,11 @@ export function createTaskforceMcpServer(options = {}) {
2026
2630
  initiativeIds: [created.id],
2027
2631
  });
2028
2632
  return {
2029
- content: [{ type: "text", text: JSON.stringify(buildInitiativeResourcePayload(created), null, 2) }],
2633
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteInitiativeResourcePayload(created, 'create_initiative:response'), null, 2) }],
2030
2634
  };
2031
2635
  }
2032
2636
  case "update_initiative": {
2033
- const { id, title, description, ownerId, order } = args;
2637
+ const { id, title, description, ownerId, order, attachments } = args;
2034
2638
  const { id: initiativeId } = resolveInitiativeByIdentifierOrThrow(id);
2035
2639
  const patch = {};
2036
2640
  if (title !== undefined)
@@ -2041,6 +2645,11 @@ export function createTaskforceMcpServer(options = {}) {
2041
2645
  patch.ownerId = resolvePlanningOwnerInput(ownerId);
2042
2646
  if (order !== undefined)
2043
2647
  patch.order = order;
2648
+ if (attachments !== undefined) {
2649
+ if (!Array.isArray(attachments))
2650
+ throw new Error('attachments must be an array');
2651
+ patch.attachments = attachments;
2652
+ }
2044
2653
  const updated = core.updateInitiative(initiativeId, patch, ACTIVE_WORKSPACE_ID);
2045
2654
  recordMcpWriteAudit('update_initiative', { initiativeId: updated.id, changedFields: Object.keys(patch) });
2046
2655
  await notifyResourceMutation({
@@ -2048,7 +2657,7 @@ export function createTaskforceMcpServer(options = {}) {
2048
2657
  initiativeIds: [updated.id],
2049
2658
  });
2050
2659
  return {
2051
- content: [{ type: "text", text: JSON.stringify(buildInitiativeResourcePayload(updated), null, 2) }],
2660
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteInitiativeResourcePayload(updated, 'update_initiative:response'), null, 2) }],
2052
2661
  };
2053
2662
  }
2054
2663
  case "archive_initiative": {
@@ -2061,7 +2670,7 @@ export function createTaskforceMcpServer(options = {}) {
2061
2670
  initiativeIds: [archived.id],
2062
2671
  });
2063
2672
  return {
2064
- content: [{ type: "text", text: JSON.stringify(buildInitiativeResourcePayload(archived), null, 2) }],
2673
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteInitiativeResourcePayload(archived, 'archive_initiative:response'), null, 2) }],
2065
2674
  };
2066
2675
  }
2067
2676
  case "unarchive_initiative": {
@@ -2074,7 +2683,7 @@ export function createTaskforceMcpServer(options = {}) {
2074
2683
  initiativeIds: [unarchived.id],
2075
2684
  });
2076
2685
  return {
2077
- content: [{ type: "text", text: JSON.stringify(buildInitiativeResourcePayload(unarchived), null, 2) }],
2686
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteInitiativeResourcePayload(unarchived, 'unarchive_initiative:response'), null, 2) }],
2078
2687
  };
2079
2688
  }
2080
2689
  case "list_workstreams": {
@@ -2090,7 +2699,9 @@ export function createTaskforceMcpServer(options = {}) {
2090
2699
  };
2091
2700
  }
2092
2701
  case "create_workstream": {
2093
- const { title, description, ownerId, initiativeId, order } = args;
2702
+ const { title, description, ownerId, initiativeId, order, attachments } = args;
2703
+ if (attachments !== undefined && !Array.isArray(attachments))
2704
+ throw new Error('attachments must be an array');
2094
2705
  const initiative = initiativeId === undefined || String(initiativeId).trim() === ''
2095
2706
  ? null
2096
2707
  : resolveInitiativeByIdentifierOrThrow(initiativeId).id;
@@ -2100,6 +2711,7 @@ export function createTaskforceMcpServer(options = {}) {
2100
2711
  ownerId: resolvePlanningOwnerInput(ownerId),
2101
2712
  initiativeId: initiative,
2102
2713
  order,
2714
+ ...(attachments !== undefined ? { attachments } : {}),
2103
2715
  }, ACTIVE_WORKSPACE_ID);
2104
2716
  recordMcpWriteAudit('create_workstream', { workstreamId: created.id, initiativeId: created.initiativeId || null });
2105
2717
  await notifyResourceMutation({
@@ -2108,11 +2720,11 @@ export function createTaskforceMcpServer(options = {}) {
2108
2720
  ...(created.initiativeId ? { includeInitiatives: true, initiativeIds: [created.initiativeId] } : {}),
2109
2721
  });
2110
2722
  return {
2111
- content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(created), null, 2) }],
2723
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteWorkstreamResourcePayload(created, 'create_workstream:response'), null, 2) }],
2112
2724
  };
2113
2725
  }
2114
2726
  case "update_workstream": {
2115
- const { id, title, description, ownerId, initiativeId, order } = args;
2727
+ const { id, title, description, ownerId, initiativeId, order, attachments } = args;
2116
2728
  const { id: workstreamId, workstream: existingWorkstream } = resolveWorkstreamByIdentifierOrThrow(id);
2117
2729
  const patch = {};
2118
2730
  if (title !== undefined)
@@ -2123,6 +2735,11 @@ export function createTaskforceMcpServer(options = {}) {
2123
2735
  patch.ownerId = resolvePlanningOwnerInput(ownerId);
2124
2736
  if (order !== undefined)
2125
2737
  patch.order = order;
2738
+ if (attachments !== undefined) {
2739
+ if (!Array.isArray(attachments))
2740
+ throw new Error('attachments must be an array');
2741
+ patch.attachments = attachments;
2742
+ }
2126
2743
  if (initiativeId !== undefined) {
2127
2744
  patch.initiativeId = String(initiativeId).trim() === ''
2128
2745
  ? null
@@ -2141,7 +2758,7 @@ export function createTaskforceMcpServer(options = {}) {
2141
2758
  ...(relatedInitiativeIds.size > 0 ? { includeInitiatives: true, initiativeIds: Array.from(relatedInitiativeIds) } : {}),
2142
2759
  });
2143
2760
  return {
2144
- content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(updated), null, 2) }],
2761
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteWorkstreamResourcePayload(updated, 'update_workstream:response'), null, 2) }],
2145
2762
  };
2146
2763
  }
2147
2764
  case "archive_workstream": {
@@ -2155,7 +2772,7 @@ export function createTaskforceMcpServer(options = {}) {
2155
2772
  ...(existingWorkstream.initiativeId ? { includeInitiatives: true, initiativeIds: [existingWorkstream.initiativeId] } : {}),
2156
2773
  });
2157
2774
  return {
2158
- content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(archived), null, 2) }],
2775
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteWorkstreamResourcePayload(archived, 'archive_workstream:response'), null, 2) }],
2159
2776
  };
2160
2777
  }
2161
2778
  case "unarchive_workstream": {
@@ -2169,7 +2786,7 @@ export function createTaskforceMcpServer(options = {}) {
2169
2786
  ...(existingWorkstream.initiativeId ? { includeInitiatives: true, initiativeIds: [existingWorkstream.initiativeId] } : {}),
2170
2787
  });
2171
2788
  return {
2172
- content: [{ type: "text", text: JSON.stringify(buildWorkstreamResourcePayload(unarchived), null, 2) }],
2789
+ content: [{ type: "text", text: JSON.stringify(buildPostWriteWorkstreamResourcePayload(unarchived, 'unarchive_workstream:response'), null, 2) }],
2173
2790
  };
2174
2791
  }
2175
2792
  case "set_task_workstream": {
@@ -2291,10 +2908,11 @@ export function createTaskforceMcpServer(options = {}) {
2291
2908
  };
2292
2909
  }
2293
2910
  case "get_task_attachments": {
2294
- const { id } = args;
2295
- const task = core.resolveTaskIdentifier(id, ACTIVE_WORKSPACE_ID);
2911
+ const { id, taskId } = args;
2912
+ const taskIdentifier = taskId ?? id;
2913
+ const task = core.resolveTaskIdentifier(taskIdentifier, ACTIVE_WORKSPACE_ID);
2296
2914
  if (!task) {
2297
- throw new Error(`Task ${id} not found`);
2915
+ throw new Error(`Task ${taskIdentifier} not found`);
2298
2916
  }
2299
2917
  return {
2300
2918
  content: [{
@@ -2377,6 +2995,32 @@ export function createTaskforceMcpServer(options = {}) {
2377
2995
  }],
2378
2996
  };
2379
2997
  }
2998
+ case "get_workstream_attachments": {
2999
+ const { id } = args;
3000
+ const target = resolvePlanningAttachmentTarget('workstream', id);
3001
+ return {
3002
+ content: [{
3003
+ type: "text",
3004
+ text: JSON.stringify(buildPlanningAttachmentListPayload('workstream', target), null, 2)
3005
+ }],
3006
+ };
3007
+ }
3008
+ case "read_workstream_attachment": {
3009
+ return readPlanningAttachment('workstream', args);
3010
+ }
3011
+ case "get_initiative_attachments": {
3012
+ const { id } = args;
3013
+ const target = resolvePlanningAttachmentTarget('initiative', id);
3014
+ return {
3015
+ content: [{
3016
+ type: "text",
3017
+ text: JSON.stringify(buildPlanningAttachmentListPayload('initiative', target), null, 2)
3018
+ }],
3019
+ };
3020
+ }
3021
+ case "read_initiative_attachment": {
3022
+ return readPlanningAttachment('initiative', args);
3023
+ }
2380
3024
  case "search_images": {
2381
3025
  assertAnnotatedAttachmentReadAccess();
2382
3026
  const { query, limit } = args;
@@ -2405,8 +3049,8 @@ export function createTaskforceMcpServer(options = {}) {
2405
3049
  }
2406
3050
  case "get_image": {
2407
3051
  assertAnnotatedAttachmentReadAccess();
2408
- const { imageId } = args;
2409
- const image = resolveImageAssetByIdentifierOrThrow(imageId);
3052
+ const { id, imageId } = args;
3053
+ const image = resolveImageAssetByIdentifierOrThrow(imageId ?? id);
2410
3054
  const sessionsForImage = core.listAnnotatedAttachmentSessions({
2411
3055
  workspaceId: ACTIVE_WORKSPACE_ID,
2412
3056
  imageAssetId: image.assetId,
@@ -2430,19 +3074,21 @@ export function createTaskforceMcpServer(options = {}) {
2430
3074
  }
2431
3075
  const linkedTasks = (workspaceAssetStore?.listLinksForAsset(image.assetId, ACTIVE_WORKSPACE_ID) || [])
2432
3076
  .filter((entry) => !entry.deletedAt)
3077
+ .filter((entry) => entry.targetType === 'task' && !!entry.taskId)
2433
3078
  .reduce((entries, link) => {
2434
- if (entries.some((entry) => entry.taskId === link.taskId))
3079
+ const taskId = link.taskId;
3080
+ if (entries.some((entry) => entry.taskId === taskId))
2435
3081
  return entries;
2436
- const task = core.resolveTaskIdentifier(link.taskId, ACTIVE_WORKSPACE_ID);
3082
+ const task = core.resolveTaskIdentifier(taskId, ACTIVE_WORKSPACE_ID);
2437
3083
  const taskReferenceLabel = task?.referenceLabel || (task ? getTaskReferenceLabel(task) : null);
2438
- const sessions = (linkedTaskSessions.get(link.taskId) || []).map((session) => ({
3084
+ const sessions = (linkedTaskSessions.get(taskId) || []).map((session) => ({
2439
3085
  id: session.id,
2440
3086
  title: session.title,
2441
3087
  updatedAt: session.updatedAt,
2442
3088
  annotationCount: Array.isArray(session.annotations) ? session.annotations.length : 0,
2443
3089
  }));
2444
3090
  entries.push({
2445
- taskId: link.taskId,
3091
+ taskId,
2446
3092
  referenceLabel: taskReferenceLabel,
2447
3093
  role: link.role,
2448
3094
  caption: link.displayName || null,
@@ -2488,15 +3134,17 @@ export function createTaskforceMcpServer(options = {}) {
2488
3134
  };
2489
3135
  }
2490
3136
  case "get_document": {
2491
- const { documentId } = args;
2492
- const asset = resolveDocumentAssetByIdentifierOrThrow(documentId);
3137
+ const { id, documentId } = args;
3138
+ const asset = resolveDocumentAssetByIdentifierOrThrow(documentId ?? id);
2493
3139
  const linkedTasks = (workspaceAssetStore?.listLinksForAsset(asset.assetId, ACTIVE_WORKSPACE_ID) || [])
2494
3140
  .filter((entry) => !entry.deletedAt)
3141
+ .filter((entry) => entry.targetType === 'task' && !!entry.taskId)
2495
3142
  .map((link) => {
2496
- const task = core.resolveTaskIdentifier(link.taskId, ACTIVE_WORKSPACE_ID);
3143
+ const taskId = link.taskId;
3144
+ const task = core.resolveTaskIdentifier(taskId, ACTIVE_WORKSPACE_ID);
2497
3145
  const taskReferenceLabel = task?.referenceLabel || (task ? getTaskReferenceLabel(task) : null);
2498
3146
  return {
2499
- taskId: link.taskId,
3147
+ taskId,
2500
3148
  referenceLabel: taskReferenceLabel,
2501
3149
  role: link.role,
2502
3150
  caption: link.displayName || null,
@@ -3012,9 +3660,13 @@ export function createTaskforceMcpServer(options = {}) {
3012
3660
  };
3013
3661
  }
3014
3662
  case "review_document": {
3015
- const { documentId, sessionId, title, comments } = args;
3016
- const asset = resolveDocumentAssetByIdentifierOrThrow(documentId);
3017
- const normalizedComments = Array.isArray(comments) ? comments : [];
3663
+ const { id, documentId, sessionId, title, comments, prompt } = args;
3664
+ const asset = resolveDocumentAssetByIdentifierOrThrow(documentId ?? id);
3665
+ const normalizedComments = Array.isArray(comments)
3666
+ ? comments
3667
+ : (typeof prompt === 'string' && prompt.trim()
3668
+ ? [{ body: prompt.trim() }]
3669
+ : []);
3018
3670
  if (normalizedComments.length === 0) {
3019
3671
  throw new Error('comments array is required');
3020
3672
  }
@@ -3188,16 +3840,19 @@ export function createTaskforceMcpServer(options = {}) {
3188
3840
  }
3189
3841
  case "get_schedule_board": {
3190
3842
  const { week, showWeekends = false } = args;
3191
- if (!week || typeof week !== 'string') {
3192
- throw new Error('week is required');
3193
- }
3194
- const board = core.getScheduleBoard(week, Boolean(showWeekends), ACTIVE_WORKSPACE_ID);
3843
+ const weekKey = typeof week === 'string' && week.trim()
3844
+ ? week.trim()
3845
+ : currentIsoWeekKeyLocal();
3846
+ const board = core.getScheduleBoard(weekKey, Boolean(showWeekends), ACTIVE_WORKSPACE_ID);
3195
3847
  return {
3196
3848
  content: [{ type: "text", text: JSON.stringify(board, null, 2) }],
3197
3849
  };
3198
3850
  }
3199
3851
  case "schedule_task": {
3200
- const { id: inputId, scheduledDate, orderInDay } = args;
3852
+ const { id: legacyInputId, taskId, scheduledDate: legacyScheduledDate, date, orderInDay: legacyOrderInDay, order } = args;
3853
+ const inputId = taskId ?? legacyInputId;
3854
+ const scheduledDate = date ?? legacyScheduledDate;
3855
+ const orderInDay = order ?? legacyOrderInDay;
3201
3856
  const { id } = resolveTaskByIdentifierOrThrow(inputId);
3202
3857
  if (!scheduledDate || typeof scheduledDate !== 'string')
3203
3858
  throw new Error('scheduledDate is required');
@@ -3230,7 +3885,8 @@ export function createTaskforceMcpServer(options = {}) {
3230
3885
  };
3231
3886
  }
3232
3887
  case "unschedule_task": {
3233
- const { id: inputId } = args;
3888
+ const { id: legacyInputId, taskId } = args;
3889
+ const inputId = taskId ?? legacyInputId;
3234
3890
  const { id } = resolveTaskByIdentifierOrThrow(inputId);
3235
3891
  const result = core.updateTaskSchedule(id, { scheduledDate: null, orderInDay: null }, ACTIVE_WORKSPACE_ID);
3236
3892
  if (!result) {
@@ -3261,13 +3917,18 @@ export function createTaskforceMcpServer(options = {}) {
3261
3917
  };
3262
3918
  }
3263
3919
  case "reorder_scheduled_tasks": {
3264
- const { moves } = args;
3265
- if (!Array.isArray(moves)) {
3920
+ const { moves, date, taskIds } = args;
3921
+ const inputMoves = Array.isArray(moves)
3922
+ ? moves
3923
+ : Array.isArray(taskIds) && typeof date === 'string'
3924
+ ? taskIds.map((taskId, index) => ({ id: taskId, scheduledDate: date, orderInDay: index }))
3925
+ : null;
3926
+ if (!Array.isArray(inputMoves)) {
3266
3927
  throw new Error('moves must be an array');
3267
3928
  }
3268
- const resolvedMoves = moves.map((move) => ({
3929
+ const resolvedMoves = inputMoves.map((move) => ({
3269
3930
  ...move,
3270
- id: resolveTaskByIdentifierOrThrow(move?.id).id
3931
+ id: resolveTaskByIdentifierOrThrow(move?.taskId ?? move?.id).id
3271
3932
  }));
3272
3933
  const result = core.reorderScheduledTasks(resolvedMoves, ACTIVE_WORKSPACE_ID);
3273
3934
  recordMcpWriteAudit('reorder_scheduled_tasks', {
@@ -3721,11 +4382,12 @@ export function createTaskforceMcpServer(options = {}) {
3721
4382
  if (!validation.ok) {
3722
4383
  throw new Error(validation.message);
3723
4384
  }
3724
- const updated = core.addComment(id, text, author, ACTIVE_WORKSPACE_ID);
4385
+ const effectiveAuthor = resolveMcpCommentAuthorInput(author);
4386
+ const updated = core.addComment(id, text, effectiveAuthor, ACTIVE_WORKSPACE_ID, { actorRef: effectiveAuthor });
3725
4387
  if (!updated) {
3726
4388
  throw new Error(`Task ${id} not found`);
3727
4389
  }
3728
- recordMcpWriteAudit('add_comment', { taskId: id, author: author || 'ai' });
4390
+ recordMcpWriteAudit('add_comment', { taskId: id, author: effectiveAuthor });
3729
4391
  await notifyResourceMutation({
3730
4392
  taskIds: [id],
3731
4393
  includeTasks: true,
@@ -3778,7 +4440,7 @@ export function createTaskforceMcpServer(options = {}) {
3778
4440
  }
3779
4441
  const updated = core.updateTask(id, {
3780
4442
  attachments: [...existing, attachment]
3781
- }, ACTIVE_WORKSPACE_ID);
4443
+ }, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
3782
4444
  if (!updated) {
3783
4445
  throw new Error(`Task ${id} not found`);
3784
4446
  }
@@ -3792,97 +4454,319 @@ export function createTaskforceMcpServer(options = {}) {
3792
4454
  };
3793
4455
  }
3794
4456
  case "save_task_attachment": {
3795
- const { id: inputId, filename, content, sourcePath, caption, overwrite = false } = args;
4457
+ const { id: inputId, filename, content, sourcePath, uploadedPath, uploadDraftId, mimeType, sizeBytes, caption, overwrite = false } = args;
3796
4458
  const { id, task } = resolveTaskByIdentifierOrThrow(inputId);
3797
4459
  if (!filename || typeof filename !== 'string')
3798
4460
  throw new Error("filename is required");
3799
4461
  const hasContent = typeof content === 'string';
3800
4462
  const hasSourcePath = typeof sourcePath === 'string' && sourcePath.trim().length > 0;
3801
- if (hasContent === hasSourcePath) {
3802
- throw new Error("Provide exactly one of: content or sourcePath");
3803
- }
4463
+ const hasUploadedPath = typeof uploadedPath === 'string' && uploadedPath.trim().length > 0;
4464
+ const contentModeCount = Number(hasContent) + Number(hasSourcePath) + Number(hasUploadedPath);
3804
4465
  const now = new Date().toISOString();
3805
4466
  const safeCaption = typeof caption === 'string' && caption.trim().length > 0
3806
4467
  ? caption.trim()
3807
4468
  : undefined;
3808
4469
  const safeFilename = sanitizeFilenameForPath(filename);
4470
+ const safeMimeType = typeof mimeType === 'string' && mimeType.trim().length > 0
4471
+ ? mimeType.trim().toLowerCase()
4472
+ : '';
4473
+ const numericSizeBytes = Number(sizeBytes);
4474
+ const hasStagedInitMetadata = !!safeMimeType && Number.isFinite(numericSizeBytes) && numericSizeBytes > 0;
3809
4475
  const { projectRoot, basePath } = core.getPaths();
3810
4476
  const overwriteTarget = overwrite === true
3811
4477
  ? canonicalAssetHelpers.resolveCanonicalOverwriteTarget(task, safeFilename, taskAttachmentHelpers.describeTaskAttachment)
3812
4478
  : null;
3813
- const canonicalAsset = overwriteTarget
3814
- ? null
3815
- : canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
4479
+ const buildAttachmentRecord = (asset, storageKey) => {
4480
+ const isMarkdownDocument = storageKey.toLowerCase().endsWith('.md') || storageKey.toLowerCase().endsWith('.markdown');
4481
+ const canonicalName = safeCaption || overwriteTarget?.asset.logicalName || safeFilename;
4482
+ const persistedCaption = isMarkdownDocument
4483
+ ? canonicalName
4484
+ : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
4485
+ return {
4486
+ path: overwriteTarget?.attachment.path || (asset?.kind === 'document' && isValidCanonicalDocumentAsset(asset) && asset.assetId
4487
+ ? `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`
4488
+ : `/api/taskforce/context/${encodeURIComponent(storageKey)}`),
4489
+ fsPath: overwriteTarget?.attachment.fsPath || storageKey,
4490
+ caption: isMarkdownDocument ? canonicalName : persistedCaption,
4491
+ displayName: isMarkdownDocument ? canonicalName : safeFilename,
4492
+ originalFilename: safeFilename,
4493
+ ...(asset?.assetId ? { assetId: asset.assetId } : {}),
4494
+ timestamp: now
4495
+ };
4496
+ };
4497
+ if (contentModeCount === 0) {
4498
+ if (!hasStagedInitMetadata) {
4499
+ throw new Error('Provide exactly one of: content, sourcePath, or uploadedPath. To start a staged remote upload, provide mimeType and sizeBytes.');
4500
+ }
4501
+ if (runtimeMode !== 'cloud' || !objectStorageClient || objectStorageConfig.provider !== 'r2' || !objectStorageConfig.r2) {
4502
+ throw new Error('Staged remote uploads require cloud runtime with object storage enabled. Otherwise provide content or sourcePath.');
4503
+ }
4504
+ const stagedAsset = canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
3816
4505
  workspaceIdRaw: ACTIVE_WORKSPACE_ID,
3817
- kind: inferAssetKind(safeFilename, inferMimeTypeFromPath(safeFilename)),
4506
+ kind: inferAssetKind(safeFilename, safeMimeType),
3818
4507
  filename: safeFilename
3819
4508
  });
3820
- const destinationPath = overwriteTarget
3821
- ? overwriteTarget.absolutePath
3822
- : path.join(basePath, canonicalAsset.relativePath);
3823
- fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
3824
- if (hasContent) {
3825
- fs.writeFileSync(destinationPath, content, 'utf8');
4509
+ const uploadDraftId = `task-attachment-draft-${randomUUID().replace(/-/g, '')}`;
4510
+ const uploadExpiresInSeconds = Math.max(1, Math.floor(objectStorageConfig.r2.signedUploadTtlSeconds));
4511
+ const uploadExpiresAt = new Date(Date.now() + uploadExpiresInSeconds * 1000).toISOString();
4512
+ core.createTaskAttachmentUploadDraft({
4513
+ draftId: uploadDraftId,
4514
+ workspaceId: ACTIVE_WORKSPACE_ID,
4515
+ taskId: id,
4516
+ actorRef: getActiveMcpActorRef(),
4517
+ storageKey: stagedAsset.relativePath,
4518
+ mimeType: safeMimeType,
4519
+ originalFilename: safeFilename,
4520
+ sizeBytes: Math.floor(numericSizeBytes),
4521
+ storageProvider: 'r2',
4522
+ });
4523
+ const uploadUrl = objectStorageClient.createSignedPutUrl(stagedAsset.relativePath, uploadExpiresInSeconds, safeMimeType);
4524
+ return {
4525
+ content: [{
4526
+ type: "text",
4527
+ text: JSON.stringify({
4528
+ success: true,
4529
+ mode: 'staged_upload',
4530
+ status: 'upload_required',
4531
+ taskId: id,
4532
+ referenceLabel: formatTaskIdentifier(task),
4533
+ filename: safeFilename,
4534
+ mimeType: safeMimeType,
4535
+ sizeBytes: Math.floor(numericSizeBytes),
4536
+ assetId: stagedAsset.assetId,
4537
+ uploadDraftId,
4538
+ expiresAt: uploadExpiresAt,
4539
+ upload: {
4540
+ uploadUrl,
4541
+ method: 'PUT',
4542
+ headers: {
4543
+ 'Content-Type': safeMimeType
4544
+ },
4545
+ uploadedPath: stagedAsset.relativePath,
4546
+ },
4547
+ finalize: {
4548
+ tool: 'save_task_attachment',
4549
+ arguments: {
4550
+ id: inputId,
4551
+ filename: safeFilename,
4552
+ uploadDraftId,
4553
+ mimeType: safeMimeType,
4554
+ uploadedPath: stagedAsset.relativePath,
4555
+ ...(safeCaption ? { caption: safeCaption } : {}),
4556
+ ...(overwrite === true ? { overwrite: true } : {}),
4557
+ }
4558
+ },
4559
+ guidance: 'Upload the raw file bytes to upload.uploadUrl with the listed headers before expiresAt, then call save_task_attachment again with finalize.arguments to attach the uploaded file to the task.'
4560
+ }, null, 2)
4561
+ }],
4562
+ };
3826
4563
  }
3827
- else {
3828
- const trimmedSource = sourcePath.trim();
3829
- const sourceResolved = path.isAbsolute(trimmedSource)
3830
- ? path.resolve(trimmedSource)
3831
- : path.resolve(projectRoot, trimmedSource);
3832
- if (!fs.existsSync(sourceResolved)) {
3833
- throw new Error(`File not found: ${trimmedSource}`);
4564
+ if (contentModeCount > 1) {
4565
+ throw new Error('Provide exactly one of: content, sourcePath, or uploadedPath');
4566
+ }
4567
+ let attachment;
4568
+ let asset = null;
4569
+ let storageKeyForAudit = '';
4570
+ let replacedStorageKey = null;
4571
+ if (hasUploadedPath) {
4572
+ if (!objectStorageClient || objectStorageConfig.provider !== 'r2') {
4573
+ throw new Error('uploadedPath requires object storage to be enabled for this MCP runtime.');
3834
4574
  }
3835
- const sourceReal = fs.realpathSync(sourceResolved);
3836
- const projectRootReal = fs.realpathSync(projectRoot);
3837
- if (!isApprovedAttachmentSourcePath(sourceReal, projectRootReal)) {
3838
- throw new Error("sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root");
4575
+ if (!workspaceAssetStore) {
4576
+ throw new Error('uploadedPath requires canonical asset storage support.');
3839
4577
  }
3840
- if (!fs.statSync(sourceReal).isFile()) {
3841
- throw new Error(`Path is not a file: ${trimmedSource}`);
4578
+ const normalizedUploadDraftId = typeof uploadDraftId === 'string' ? uploadDraftId.trim() : '';
4579
+ if (!normalizedUploadDraftId) {
4580
+ throw new Error('uploadedPath finalization requires uploadDraftId from the staged upload init response.');
3842
4581
  }
3843
- fs.copyFileSync(sourceReal, destinationPath);
3844
- }
3845
- const relativePath = overwriteTarget
3846
- ? overwriteTarget.relativePath
3847
- : toPosixPath(path.relative(projectRoot, destinationPath));
3848
- if (relativePath.endsWith('.md')) {
3849
- const fileContent = fs.readFileSync(destinationPath, 'utf8');
3850
- const validation = validateStructuredDocForAttach(relativePath, fileContent);
3851
- if (!validation.ok) {
3852
- throw new Error(validation.message);
4582
+ const draft = core.getTaskAttachmentUploadDraft(normalizedUploadDraftId);
4583
+ if (!draft || draft.discardedAt || draft.consumedAt) {
4584
+ throw new Error('uploadDraftId is invalid, expired, or already consumed.');
4585
+ }
4586
+ if (draft.workspaceId !== ACTIVE_WORKSPACE_ID) {
4587
+ throw new Error('uploadDraftId does not belong to the active workspace.');
4588
+ }
4589
+ if (draft.taskId !== id) {
4590
+ throw new Error('uploadDraftId does not belong to this task.');
4591
+ }
4592
+ if (draft.actorRef !== getActiveMcpActorRef()) {
4593
+ throw new Error('uploadDraftId was created by a different actor and cannot finalize this attachment.');
4594
+ }
4595
+ const normalizedUploadedPath = normalizeTaskDataRelativePath(uploadedPath.trim());
4596
+ if (!normalizedUploadedPath) {
4597
+ throw new Error('uploadedPath must be a valid Taskforce storage path.');
4598
+ }
4599
+ if (draft.storageKey !== normalizedUploadedPath) {
4600
+ throw new Error('uploadedPath does not match the staged upload draft.');
4601
+ }
4602
+ if (inferWorkspaceIdFromStoragePath(normalizedUploadedPath) !== ACTIVE_WORKSPACE_ID) {
4603
+ throw new Error('uploadedPath does not belong to the active workspace.');
4604
+ }
4605
+ const uploadTtlSeconds = objectStorageConfig.provider === 'r2' && objectStorageConfig.r2
4606
+ ? Math.max(1, Math.floor(objectStorageConfig.r2.signedUploadTtlSeconds))
4607
+ : 0;
4608
+ if (uploadTtlSeconds > 0) {
4609
+ const draftCreatedAtMs = Date.parse(draft.createdAt);
4610
+ if (Number.isFinite(draftCreatedAtMs) && Date.now() > draftCreatedAtMs + uploadTtlSeconds * 1000) {
4611
+ core.discardTaskAttachmentUploadDraft(draft.draftId);
4612
+ await objectStorageClient.deleteObject(normalizedUploadedPath).catch(() => { });
4613
+ throw new Error('uploadDraftId has expired. Start a new staged upload.');
4614
+ }
4615
+ }
4616
+ const uploadedObject = await objectStorageClient.getObject(normalizedUploadedPath);
4617
+ if (!uploadedObject) {
4618
+ core.discardTaskAttachmentUploadDraft(draft.draftId);
4619
+ throw new Error(`Uploaded file not found: ${normalizedUploadedPath}`);
4620
+ }
4621
+ if (safeFilename !== draft.originalFilename) {
4622
+ throw new Error('filename does not match the staged upload draft.');
4623
+ }
4624
+ const draftMimeType = String(draft.mimeType || '').trim().toLowerCase() || 'application/octet-stream';
4625
+ if (safeMimeType && safeMimeType !== draftMimeType) {
4626
+ throw new Error('mimeType does not match the staged upload draft.');
4627
+ }
4628
+ const uploadedContentType = String(uploadedObject.contentType || '').trim().toLowerCase();
4629
+ if (uploadedContentType && uploadedContentType !== draftMimeType) {
4630
+ throw new Error('Uploaded object content type does not match the staged upload draft.');
4631
+ }
4632
+ if (uploadedObject.body.length !== draft.sizeBytes) {
4633
+ throw new Error('Uploaded object size does not match the staged upload draft.');
4634
+ }
4635
+ const resolvedMimeType = draftMimeType || uploadedContentType || inferMimeTypeFromPath(safeFilename);
4636
+ if (normalizedUploadedPath.endsWith('.md') || normalizedUploadedPath.endsWith('.markdown')) {
4637
+ const validation = validateStructuredDocForAttach(normalizedUploadedPath, uploadedObject.body.toString('utf8'));
4638
+ if (!validation.ok) {
4639
+ throw new Error(validation.message);
4640
+ }
4641
+ }
4642
+ const virusScanConfig = resolveVirusScanConfigFromEnv(process.env);
4643
+ const scan = await scanBufferForThreats(uploadedObject.body, virusScanConfig);
4644
+ if (scan.verdict === 'infected') {
4645
+ await objectStorageClient.deleteObject(normalizedUploadedPath).catch(() => { });
4646
+ throw new Error('File rejected by virus scanner.');
3853
4647
  }
4648
+ if (scan.verdict === 'error' && virusScanConfig.failClosed) {
4649
+ throw new Error('Virus scan failed. Upload blocked.');
4650
+ }
4651
+ const kind = inferAssetKind(safeFilename, resolvedMimeType);
4652
+ const embeddedAssetId = normalizedUploadedPath.match(/(^|\/)(asset-[a-f0-9]{32})(?=[^/]*$)/i)?.[2] || null;
4653
+ const existingUploadedAsset = workspaceAssetStore.getByStorageKey(normalizedUploadedPath, ACTIVE_WORKSPACE_ID);
4654
+ const isMarkdownDocument = normalizedUploadedPath.toLowerCase().endsWith('.md') || normalizedUploadedPath.toLowerCase().endsWith('.markdown');
4655
+ const canonicalName = safeCaption || overwriteTarget?.asset.logicalName || safeFilename;
4656
+ const persistedCaption = isMarkdownDocument
4657
+ ? canonicalName
4658
+ : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
4659
+ replacedStorageKey = overwriteTarget?.asset.storageKey && overwriteTarget.asset.storageKey !== normalizedUploadedPath
4660
+ ? overwriteTarget.asset.storageKey
4661
+ : null;
4662
+ asset = workspaceAssetStore.upsertAsset({
4663
+ assetId: overwriteTarget?.asset.assetId || existingUploadedAsset?.assetId || embeddedAssetId || `asset-${randomUUID().replace(/-/g, '')}`,
4664
+ workspaceId: ACTIVE_WORKSPACE_ID,
4665
+ kind,
4666
+ logicalName: isMarkdownDocument ? canonicalName : safeFilename,
4667
+ originalFilename: safeFilename,
4668
+ mimeType: resolvedMimeType,
4669
+ storageProvider: 'r2',
4670
+ storageKey: normalizedUploadedPath,
4671
+ contentSha256: createHash('sha256').update(uploadedObject.body).digest('hex'),
4672
+ sizeBytes: uploadedObject.body.length,
4673
+ documentId: kind === 'document'
4674
+ ? (overwriteTarget?.asset.documentId || existingUploadedAsset?.documentId || `doc-${randomUUID().replace(/-/g, '')}`)
4675
+ : null,
4676
+ version: kind === 'document'
4677
+ ? Math.max(1, Number(overwriteTarget?.asset.version || existingUploadedAsset?.version || 0) + 1)
4678
+ : null,
4679
+ isLatest: true,
4680
+ scanStatus: scanVerdictToCanonicalStatus(scan.verdict),
4681
+ scanEngine: scan.engine || null,
4682
+ scanDetails: scan.details || null,
4683
+ createdByUserId: overwriteTarget?.asset.createdByUserId || getActiveMcpActorRef(),
4684
+ createdAt: overwriteTarget?.asset.createdAt || existingUploadedAsset?.createdAt || now,
4685
+ updatedAt: now,
4686
+ deletedAt: null,
4687
+ purgeAfter: null,
4688
+ });
4689
+ workspaceAssetStore.upsertLink({
4690
+ workspaceId: ACTIVE_WORKSPACE_ID,
4691
+ assetId: asset.assetId,
4692
+ taskId: id,
4693
+ role: 'attachment',
4694
+ displayName: kind === 'document' ? null : persistedCaption,
4695
+ updatedAt: now,
4696
+ deletedAt: null,
4697
+ });
4698
+ core.markTaskAttachmentUploadDraftConsumed(draft.draftId);
4699
+ attachment = buildAttachmentRecord(asset, normalizedUploadedPath);
4700
+ storageKeyForAudit = normalizedUploadedPath;
3854
4701
  }
3855
- const isMarkdownDocument = relativePath.toLowerCase().endsWith('.md') || relativePath.toLowerCase().endsWith('.markdown');
3856
- const canonicalName = safeCaption || overwriteTarget?.asset.logicalName || safeFilename;
3857
- const persistedCaption = isMarkdownDocument
3858
- ? canonicalName
3859
- : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
3860
- const asset = canonicalAssetHelpers.ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
3861
- originalFilename: safeFilename,
3862
- logicalName: isMarkdownDocument ? canonicalName : safeFilename,
3863
- ...(overwriteTarget
3864
- ? {
3865
- assetId: overwriteTarget.asset.assetId,
3866
- documentId: overwriteTarget.asset.documentId,
3867
- createdAt: overwriteTarget.asset.createdAt,
3868
- createdByUserId: overwriteTarget.asset.createdByUserId,
3869
- scanStatus: overwriteTarget.asset.scanStatus,
3870
- scanEngine: overwriteTarget.asset.scanEngine,
3871
- scanDetails: overwriteTarget.asset.scanDetails,
4702
+ else {
4703
+ const canonicalAsset = overwriteTarget
4704
+ ? null
4705
+ : canonicalAssetHelpers.buildCanonicalWorkspaceAssetRelativePath({
4706
+ workspaceIdRaw: ACTIVE_WORKSPACE_ID,
4707
+ kind: inferAssetKind(safeFilename, inferMimeTypeFromPath(safeFilename)),
4708
+ filename: safeFilename
4709
+ });
4710
+ const destinationPath = overwriteTarget
4711
+ ? overwriteTarget.absolutePath
4712
+ : path.join(basePath, canonicalAsset.relativePath);
4713
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
4714
+ if (hasContent) {
4715
+ fs.writeFileSync(destinationPath, content, 'utf8');
4716
+ }
4717
+ else {
4718
+ const trimmedSource = sourcePath.trim();
4719
+ const sourceResolved = path.isAbsolute(trimmedSource)
4720
+ ? path.resolve(trimmedSource)
4721
+ : path.resolve(projectRoot, trimmedSource);
4722
+ if (!fs.existsSync(sourceResolved)) {
4723
+ throw new Error(`File not found: ${trimmedSource}`);
3872
4724
  }
3873
- : {})
3874
- });
3875
- const attachment = {
3876
- path: overwriteTarget?.attachment.path || (asset?.kind === 'document' && isValidCanonicalDocumentAsset(asset) && asset.assetId
3877
- ? `/api/taskforce/documents/${encodeURIComponent(asset.assetId)}/content`
3878
- : `/api/taskforce/context/${encodeURIComponent((canonicalAsset || { relativePath: overwriteTarget?.descriptor.fsPath || '' }).relativePath)}`),
3879
- fsPath: overwriteTarget?.attachment.fsPath || canonicalAsset?.relativePath || overwriteTarget?.descriptor.fsPath,
3880
- caption: isMarkdownDocument ? canonicalName : persistedCaption,
3881
- displayName: isMarkdownDocument ? canonicalName : safeFilename,
3882
- originalFilename: safeFilename,
3883
- ...(asset?.assetId ? { assetId: asset.assetId } : {}),
3884
- timestamp: now
3885
- };
4725
+ const sourceReal = fs.realpathSync(sourceResolved);
4726
+ const projectRootReal = fs.realpathSync(projectRoot);
4727
+ if (!isApprovedAttachmentSourcePath(sourceReal, projectRootReal)) {
4728
+ throw new Error("sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root");
4729
+ }
4730
+ if (!fs.statSync(sourceReal).isFile()) {
4731
+ throw new Error(`Path is not a file: ${trimmedSource}`);
4732
+ }
4733
+ fs.copyFileSync(sourceReal, destinationPath);
4734
+ }
4735
+ const relativePath = overwriteTarget
4736
+ ? overwriteTarget.relativePath
4737
+ : toPosixPath(path.relative(projectRoot, destinationPath));
4738
+ if (relativePath.endsWith('.md')) {
4739
+ const fileContent = fs.readFileSync(destinationPath, 'utf8');
4740
+ const validation = validateStructuredDocForAttach(relativePath, fileContent);
4741
+ if (!validation.ok) {
4742
+ throw new Error(validation.message);
4743
+ }
4744
+ }
4745
+ const isMarkdownDocument = relativePath.toLowerCase().endsWith('.md') || relativePath.toLowerCase().endsWith('.markdown');
4746
+ const canonicalName = safeCaption || overwriteTarget?.asset.logicalName || safeFilename;
4747
+ const persistedCaption = isMarkdownDocument
4748
+ ? canonicalName
4749
+ : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
4750
+ asset = await canonicalAssetHelpers.ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
4751
+ originalFilename: safeFilename,
4752
+ logicalName: isMarkdownDocument ? canonicalName : safeFilename,
4753
+ ...(overwriteTarget
4754
+ ? {
4755
+ assetId: overwriteTarget.asset.assetId,
4756
+ documentId: overwriteTarget.asset.documentId,
4757
+ createdAt: overwriteTarget.asset.createdAt,
4758
+ createdByUserId: overwriteTarget.asset.createdByUserId,
4759
+ scanStatus: overwriteTarget.asset.scanStatus,
4760
+ scanEngine: overwriteTarget.asset.scanEngine,
4761
+ scanDetails: overwriteTarget.asset.scanDetails,
4762
+ }
4763
+ : {
4764
+ createdByUserId: getActiveMcpActorRef(),
4765
+ })
4766
+ });
4767
+ attachment = buildAttachmentRecord(asset, canonicalAsset?.relativePath || overwriteTarget?.descriptor.fsPath || relativePath);
4768
+ storageKeyForAudit = String(attachment.fsPath || '');
4769
+ }
3886
4770
  const existing = Array.isArray(task.attachments)
3887
4771
  ? task.attachments
3888
4772
  : [];
@@ -3892,11 +4776,22 @@ export function createTaskforceMcpServer(options = {}) {
3892
4776
  : item));
3893
4777
  const updated = core.updateTask(id, {
3894
4778
  attachments: updatedAttachments
3895
- }, ACTIVE_WORKSPACE_ID);
4779
+ }, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
3896
4780
  if (!updated) {
3897
4781
  throw new Error(`Task ${id} not found`);
3898
4782
  }
3899
- recordMcpWriteAudit('save_task_attachment', { taskId: id, path: attachment.fsPath, overwrite: true, attachmentId: asset?.assetId || overwriteTarget.descriptor.attachmentId });
4783
+ if (replacedStorageKey) {
4784
+ if (overwriteTarget.asset.storageProvider === 'r2' && objectStorageClient) {
4785
+ await objectStorageClient.deleteObject(replacedStorageKey).catch(() => { });
4786
+ }
4787
+ else if (overwriteTarget.asset.storageProvider === 'local') {
4788
+ const replacedAbsolutePath = path.resolve(basePath, replacedStorageKey);
4789
+ if (replacedAbsolutePath.startsWith(path.resolve(basePath)) && fs.existsSync(replacedAbsolutePath)) {
4790
+ fs.rmSync(replacedAbsolutePath, { force: true });
4791
+ }
4792
+ }
4793
+ }
4794
+ recordMcpWriteAudit('save_task_attachment', { taskId: id, path: storageKeyForAudit, overwrite: true, attachmentId: asset?.assetId || overwriteTarget.descriptor.attachmentId });
3900
4795
  await notifyResourceMutation({
3901
4796
  taskIds: [id],
3902
4797
  includeTasks: true,
@@ -3924,11 +4819,11 @@ export function createTaskforceMcpServer(options = {}) {
3924
4819
  }
3925
4820
  const updated = core.updateTask(id, {
3926
4821
  attachments: [...existing, attachment]
3927
- }, ACTIVE_WORKSPACE_ID);
4822
+ }, ACTIVE_WORKSPACE_ID, { actorRef: getActiveMcpActorRef() });
3928
4823
  if (!updated) {
3929
4824
  throw new Error(`Task ${id} not found`);
3930
4825
  }
3931
- recordMcpWriteAudit('save_task_attachment', { taskId: id, path: attachment.fsPath });
4826
+ recordMcpWriteAudit('save_task_attachment', { taskId: id, path: storageKeyForAudit });
3932
4827
  await notifyResourceMutation({
3933
4828
  taskIds: [id],
3934
4829
  includeTasks: true,
@@ -3937,6 +4832,12 @@ export function createTaskforceMcpServer(options = {}) {
3937
4832
  content: [{ type: "text", text: `Task attachment saved and attached to ${formatTaskIdentifier(task)}\n${JSON.stringify(attachment, null, 2)}` }],
3938
4833
  };
3939
4834
  }
4835
+ case "save_workstream_attachment": {
4836
+ return savePlanningAttachment('workstream', args);
4837
+ }
4838
+ case "save_initiative_attachment": {
4839
+ return savePlanningAttachment('initiative', args);
4840
+ }
3940
4841
  case "delete_task_attachment": {
3941
4842
  const { taskId: inputTaskId, attachmentId } = args;
3942
4843
  if (!attachmentId || typeof attachmentId !== 'string')
@@ -3996,6 +4897,12 @@ export function createTaskforceMcpServer(options = {}) {
3996
4897
  }],
3997
4898
  };
3998
4899
  }
4900
+ case "delete_workstream_attachment": {
4901
+ return deletePlanningAttachment('workstream', args);
4902
+ }
4903
+ case "delete_initiative_attachment": {
4904
+ return deletePlanningAttachment('initiative', args);
4905
+ }
3999
4906
  case "list_archive": {
4000
4907
  const { format = "markdown" } = args;
4001
4908
  const archive = core.listArchive(ACTIVE_WORKSPACE_ID);
@@ -4247,7 +5154,7 @@ export function createTaskforceMcpServer(options = {}) {
4247
5154
  if (!handler) {
4248
5155
  throw new Error(`Unknown tool: ${name}`);
4249
5156
  }
4250
- return await handler((args || {}));
5157
+ return await executeMcpToolWithReadRetry(name, (args || {}), handler);
4251
5158
  }
4252
5159
  catch (error) {
4253
5160
  const recoverable = buildRecoverableToolErrorResult(error, auditRequestId);
@@ -4274,7 +5181,14 @@ export function createTaskforceMcpServer(options = {}) {
4274
5181
  };
4275
5182
  }
4276
5183
  export async function startTaskforceMcpStdioServer(options = {}) {
4277
- const runtime = createTaskforceMcpServer(options);
5184
+ const envAuth = buildStdioAuthFromEnv();
5185
+ const runtime = createTaskforceMcpServer({
5186
+ ...options,
5187
+ auth: {
5188
+ ...envAuth,
5189
+ ...(options.auth || {}),
5190
+ },
5191
+ });
4278
5192
  const transport = new StdioServerTransport();
4279
5193
  await runtime.server.connect(transport);
4280
5194
  return { ...runtime, transport };