@taskforcehq/taskforce 0.3.301 → 0.3.303

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/Taskforce.module.css +37 -2
  2. package/dist/TaskforceCore.filter-persistence.test.js +1 -1
  3. package/dist/TaskforceCore.js +72 -33
  4. package/dist/TaskforceCore.test.js +848 -130
  5. package/dist/components/features/TaskSettings.js +14 -82
  6. package/dist/components/features/TaskSettings.test.js +33 -70
  7. package/dist/components/features/settings/TaxonomyLibraryManager.js +8 -2
  8. package/dist/components/features/settings/TaxonomyLibraryManager.test.js +1 -1
  9. package/dist/components/task/TaskActionHeader.js +6 -3
  10. package/dist/components/task/TaskCard.js +6 -3
  11. package/dist/components/task/TaskForm.js +2 -2
  12. package/dist/components/task/TaskForm.test.js +41 -2
  13. package/dist/components/task/TaskKanban.test.js +17 -0
  14. package/dist/components/ui/ReferenceBadgeButton.js +1 -1
  15. package/dist/components/views/PlansPage.d.ts +3 -1
  16. package/dist/components/views/PlansPage.js +109 -49
  17. package/dist/components/views/PlansPage.test.d.ts +1 -0
  18. package/dist/components/views/PlansPage.test.js +100 -0
  19. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  20. package/dist/components/views/StandaloneLayout.js +473 -256
  21. package/dist/components/views/panels/PlanningDrawer.js +18 -1
  22. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +48 -0
  23. package/dist/components/views/standalone/modals/AccountHubModal.js +36 -0
  24. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  25. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  26. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.d.ts +9 -0
  27. package/dist/components/views/standalone/modals/CreateWorkspaceConfirmModal.js +8 -0
  28. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +26 -0
  29. package/dist/components/views/standalone/modals/EditProfileModal.js +10 -0
  30. package/dist/components/views/standalone/modals/HelpModal.d.ts +9 -0
  31. package/dist/components/views/standalone/modals/HelpModal.js +8 -0
  32. package/dist/components/views/standalone/modals/ScheduleWarningModal.d.ts +13 -0
  33. package/dist/components/views/standalone/modals/ScheduleWarningModal.js +6 -0
  34. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +61 -0
  35. package/dist/components/views/standalone/modals/SyncStatusModal.js +38 -0
  36. package/dist/components/views/standalone/modals/TeamManagementModal.d.ts +64 -0
  37. package/dist/components/views/standalone/modals/TeamManagementModal.js +10 -0
  38. package/dist/components/views/standalone/modals/types.d.ts +2 -0
  39. package/dist/components/views/standalone/modals/types.js +1 -0
  40. package/dist/components/views/teamManagementAccess.d.ts +1 -1
  41. package/dist/components/views/teamManagementAccess.js +2 -2
  42. package/dist/components/views/teamManagementAccess.test.js +3 -3
  43. package/dist/config/deployment.js +2 -2
  44. package/dist/config/deployment.test.js +5 -3
  45. package/dist/core/AiProfileService.js +7 -2
  46. package/dist/core/AiProfiles.test.js +19 -0
  47. package/dist/core/EntitlementsPolicy.test.js +2 -2
  48. package/dist/core/GlobalSettingsService.js +78 -18
  49. package/dist/core/PlanEntitlementService.d.ts +29 -5
  50. package/dist/core/PlanEntitlementService.js +297 -219
  51. package/dist/core/SignupMonetizationSettings.test.js +173 -49
  52. package/dist/core/SyncReconciliationService.d.ts +2 -0
  53. package/dist/core/SyncReconciliationService.js +3 -0
  54. package/dist/core/SystemAdmin.test.js +186 -96
  55. package/dist/core/TaskAuditTimeline.test.js +40 -0
  56. package/dist/core/Taskforce.d.ts +100 -10
  57. package/dist/core/Taskforce.ids.test.js +133 -0
  58. package/dist/core/Taskforce.js +459 -127
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.d.ts +1 -0
  61. package/dist/core/UserProfileAvatarDrafts.test.js +111 -0
  62. package/dist/core/taskReferencePolicy.d.ts +28 -0
  63. package/dist/core/taskReferencePolicy.js +189 -0
  64. package/dist/core/types.d.ts +13 -12
  65. package/dist/hooks/tasks/useTaskFormActions.js +3 -0
  66. package/dist/hooks/useSyncOrchestrator.js +17 -3
  67. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +97 -5
  68. package/dist/hooks/useTaskData.js +3 -2
  69. package/dist/hooks/useTaskData.test.js +66 -0
  70. package/dist/hooks/useTaskforce.d.ts +8 -0
  71. package/dist/hooks/useTaskforce.js +72 -19
  72. package/dist/hooks/useTaskforce.sync-behavior.test.js +185 -6
  73. package/dist/mcp/aiProfileBootstrap.d.ts +29 -0
  74. package/dist/mcp/aiProfileBootstrap.js +80 -0
  75. package/dist/mcp/aiProfileBootstrap.test.d.ts +1 -0
  76. package/dist/mcp/aiProfileBootstrap.test.js +68 -0
  77. package/dist/mcp/clientIntegrations.d.ts +2 -8
  78. package/dist/mcp/clientIntegrations.js +21 -40
  79. package/dist/mcp/clientIntegrations.test.js +28 -2
  80. package/dist/mcp/clientProfileDefaults.d.ts +12 -0
  81. package/dist/mcp/clientProfileDefaults.js +37 -0
  82. package/dist/mcp/runtime.d.ts +10 -7
  83. package/dist/mcp/runtime.js +131 -48
  84. package/dist/mcp/runtime.test.js +321 -83
  85. package/dist/migrations/billingSchemaParity.test.js +3 -0
  86. package/dist/migrations/taskSchemaMigrations.js +68 -1
  87. package/dist/qaOpen.test.d.ts +1 -0
  88. package/dist/qaOpen.test.js +56 -0
  89. package/dist/server/index.cookieProxy.test.js +2 -0
  90. package/dist/server/index.d.ts +1 -0
  91. package/dist/server/index.js +17 -1
  92. package/dist/server/index.test.js +15 -1
  93. package/dist/server/mockStripe.d.ts +1 -0
  94. package/dist/server/mockStripe.js +2 -0
  95. package/dist/server/routes/admin.js +105 -45
  96. package/dist/server/routes/auth.d.ts +6 -0
  97. package/dist/server/routes/auth.js +505 -30
  98. package/dist/server/routes/billing.js +517 -24
  99. package/dist/server/routes/billing.test.js +967 -45
  100. package/dist/server/routes/documents.js +6 -1
  101. package/dist/server/routes/shared.d.ts +1 -1
  102. package/dist/server/routes/shared.js +16 -6
  103. package/dist/server/routes/sync.integration.test.js +290 -0
  104. package/dist/server/routes/sync.js +234 -1922
  105. package/dist/server/routes/tasks.js +18 -4
  106. package/dist/server/routes.js +29 -22
  107. package/dist/server/routes.test.js +639 -82
  108. package/dist/shared/runtimeContract.d.ts +1 -1
  109. package/dist/sync/cloudSyncApi.d.ts +1 -0
  110. package/dist/sync/collaborationSyncPayload.d.ts +17 -0
  111. package/dist/sync/collaborationSyncPayload.js +131 -0
  112. package/dist/sync/collaborationSyncPayload.test.d.ts +1 -0
  113. package/dist/sync/collaborationSyncPayload.test.js +104 -0
  114. package/dist/sync/collaborationSyncState.d.ts +15 -0
  115. package/dist/sync/collaborationSyncState.js +60 -0
  116. package/dist/sync/contentSyncPayload.d.ts +45 -0
  117. package/dist/sync/contentSyncPayload.js +104 -0
  118. package/dist/sync/contentSyncPayload.test.d.ts +1 -0
  119. package/dist/sync/contentSyncPayload.test.js +104 -0
  120. package/dist/sync/contentSyncState.d.ts +98 -0
  121. package/dist/sync/contentSyncState.js +1004 -0
  122. package/dist/sync/planningSyncPayload.d.ts +9 -0
  123. package/dist/sync/planningSyncPayload.js +82 -0
  124. package/dist/sync/planningSyncPayload.test.d.ts +1 -0
  125. package/dist/sync/planningSyncPayload.test.js +68 -0
  126. package/dist/sync/syncApplyHandlers.d.ts +83 -0
  127. package/dist/sync/syncApplyHandlers.js +445 -0
  128. package/dist/sync/syncService.d.ts +1 -0
  129. package/dist/sync/syncService.js +21 -8
  130. package/dist/sync/taskSyncPayload.d.ts +33 -0
  131. package/dist/sync/taskSyncPayload.js +126 -0
  132. package/dist/sync/taskSyncPayload.test.d.ts +1 -0
  133. package/dist/sync/taskSyncPayload.test.js +54 -0
  134. package/dist/sync/workspacePullFeed.d.ts +38 -0
  135. package/dist/sync/workspacePullFeed.js +349 -0
  136. package/dist/sync/workspacePullFeed.test.d.ts +1 -0
  137. package/dist/sync/workspacePullFeed.test.js +278 -0
  138. package/dist/sync/workspaceSyncModel.d.ts +4 -1
  139. package/dist/sync/workspaceSyncModel.js +9 -2
  140. package/dist/sync/workspaceSyncModel.test.js +29 -0
  141. package/dist/test/setup.js +1 -1
  142. package/dist/types.d.ts +1 -0
  143. package/dist/ui/assets/{AgentsModule-Bdq3T1Ts.js → AgentsModule-CpsTmrIW.js} +1 -1
  144. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-jKUVo-PJ.js → AnnotatedAttachmentWorkspace-C_TmXZwA.js} +1 -1
  145. package/dist/ui/assets/{DocumentWorkspace-DuQa-uIC.js → DocumentWorkspace-C3GyzrWm.js} +2 -2
  146. package/dist/ui/assets/{InitiativesModule-C8Pi2VTk.js → InitiativesModule-4-Rh2K2n.js} +1 -1
  147. package/dist/ui/assets/PlansPage-BP7AYOqr.js +1 -0
  148. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +8 -0
  149. package/dist/ui/assets/{TaskSettings-BwB9NtmJ.css → TaskSettings-CnIBL_Eb.css} +1 -1
  150. package/dist/ui/assets/{WorkflowsModule-b6TLuHwJ.js → WorkflowsModule-CGk9s3NJ.js} +1 -1
  151. package/dist/ui/assets/index-CLIMgR6g.js +6 -0
  152. package/dist/ui/assets/index-DneETxcu.css +1 -0
  153. package/dist/ui/assets/{vendor-icons-QZyhEwgT.js → vendor-icons-pWocEipT.js} +1 -1
  154. package/dist/ui/index.html +3 -3
  155. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  156. package/dist/utils/accountProfileSummaryCache.js +97 -0
  157. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  158. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  159. package/dist/utils/taskActivity.js +17 -4
  160. package/dist/utils/taskActivity.test.js +27 -0
  161. package/dist/utils/taskNormalization.js +10 -2
  162. package/dist/utils/taskReferences.d.ts +11 -1
  163. package/dist/utils/taskReferences.js +33 -1
  164. package/package.json +4 -3
  165. package/scripts/qa-open-lib.mjs +175 -0
  166. package/scripts/qa-open-lib.mjs.d.ts +49 -0
  167. package/scripts/qa-open.mjs +352 -0
  168. package/dist/ui/assets/PlansPage-B6BDECSi.js +0 -1
  169. package/dist/ui/assets/TaskSettings-DUJgRTPU.js +0 -8
  170. package/dist/ui/assets/index-BiiF1cLQ.css +0 -1
  171. package/dist/ui/assets/index-xBWLPOEb.js +0 -6
