@taskforcehq/taskforce 0.3.303 → 0.3.305

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 (237) hide show
  1. package/README.md +140 -247
  2. package/dist/Taskforce.module.css +102 -5
  3. package/dist/TaskforceCore.js +635 -238
  4. package/dist/TaskforceCore.test.js +2243 -495
  5. package/dist/annotatedAttachments/service.d.ts +1 -1
  6. package/dist/annotatedAttachments/types.d.ts +1 -1
  7. package/dist/compat/workspaceSyncCompat.js +6 -0
  8. package/dist/components/features/AgentsModule.js +1 -1
  9. package/dist/components/features/AgentsModule.test.js +1 -1
  10. package/dist/components/features/DocumentIndex.d.ts +1 -1
  11. package/dist/components/features/DocumentIndex.js +1 -1
  12. package/dist/components/features/DocumentViewer.d.ts +1 -1
  13. package/dist/components/features/DocumentViewer.js +32 -9
  14. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  15. package/dist/components/features/DocumentWorkspace.js +12 -155
  16. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  17. package/dist/components/features/TaskSettings.js +87 -41
  18. package/dist/components/features/TaskSettings.test.js +180 -40
  19. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  20. package/dist/components/features/documentWorkspaceModel.js +57 -0
  21. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  22. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  23. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  24. package/dist/components/task/TaskActionHeader.js +3 -3
  25. package/dist/components/task/TaskActionHeader.test.js +8 -0
  26. package/dist/components/task/TaskCard.js +16 -1
  27. package/dist/components/task/TaskCard.test.js +31 -0
  28. package/dist/components/task/TaskForm.d.ts +9 -8
  29. package/dist/components/task/TaskForm.js +374 -219
  30. package/dist/components/task/TaskForm.test.js +482 -361
  31. package/dist/components/task/TaskKanban.d.ts +1 -1
  32. package/dist/components/task/TaskKanban.js +33 -161
  33. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  34. package/dist/components/task/TaskStatusActions.js +8 -3
  35. package/dist/components/task/TaskStatusActions.test.js +9 -0
  36. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  37. package/dist/components/task/taskKanbanDropRules.js +187 -0
  38. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  39. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  40. package/dist/components/ui/Modal.d.ts +1 -1
  41. package/dist/components/ui/Modal.js +1 -0
  42. package/dist/components/views/AppShellHeader.js +5 -1
  43. package/dist/components/views/AppShellHeader.test.js +4 -0
  44. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  45. package/dist/components/views/PlanComparisonPage.js +9 -10
  46. package/dist/components/views/PlanComparisonPage.test.js +55 -41
  47. package/dist/components/views/PlansPage.d.ts +5 -1
  48. package/dist/components/views/PlansPage.js +150 -81
  49. package/dist/components/views/PlansPage.test.js +367 -41
  50. package/dist/components/views/StandaloneLayout.js +135 -54
  51. package/dist/components/views/WidgetView.js +11 -2
  52. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  53. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  54. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  55. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  56. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  57. package/dist/config/envSchema.js +1 -1
  58. package/dist/core/AiProfileService.d.ts +6 -1
  59. package/dist/core/AiProfileService.js +161 -16
  60. package/dist/core/AiProfileService.test.js +3 -1
  61. package/dist/core/AiProfiles.test.js +200 -0
  62. package/dist/core/AuthTokenService.test.d.ts +1 -0
  63. package/dist/core/AuthTokenService.test.js +78 -0
  64. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  65. package/dist/core/EntitlementsPolicy.test.js +75 -13
  66. package/dist/core/PlanEntitlementService.d.ts +9 -1
  67. package/dist/core/PlanEntitlementService.js +144 -19
  68. package/dist/core/PlanVersionPolicy.test.js +19 -26
  69. package/dist/core/SignupMonetizationSettings.test.js +75 -15
  70. package/dist/core/SystemAdmin.test.js +212 -56
  71. package/dist/core/TaskActivityService.d.ts +4 -1
  72. package/dist/core/TaskActivityService.js +45 -0
  73. package/dist/core/TaskAuditTimeline.test.js +52 -1
  74. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  75. package/dist/core/Taskforce.d.ts +18 -11
  76. package/dist/core/Taskforce.js +93 -12
  77. package/dist/core/WorkspacePlanMode.test.js +73 -8
  78. package/dist/core/shared.d.ts +2 -0
  79. package/dist/core/shared.js +11 -0
  80. package/dist/documentReviews/service.d.ts +1 -1
  81. package/dist/documentReviews/types.d.ts +1 -1
  82. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  83. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  84. package/dist/hooks/sync/auth.d.ts +37 -0
  85. package/dist/hooks/sync/auth.js +63 -0
  86. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  87. package/dist/hooks/sync/bootstrap.js +52 -0
  88. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  89. package/dist/hooks/sync/orchestratorShared.js +209 -0
  90. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  91. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  92. package/dist/hooks/sync/realtime.d.ts +25 -0
  93. package/dist/hooks/sync/realtime.js +60 -0
  94. package/dist/hooks/sync/recovery.d.ts +69 -0
  95. package/dist/hooks/sync/recovery.js +118 -0
  96. package/dist/hooks/sync/transfers.d.ts +123 -0
  97. package/dist/hooks/sync/transfers.js +435 -0
  98. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  99. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  100. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  101. package/dist/hooks/ui/useResourceExport.js +5 -2
  102. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  103. package/dist/hooks/useRealtimeSync.js +2 -1
  104. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  105. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  106. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  107. package/dist/hooks/useSyncOrchestrator.d.ts +12 -8
  108. package/dist/hooks/useSyncOrchestrator.js +388 -879
  109. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +641 -6
  110. package/dist/hooks/useTaskMutations.d.ts +2 -1
  111. package/dist/hooks/useTaskMutations.js +57 -7
  112. package/dist/hooks/useTaskMutations.test.js +176 -0
  113. package/dist/hooks/useTaskforce.d.ts +13 -4
  114. package/dist/hooks/useTaskforce.js +302 -817
  115. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  116. package/dist/hooks/useWorkspaceSyncController.d.ts +4 -1
  117. package/dist/hooks/useWorkspaceSyncController.js +33 -11
  118. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  119. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  120. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  121. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  122. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  123. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  124. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  125. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  126. package/dist/localization/locales/en-US.js +2 -2
  127. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  128. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  129. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  130. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  131. package/dist/mcp/clientIntegrations.d.ts +12 -1
  132. package/dist/mcp/clientIntegrations.js +161 -6
  133. package/dist/mcp/clientIntegrations.test.js +76 -4
  134. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  135. package/dist/mcp/collaborationRegistrar.js +71 -0
  136. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  137. package/dist/mcp/documentAssetRegistrar.js +346 -0
  138. package/dist/mcp/documentHelpers.d.ts +100 -0
  139. package/dist/mcp/documentHelpers.js +161 -0
  140. package/dist/mcp/runtime.js +2455 -3925
  141. package/dist/mcp/runtime.test.js +240 -8
  142. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  143. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  144. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  145. package/dist/mcp/taskAttachmentTypes.js +1 -0
  146. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  147. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  148. package/dist/mcp/toolCatalog.d.ts +35 -0
  149. package/dist/mcp/toolCatalog.js +3 -0
  150. package/dist/plugins/vite.js +1 -1
  151. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  152. package/dist/runtime/appGateDefinitions.js +1 -1
  153. package/dist/server/index.d.ts +1 -0
  154. package/dist/server/index.js +46 -3
  155. package/dist/server/index.test.js +12 -1
  156. package/dist/server/routes/admin.d.ts +1 -5
  157. package/dist/server/routes/admin.js +151 -80
  158. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  159. package/dist/server/routes/annotatedAttachments.js +1 -1
  160. package/dist/server/routes/auth.d.ts +1 -4
  161. package/dist/server/routes/auth.js +59 -64
  162. package/dist/server/routes/authSupport.d.ts +30 -0
  163. package/dist/server/routes/authSupport.js +81 -0
  164. package/dist/server/routes/billing.d.ts +1 -1
  165. package/dist/server/routes/billing.js +122 -255
  166. package/dist/server/routes/billing.test.js +257 -86
  167. package/dist/server/routes/documentReviews.d.ts +1 -1
  168. package/dist/server/routes/documentReviews.js +1 -1
  169. package/dist/server/routes/documents.d.ts +4 -2
  170. package/dist/server/routes/documents.js +73 -7
  171. package/dist/server/routes/primitives.d.ts +11 -0
  172. package/dist/server/routes/primitives.js +73 -0
  173. package/dist/server/routes/resources.d.ts +1 -1
  174. package/dist/server/routes/resources.js +1 -1
  175. package/dist/server/routes/shared.d.ts +31 -1
  176. package/dist/server/routes/shared.js +26 -0
  177. package/dist/server/routes/sync.d.ts +1 -1
  178. package/dist/server/routes/sync.integration.test.js +20 -0
  179. package/dist/server/routes/sync.js +111 -238
  180. package/dist/server/routes/tasks.d.ts +1 -1
  181. package/dist/server/routes/tasks.js +50 -8
  182. package/dist/server/routes/workspaces.d.ts +1 -1
  183. package/dist/server/routes/workspaces.js +1 -1
  184. package/dist/server/routes.d.ts +2 -9
  185. package/dist/server/routes.js +84 -181
  186. package/dist/server/routes.test.js +912 -32
  187. package/dist/shared/taskActor.d.ts +10 -0
  188. package/dist/shared/taskActor.js +1 -0
  189. package/dist/sync/collaborationSyncPayload.js +4 -0
  190. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  191. package/dist/sync/collaborationSyncState.d.ts +3 -1
  192. package/dist/sync/collaborationSyncState.js +19 -0
  193. package/dist/sync/contentSyncState.js +28 -17
  194. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  195. package/dist/sync/syncApplyHandlers.js +370 -8
  196. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  197. package/dist/sync/syncApplyHandlers.test.js +379 -0
  198. package/dist/sync/syncService.d.ts +9 -1
  199. package/dist/sync/syncService.js +44 -2
  200. package/dist/sync/syncService.test.js +62 -0
  201. package/dist/sync/workspacePullFeed.d.ts +1 -0
  202. package/dist/sync/workspacePullFeed.js +66 -59
  203. package/dist/sync/workspacePullFeed.test.js +72 -4
  204. package/dist/sync/workspaceSyncModel.d.ts +25 -1
  205. package/dist/sync/workspaceSyncModel.js +77 -3
  206. package/dist/sync/workspaceSyncModel.test.js +153 -0
  207. package/dist/sync/workspaceSyncState.d.ts +2 -0
  208. package/dist/sync/workspaceSyncState.js +1 -0
  209. package/dist/types.d.ts +2 -10
  210. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +1 -0
  211. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  212. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  213. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  214. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  215. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  216. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  217. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  218. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-BcS4afRn.js} +1 -1
  219. package/dist/ui/assets/index-C2-OXZV7.css +1 -0
  220. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  221. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-BSUaRwF8.js} +1 -1
  222. package/dist/ui/index.html +4 -4
  223. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  224. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  225. package/dist/utils/taskActivity.js +4 -0
  226. package/dist/utils/taskActivity.test.js +25 -1
  227. package/package.json +10 -1
  228. package/scripts/check-cloud-mcp.mjs +83 -0
  229. package/scripts/playwright-auth.sh +9 -1
  230. package/scripts/run-billing-performance-smoke.sh +24 -0
  231. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  232. package/dist/ui/assets/DocumentWorkspace-C3GyzrWm.js +0 -250
  233. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  234. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  235. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  236. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  237. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -436,11 +436,11 @@ describe('Taskforce MCP tool descriptions', () => {
436
436
  expect(linkTaskContext.description).toContain('external http(s) URL');
437
437
  expect(saveTaskAttachment.description).toContain('generated content');
438
438
  expect(saveTaskAttachment.description).toContain('overwrite=true');
439
- expect(saveTaskAttachment.description).toContain('AI-authored markdown/text should usually be passed inline via content');
439
+ expect(saveTaskAttachment.description).toContain('use inline content for AI-authored markdown/text');
440
440
  expect(saveTaskAttachment.inputSchema).toBeDefined();
441
441
  const saveTaskAttachmentSchema = saveTaskAttachment.inputSchema;
442
442
  expect(saveTaskAttachmentSchema.properties.content.description).toContain('use this for AI-authored markdown/text attachments');
443
- expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('local runtime only today');
443
+ expect(saveTaskAttachmentSchema.properties.sourcePath.description).toContain('approved agent workspace root');
444
444
  expect(resolveProfile.description).toContain('AI collaborator profile');
445
445
  expect(resolveProfile.description).toContain('Update semantics');
446
446
  expect(resolveProfile.description).toContain('empty strings are ignored rather than clearing');
@@ -733,6 +733,56 @@ describe('Taskforce MCP tool descriptions', () => {
733
733
  fs.rmSync(projectRoot, { recursive: true, force: true });
734
734
  }
735
735
  });
736
+ it('updates and persists metadata when cloud auto-bind reuses an existing exact-match profile', async () => {
737
+ const projectRoot = createProjectRoot();
738
+ try {
739
+ const initial = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
740
+ name: 'Automation Codex',
741
+ icon: 'Bot',
742
+ color: '#64748b'
743
+ }));
744
+ expect(initial.profile.description).toBeNull();
745
+ expect(initial.profile.role).toBeNull();
746
+ const currentProfile = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
747
+ workspaceId: 'workspace-1',
748
+ resolveProfile: false,
749
+ authAiProfileName: 'Automation Codex',
750
+ authAiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
751
+ authAiProfileRole: 'Coding Agent',
752
+ authAiProfileProvider: 'OpenAI',
753
+ authAiProfileModel: 'Codex',
754
+ authAiProfileSurfaceType: 'coding_tool'
755
+ }));
756
+ expect(currentProfile.profile).toEqual(expect.objectContaining({
757
+ id: initial.profile.id,
758
+ name: 'Automation Codex',
759
+ description: 'OpenAI Codex coding agent connected through MCP.',
760
+ role: 'Coding Agent',
761
+ provider: 'OpenAI',
762
+ model: 'Codex',
763
+ surfaceType: 'coding_tool'
764
+ }));
765
+ const reloadedCore = new TaskforceCore({
766
+ projectRoot,
767
+ storagePath: path.join(projectRoot, '.taskforce')
768
+ });
769
+ expect(reloadedCore.listAiProfiles('workspace-1')).toEqual([
770
+ expect.objectContaining({
771
+ id: initial.profile.id,
772
+ name: 'Automation Codex',
773
+ description: 'OpenAI Codex coding agent connected through MCP.',
774
+ role: 'Coding Agent',
775
+ provider: 'OpenAI',
776
+ model: 'Codex',
777
+ surfaceType: 'coding_tool'
778
+ })
779
+ ]);
780
+ reloadedCore.close();
781
+ }
782
+ finally {
783
+ fs.rmSync(projectRoot, { recursive: true, force: true });
784
+ }
785
+ });
736
786
  it('allows mutating MCP tools in a fresh runtime when profileToken is passed', async () => {
737
787
  const projectRoot = createProjectRoot();
738
788
  try {
@@ -816,6 +866,60 @@ describe('Taskforce MCP tool descriptions', () => {
816
866
  fs.rmSync(projectRoot, { recursive: true, force: true });
817
867
  }
818
868
  });
869
+ it('blocks cloud auto-bind when the workspace has no remaining ai profile seats', async () => {
870
+ const projectRoot = createProjectRoot();
871
+ try {
872
+ const core = new TaskforceCore({
873
+ projectRoot,
874
+ storagePath: path.join(projectRoot, '.taskforce')
875
+ });
876
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
877
+ core.ensureBootstrapUserAccess({
878
+ userId: 'owner-u1',
879
+ workspaceId: 'workspace-1',
880
+ role: 'owner',
881
+ email: 'owner@example.com'
882
+ });
883
+ core.updatePlanVersionFeatures({
884
+ planVersionId: 'free-v1',
885
+ features: [
886
+ { featureKey: 'collaboration.ai_profiles', access: 'limited', config: { maxAiProfiles: 1 } }
887
+ ]
888
+ });
889
+ core.updateAccountEntitlement({
890
+ accountId: 'owner-u1',
891
+ planVersionId: 'free-v1',
892
+ entitlementState: 'active',
893
+ actorUserId: 'system-admin',
894
+ actorRole: 'system_admin'
895
+ });
896
+ core.resolveAiProfile({
897
+ workspaceId: 'workspace-1',
898
+ name: 'Existing Automation Bot'
899
+ });
900
+ core.close();
901
+ const result = await callTool(projectRoot, 'cloud', 'create_task', {
902
+ title: 'Blocked automation write'
903
+ }, {
904
+ workspaceId: 'workspace-1',
905
+ resolveProfile: false,
906
+ authAiProfileName: 'Automation Codex'
907
+ });
908
+ expect(result.isError).toBe(true);
909
+ const payload = parseToolJson(result);
910
+ expect(payload.code).toBe('AI_PROFILE_LIMIT_REACHED');
911
+ expect(payload.error).toContain('No AI seats are available for this workspace');
912
+ expect(payload.details).toEqual({
913
+ workspaceId: 'workspace-1',
914
+ limit: 1,
915
+ used: 1,
916
+ remaining: 0
917
+ });
918
+ }
919
+ finally {
920
+ fs.rmSync(projectRoot, { recursive: true, force: true });
921
+ }
922
+ });
819
923
  it('rejects invalid profileToken values on mutating MCP tools', async () => {
820
924
  const projectRoot = createProjectRoot();
821
925
  try {
@@ -876,6 +980,61 @@ describe('Taskforce MCP tool descriptions', () => {
876
980
  fs.rmSync(projectRoot, { recursive: true, force: true });
877
981
  }
878
982
  });
983
+ it('surfaces ai profile seat limits when an invalid per-call token falls back to a configured AI name', async () => {
984
+ const projectRoot = createProjectRoot();
985
+ try {
986
+ const core = new TaskforceCore({
987
+ projectRoot,
988
+ storagePath: path.join(projectRoot, '.taskforce')
989
+ });
990
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
991
+ core.ensureBootstrapUserAccess({
992
+ userId: 'owner-u1',
993
+ workspaceId: 'workspace-1',
994
+ role: 'owner',
995
+ email: 'owner@example.com'
996
+ });
997
+ core.updatePlanVersionFeatures({
998
+ planVersionId: 'free-v1',
999
+ features: [
1000
+ { featureKey: 'collaboration.ai_profiles', access: 'limited', config: { maxAiProfiles: 1 } }
1001
+ ]
1002
+ });
1003
+ core.updateAccountEntitlement({
1004
+ accountId: 'owner-u1',
1005
+ planVersionId: 'free-v1',
1006
+ entitlementState: 'active',
1007
+ actorUserId: 'system-admin',
1008
+ actorRole: 'system_admin'
1009
+ });
1010
+ core.resolveAiProfile({
1011
+ workspaceId: 'workspace-1',
1012
+ name: 'Existing Automation Bot'
1013
+ });
1014
+ core.close();
1015
+ const result = await callTool(projectRoot, 'cloud', 'create_task', {
1016
+ title: 'Recovered per-call token write',
1017
+ profileToken: 'tfp_invalid_token',
1018
+ }, {
1019
+ workspaceId: 'workspace-1',
1020
+ resolveProfile: false,
1021
+ authAiProfileName: 'Automation Codex'
1022
+ });
1023
+ expect(result.isError).toBe(true);
1024
+ const payload = parseToolJson(result);
1025
+ expect(payload.code).toBe('AI_PROFILE_LIMIT_REACHED');
1026
+ expect(payload.error).toContain('No AI seats are available for this workspace');
1027
+ expect(payload.details).toEqual({
1028
+ workspaceId: 'workspace-1',
1029
+ limit: 1,
1030
+ used: 1,
1031
+ remaining: 0
1032
+ });
1033
+ }
1034
+ finally {
1035
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1036
+ }
1037
+ });
879
1038
  it('claims unassigned tasks and marks them in progress through start_task', async () => {
880
1039
  const projectRoot = createProjectRoot();
881
1040
  try {
@@ -1667,6 +1826,40 @@ describe('Taskforce MCP tool descriptions', () => {
1667
1826
  fs.rmSync(projectRoot, { recursive: true, force: true });
1668
1827
  }
1669
1828
  });
1829
+ it('returns summary-only rows from list_tasks json output', async () => {
1830
+ const projectRoot = createProjectRoot();
1831
+ try {
1832
+ const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
1833
+ title: 'List summary task',
1834
+ description: 'Verbose description that should not appear in list rows'
1835
+ }));
1836
+ await callTool(projectRoot, 'local', 'replace_task_checklist', {
1837
+ taskId: created.referenceLabel,
1838
+ items: [
1839
+ { title: 'Checklist item one', isCompleted: false }
1840
+ ]
1841
+ });
1842
+ await callTool(projectRoot, 'local', 'add_comment', {
1843
+ id: created.referenceLabel,
1844
+ text: 'Verbose comment that should not appear in list rows',
1845
+ author: 'ai-profile-019d15c5-6ad3-7bf0-9b9e-fa9c366c3ff4'
1846
+ });
1847
+ const listed = parseToolJson(await callTool(projectRoot, 'local', 'list_tasks', {
1848
+ format: 'json'
1849
+ }));
1850
+ const row = listed.find((task) => task.referenceLabel === created.referenceLabel);
1851
+ expect(row).toBeTruthy();
1852
+ expect(row.description).toBeUndefined();
1853
+ expect(row.comments).toBeUndefined();
1854
+ expect(row.taskEvents).toBeUndefined();
1855
+ expect(row.activity).toBeUndefined();
1856
+ expect(row.attachments).toBeUndefined();
1857
+ expect(row.checklistItems).toBeUndefined();
1858
+ }
1859
+ finally {
1860
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1861
+ }
1862
+ });
1670
1863
  it('reads task resources by T-XXX reference and exposes version/scope in config resource', async () => {
1671
1864
  const projectRoot = createProjectRoot();
1672
1865
  try {
@@ -1961,8 +2154,45 @@ describe('Taskforce MCP tool descriptions', () => {
1961
2154
  fs.rmSync(projectRoot, { recursive: true, force: true });
1962
2155
  }
1963
2156
  });
1964
- it('rejects sourcePath for save_task_attachment in cloud runtime', async () => {
2157
+ it('allows save_task_attachment sourcePath in cloud runtime for approved agent workspace roots', async () => {
1965
2158
  const projectRoot = createProjectRoot();
2159
+ const agentWorkspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-agent-workspace-'));
2160
+ const previousRoots = process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
2161
+ try {
2162
+ const core = new TaskforceCore({
2163
+ projectRoot,
2164
+ storagePath: path.join(projectRoot, '.taskforce')
2165
+ });
2166
+ const task = core.createTask({ title: 'Cloud sourcePath allow test' }, { workspaceId: 'workspace-1' });
2167
+ core.close();
2168
+ const sourcePath = path.join(agentWorkspaceRoot, 'implementation-plan.md');
2169
+ fs.writeFileSync(sourcePath, '# Recovered implementation plan', 'utf8');
2170
+ process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = agentWorkspaceRoot;
2171
+ await callTool(projectRoot, 'cloud', 'save_task_attachment', {
2172
+ id: task.id,
2173
+ filename: 'implementation-plan.md',
2174
+ sourcePath
2175
+ });
2176
+ const readBack = parseToolJson(await callTool(projectRoot, 'cloud', 'read_task_attachment', {
2177
+ taskId: task.id,
2178
+ attachmentId: parseToolJson(await callTool(projectRoot, 'cloud', 'get_task_attachments', { id: task.id })).attachments[0]?.attachmentId
2179
+ }));
2180
+ expect(readBack.content).toContain('# Recovered implementation plan');
2181
+ }
2182
+ finally {
2183
+ if (previousRoots === undefined) {
2184
+ delete process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS;
2185
+ }
2186
+ else {
2187
+ process.env.TASKFORCE_ALLOWED_ATTACHMENT_SOURCE_ROOTS = previousRoots;
2188
+ }
2189
+ fs.rmSync(projectRoot, { recursive: true, force: true });
2190
+ fs.rmSync(agentWorkspaceRoot, { recursive: true, force: true });
2191
+ }
2192
+ });
2193
+ it('rejects save_task_attachment sourcePath in cloud runtime for unapproved roots', async () => {
2194
+ const projectRoot = createProjectRoot();
2195
+ const externalRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-cloud-unapproved-'));
1966
2196
  try {
1967
2197
  const core = new TaskforceCore({
1968
2198
  projectRoot,
@@ -1970,16 +2200,18 @@ describe('Taskforce MCP tool descriptions', () => {
1970
2200
  });
1971
2201
  const task = core.createTask({ title: 'Cloud sourcePath reject test' }, { workspaceId: 'workspace-1' });
1972
2202
  core.close();
2203
+ const sourcePath = path.join(externalRoot, 'outside.md');
2204
+ fs.writeFileSync(sourcePath, '# Outside approved roots', 'utf8');
1973
2205
  const result = await callTool(projectRoot, 'cloud', 'save_task_attachment', {
1974
2206
  id: task.id,
1975
- filename: 'copy.txt',
1976
- sourcePath: 'docs/source.txt'
2207
+ filename: 'outside.md',
2208
+ sourcePath
1977
2209
  });
1978
- expect(result.content?.[0]?.text).toContain('does not support sourcePath for save_task_attachment yet');
1979
- expect(result.content?.[0]?.text).toContain('resend the attachment using inline content instead');
2210
+ expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
1980
2211
  }
1981
2212
  finally {
1982
2213
  fs.rmSync(projectRoot, { recursive: true, force: true });
2214
+ fs.rmSync(externalRoot, { recursive: true, force: true });
1983
2215
  }
1984
2216
  });
1985
2217
  it('accepts task references for save_task_attachment', async () => {
@@ -2331,7 +2563,7 @@ describe('Taskforce MCP tool descriptions', () => {
2331
2563
  filename: 'outside.md',
2332
2564
  sourcePath: externalSourcePath
2333
2565
  });
2334
- expect(result.content?.[0]?.text).toContain('sourcePath must be within project root or a Taskforce-managed attachment source');
2566
+ expect(result.content?.[0]?.text).toContain('sourcePath must be within project root, a Taskforce-managed attachment source, or an approved agent workspace root');
2335
2567
  }
2336
2568
  finally {
2337
2569
  fs.rmSync(projectRoot, { recursive: true, force: true });
@@ -0,0 +1,41 @@
1
+ import type { WorkspaceAssetEntry, WorkspaceAssetKind, WorkspaceAssetStore } from '../storage/workspaceAssetStore.js';
2
+ import type { McpTaskAttachmentDescriptor, TaskAttachmentRecord } from './taskAttachmentTypes.js';
3
+ export interface TaskAttachmentHelpersDeps {
4
+ activeWorkspaceId: string;
5
+ workspaceAssetStore: WorkspaceAssetStore | null;
6
+ getImageReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
7
+ getDocumentReferenceLabel: (asset: WorkspaceAssetEntry | null | undefined) => string | null;
8
+ inferMimeTypeFromPath: (filePath: string) => string;
9
+ inferAssetKind: (filePath: string, mimeType: string) => WorkspaceAssetKind;
10
+ isTextReadableMimeType: (mimeTypeRaw: string) => boolean;
11
+ isValidCanonicalDocumentAsset: (params: Pick<WorkspaceAssetEntry, 'kind' | 'storageKey' | 'originalFilename' | 'mimeType'>) => boolean;
12
+ canonicalAssetHelpers: {
13
+ getAttachmentAssetRecord: (attachment: TaskAttachmentRecord) => WorkspaceAssetEntry | null;
14
+ normalizeAttachmentStorageKey: (pathValue: unknown, fsPathValue: unknown) => string | null;
15
+ buildSyntheticAttachmentId: (taskId: string, attachment: TaskAttachmentRecord) => string;
16
+ toTaskAttachmentRecord: (attachment: string | TaskAttachmentRecord) => TaskAttachmentRecord;
17
+ };
18
+ documentHelpers: {
19
+ buildCanonicalDocumentAttachmentHint: (asset: {
20
+ assetId: string;
21
+ documentId: string | null;
22
+ referenceNumber?: number | null;
23
+ mimeType?: string | null;
24
+ }) => {
25
+ documentId: string | null;
26
+ preferredNextTool: string;
27
+ preferredWorkflow: string;
28
+ suggestedArgs: {
29
+ documentId: string;
30
+ };
31
+ guidance: string;
32
+ } | null;
33
+ };
34
+ }
35
+ export declare function createTaskAttachmentHelpers(deps: TaskAttachmentHelpersDeps): {
36
+ describeTaskAttachment: (taskId: string, attachmentInput: string | TaskAttachmentRecord) => McpTaskAttachmentDescriptor;
37
+ listTaskAttachmentDescriptors: (task: {
38
+ id: string;
39
+ attachments?: Array<string | TaskAttachmentRecord> | null;
40
+ }) => McpTaskAttachmentDescriptor[];
41
+ };
@@ -0,0 +1,60 @@
1
+ export function createTaskAttachmentHelpers(deps) {
2
+ const isCanonicalDocumentAssetRecord = (asset) => deps.isValidCanonicalDocumentAsset(asset);
3
+ const describeTaskAttachment = (taskId, attachmentInput) => {
4
+ const attachment = deps.canonicalAssetHelpers.toTaskAttachmentRecord(attachmentInput);
5
+ const asset = deps.canonicalAssetHelpers.getAttachmentAssetRecord(attachment);
6
+ const storageKey = deps.canonicalAssetHelpers.normalizeAttachmentStorageKey(attachment.path, attachment.fsPath);
7
+ const assetId = String(attachment.assetId || asset?.assetId || '').trim() || undefined;
8
+ const source = (asset?.storageProvider === 'external' || /^https?:\/\//i.test(String(attachment.path || '').trim()))
9
+ ? 'external'
10
+ : 'canonical';
11
+ const mimeType = String(asset?.mimeType
12
+ || (storageKey && !/^https?:\/\//i.test(storageKey) ? deps.inferMimeTypeFromPath(storageKey) : '')
13
+ || deps.inferMimeTypeFromPath(String(attachment.originalFilename || attachment.displayName || attachment.caption || attachment.path || ''))).trim() || 'application/octet-stream';
14
+ const kind = asset?.kind || deps.inferAssetKind(storageKey || String(attachment.path || ''), mimeType);
15
+ const isCanonicalDocument = Boolean(asset && isCanonicalDocumentAssetRecord(asset)) || (!asset && kind === 'document');
16
+ const referenceNumber = typeof attachment.referenceNumber === 'number'
17
+ ? attachment.referenceNumber
18
+ : (typeof asset?.referenceNumber === 'number' ? asset.referenceNumber : null);
19
+ const referenceLabel = String(attachment.referenceLabel
20
+ || (kind === 'image' ? deps.getImageReferenceLabel(asset) : '')
21
+ || (isCanonicalDocument ? deps.getDocumentReferenceLabel(asset) : '')).trim() || null;
22
+ const readableAsText = source === 'canonical' && deps.isTextReadableMimeType(mimeType);
23
+ const canonicalDocumentHint = isCanonicalDocument && source === 'canonical' && asset
24
+ ? deps.documentHelpers.buildCanonicalDocumentAttachmentHint(asset)
25
+ : null;
26
+ return {
27
+ attachmentId: assetId || deps.canonicalAssetHelpers.buildSyntheticAttachmentId(taskId, attachment),
28
+ ...(assetId ? { assetId } : {}),
29
+ ...(referenceNumber ? { referenceNumber } : {}),
30
+ ...(referenceLabel ? { referenceLabel } : {}),
31
+ ...(canonicalDocumentHint?.documentId ? { documentId: canonicalDocumentHint.documentId } : {}),
32
+ path: String(attachment.path || '').trim(),
33
+ ...(storageKey && !/^https?:\/\//i.test(storageKey) ? { fsPath: storageKey } : (attachment.fsPath ? { fsPath: String(attachment.fsPath).trim() } : {})),
34
+ ...(attachment.caption ? { caption: String(attachment.caption).trim() } : {}),
35
+ ...(attachment.displayName ? { displayName: String(attachment.displayName).trim() } : {}),
36
+ ...(attachment.originalFilename ? { originalFilename: String(attachment.originalFilename).trim() } : {}),
37
+ kind: isCanonicalDocument ? 'document' : kind,
38
+ mimeType,
39
+ sizeBytes: asset ? Number(asset.sizeBytes || 0) : null,
40
+ ...(attachment.timestamp ? { timestamp: String(attachment.timestamp).trim() } : {}),
41
+ source,
42
+ readableAsText,
43
+ ...(canonicalDocumentHint ? {
44
+ preferredNextTool: canonicalDocumentHint.preferredNextTool,
45
+ preferredWorkflow: canonicalDocumentHint.preferredWorkflow,
46
+ suggestedArgs: canonicalDocumentHint.suggestedArgs,
47
+ guidance: canonicalDocumentHint.guidance,
48
+ } : {}),
49
+ };
50
+ };
51
+ const listTaskAttachmentDescriptors = (task) => {
52
+ if (!task || !Array.isArray(task.attachments))
53
+ return [];
54
+ return task.attachments.map((attachment) => describeTaskAttachment(task.id, attachment));
55
+ };
56
+ return {
57
+ describeTaskAttachment,
58
+ listTaskAttachmentDescriptors,
59
+ };
60
+ }
@@ -0,0 +1,37 @@
1
+ import type { WorkspaceAssetKind } from '../storage/workspaceAssetStore.js';
2
+ export type TaskAttachmentRecord = {
3
+ path: string;
4
+ fsPath?: string;
5
+ caption?: string;
6
+ displayName?: string;
7
+ originalFilename?: string;
8
+ timestamp?: string;
9
+ assetId?: string;
10
+ referenceNumber?: number | null;
11
+ referenceLabel?: string | null;
12
+ taskId?: string | null;
13
+ };
14
+ export type McpTaskAttachmentDescriptor = {
15
+ attachmentId: string;
16
+ assetId?: string;
17
+ referenceNumber?: number | null;
18
+ referenceLabel?: string | null;
19
+ documentId?: string | null;
20
+ path: string;
21
+ fsPath?: string;
22
+ caption?: string;
23
+ displayName?: string;
24
+ originalFilename?: string;
25
+ kind: WorkspaceAssetKind;
26
+ mimeType: string;
27
+ sizeBytes: number | null;
28
+ timestamp?: string;
29
+ source: 'canonical' | 'external';
30
+ readableAsText: boolean;
31
+ preferredNextTool?: string;
32
+ preferredWorkflow?: string;
33
+ suggestedArgs?: {
34
+ documentId: string;
35
+ };
36
+ guidance?: string;
37
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { DomainToolExecutor, RegisterMcpTool } from './toolCatalog.js';
2
+ export declare function registerTaskPlanningTools(registerTool: RegisterMcpTool, executeTool: DomainToolExecutor): void;