@taskforcehq/taskforce 0.3.301 → 0.3.303

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 (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import fs from 'fs';
3
3
  import os from 'os';
4
4
  import path from 'path';
@@ -7,61 +7,8 @@ import { MCP_TOOL_REGISTRY } from './toolRegistry';
7
7
  import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce';
8
8
  import { WorkspaceAssetStore } from '../storage/workspaceAssetStore';
9
9
  import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, APP_GATE_DEFINITIONS, } from '../runtime/appGates';
10
- function createProjectRoot() {
11
- const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-runtime-'));
12
- const core = new TaskforceCore({
13
- projectRoot,
14
- storagePath: path.join(projectRoot, '.taskforce')
15
- });
16
- try {
17
- Object.defineProperty(core, 'getAccountAccessStatus', {
18
- configurable: true,
19
- value: () => ({
20
- gate: 'ok',
21
- hasEntitlement: true,
22
- canAccessApp: true,
23
- canAccessPlans: true,
24
- signupMonetizationStrategy: 'none',
25
- planSelectionRequired: false,
26
- message: null,
27
- entitlement: null
28
- })
29
- });
30
- core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
31
- core.ensureBootstrapUserAccess({
32
- userId: 'owner-1',
33
- workspaceId: 'workspace-1',
34
- role: 'owner',
35
- email: 'owner-1@example.com'
36
- });
37
- }
38
- finally {
39
- core.close();
40
- }
41
- return projectRoot;
42
- }
43
- async function listTools(projectRoot, runtimeMode) {
44
- const runtime = createTaskforceMcpServer({
45
- projectRoot,
46
- runtimeMode,
47
- logStartup: false,
48
- auth: runtimeMode === 'cloud'
49
- ? { scopes: ['tasks:read', 'tasks:write', 'workspace:read'] }
50
- : undefined
51
- });
52
- const handler = runtime.server._requestHandlers.get('tools/list');
53
- if (!handler) {
54
- throw new Error('tools/list handler not registered');
55
- }
56
- return await handler({
57
- jsonrpc: '2.0',
58
- id: 'test-tools-list',
59
- method: 'tools/list',
60
- params: {}
61
- }, {});
62
- }
63
- async function callTool(projectRoot, runtimeMode, name, args, options) {
64
- const runtime = createTaskforceMcpServer({
10
+ function createRuntime(projectRoot, runtimeMode, options) {
11
+ return createTaskforceMcpServer({
65
12
  projectRoot,
66
13
  runtimeMode,
67
14
  workspaceId: options?.workspaceId,
@@ -73,6 +20,8 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
73
20
  actorType: 'oauth',
74
21
  aiProfileToken: options?.authAiProfileToken || null,
75
22
  aiProfileName: options?.authAiProfileName || null,
23
+ aiProfileIcon: options?.authAiProfileIcon || null,
24
+ aiProfileColor: options?.authAiProfileColor || null,
76
25
  aiProfileDescription: options?.authAiProfileDescription || null,
77
26
  aiProfileRole: options?.authAiProfileRole || null,
78
27
  aiProfileProvider: options?.authAiProfileProvider || null,
@@ -81,6 +30,8 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
81
30
  }
82
31
  : undefined
83
32
  });
33
+ }
34
+ async function callToolWithRuntime(runtime, name, args, options) {
84
35
  const handler = runtime.server._requestHandlers.get('tools/call');
85
36
  if (!handler) {
86
37
  throw new Error('tools/call handler not registered');
@@ -110,20 +61,71 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
110
61
  }
111
62
  }, {});
112
63
  }
