@taskforcehq/taskforce 0.3.301 → 0.3.302

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 (147) hide show
  1. package/dist/Taskforce.module.css +26 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +13 -24
  4. package/dist/TaskforceCore.test.js +167 -152
  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/ui/ReferenceBadgeButton.js +1 -1
  14. package/dist/components/views/PlansPage.js +9 -8
  15. package/dist/components/views/StandaloneLayout.js +466 -204
  16. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  17. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
  18. package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
  19. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  20. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  21. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  22. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  23. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  24. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  25. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  26. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  27. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  28. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  29. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  30. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  31. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  32. package/dist/components/views/standalone/modals/types.js +1 -0
  33. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  34. package/dist/components/views/teamManagementAccess.js +2 -2
  35. package/dist/components/views/teamManagementAccess.test.js +3 -3
  36. package/dist/config/deployment.js +2 -2
  37. package/dist/config/deployment.test.js +5 -3
  38. package/dist/core/AiProfileService.js +7 -2
  39. package/dist/core/AiProfiles.test.js +19 -0
  40. package/dist/core/SyncReconciliationService.d.ts +2 -0
  41. package/dist/core/SyncReconciliationService.js +3 -0
  42. package/dist/core/TaskAuditTimeline.test.js +40 -0
  43. package/dist/core/Taskforce.d.ts +49 -0
  44. package/dist/core/Taskforce.ids.test.js +133 -0
  45. package/dist/core/Taskforce.js +241 -78
  46. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  47. package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
  48. package/dist/core/taskReferencePolicy.d.ts +28 -0
  49. package/dist/core/taskReferencePolicy.js +189 -0
  50. package/dist/core/types.d.ts +2 -0
  51. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  52. package/dist/hooks/useTaskData.js +3 -2
  53. package/dist/hooks/useTaskData.test.js +66 -0
  54. package/dist/hooks/useTaskforce.d.ts +5 -0
  55. package/dist/hooks/useTaskforce.js +63 -15
  56. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  57. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  58. package/dist/mcp/aiProfileBootstrap.js +80 -0
  59. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  60. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  61. package/dist/mcp/clientIntegrations.d.ts +2 -8
  62. package/dist/mcp/clientIntegrations.js +21 -40
  63. package/dist/mcp/clientIntegrations.test.js +28 -2
  64. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  65. package/dist/mcp/clientProfileDefaults.js +37 -0
  66. package/dist/mcp/runtime.d.ts +10 -7
  67. package/dist/mcp/runtime.js +41 -19
  68. package/dist/mcp/runtime.test.js +49 -2
  69. package/dist/migrations/taskSchemaMigrations.js +53 -1
  70. package/dist/qaOpen.test.d.ts +1 -0
  71. package/dist/qaOpen.test.js +56 -0
  72. package/dist/server/index.cookieProxy.test.js +1 -0
  73. package/dist/server/index.d.ts +1 -0
  74. package/dist/server/index.js +15 -1
  75. package/dist/server/index.test.js +14 -1
  76. package/dist/server/routes/admin.js +25 -22
  77. package/dist/server/routes/auth.d.ts +5 -0
  78. package/dist/server/routes/auth.js +350 -16
  79. package/dist/server/routes/billing.js +11 -1
  80. package/dist/server/routes/billing.test.js +32 -2
  81. package/dist/server/routes/documents.js +6 -1
  82. package/dist/server/routes/shared.d.ts +1 -1
  83. package/dist/server/routes/shared.js +11 -3
  84. package/dist/server/routes/sync.integration.test.js +130 -0
  85. package/dist/server/routes/sync.js +226 -1924
  86. package/dist/server/routes/tasks.js +18 -4
  87. package/dist/server/routes.js +26 -19
  88. package/dist/server/routes.test.js +352 -6
  89. package/dist/shared/runtimeContract.d.ts +1 -1
  90. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  91. package/dist/sync/collaborationSyncPayload.js +131 -0
  92. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  93. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  94. package/dist/sync/collaborationSyncState.d.ts +15 -0
  95. package/dist/sync/collaborationSyncState.js +60 -0
  96. package/dist/sync/contentSyncPayload.d.ts +45 -0
  97. package/dist/sync/contentSyncPayload.js +104 -0
  98. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  99. package/dist/sync/contentSyncPayload.test.js +104 -0
  100. package/dist/sync/contentSyncState.d.ts +88 -0
  101. package/dist/sync/contentSyncState.js +743 -0
  102. package/dist/sync/planningSyncPayload.d.ts +9 -0
  103. package/dist/sync/planningSyncPayload.js +82 -0
  104. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  105. package/dist/sync/planningSyncPayload.test.js +68 -0
  106. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  107. package/dist/sync/syncApplyHandlers.js +445 -0
  108. package/dist/sync/taskSyncPayload.d.ts +33 -0
  109. package/dist/sync/taskSyncPayload.js +126 -0
  110. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  111. package/dist/sync/taskSyncPayload.test.js +54 -0
  112. package/dist/sync/workspacePullFeed.d.ts +28 -0
  113. package/dist/sync/workspacePullFeed.js +301 -0
  114. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  115. package/dist/sync/workspacePullFeed.test.js +166 -0
  116. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  117. package/dist/sync/workspaceSyncModel.js +9 -2
  118. package/dist/sync/workspaceSyncModel.test.js +29 -0
  119. package/dist/test/setup.js +1 -1
  120. package/dist/types.d.ts +1 -0
  121. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
  122. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
  123. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
  124. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
  125. package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
  126. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
  127. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  128. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
  129. package/dist/ui/assets/index-MXeWoebC.css +1 -0
  130. package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
  131. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
  132. package/dist/ui/index.html +3 -3
  133. package/dist/utils/billingStatusCache.d.ts +21 -0
  134. package/dist/utils/billingStatusCache.js +70 -0
  135. package/dist/utils/taskActivity.js +17 -4
  136. package/dist/utils/taskActivity.test.js +27 -0
  137. package/dist/utils/taskNormalization.js +10 -2
  138. package/dist/utils/taskReferences.d.ts +11 -1
  139. package/dist/utils/taskReferences.js +33 -1
  140. package/package.json +2 -1
  141. package/scripts/qa-open-lib.mjs +175 -0
  142. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  143. package/scripts/qa-open.mjs +352 -0
  144. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  145. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  146. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  147. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -45,6 +45,8 @@ describe('clientIntegrations', () => {
45
45
  serverId: 'taskforce-workspace-alpha',
46
46
  aiProfileName: 'Automation Codex',
47
47
  aiProfileToken: 'tfp_automation_codex',
48
+ aiProfileIcon: 'Code2',
49
+ aiProfileColor: '#10a37f',
48
50
  aiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
49
51
  aiProfileRole: 'Coding Agent',
50
52
  aiProfileProvider: 'OpenAI',
@@ -57,26 +59,34 @@ describe('clientIntegrations', () => {
57
59
  tokenValue: '<<GENERATE_TOKEN>>',
58
60
  serverId: 'taskforce-workspace-alpha',
59
61
  aiProfileName: 'Automation Codex',
62
+ aiProfileIcon: 'Code2',
63
+ aiProfileColor: '#10a37f',
60
64
  aiProfileRole: 'Coding Agent',
61
65
  });
62
66
  expect(codex.renderedText).toContain('X-Taskforce-AI-Name = "Automation Codex"');
63
67
  expect(codex.renderedText).toContain('X-Taskforce-AI-Profile-Token = "tfp_automation_codex"');
68
+ expect(codex.renderedText).toContain('X-Taskforce-AI-Icon = "Code2"');
69
+ expect(codex.renderedText).toContain('X-Taskforce-AI-Color = "#10a37f"');
64
70
  expect(codex.renderedText).toContain('X-Taskforce-AI-Description = "OpenAI Codex coding agent connected through MCP."');
65
71
  expect(codex.renderedText).toContain('X-Taskforce-AI-Role = "Coding Agent"');
66
72
  expect(codex.renderedText).toContain('X-Taskforce-AI-Provider = "OpenAI"');
67
73
  expect(codex.renderedText).toContain('X-Taskforce-AI-Model = "Codex"');
68
74
  expect(codex.renderedText).toContain('X-Taskforce-AI-Surface-Type = "coding_tool"');
69
- expect(codex.instructionText).toContain('automated AI workers');
75
+ expect(codex.instructionText).toContain('Use the generated config exactly as shown.');
76
+ expect(codex.instructionText).toContain('generated AI bootstrap headers');
70
77
  expect(vscode.serverConfig).toEqual({
71
78
  type: 'http',
72
79
  url: 'https://performance-app.taskforcehq.ai/mcp',
73
80
  headers: {
74
81
  Authorization: 'Bearer <<GENERATE_TOKEN>>',
75
82
  'X-Taskforce-AI-Name': 'Automation Codex',
83
+ 'X-Taskforce-AI-Icon': 'Code2',
84
+ 'X-Taskforce-AI-Color': '#10a37f',
76
85
  'X-Taskforce-AI-Role': 'Coding Agent',
77
86
  }
78
87
  });
79
- expect(vscode.instructionText).toContain('automated AI workers');
88
+ expect(vscode.instructionText).toContain('Use the generated config exactly as shown.');
89
+ expect(vscode.instructionText).toContain('generated AI bootstrap headers');
80
90
  });
