@taskforcehq/taskforce 0.3.302 → 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 (77) hide show
  1. package/dist/Taskforce.module.css +11 -0
  2. package/dist/TaskforceCore.js +59 -9
  3. package/dist/TaskforceCore.test.js +717 -14
  4. package/dist/components/task/TaskKanban.test.js +17 -0
  5. package/dist/components/views/PlansPage.d.ts +3 -1
  6. package/dist/components/views/PlansPage.js +110 -51
  7. package/dist/components/views/PlansPage.test.d.ts +1 -0
  8. package/dist/components/views/PlansPage.test.js +100 -0
  9. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  10. package/dist/components/views/StandaloneLayout.js +106 -151
  11. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  12. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  13. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  14. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  15. package/dist/core/EntitlementsPolicy.test.js +2 -2
  16. package/dist/core/GlobalSettingsService.js +78 -18
  17. package/dist/core/PlanEntitlementService.d.ts +29 -5
  18. package/dist/core/PlanEntitlementService.js +297 -219
  19. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  20. package/dist/core/SystemAdmin.test.js +186 -96
  21. package/dist/core/Taskforce.d.ts +51 -10
  22. package/dist/core/Taskforce.js +218 -49
  23. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  24. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  25. package/dist/core/types.d.ts +11 -12
  26. package/dist/hooks/useSyncOrchestrator.js +17 -3
  27. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  28. package/dist/hooks/useTaskforce.d.ts +3 -0
  29. package/dist/hooks/useTaskforce.js +9 -4
  30. package/dist/mcp/runtime.js +90 -29
  31. package/dist/mcp/runtime.test.js +272 -81
  32. package/dist/migrations/billingSchemaParity.test.js +3 -0
  33. package/dist/migrations/taskSchemaMigrations.js +15 -0
  34. package/dist/server/index.cookieProxy.test.js +1 -0
  35. package/dist/server/index.js +2 -0
  36. package/dist/server/index.test.js +2 -1
  37. package/dist/server/mockStripe.d.ts +1 -0
  38. package/dist/server/mockStripe.js +2 -0
  39. package/dist/server/routes/admin.js +80 -23
  40. package/dist/server/routes/auth.d.ts +1 -0
  41. package/dist/server/routes/auth.js +194 -53
  42. package/dist/server/routes/billing.js +506 -23
  43. package/dist/server/routes/billing.test.js +937 -45
  44. package/dist/server/routes/shared.js +5 -3
  45. package/dist/server/routes/sync.integration.test.js +160 -0
  46. package/dist/server/routes/sync.js +15 -5
  47. package/dist/server/routes.js +3 -3
  48. package/dist/server/routes.test.js +287 -76
  49. package/dist/sync/cloudSyncApi.d.ts +1 -0
  50. package/dist/sync/contentSyncState.d.ts +10 -0
  51. package/dist/sync/contentSyncState.js +277 -16
  52. package/dist/sync/syncService.d.ts +1 -0
  53. package/dist/sync/syncService.js +21 -8
  54. package/dist/sync/workspacePullFeed.d.ts +10 -0
  55. package/dist/sync/workspacePullFeed.js +111 -63
  56. package/dist/sync/workspacePullFeed.test.js +112 -0
  57. package/dist/ui/assets/{AgentsModule-JG5jc3he.js → AgentsModule-CpsTmrIW.js} +1 -1
  58. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  59. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-C3GyzrWm.js} +1 -1
  60. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  61. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  62. package/dist/ui/assets/{TaskSettings-Bqd4cCzW.js → TaskSettings-BOC5F6Ag.js} +1 -1
  63. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  64. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  65. package/dist/ui/assets/index-DneETxcu.css +1 -0
  66. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-pWocEipT.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  69. package/dist/utils/accountProfileSummaryCache.js +97 -0
  70. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  71. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  72. package/package.json +3 -3
  73. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  74. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  75. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  76. package/dist/utils/billingStatusCache.d.ts +0 -21
  77. package/dist/utils/billingStatusCache.js +0 -70
