@taskforcehq/taskforce 0.3.327 → 0.3.328

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/Taskforce.module.css +24 -53
  2. package/dist/components/activity/EntityActivityTimeline.js +15 -5
  3. package/dist/components/context/AttachmentPreviewHost.js +1 -1
  4. package/dist/components/context/ContextAttachmentManager.d.ts +3 -0
  5. package/dist/components/context/ContextAttachmentManager.js +116 -20
  6. package/dist/components/features/planning/PlanningEntitySummary.d.ts +3 -1
  7. package/dist/components/features/planning/PlanningEntitySummary.js +3 -2
  8. package/dist/components/features/planning/PlanningLinkPicker.d.ts +3 -1
  9. package/dist/components/features/planning/PlanningLinkPicker.js +5 -3
  10. package/dist/components/features/planning/PlanningModule.js +26 -9
  11. package/dist/components/features/planning/PlanningVisualIdentity.d.ts +14 -0
  12. package/dist/components/features/planning/PlanningVisualIdentity.js +106 -0
  13. package/dist/components/features/planning/planningWorkspaceModel.d.ts +11 -0
  14. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  15. package/dist/components/task/TaskActionHeader.js +2 -2
  16. package/dist/components/task/TaskCard.js +37 -4
  17. package/dist/components/task/TaskContextUpload.d.ts +3 -0
  18. package/dist/components/task/TaskContextUpload.js +2 -2
  19. package/dist/components/task/TaskForm.d.ts +4 -0
  20. package/dist/components/task/TaskForm.js +32 -7
  21. package/dist/components/task/TaskImageReviews.js +16 -16
  22. package/dist/components/task/TaskStatusActions.js +11 -2
  23. package/dist/components/task/TaskWorkstreamPicker.js +2 -1
  24. package/dist/components/ui/DocumentTypeIcon.d.ts +7 -0
  25. package/dist/components/ui/DocumentTypeIcon.js +8 -0
  26. package/dist/components/ui/TaxonomyDropdown.d.ts +9 -1
  27. package/dist/components/ui/TaxonomyDropdown.js +30 -3
  28. package/dist/components/views/StandaloneLayout.js +62 -9
  29. package/dist/components/views/WidgetView.js +8 -5
  30. package/dist/components/views/panels/PlanningDrawer.d.ts +7 -1
  31. package/dist/components/views/panels/PlanningDrawer.js +48 -44
  32. package/dist/config/envSchema.js +1 -0
  33. package/dist/config/localServiceCloudTarget.d.ts +10 -0
  34. package/dist/config/localServiceCloudTarget.js +76 -0
  35. package/dist/core/AiProfileService.d.ts +1 -0
  36. package/dist/core/AiProfileService.js +2 -1
  37. package/dist/core/AttachmentLinkService.js +7 -4
  38. package/dist/core/DeletedTaskLifecycleService.d.ts +4 -0
  39. package/dist/core/DeletedTaskLifecycleService.js +21 -0
  40. package/dist/core/McpTokenService.d.ts +4 -0
  41. package/dist/core/McpTokenService.js +52 -0
  42. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +5 -0
  43. package/dist/core/TaskAttachmentLinksDurableMutationService.js +20 -11
  44. package/dist/core/Taskforce.d.ts +63 -1
  45. package/dist/core/Taskforce.js +558 -73
  46. package/dist/core/types.d.ts +2 -1
  47. package/dist/documentReviews/DocumentReviewCommentStore.d.ts +11 -0
  48. package/dist/documentReviews/DocumentReviewCommentStore.js +35 -2
  49. package/dist/documentReviews/service.d.ts +4 -0
  50. package/dist/documentReviews/service.js +141 -46
  51. package/dist/hooks/tasks/useTaskFormActions.d.ts +3 -0
  52. package/dist/hooks/tasks/useTaskFormActions.js +33 -7
  53. package/dist/hooks/useTaskforce.d.ts +59 -50
  54. package/dist/hooks/useTaskforce.js +103 -1
  55. package/dist/mcp/canonicalAssetHelpers.d.ts +13 -0
  56. package/dist/mcp/canonicalAssetHelpers.js +39 -5
  57. package/dist/mcp/collaborationRegistrar.js +15 -13
  58. package/dist/mcp/documentAssetRegistrar.js +16 -9
  59. package/dist/mcp/localServiceProxy.js +4 -1
  60. package/dist/mcp/runtime.d.ts +16 -0
  61. package/dist/mcp/runtime.js +566 -171
  62. package/dist/mcp/taskAttachmentCommandAdapter.d.ts +2 -0
  63. package/dist/mcp/taskAttachmentCommandAdapter.js +14 -8
  64. package/dist/mcp/taskPlanningRegistrar.js +42 -34
  65. package/dist/mcp/toolRegistry.js +2 -2
  66. package/dist/migrations/taskSchemaMigrations.js +35 -0
  67. package/dist/runtime/appGateDefinitions.d.ts +2 -2
  68. package/dist/runtime/appGateDefinitions.js +2 -2
  69. package/dist/server/appAccess.d.ts +12 -0
  70. package/dist/server/appAccess.js +21 -0
  71. package/dist/server/authorizedWorkspaceAccess.d.ts +12 -0
  72. package/dist/server/authorizedWorkspaceAccess.js +40 -0
  73. package/dist/server/cloudCollectionRead.d.ts +25 -0
  74. package/dist/server/cloudCollectionRead.js +120 -0
  75. package/dist/server/cloudCollectionReadPool.d.ts +52 -0
  76. package/dist/server/cloudCollectionReadPool.js +140 -0
  77. package/dist/server/cloudCollectionReadWorker.d.ts +1 -0
  78. package/dist/server/cloudCollectionReadWorker.js +36 -0
  79. package/dist/server/index.d.ts +3 -11
  80. package/dist/server/index.js +53 -40
  81. package/dist/server/localServiceLease.d.ts +3 -0
  82. package/dist/server/localServiceLease.js +9 -1
  83. package/dist/server/routes/documents.d.ts +5 -2
  84. package/dist/server/routes/documents.js +160 -13
  85. package/dist/server/routes/durableTaskHttpRouters.d.ts +2 -0
  86. package/dist/server/routes/durableTaskHttpRouters.js +2 -2
  87. package/dist/server/routes/localService.js +2 -0
  88. package/dist/server/routes/primitives.d.ts +2 -0
  89. package/dist/server/routes/shared.d.ts +24 -0
  90. package/dist/server/routes/shared.js +114 -18
  91. package/dist/server/routes/sync.js +22 -0
  92. package/dist/server/routes/syncV3FeedRoutes.js +4 -2
  93. package/dist/server/routes/syncV3LocalCaptureRoutes.js +20 -2
  94. package/dist/server/routes/syncV3OperationRoutes.js +17 -2
  95. package/dist/server/routes/tasks.js +199 -89
  96. package/dist/server/routes.js +35 -7
  97. package/dist/service/localServiceCli.js +36 -25
  98. package/dist/service/localServiceClientLifecycle.js +67 -6
  99. package/dist/shared/planningIdentity.d.ts +20 -0
  100. package/dist/shared/planningIdentity.js +79 -0
  101. package/dist/storage/postgresAdapter.js +4 -0
  102. package/dist/storage/postgresRequestDiagnostics.d.ts +8 -0
  103. package/dist/storage/postgresRequestDiagnostics.js +24 -0
  104. package/dist/storage/workspaceAssetStore.d.ts +32 -0
  105. package/dist/storage/workspaceAssetStore.js +70 -0
  106. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +16 -0
  107. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +70 -22
  108. package/dist/sync/coordinator/workspaceSyncTransportGateway.js +4 -2
  109. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +5 -0
  110. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.d.ts +1 -1
  111. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +15 -0
  112. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +26 -3
  113. package/dist/sync/engine/workspaceSyncV2ChangeDispatcher.js +2 -0
  114. package/dist/sync/syncApplyHandlers.d.ts +3 -1
  115. package/dist/sync/syncApplyHandlers.js +29 -0
  116. package/dist/sync/syncService.js +9 -0
  117. package/dist/sync/taskSyncPayload.d.ts +1 -0
  118. package/dist/sync/taskSyncPayload.js +6 -0
  119. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +1 -0
  120. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +21 -3
  121. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +17 -4
  122. package/dist/sync/v3/localInitiativeOutboxService.js +4 -2
  123. package/dist/sync/v3/localOutboxDispatcher.js +5 -1
  124. package/dist/sync/v3/localTaskAttachmentLinksOutboxHandler.js +12 -2
  125. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +1 -0
  126. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +15 -3
  127. package/dist/sync/v3/localWorkstreamOutboxService.js +4 -2
  128. package/dist/sync/v3/syncDurabilityStore.d.ts +8 -0
  129. package/dist/sync/v3/syncDurabilityStore.js +171 -33
  130. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +1 -0
  131. package/dist/sync/v3/taskAttachmentLinksMutationService.js +18 -2
  132. package/dist/sync/v3/taskMetadataCoexistence.js +4 -0
  133. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +1 -9
  134. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +1 -0
  135. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +7 -1
  136. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +15 -3
  137. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +4 -0
  138. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +29 -6
  139. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +1 -0
  140. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +29 -7
  141. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +62 -7
  142. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +1 -0
  143. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.d.ts +2 -0
  144. package/dist/sync/v3/workspaceSyncV3RepairSqlBatching.js +10 -0
  145. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.d.ts +1 -1
  146. package/dist/sync/v3/workspaceSyncV3TaskRootProjection.js +1 -1
  147. package/dist/sync/workspacePullFeed.d.ts +6 -1
  148. package/dist/sync/workspacePullFeed.js +172 -161
  149. package/dist/sync/workspaceSyncModel.d.ts +11 -0
  150. package/dist/sync/workspaceSyncModel.js +27 -2
  151. package/dist/types.d.ts +8 -0
  152. package/dist/ui/assets/{AiIdentityRosterCard-DB1BLO_j.js → AiIdentityRosterCard-MZs7lVED.js} +1 -1
  153. package/dist/ui/assets/{AiProfilesModule-DPxQeF9N.js → AiProfilesModule-C3i0LOxe.js} +1 -1
  154. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-DHNYaNg6.js → AnnotatedAttachmentWorkspace-DVZk5Tln.js} +1 -1
  155. package/dist/ui/assets/AssetTraySortControl-Kf1pelSy.js +1 -0
  156. package/dist/ui/assets/ContextAttachmentManager-B8boOs6Q.js +3 -0
  157. package/dist/ui/assets/{DocumentWorkspace-BC7sy5TG.js → DocumentWorkspace-DXH4XUsB.js} +4 -4
  158. package/dist/ui/assets/EntityActivityTimeline-BtSTq0ON.js +1 -0
  159. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +1 -0
  160. package/dist/ui/assets/PlanningModule-CoIR9hbV.css +1 -0
  161. package/dist/ui/assets/{PlansPage-BVH_NfgT.js → PlansPage-ChqgxALc.js} +1 -1
  162. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +1 -0
  163. package/dist/ui/assets/TaskContextUpload-qmBeUw3u.css +1 -0
  164. package/dist/ui/assets/{TaskSettings-xVsxSa0g.js → TaskSettings-g4ECKNOz.js} +1 -1
  165. package/dist/ui/assets/{TaskforceAgentsModule-CkAxW19K.js → TaskforceAgentsModule-BazdUwxZ.js} +1 -1
  166. package/dist/ui/assets/WorkflowManagerModule-DWFspC-o.js +1 -0
  167. package/dist/ui/assets/index-BUplSsv_.js +7 -0
  168. package/dist/ui/assets/index-gz2EXuoK.css +1 -0
  169. package/dist/ui/assets/{vendor-icons-B8ZNsH1g.js → vendor-icons-BkFLXavV.js} +1 -1
  170. package/dist/ui/index.html +3 -3
  171. package/dist/utils/constants.d.ts +2 -2
  172. package/dist/utils/constants.js +1 -1
  173. package/dist/utils/contextAttachmentUpload.d.ts +2 -0
  174. package/dist/utils/contextAttachmentUpload.js +7 -0
  175. package/dist/utils/contextFiles.d.ts +1 -1
  176. package/dist/utils/contextFiles.js +4 -4
  177. package/package.json +6 -1
  178. package/dist/ui/assets/AssetTraySortControl-DphbyHVc.js +0 -1
  179. package/dist/ui/assets/ContextAttachmentManager-CMxSzHqc.js +0 -3
  180. package/dist/ui/assets/EntityActivityTimeline-pJ2ZjZ-k.js +0 -1
  181. package/dist/ui/assets/PlanningModule-CeTqXMMz.css +0 -1
  182. package/dist/ui/assets/PlanningModule-t5GbvVR9.js +0 -1
  183. package/dist/ui/assets/TaskContextUpload-Ds4LnDzV.css +0 -1
  184. package/dist/ui/assets/TaskContextUpload-bl4fOOUz.js +0 -1
  185. package/dist/ui/assets/WorkflowManagerModule-BKqIGVnF.js +0 -1
  186. package/dist/ui/assets/index-BzJlVnNS.css +0 -1
  187. package/dist/ui/assets/index-Cx7vZJHL.js +0 -7