113
- async function readResource(projectRoot, runtimeMode, uri, options) {
114
- const runtime = createTaskforceMcpServer({
64
+ async function subscribeResource(runtime, uri) {
65
+ const handler = runtime.server._requestHandlers.get('resources/subscribe');
66
+ if (!handler) {
67
+ throw new Error('resources/subscribe handler not registered');
68
+ }
69
+ await handler({
70
+ jsonrpc: '2.0',
71
+ id: `test-resources-subscribe-${uri}`,
72
+ method: 'resources/subscribe',
73
+ params: { uri }
74
+ }, {});
75
+ }
76
+ function createProjectRoot() {
77
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-mcp-runtime-'));
78
+ const core = new TaskforceCore({
115
79
  projectRoot,
116
- runtimeMode,
117
- workspaceId: options?.workspaceId,
118
- logStartup: false,
119
- auth: runtimeMode === 'cloud'
120
- ? {
121
- scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
122
- userId: options?.authUserId || 'owner-1',
123
- actorType: 'oauth'
124
- }
125
- : undefined
80
+ storagePath: path.join(projectRoot, '.taskforce')
81
+ });
82
+ try {
83
+ Object.defineProperty(core, 'getAccountAccessStatus', {
84
+ configurable: true,
85
+ value: () => ({
86
+ gate: 'ok',
87
+ hasEntitlement: true,
88
+ canAccessApp: true,
89
+ canAccessPlans: true,
90
+ planSelectionRequired: false,
91
+ message: null,
92
+ entitlement: null
93
+ })
94
+ });
95
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
96
+ core.ensureBootstrapUserAccess({
97
+ userId: 'owner-1',
98
+ workspaceId: 'workspace-1',
99
+ role: 'owner',
100
+ email: 'owner-1@example.com'
101
+ });
102
+ }
103
+ finally {
104
+ core.close();
105
+ }
106
+ return projectRoot;
107
+ }
108
+ async function listTools(projectRoot, runtimeMode) {
109
+ const runtime = createRuntime(projectRoot, runtimeMode);
110
+ const handler = runtime.server._requestHandlers.get('tools/list');
111
+ if (!handler) {
112
+ throw new Error('tools/list handler not registered');
113
+ }
114
+ return await handler({
115
+ jsonrpc: '2.0',
116
+ id: 'test-tools-list',
117
+ method: 'tools/list',
118
+ params: {}
119
+ }, {});
120
+ }
121
+ async function callTool(projectRoot, runtimeMode, name, args, options) {
122
+ const runtime = createRuntime(projectRoot, runtimeMode, options);
123
+ return await callToolWithRuntime(runtime, name, args, {
124
+ resolveProfile: options?.resolveProfile,
126
125
  });
126
+ }
127
+ async function readResource(projectRoot, runtimeMode, uri, options) {
128
+ const runtime = createRuntime(projectRoot, runtimeMode, options);
127
129
  const handler = runtime.server._requestHandlers.get('resources/read');
128
130
  if (!handler) {
129
131
  throw new Error('resources/read handler not registered');
@@ -136,19 +138,7 @@ async function readResource(projectRoot, runtimeMode, uri, options) {
136
138
  }, {});
137
139
  }
138
140
  async function listResources(projectRoot, runtimeMode, options) {
139
- const runtime = createTaskforceMcpServer({
140
- projectRoot,
141
- runtimeMode,
142
- workspaceId: options?.workspaceId,
143
- logStartup: false,
144
- auth: runtimeMode === 'cloud'
145
- ? {
146
- scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
147
- userId: options?.authUserId || 'owner-1',
148
- actorType: 'oauth'
149
- }
150
- : undefined
151
- });
141
+ const runtime = createRuntime(projectRoot, runtimeMode, options);
152
142
  const handler = runtime.server._requestHandlers.get('resources/list');
153
143
  if (!handler) {
154
144
  throw new Error('resources/list handler not registered');
@@ -302,6 +292,29 @@ describe('Taskforce MCP tool descriptions', () => {
302
292
  fs.rmSync(projectRoot, { recursive: true, force: true });
303
293
  }
304
294
  });
295
+ it('does not let TASKFORCE_TOOLS prune the runtime tool catalog', async () => {
296
+ const projectRoot = createProjectRoot();
297
+ const previousValue = process.env.TASKFORCE_TOOLS;
298
+ process.env.TASKFORCE_TOOLS = 'get_task';
299
+ try {
300
+ const result = await listTools(projectRoot, 'cloud');
301
+ const runtimeToolNames = result.tools.map((tool) => tool.name).sort();
302
+ const registryToolNames = Object.entries(MCP_TOOL_REGISTRY)
303
+ .filter(([, entry]) => entry.permission.kind !== 'local-only' && entry.permission.kind !== 'session-only')
304
+ .map(([name]) => name)
305
+ .sort();
306
+ expect(runtimeToolNames).toEqual(registryToolNames);
307
+ }
308
+ finally {
309
+ if (previousValue === undefined) {
310
+ delete process.env.TASKFORCE_TOOLS;
311
+ }
312
+ else {
313
+ process.env.TASKFORCE_TOOLS = previousValue;
314
+ }
315
+ fs.rmSync(projectRoot, { recursive: true, force: true });
316
+ }
317
+ });
305
318
  it('includes local-only guidance in local runtime tool descriptions', async () => {
306
319
  const projectRoot = createProjectRoot();
307
320
  try {
@@ -713,6 +726,8 @@ describe('Taskforce MCP tool descriptions', () => {
713
726
  model: 'Codex',
714
727
  surfaceType: 'coding_tool'
715
728
  }));
729
+ expect(currentProfile.bindingSource).toBe('connection_bootstrap');
730
+ expect(currentProfile.writeReady).toBe(true);
716
731
  }
717
732
  finally {
718
733
  fs.rmSync(projectRoot, { recursive: true, force: true });
@@ -788,6 +803,8 @@ describe('Taskforce MCP tool descriptions', () => {
788
803
  authAiProfileName: 'Automation Codex'
789
804
  }));
790
805
  expect(currentProfile.identityBound).toBe(true);
806
+ expect(currentProfile.bindingSource).toBe('connection_bootstrap');
807
+ expect(currentProfile.writeReady).toBe(true);
791
808
  expect(currentProfile.profileToken).toMatch(/^tfp_/);
792
809
  expect(currentProfile.profile).toEqual(expect.objectContaining({
793
810
  name: 'Automation Codex',
@@ -817,6 +834,27 @@ describe('Taskforce MCP tool descriptions', () => {
817
834
  fs.rmSync(projectRoot, { recursive: true, force: true });
818
835
  }
819
836
  });
837
+ it('reports token_lookup binding when get_current_profile is inspected with an explicit profileToken', async () => {
838
+ const projectRoot = createProjectRoot();
839
+ try {
840
+ const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
841
+ name: 'Codex'
842
+ }, { resolveProfile: false }));
843
+ const payload = parseToolJson(await callTool(projectRoot, 'local', 'get_current_profile', {
844
+ profileToken: resolved.profile.profileToken,
845
+ }, { resolveProfile: false }));
846
+ expect(payload.identityBound).toBe(false);
847
+ expect(payload.bindingSource).toBe('token_lookup');
848
+ expect(payload.writeReady).toBe(true);
849
+ expect(payload.profile).toEqual(expect.objectContaining({
850
+ id: resolved.profile.id,
851
+ profileToken: resolved.profile.profileToken,
852
+ }));
853
+ }
854
+ finally {
855
+ fs.rmSync(projectRoot, { recursive: true, force: true });
856
+ }
857
+ });
820
858
  it('recovers from an invalid per-call profileToken when the connection has a valid AI name', async () => {
821
859
  const projectRoot = createProjectRoot();
822
860
  try {
@@ -1214,6 +1252,8 @@ describe('Taskforce MCP tool descriptions', () => {
1214
1252
  }
1215
1253
  }, {}));
1216
1254
  expect(payload.identityBound).toBe(true);
1255
+ expect(payload.bindingSource).toBe('session');
1256
+ expect(payload.writeReady).toBe(true);
1217
1257
  expect(payload.profile).toEqual(expect.objectContaining({
1218
1258
  id: resolved.profile.id,
1219
1259
  username: resolved.profile.username,
@@ -1235,6 +1275,8 @@ describe('Taskforce MCP tool descriptions', () => {
1235
1275
  resolveProfile: false
1236
1276
  }));
1237
1277
  expect(payload.identityBound).toBe(false);
1278
+ expect(payload.bindingSource).toBe('none');
1279
+ expect(payload.writeReady).toBe(false);
1238
1280
  expect(payload.profile).toBeNull();
1239
1281
  expect(payload.taskFilter).toBeNull();
1240
1282
  expect(payload.nextTool).toBe('resolve_profile');
@@ -1251,13 +1293,29 @@ describe('Taskforce MCP tool descriptions', () => {
1251
1293
  const payload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
1252
1294
  workspaceId: 'workspace-1',
1253
1295
  resolveProfile: false,
1254
- authAiProfileName: 'Automation Codex'
1296
+ authAiProfileName: 'Automation Codex',
1297
+ authAiProfileIcon: 'Code2',
1298
+ authAiProfileColor: '#10a37f',
1299
+ authAiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
1300
+ authAiProfileRole: 'Coding Agent',
1301
+ authAiProfileProvider: 'OpenAI',
1302
+ authAiProfileModel: 'Codex',
1303
+ authAiProfileSurfaceType: 'coding_tool'
1255
1304
  }));
1256
1305
  expect(payload.identityBound).toBe(true);
1306
+ expect(payload.bindingSource).toBe('connection_bootstrap');
1307
+ expect(payload.writeReady).toBe(true);
1257
1308
  expect(payload.profile).toEqual(expect.objectContaining({
1258
1309
  name: 'Automation Codex',
1259
1310
  username: 'automation-codex',
1260
- profileToken: expect.stringMatching(/^tfp_/)
1311
+ profileToken: expect.stringMatching(/^tfp_/),
1312
+ icon: 'Code2',
1313
+ color: '#10a37f',
1314
+ description: 'OpenAI Codex coding agent connected through MCP.',
1315
+ role: 'Coding Agent',
1316
+ provider: 'OpenAI',
1317
+ model: 'Codex',
1318
+ surfaceType: 'coding_tool'
1261
1319
  }));
1262
1320
  expect(payload.taskFilter).toEqual({ assignee: payload.profile.id });
1263
1321
  expect(payload.nextTool).toBe('list_tasks');
@@ -1453,6 +1511,150 @@ describe('Taskforce MCP tool descriptions', () => {
1453
1511
  fs.rmSync(projectRoot, { recursive: true, force: true });
1454
1512
  }
1455
1513
  });
1514
+ it('builds workstream listings from one historical task snapshot and returns task summaries', async () => {
1515
+ const projectRoot = createProjectRoot();
1516
+ const core = new TaskforceCore({
1517
+ projectRoot,
1518
+ storagePath: path.join(projectRoot, '.taskforce')
1519
+ });
1520
+ try {
1521
+ const category = core.getCategories()[0]?.value;
1522
+ const type = core.getTypes()[0]?.value;
1523
+ const firstWorkstream = core.createWorkstream({ title: 'Workstream one' }, 'workspace-1');
1524
+ const secondWorkstream = core.createWorkstream({ title: 'Workstream two' }, 'workspace-1');
1525
+ core.createTask({
1526
+ title: 'Linked task one',
1527
+ category,
1528
+ type,
1529
+ workstreamId: firstWorkstream.id,
1530
+ }, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
1531
+ core.createTask({
1532
+ title: 'Linked task two',
1533
+ category,
1534
+ type,
1535
+ workstreamId: secondWorkstream.id,
1536
+ }, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
1537
+ const archivedTask = core.createTask({
1538
+ title: 'Archived linked task',
1539
+ category,
1540
+ type,
1541
+ workstreamId: firstWorkstream.id,
1542
+ status: 'done',
1543
+ }, { workspaceId: 'workspace-1', actorRef: 'owner-1' });
1544
+ core.archiveTask(archivedTask.id, false, undefined, 'workspace-1', { actorRef: 'owner-1' });
1545
+ }
1546
+ finally {
1547
+ core.close();
1548
+ }
1549
+ const listTasksSpy = vi.spyOn(TaskforceCore.prototype, 'listTasks');
1550
+ const listArchiveSpy = vi.spyOn(TaskforceCore.prototype, 'listArchive');
1551
+ try {
1552
+ const beforeListTasksCalls = listTasksSpy.mock.calls.length;
1553
+ const beforeListArchiveCalls = listArchiveSpy.mock.calls.length;
1554
+ const payload = parseToolJson(await callTool(projectRoot, 'local', 'list_workstreams', {}));
1555
+ const firstWorkstreamPayload = payload.workstreams.find((entry) => entry.workstream.title === 'Workstream one');
1556
+ const linkedTask = firstWorkstreamPayload?.tasks.find((task) => task.title === 'Archived linked task');
1557
+ expect(listTasksSpy.mock.calls.length - beforeListTasksCalls).toBe(1);
1558
+ expect(listArchiveSpy.mock.calls.length - beforeListArchiveCalls).toBe(1);
1559
+ expect(linkedTask).toMatchObject({
1560
+ title: 'Archived linked task',
1561
+ workstreamId: firstWorkstreamPayload?.workstream.id,
1562
+ status: 'done',
1563
+ });
1564
+ expect(linkedTask.description).toBeUndefined();
1565
+ expect(linkedTask.comments).toBeUndefined();
1566
+ expect(linkedTask.activity).toBeUndefined();
1567
+ expect(linkedTask.attachments).toBeUndefined();
1568
+ expect(linkedTask.checklistItems).toBeUndefined();
1569
+ }
1570
+ finally {
1571
+ listTasksSpy.mockRestore();
1572
+ listArchiveSpy.mockRestore();
1573
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1574
+ }
1575
+ });
1576
+ it('queues bulk resource invalidations instead of awaiting them in the write path', async () => {
1577
+ const projectRoot = createProjectRoot();
1578
+ const runtime = createRuntime(projectRoot, 'local');
1579
+ const resourceUri = `taskforce://${path.basename(projectRoot)}/tasks`;
1580
+ const sendResourceUpdatedSpy = vi
1581
+ .spyOn(runtime.server, 'sendResourceUpdated')
1582
+ .mockImplementation(async () => {
1583
+ await new Promise((resolve) => setTimeout(resolve, 25));
1584
+ });
1585
+ try {
1586
+ await subscribeResource(runtime, resourceUri);
1587
+ const created = parseToolJson(await callToolWithRuntime(runtime, 'create_task', {
1588
+ title: 'Queued invalidation task'
1589
+ }));
1590
+ expect(created.title).toBe('Queued invalidation task');
1591
+ expect(sendResourceUpdatedSpy).not.toHaveBeenCalled();
1592
+ await new Promise((resolve) => setTimeout(resolve, 40));
1593
+ expect(sendResourceUpdatedSpy).toHaveBeenCalledTimes(1);
1594
+ expect(sendResourceUpdatedSpy).toHaveBeenCalledWith({ uri: resourceUri });
1595
+ }
1596
+ finally {
1597
+ sendResourceUpdatedSpy.mockRestore();
1598
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1599
+ }
1600
+ });
1601
+ it('reads the bulk tasks resource as summary-only rows', async () => {
1602
+ const projectRoot = createProjectRoot();
1603
+ try {
1604
+ const created = parseToolJson(await callTool(projectRoot, 'local', 'create_task', {
1605
+ title: 'Verbose task',
1606
+ description: 'This should stay off the bulk tasks resource'
1607
+ }));
1608
+ await callTool(projectRoot, 'local', 'add_comment', {
1609
+ id: created.referenceLabel,
1610
+ text: 'This comment should stay off the bulk tasks resource',
1611
+ author: 'ai-profile-019d15c5-6ad3-7bf0-9b9e-fa9c366c3ff4'
1612
+ });
1613
+ const taskResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/tasks`, {
1614
+ workspaceId: 'workspace-1',
1615
+ authUserId: 'owner-1'
1616
+ });
1617
+ const payload = JSON.parse(String(taskResource.contents[0]?.text || '{}'));
1618
+ const resourceTask = payload.tasks.find((task) => task.referenceLabel === created.referenceLabel);
1619
+ expect(resourceTask).toMatchObject({
1620
+ referenceLabel: created.referenceLabel,
1621
+ title: 'Verbose task',
1622
+ });
1623
+ expect(resourceTask.description).toBeUndefined();
1624
+ expect(resourceTask.comments).toBeUndefined();
1625
+ expect(resourceTask.activity).toBeUndefined();
1626
+ expect(resourceTask.attachments).toBeUndefined();
1627
+ expect(resourceTask.checklistItems).toBeUndefined();
1628
+ }
1629
+ finally {
1630
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1631
+ }
1632
+ });
1633
+ it('reads initiative listings with summary initiative and workstream records only', async () => {
1634
+ const projectRoot = createProjectRoot();
1635
+ try {
1636
+ const initiative = parseToolJson(await callTool(projectRoot, 'local', 'create_initiative', {
1637
+ title: 'Summary initiative',
1638
+ description: 'Verbose initiative details should stay off the bulk listing'
1639
+ }));
1640
+ await callTool(projectRoot, 'local', 'create_workstream', {
1641
+ title: 'Summary workstream',
1642
+ description: 'Verbose workstream details should stay off the bulk listing',
1643
+ initiativeId: initiative.initiative.referenceLabel
1644
+ });
1645
+ const initiatives = parseToolJson(await callTool(projectRoot, 'local', 'list_initiatives', {}));
1646
+ const listedInitiative = initiatives.initiatives.find((entry) => entry.initiative.referenceLabel === initiative.initiative.referenceLabel);
1647
+ const listedWorkstream = listedInitiative?.workstreams.find((entry) => entry.title === 'Summary workstream');
1648
+ expect(listedInitiative).toBeTruthy();
1649
+ expect(listedInitiative.initiative.description).toBeUndefined();
1650
+ expect(listedWorkstream).toBeTruthy();
1651
+ expect(listedWorkstream.description).toBeUndefined();
1652
+ expect(listedWorkstream.taskIds).toBeUndefined();
1653
+ }
1654
+ finally {
1655
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1656
+ }
1657
+ });
1456
1658
  it('creates MCP tasks as unassigned when no explicit assignee is provided', async () => {
1457
1659
  const projectRoot = createProjectRoot();
1458
1660
  try {
@@ -1494,6 +1696,42 @@ describe('Taskforce MCP tool descriptions', () => {
1494
1696
  fs.rmSync(projectRoot, { recursive: true, force: true });
1495
1697
  }
1496
1698
  });
1699
+ it('returns summary-shaped task entries from the shared tasks resource', async () => {
1700
+ const projectRoot = createProjectRoot();
1701
+ try {
1702
+ const core = new TaskforceCore({
1703
+ projectRoot,
1704
+ storagePath: path.join(projectRoot, '.taskforce')
1705
+ });
1706
+ core.createTask({
1707
+ title: 'Resource summary task',
1708
+ description: 'Verbose task description',
1709
+ comments: [{ id: 'comment-1', author: 'user', text: 'Hello', timestamp: '2026-04-13T01:00:00.000Z' }],
1710
+ attachments: [{ name: 'note.md', path: 'notes/note.md' }],
1711
+ checklistItems: [{ id: 'item-1', taskId: 'temp', title: 'Check me', isCompleted: false, order: 0, createdAt: '2026-04-13T01:00:00.000Z' }],
1712
+ }, {
1713
+ workspaceId: 'workspace-1',
1714
+ actorRef: 'owner-1'
1715
+ });
1716
+ core.close();
1717
+ const tasksResource = await readResource(projectRoot, 'cloud', `taskforce://${path.basename(projectRoot)}/tasks`, {
1718
+ workspaceId: 'workspace-1',
1719
+ authUserId: 'owner-1'
1720
+ });
1721
+ const payload = JSON.parse(String(tasksResource.contents[0]?.text || '{}'));
1722
+ const task = payload.tasks.find((entry) => entry.title === 'Resource summary task');
1723
+ expect(task).toBeDefined();
1724
+ expect(task.description).toBeUndefined();
1725
+ expect(task.comments).toBeUndefined();
1726
+ expect(task.taskEvents).toBeUndefined();
1727
+ expect(task.activity).toBeUndefined();
1728
+ expect(task.attachments).toBeUndefined();
1729
+ expect(task.checklistItems).toBeUndefined();
1730
+ }
1731
+ finally {
1732
+ fs.rmSync(projectRoot, { recursive: true, force: true });
1733
+ }
1734
+ });
1497
1735
  it('persists save_task_attachment through canonical task attachments', async () => {
1498
1736
  const projectRoot = createProjectRoot();
1499
1737
  try {
@@ -25,6 +25,7 @@ describe('billing schema parity', () => {
25
25
  expect(names.has('user_billing')).toBe(true);
26
26
  expect(names.has('stripe_webhook_events')).toBe(true);
27
27
  expect(names.has('stripe_webhook_failures')).toBe(true);
28
+ expect(names.has('billing_checkout_attempts')).toBe(true);
28
29
  expect(names.has('billing_config')).toBe(true);
29
30
  expect(names.has('billing_price_mappings')).toBe(true);
30
31
  }
@@ -101,9 +102,11 @@ describe('billing schema parity', () => {
101
102
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS user_billing');
102
103
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS stripe_webhook_events');
103
104
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS stripe_webhook_failures');
105
+ expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_checkout_attempts');
104
106
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_config');
105
107
  expect(sql).toContain('CREATE TABLE IF NOT EXISTS billing_price_mappings');
106
108
  expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_purge_after');
109
+ expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_user_status');
107
110
  expect(sql).toContain('CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_plan');
108
111
  expect(sql).toContain('CREATE UNIQUE INDEX IF NOT EXISTS idx_billing_price_mappings_active_interval');
109
112
  });
@@ -122,9 +122,13 @@ export function ensureAssigneeTaskSchema(db) {
122
122
  export function ensureTaskReferenceSchema(db) {
123
123
  const columns = db.prepare(`PRAGMA table_info(tasks)`).all();
124
124
  const hasReferenceNumberColumn = columns.some((c) => c.name === 'reference_number');
125
+ const hasLocalReferenceNumberColumn = columns.some((c) => c.name === 'local_reference_number');
125
126
  if (!hasReferenceNumberColumn) {
126
127
  db.exec(`ALTER TABLE tasks ADD COLUMN reference_number INTEGER`);
127
128
  }
129
+ if (!hasLocalReferenceNumberColumn) {
130
+ db.exec(`ALTER TABLE tasks ADD COLUMN local_reference_number INTEGER`);
131
+ }
128
132
  db.exec(`
129
133
  CREATE TABLE IF NOT EXISTS workspace_task_reference_sequences (
130
134
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -132,6 +136,12 @@ export function ensureTaskReferenceSchema(db) {
132
136
  next_reference_number INTEGER NOT NULL DEFAULT 0,
133
137
  PRIMARY KEY (tenant_id, workspace_id)
134
138
  );
139
+ CREATE TABLE IF NOT EXISTS workspace_local_task_reference_sequences (
140
+ tenant_id TEXT NOT NULL DEFAULT 'default',
141
+ workspace_id TEXT NOT NULL DEFAULT 'default',
142
+ next_reference_number INTEGER NOT NULL DEFAULT 0,
143
+ PRIMARY KEY (tenant_id, workspace_id)
144
+ );
135
145
  `);
136
146
  const workspaceRows = db.prepare(`
137
147
  SELECT DISTINCT
@@ -141,7 +151,7 @@ export function ensureTaskReferenceSchema(db) {
141
151
  ORDER BY tenant_id ASC, workspace_id ASC
142
152
  `).all();
143
153
  const selectRows = db.prepare(`
144
- SELECT id, reference_number, created_at
154
+ SELECT id, reference_number, local_reference_number, created_at
145
155
  FROM tasks
146
156
  WHERE tenant_id = ? AND workspace_id = ?
147
157
  ORDER BY created_at ASC, id ASC
@@ -171,11 +181,20 @@ export function ensureTaskReferenceSchema(db) {
171
181
  const currentReferenceNumber = Number.isFinite(rawReferenceNumber) && rawReferenceNumber > 0
172
182
  ? Math.floor(rawReferenceNumber)
173
183
  : null;
184
+ const rawLocalReferenceNumber = Number(row.local_reference_number);
185
+ const currentLocalReferenceNumber = Number.isFinite(rawLocalReferenceNumber) && rawLocalReferenceNumber > 0
186
+ ? Math.floor(rawLocalReferenceNumber)
187
+ : null;
174
188
  if (currentReferenceNumber && !seen.has(currentReferenceNumber)) {
175
189
  seen.add(currentReferenceNumber);
176
190
  nextReferenceNumber = Math.max(nextReferenceNumber, currentReferenceNumber);
177
191
  continue;
178
192
  }
193
+ // Provisional local references are intentionally non-canonical and should
194
+ // not be promoted to T-### during schema repair or restart migrations.
195
+ if (!currentReferenceNumber && currentLocalReferenceNumber) {
196
+ continue;
197
+ }
179
198
  nextReferenceNumber += 1;
180
199
  seen.add(nextReferenceNumber);
181
200
  updateReference.run(nextReferenceNumber, tenantId, workspaceId, taskId);
@@ -185,6 +204,8 @@ export function ensureTaskReferenceSchema(db) {
185
204
  db.exec(`
186
205
  CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_workspace_reference
187
206
  ON tasks(tenant_id, workspace_id, reference_number);
207
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_workspace_local_reference
208
+ ON tasks(tenant_id, workspace_id, local_reference_number);
188
209
  `);
189
210
  }
190
211
  export function ensureWorkspaceSchema(db) {
@@ -215,6 +236,23 @@ export function ensureWorkspaceSchema(db) {
215
236
  updated_at TEXT
216
237
  );
217
238
 
239
+ CREATE TABLE IF NOT EXISTS user_avatar_upload_drafts (
240
+ draft_id TEXT PRIMARY KEY,
241
+ tenant_id TEXT NOT NULL DEFAULT 'default',
242
+ user_id TEXT NOT NULL,
243
+ storage_key TEXT NOT NULL,
244
+ mime_type TEXT NOT NULL,
245
+ original_filename TEXT NOT NULL,
246
+ size_bytes INTEGER NOT NULL DEFAULT 0,
247
+ storage_provider TEXT NOT NULL DEFAULT 'local',
248
+ finalized_at TEXT,
249
+ consumed_at TEXT,
250
+ discarded_at TEXT,
251
+ created_at TEXT NOT NULL,
252
+ updated_at TEXT NOT NULL,
253
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
254
+ );
255
+
218
256
  CREATE TABLE IF NOT EXISTS auth_tokens (
219
257
  id TEXT PRIMARY KEY,
220
258
  tenant_id TEXT NOT NULL DEFAULT 'default',
@@ -356,6 +394,20 @@ export function ensureWorkspaceSchema(db) {
356
394
  if (!hasWorkspaceIdColumn) {
357
395
  db.exec(`ALTER TABLE auth_tokens ADD COLUMN workspace_id TEXT`);
358
396
  }
397
+ if (db.provider === 'postgres') {
398
+ try {
399
+ db.exec(`ALTER TABLE user_avatar_upload_drafts ADD COLUMN IF NOT EXISTS consumed_at TEXT`);
400
+ }
401
+ catch {
402
+ // Best-effort repair for older dev databases.
403
+ }
404
+ }
405
+ else {
406
+ const avatarDraftColumns = db.prepare(`PRAGMA table_info(user_avatar_upload_drafts)`).all();
407
+ if (avatarDraftColumns.length > 0 && !avatarDraftColumns.some((c) => c.name === 'consumed_at')) {
408
+ db.exec(`ALTER TABLE user_avatar_upload_drafts ADD COLUMN consumed_at TEXT`);
409
+ }
410
+ }
359
411
  const mcpTokenColumns = db.prepare(`PRAGMA table_info(mcp_access_tokens)`).all();
360
412
  if (mcpTokenColumns.length > 0) {
361
413
  const requiredColumns = [
@@ -441,6 +493,18 @@ export function ensureBillingSchema(db) {
441
493
  created_at TEXT NOT NULL
442
494
  );
443
495
 
496
+ CREATE TABLE IF NOT EXISTS billing_checkout_attempts (
497
+ session_id TEXT PRIMARY KEY,
498
+ tenant_id TEXT NOT NULL DEFAULT 'default',
499
+ user_id TEXT NOT NULL,
500
+ plan_version_id TEXT NOT NULL,
501
+ billing_interval TEXT,
502
+ status TEXT NOT NULL DEFAULT 'open',
503
+ expires_at TEXT,
504
+ created_at TEXT NOT NULL,
505
+ updated_at TEXT NOT NULL
506
+ );
507
+
444
508
  CREATE TABLE IF NOT EXISTS billing_config (
445
509
  tenant_id TEXT PRIMARY KEY,
446
510
  config_json TEXT NOT NULL DEFAULT '{}',
@@ -468,6 +532,8 @@ export function ensureBillingSchema(db) {
468
532
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_tenant ON stripe_webhook_failures(tenant_id);
469
533
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_event ON stripe_webhook_failures(event_id);
470
534
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_purge_after ON stripe_webhook_failures(purge_after);
535
+ CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_user_status ON billing_checkout_attempts(tenant_id, user_id, status, created_at);
536
+ CREATE INDEX IF NOT EXISTS idx_billing_checkout_attempts_plan_status ON billing_checkout_attempts(tenant_id, plan_version_id, status, created_at);
471
537
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_plan ON billing_price_mappings(tenant_id, plan_version_id);
472
538
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_active ON billing_price_mappings(tenant_id, active);
473
539
  `);
@@ -671,6 +737,7 @@ export function ensureEntitlementsSchema(db) {
671
737
  updated_by TEXT,
672
738
  created_at TEXT NOT NULL,
673
739
  updated_at TEXT NOT NULL,
740
+ FOREIGN KEY (account_id) REFERENCES users(id) ON DELETE CASCADE,
674
741
  FOREIGN KEY (plan_version_id) REFERENCES plan_versions(plan_version_id)
675
742
  );
676
743
 
@@ -0,0 +1 @@
1
+ export {};