@@ -1664,7 +1664,7 @@ export function createTaskforceMcpServer(options = {}) {
1664
1664
  console.error(`[Taskforce MCP] Failed to send resources/updated for ${uri}: ${error.message}`);
1665
1665
  }
1666
1666
  }
1667
- async function notifyResourceMutation(options) {
1667
+ function collectResourceMutationUris(options) {
1668
1668
  const uris = new Set();
1669
1669
  if (options.includeConfig)
1670
1670
  uris.add(RESOURCE_CONFIG_URI);
@@ -1691,10 +1691,39 @@ export function createTaskforceMcpServer(options = {}) {
1691
1691
  uris.add(workstreamUriFor(workstreamId));
1692
1692
  }
1693
1693
  }
1694
+ return uris;
1695
+ }
1696
+ const pendingResourceInvalidations = new Set();
1697
+ let resourceInvalidationFlushHandle = null;
1698
+ function scheduleResourceInvalidationFlush() {
1699
+ if (resourceInvalidationFlushHandle)
1700
+ return;
1701
+ resourceInvalidationFlushHandle = setTimeout(() => {
1702
+ resourceInvalidationFlushHandle = null;
1703
+ void flushScheduledResourceInvalidations();
1704
+ }, 0);
1705
+ }
1706
+ async function flushScheduledResourceInvalidations() {
1707
+ if (pendingResourceInvalidations.size === 0)
1708
+ return;
1709
+ const uris = Array.from(pendingResourceInvalidations);
1710
+ pendingResourceInvalidations.clear();
1694
1711
  for (const uri of uris) {
1695
1712
  await notifyResourceUpdatedIfSubscribed(uri);
1696
1713
  }
1697
1714
  }
