@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.
- package/README.md +0 -2
- package/dist/Taskforce.module.css +603 -107
- package/dist/TaskforceCore.js +216 -43
- package/dist/TaskforceCore.test.js +611 -28
- package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/assets/fonts/Noir_medium.woff2 +0 -0
- package/dist/assets/fonts/Noir_regular.woff2 +0 -0
- package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/compat/workspaceSyncCompat.js +46 -2
- package/dist/compat/workspaceSyncCompat.test.js +38 -1
- package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
- package/dist/components/activity/EntityActivityTimeline.js +81 -0
- package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
- package/dist/components/context/ContextAttachmentManager.js +561 -0
- package/dist/components/features/AgentsModule.d.ts +14 -1
- package/dist/components/features/AgentsModule.js +366 -18
- package/dist/components/features/AgentsModule.test.js +507 -8
- package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
- package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
- package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
- package/dist/components/features/DocumentIndex.d.ts +2 -2
- package/dist/components/features/DocumentIndex.js +3 -4
- package/dist/components/features/DocumentViewer.d.ts +7 -1
- package/dist/components/features/DocumentViewer.js +62 -4
- package/dist/components/features/DocumentWorkspace.d.ts +10 -2
- package/dist/components/features/DocumentWorkspace.js +15 -4
- package/dist/components/features/DocumentWorkspace.test.js +51 -1
- package/dist/components/features/TaskSettings.d.ts +2 -0
- package/dist/components/features/TaskSettings.js +83 -78
- package/dist/components/features/TaskSettings.test.js +171 -82
- package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
- package/dist/components/task/TaskActionHeader.js +11 -5
- package/dist/components/task/TaskActionHeader.test.js +10 -0
- package/dist/components/task/TaskCard.js +24 -29
- package/dist/components/task/TaskCard.test.js +38 -3
- package/dist/components/task/TaskContextUpload.js +4 -526
- package/dist/components/task/TaskContextUpload.test.js +3 -3
- package/dist/components/task/TaskForm.js +61 -116
- package/dist/components/task/TaskForm.test.js +110 -8
- package/dist/components/task/TaskKanban.d.ts +2 -1
- package/dist/components/task/TaskKanban.js +94 -21
- package/dist/components/task/TaskKanban.test.js +36 -0
- package/dist/components/task/TaskList.js +5 -1
- package/dist/components/task/TaskList.test.js +4 -0
- package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
- package/dist/components/ui/EditableAvatarButton.js +18 -0
- package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
- package/dist/components/ui/HierarchyActionBadge.js +5 -0
- package/dist/components/views/AppShellHeader.d.ts +3 -1
- package/dist/components/views/AppShellHeader.js +27 -11
- package/dist/components/views/AppShellHeader.test.js +18 -1
- package/dist/components/views/PlanComparisonPage.d.ts +9 -1
- package/dist/components/views/PlanComparisonPage.js +78 -14
- package/dist/components/views/PlanComparisonPage.test.js +257 -0
- package/dist/components/views/PlansPage.js +127 -36
- package/dist/components/views/PlansPage.test.js +294 -12
- package/dist/components/views/StandaloneLayout.js +529 -592
- package/dist/components/views/WidgetView.js +10 -8
- package/dist/components/views/panels/PlanningDrawer.d.ts +25 -5
- package/dist/components/views/panels/PlanningDrawer.js +270 -55
- package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
- package/dist/components/views/panels/PlanningDrawer.test.js +529 -0
- package/dist/components/views/planningScope.d.ts +20 -0
- package/dist/components/views/planningScope.js +25 -0
- package/dist/components/views/planningScope.test.d.ts +1 -0
- package/dist/components/views/planningScope.test.js +60 -0
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
- package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
- package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
- package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
- package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
- package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
- package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
- package/dist/config/envSchema.js +1 -0
- package/dist/core/AiProfileService.d.ts +46 -2
- package/dist/core/AiProfileService.js +1073 -82
- package/dist/core/AiProfileService.test.js +19 -1
- package/dist/core/AiProfiles.test.js +1249 -44
- package/dist/core/AttachmentLinkService.d.ts +3 -0
- package/dist/core/AttachmentLinkService.js +110 -23
- package/dist/core/AuthIdentityService.d.ts +107 -0
- package/dist/core/AuthIdentityService.js +284 -0
- package/dist/core/AuthTokenService.d.ts +4 -0
- package/dist/core/AuthTokenService.js +34 -6
- package/dist/core/AuthTokenService.test.js +3 -3
- package/dist/core/EntitlementsPolicy.test.js +2 -2
- package/dist/core/GlobalSettingsService.js +127 -6
- package/dist/core/McpTokenService.d.ts +12 -0
- package/dist/core/McpTokenService.js +44 -1
- package/dist/core/PlanEntitlementService.d.ts +23 -2
- package/dist/core/PlanEntitlementService.js +343 -57
- package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
- package/dist/core/PlanFeatureCatalog.test.js +84 -0
- package/dist/core/PlanVersionPolicy.test.js +35 -2
- package/dist/core/PlanningEntities.test.js +418 -4
- package/dist/core/SignupMonetizationSettings.test.js +199 -27
- package/dist/core/SyncReconciliationService.js +2 -65
- package/dist/core/SystemAdmin.test.js +42 -42
- package/dist/core/TaskActivityService.d.ts +28 -1
- package/dist/core/TaskActivityService.js +227 -46
- package/dist/core/TaskActivitySync.test.d.ts +1 -0
- package/dist/core/TaskActivitySync.test.js +90 -0
- package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
- package/dist/core/TaskAuditTimeline.test.js +96 -0
- package/dist/core/Taskforce.d.ts +415 -40
- package/dist/core/Taskforce.ids.test.js +58 -0
- package/dist/core/Taskforce.js +1543 -266
- package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
- package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
- package/dist/core/Taskforce.mcpAuth.test.js +109 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
- package/dist/core/WorkspaceLifecycleService.js +0 -5
- package/dist/core/WorkspacePermissions.test.js +25 -1
- package/dist/core/WorkspacePlanMode.test.js +117 -16
- package/dist/core/shared.d.ts +1 -1
- package/dist/core/shared.js +5 -1
- package/dist/core/types.d.ts +75 -2
- package/dist/hooks/auth/useTaskforceAuthBootstrap.js +28 -2
- package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
- package/dist/hooks/bootstrapTimeouts.js +8 -0
- package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
- package/dist/hooks/bootstrapTimeouts.test.js +12 -0
- package/dist/hooks/sync/bootstrap.js +2 -0
- package/dist/hooks/sync/controlPlane.d.ts +37 -0
- package/dist/hooks/sync/controlPlane.js +78 -0
- package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
- package/dist/hooks/sync/controlPlane.test.js +121 -0
- package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
- package/dist/hooks/sync/lifecyclePolicy.js +93 -0
- package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
- package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
- package/dist/hooks/sync/orchestratorShared.d.ts +13 -4
- package/dist/hooks/sync/orchestratorShared.js +129 -48
- package/dist/hooks/sync/orchestratorShared.test.js +50 -1
- package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
- package/dist/hooks/sync/pullLifecycle.js +24 -0
- package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
- package/dist/hooks/sync/pullLifecycle.test.js +80 -0
- package/dist/hooks/sync/recovery.d.ts +16 -2
- package/dist/hooks/sync/recovery.js +179 -53
- package/dist/hooks/sync/recovery.test.d.ts +1 -0
- package/dist/hooks/sync/recovery.test.js +292 -0
- package/dist/hooks/sync/transfers.d.ts +16 -3
- package/dist/hooks/sync/transfers.js +210 -66
- package/dist/hooks/sync/transfers.test.d.ts +1 -0
- package/dist/hooks/sync/transfers.test.js +928 -0
- package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
- package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
- package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
- package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
- package/dist/hooks/sync/useSyncStatusActions.js +88 -0
- package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
- package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
- package/dist/hooks/sync/useSyncStatusControls.js +60 -0
- package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
- package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
- package/dist/hooks/useFilterSort.js +13 -13
- package/dist/hooks/useFilterSort.test.js +87 -1
- package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
- package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
- package/dist/hooks/useSyncOrchestrator.js +409 -183
- package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
- package/dist/hooks/useTaskData.js +8 -3
- package/dist/hooks/useTaskData.test.js +45 -0
- package/dist/hooks/useTaskforce.d.ts +145 -52
- package/dist/hooks/useTaskforce.js +592 -253
- package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
- package/dist/hooks/useTaskforce.runtime-routing.test.js +246 -0
- package/dist/hooks/useTaskforce.sync-behavior.test.js +2468 -201
- package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
- package/dist/hooks/useWorkspaceSyncController.js +13 -2
- package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
- package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +328 -40
- package/dist/localization/locales/en-US.d.ts +0 -1
- package/dist/localization/locales/en-US.js +0 -1
- package/dist/localization/locales/es-419.js +0 -1
- package/dist/localization/locales/pt-BR.js +0 -1
- package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
- package/dist/mcp/canonicalAssetHelpers.js +18 -10
- package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
- package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
- package/dist/mcp/clientIntegrations.d.ts +1 -0
- package/dist/mcp/clientIntegrations.js +25 -0
- package/dist/mcp/clientIntegrations.test.js +14 -3
- package/dist/mcp/clientProfileDefaults.js +3 -3
- package/dist/mcp/collaborationRegistrar.js +7 -7
- package/dist/mcp/documentAssetRegistrar.js +116 -10
- package/dist/mcp/runtime.d.ts +3 -0
- package/dist/mcp/runtime.js +1099 -185
- package/dist/mcp/runtime.test.js +1491 -140
- package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
- package/dist/mcp/taskPlanningRegistrar.js +48 -0
- package/dist/mcp/toolRegistry.d.ts +56 -0
- package/dist/mcp/toolRegistry.js +8 -0
- package/dist/mcp/toolRegistry.test.js +4 -0
- package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
- package/dist/migrations/taskSchemaMigrations.js +192 -61
- package/dist/migrations/taskSchemaMigrations.test.js +15 -0
- package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/dist/resources/templates/workflows/collaborate.yaml +1 -1
- package/dist/resources/templates/workflows/evaluate.yaml +2 -2
- package/dist/resources/templates/workflows/plan.yaml +1 -1
- package/dist/resources/templates/workflows/review.yaml +2 -2
- package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
- package/dist/server/aiProfileAvatarAssets.js +319 -0
- package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
- package/dist/server/aiProfileAvatarAssets.test.js +213 -0
- package/dist/server/annotatedAttachmentsRoutes.test.js +69 -10
- package/dist/server/auth/providers/apple.d.ts +33 -0
- package/dist/server/auth/providers/apple.js +82 -0
- package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
- package/dist/server/auth/providers/appleClientSecret.js +65 -0
- package/dist/server/auth/providers/github.d.ts +26 -0
- package/dist/server/auth/providers/github.js +86 -0
- package/dist/server/auth/providers/google.d.ts +21 -0
- package/dist/server/auth/providers/google.js +56 -0
- package/dist/server/auth/providers/types.d.ts +21 -0
- package/dist/server/auth/providers/types.js +1 -0
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.js +6 -3
- package/dist/server/documentReviewRoutes.test.js +36 -3
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.js +124 -9
- package/dist/server/index.rateLimit.test.js +3 -0
- package/dist/server/index.test.js +111 -3
- package/dist/server/routes/admin.d.ts +3 -0
- package/dist/server/routes/admin.js +459 -28
- package/dist/server/routes/annotatedAttachments.js +51 -0
- package/dist/server/routes/auth.d.ts +26 -2
- package/dist/server/routes/auth.js +1170 -71
- package/dist/server/routes/authSupport.d.ts +5 -1
- package/dist/server/routes/authSupport.js +13 -2
- package/dist/server/routes/billing.js +372 -35
- package/dist/server/routes/billing.test.js +595 -12
- package/dist/server/routes/documents.d.ts +1 -0
- package/dist/server/routes/documents.js +182 -70
- package/dist/server/routes/shared.d.ts +28 -2
- package/dist/server/routes/shared.js +69 -14
- package/dist/server/routes/sync.integration.test.js +658 -39
- package/dist/server/routes/sync.js +74 -608
- package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
- package/dist/server/routes/syncAuxRoutes.js +91 -0
- package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
- package/dist/server/routes/syncAuxRoutes.test.js +158 -0
- package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
- package/dist/server/routes/syncPullApplyRoutes.js +204 -0
- package/dist/server/routes/syncPushRoutes.d.ts +24 -0
- package/dist/server/routes/syncPushRoutes.js +212 -0
- package/dist/server/routes/syncRouteGuards.d.ts +36 -0
- package/dist/server/routes/syncRouteGuards.js +67 -0
- package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
- package/dist/server/routes/syncRouteGuards.test.js +94 -0
- package/dist/server/routes/syncRouteTypes.d.ts +13 -0
- package/dist/server/routes/syncRouteTypes.js +1 -0
- package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
- package/dist/server/routes/syncSnapshotRoutes.js +182 -0
- package/dist/server/routes/tasks.js +55 -10
- package/dist/server/routes/workspaces.js +36 -5
- package/dist/server/routes.js +147 -11
- package/dist/server/routes.test.js +2704 -183
- package/dist/services/workflowExportSnapshots.test.js +2 -0
- package/dist/shared/aiProfileSeatScope.d.ts +5 -0
- package/dist/shared/aiProfileSeatScope.js +21 -0
- package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
- package/dist/shared/aiProfileSurfaceType.js +10 -8
- package/dist/shared/passwordPolicy.d.ts +13 -0
- package/dist/shared/passwordPolicy.js +84 -0
- package/dist/shared/passwordPolicy.test.d.ts +1 -0
- package/dist/shared/passwordPolicy.test.js +34 -0
- package/dist/storage/objectStorage.d.ts +1 -0
- package/dist/storage/objectStorage.js +2 -1
- package/dist/storage/objectStorageClient.d.ts +1 -0
- package/dist/storage/objectStorageClient.js +41 -8
- package/dist/storage/objectStorageClient.test.js +26 -1
- package/dist/storage/objectStorageConfig.js +3 -1
- package/dist/storage/objectStorageConfig.test.js +13 -1
- package/dist/storage/workspaceAssetStore.d.ts +15 -3
- package/dist/storage/workspaceAssetStore.js +132 -44
- package/dist/storage/workspaceAssetStore.test.js +93 -6
- package/dist/styles/fonts.css +70 -0
- package/dist/sync/collaborationSyncPayload.d.ts +6 -1
- package/dist/sync/collaborationSyncPayload.js +19 -11
- package/dist/sync/collaborationSyncPayload.test.js +14 -1
- package/dist/sync/collaborationSyncState.d.ts +3 -3
- package/dist/sync/collaborationSyncState.js +19 -12
- package/dist/sync/contentSyncPayload.d.ts +5 -1
- package/dist/sync/contentSyncPayload.js +9 -0
- package/dist/sync/contentSyncPayload.test.js +39 -0
- package/dist/sync/contentSyncState.d.ts +9 -9
- package/dist/sync/contentSyncState.js +248 -107
- package/dist/sync/planningSyncPayload.js +5 -0
- package/dist/sync/planningSyncPayload.test.js +24 -0
- package/dist/sync/syncApplyHandlers.d.ts +11 -4
- package/dist/sync/syncApplyHandlers.js +115 -42
- package/dist/sync/syncApplyHandlers.test.js +251 -4
- package/dist/sync/syncFieldSemantics.d.ts +12 -0
- package/dist/sync/syncFieldSemantics.js +55 -0
- package/dist/sync/syncResourceAdapters.d.ts +46 -0
- package/dist/sync/syncResourceAdapters.js +79 -0
- package/dist/sync/syncService.d.ts +6 -0
- package/dist/sync/syncService.js +65 -1
- package/dist/sync/syncService.test.js +96 -3
- package/dist/sync/taskSyncChangeKey.d.ts +2 -0
- package/dist/sync/taskSyncChangeKey.js +143 -0
- package/dist/sync/taskSyncPayload.js +3 -8
- package/dist/sync/workspacePullFeed.d.ts +4 -0
- package/dist/sync/workspacePullFeed.js +132 -72
- package/dist/sync/workspacePullFeed.test.js +92 -0
- package/dist/sync/workspaceRepair.js +5 -2
- package/dist/sync/workspaceSyncModel.d.ts +73 -6
- package/dist/sync/workspaceSyncModel.js +342 -73
- package/dist/sync/workspaceSyncModel.test.js +559 -14
- package/dist/sync/workspaceSyncState.d.ts +17 -0
- package/dist/sync/workspaceSyncState.js +2 -1
- package/dist/sync/workspaceSyncSurface.js +3 -8
- package/dist/types.d.ts +50 -2
- package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
- package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
- package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
- package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
- package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
- package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
- package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
- package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
- package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-D4XseTwW.js} +1 -1
- package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
- package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
- package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
- package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
- package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
- package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
- package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
- package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
- package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-oOA9bcdK.js} +4 -4
- package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
- package/dist/ui/assets/index-607WPo_X.js +4 -0
- package/dist/ui/assets/index-CYyodXsg.css +1 -0
- package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-B32hGuF2.js} +1 -1
- package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
- package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
- package/dist/ui/fonts/Noir_medium.otf +0 -0
- package/dist/ui/fonts/Noir_medium.woff2 +0 -0
- package/dist/ui/fonts/Noir_regular.otf +0 -0
- package/dist/ui/fonts/Noir_regular.woff2 +0 -0
- package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
- package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/product/agent manager.png +0 -0
- package/dist/ui/product/agent_manager_02.png +0 -0
- package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
- package/dist/utils/aiProfileEvents.d.ts +7 -0
- package/dist/utils/aiProfileEvents.js +6 -0
- package/dist/utils/assignees.d.ts +8 -0
- package/dist/utils/assignees.js +10 -1
- package/dist/utils/assignees.test.d.ts +1 -0
- package/dist/utils/assignees.test.js +25 -0
- package/dist/utils/avatarDisplay.d.ts +1 -0
- package/dist/utils/avatarDisplay.js +15 -0
- package/dist/utils/avatarUpload.d.ts +27 -0
- package/dist/utils/avatarUpload.js +132 -0
- package/dist/utils/avatarUpload.test.d.ts +1 -0
- package/dist/utils/avatarUpload.test.js +40 -0
- package/dist/utils/bootstrapTrace.d.ts +7 -0
- package/dist/utils/bootstrapTrace.js +29 -0
- package/dist/utils/commercialLifecyclePresentation.js +3 -0
- package/dist/utils/contextAssetEvents.d.ts +2 -0
- package/dist/utils/contextFiles.d.ts +3 -1
- package/dist/utils/contextFiles.js +2 -1
- package/dist/utils/syncEventDetails.d.ts +3 -0
- package/dist/utils/syncEventDetails.js +64 -0
- package/dist/utils/syncEventDetails.test.d.ts +1 -0
- package/dist/utils/syncEventDetails.test.js +23 -0
- package/dist/utils/syncEventPresentation.d.ts +15 -0
- package/dist/utils/syncEventPresentation.js +129 -0
- package/dist/utils/syncEventPresentation.test.d.ts +1 -0
- package/dist/utils/syncEventPresentation.test.js +64 -0
- package/dist/utils/syncStatusPresentation.d.ts +72 -0
- package/dist/utils/syncStatusPresentation.js +217 -0
- package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
- package/dist/utils/syncStatusPresentation.test.js +164 -0
- package/dist/utils/taskActivity.d.ts +9 -1
- package/dist/utils/taskActivity.js +290 -22
- package/dist/utils/taskActivity.test.js +141 -8
- package/dist/utils/taskSearch.d.ts +22 -0
- package/dist/utils/taskSearch.js +87 -0
- package/dist/utils/taskSearch.test.d.ts +1 -0
- package/dist/utils/taskSearch.test.js +69 -0
- package/dist/utils/taskUtils.js +10 -3
- package/dist/utils/taskUtils.test.d.ts +1 -0
- package/dist/utils/taskUtils.test.js +19 -0
- package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
- package/dist/utils/workspaceSyncPresentation.js +68 -2
- package/dist/utils/workspaceSyncPresentation.test.js +36 -1
- package/package.json +3 -2
- package/scripts/export-sqlite-to-postgres.mjs +7 -9
- package/scripts/run-e2e-realtime.sh +17 -3
- package/scripts/run-smoke.sh +17 -5
- package/scripts/run-soak.sh +17 -5
- package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
- package/src/resources/templates/workflows/collaborate.yaml +1 -1
- package/src/resources/templates/workflows/evaluate.yaml +2 -2
- package/src/resources/templates/workflows/plan.yaml +1 -1
- package/src/resources/templates/workflows/review.yaml +2 -2
- package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BG6P_GVW.js +0 -3
- package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
- package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
- package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
- package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
- package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
- package/dist/ui/assets/TaskSettings-ln0aF7xc.js +0 -9
- package/dist/ui/assets/index-C2-OXZV7.css +0 -1
- package/dist/ui/assets/index-DcSI5F86.js +0 -6
- package/scripts/migrate-planning-model.ts +0 -233
package/dist/mcp/runtime.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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
|
-
/^
|
|
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
|
|
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 =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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,
|
|
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 === '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
3079
|
+
const taskId = link.taskId;
|
|
3080
|
+
if (entries.some((entry) => entry.taskId === taskId))
|
|
2435
3081
|
return entries;
|
|
2436
|
-
const task = core.resolveTaskIdentifier(
|
|
3082
|
+
const task = core.resolveTaskIdentifier(taskId, ACTIVE_WORKSPACE_ID);
|
|
2437
3083
|
const taskReferenceLabel = task?.referenceLabel || (task ? getTaskReferenceLabel(task) : null);
|
|
2438
|
-
const sessions = (linkedTaskSessions.get(
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
const board = core.getScheduleBoard(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
3802
|
-
|
|
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
|
|
3814
|
-
|
|
3815
|
-
|
|
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,
|
|
4506
|
+
kind: inferAssetKind(safeFilename, safeMimeType),
|
|
3818
4507
|
filename: safeFilename
|
|
3819
4508
|
});
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
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
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
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
|
-
|
|
3836
|
-
|
|
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
|
-
|
|
3841
|
-
|
|
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
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
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
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
?
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
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
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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 };
|