@@ -24,9 +24,11 @@
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
26
  import { normalizeTaskDataRelativePath } from '../../storage/objectStorageClient.js';
27
+ import { shouldUseProvisionalTaskReferences } from '../../utils/taskReferences.js';
27
28
  import { purgeExpiredDocuments } from '../../storage/documentPurge.js';
28
29
  import { parsePlan } from '../../utils/planParser.js';
29
30
  import { getCanonicalDocumentName, getReadableDocumentName, isGeneratedDocumentName } from '../../utils/documentNames.js';
31
+ import { requestRuntimeMode } from './shared.js';
30
32
  import { parseJsonBody } from '../routes.js';
31
33
  export function registerDocumentRoutes(deps) {
32
34
  const { addRoute, core, context, requestWorkspaceId, ensureAdminRole, resolveEffectiveObjectStorageConfig, getObjectStorageClient, resolveStorageWorkspaceId, ensureContextUploadAllowed, parseStorageFolderSegments, buildCanonicalWorkspaceAssetRelativePath, inferAssetKindFromPath, appendStorageTelemetry, scanAndRecordDocument, buildTaskAttachmentPayload, workspaceAssetStore, taskAssetStore, normalizeDocumentPathInput, getAssetMetadata, getCanonicalAssetMetadata, documentRetentionDays, appendDocumentPurgeHistory, markAssetDeleted, isAssetDeleted, clearAssetDeleted, ensureWithinDir, sanitizeDownloadFilename, contentTypeForExt, inferInlineDocumentFromPath, resolveCanonicalDocumentRead, inferWorkspaceIdFromStoragePath, inferTaskIdFromStoragePath, } = deps;
@@ -1097,7 +1099,10 @@ export function registerDocumentRoutes(deps) {
1097
1099
  type: node.type || 'feature',
1098
1100
  priority: typeof node.priority === 'number' ? node.priority : 2,
1099
1101
  status: 'task',
1100
- }, { workspaceId });
1102
+ }, {
1103
+ workspaceId,
1104
+ provisionalLocalReference: shouldUseProvisionalTaskReferences(requestRuntimeMode(req))
1105
+ });
1101
1106
  created.push(taskItem.id);