1715
+ function scheduleResourceMutation(options) {
1716
+ const uris = collectResourceMutationUris(options);
1717
+ if (uris.size === 0)
1718
+ return;
1719
+ for (const uri of uris) {
1720
+ pendingResourceInvalidations.add(uri);
1721
+ }
1722
+ scheduleResourceInvalidationFlush();
1723
+ }
1724
+ async function notifyResourceMutation(options) {
1725
+ scheduleResourceMutation(options);
1726
+ }
1698
1727
  function buildConfigResourcePayload() {
1699
1728
  return {
1700
1729
  project: {
@@ -1716,13 +1745,26 @@ export function createTaskforceMcpServer(options = {}) {
1716
1745
  };
1717
1746
  }
1718
1747
  function buildTasksResourcePayload() {
1719
- const tasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks;
1748
+ const tasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks
1749
+ .map((task) => buildPlanningTaskSummary(task));
1720
1750
  return {
1721
1751
  total: tasks.length,
1722
1752
  tasks,
1723
1753
  };
1724
1754
  }
1725
- function listHistoricalWorkspaceTasks() {
1755
+ function buildPlanningTaskSummary(task) {
1756
+ const { description, comments, taskEvents, activity, createdByActor, assigneeActor, attachments, checklistItems, ...rest } = task;
1757
+ return rest;
1758
+ }
1759
+ function buildPlanningWorkstreamSummary(workstream) {
1760
+ const { description, taskIds, ...rest } = workstream;
1761
+ return rest;
1762
+ }
1763
+ function buildPlanningInitiativeSummary(initiative) {
1764
+ const { description, ...rest } = initiative;
1765
+ return rest;
1766
+ }
1767
+ function buildHistoricalWorkspaceTaskSnapshot() {
1726
1768
  const activeTasks = core.listTasks(ACTIVE_WORKSPACE_ID).tasks;
1727
1769
  const archivedTasks = core.listArchive(ACTIVE_WORKSPACE_ID).archived;
1728
1770
  const byId = new Map();
@@ -1730,40 +1772,70 @@ export function createTaskforceMcpServer(options = {}) {
1730
1772
  byId.set(task.id, task);
1731
1773
  for (const task of archivedTasks)
1732
1774
  byId.set(task.id, task);
1733
- return Array.from(byId.values());
1775
+ const tasks = Array.from(byId.values());
1776
+ const tasksByWorkstreamId = new Map();
1777
+ for (const task of tasks) {
1778
+ const normalizedWorkstreamId = String(task.workstreamId || '').trim();
1779
+ if (!normalizedWorkstreamId)
1780
+ continue;
1781
+ const current = tasksByWorkstreamId.get(normalizedWorkstreamId) || [];
1782
+ current.push(buildPlanningTaskSummary(task));
1783
+ tasksByWorkstreamId.set(normalizedWorkstreamId, current);
1784
+ }
1785
+ return {
1786
+ tasks,
1787
+ tasksByWorkstreamId,
1788
+ };
1734
1789
  }
1735
- function buildInitiativeResourcePayload(initiative) {
1736
- const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID)
1737
- .filter((workstream) => workstream.initiativeId === initiative.id);
1738
- const tasks = listHistoricalWorkspaceTasks()
1739
- .filter((task) => workstreams.some((workstream) => workstream.id === task.workstreamId));
1790
+ function buildInitiativeResourcePayload(initiative, options) {
1791
+ const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
1792
+ const workstreams = options?.workstreamsByInitiativeId?.get(initiative.id)
1793
+ || core.listWorkstreams(ACTIVE_WORKSPACE_ID)
1794
+ .filter((workstream) => workstream.initiativeId === initiative.id);
1795
+ const tasks = workstreams.flatMap((workstream) => tasksByWorkstreamId.get(workstream.id) || []);
1740
1796
  return {
1741
- initiative,
1797
+ initiative: buildPlanningInitiativeSummary(initiative),
1742
1798
  workstreamCount: workstreams.length,
1743
1799
  taskCount: tasks.length,
1744
- workstreams,
1800
+ workstreams: workstreams.map((workstream) => buildPlanningWorkstreamSummary(workstream)),
1745
1801
  };
1746
1802
  }
1747
- function buildWorkstreamResourcePayload(workstream) {
1748
- const tasks = listHistoricalWorkspaceTasks()
1749
- .filter((task) => task.workstreamId === workstream.id);
1803
+ function buildWorkstreamResourcePayload(workstream, options) {
1804
+ const tasksByWorkstreamId = options?.tasksByWorkstreamId || buildHistoricalWorkspaceTaskSnapshot().tasksByWorkstreamId;
1805
+ const tasks = tasksByWorkstreamId.get(workstream.id)
1806
+ || [];
1750
1807
  return {
1751
- workstream,
1808
+ workstream: buildPlanningWorkstreamSummary(workstream),
1752
1809
  taskCount: tasks.length,
1753
1810
  tasks,
1754
1811
  };
1755
1812
  }
1756
1813
  function buildInitiativesResourcePayload() {
1814
+ const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID);
1815
+ const workstreamsByInitiativeId = new Map();
1816
+ for (const workstream of workstreams) {
1817
+ const normalizedInitiativeId = String(workstream.initiativeId || '').trim();
1818
+ if (!normalizedInitiativeId)
1819
+ continue;
1820
+ const current = workstreamsByInitiativeId.get(normalizedInitiativeId) || [];
1821
+ current.push(workstream);
1822
+ workstreamsByInitiativeId.set(normalizedInitiativeId, current);
1823
+ }
1824
+ const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
1757
1825
  const initiatives = core.listInitiatives(ACTIVE_WORKSPACE_ID)
1758
- .map((initiative) => buildInitiativeResourcePayload(initiative));
1826
+ .map((initiative) => buildInitiativeResourcePayload(initiative, {
1827
+ workstreamsByInitiativeId,
1828
+ tasksByWorkstreamId,
1829
+ }));
1759
1830
  return {
1760
1831
  total: initiatives.length,
1761
1832
  initiatives,
1762
1833
  };
1763
1834
  }
1764
1835
  function buildWorkstreamsResourcePayload() {
1836
+ const { tasksByWorkstreamId } = buildHistoricalWorkspaceTaskSnapshot();
1765
1837
  const workstreams = core.listWorkstreams(ACTIVE_WORKSPACE_ID)
1766
- .map((workstream) => buildWorkstreamResourcePayload(workstream));
1838
+ .map((workstream) => buildWorkstreamResourcePayload(workstream, { tasksByWorkstreamId }));
1767
1839
  return {
1768
1840
  total: workstreams.length,
1769
1841
  workstreams,
@@ -3230,18 +3302,7 @@ export function createTaskforceMcpServer(options = {}) {
3230
3302
  }
3231
3303
  }
3232
3304
  };
3233
- }).filter((tool) => {
3234
- if (!hasPermission(TOOL_PERMISSIONS[tool.name]))
3235
- return false;
3236
- const taskforceToolsEnv = process.env.TASKFORCE_TOOLS;
3237
- if (taskforceToolsEnv) {
3238
- const enabledTools = taskforceToolsEnv.split(',').map(t => t.trim()).filter(Boolean);
3239
- if (enabledTools.length > 0 && !enabledTools.includes(tool.name)) {
3240
- return false;
3241
- }
3242
- }
3243
- return true;
3244
- });
3305
+ }).filter((tool) => hasPermission(TOOL_PERMISSIONS[tool.name]));
3245
3306
  return {
3246
3307
  tools,
3247
3308
  };