@@ -52,6 +52,7 @@ export interface SaveTaskAttachmentInput {
52
52
  sizeBytes?: unknown;
53
53
  expectedSizeBytes?: unknown;
54
54
  expectedSha256?: unknown;
55
+ displayName?: unknown;
55
56
  caption?: unknown;
56
57
  overwrite?: unknown;
57
58
  overwriteAssetId?: unknown;
@@ -69,6 +70,7 @@ export type SaveTaskAttachmentResult = {
69
70
  expiresAt: string;
70
71
  uploadUrl: string;
71
72
  uploadedPath: string;
73
+ displayName?: string;
72
74
  caption?: string;
73
75
  overwrite: boolean;
74
76
  expectedSizeBytes?: unknown;
@@ -20,7 +20,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
20
20
  return {
21
21
  buildAttachmentResponse,
22
22
  async save(input) {
23
- const { id: inputId, filename, content, sourcePath, uploadedPath, uploadDraftId, mimeType, sizeBytes, expectedSizeBytes, expectedSha256, caption, overwrite = false, overwriteAssetId, } = input;
23
+ const { id: inputId, filename, content, sourcePath, uploadedPath, uploadDraftId, mimeType, sizeBytes, expectedSizeBytes, expectedSha256, displayName, caption, overwrite = false, overwriteAssetId, } = input;
24
24
  const { id, task } = deps.resolveTask(inputId);
25
25
  const durableAttachmentLinksEnabled = deps.durableAttachmentLinksEnabled(id);
26
26
  if (!filename || typeof filename !== 'string')
@@ -38,6 +38,9 @@ export function createTaskAttachmentCommandAdapter(deps) {
38
38
  ? caption.trim()
39
39
  : undefined;
40
40
  const safeFilename = deps.sanitizeFilename(filename);
41
+ const safeDisplayName = typeof displayName === 'string' && displayName.trim().length > 0
42
+ ? displayName.trim()
43
+ : safeFilename;
41
44
  const stableAttachmentAssetId = deps.stableRequestId('operation', `task-attachment-asset:${id}:${safeFilename}`)?.replace(/^operation-/, 'asset-') || `asset-${randomUUID().replace(/-/g, '')}`;
42
45
  const safeMimeType = typeof mimeType === 'string' && mimeType.trim().length > 0
43
46
  ? mimeType.trim().toLowerCase()
@@ -53,7 +56,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
53
56
  const overwriteTitle = overwriteTarget
54
57
  ? deps.workspaceAssetStore?.getDocument?.(overwriteTarget.asset.assetId, deps.activeWorkspaceId)?.title
55
58
  : null;
56
- const canonicalName = safeCaption || overwriteTitle || safeFilename;
59
+ const canonicalName = safeCaption || overwriteTitle || overwriteTarget?.asset.logicalName || safeFilename;
57
60
  const persistedCaption = isMarkdownDocument
58
61
  ? canonicalName
59
62
  : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
@@ -63,7 +66,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
63
66
  : `/api/taskforce/context/${encodeURIComponent(storageKey)}`),
64
67
  fsPath: asset?.storageKey || overwriteTarget?.attachment.fsPath || storageKey,
65
68
  caption: isMarkdownDocument ? canonicalName : persistedCaption,
66
- displayName: isMarkdownDocument ? canonicalName : safeFilename,
69
+ displayName: safeDisplayName,
67
70
  originalFilename: safeFilename,
68
71
  ...(asset?.assetId ? { assetId: asset.assetId } : {}),
69
72
  timestamp: now,
@@ -110,6 +113,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
110
113
  expiresAt,
111
114
  uploadUrl: objectStorageClient.createSignedPutUrl(stagedAsset.relativePath, uploadExpiresInSeconds, safeMimeType),
112
115
  uploadedPath: stagedAsset.relativePath,
116
+ ...(safeDisplayName !== safeFilename ? { displayName: safeDisplayName } : {}),
113
117
  ...(safeCaption ? { caption: safeCaption } : {}),
114
118
  overwrite: overwrite === true,
115
119
  ...(expectedSizeBytes !== undefined ? { expectedSizeBytes } : {}),
@@ -218,7 +222,8 @@ export function createTaskAttachmentCommandAdapter(deps) {
218
222
  const overwriteTitle = overwriteTarget
219
223
  ? deps.workspaceAssetStore?.getDocument?.(overwriteTarget.asset.assetId, deps.activeWorkspaceId)?.title
220
224
  : null;
221
- const canonicalName = safeCaption || overwriteTitle || safeFilename;
225
+ const canonicalName = safeCaption || overwriteTitle || overwriteTarget?.asset.logicalName
226
+ || existingUploadedAsset?.logicalName || safeFilename;
222
227
  const persistedCaption = isMarkdownDocument
223
228
  ? canonicalName
224
229
  : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
@@ -234,7 +239,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
234
239
  assetId: overwriteTarget?.asset.assetId || existingUploadedAsset?.assetId || embeddedAssetId || `asset-${randomUUID().replace(/-/g, '')}`,
235
240
  workspaceId: deps.activeWorkspaceId,
236
241
  kind,
237
- logicalName: isMarkdownDocument ? canonicalName : safeFilename,
242
+ logicalName: canonicalName,
238
243
  originalFilename: safeFilename,
239
244
  mimeType: resolvedMimeType,
240
245
  storageProvider: 'r2',
@@ -269,7 +274,7 @@ export function createTaskAttachmentCommandAdapter(deps) {
269
274
  assetId: asset.assetId,
270
275
  taskId: id,
271
276
  role: 'attachment',
272
- displayName: kind === 'document' ? null : persistedCaption,
277
+ displayName: kind === 'document' ? null : safeDisplayName,
273
278
  updatedAt: now,
274
279
  deletedAt: null,
275
280
  });
@@ -324,13 +329,14 @@ export function createTaskAttachmentCommandAdapter(deps) {
324
329
  const overwriteTitle = overwriteTarget
325
330
  ? deps.workspaceAssetStore?.getDocument?.(overwriteTarget.asset.assetId, deps.activeWorkspaceId)?.title
326
331
  : null;
327
- const canonicalName = safeCaption || overwriteTitle || safeFilename;
332
+ const canonicalName = safeCaption || overwriteTitle || overwriteTarget?.asset.logicalName || safeFilename;
328
333
  const persistedCaption = isMarkdownDocument
329
334
  ? canonicalName
330
335
  : (safeCaption || overwriteTarget?.attachment.caption || safeFilename);
331
336
  asset = await deps.canonicalAssetHelpers.ensureCanonicalTaskAttachment(relativePath, id, persistedCaption, now, 'attachment', {
332
337
  originalFilename: safeFilename,
333
- logicalName: isMarkdownDocument ? canonicalName : safeFilename,
338
+ logicalName: canonicalName,
339
+ displayName: safeDisplayName,
334
340
  ...(!overwriteTarget ? { assetId: stableAttachmentAssetId } : {}),
335
341
  ...(overwriteTarget
336
342
  ? {
@@ -112,14 +112,14 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
112
112
  },
113
113
  }));
114
114
  register('list_tasks', (context) => ({
115
- description: context.describeTool('List active tasks with optional filters, including workstreamId (an ID or WS-123 reference). Use offset and limit for bounded pagination. Use openOnly=true to exclude done/cancelled. Use format=json for programmatic use. Returns compact task summaries without descriptions, comments, activity, attachments, or event streams. When a task URL is provided, preserve it when presenting the task so the user can open it directly. Use get_task for one task’s details.'),
115
+ description: context.describeTool('List active tasks with filters and bounded pagination. Use workstreamId with an ID or WS-123 and openOnly=true to exclude done/cancelled. Use format=json for programmatic use. Returns compact summaries; use get_task for details. When a task URL is returned, preserve it when presenting the task.'),
116
116
  inputSchema: {
117
117
  properties: {
118
118
  format: stringProperty('Toggle output format (default: markdown).', ['markdown', 'json']),
119
119
  category: stringProperty('Filter by category.'),
120
120
  status: stringProperty('Filter by status.', ['task', 'on-hold', 'in-progress', 'review', 'done', 'cancelled']),
121
121
  openOnly: booleanProperty('When true, only include open tasks (task, on-hold, in-progress, review).'),
122
- assignee: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
122
+ assignee: stringProperty(context.assigneeHint),
123
123
  priority: numberProperty('Filter by priority level.'),
124
124
  type: stringProperty('Filter by task type.', context.typeEnum),
125
125
  workstreamId: stringProperty('Filter by workstream ID or workstream reference such as WS-123.'),
@@ -131,7 +131,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
131
131
  },
132
132
  }));
133
133
  register('search_tasks', (context) => ({
134
- description: context.describeTool('Search active and archived tasks by keyword or exact task reference. Matches title, description, internal ID, canonical T-* references, and provisional local LT-* references. Results use linked task references when an application URL is available; preserve those links when presenting tasks. Use scope=\'active\' or scope=\'archive\' to narrow results. Prefer this over list_tasks when looking for a specific task by name.'),
134
+ description: context.describeTool('Search active and archived tasks by keyword, ID, T-* reference, or provisional LT-* reference. Use scope to narrow results. Prefer this over list_tasks when looking for a specific task by name. When results include links, preserve those links when presenting tasks.'),
135
135
  inputSchema: {
136
136
  properties: {
137
137
  query: stringProperty('Search term or exact task reference such as T-123 or LT-123.'),
@@ -150,6 +150,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
150
150
  }));
151
151
  register('link_task_relationship', (context) => ({
152
152
  description: context.describeTool('Create an idempotent structured relationship between two tasks. blocks(A,B) is the inverse view of depends_on(B,A). Rejects self-links, missing tasks, and dependency/supersession cycles.'),
153
+ annotations: { idempotentHint: true },
153
154
  inputSchema: { properties: {
154
155
  sourceTaskId: stringProperty('Source task ID or reference.'),
155
156
  targetTaskId: stringProperty('Target task ID or reference.'),
@@ -159,6 +160,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
159
160
  }));
160
161
  register('unlink_task_relationship', (context) => ({
161
162
  description: context.describeTool('Idempotently remove a structured task relationship. The relationship is tombstoned so deletion converges across sync.'),
163
+ annotations: { idempotentHint: true },
162
164
  inputSchema: { properties: {
163
165
  sourceTaskId: stringProperty('Source task ID or reference.'),
164
166
  targetTaskId: stringProperty('Target task ID or reference.'),
@@ -167,11 +169,11 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
167
169
  }, required: ['sourceTaskId', 'targetTaskId', 'type'] },
168
170
  }));
169
171
  register('get_task', (context) => ({
170
- description: context.describeTool('Get one task by ID or T-123 reference. The default compact response returns scalar task fields and collection counts without comments, history, checklist items, or attachments. When the response includes url, preserve it in any task reference you present so the user can open the task directly. Pass detail=full for comments and attachment/checklist metadata, or detail=history for one chronological comment/event stream. Use get_task_attachments for attachment bodies; use D-123/I-123 tools for documents/images. When showing this task to the user in a client that supports rich inline rendering, prefer a compact visual summary that retains the task link.'),
172
+ description: context.describeTool('Get one task by ID or T-123 reference. The default compact response returns scalar fields and collection counts. Use detail=full for comments and attachment/checklist metadata, or detail=history for chronology. Use get_task_attachments for attachment bodies; use D-123/I-123 tools for documents/images. When url is returned, preserve it in any task reference you present.'),
171
173
  inputSchema: {
172
174
  properties: {
173
175
  id: stringProperty('Task ID or task reference such as T-123.'),
174
- detail: stringProperty('Response detail level (default: compact). Use full for comments and attachment/checklist metadata, or history for chronological task events.', ['compact', 'full', 'history']),
176
+ detail: stringProperty('Response detail: compact, full, or chronological history.', ['compact', 'full', 'history']),
175
177
  },
176
178
  required: ['id'],
177
179
  },
@@ -185,24 +187,24 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
185
187
  },
186
188
  }));
187
189
  register('get_initiative', (context) => ({
188
- description: context.describeTool('Get one initiative by ID or IN-123 reference. The default compact response returns summary counts. Use detail=planning for current attachments plus paginated active/archived workstream summaries and historical task counts, detail=history for bounded chronological history, or detail=full for the compatible rich response.'),
190
+ description: context.describeTool('Get one initiative by ID or IN-123. Compact returns counts; detail=planning adds attachments, paginated workstreams, and historical task counts; detail=history returns bounded chronology; full returns the compatible rich response.'),
189
191
  inputSchema: {
190
192
  properties: {
191
193
  id: stringProperty('Initiative ID or initiative reference such as IN-123.'),
192
- detail: stringProperty('Response detail level (default: compact). Planning excludes history; history is chronological and bounded; full preserves the rich response.', ['compact', 'planning', 'history', 'full']),
193
- limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Page size for planning workstreams or history entries (default: 25, max: 100).' },
194
- offset: { type: 'integer', minimum: 0, description: 'Non-negative page offset for planning workstreams or history entries (default: 0).' },
194
+ detail: stringProperty('Response detail: compact, planning, history, or full.', ['compact', 'planning', 'history', 'full']),
195
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Planning/history page size (default 25; max 100).' },
196
+ offset: { type: 'integer', minimum: 0, description: 'Planning/history page offset (default 0).' },
195
197
  },
196
198
  required: ['id'],
197
199
  },
198
200
  }));
199
201
  register('create_initiative', (context) => ({
200
- description: context.describeTool('Create a new initiative. Use this for the top planning layer when a body of work should coordinate multiple workstreams.'),
202
+ description: context.describeTool('Create a new initiative. Pass idempotencyKey when retrying after an ambiguous timeout; reuse it only with the exact same payload.'),
201
203
  inputSchema: {
202
204
  properties: {
203
205
  title: stringProperty('Initiative title.'),
204
206
  description: stringProperty('Optional initiative description.'),
205
- ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
207
+ ownerId: stringProperty(context.assigneeHint),
206
208
  order: numberProperty('Optional sort order among initiatives.'),
207
209
  attachments: arrayProperty('Optional attachment descriptors to store on the initiative. Prefer save_initiative_attachment for generated content or files.', {
208
210
  type: 'object',
@@ -216,6 +218,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
216
218
  },
217
219
  required: ['path'],
218
220
  }),
221
+ idempotencyKey: { type: 'string', minLength: 1, maxLength: 128, description: 'Optional opaque retry key scoped to the active workspace, actor, and tool.' },
219
222
  },
220
223
  required: ['title'],
221
224
  },
@@ -227,9 +230,9 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
227
230
  id: stringProperty('Initiative ID or initiative reference such as IN-123.'),
228
231
  title: stringProperty('Updated initiative title.'),
229
232
  description: stringProperty('Updated initiative description.'),
230
- ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
233
+ ownerId: stringProperty(context.assigneeHint),
231
234
  order: numberProperty('Optional sort order among initiatives.'),
232
- attachments: arrayProperty('Replacement attachment descriptors for the initiative. Prefer save_initiative_attachment and delete_initiative_attachment for generated content or files.', {
235
+ attachments: arrayProperty('Replacement attachment descriptors for the initiative. Prefer save_initiative_attachment for generated content or files. Removing managed attachments requires delete_initiative_attachment in the extended profile.', {
233
236
  type: 'object',
234
237
  properties: {
235
238
  path: { type: 'string' },
@@ -246,11 +249,12 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
246
249
  },
247
250
  }));
248
251
  register('add_initiative_comment', (context) => ({
249
- description: context.describeTool('Add a comment to an initiative. Use this for initiative-level progress notes, decisions, coordination context, and audit trails. Comments appear in the initiative activity feed.'),
252
+ description: context.describeTool('Add a comment to an initiative. Pass idempotencyKey when retrying after an ambiguous timeout; reuse it only with the exact same target and text.'),
250
253
  inputSchema: {
251
254
  properties: {
252
255
  id: stringProperty('Initiative ID or initiative reference such as IN-123.'),
253
256
  text: stringProperty('Comment text.'),
257
+ idempotencyKey: { type: 'string', minLength: 1, maxLength: 128, description: 'Optional opaque retry key scoped to the active workspace, actor, and tool.' },
254
258
  },
255
259
  required: ['id', 'text'],
256
260
  },
@@ -282,24 +286,24 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
282
286
  },
283
287
  }));
284
288
  register('get_workstream', (context) => ({
285
- description: context.describeTool('Get one workstream by ID or WS-123 reference. The default compact response returns a summary and task count. Use detail=planning for current attachments and explicit historical task counts without nested tasks, detail=history for bounded chronological history, or detail=full for the compatible rich response. Use list_tasks with workstreamId for bounded active task discovery.'),
289
+ description: context.describeTool('Get one workstream by ID or WS-123. Compact returns a summary and task count; detail=planning adds attachments and historical counts; detail=history returns bounded chronology; full returns the compatible rich response. Use list_tasks with workstreamId for active tasks.'),
286
290
  inputSchema: {
287
291
  properties: {
288
292
  id: stringProperty('Workstream ID or workstream reference such as WS-123.'),
289
- detail: stringProperty('Response detail level (default: compact). Planning excludes history and nested tasks; history is chronological and bounded; full preserves the rich response.', ['compact', 'planning', 'history', 'full']),
290
- limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Page size for history entries (default: 25, max: 100).' },
291
- offset: { type: 'integer', minimum: 0, description: 'Non-negative history page offset (default: 0).' },
293
+ detail: stringProperty('Response detail: compact, planning, history, or full.', ['compact', 'planning', 'history', 'full']),
294
+ limit: { type: 'integer', minimum: 1, maximum: 100, description: 'History page size (default 25; max 100).' },
295
+ offset: { type: 'integer', minimum: 0, description: 'History page offset (default 0).' },
292
296
  },
293
297
  required: ['id'],
294
298
  },
295
299
  }));
296
300
  register('create_workstream', (context) => ({
297
- description: context.describeTool('Create a new workstream. Use initiativeId to link it to an initiative, or omit initiativeId to create a standalone workstream.'),
301
+ description: context.describeTool('Create a new workstream. Pass idempotencyKey when retrying after an ambiguous timeout; reuse it only with the exact same payload.'),
298
302
  inputSchema: {
299
303
  properties: {
300
304
  title: stringProperty('Workstream title.'),
301
305
  description: stringProperty('Optional workstream description.'),
302
- ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
306
+ ownerId: stringProperty(context.assigneeHint),
303
307
  initiativeId: stringProperty('Optional initiative ID or initiative reference such as IN-123.'),
304
308
  order: numberProperty('Optional sort order among workstreams.'),
305
309
  attachments: arrayProperty('Optional attachment descriptors to store on the workstream. Prefer save_workstream_attachment for generated content or files.', {
@@ -314,6 +318,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
314
318
  },
315
319
  required: ['path'],
316
320
  }),
321
+ idempotencyKey: { type: 'string', minLength: 1, maxLength: 128, description: 'Optional opaque retry key scoped to the active workspace, actor, and tool.' },
317
322
  },
318
323
  required: ['title'],
319
324
  },
@@ -325,10 +330,10 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
325
330
  id: stringProperty('Workstream ID or workstream reference such as WS-123.'),
326
331
  title: stringProperty('Updated workstream title.'),
327
332
  description: stringProperty('Updated workstream description.'),
328
- ownerId: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
333
+ ownerId: stringProperty(context.assigneeHint),
329
334
  initiativeId: stringProperty('Optional initiative ID or initiative reference such as IN-123. Pass an empty string to unlink.'),
330
335
  order: numberProperty('Optional sort order among workstreams.'),
331
- attachments: arrayProperty('Replacement attachment descriptors for the workstream. Prefer save_workstream_attachment and delete_workstream_attachment for generated content or files.', {
336
+ attachments: arrayProperty('Replacement attachment descriptors for the workstream. Prefer save_workstream_attachment for generated content or files. Removing managed attachments requires delete_workstream_attachment in the extended profile.', {
332
337
  type: 'object',
333
338
  properties: {
334
339
  path: { type: 'string' },
@@ -345,11 +350,12 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
345
350
  },
346
351
  }));
347
352
  register('add_workstream_comment', (context) => ({
348
- description: context.describeTool('Add a comment to a workstream. Use this for workstream-level progress notes, decisions, coordination context, and audit trails. Comments appear in the workstream activity feed.'),
353
+ description: context.describeTool('Add a comment to a workstream. Pass idempotencyKey when retrying after an ambiguous timeout; reuse it only with the exact same target and text.'),
349
354
  inputSchema: {
350
355
  properties: {
351
356
  id: stringProperty('Workstream ID or workstream reference such as WS-123.'),
352
357
  text: stringProperty('Comment text.'),
358
+ idempotencyKey: { type: 'string', minLength: 1, maxLength: 128, description: 'Optional opaque retry key scoped to the active workspace, actor, and tool.' },
353
359
  },
354
360
  required: ['id', 'text'],
355
361
  },
@@ -374,6 +380,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
374
380
  }));
375
381
  register('update_task_workstream', (context) => ({
376
382
  description: context.describeTool('Set or clear the workstream for one task. Pass workstreamId to assign or reassign; pass an empty string or null to unlink the task from its current workstream.'),
383
+ annotations: { idempotentHint: true },
377
384
  inputSchema: {
378
385
  properties: {
379
386
  taskId: stringProperty('Task ID or task reference such as T-123.'),
@@ -447,13 +454,13 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
447
454
  inputSchema: {
448
455
  properties: {
449
456
  taskId: stringProperty('Task ID or task reference such as T-123.'),
450
- id: stringProperty('Alias for taskId. Task ID or task reference such as T-123.'),
451
- date: nullableStringProperty('Target date in YYYY-MM-DD format. Pass an empty string or null with taskId to unschedule.'),
452
- scheduledDate: nullableStringProperty('Alias for date. Target date in YYYY-MM-DD format. Pass an empty string or null with taskId to unschedule.'),
457
+ id: stringProperty('Alias for taskId.'),
458
+ date: nullableStringProperty('YYYY-MM-DD; null or empty with taskId unschedules.'),
459
+ scheduledDate: nullableStringProperty('Alias for date.'),
453
460
  order: numberProperty('Optional position within that day.'),
454
- orderInDay: numberProperty('Alias for order. Optional position within that day.'),
455
- taskIds: arrayProperty('Ordered task IDs or task references for replacing one scheduled day order.', { type: 'string' }),
456
- moves: arrayProperty('Advanced reorder moves. Each move may include id or taskId, date or scheduledDate, and order or orderInDay.', {
461
+ orderInDay: numberProperty('Alias for order.'),
462
+ taskIds: arrayProperty('Ordered task IDs or references replacing one day.', { type: 'string' }),
463
+ moves: arrayProperty('Advanced reorder moves using task, date, and order aliases.', {
457
464
  type: 'object',
458
465
  properties: {
459
466
  id: { type: 'string' },
@@ -513,7 +520,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
513
520
  },
514
521
  }));
515
522
  register('create_task', (context) => ({
516
- description: context.describeTool('Create a new task. Pass idempotencyKey when the call may be retried after a timeout; the same key and payload return the original task, while a changed payload is rejected. Current taxonomy values are included in this schema; call get_config only for richer metadata or after a validation mismatch.'),
523
+ description: context.describeTool('Create a task. For retries, the same idempotencyKey and payload return the original; changed payloads conflict. Taxonomy enums are current; use get_config for metadata or after validation errors.'),
517
524
  inputSchema: {
518
525
  properties: {
519
526
  title: stringProperty('Task title.'),
@@ -524,8 +531,8 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
524
531
  description: 'Optional opaque retry key scoped to the active workspace and AI profile.',
525
532
  },
526
533
  description: stringProperty('Optional task description.'),
527
- assignee: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
528
- workstreamId: stringProperty('Optional workstream ID or workstream reference such as WS-123. Use this to create the task directly inside a workstream without a separate update_task_workstream call. Call list_workstreams with query first if you only know the workstream name.'),
534
+ assignee: stringProperty(context.assigneeHint),
535
+ workstreamId: stringProperty('Workstream ID or WS-123; use to create the task directly inside a workstream. Search list_workstreams if needed.'),
529
536
  category: stringProperty('Task category value.', context.categoryEnum),
530
537
  type: stringProperty('Task type value.', context.typeEnum),
531
538
  priority: numberProperty(context.priorityDesc),
@@ -548,6 +555,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
548
555
  }));
549
556
  register('reopen_task', (context) => ({
550
557
  description: context.describeTool('Safely resume reviewed work in one call. Reopens archived done tasks or active/archived review tasks to in-progress, preserves an existing assignee, and claims only unassigned work. Cancelled tasks are rejected. Already in-progress tasks are idempotent no-ops.'),
558
+ annotations: { idempotentHint: true },
551
559
  inputSchema: {
552
560
  properties: {
553
561
  id: stringProperty('Task ID or task reference such as T-123.'),
@@ -562,7 +570,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
562
570
  id: stringProperty('Task ID or task reference such as T-123.'),
563
571
  title: stringProperty('Updated task title.'),
564
572
  description: stringProperty('Updated task description.'),
565
- assignee: stringProperty(`${context.assigneeHint} Call list_assignees to inspect the current eligible IDs.`),
573
+ assignee: stringProperty(context.assigneeHint),
566
574
  workstreamId: stringProperty('Updated workstream ID or workstream reference such as WS-123. Pass an empty string to unlink the task. Call list_workstreams with query first if you only know the workstream name.'),
567
575
  category: stringProperty('Updated category value.', context.categoryEnum),
568
576
  type: stringProperty('Updated task type value.', context.typeEnum),
@@ -576,7 +584,7 @@ export function registerTaskPlanningTools(registerTool, executeTool) {
576
584
  },
577
585
  }));
578
586
  register('finish_task', (context) => ({
579
- description: context.describeTool('default end-of-task handoff for active work. Archived tasks are rejected; call reopen_task first. Use outcome=\'done\' when work is complete and outcome=\'review\' when human validation or approval is needed. If you report completed work on a tracked task, either finish it with this tool or explain why it remains open. Both outcomes remain active and unarchived. This tool never archives automatically; use archive_task separately to move work to history. The response always reports completion.archived from persisted state.'),
587
+ description: context.describeTool('Default end-of-task handoff. Use outcome=\'done\' for completed work or outcome=\'review\' for validation. Both remain active and unarchived; use archive_task for history. Archived tasks require reopen_task first.'),
580
588
  inputSchema: {
581
589
  properties: {
582
590
  id: stringProperty('Task ID or task reference such as T-123.'),
@@ -52,10 +52,10 @@ export const MCP_TOOL_REGISTRY = Object.freeze({
52
52
  replace_task_checklist: defaultTool({ kind: 'scope', scope: 'tasks:write' }, true),
53
53
  get_task_attachments: defaultTool({ kind: 'scope', scope: 'tasks:read' }, false),
54
54
  get_workstream_attachments: extendedTool({ kind: 'scope', scope: 'tasks:read' }, false),
55
- save_workstream_attachment: extendedTool({ kind: 'scope', scope: 'tasks:write' }, true),
55
+ save_workstream_attachment: defaultTool({ kind: 'scope', scope: 'tasks:write' }, true),
56
56
  delete_workstream_attachment: extendedTool({ kind: 'scope', scope: 'tasks:write' }, true),
57
57
  get_initiative_attachments: extendedTool({ kind: 'scope', scope: 'tasks:read' }, false),
58
- save_initiative_attachment: extendedTool({ kind: 'scope', scope: 'tasks:write' }, true),
58
+ save_initiative_attachment: defaultTool({ kind: 'scope', scope: 'tasks:write' }, true),
59
59
  delete_initiative_attachment: extendedTool({ kind: 'scope', scope: 'tasks:write' }, true),
60
60
  search_images: defaultTool({ kind: 'scope', scope: 'tasks:read' }, false),
61
61
  get_image: defaultTool({ kind: 'scope', scope: 'tasks:read' }, false),
@@ -1160,6 +1160,22 @@ export function ensureWorkspaceAssetsSchema(db) {
1160
1160
  ON workspace_asset_links(tenant_id, workspace_id, asset_id, deleted_at, updated_at);
1161
1161
  CREATE INDEX IF NOT EXISTS idx_workspace_asset_links_workspace_updated
1162
1162
  ON workspace_asset_links(tenant_id, workspace_id, updated_at, asset_id);
1163
+
1164
+ CREATE TABLE IF NOT EXISTS workspace_asset_upload_drafts (
1165
+ tenant_id TEXT NOT NULL DEFAULT 'default',
1166
+ workspace_id TEXT NOT NULL,
1167
+ draft_id TEXT NOT NULL,
1168
+ asset_id TEXT NOT NULL,
1169
+ storage_key TEXT NOT NULL,
1170
+ purpose TEXT NOT NULL,
1171
+ created_by_user_id TEXT NOT NULL,
1172
+ created_at TEXT NOT NULL,
1173
+ retired_at TEXT,
1174
+ PRIMARY KEY (tenant_id, workspace_id, draft_id, asset_id)
1175
+ );
1176
+
1177
+ CREATE INDEX IF NOT EXISTS idx_workspace_asset_upload_drafts_expiry
1178
+ ON workspace_asset_upload_drafts(tenant_id, workspace_id, retired_at, created_at);
1163
1179
  `);
1164
1180
  const linkColumns = db.prepare(`PRAGMA table_info(workspace_asset_links)`).all();
1165
1181
  const hasDisplayNameColumn = linkColumns.some((c) => c.name === 'display_name');
@@ -2801,6 +2817,9 @@ export function ensureDocumentReviewSessionsSchema(db) {
2801
2817
  thread_status_updated_at TEXT,
2802
2818
  resolved_at TEXT,
2803
2819
  resolved_by_actor_id TEXT,
2820
+ idempotency_key TEXT,
2821
+ idempotency_fingerprint TEXT,
2822
+ idempotency_actor_scope TEXT NOT NULL DEFAULT '',
2804
2823
  order_index INTEGER NOT NULL DEFAULT 0,
2805
2824
  created_at TEXT NOT NULL,
2806
2825
  updated_at TEXT NOT NULL,
@@ -2830,6 +2849,22 @@ export function ensureDocumentReviewSessionsSchema(db) {
2830
2849
  if (!commentColumnNames.has('deleted_by_actor_id')) {
2831
2850
  addCommentColumn('deleted_by_actor_id');
2832
2851
  }
2852
+ if (!commentColumnNames.has('idempotency_key')) {
2853
+ addCommentColumn('idempotency_key');
2854
+ }
2855
+ if (!commentColumnNames.has('idempotency_fingerprint')) {
2856
+ addCommentColumn('idempotency_fingerprint');
2857
+ }
2858
+ if (!commentColumnNames.has('idempotency_actor_scope')) {
2859
+ const ifNotExists = db.provider === 'postgres' ? ' IF NOT EXISTS' : '';
2860
+ db.exec(`ALTER TABLE document_review_comments ADD COLUMN${ifNotExists} idempotency_actor_scope TEXT NOT NULL DEFAULT ''`);
2861
+ }
2862
+ db.exec(`
2863
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_document_review_comments_idempotency
2864
+ ON document_review_comments (
2865
+ tenant_id, workspace_id, idempotency_actor_scope, idempotency_key
2866
+ )
2867
+ `);
2833
2868
  if (!commentColumnNames.has('parent_comment_id')) {
2834
2869
  addCommentColumn('parent_comment_id');
2835
2870
  }
@@ -12,13 +12,13 @@ export declare const APP_GATE_DEFINITIONS: {
12
12
  readonly removalMilestone: "Document Manager GA";
13
13
  };
14
14
  readonly 'workflow_manager.workspace_access': {
15
- readonly enabled: true;
15
+ readonly enabled: false;
16
16
  readonly description: "Workflow Manager workspace readiness gate.";
17
17
  readonly owner: "taskforce";
18
18
  readonly removalMilestone: "Workflow Manager GA";
19
19
  };
20
20
  readonly 'taskforce_agents.workspace_access': {
21
- readonly enabled: true;
21
+ readonly enabled: false;
22
22
  readonly description: "Taskforce Agents workspace readiness gate.";
23
23
  readonly owner: "taskforce";
24
24
  readonly removalMilestone: "Taskforce Agents workspace GA";
@@ -12,13 +12,13 @@ export const APP_GATE_DEFINITIONS = {
12
12
  removalMilestone: 'Document Manager GA'
13
13
  },
14
14
  'workflow_manager.workspace_access': {
15
- enabled: true,
15
+ enabled: false,
16
16
  description: 'Workflow Manager workspace readiness gate.',
17
17
  owner: 'taskforce',
18
18
  removalMilestone: 'Workflow Manager GA'
19
19
  },
20
20
  'taskforce_agents.workspace_access': {
21
- enabled: true,
21
+ enabled: false,
22
22
  description: 'Taskforce Agents workspace readiness gate.',
23
23
  owner: 'taskforce',
24
24
  removalMilestone: 'Taskforce Agents workspace GA'
@@ -0,0 +1,12 @@
1
+ import type { AccountAccessStatus } from '../core/types.js';
2
+ export interface AppAccessDeniedResponse {
3
+ statusCode: 403;
4
+ body: {
5
+ error: string;
6
+ code: string;
7
+ gate: AccountAccessStatus['gate'];
8
+ canAccessPlans: boolean;
9
+ planSelectionRequired: boolean;
10
+ };
11
+ }
12
+ export declare function resolveAppAccessDeniedResponse(accountAccess: AccountAccessStatus): AppAccessDeniedResponse | null;
@@ -0,0 +1,21 @@
1
+ export function resolveAppAccessDeniedResponse(accountAccess) {
2
+ if (accountAccess.canAccessApp)
3
+ return null;
4
+ const code = accountAccess.gate === 'plan_selection_required'
5
+ ? 'PLAN_SELECTION_REQUIRED'
6
+ : accountAccess.gate === 'checkout_pending'
7
+ ? 'CHECKOUT_PENDING'
8
+ : accountAccess.gate === 'misconfigured'
9
+ ? 'ACCOUNT_ACCESS_MISCONFIGURED'
10
+ : 'ACCOUNT_ENTITLEMENT_REQUIRED';
11
+ return {
12
+ statusCode: 403,
13
+ body: {
14
+ error: accountAccess.message || 'This account cannot access the app.',
15
+ code,
16
+ gate: accountAccess.gate,
17
+ canAccessPlans: accountAccess.canAccessPlans,
18
+ planSelectionRequired: accountAccess.planSelectionRequired
19
+ }
20
+ };
21
+ }
@@ -0,0 +1,12 @@
1
+ import type { TaskforceCore } from '../core/Taskforce.js';
2
+ import type { RequestAccessContext } from './auth.js';
3
+ export interface AuthorizedRequestAccess extends RequestAccessContext {
4
+ disabled?: boolean;
5
+ accountDisabled?: boolean;
6
+ workspaceDisabled?: boolean;
7
+ }
8
+ type WorkspaceAccessCore = Pick<TaskforceCore, 'resolveUserAccess' | 'getSystemRole'>;
9
+ export declare function resolveAuthorizedWorkspaceAccess(core: WorkspaceAccessCore, access: AuthorizedRequestAccess, requestedWorkspaceId: string): AuthorizedRequestAccess;
10
+ export declare function isWorkspaceAccessRecoveryRoute(pathname: string): boolean;
11
+ export declare function resolveDisabledAccessError(access: Pick<AuthorizedRequestAccess, 'disabled' | 'accountDisabled' | 'workspaceDisabled'>, pathname: string): string | null;
12
+ export {};
@@ -0,0 +1,40 @@
1
+ export function resolveAuthorizedWorkspaceAccess(core, access, requestedWorkspaceId) {
2
+ if (!access.authenticated || !access.userId || access.userId === 'anonymous')
3
+ return access;
4
+ const trimmedRequestedWorkspaceId = String(requestedWorkspaceId || '').trim();
5
+ const candidateWorkspaceIds = [
6
+ trimmedRequestedWorkspaceId,
7
+ String(access.workspaceId || '').trim()
8
+ ].filter((workspaceId, index, values) => Boolean(workspaceId) && values.indexOf(workspaceId) === index);
9
+ for (const workspaceId of candidateWorkspaceIds) {
10
+ const resolved = core.resolveUserAccess(access.userId, workspaceId, access.role);
11
+ if (!resolved)
12
+ continue;
13
+ return {
14
+ ...access,
15
+ role: resolved.role,
16
+ workspaceId: resolved.workspaceId,
17
+ userId: resolved.userId,
18
+ systemRole: core.getSystemRole(resolved.userId),
19
+ disabled: resolved.disabled,
20
+ accountDisabled: resolved.accountDisabled ?? resolved.disabled,
21
+ workspaceDisabled: resolved.workspaceDisabled ?? false
22
+ };
23
+ }
24
+ return access;
25
+ }
26
+ export function isWorkspaceAccessRecoveryRoute(pathname) {
27
+ return pathname === '/api/taskforce/workspaces'
28
+ || pathname === '/api/taskforce/session/workspace';
29
+ }
30
+ export function resolveDisabledAccessError(access, pathname) {
31
+ if (pathname.startsWith('/api/taskforce/auth/'))
32
+ return null;
33
+ if (access.accountDisabled ?? access.disabled) {
34
+ return 'Forbidden: user account is disabled.';
35
+ }
36
+ if (access.workspaceDisabled && !isWorkspaceAccessRecoveryRoute(pathname)) {
37
+ return 'Forbidden: workspace access is not active for this user.';
38
+ }
39
+ return null;
40
+ }
@@ -0,0 +1,25 @@
1
+ import type { TaskforceCore } from '../core/Taskforce.js';
2
+ import { type AuthorizedRequestAccess } from './authorizedWorkspaceAccess.js';
3
+ export type CloudCollectionReadOperation = 'tasks' | 'archive' | 'deleted' | 'planning-bootstrap';
4
+ export interface CloudCollectionReadRequest {
5
+ operation: CloudCollectionReadOperation;
6
+ pathname: string;
7
+ requestedWorkspaceId: string;
8
+ access: AuthorizedRequestAccess;
9
+ performanceTrace: boolean;
10
+ }
11
+ export interface CloudCollectionReadResponse {
12
+ statusCode: number;
13
+ headers: Record<string, string>;
14
+ body: string;
15
+ workspaceId: string;
16
+ performance?: {
17
+ durationMs: number;
18
+ details: Record<string, number>;
19
+ };
20
+ }
21
+ type CloudCollectionReadCore = Pick<TaskforceCore, 'resolveUserAccess' | 'getSystemRole' | 'getAccountAccessStatus' | 'listTasks' | 'listArchive' | 'listDeletedTasksReadOnly' | 'listPlanningBootstrap'>;
22
+ export declare function resolveCloudCollectionReadOperation(method: string | undefined, pathname: string): CloudCollectionReadOperation | null;
23
+ export declare function executeCloudCollectionRead(core: CloudCollectionReadCore, request: CloudCollectionReadRequest): CloudCollectionReadResponse;
24
+ export declare function cloudCollectionReadRoute(operation: CloudCollectionReadOperation): string;
25
+ export {};