@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.
- package/dist/Taskforce.module.css +26 -2
- package/dist/TaskforceCore.filter-persistence.test.js +1 -1
- package/dist/TaskforceCore.js +13 -24
- package/dist/TaskforceCore.test.js +167 -152
- package/dist/components/features/TaskSettings.js +14 -82
- package/dist/components/features/TaskSettings.test.js +33 -70
- package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
- package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
- package/dist/components/task/TaskActionHeader.js +6 -3
- package/dist/components/task/TaskCard.js +6 -3
- package/dist/components/task/TaskForm.js +2 -2
- package/dist/components/task/TaskForm.test.js +41 -2
- package/dist/components/ui/ReferenceBadgeButton.js +1 -1
- package/dist/components/views/PlansPage.js +9 -8
- package/dist/components/views/StandaloneLayout.js +466 -204
- package/dist/components/views/panels/PlanningDrawer.js +18 -1
- package/dist/components/views/standalone/modals/AccountHubModal.d.ts +43 -0
- package/dist/components/views/standalone/modals/AccountHubModal.js +22 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
- package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
- package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
- package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
- package/dist/components/views/standalone/modals/HelpModal.js +8 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
- package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
- package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
- package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
- package/dist/components/views/standalone/modals/types.d.ts +2 -0
- package/dist/components/views/standalone/modals/types.js +1 -0
- package/dist/components/views/teamManagementAccess.d.ts +1 -1
- package/dist/components/views/teamManagementAccess.js +2 -2
- package/dist/components/views/teamManagementAccess.test.js +3 -3
- package/dist/config/deployment.js +2 -2
- package/dist/config/deployment.test.js +5 -3
- package/dist/core/AiProfileService.js +7 -2
- package/dist/core/AiProfiles.test.js +19 -0
- package/dist/core/SyncReconciliationService.d.ts +2 -0
- package/dist/core/SyncReconciliationService.js +3 -0
- package/dist/core/TaskAuditTimeline.test.js +40 -0
- package/dist/core/Taskforce.d.ts +49 -0
- package/dist/core/Taskforce.ids.test.js +133 -0
- package/dist/core/Taskforce.js +241 -78
- package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
- package/dist/core/UserProfileAvatarDrafts.test.js +102 -0
- package/dist/core/taskReferencePolicy.d.ts +28 -0
- package/dist/core/taskReferencePolicy.js +189 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/hooks/tasks/useTaskFormActions.js +3 -0
- package/dist/hooks/useTaskData.js +3 -2
- package/dist/hooks/useTaskData.test.js +66 -0
- package/dist/hooks/useTaskforce.d.ts +5 -0
- package/dist/hooks/useTaskforce.js +63 -15
- package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
- package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
- package/dist/mcp/aiProfileBootstrap.js +80 -0
- package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
- package/dist/mcp/aiProfileBootstrap.test.js +68 -0
- package/dist/mcp/clientIntegrations.d.ts +2 -8
- package/dist/mcp/clientIntegrations.js +21 -40
- package/dist/mcp/clientIntegrations.test.js +28 -2
- package/dist/mcp/clientProfileDefaults.d.ts +12 -0
- package/dist/mcp/clientProfileDefaults.js +37 -0
- package/dist/mcp/runtime.d.ts +10 -7
- package/dist/mcp/runtime.js +41 -19
- package/dist/mcp/runtime.test.js +49 -2
- package/dist/migrations/taskSchemaMigrations.js +53 -1
- package/dist/qaOpen.test.d.ts +1 -0
- package/dist/qaOpen.test.js +56 -0
- package/dist/server/index.cookieProxy.test.js +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +15 -1
- package/dist/server/index.test.js +14 -1
- package/dist/server/routes/admin.js +25 -22
- package/dist/server/routes/auth.d.ts +5 -0
- package/dist/server/routes/auth.js +350 -16
- package/dist/server/routes/billing.js +11 -1
- package/dist/server/routes/billing.test.js +32 -2
- package/dist/server/routes/documents.js +6 -1
- package/dist/server/routes/shared.d.ts +1 -1
- package/dist/server/routes/shared.js +11 -3
- package/dist/server/routes/sync.integration.test.js +130 -0
- package/dist/server/routes/sync.js +226 -1924
- package/dist/server/routes/tasks.js +18 -4
- package/dist/server/routes.js +26 -19
- package/dist/server/routes.test.js +352 -6
- package/dist/shared/runtimeContract.d.ts +1 -1
- package/dist/sync/collaborationSyncPayload.d.ts +17 -0
- package/dist/sync/collaborationSyncPayload.js +131 -0
- package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
- package/dist/sync/collaborationSyncPayload.test.js +104 -0
- package/dist/sync/collaborationSyncState.d.ts +15 -0
- package/dist/sync/collaborationSyncState.js +60 -0
- package/dist/sync/contentSyncPayload.d.ts +45 -0
- package/dist/sync/contentSyncPayload.js +104 -0
- package/dist/sync/contentSyncPayload.test.d.ts +1 -0
- package/dist/sync/contentSyncPayload.test.js +104 -0
- package/dist/sync/contentSyncState.d.ts +88 -0
- package/dist/sync/contentSyncState.js +743 -0
- package/dist/sync/planningSyncPayload.d.ts +9 -0
- package/dist/sync/planningSyncPayload.js +82 -0
- package/dist/sync/planningSyncPayload.test.d.ts +1 -0
- package/dist/sync/planningSyncPayload.test.js +68 -0
- package/dist/sync/syncApplyHandlers.d.ts +83 -0
- package/dist/sync/syncApplyHandlers.js +445 -0
- package/dist/sync/taskSyncPayload.d.ts +33 -0
- package/dist/sync/taskSyncPayload.js +126 -0
- package/dist/sync/taskSyncPayload.test.d.ts +1 -0
- package/dist/sync/taskSyncPayload.test.js +54 -0
- package/dist/sync/workspacePullFeed.d.ts +28 -0
- package/dist/sync/workspacePullFeed.js +301 -0
- package/dist/sync/workspacePullFeed.test.d.ts +1 -0
- package/dist/sync/workspacePullFeed.test.js +166 -0
- package/dist/sync/workspaceSyncModel.d.ts +4 -1
- package/dist/sync/workspaceSyncModel.js +9 -2
- package/dist/sync/workspaceSyncModel.test.js +29 -0
- package/dist/test/setup.js +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-DJvLJI3A.js} +1 -1
- package/dist/ui/assets/PlansPage-BMPlpFpw.js +1 -0
- package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +8 -0
- package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
- package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-BguHeQaV.js} +1 -1
- package/dist/ui/assets/index-MXeWoebC.css +1 -0
- package/dist/ui/assets/index-YJ4k5QNa.js +6 -0
- package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-C8EED3Oh.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/utils/billingStatusCache.d.ts +21 -0
- package/dist/utils/billingStatusCache.js +70 -0
- package/dist/utils/taskActivity.js +17 -4
- package/dist/utils/taskActivity.test.js +27 -0
- package/dist/utils/taskNormalization.js +10 -2
- package/dist/utils/taskReferences.d.ts +11 -1
- package/dist/utils/taskReferences.js +33 -1
- package/package.json +2 -1
- package/scripts/qa-open-lib.mjs +175 -0
- package/scripts/qa-open-lib.mjs.d.ts +49 -0
- package/scripts/qa-open.mjs +352 -0
- package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
- package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
- package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
- 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('
|
|
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('
|
|
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
|
+
}
|
package/dist/mcp/runtime.d.ts
CHANGED
|
@@ -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?:
|
|
17
|
-
aiProfileName?:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
package/dist/mcp/runtime.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
}, {
|
|
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
|
package/dist/mcp/runtime.test.js
CHANGED
|
@@ -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);
|