1102
1107
  totalCreated++;
1103
1108
  }
@@ -30,6 +30,6 @@ export declare function makeResolveAdminRequestActor(core: TaskforceCore, contex
30
30
  systemRole: "system_admin" | "user";
31
31
  };
32
32
  export declare function makeAuditAdminAction(core: TaskforceCore, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
33
- export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
33
+ export declare function makeAuditAuthAction(core: TaskforceCore, requestWorkspaceId: (req: IncomingMessage) => string, context: RouteContext): (req: IncomingMessage, action: string, details: Record<string, unknown>) => void;
34
34
  export declare function makeRequireCloudRuntime(context: RouteContext): (res: ServerResponse, feature: string) => boolean;
35
35
  export declare function makeEnsureCloudPlanData(context: RouteContext): (req: IncomingMessage, res: ServerResponse) => boolean;
@@ -167,9 +167,11 @@ export function makeEnsureAppAccess(core, context) {
167
167
  return true;
168
168
  const code = accountAccess.gate === 'plan_selection_required'
169
169
  ? 'PLAN_SELECTION_REQUIRED'
170
- : accountAccess.gate === 'misconfigured'
171
- ? 'ACCOUNT_ACCESS_MISCONFIGURED'
172
- : 'ACCOUNT_ENTITLEMENT_REQUIRED';
170
+ : accountAccess.gate === 'checkout_pending'
171
+ ? 'CHECKOUT_PENDING'
172
+ : accountAccess.gate === 'misconfigured'
173
+ ? 'ACCOUNT_ACCESS_MISCONFIGURED'
174
+ : 'ACCOUNT_ENTITLEMENT_REQUIRED';
173
175
  res.writeHead(403, { 'Content-Type': 'application/json' });
174
176
  res.end(JSON.stringify({
175
177
  error: accountAccess.message || 'This account cannot access the app.',
@@ -257,13 +259,21 @@ export function makeAuditAdminAction(core, context) {
257
259
  console.info(`[Taskforce Admin Audit] ${JSON.stringify(payload)}`);
258
260
  };
259
261
  }
260
- export function makeAuditAuthAction(core, requestWorkspaceId) {
262
+ export function makeAuditAuthAction(core, requestWorkspaceId, context) {
261
263
  return function auditAuthAction(req, action, details) {
264
+ const authConfig = context.authConfig;
265
+ const resolvedAccess = authConfig?.enabled ? resolveRequestAccess(req, authConfig) : null;
266
+ const actorUserId = resolvedAccess?.authenticated && resolvedAccess.userId && resolvedAccess.userId !== 'anonymous'
267
+ ? resolvedAccess.userId
268
+ : requestUserId(req);
269
+ const actorRole = resolvedAccess?.authenticated
270
+ ? String(resolvedAccess.role || '').trim().toLowerCase() || requestRole(req)
271
+ : requestRole(req);
262
272
  const payload = {
263
273
  ts: new Date().toISOString(),
264
274
  action,
265
- actorUserId: requestUserId(req),
266
- actorRole: requestRole(req),
275
+ actorUserId,
276
+ actorRole,
267
277
  workspaceId: requestWorkspaceId(req),
268
278
  details
269
279
  };
@@ -140,6 +140,49 @@ describe('Server Routes Workspace Sync Integration', () => {
140
140
  expect(task?.status).toBe('in-progress');
141
141
  expect(defaultWorkspaceTasks.find((item) => item.id === 'task-cloud-1')).toBeUndefined();
142
142
  });
143
+ it('lets the cloud task keep a colliding reference number during apply-local and records a diagnostic', async () => {
144
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-task-ref-collision-');
145
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
146
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
147
+ const localTask = core.createTask({ title: 'Local incumbent' }, { workspaceId });
148
+ expect(localTask.referenceLabel).toBe('T-1');
149
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
150
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
151
+ expect(match).toBeTruthy();
152
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
153
+ changes: [
154
+ {
155
+ op: 'upsert',
156
+ archived: false,
157
+ task: {
158
+ id: 'task-cloud-ref-1',
159
+ title: 'Cloud winner',
160
+ status: 'task',
161
+ referenceNumber: 1,
162
+ updatedAt: '2026-04-10T16:00:00.000Z'
163
+ }
164
+ }
165
+ ]
166
+ }, { 'x-taskforce-workspace-id': workspaceId });
167
+ const res = createMockRes();
168
+ await match?.handler(req, res, {});
169
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
170
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
171
+ expect(reloadedCore.getTask('task-cloud-ref-1', workspaceId)?.referenceLabel).toBe('T-1');
172
+ expect(reloadedCore.getTask(localTask.id, workspaceId)?.referenceLabel).toBe('T-2');
173
+ expect(reloadedCore.listSyncEvents(workspaceId, 10)).toEqual(expect.arrayContaining([
174
+ expect.objectContaining({
175
+ eventType: 'apply',
176
+ status: 'error',
177
+ errorMessage: 'Detected task reference number mismatch during normal sync.',
178
+ details: expect.objectContaining({
179
+ referenceLabel: 'T-1',
180
+ existingTaskId: localTask.id,
181
+ taskId: 'task-cloud-ref-1'
182
+ })
183
+ })
184
+ ]));
185
+ });
143
186
  it('persists synced comments via apply-local route', async () => {
144
187
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-comments-');
145
188
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
@@ -1268,6 +1311,166 @@ describe('Server Routes Workspace Sync Integration', () => {
1268
1311
  })
1269
1312
  ]);
1270
1313
  });
1314
+ it('lets the cloud document ref win during normal sync apply and records a diagnostic', async () => {
1315
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
1316
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1317
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1318
+ core.createTask({
1319
+ id: 'task-doc-ref-drift-1',
1320
+ title: 'Task with drifting synced markdown asset',
1321
+ status: 'task'
1322
+ }, {
1323
+ workspaceId,
1324
+ allowClientProvidedId: true
1325
+ });
1326
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1327
+ seededStore.upsertAsset({
1328
+ assetId: 'asset-doc-ref-drift-1',
1329
+ workspaceId,
1330
+ kind: 'document',
1331
+ originalFilename: 'phase1-ref-drift.md',
1332
+ mimeType: 'text/markdown',
1333
+ storageProvider: 'local',
1334
+ storageKey: 'docs/phase1-ref-drift.md',
1335
+ contentSha256: 'sha-doc-ref-drift-local',
1336
+ sizeBytes: 10,
1337
+ documentId: 'doc-ref-drift-1',
1338
+ referenceNumber: 12,
1339
+ createdAt: '2026-03-17T11:55:00.000Z',
1340
+ updatedAt: '2026-03-17T11:55:00.000Z'
1341
+ });
1342
+ seededStore.upsertLink({
1343
+ workspaceId,
1344
+ assetId: 'asset-doc-ref-drift-1',
1345
+ taskId: 'task-doc-ref-drift-1',
1346
+ role: 'attachment',
1347
+ updatedAt: '2026-03-17T11:55:00.000Z',
1348
+ deletedAt: null
1349
+ });
1350
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1351
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1352
+ expect(match).toBeTruthy();
1353
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1354
+ changes: [
1355
+ {
1356
+ op: 'document-upsert',
1357
+ path: 'docs/phase1-ref-drift.md',
1358
+ updatedAt: '2026-03-17T12:00:00.000Z',
1359
+ content: '# cloud canonical markdown\n',
1360
+ assetId: 'asset-doc-ref-drift-1',
1361
+ documentId: 'doc-ref-drift-1',
1362
+ referenceNumber: 18,
1363
+ version: 2,
1364
+ taskId: 'task-doc-ref-drift-1',
1365
+ originalFilename: 'phase1-ref-drift.md',
1366
+ linkRole: 'attachment'
1367
+ }
1368
+ ]
1369
+ }, { 'x-taskforce-workspace-id': workspaceId });
1370
+ const res = createMockRes();
1371
+ await match?.handler(req, res, {});
1372
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1373
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1374
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1375
+ const asset = store.get('asset-doc-ref-drift-1', workspaceId);
1376
+ const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1377
+ expect(asset).toEqual(expect.objectContaining({
1378
+ assetId: 'asset-doc-ref-drift-1',
1379
+ referenceNumber: 18,
1380
+ version: 2
1381
+ }));
1382
+ expect(syncEvents).toEqual(expect.arrayContaining([
1383
+ expect.objectContaining({
1384
+ eventType: 'apply',
1385
+ status: 'error',
1386
+ errorMessage: 'Detected document reference number mismatch during normal sync.',
1387
+ details: expect.objectContaining({
1388
+ assetId: 'asset-doc-ref-drift-1',
1389
+ existingReferenceNumber: 12,
1390
+ incomingReferenceNumber: 18
1391
+ })
1392
+ })
1393
+ ]));
1394
+ });
1395
+ it('lets the cloud image ref win during normal sync apply and records a diagnostic', async () => {
1396
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
1397
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1398
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1399
+ core.createTask({
1400
+ id: 'task-image-ref-drift-1',
1401
+ title: 'Task with drifting synced image asset',
1402
+ status: 'task'
1403
+ }, {
1404
+ workspaceId,
1405
+ allowClientProvidedId: true
1406
+ });
1407
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1408
+ seededStore.upsertAsset({
1409
+ assetId: 'asset-image-ref-drift-1',
1410
+ workspaceId,
1411
+ kind: 'image',
1412
+ originalFilename: 'hero.png',
1413
+ mimeType: 'image/png',
1414
+ storageProvider: 'local',
1415
+ storageKey: 'workspaces/workspace-local-active/assets/images/hero.png',
1416
+ contentSha256: 'sha-image-ref-drift-local',
1417
+ sizeBytes: 10,
1418
+ referenceNumber: 12,
1419
+ createdAt: '2026-03-17T11:55:00.000Z',
1420
+ updatedAt: '2026-03-17T11:55:00.000Z'
1421
+ });
1422
+ seededStore.upsertLink({
1423
+ workspaceId,
1424
+ assetId: 'asset-image-ref-drift-1',
1425
+ taskId: 'task-image-ref-drift-1',
1426
+ role: 'image',
1427
+ updatedAt: '2026-03-17T11:55:00.000Z',
1428
+ deletedAt: null
1429
+ });
1430
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1431
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1432
+ expect(match).toBeTruthy();
1433
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1434
+ changes: [
1435
+ {
1436
+ op: 'asset-upsert',
1437
+ path: 'workspaces/workspace-local-active/assets/images/hero.png',
1438
+ updatedAt: '2026-03-17T12:05:00.000Z',
1439
+ contentBase64: Buffer.from('cloud-hero').toString('base64'),
1440
+ assetId: 'asset-image-ref-drift-1',
1441
+ kind: 'image',
1442
+ mimeType: 'image/png',
1443
+ referenceNumber: 18,
1444
+ taskId: 'task-image-ref-drift-1',
1445
+ originalFilename: 'hero.png',
1446
+ linkRole: 'image'
1447
+ }
1448
+ ]
1449
+ }, { 'x-taskforce-workspace-id': workspaceId });
1450
+ const res = createMockRes();
1451
+ await match?.handler(req, res, {});
1452
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1453
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1454
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1455
+ const asset = store.get('asset-image-ref-drift-1', workspaceId);
1456
+ const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1457
+ expect(asset).toEqual(expect.objectContaining({
1458
+ assetId: 'asset-image-ref-drift-1',
1459
+ referenceNumber: 18
1460
+ }));
1461
+ expect(syncEvents).toEqual(expect.arrayContaining([
1462
+ expect.objectContaining({
1463
+ eventType: 'apply',
1464
+ status: 'error',
1465
+ errorMessage: 'Detected asset reference number mismatch during normal sync.',
1466
+ details: expect.objectContaining({
1467
+ assetId: 'asset-image-ref-drift-1',
1468
+ existingReferenceNumber: 12,
1469
+ incomingReferenceNumber: 18
1470
+ })
1471
+ })
1472
+ ]));
1473
+ });
1271
1474
  it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