81
91
  it('builds JSON text config for OpenClaw using mcp.servers', () => {
82
92
  const integration = buildCloudMcpClientIntegration({
@@ -97,6 +107,22 @@ describe('clientIntegrations', () => {
97
107
  });
98
108
  expect(integration.instructionText).toContain('~/.openclaw/openclaw.json');
99
109
  });
110
+ it('keeps OAuth connector instructions focused on the URL flow instead of generated config language', () => {
111
+ const claudeDesktop = buildCloudMcpClientIntegration({
112
+ clientId: 'claude-desktop',
113
+ endpoint: 'https://performance-app.taskforcehq.ai/mcp',
114
+ tokenValue: '<<GENERATE_TOKEN>>',
115
+ serverId: 'taskforce-workspace-alpha',
116
+ });
117
+ const chatgptDesktop = buildCloudMcpClientIntegration({
118
+ clientId: 'chatgpt-desktop',
119
+ endpoint: 'https://performance-app.taskforcehq.ai/mcp',
120
+ tokenValue: '<<GENERATE_TOKEN>>',
121
+ serverId: 'taskforce-workspace-alpha',
122
+ });
123
+ expect(claudeDesktop.instructionText).not.toContain('generated config exactly as shown');
124
+ expect(chatgptDesktop.instructionText).not.toContain('generated config exactly as shown');
125
+ });
100
126
  it('builds command config for Gemini CLI using http transport', () => {
101
127
  const integration = buildCloudMcpClientIntegration({
102
128
  clientId: 'gemini-cli',
@@ -0,0 +1,12 @@
1
+ import type { McpClientId } from './clientIntegrations.js';
2
+ import type { AiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
3
+ export interface AiProfileMetadataDefaults {
4
+ icon: string | null;
5
+ color: string | null;
6
+ description: string | null;
7
+ role: string | null;
8
+ provider: string | null;
9
+ model: string | null;
10
+ surfaceType: AiProfileSurfaceType | null;
11
+ }
12
+ export declare function getDefaultAiProfileMetadataForClient(clientId: McpClientId): AiProfileMetadataDefaults;
@@ -0,0 +1,37 @@
1
+ const CLIENT_LABELS = {
2
+ cline: 'Cline',
3
+ windsurf: 'Windsurf',
4
+ antigravity: 'Antigravity',
5
+ openclaw: 'OpenClaw',
6
+ };
7
+ export function getDefaultAiProfileMetadataForClient(clientId) {
8
+ switch (clientId) {
9
+ case 'codex':
10
+ return { icon: 'Code2', color: '#10a37f', description: 'OpenAI Codex coding agent connected through MCP.', role: 'Coding Agent', provider: 'OpenAI', model: 'Codex', surfaceType: 'coding_tool' };
11
+ case 'chatgpt-desktop':
12
+ return { icon: 'MessageSquare', color: '#10a37f', description: 'ChatGPT desktop app connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'OpenAI', model: 'ChatGPT', surfaceType: 'chat_app' };
13
+ case 'claude-code':
14
+ return { icon: 'Sparkles', color: '#d97706', description: 'Claude Code coding agent connected through MCP.', role: 'Coding Agent', provider: 'Anthropic', model: 'Claude Code', surfaceType: 'coding_tool' };
15
+ case 'claude-desktop':
16
+ return { icon: 'MessageSquareMore', color: '#d97706', description: 'Claude Desktop connected through the Taskforce MCP connector.', role: 'AI Assistant', provider: 'Anthropic', model: 'Claude', surfaceType: 'chat_app' };
17
+ case 'gemini-cli':
18
+ return { icon: 'Gem', color: '#2563eb', description: 'Gemini CLI coding agent connected through MCP.', role: 'Coding Agent', provider: 'Google', model: 'Gemini', surfaceType: 'coding_tool' };
19
+ case 'vscode':
20
+ return { icon: 'Code2', color: '#2563eb', description: 'VS Code MCP-connected coding agent.', role: 'Coding Agent', provider: null, model: null, surfaceType: 'ide' };
21
+ case 'cline':
22
+ case 'windsurf':
23
+ case 'antigravity':
24
+ case 'openclaw':
25
+ return {
26
+ icon: 'Bot',
27
+ color: '#8b5cf6',
28
+ description: `${CLIENT_LABELS[clientId] || 'MCP'} coding agent connected through MCP.`,
29
+ role: 'Coding Agent',
30
+ provider: null,
31
+ model: null,
32
+ surfaceType: 'coding_tool',
33
+ };
34
+ default:
35
+ return { icon: null, color: null, description: null, role: null, provider: null, model: null, surfaceType: null };
36
+ }
37
+ }
@@ -1,6 +1,7 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { McpAccessTokenScope, TaskforceCore } from '../core/Taskforce.js';
4
+ import { type AiProfileBootstrapConfig } from './aiProfileBootstrap.js';
4
5
  export interface TaskforceMcpServerOptions {
5
6
  projectRoot?: string;
6
7
  tenantId?: string;
@@ -13,13 +14,15 @@ export interface TaskforceMcpServerOptions {
13
14
  scopes?: McpAccessTokenScope[] | null;
14
15
  userId?: string | null;
15
16
  tokenId?: string | null;
16
- aiProfileToken?: string | null;
17
- aiProfileName?: string | null;
18
- aiProfileDescription?: string | null;
19
- aiProfileRole?: string | null;
20
- aiProfileProvider?: string | null;
21
- aiProfileModel?: string | null;
22
- aiProfileSurfaceType?: string | null;
17
+ aiProfileToken?: AiProfileBootstrapConfig['aiProfileToken'];
18
+ aiProfileName?: AiProfileBootstrapConfig['aiProfileName'];
19
+ aiProfileIcon?: AiProfileBootstrapConfig['aiProfileIcon'];
20
+ aiProfileColor?: AiProfileBootstrapConfig['aiProfileColor'];
21
+ aiProfileDescription?: AiProfileBootstrapConfig['aiProfileDescription'];
22
+ aiProfileRole?: AiProfileBootstrapConfig['aiProfileRole'];
23
+ aiProfileProvider?: AiProfileBootstrapConfig['aiProfileProvider'];
24
+ aiProfileModel?: AiProfileBootstrapConfig['aiProfileModel'];
25
+ aiProfileSurfaceType?: AiProfileBootstrapConfig['aiProfileSurfaceType'];
23
26
  actorType?: 'local' | 'user' | 'token' | 'oauth';
24
27
  requestId?: string | null;
25
28
  ip?: string | null;
@@ -6,6 +6,7 @@ import * as path from 'path';
6
6
  import { createHash, randomUUID } from 'node:crypto';
7
7
  import { TaskforceCore, TaskforceRuleError } from '../core/Taskforce.js'; // Note .js extension for ESM
8
8
  import { COMPLEXITY_OPTIONS, resolveNumberOption, resolveStringOption, STATUS_OPTIONS } from './optionResolution.js';
9
+ import { buildResolveAiProfileInputFromBootstrap } from './aiProfileBootstrap.js';
9
10
  import { validateStructuredDocForAttach } from './structuredDocValidation.js';
10
11
  import { validateStructuredCommentForAdd } from './structuredCommentValidation.js';
11
12
  import { resolveDatabaseConfigFromEnv } from '../storage/providerConfig.js';
@@ -16,9 +17,9 @@ import { ANNOTATED_ATTACHMENTS_WORKSPACE_FEATURE_KEY, resolveAppGateAccess, } fr
16
17
  import { getDocumentReferenceLabel, parseDocumentReference } from '../utils/documentReferences.js';
17
18
  import { getImageReferenceLabel, parseImageReference } from '../utils/imageReferences.js';
18
19
  import { inferCanonicalAssetKind, isValidCanonicalDocumentAsset } from '../utils/canonicalAssetKind.js';
20
+ import { getTaskReferenceLabel, shouldUseProvisionalTaskReferences } from '../utils/taskReferences.js';
19
21
  import { AI_PROFILE_SURFACE_TYPES, parseAiProfileSurfaceType } from '../shared/aiProfileSurfaceType.js';
20
22
  import { formatWorkstreamReference, resolveInitiativeReference, parseWorkstreamReference, } from '../utils/planningReferences.js';
21
- import { getTaskReferenceLabel } from '../utils/taskReferences.js';
22
23
  import { MCP_TOOL_PERMISSIONS, MCP_WRITE_TOOL_NAMES } from './toolRegistry.js';
23
24
  export function isMcpWriteToolName(name) {
24
25
  return MCP_WRITE_TOOL_NAMES.has(String(name || '').trim());
@@ -71,7 +72,7 @@ function resolveRecoverableToolError(error) {
71
72
  code: 'AI_PROFILE_REQUIRED',
72
73
  nextTool: 'resolve_profile',
73
74
  suggestedArgs: {},
74
- retryHint: 'Call resolve_profile, store the returned profileToken, and pass profileToken on later mutating calls if the client is stateless.'
75
+ retryHint: 'Call get_current_profile to inspect binding state first, or call resolve_profile to bind an AI identity. Only pass profileToken on later mutating calls if your client is stateless.'
75
76
  };
76
77
  }
77
78
  if (/^Invalid AI profile token\./i.test(message)) {
@@ -80,7 +81,7 @@ function resolveRecoverableToolError(error) {
80
81
  code: 'INVALID_AI_PROFILE_TOKEN',
81
82
  nextTool: 'resolve_profile',
82
83
  suggestedArgs: {},
83
- retryHint: 'Call resolve_profile again to obtain a fresh profileToken, then retry the original mutating tool call.'
84
+ retryHint: 'Call get_current_profile to confirm whether ambient identity is already bound. If not, call resolve_profile again to obtain a fresh profileToken, then retry the original mutating tool call.'
84
85
  };
85
86
  }
86
87
  if (error instanceof TaskforceRuleError) {
@@ -586,7 +587,7 @@ export function createTaskforceMcpServer(options = {}) {
586
587
  },
587
588
  tools: {},
588
589
  },
589
- instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: At the start of any session, call resolve_profile before attempting any mutating Taskforce tools. Pass your name and a profileToken if you received one in a previous session. Store the returned profileToken for reuse across sessions. Use the returned profile id as the assignee or author on tasks and comments you create.\n\nStateless client pattern: call resolve_profile, save the returned profileToken, and include profileToken on later mutating tool calls if your MCP client does not preserve session identity between calls.\n\nAutomation-friendly connection pattern: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes without requiring profileToken on every tool call.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, coding_tool for the coding surface or terminal tool itself, chat_app for a conversational app identity, ide for an IDE-native copilot identity, and other only when none of those fit. If you are unsure and you are registering a named collaborator such as a teammate-style AI persona, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use get_annotated_attachment_sessions to list saved sessions, then use read_annotated_attachment_payload for the structured annotation payload.\n\nFor documents, use search_documents first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_document, then read_document_content to inspect the document body. For a lower-friction review flow, prefer review_document; otherwise use get_document_review_sessions or create_document_review_session, then add_document_review_comment to record review feedback.`,
590
+ instructions: `You are an AI collaborator connected to Taskforce, a shared task management system for AI-developer pairs.\n\nIMPORTANT: Before mutating Taskforce data, ensure Taskforce knows which AI profile you are acting as. The preferred path is ambient session binding: if your MCP connection is configured with x-taskforce-ai-name and optionally x-taskforce-ai-profile-token headers, Taskforce can auto-bind that AI identity for get_current_profile and later writes. If no AI identity is currently bound, call resolve_profile with your name. Reuse the returned profileToken across sessions, especially for stateless clients.\n\nStateless client pattern: call resolve_profile, save the returned profileToken, and include profileToken on later mutating tool calls only when your MCP client does not preserve session identity between calls.\n\nTo inspect your current attribution state before writes, call get_current_profile. It reports whether identity is bound, how it was resolved, and the suggested args to use with list_tasks.\n\nWhen setting resolve_profile.surfaceType, classify yourself by the kind of identity you are registering: use agent for a persistent named AI collaborator in Taskforce, coding_tool for the coding surface or terminal tool itself, chat_app for a conversational app identity, ide for an IDE-native copilot identity, and other only when none of those fit. If you are unsure and you are registering a named collaborator such as a teammate-style AI persona, choose agent.\n\nTo get tasks assigned to yourself, call get_current_profile, then use the returned suggestedArgs with list_tasks. When you begin active work on a tracked task, prefer start_task. It sets the task to in-progress and claims it for your AI profile when the task is currently unassigned. When work on a tracked task is complete, prefer finish_task for the final handoff: use outcome='review' when the next step is human validation or approval, and use outcome='done' only when no explicit human action is requested. Do not leave a task in-progress after reporting completed implementation unless the user explicitly wants more work to continue in the same task.\n\nTaxonomy rule: always call get_config before setting category, type, priority, approach, status, or complexity. Do not rely on remembered enum values.\n\nUse identifier prefixes to choose the correct retrieval path: T-123 goes to task tools such as get_task, IN-123 goes to initiative tools such as get_initiative, WS-123 goes to workstream tools such as get_workstream, I-123 goes to image tools such as get_image, and D-123 goes to document tools such as get_document. Do not send I-123 or D-123 identifiers to get_task.\n\nFor annotated images, use search_images first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_image, then use get_annotated_attachment_sessions to list saved sessions, then use read_annotated_attachment_payload for the structured annotation payload.\n\nFor documents, use search_documents first when you know only a keyword, filename, or rough title. Once you have the exact identifier, call get_document, then read_document_content to inspect the document body. For a lower-friction review flow, prefer review_document; otherwise use get_document_review_sessions or create_document_review_session, then add_document_review_comment to record review feedback.`,
590
591
  });
591
592
  const TOOL_PERMISSIONS = MCP_TOOL_PERMISSIONS;
592
593
  const RESOURCE_PERMISSIONS = {
@@ -923,6 +924,8 @@ export function createTaskforceMcpServer(options = {}) {
923
924
  const auditTokenId = options.auth?.tokenId ? String(options.auth.tokenId).trim() : null;
924
925
  const configuredAiProfileToken = options.auth?.aiProfileToken ? String(options.auth.aiProfileToken).trim() || null : null;
925
926
  const configuredAiProfileName = options.auth?.aiProfileName ? String(options.auth.aiProfileName).trim() || null : null;
927
+ const configuredAiProfileIcon = options.auth?.aiProfileIcon ? String(options.auth.aiProfileIcon).trim() || null : null;
928
+ const configuredAiProfileColor = options.auth?.aiProfileColor ? String(options.auth.aiProfileColor).trim() || null : null;
926
929
  const configuredAiProfileDescription = options.auth?.aiProfileDescription ? String(options.auth.aiProfileDescription).trim() || null : null;
927
930
  const configuredAiProfileRole = options.auth?.aiProfileRole ? String(options.auth.aiProfileRole).trim() || null : null;
928
931
  const configuredAiProfileProvider = options.auth?.aiProfileProvider ? String(options.auth.aiProfileProvider).trim() || null : null;
@@ -932,6 +935,7 @@ export function createTaskforceMcpServer(options = {}) {
932
935
  const auditIp = options.auth?.ip ? String(options.auth.ip).trim() || null : null;
933
936
  const auditUserAgent = options.auth?.userAgent ? String(options.auth.userAgent).trim() || null : null;
934
937
  let activeAiProfileId = null;
938
+ let activeAiBindingSource = null;
935
939
  function getEligibleAssigneeActors() {
936
940
  return core.listAssignableActors(ACTIVE_WORKSPACE_ID)
937
941
  .filter((actor) => String(actor.id || '').trim().toLowerCase() !== 'agent')
@@ -1084,20 +1088,24 @@ export function createTaskforceMcpServer(options = {}) {
1084
1088
  const token = String(args.profileToken || '').trim();
1085
1089
  return token || null;
1086
1090
  }
1091
+ function getCurrentAiBindingSource() {
1092
+ if (!activeAiProfileId)
1093
+ return 'none';
1094
+ return activeAiBindingSource || 'session';
1095
+ }
1087
1096
  function buildConfiguredAiProfileResolveInput(includeToken = true) {
1088
- if (!configuredAiProfileName)
1089
- return null;
1090
1097
  const configuredSurfaceType = parseAiProfileSurfaceType(configuredAiProfileSurfaceType);
1091
- return {
1092
- workspaceId: ACTIVE_WORKSPACE_ID,
1093
- name: configuredAiProfileName,
1094
- ...(includeToken && configuredAiProfileToken ? { profileToken: configuredAiProfileToken } : {}),
1095
- ...(configuredAiProfileDescription ? { description: configuredAiProfileDescription } : {}),
1096
- ...(configuredAiProfileRole ? { role: configuredAiProfileRole } : {}),
1097
- ...(configuredAiProfileProvider ? { provider: configuredAiProfileProvider } : {}),
1098
- ...(configuredAiProfileModel ? { model: configuredAiProfileModel } : {}),
1099
- ...(configuredSurfaceType ? { surfaceType: configuredSurfaceType } : {}),
1100
- };
1098
+ return buildResolveAiProfileInputFromBootstrap(ACTIVE_WORKSPACE_ID, {
1099
+ aiProfileToken: configuredAiProfileToken,
1100
+ aiProfileName: configuredAiProfileName,
1101
+ aiProfileIcon: configuredAiProfileIcon,
1102
+ aiProfileColor: configuredAiProfileColor,
1103
+ aiProfileDescription: configuredAiProfileDescription,
1104
+ aiProfileRole: configuredAiProfileRole,
1105
+ aiProfileProvider: configuredAiProfileProvider,
1106
+ aiProfileModel: configuredAiProfileModel,
1107
+ aiProfileSurfaceType: configuredSurfaceType,
1108
+ }, includeToken);
1101
1109
  }
1102
1110
  function ensureResolvedProfileFromConnection(defaults) {
1103
1111
  if (activeAiProfileId)
@@ -1110,6 +1118,7 @@ export function createTaskforceMcpServer(options = {}) {
1110
1118
  try {
1111
1119
  const resolved = core.resolveAiProfile(configuredResolveInput);
1112
1120
  activeAiProfileId = resolved.profile.id;
1121
+ activeAiBindingSource = 'connection_bootstrap';
1113
1122
  return;
1114
1123
  }
1115
1124
  catch {
@@ -1122,6 +1131,7 @@ export function createTaskforceMcpServer(options = {}) {
1122
1131
  });
1123
1132
  if (profile) {
1124
1133
  activeAiProfileId = profile.id;
1134
+ activeAiBindingSource = 'connection_bootstrap';
1125
1135
  return;
1126
1136
  }
1127
1137
  if (!allowCreateFromName || !configuredAiProfileName) {
@@ -1131,6 +1141,7 @@ export function createTaskforceMcpServer(options = {}) {
1131
1141
  if (allowCreateFromName && configuredResolveFallbackInput) {
1132
1142
  const resolved = core.resolveAiProfile(configuredResolveFallbackInput);
1133
1143
  activeAiProfileId = resolved.profile.id;
1144
+ activeAiBindingSource = 'connection_bootstrap';
1134
1145
  }
1135
1146
  }
1136
1147
  function ensureResolvedProfileForTool(name, args) {
@@ -1151,6 +1162,7 @@ export function createTaskforceMcpServer(options = {}) {
1151
1162
  });
1152
1163
  if (profile) {
1153
1164
  activeAiProfileId = profile.id;
1165
+ activeAiBindingSource = 'token_lookup';
1154
1166
  return;
1155
1167
  }
1156
1168
  const configuredResolveInput = buildConfiguredAiProfileResolveInput(false);
@@ -1158,6 +1170,7 @@ export function createTaskforceMcpServer(options = {}) {
1158
1170
  try {
1159
1171
  const resolved = core.resolveAiProfile(configuredResolveInput);
1160
1172
  activeAiProfileId = resolved.profile.id;
1173
+ activeAiBindingSource = 'connection_bootstrap';
1161
1174
  return;
1162
1175
  }
1163
1176
  catch {
@@ -2038,8 +2051,8 @@ export function createTaskforceMcpServer(options = {}) {
2038
2051
  type: "object",
2039
2052
  properties: {
2040
2053
  name: { type: "string", description: "Display name for the AI collaborator." },
2041
- icon: { type: "string", description: "Optional icon name or emoji saved with the AI profile." },
2042
- color: { type: "string", description: "Optional profile color saved with the AI profile." },
2054
+ icon: { type: "string", description: "Optional icon name or emoji saved with the AI profile. Omit to leave unchanged when profileToken is provided." },
2055
+ color: { type: "string", description: "Optional profile color saved with the AI profile. Omit to leave unchanged when profileToken is provided." },
2043
2056
  description: { type: "string", description: "Optional short description stored on the AI profile. Omit to leave unchanged when profileToken is provided." },
2044
2057
  role: { type: "string", description: "Optional role label stored on the AI profile. Omit to leave unchanged when profileToken is provided." },
2045
2058
  provider: { type: "string", description: "Optional provider name stored as secondary diagnostic metadata on the AI profile. Omit to leave unchanged when profileToken is provided." },
@@ -3311,6 +3324,7 @@ export function createTaskforceMcpServer(options = {}) {
3311
3324
  profileToken: typeof profileToken === 'string' ? profileToken : null
3312
3325
  });
3313
3326
  activeAiProfileId = resolved.profile.id;
3327
+ activeAiBindingSource = 'session';
3314
3328
  recordMcpWriteAudit('resolve_profile', {
3315
3329
  profileId: resolved.profile.id,
3316
3330
  created: resolved.created
@@ -3358,6 +3372,7 @@ export function createTaskforceMcpServer(options = {}) {
3358
3372
  let profile = activeAiProfileId
3359
3373
  ? core.listAiProfiles(ACTIVE_WORKSPACE_ID).find((entry) => entry.id === activeAiProfileId) || null
3360
3374
  : null;
3375
+ let bindingSource = getCurrentAiBindingSource();
3361
3376
  if (!profile && typeof profileToken === 'string' && profileToken.trim()) {
3362
3377
  profile = core.getAiProfileByToken({
3363
3378
  workspaceId: ACTIVE_WORKSPACE_ID,
@@ -3366,6 +3381,7 @@ export function createTaskforceMcpServer(options = {}) {
3366
3381
  if (!profile) {
3367
3382
  throw new Error('Invalid AI profile token. Call resolve_profile again and retry.');
3368
3383
  }
3384
+ bindingSource = 'token_lookup';
3369
3385
  }
3370
3386
  return {
3371
3387
  content: [{
@@ -3373,6 +3389,8 @@ export function createTaskforceMcpServer(options = {}) {
3373
3389
  text: JSON.stringify({
3374
3390
  workspaceId: ACTIVE_WORKSPACE_ID,
3375
3391
  identityBound: Boolean(activeAiProfileId),
3392
+ bindingSource,
3393
+ writeReady: Boolean(profile),
3376
3394
  profileToken: profile?.profileToken ?? null,
3377
3395
  profile: profile ? buildAiProfileDescriptor(profile) : null,
3378
3396
  taskFilter: profile ? { assignee: profile.id } : null,
@@ -4779,7 +4797,11 @@ export function createTaskforceMcpServer(options = {}) {
4779
4797
  ...createArgs,
4780
4798
  ...normalized,
4781
4799
  createdBy: getActiveMcpCreatedBy()
4782
- }, { workspaceId: ACTIVE_WORKSPACE_ID, actorRef: getActiveMcpActorRef() });
4800
+ }, {
4801
+ workspaceId: ACTIVE_WORKSPACE_ID,
4802
+ actorRef: getActiveMcpActorRef(),
4803
+ provisionalLocalReference: shouldUseProvisionalTaskReferences(runtimeMode)
4804
+ });
4783
4805
  recordMcpWriteAudit('create_task', {
4784
4806
  taskId: task?.id || null,
4785
4807
  title: task?.title || null
@@ -73,6 +73,8 @@ async function callTool(projectRoot, runtimeMode, name, args, options) {
73
73
  actorType: 'oauth',
74
74
  aiProfileToken: options?.authAiProfileToken || null,
75
75
  aiProfileName: options?.authAiProfileName || null,
76
+ aiProfileIcon: options?.authAiProfileIcon || null,
77
+ aiProfileColor: options?.authAiProfileColor || null,
76
78
  aiProfileDescription: options?.authAiProfileDescription || null,
77
79
  aiProfileRole: options?.authAiProfileRole || null,
78
80
  aiProfileProvider: options?.authAiProfileProvider || null,
@@ -713,6 +715,8 @@ describe('Taskforce MCP tool descriptions', () => {
713
715
  model: 'Codex',
714
716
  surfaceType: 'coding_tool'
715
717
  }));
718
+ expect(currentProfile.bindingSource).toBe('connection_bootstrap');
719
+ expect(currentProfile.writeReady).toBe(true);
716
720
  }
717
721
  finally {
718
722
  fs.rmSync(projectRoot, { recursive: true, force: true });
@@ -788,6 +792,8 @@ describe('Taskforce MCP tool descriptions', () => {
788
792
  authAiProfileName: 'Automation Codex'
789
793
  }));
790
794
  expect(currentProfile.identityBound).toBe(true);
795
+ expect(currentProfile.bindingSource).toBe('connection_bootstrap');
796
+ expect(currentProfile.writeReady).toBe(true);
791
797
  expect(currentProfile.profileToken).toMatch(/^tfp_/);
792
798
  expect(currentProfile.profile).toEqual(expect.objectContaining({
793
799
  name: 'Automation Codex',
@@ -817,6 +823,27 @@ describe('Taskforce MCP tool descriptions', () => {
817
823
  fs.rmSync(projectRoot, { recursive: true, force: true });
818
824
  }
819
825
  });
826
+ it('reports token_lookup binding when get_current_profile is inspected with an explicit profileToken', async () => {
827
+ const projectRoot = createProjectRoot();
828
+ try {
829
+ const resolved = parseToolJson(await callTool(projectRoot, 'local', 'resolve_profile', {
830
+ name: 'Codex'
831
+ }, { resolveProfile: false }));
832
+ const payload = parseToolJson(await callTool(projectRoot, 'local', 'get_current_profile', {
833
+ profileToken: resolved.profile.profileToken,
834
+ }, { resolveProfile: false }));
835
+ expect(payload.identityBound).toBe(false);
836
+ expect(payload.bindingSource).toBe('token_lookup');
837
+ expect(payload.writeReady).toBe(true);
838
+ expect(payload.profile).toEqual(expect.objectContaining({
839
+ id: resolved.profile.id,
840
+ profileToken: resolved.profile.profileToken,
841
+ }));
842
+ }
843
+ finally {
844
+ fs.rmSync(projectRoot, { recursive: true, force: true });
845
+ }
846
+ });
820
847
  it('recovers from an invalid per-call profileToken when the connection has a valid AI name', async () => {
821
848
  const projectRoot = createProjectRoot();
822
849
  try {
@@ -1214,6 +1241,8 @@ describe('Taskforce MCP tool descriptions', () => {
1214
1241
  }
1215
1242
  }, {}));
1216
1243
  expect(payload.identityBound).toBe(true);
1244
+ expect(payload.bindingSource).toBe('session');
1245
+ expect(payload.writeReady).toBe(true);
1217
1246
  expect(payload.profile).toEqual(expect.objectContaining({
1218
1247
  id: resolved.profile.id,
1219
1248
  username: resolved.profile.username,
@@ -1235,6 +1264,8 @@ describe('Taskforce MCP tool descriptions', () => {
1235
1264
  resolveProfile: false
1236
1265
  }));
1237
1266
  expect(payload.identityBound).toBe(false);
1267
+ expect(payload.bindingSource).toBe('none');
1268
+ expect(payload.writeReady).toBe(false);
1238
1269
  expect(payload.profile).toBeNull();
1239
1270
  expect(payload.taskFilter).toBeNull();
1240
1271
  expect(payload.nextTool).toBe('resolve_profile');
@@ -1251,13 +1282,29 @@ describe('Taskforce MCP tool descriptions', () => {
1251
1282
  const payload = parseToolJson(await callTool(projectRoot, 'cloud', 'get_current_profile', {}, {
1252
1283
  workspaceId: 'workspace-1',
1253
1284
  resolveProfile: false,
1254
- authAiProfileName: 'Automation Codex'
1285
+ authAiProfileName: 'Automation Codex',
1286
+ authAiProfileIcon: 'Code2',
1287
+ authAiProfileColor: '#10a37f',
1288
+ authAiProfileDescription: 'OpenAI Codex coding agent connected through MCP.',
1289
+ authAiProfileRole: 'Coding Agent',
1290
+ authAiProfileProvider: 'OpenAI',
1291
+ authAiProfileModel: 'Codex',
1292
+ authAiProfileSurfaceType: 'coding_tool'
1255
1293
  }));
1256
1294
  expect(payload.identityBound).toBe(true);
1295
+ expect(payload.bindingSource).toBe('connection_bootstrap');
1296
+ expect(payload.writeReady).toBe(true);
1257
1297
  expect(payload.profile).toEqual(expect.objectContaining({
1258
1298
  name: 'Automation Codex',
1259
1299
  username: 'automation-codex',
1260
- profileToken: expect.stringMatching(/^tfp_/)
1300
+ profileToken: expect.stringMatching(/^tfp_/),
1301
+ icon: 'Code2',
1302
+ color: '#10a37f',
1303
+ description: 'OpenAI Codex coding agent connected through MCP.',
1304
+ role: 'Coding Agent',
1305
+ provider: 'OpenAI',
1306
+ model: 'Codex',
1307
+ surfaceType: 'coding_tool'
1261
1308
  }));
1262
1309
  expect(payload.taskFilter).toEqual({ assignee: payload.profile.id });
1263
1310
  expect(payload.nextTool).toBe('list_tasks');
@@ -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 = [
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ // @ts-ignore - JS helper module without generated .d.ts; covered by focused tests.
5
+ import { assertQaProjectRootSafe, matchExpectedWorkspace, parseEnvFileContents, parseQaArgs, resolveExpectedWorkspace, resolveQaPaths } from '../scripts/qa-open-lib.mjs';
6
+ describe('qa open helper library', () => {
7
+ it('derives a stable QA root layout for an agent', () => {
8
+ const homeDir = path.join(os.tmpdir(), 'qa-open-test-home');
9
+ const paths = resolveQaPaths({ agentId: 'Codex Main', homeDir });
10
+ expect(paths.agentId).toBe('codex-main');
11
+ expect(paths.root).toContain(path.join('.taskforce', 'qa', 'codex-main'));
12
+ expect(paths.projectRoot).toContain(path.join('.taskforce', 'qa', 'codex-main', 'project'));
13
+ expect(paths.playwrightStorageState).toContain(path.join('.taskforce', 'qa', 'codex-main', 'playwright-auth.json'));
14
+ });
15
+ it('parses env files with comments, export statements, and quoted values', () => {
16
+ const env = parseEnvFileContents(`
17
+ # comment
18
+ export TASKFORCE_E2E_EMAIL="codex@example.com"
19
+ TASKFORCE_E2E_PASSWORD='secret'
20
+ TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG=codex-qa
21
+ `);
22
+ expect(env.TASKFORCE_E2E_EMAIL).toBe('codex@example.com');
23
+ expect(env.TASKFORCE_E2E_PASSWORD).toBe('secret');
24
+ expect(env.TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG).toBe('codex-qa');
25
+ });
26
+ it('rejects QA roots inside the repository', () => {
27
+ const repoRoot = '/repo/taskforce';
28
+ expect(() => assertQaProjectRootSafe('/repo/taskforce', repoRoot)).toThrow(/inside the repository/i);
29
+ expect(() => assertQaProjectRootSafe('/repo/taskforce/tmp/qa', repoRoot)).toThrow(/inside the repository/i);
30
+ expect(() => assertQaProjectRootSafe('/tmp/taskforce-qa', repoRoot)).not.toThrow();
31
+ });
32
+ it('matches expected workspace by slug against the current workspace id', () => {
33
+ const match = matchExpectedWorkspace({
34
+ currentWorkspaceId: 'workspace-2',
35
+ workspaces: [
36
+ { id: 'workspace-1', slug: 'alpha' },
37
+ { id: 'workspace-2', slug: 'beta' }
38
+ ]
39
+ }, { workspaceId: null, workspaceSlug: 'beta' });
40
+ expect(match.ok).toBe(true);
41
+ });
42
+ it('builds args and expected workspace from env', () => {
43
+ const parsed = parseQaArgs([], {
44
+ TASKFORCE_QA_AGENT: 'Claude QA',
45
+ TASKFORCE_QA_MODE: 'staging',
46
+ TASKFORCE_QA_PORT: '5199'
47
+ });
48
+ expect(parsed.agentId).toBe('claude-qa');
49
+ expect(parsed.mode).toBe('staging');
50
+ expect(parsed.preferredPort).toBe(5199);
51
+ const expected = resolveExpectedWorkspace({
52
+ TASKFORCE_QA_EXPECTED_WORKSPACE_ID: 'workspace-123'
53
+ });
54
+ expect(expected).toEqual({ workspaceId: 'workspace-123', workspaceSlug: null });
55
+ });
56
+ });
@@ -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/team-management-access')).toBe(true);
25
26
  expect(shouldProxyCloudPath('/api/taskforce/admin/mcp-audit-logs')).toBe(true);
26
27
  expect(shouldProxyCloudPath('/api/taskforce/billing/status')).toBe(true);
27
28
  expect(shouldProxyCloudPath('/api/taskforce/billing/checkout-session')).toBe(true);