@@ -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,
@@ -83,6 +30,8 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
83
30
  }
84
31
  : undefined
85
32
  });
33
+ }
34
+ async function callToolWithRuntime(runtime, name, args, options) {
86
35
  const handler = runtime.server._requestHandlers.get('tools/call');
87
36
  if (!handler) {
88
37
  throw new Error('tools/call handler not registered');
@@ -112,20 +61,71 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
112
61
  }
113
62
  }, {});
114
63
  }
115
- async function readResource(projectRoot, runtimeMode, uri, options) {
116
- 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({
117
79
  projectRoot,
118
- runtimeMode,
119
- workspaceId: options?.workspaceId,
120
- logStartup: false,
121
- auth: runtimeMode === 'cloud'
122
- ? {
123
- scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
124
- userId: options?.authUserId || 'owner-1',
125
- actorType: 'oauth'
126
- }
127
- : undefined
80
+ storagePath: path.join(projectRoot, '.taskforce')
128
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,
125
+ });
126
+ }
127
+ async function readResource(projectRoot, runtimeMode, uri, options) {
128
+ const runtime = createRuntime(projectRoot, runtimeMode, options);
129
129
  const handler = runtime.server._requestHandlers.get('resources/read');
130
130
  if (!handler) {
131
131
  throw new Error('resources/read handler not registered');
@@ -138,19 +138,7 @@ async function readResource(projectRoot, runtimeMode, uri, options) {
138
138
  }, {});
139
139
  }