1272
1475
  const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
1273
1476
  const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-local-');
@@ -1487,6 +1690,93 @@ describe('Server Routes Workspace Sync Integration', () => {
1487
1690
  })
1488
1691
  ]);
1489
1692
  });
1693
+ it('omits local provisional reference fields from authenticated pull payloads', async () => {
1694
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-pull-task-ref-payload-');
1695
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1696
+ seedAuthenticatedWorkspace(seedCore);
1697
+ const created = seedCore.createTask({
1698
+ title: 'Offline draft',
1699
+ status: 'task'
1700
+ }, {
1701
+ workspaceId,
1702
+ provisionalLocalReference: true
1703
+ });
1704
+ expect(created.localReferenceNumber).toBe(1);
1705
+ expect(created.referenceLabel).toBe('LT-1');
1706
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1707
+ const routes = createRoutes(core, { authConfig });
1708
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1709
+ expect(pullMatch).toBeTruthy();
1710
+ const cookie = createAuthCookie(workspaceId);
1711
+ const pullReq = createMockReq('GET', '/api/taskforce/sync/workspace/pull?workspaceId=workspace-local-active&limit=200', undefined, { cookie });
1712
+ const pullRes = createMockRes();
1713
+ await pullMatch?.handler(pullReq, pullRes, {});
1714
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1715
+ const pullBody = readJsonBody(pullRes);
1716
+ const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1717
+ .find((change) => change?.op === 'upsert' && change?.task?.id === created.id);
1718
+ expect(upsert?.task).toEqual(expect.objectContaining({
1719
+ id: created.id,
1720
+ title: 'Offline draft',
1721
+ referenceNumber: null
1722
+ }));
1723
+ expect(upsert?.task).not.toHaveProperty('localReferenceNumber');
1724
+ expect(upsert?.task).not.toHaveProperty('referenceLabel');
1725
+ });
1726
+ it('returns a pushed provisional task on pull even when the caller cursor is newer than the stale local watermark', async () => {
1727
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-pull-provisional-task-');
1728
+ const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1729
+ seedAuthenticatedWorkspace(seedCore);
1730
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1731
+ const routes = createRoutes(core, { authConfig });
1732
+ const pushMatch = matchRoute('POST', '/api/taskforce/sync/workspace/push', routes);
1733
+ const pullMatch = matchRoute('GET', '/api/taskforce/sync/workspace/pull', routes);
1734
+ expect(pushMatch).toBeTruthy();
1735
+ expect(pullMatch).toBeTruthy();
1736
+ const cookie = createAuthCookie(workspaceId);
1737
+ const taskId = 'task-provisional-roundtrip-1';
1738
+ const staleUpdatedAt = '2026-04-11T21:29:04.121Z';
1739
+ const pushReq = createMockReq('POST', '/api/taskforce/sync/workspace/push', {
1740
+ workspaceId,
1741
+ idempotencyKey: 'task-provisional-roundtrip-1',
1742
+ changes: [
1743
+ {
1744
+ op: 'upsert',
1745
+ archived: false,
1746
+ task: {
1747
+ id: taskId,
1748
+ title: 'Offline provisional task',
1749
+ status: 'task',
1750
+ createdAt: '2026-04-11T21:28:52.410Z',
1751
+ updatedAt: staleUpdatedAt,
1752
+ referenceNumber: null
1753
+ }
1754
+ }
1755
+ ]
1756
+ }, { cookie });
1757
+ const pushRes = createMockRes();
1758
+ await pushMatch?.handler(pushReq, pushRes, {});
1759
+ expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1760
+ const persisted = core.getTask(taskId, workspaceId);
1761
+ expect(persisted?.referenceNumber).toBe(1);
1762
+ expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse(staleUpdatedAt));
1763
+ const cursor = Buffer.from(JSON.stringify({
1764
+ u: '2026-04-11T21:29:05.000Z',
1765
+ id: 'seed'
1766
+ })).toString('base64url');
1767
+ const pullReq = createMockReq('GET', `/api/taskforce/sync/workspace/pull?workspaceId=${workspaceId}&limit=200&cursor=${cursor}`, undefined, { cookie });
1768
+ const pullRes = createMockRes();
1769
+ await pullMatch?.handler(pullReq, pullRes, {});
1770
+ expect(pullRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1771
+ const pullBody = readJsonBody(pullRes);
1772
+ const upsert = (Array.isArray(pullBody?.changes) ? pullBody.changes : [])
1773
+ .find((change) => change?.op === 'upsert' && change?.task?.id === taskId);
1774
+ expect(upsert?.task).toEqual(expect.objectContaining({
1775
+ id: taskId,
1776
+ title: 'Offline provisional task',
1777
+ referenceNumber: 1
1778
+ }));
1779
+ });
1490
1780
  it('keeps document deleted when push batch includes upsert and delete for same path', async () => {
1491
1781
  const projectRoot = createIsolatedProjectRoot('taskforce-sync-push-doc-delete-wins-');
1492
1782
  const seedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });