@taskforcehq/taskforce 0.3.300 → 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/README.md +19 -0
- 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 +490 -207
- package/dist/components/views/panels/FilterToolbar.d.ts +2 -0
- package/dist/components/views/panels/FilterToolbarControls.d.ts +1 -0
- package/dist/components/views/panels/FilterToolbarControls.js +1 -1
- package/dist/components/views/panels/PlanningDrawer.js +19 -2
- 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 +16 -11
- package/dist/hooks/useTaskData.test.js +127 -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 +45 -23
- package/dist/mcp/runtime.test.js +56 -3
- 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-TVmCgizS.js → AgentsModule-JG5jc3he.js} +1 -1
- package/dist/ui/assets/{AnnotatedAttachmentWorkspace-CAyhnPeR.js → AnnotatedAttachmentWorkspace-Dvi02Y7q.js} +1 -1
- package/dist/ui/assets/{DocumentWorkspace-BOCw5e4D.js → DocumentWorkspace-iys3zfca.js} +2 -2
- package/dist/ui/assets/{InitiativesModule-CN0r02Oi.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-CY9R2zPB.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 +4 -1
- package/scripts/deploy-prod.sh +41 -0
- package/scripts/deploy-staging.sh +23 -0
- 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-FcQSNLZd.js +0 -1
- package/dist/ui/assets/TaskSettings-Cw5zYD1L.js +0 -8
- package/dist/ui/assets/index-Bzqy5QGm.js +0 -6
- package/dist/ui/assets/index-DxRiI3tY.css +0 -1
package/dist/ui/index.html
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
|
|
9
9
|
<link rel="apple-touch-icon" href="/taskforce/favicon/apple-touch-icon.png" />
|
|
10
10
|
<title>Taskforce Dashboard</title>
|
|
11
|
-
<script type="module" crossorigin src="/taskforce/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/taskforce/assets/index-YJ4k5QNa.js"></script>
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-react-CKJs5o3c.js">
|
|
13
|
-
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-icons-
|
|
13
|
+
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-icons-C8EED3Oh.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-markdown-BUxTU7dS.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-dnd-DRzYolkg.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/taskforce/assets/vendor-router-BbWMxlnO.js">
|
|
17
|
-
<link rel="stylesheet" crossorigin href="/taskforce/assets/index-
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/taskforce/assets/index-MXeWoebC.css">
|
|
18
18
|
</head>
|
|
19
19
|
<body>
|
|
20
20
|
<div id="root"></div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type BillingStatusPayload = {
|
|
2
|
+
stripeStatus?: string | null;
|
|
3
|
+
stripeCustomerId?: string | null;
|
|
4
|
+
stripeSubscriptionId?: string | null;
|
|
5
|
+
stripePriceId?: string | null;
|
|
6
|
+
billingInterval?: 'month' | 'year' | null;
|
|
7
|
+
trialEnd?: string | null;
|
|
8
|
+
currentPeriodEnd?: string | null;
|
|
9
|
+
effectiveUntil?: string | null;
|
|
10
|
+
entitlementState?: string;
|
|
11
|
+
planVersionId?: string;
|
|
12
|
+
planId?: string;
|
|
13
|
+
planName?: string | null;
|
|
14
|
+
gate?: string | null;
|
|
15
|
+
message?: string | null;
|
|
16
|
+
planSelectionRequired?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export declare function invalidateBillingStatusCache(rawUrl: string): void;
|
|
19
|
+
export declare function loadBillingStatusCached(rawUrl: string, options?: {
|
|
20
|
+
force?: boolean;
|
|
21
|
+
}): Promise<BillingStatusPayload | null>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { logBootstrapDebug } from '../hooks/workspace/workspaceLocalState';
|
|
2
|
+
const BILLING_STATUS_CACHE_TTL_MS = 5_000;
|
|
3
|
+
const billingStatusCache = new Map();
|
|
4
|
+
function getTimingNow() {
|
|
5
|
+
return typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
6
|
+
}
|
|
7
|
+
function normalizeBillingStatusUrl(raw) {
|
|
8
|
+
return String(raw || '').trim();
|
|
9
|
+
}
|
|
10
|
+
export function invalidateBillingStatusCache(rawUrl) {
|
|
11
|
+
billingStatusCache.delete(normalizeBillingStatusUrl(rawUrl));
|
|
12
|
+
}
|
|
13
|
+
export async function loadBillingStatusCached(rawUrl, options) {
|
|
14
|
+
const url = normalizeBillingStatusUrl(rawUrl);
|
|
15
|
+
if (!url)
|
|
16
|
+
return null;
|
|
17
|
+
const force = options?.force === true;
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const cached = billingStatusCache.get(url);
|
|
20
|
+
if (!force && cached?.payload !== undefined && (now - cached.fetchedAt) < BILLING_STATUS_CACHE_TTL_MS) {
|
|
21
|
+
logBootstrapDebug('billing_status_cache_hit', {
|
|
22
|
+
url,
|
|
23
|
+
ageMs: now - cached.fetchedAt
|
|
24
|
+
});
|
|
25
|
+
return cached.payload;
|
|
26
|
+
}
|
|
27
|
+
if (!force && cached?.promise) {
|
|
28
|
+
logBootstrapDebug('billing_status_request_reused', {
|
|
29
|
+
url
|
|
30
|
+
});
|
|
31
|
+
return cached.promise;
|
|
32
|
+
}
|
|
33
|
+
const startedAt = getTimingNow();
|
|
34
|
+
const request = (async () => {
|
|
35
|
+
const res = await fetch(url, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
credentials: 'include'
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(String((await res.json().catch(() => ({})))?.error || 'Failed to load billing status.'));
|
|
41
|
+
}
|
|
42
|
+
const payload = await res.json().catch(() => ({}));
|
|
43
|
+
const normalized = (payload || null);
|
|
44
|
+
billingStatusCache.set(url, {
|
|
45
|
+
payload: normalized,
|
|
46
|
+
fetchedAt: Date.now(),
|
|
47
|
+
promise: null
|
|
48
|
+
});
|
|
49
|
+
logBootstrapDebug('billing_status_loaded', {
|
|
50
|
+
url,
|
|
51
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
52
|
+
fromCache: false
|
|
53
|
+
});
|
|
54
|
+
return normalized;
|
|
55
|
+
})().catch((error) => {
|
|
56
|
+
billingStatusCache.delete(url);
|
|
57
|
+
logBootstrapDebug('billing_status_failed', {
|
|
58
|
+
url,
|
|
59
|
+
durationMs: Math.round(getTimingNow() - startedAt),
|
|
60
|
+
error: error instanceof Error ? error.message : String(error || 'Unknown error')
|
|
61
|
+
});
|
|
62
|
+
throw error;
|
|
63
|
+
});
|
|
64
|
+
billingStatusCache.set(url, {
|
|
65
|
+
payload: cached?.payload ?? null,
|
|
66
|
+
fetchedAt: cached?.fetchedAt ?? 0,
|
|
67
|
+
promise: request
|
|
68
|
+
});
|
|
69
|
+
return request;
|
|
70
|
+
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
function getActivityFieldLabel(field) {
|
|
2
|
+
if (field === 'title')
|
|
3
|
+
return 'TITLE';
|
|
4
|
+
if (field === 'description')
|
|
5
|
+
return 'DESCRIPTION';
|
|
6
|
+
if (field === 'assignee')
|
|
7
|
+
return 'ASSIGNED TO';
|
|
8
|
+
return field;
|
|
9
|
+
}
|
|
1
10
|
function formatValue(value, field, formatter) {
|
|
2
11
|
const formatted = formatter?.(field, value);
|
|
3
12
|
if (typeof formatted === 'string')
|
|
@@ -66,16 +75,20 @@ export function summarizeTaskChange(field, change, formatter) {
|
|
|
66
75
|
if (field === 'checklistItems') {
|
|
67
76
|
return summarizeChecklistChange(change);
|
|
68
77
|
}
|
|
78
|
+
const label = getActivityFieldLabel(field);
|
|
79
|
+
if (field === 'title') {
|
|
80
|
+
return `${label}: updated`;
|
|
81
|
+
}
|
|
69
82
|
if (field === 'description') {
|
|
70
83
|
const hadBefore = formatValue(change?.from, field, formatter) !== 'none';
|
|
71
84
|
const hasAfter = formatValue(change?.to, field, formatter) !== 'none';
|
|
72
85
|
if (!hadBefore && hasAfter)
|
|
73
|
-
return
|
|
86
|
+
return `${label}: added`;
|
|
74
87
|
if (hadBefore && !hasAfter)
|
|
75
|
-
return
|
|
76
|
-
return
|
|
88
|
+
return `${label}: cleared`;
|
|
89
|
+
return `${label}: updated`;
|
|
77
90
|
}
|
|
78
|
-
return `${
|
|
91
|
+
return `${label}: ${formatValue(change?.from, field, formatter)} -> ${formatValue(change?.to, field, formatter)}`;
|
|
79
92
|
}
|
|
80
93
|
export function summarizeTaskEvent(event, formatter) {
|
|
81
94
|
const changes = event.details?.changes || {};
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { summarizeTaskChange } from './taskActivity';
|
|
3
3
|
describe('summarizeTaskChange', () => {
|
|
4
|
+
it('summarizes title edits without dumping before and after text', () => {
|
|
5
|
+
expect(summarizeTaskChange('title', {
|
|
6
|
+
from: 'Old title',
|
|
7
|
+
to: 'New title',
|
|
8
|
+
})).toBe('TITLE: updated');
|
|
9
|
+
});
|
|
10
|
+
it('summarizes assignee changes with uppercase labels and formatted values', () => {
|
|
11
|
+
expect(summarizeTaskChange('assignee', { from: 'unassigned', to: 'member-1' }, (_field, value) => {
|
|
12
|
+
if (value === 'unassigned')
|
|
13
|
+
return 'Unassigned';
|
|
14
|
+
if (value === 'member-1')
|
|
15
|
+
return 'Emma Peel';
|
|
16
|
+
return undefined;
|
|
17
|
+
})).toBe('ASSIGNED TO: Unassigned -> Emma Peel');
|
|
18
|
+
});
|
|
19
|
+
it('summarizes description additions without rendering the body text', () => {
|
|
20
|
+
expect(summarizeTaskChange('description', {
|
|
21
|
+
from: '',
|
|
22
|
+
to: 'Line one\n\nLine two',
|
|
23
|
+
})).toBe('DESCRIPTION: added');
|
|
24
|
+
});
|
|
25
|
+
it('summarizes description clears without rendering the old body text', () => {
|
|
26
|
+
expect(summarizeTaskChange('description', {
|
|
27
|
+
from: 'Old body',
|
|
28
|
+
to: '',
|
|
29
|
+
})).toBe('DESCRIPTION: cleared');
|
|
30
|
+
});
|
|
4
31
|
it('summarizes checklist completion clearly', () => {
|
|
5
32
|
expect(summarizeTaskChange('checklistItems', {
|
|
6
33
|
from: [{ id: 'a', title: 'First item', isCompleted: false, order: 0 }],
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneDefaultPriorityDefinitions } from '../shared/taxonomyDefaults.js';
|
|
2
|
-
import { formatTaskReference } from './taskReferences.js';
|
|
2
|
+
import { formatLocalTaskReference, formatTaskReference } from './taskReferences.js';
|
|
3
3
|
export const DEFAULT_PRIORITY_DEFINITIONS = cloneDefaultPriorityDefinitions();
|
|
4
4
|
const PRIORITY_VALUE_BY_LABEL = {
|
|
5
5
|
low: 1,
|
|
@@ -75,12 +75,20 @@ export function normalizeTaskFromApi(task) {
|
|
|
75
75
|
: Number.isFinite(Number(task.referenceNumber))
|
|
76
76
|
? Number(task.referenceNumber)
|
|
77
77
|
: null;
|
|
78
|
+
const localReferenceNumber = typeof task.localReferenceNumber === 'number'
|
|
79
|
+
? task.localReferenceNumber
|
|
80
|
+
: Number.isFinite(Number(task.localReferenceNumber))
|
|
81
|
+
? Number(task.localReferenceNumber)
|
|
82
|
+
: null;
|
|
78
83
|
return {
|
|
79
84
|
...task,
|
|
80
85
|
referenceNumber: referenceNumber && referenceNumber > 0 ? Math.floor(referenceNumber) : null,
|
|
86
|
+
localReferenceNumber: localReferenceNumber && localReferenceNumber > 0 ? Math.floor(localReferenceNumber) : null,
|
|
81
87
|
referenceLabel: typeof task.referenceLabel === 'string' && task.referenceLabel.trim().length > 0
|
|
82
88
|
? task.referenceLabel.trim()
|
|
83
|
-
:
|
|
89
|
+
: (referenceNumber
|
|
90
|
+
? formatTaskReference(referenceNumber)
|
|
91
|
+
: formatLocalTaskReference(localReferenceNumber)),
|
|
84
92
|
assignee: (() => {
|
|
85
93
|
const raw = String(task.assignee || '').trim();
|
|
86
94
|
const normalized = raw.toLowerCase();
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { TaskItem } from '../types';
|
|
2
2
|
export declare const TASK_REFERENCE_PREFIX = "T-";
|
|
3
|
+
export declare const LOCAL_TASK_REFERENCE_PREFIX = "LT-";
|
|
4
|
+
export interface TaskReferenceDisplayState {
|
|
5
|
+
label: string;
|
|
6
|
+
isProvisional: boolean;
|
|
7
|
+
}
|
|
3
8
|
export declare function formatTaskReference(value: number | Pick<TaskItem, 'referenceNumber'> | null | undefined): string;
|
|
9
|
+
export declare function formatLocalTaskReference(value: number | Pick<TaskItem, 'localReferenceNumber'> | null | undefined): string;
|
|
4
10
|
export declare function parseTaskReference(value: string | null | undefined): number | null;
|
|
5
|
-
export declare function
|
|
11
|
+
export declare function parseLocalTaskReference(value: string | null | undefined): number | null;
|
|
12
|
+
export declare function getTaskReferenceLabel(task: Pick<TaskItem, 'referenceNumber' | 'referenceLabel' | 'localReferenceNumber'> | null | undefined): string;
|
|
13
|
+
export declare function isTaskReferenceProvisional(task: Pick<TaskItem, 'referenceNumber' | 'localReferenceNumber'> | null | undefined): boolean;
|
|
14
|
+
export declare function getTaskReferenceDisplayState(task: Pick<TaskItem, 'referenceNumber' | 'referenceLabel' | 'localReferenceNumber'> | null | undefined): TaskReferenceDisplayState;
|
|
15
|
+
export declare function shouldUseProvisionalTaskReferences(runtimeMode: string | null | undefined): boolean;
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import { formatEntityReference, getEntityReferenceLabel, parseEntityReference, } from './entityReferences.js';
|
|
2
2
|
export const TASK_REFERENCE_PREFIX = 'T-';
|
|
3
|
+
export const LOCAL_TASK_REFERENCE_PREFIX = 'LT-';
|
|
3
4
|
export function formatTaskReference(value) {
|
|
4
5
|
return formatEntityReference(TASK_REFERENCE_PREFIX, value);
|
|
5
6
|
}
|
|
7
|
+
export function formatLocalTaskReference(value) {
|
|
8
|
+
if (typeof value === 'number' || value === null || value === undefined) {
|
|
9
|
+
return formatEntityReference(LOCAL_TASK_REFERENCE_PREFIX, value);
|
|
10
|
+
}
|
|
11
|
+
return formatEntityReference(LOCAL_TASK_REFERENCE_PREFIX, {
|
|
12
|
+
referenceNumber: value.localReferenceNumber ?? null
|
|
13
|
+
});
|
|
14
|
+
}
|
|
6
15
|
export function parseTaskReference(value) {
|
|
7
16
|
return parseEntityReference(TASK_REFERENCE_PREFIX, value);
|
|
8
17
|
}
|
|
18
|
+
export function parseLocalTaskReference(value) {
|
|
19
|
+
return parseEntityReference(LOCAL_TASK_REFERENCE_PREFIX, value);
|
|
20
|
+
}
|
|
9
21
|
export function getTaskReferenceLabel(task) {
|
|
10
|
-
|
|
22
|
+
if (!task)
|
|
23
|
+
return '';
|
|
24
|
+
if (typeof task.referenceLabel === 'string' && task.referenceLabel.trim().length > 0) {
|
|
25
|
+
return task.referenceLabel.trim();
|
|
26
|
+
}
|
|
27
|
+
const canonicalLabel = getEntityReferenceLabel(TASK_REFERENCE_PREFIX, task);
|
|
28
|
+
if (canonicalLabel)
|
|
29
|
+
return canonicalLabel;
|
|
30
|
+
return formatLocalTaskReference(task.localReferenceNumber);
|
|
31
|
+
}
|
|
32
|
+
export function isTaskReferenceProvisional(task) {
|
|
33
|
+
return !task?.referenceNumber && !!task?.localReferenceNumber;
|
|
34
|
+
}
|
|
35
|
+
export function getTaskReferenceDisplayState(task) {
|
|
36
|
+
return {
|
|
37
|
+
label: getTaskReferenceLabel(task),
|
|
38
|
+
isProvisional: isTaskReferenceProvisional(task)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function shouldUseProvisionalTaskReferences(runtimeMode) {
|
|
42
|
+
return String(runtimeMode || '').trim().toLowerCase() === 'local';
|
|
11
43
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taskforcehq/taskforce",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.302",
|
|
4
4
|
"description": "Mission Command center for AI-powered task management (beta - under active development)",
|
|
5
5
|
"author": "Taskforce HQ <hello@taskforcehq.ai> (https://taskforcehq.ai)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
"check:localization-strings": "node scripts/check-localization-strings.mjs",
|
|
61
61
|
"version:next": "node scripts/release-version.mjs --dry-run",
|
|
62
62
|
"version:bump": "node scripts/release-version.mjs",
|
|
63
|
+
"deploy:staging": "bash scripts/deploy-staging.sh",
|
|
64
|
+
"deploy:prod": "bash scripts/deploy-prod.sh",
|
|
63
65
|
"test:localization": "vitest run src/__tests__/localizationCatalogs.test.ts",
|
|
64
66
|
"check:runtime-boundaries": "node scripts/check-runtime-boundaries.mjs",
|
|
65
67
|
"test:workflow-readiness": "npm run check:workflows && vitest src/__tests__/workflowSimulation.test.ts src/__tests__/workflowExportMatrix.test.ts src/__tests__/workflowExportSnapshots.test.ts src/__tests__/workflowDocumentFixtures.test.ts src/__tests__/structuredCommentValidation.test.ts src/__tests__/structuredDocValidation.test.ts src/__tests__/workflowV2Policy.test.ts src/__tests__/workflowGeneration.test.ts src/__tests__/templates.test.ts",
|
|
@@ -76,6 +78,7 @@
|
|
|
76
78
|
"test:e2e:realtime:reconnect": "npm run test:e2e:realtime -- tests/e2e/realtime-reconnect.spec.ts",
|
|
77
79
|
"playwright:auth:bootstrap": "node scripts/bootstrap-playwright-auth.mjs",
|
|
78
80
|
"playwright:auth": "sh scripts/playwright-auth.sh",
|
|
81
|
+
"qa:open": "node scripts/qa-open.mjs",
|
|
79
82
|
"check:env": "node --import tsx scripts/check-env.ts --target=server --strict=true",
|
|
80
83
|
"check:env:soak": "node --import tsx scripts/check-env.ts --target=soak --strict=true",
|
|
81
84
|
"check:compat": "node scripts/check-compat-boundaries.mjs",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
cd "$ROOT_DIR"
|
|
6
|
+
|
|
7
|
+
BUMP_TYPE="${1:-patch}"
|
|
8
|
+
ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
9
|
+
|
|
10
|
+
case "$BUMP_TYPE" in
|
|
11
|
+
patch|minor|major)
|
|
12
|
+
;;
|
|
13
|
+
*)
|
|
14
|
+
echo "Invalid version bump '$BUMP_TYPE'. Use patch, minor, or major." >&2
|
|
15
|
+
exit 1
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
20
|
+
echo "Working tree is not clean. Commit or stash changes before deploying to production." >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
git checkout main
|
|
25
|
+
git pull --ff-only origin main
|
|
26
|
+
git merge --no-ff --no-commit staging
|
|
27
|
+
npm version "$BUMP_TYPE" --no-git-tag-version
|
|
28
|
+
|
|
29
|
+
VERSION="$(node -p "JSON.parse(require('fs').readFileSync('package.json','utf8')).version")"
|
|
30
|
+
|
|
31
|
+
git add package.json package-lock.json
|
|
32
|
+
git commit -m "chore(release): v${VERSION}"
|
|
33
|
+
git tag "v${VERSION}"
|
|
34
|
+
git push origin main
|
|
35
|
+
git push origin "v${VERSION}"
|
|
36
|
+
|
|
37
|
+
if [[ "$ORIGINAL_BRANCH" != "main" ]]; then
|
|
38
|
+
git checkout "$ORIGINAL_BRANCH"
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
echo "Promoted staging to main, bumped version to ${VERSION}, and pushed tag v${VERSION}."
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
cd "$ROOT_DIR"
|
|
6
|
+
|
|
7
|
+
ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
|
8
|
+
|
|
9
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
10
|
+
echo "Working tree is not clean. Commit or stash changes before deploying to staging." >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
git checkout staging
|
|
15
|
+
git pull --ff-only origin staging
|
|
16
|
+
git merge --no-ff dev -m "chore(release): promote dev to staging"
|
|
17
|
+
git push origin staging
|
|
18
|
+
|
|
19
|
+
if [[ "$ORIGINAL_BRANCH" != "staging" ]]; then
|
|
20
|
+
git checkout "$ORIGINAL_BRANCH"
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
echo "Promoted dev to staging."
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const QA_MODES = ['local', 'performance', 'staging'];
|
|
6
|
+
|
|
7
|
+
export const QA_CLOUD_BASE_URLS = {
|
|
8
|
+
performance: 'https://performance-app.taskforcehq.ai',
|
|
9
|
+
staging: 'https://staging-app.taskforcehq.ai'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function normalizePort(raw) {
|
|
13
|
+
const value = String(raw || '').trim();
|
|
14
|
+
if (!value) return null;
|
|
15
|
+
const parsed = Number.parseInt(value, 10);
|
|
16
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function stripQuotes(raw) {
|
|
23
|
+
const value = String(raw || '').trim();
|
|
24
|
+
if (
|
|
25
|
+
(value.startsWith('"') && value.endsWith('"'))
|
|
26
|
+
|| (value.startsWith("'") && value.endsWith("'"))
|
|
27
|
+
) {
|
|
28
|
+
return value.slice(1, -1);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeQaMode(raw) {
|
|
34
|
+
const normalized = String(raw || '').trim().toLowerCase();
|
|
35
|
+
return QA_MODES.includes(normalized) ? normalized : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sanitizeQaAgentId(raw) {
|
|
39
|
+
const normalized = String(raw || '')
|
|
40
|
+
.trim()
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
.replace(/-{2,}/g, '-');
|
|
45
|
+
return normalized || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseQaArgs(argv, env = process.env) {
|
|
49
|
+
const positional = (argv || []).filter((arg) => !String(arg || '').startsWith('--'));
|
|
50
|
+
const mode = normalizeQaMode(positional[0] || env.TASKFORCE_QA_MODE || 'local');
|
|
51
|
+
const preferredPort = normalizePort(positional[1] || env.TASKFORCE_QA_PORT);
|
|
52
|
+
const agentId = sanitizeQaAgentId(env.TASKFORCE_QA_AGENT);
|
|
53
|
+
return { mode, agentId, preferredPort };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveQaPaths({ agentId, homeDir = os.homedir() }) {
|
|
57
|
+
const safeAgentId = sanitizeQaAgentId(agentId);
|
|
58
|
+
if (!safeAgentId) {
|
|
59
|
+
throw new Error('TASKFORCE_QA_AGENT is required and must contain at least one safe path character.');
|
|
60
|
+
}
|
|
61
|
+
const root = path.resolve(homeDir, '.taskforce', 'qa', safeAgentId);
|
|
62
|
+
return {
|
|
63
|
+
agentId: safeAgentId,
|
|
64
|
+
root,
|
|
65
|
+
projectRoot: path.join(root, 'project'),
|
|
66
|
+
envFile: path.join(root, 'env'),
|
|
67
|
+
logsDir: path.join(root, 'logs'),
|
|
68
|
+
artifactsDir: path.join(root, 'artifacts'),
|
|
69
|
+
homeDir: path.join(root, 'home'),
|
|
70
|
+
playwrightStorageState: path.join(root, 'playwright-auth.json')
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isPathInside(parentPath, childPath) {
|
|
75
|
+
const parent = path.resolve(parentPath);
|
|
76
|
+
const child = path.resolve(childPath);
|
|
77
|
+
const relative = path.relative(parent, child);
|
|
78
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function assertQaProjectRootSafe(projectRoot, repoRoot) {
|
|
82
|
+
if (isPathInside(repoRoot, projectRoot)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Refusing to use QA project root inside the repository: ${projectRoot}\n`
|
|
85
|
+
+ `Move the QA root outside ${repoRoot}.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseEnvFileContents(raw) {
|
|
91
|
+
const env = {};
|
|
92
|
+
const lines = String(raw || '').split(/\r?\n/);
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
const trimmed = line.trim();
|
|
95
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
96
|
+
const normalized = trimmed.startsWith('export ') ? trimmed.slice(7).trim() : trimmed;
|
|
97
|
+
const separatorIndex = normalized.indexOf('=');
|
|
98
|
+
if (separatorIndex <= 0) continue;
|
|
99
|
+
const key = normalized.slice(0, separatorIndex).trim();
|
|
100
|
+
if (!key) continue;
|
|
101
|
+
const value = normalized.slice(separatorIndex + 1);
|
|
102
|
+
env[key] = stripQuotes(value);
|
|
103
|
+
}
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function loadEnvFile(filePath) {
|
|
108
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
109
|
+
return parseEnvFileContents(raw);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function ensureQaRootStructure(paths, options = {}) {
|
|
113
|
+
const projectName = String(options.projectName || `taskforce-qa-${paths.agentId}`).trim();
|
|
114
|
+
fs.mkdirSync(paths.root, { recursive: true });
|
|
115
|
+
fs.mkdirSync(paths.projectRoot, { recursive: true });
|
|
116
|
+
fs.mkdirSync(paths.logsDir, { recursive: true });
|
|
117
|
+
fs.mkdirSync(paths.artifactsDir, { recursive: true });
|
|
118
|
+
fs.mkdirSync(paths.homeDir, { recursive: true });
|
|
119
|
+
|
|
120
|
+
const packageJsonPath = path.join(paths.projectRoot, 'package.json');
|
|
121
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
122
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify({ name: projectName }, null, 2) + '\n', 'utf8');
|
|
123
|
+
}
|
|
124
|
+
return packageJsonPath;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function resolveExpectedWorkspace(env) {
|
|
128
|
+
const workspaceId = String(env.TASKFORCE_QA_EXPECTED_WORKSPACE_ID || '').trim();
|
|
129
|
+
const workspaceSlug = String(env.TASKFORCE_QA_EXPECTED_WORKSPACE_SLUG || '').trim();
|
|
130
|
+
if (!workspaceId && !workspaceSlug) return null;
|
|
131
|
+
return {
|
|
132
|
+
workspaceId: workspaceId || null,
|
|
133
|
+
workspaceSlug: workspaceSlug || null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function matchExpectedWorkspace(result, expected) {
|
|
138
|
+
const nestedWorkspaces = Array.isArray(result?.workspaces?.workspaces) ? result.workspaces.workspaces : [];
|
|
139
|
+
const workspaces = Array.isArray(result?.workspaces) ? result.workspaces : nestedWorkspaces;
|
|
140
|
+
const currentWorkspaceId = String(result?.currentWorkspaceId || '').trim();
|
|
141
|
+
if (!expected) {
|
|
142
|
+
return {
|
|
143
|
+
ok: true,
|
|
144
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
145
|
+
reason: 'No expected workspace configured.'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (expected.workspaceId) {
|
|
150
|
+
return {
|
|
151
|
+
ok: currentWorkspaceId === expected.workspaceId,
|
|
152
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
153
|
+
reason: currentWorkspaceId === expected.workspaceId
|
|
154
|
+
? 'Matched expected workspace id.'
|
|
155
|
+
: `Expected workspace id ${expected.workspaceId} but current workspace id was ${currentWorkspaceId || '(empty)'}.`
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const matchedBySlug = workspaces.find((workspace) => String(workspace?.slug || '').trim() === expected.workspaceSlug);
|
|
160
|
+
if (!matchedBySlug) {
|
|
161
|
+
return {
|
|
162
|
+
ok: false,
|
|
163
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
164
|
+
reason: `Expected workspace slug ${expected.workspaceSlug} was not present in the available workspaces list.`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
ok: currentWorkspaceId === String(matchedBySlug.id || '').trim(),
|
|
170
|
+
matchedWorkspaceId: currentWorkspaceId || null,
|
|
171
|
+
reason: currentWorkspaceId === String(matchedBySlug.id || '').trim()
|
|
172
|
+
? 'Matched expected workspace slug.'
|
|
173
|
+
: `Expected workspace slug ${expected.workspaceSlug} to resolve to workspace id ${String(matchedBySlug.id || '').trim()}, but current workspace id was ${currentWorkspaceId || '(empty)'}.`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const QA_MODES: string[];
|
|
2
|
+
export const QA_CLOUD_BASE_URLS: Record<string, string>;
|
|
3
|
+
|
|
4
|
+
export function normalizeQaMode(raw: unknown): string | null;
|
|
5
|
+
export function sanitizeQaAgentId(raw: unknown): string | null;
|
|
6
|
+
export function parseQaArgs(
|
|
7
|
+
argv?: string[],
|
|
8
|
+
env?: Record<string, string | undefined>
|
|
9
|
+
): { mode: string | null; agentId: string | null; preferredPort: number | null };
|
|
10
|
+
export function resolveQaPaths(input: {
|
|
11
|
+
agentId: string;
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
}): {
|
|
14
|
+
agentId: string;
|
|
15
|
+
root: string;
|
|
16
|
+
projectRoot: string;
|
|
17
|
+
envFile: string;
|
|
18
|
+
logsDir: string;
|
|
19
|
+
artifactsDir: string;
|
|
20
|
+
homeDir: string;
|
|
21
|
+
playwrightStorageState: string;
|
|
22
|
+
};
|
|
23
|
+
export function isPathInside(parentPath: string, childPath: string): boolean;
|
|
24
|
+
export function assertQaProjectRootSafe(projectRoot: string, repoRoot: string): void;
|
|
25
|
+
export function parseEnvFileContents(raw: unknown): Record<string, string>;
|
|
26
|
+
export function loadEnvFile(filePath: string): Record<string, string>;
|
|
27
|
+
export function ensureQaRootStructure(
|
|
28
|
+
paths: {
|
|
29
|
+
agentId: string;
|
|
30
|
+
root: string;
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
logsDir: string;
|
|
33
|
+
artifactsDir: string;
|
|
34
|
+
homeDir: string;
|
|
35
|
+
},
|
|
36
|
+
options?: { projectName?: string }
|
|
37
|
+
): string;
|
|
38
|
+
export function resolveExpectedWorkspace(env: Record<string, string | undefined>): {
|
|
39
|
+
workspaceId: string | null;
|
|
40
|
+
workspaceSlug: string | null;
|
|
41
|
+
} | null;
|
|
42
|
+
export function matchExpectedWorkspace(
|
|
43
|
+
result: unknown,
|
|
44
|
+
expected: { workspaceId: string | null; workspaceSlug: string | null } | null
|
|
45
|
+
): {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
matchedWorkspaceId: string | null;
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|