140
140
  async function listResources(projectRoot, runtimeMode, options) {
141
- const runtime = createTaskforceMcpServer({
142
- projectRoot,
143
- runtimeMode,
144
- workspaceId: options?.workspaceId,
145
- logStartup: false,
146
- auth: runtimeMode === 'cloud'
147
- ? {
148
- scopes: ['tasks:read', 'tasks:write', 'workspace:read'],
149
- userId: options?.authUserId || 'owner-1',
150
- actorType: 'oauth'
151
- }
152
- : undefined
153
- });
141
+ const runtime = createRuntime(projectRoot, runtimeMode, options);
154
142
  const handler = runtime.server._requestHandlers.get('resources/list');
155
143
  if (!handler) {
156
144
  throw new Error('resources/list handler not registered');
@@ -304,6 +292,29 @@ describe('Taskforce MCP tool descriptions', () => {
304
292
  fs.rmSync(projectRoot, { recursive: true, force: true });
305
293
  }
306
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
+ });
307
318
  it('includes local-only guidance in local runtime tool descriptions', async () => {
308
319
  const projectRoot = createProjectRoot();
309
320
  try {
@@ -1500,6 +1511,150 @@ describe('Taskforce MCP tool descriptions', () => {
1500
1511
  fs.rmSync(projectRoot, { recursive: true, force: true });
1501
1512
  }
1502
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
+ });
1503
1658
  it('creates MCP tasks as unassigned when no explicit assignee is provided', async () => {
1504
1659
  const projectRoot = createProjectRoot();
1505
1660
  try {
@@ -1541,6 +1696,42 @@ describe('Taskforce MCP tool descriptions', () => {
1541
1696
  fs.rmSync(projectRoot, { recursive: true, force: true });
1542
1697
  }
1543
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
+ });
1544
1735
  it('persists save_task_attachment through canonical task attachments', async () => {
1545
1736
  const projectRoot = createProjectRoot();
1546
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
  });
@@ -493,6 +493,18 @@ export function ensureBillingSchema(db) {
493
493
  created_at TEXT NOT NULL
494
494
  );
495
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
+
496
508
  CREATE TABLE IF NOT EXISTS billing_config (
497
509
  tenant_id TEXT PRIMARY KEY,
498
510
  config_json TEXT NOT NULL DEFAULT '{}',
@@ -520,6 +532,8 @@ export function ensureBillingSchema(db) {
520
532
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_tenant ON stripe_webhook_failures(tenant_id);
521
533
  CREATE INDEX IF NOT EXISTS idx_stripe_webhook_failures_event ON stripe_webhook_failures(event_id);
522
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);
523
537
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_plan ON billing_price_mappings(tenant_id, plan_version_id);
524
538
  CREATE INDEX IF NOT EXISTS idx_billing_price_mappings_active ON billing_price_mappings(tenant_id, active);
525
539
  `);
@@ -723,6 +737,7 @@ export function ensureEntitlementsSchema(db) {
723
737
  updated_by TEXT,
724
738
  created_at TEXT NOT NULL,
725
739
  updated_at TEXT NOT NULL,
740
+ FOREIGN KEY (account_id) REFERENCES users(id) ON DELETE CASCADE,
726
741
  FOREIGN KEY (plan_version_id) REFERENCES plan_versions(plan_version_id)
727
742
  );
728
743
 
@@ -22,6 +22,7 @@ describe('proxy cookie rewrite helpers', () => {
22
22
  expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
23
23
  expect(shouldProxyCloudPath('/api/taskforce/version')).toBe(true);
24
24
  expect(shouldProxyCloudPath('/api/taskforce/account/entitlements')).toBe(true);
25
+ expect(shouldProxyCloudPath('/api/taskforce/account/profile-summary')).toBe(true);
25
26
  expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
26
27
  expect(shouldProxyCloudPath('/api/taskforce/admin/mcp-audit-logs')).toBe(true);
27
28
  expect(shouldProxyCloudPath('/api/taskforce/billing/status')).toBe(true);
@@ -109,6 +109,8 @@ export function shouldProxyCloudPath(pathname) {
109
109
  || pathname === PUBLIC_SITE_CONFIG_SCRIPT_PATH
110
110
  || pathname === '/api/taskforce/version'
111
111
  || pathname === '/api/taskforce/account/entitlements'
112
+ || pathname === '/api/taskforce/account/profile-summary'
113
+ || pathname === '/api/taskforce/account/team-management-access'
112
114
  || pathname.startsWith('/api/taskforce/admin/')
113
115
  || pathname.startsWith('/api/taskforce/billing/')
114
116
  || pathname.startsWith('/api/taskforce/auth/')
@@ -86,7 +86,8 @@ describe('shouldProxyCloudPath', () => {
86
86
  expect(shouldProxyCloudPath('/api/taskforce/admin/users')).toBe(true);
87
87
  expect(shouldProxyCloudPath('/api/taskforce/billing/plans')).toBe(true);
88
88
  expect(shouldProxyCloudPath('/api/taskforce/auth/login')).toBe(true);
89
- expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(false);
89
+ expect(shouldProxyCloudPath('/api/taskforce/account/profile-summary')).toBe(true);
90
+ expect(shouldProxyCloudPath('/api/taskforce/account/team-management-access')).toBe(true);
90
91
  expect(shouldProxyCloudPath('/api/taskforce/public/site-config')).toBe(true);
91
92
  expect(shouldProxyCloudPath('/api/taskforce/public/site-config-script')).toBe(true);
92
93
  });