@taskforcehq/taskforce 0.3.302 → 0.3.304

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 (192) hide show
  1. package/dist/Taskforce.module.css +113 -5
  2. package/dist/TaskforceCore.js +560 -208
  3. package/dist/TaskforceCore.test.js +1833 -91
  4. package/dist/annotatedAttachments/service.d.ts +1 -1
  5. package/dist/annotatedAttachments/types.d.ts +1 -1
  6. package/dist/components/features/AgentsModule.js +1 -1
  7. package/dist/components/features/AgentsModule.test.js +1 -1
  8. package/dist/components/features/TaskSettings.js +58 -38
  9. package/dist/components/features/TaskSettings.test.js +139 -40
  10. package/dist/components/task/TaskActionHeader.d.ts +2 -1
  11. package/dist/components/task/TaskActionHeader.js +3 -3
  12. package/dist/components/task/TaskActionHeader.test.js +8 -0
  13. package/dist/components/task/TaskCard.js +16 -1
  14. package/dist/components/task/TaskCard.test.js +31 -0
  15. package/dist/components/task/TaskForm.d.ts +2 -1
  16. package/dist/components/task/TaskForm.js +22 -4
  17. package/dist/components/task/TaskForm.test.js +46 -0
  18. package/dist/components/task/TaskKanban.d.ts +1 -1
  19. package/dist/components/task/TaskKanban.js +31 -160
  20. package/dist/components/task/TaskKanban.test.js +17 -0
  21. package/dist/components/task/TaskStatusActions.d.ts +6 -1
  22. package/dist/components/task/TaskStatusActions.js +8 -3
  23. package/dist/components/task/TaskStatusActions.test.js +9 -0
  24. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  25. package/dist/components/task/taskKanbanDropRules.js +187 -0
  26. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  27. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  28. package/dist/components/ui/Modal.d.ts +1 -1
  29. package/dist/components/ui/Modal.js +1 -0
  30. package/dist/components/views/AppShellHeader.js +5 -1
  31. package/dist/components/views/AppShellHeader.test.js +4 -0
  32. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  33. package/dist/components/views/PlanComparisonPage.js +4 -2
  34. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  35. package/dist/components/views/PlansPage.d.ts +7 -1
  36. package/dist/components/views/PlansPage.js +139 -53
  37. package/dist/components/views/PlansPage.test.d.ts +1 -0
  38. package/dist/components/views/PlansPage.test.js +157 -0
  39. package/dist/components/views/StandaloneLayout.d.ts +1 -0
  40. package/dist/components/views/StandaloneLayout.js +216 -186
  41. package/dist/components/views/WidgetView.js +11 -2
  42. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +8 -3
  43. package/dist/components/views/standalone/modals/AccountHubModal.js +16 -2
  44. package/dist/components/views/standalone/modals/AccountHubModal.test.d.ts +1 -0
  45. package/dist/components/views/standalone/modals/AccountHubModal.test.js +26 -0
  46. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  47. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  48. package/dist/core/EntitlementsPolicy.test.js +2 -2
  49. package/dist/core/GlobalSettingsService.js +78 -18
  50. package/dist/core/PlanEntitlementService.d.ts +31 -5
  51. package/dist/core/PlanEntitlementService.js +350 -222
  52. package/dist/core/SignupMonetizationSettings.test.js +208 -49
  53. package/dist/core/SystemAdmin.test.js +186 -96
  54. package/dist/core/TaskActivityService.d.ts +4 -1
  55. package/dist/core/TaskActivityService.js +45 -0
  56. package/dist/core/TaskAuditTimeline.test.js +52 -1
  57. package/dist/core/Taskforce.d.ts +65 -20
  58. package/dist/core/Taskforce.js +261 -51
  59. package/dist/core/Taskforce.mcpAuth.test.js +0 -2
  60. package/dist/core/UserProfileAvatarDrafts.test.js +12 -3
  61. package/dist/core/WorkspacePlanMode.test.js +65 -0
  62. package/dist/core/types.d.ts +11 -12
  63. package/dist/documentReviews/service.d.ts +1 -1
  64. package/dist/documentReviews/types.d.ts +1 -1
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  66. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  67. package/dist/hooks/sync/auth.d.ts +37 -0
  68. package/dist/hooks/sync/auth.js +61 -0
  69. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  70. package/dist/hooks/sync/bootstrap.js +52 -0
  71. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  72. package/dist/hooks/sync/orchestratorShared.js +209 -0
  73. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  74. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  75. package/dist/hooks/sync/realtime.d.ts +25 -0
  76. package/dist/hooks/sync/realtime.js +60 -0
  77. package/dist/hooks/sync/recovery.d.ts +69 -0
  78. package/dist/hooks/sync/recovery.js +118 -0
  79. package/dist/hooks/sync/transfers.d.ts +122 -0
  80. package/dist/hooks/sync/transfers.js +431 -0
  81. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  82. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  83. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  84. package/dist/hooks/ui/useResourceExport.js +5 -2
  85. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  86. package/dist/hooks/useRealtimeSync.js +2 -1
  87. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  88. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  89. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  90. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  91. package/dist/hooks/useSyncOrchestrator.js +315 -860
  92. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +665 -8
  93. package/dist/hooks/useTaskMutations.js +45 -6
  94. package/dist/hooks/useTaskMutations.test.js +147 -0
  95. package/dist/hooks/useTaskforce.d.ts +11 -2
  96. package/dist/hooks/useTaskforce.js +290 -812
  97. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  98. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  99. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  100. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  101. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  102. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  103. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  104. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  105. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  106. package/dist/localization/locales/en-US.js +2 -2
  107. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  108. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  109. package/dist/mcp/clientIntegrations.d.ts +12 -1
  110. package/dist/mcp/clientIntegrations.js +161 -6
  111. package/dist/mcp/clientIntegrations.test.js +76 -4
  112. package/dist/mcp/documentHelpers.d.ts +100 -0
  113. package/dist/mcp/documentHelpers.js +161 -0
  114. package/dist/mcp/runtime.js +264 -505
  115. package/dist/mcp/runtime.test.js +353 -89
  116. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  117. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  118. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  119. package/dist/mcp/taskAttachmentTypes.js +1 -0
  120. package/dist/migrations/billingSchemaParity.test.js +3 -0
  121. package/dist/migrations/taskSchemaMigrations.js +15 -0
  122. package/dist/plugins/vite.js +1 -1
  123. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  124. package/dist/runtime/appGateDefinitions.js +1 -1
  125. package/dist/server/index.cookieProxy.test.js +1 -0
  126. package/dist/server/index.d.ts +1 -0
  127. package/dist/server/index.js +48 -3
  128. package/dist/server/index.test.js +14 -2
  129. package/dist/server/mockStripe.d.ts +1 -0
  130. package/dist/server/mockStripe.js +2 -0
  131. package/dist/server/routes/admin.js +91 -40
  132. package/dist/server/routes/auth.d.ts +1 -0
  133. package/dist/server/routes/auth.js +231 -55
  134. package/dist/server/routes/billing.js +473 -39
  135. package/dist/server/routes/billing.test.js +972 -37
  136. package/dist/server/routes/shared.d.ts +30 -0
  137. package/dist/server/routes/shared.js +31 -3
  138. package/dist/server/routes/sync.integration.test.js +180 -0
  139. package/dist/server/routes/sync.js +125 -242
  140. package/dist/server/routes/tasks.js +49 -7
  141. package/dist/server/routes.js +11 -22
  142. package/dist/server/routes.test.js +530 -81
  143. package/dist/shared/taskActor.d.ts +10 -0
  144. package/dist/shared/taskActor.js +1 -0
  145. package/dist/sync/cloudSyncApi.d.ts +1 -0
  146. package/dist/sync/collaborationSyncPayload.js +4 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -1
  149. package/dist/sync/collaborationSyncState.js +19 -0
  150. package/dist/sync/contentSyncState.d.ts +10 -0
  151. package/dist/sync/contentSyncState.js +277 -14
  152. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  153. package/dist/sync/syncApplyHandlers.js +370 -8
  154. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  155. package/dist/sync/syncApplyHandlers.test.js +377 -0
  156. package/dist/sync/syncService.d.ts +9 -1
  157. package/dist/sync/syncService.js +57 -10
  158. package/dist/sync/syncService.test.js +48 -0
  159. package/dist/sync/workspacePullFeed.d.ts +11 -0
  160. package/dist/sync/workspacePullFeed.js +123 -68
  161. package/dist/sync/workspacePullFeed.test.js +180 -0
  162. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  163. package/dist/sync/workspaceSyncModel.js +65 -0
  164. package/dist/sync/workspaceSyncModel.test.js +119 -0
  165. package/dist/types.d.ts +2 -10
  166. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  167. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-Dvi02Y7q.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  168. package/dist/ui/assets/{DocumentWorkspace-iys3zfca.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  169. package/dist/ui/assets/{InitiativesModule-DJvLJI3A.js → InitiativesModule-BjR6iBf9.js} +1 -1
  170. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  171. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  172. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  173. package/dist/ui/assets/{WorkflowsModule-BguHeQaV.js → WorkflowsModule-DnFqdUME.js} +1 -1
  174. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  175. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  176. package/dist/ui/assets/{vendor-icons-C8EED3Oh.js → vendor-icons-I7ZwG1z_.js} +1 -1
  177. package/dist/ui/index.html +4 -4
  178. package/dist/utils/accountProfileSummaryCache.d.ts +31 -0
  179. package/dist/utils/accountProfileSummaryCache.js +97 -0
  180. package/dist/utils/accountProfileSummaryCache.test.d.ts +1 -0
  181. package/dist/utils/accountProfileSummaryCache.test.js +63 -0
  182. package/dist/utils/taskActivity.js +4 -0
  183. package/dist/utils/taskActivity.test.js +25 -1
  184. package/package.json +5 -3
  185. package/scripts/playwright-auth.sh +4 -0
  186. package/dist/ui/assets/AgentsModule-JG5jc3he.js +0 -1
  187. package/dist/ui/assets/PlansPage-BMPlpFpw.js +0 -1
  188. package/dist/ui/assets/TaskSettings-Bqd4cCzW.js +0 -8
  189. package/dist/ui/assets/index-MXeWoebC.css +0 -1
  190. package/dist/ui/assets/index-YJ4k5QNa.js +0 -6
  191. package/dist/utils/billingStatusCache.d.ts +0 -21
  192. package/dist/utils/billingStatusCache.js +0 -70
@@ -7,8 +7,38 @@
7
7
  */
8
8
  import { IncomingMessage, ServerResponse } from 'http';
9
9
  import { TaskforceCore } from '../../core/Taskforce.js';
10
+ import type { McpAccessTokenAuthResult, McpOAuthAccessTokenAuthResult, McpAccessTokenScope } from '../../core/Taskforce.js';
10
11
  import type { RouteContext } from '../routes.js';
12
+ import type { AiProfileBootstrapConfig } from '../../mcp/aiProfileBootstrap.js';
11
13
  export declare function resolveErrorStatusCode(error: unknown, fallback?: number): number;
14
+ export declare function buildCloudMcpRuntimeAuth(input: {
15
+ req: IncomingMessage;
16
+ auth: {
17
+ authenticatedViaToken: McpAccessTokenAuthResult | null;
18
+ authenticatedViaOAuth: McpOAuthAccessTokenAuthResult | null;
19
+ userId: string | null;
20
+ } & Required<AiProfileBootstrapConfig>;
21
+ requestHeader: (req: IncomingMessage, name: string, fallback?: string) => string;
22
+ requestId?: string | null;
23
+ authScopes?: McpAccessTokenScope[] | null;
24
+ }): {
25
+ scopes?: McpAccessTokenScope[] | null;
26
+ userId?: string | null;
27
+ tokenId?: string | null;
28
+ aiProfileToken?: AiProfileBootstrapConfig['aiProfileToken'];
29
+ aiProfileName?: AiProfileBootstrapConfig['aiProfileName'];
30
+ aiProfileIcon?: AiProfileBootstrapConfig['aiProfileIcon'];
31
+ aiProfileColor?: AiProfileBootstrapConfig['aiProfileColor'];
32
+ aiProfileDescription?: AiProfileBootstrapConfig['aiProfileDescription'];
33
+ aiProfileRole?: AiProfileBootstrapConfig['aiProfileRole'];
34
+ aiProfileProvider?: AiProfileBootstrapConfig['aiProfileProvider'];
35
+ aiProfileModel?: AiProfileBootstrapConfig['aiProfileModel'];
36
+ aiProfileSurfaceType?: AiProfileBootstrapConfig['aiProfileSurfaceType'];
37
+ actorType?: 'local' | 'user' | 'token' | 'oauth';
38
+ requestId?: string | null;
39
+ ip?: string | null;
40
+ userAgent?: string | null;
41
+ };
12
42
  export declare function requestHeader(req: IncomingMessage, name: string, fallback?: string): string;
13
43
  export declare function requestWorkspaceHeader(req: IncomingMessage): string;
14
44
  export declare function requestAuthoritativeWorkspaceId(req: IncomingMessage): string;
@@ -17,6 +17,32 @@ export function resolveErrorStatusCode(error, fallback = 400) {
17
17
  }
18
18
  return fallback;
19
19
  }
20
+ export function buildCloudMcpRuntimeAuth(input) {
21
+ const { req, auth, requestHeader, requestId, authScopes } = input;
22
+ const actorType = auth.authenticatedViaToken
23
+ ? 'token'
24
+ : auth.authenticatedViaOAuth
25
+ ? 'oauth'
26
+ : 'user';
27
+ return {
28
+ scopes: authScopes || auth.authenticatedViaToken?.scopes || auth.authenticatedViaOAuth?.scopes || [],
29
+ userId: auth.authenticatedViaToken?.userId || auth.authenticatedViaOAuth?.userId || auth.userId || null,
30
+ tokenId: auth.authenticatedViaToken?.tokenId || auth.authenticatedViaOAuth?.grantId || null,
31
+ aiProfileToken: auth.aiProfileToken,
32
+ aiProfileName: auth.aiProfileName,
33
+ aiProfileIcon: auth.aiProfileIcon,
34
+ aiProfileColor: auth.aiProfileColor,
35
+ aiProfileDescription: auth.aiProfileDescription,
36
+ aiProfileRole: auth.aiProfileRole,
37
+ aiProfileProvider: auth.aiProfileProvider,
38
+ aiProfileModel: auth.aiProfileModel,
39
+ aiProfileSurfaceType: auth.aiProfileSurfaceType,
40
+ actorType,
41
+ requestId: String(requestId || req.headers['x-request-id'] || req.headers['x-railway-request-id'] || '').trim() || undefined,
42
+ ip: req.socket?.remoteAddress || null,
43
+ userAgent: requestHeader(req, 'user-agent', '') || null,
44
+ };
45
+ }
20
46
  // === SECTION: Request header helpers ===
21
47
  export function requestHeader(req, name, fallback = '') {
22
48
  const value = req.headers?.[name.toLowerCase()];
@@ -167,9 +193,11 @@ export function makeEnsureAppAccess(core, context) {
167
193
  return true;
168
194
  const code = accountAccess.gate === 'plan_selection_required'
169
195
  ? 'PLAN_SELECTION_REQUIRED'
170
- : accountAccess.gate === 'misconfigured'
171
- ? 'ACCOUNT_ACCESS_MISCONFIGURED'
172
- : 'ACCOUNT_ENTITLEMENT_REQUIRED';
196
+ : accountAccess.gate === 'checkout_pending'
197
+ ? 'CHECKOUT_PENDING'
198
+ : accountAccess.gate === 'misconfigured'
199
+ ? 'ACCOUNT_ACCESS_MISCONFIGURED'
200
+ : 'ACCOUNT_ENTITLEMENT_REQUIRED';
173
201
  res.writeHead(403, { 'Content-Type': 'application/json' });
174
202
  res.end(JSON.stringify({
175
203
  error: accountAccess.message || 'This account cannot access the app.',
@@ -344,6 +344,10 @@ describe('Server Routes Workspace Sync Integration', () => {
344
344
  username: 'codex',
345
345
  icon: 'Bot',
346
346
  color: '#0ea5e9',
347
+ description: 'Terminal-first coding agent',
348
+ role: 'Implementer',
349
+ provider: 'OpenAI',
350
+ model: 'gpt-5.4',
347
351
  surfaceType: 'coding_tool',
348
352
  createdAt: '2026-03-22T10:00:00.000Z',
349
353
  updatedAt: '2026-03-22T10:05:00.000Z',
@@ -361,6 +365,10 @@ describe('Server Routes Workspace Sync Integration', () => {
361
365
  id: 'ai-profile-codex',
362
366
  name: 'Codex',
363
367
  username: 'codex',
368
+ description: 'Terminal-first coding agent',
369
+ role: 'Implementer',
370
+ provider: 'OpenAI',
371
+ model: 'gpt-5.4',
364
372
  surfaceType: 'coding_tool'
365
373
  })
366
374
  ]);
@@ -1311,6 +1319,166 @@ describe('Server Routes Workspace Sync Integration', () => {
1311
1319
  })
1312
1320
  ]);
1313
1321
  });
1322
+ it('lets the cloud document ref win during normal sync apply and records a diagnostic', async () => {
1323
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-doc-ref-drift-');
1324
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1325
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1326
+ core.createTask({
1327
+ id: 'task-doc-ref-drift-1',
1328
+ title: 'Task with drifting synced markdown asset',
1329
+ status: 'task'
1330
+ }, {
1331
+ workspaceId,
1332
+ allowClientProvidedId: true
1333
+ });
1334
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1335
+ seededStore.upsertAsset({
1336
+ assetId: 'asset-doc-ref-drift-1',
1337
+ workspaceId,
1338
+ kind: 'document',
1339
+ originalFilename: 'phase1-ref-drift.md',
1340
+ mimeType: 'text/markdown',
1341
+ storageProvider: 'local',
1342
+ storageKey: 'docs/phase1-ref-drift.md',
1343
+ contentSha256: 'sha-doc-ref-drift-local',
1344
+ sizeBytes: 10,
1345
+ documentId: 'doc-ref-drift-1',
1346
+ referenceNumber: 12,
1347
+ createdAt: '2026-03-17T11:55:00.000Z',
1348
+ updatedAt: '2026-03-17T11:55:00.000Z'
1349
+ });
1350
+ seededStore.upsertLink({
1351
+ workspaceId,
1352
+ assetId: 'asset-doc-ref-drift-1',
1353
+ taskId: 'task-doc-ref-drift-1',
1354
+ role: 'attachment',
1355
+ updatedAt: '2026-03-17T11:55:00.000Z',
1356
+ deletedAt: null
1357
+ });
1358
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1359
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1360
+ expect(match).toBeTruthy();
1361
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1362
+ changes: [
1363
+ {
1364
+ op: 'document-upsert',
1365
+ path: 'docs/phase1-ref-drift.md',
1366
+ updatedAt: '2026-03-17T12:00:00.000Z',
1367
+ content: '# cloud canonical markdown\n',
1368
+ assetId: 'asset-doc-ref-drift-1',
1369
+ documentId: 'doc-ref-drift-1',
1370
+ referenceNumber: 18,
1371
+ version: 2,
1372
+ taskId: 'task-doc-ref-drift-1',
1373
+ originalFilename: 'phase1-ref-drift.md',
1374
+ linkRole: 'attachment'
1375
+ }
1376
+ ]
1377
+ }, { 'x-taskforce-workspace-id': workspaceId });
1378
+ const res = createMockRes();
1379
+ await match?.handler(req, res, {});
1380
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1381
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1382
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1383
+ const asset = store.get('asset-doc-ref-drift-1', workspaceId);
1384
+ const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1385
+ expect(asset).toEqual(expect.objectContaining({
1386
+ assetId: 'asset-doc-ref-drift-1',
1387
+ referenceNumber: 18,
1388
+ version: 2
1389
+ }));
1390
+ expect(syncEvents).toEqual(expect.arrayContaining([
1391
+ expect.objectContaining({
1392
+ eventType: 'apply',
1393
+ status: 'error',
1394
+ errorMessage: 'Detected document reference number mismatch during normal sync.',
1395
+ details: expect.objectContaining({
1396
+ assetId: 'asset-doc-ref-drift-1',
1397
+ existingReferenceNumber: 12,
1398
+ incomingReferenceNumber: 18
1399
+ })
1400
+ })
1401
+ ]));
1402
+ });
1403
+ it('lets the cloud image ref win during normal sync apply and records a diagnostic', async () => {
1404
+ const projectRoot = createIsolatedProjectRoot('taskforce-sync-apply-image-ref-drift-');
1405
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1406
+ core.createWorkspace({ workspaceId, name: 'Local Active Workspace' });
1407
+ core.createTask({
1408
+ id: 'task-image-ref-drift-1',
1409
+ title: 'Task with drifting synced image asset',
1410
+ status: 'task'
1411
+ }, {
1412
+ workspaceId,
1413
+ allowClientProvidedId: true
1414
+ });
1415
+ const seededStore = new WorkspaceAssetStore(core.getDatabaseAdapter(), core.getTenantId());
1416
+ seededStore.upsertAsset({
1417
+ assetId: 'asset-image-ref-drift-1',
1418
+ workspaceId,
1419
+ kind: 'image',
1420
+ originalFilename: 'hero.png',
1421
+ mimeType: 'image/png',
1422
+ storageProvider: 'local',
1423
+ storageKey: 'workspaces/workspace-local-active/assets/images/hero.png',
1424
+ contentSha256: 'sha-image-ref-drift-local',
1425
+ sizeBytes: 10,
1426
+ referenceNumber: 12,
1427
+ createdAt: '2026-03-17T11:55:00.000Z',
1428
+ updatedAt: '2026-03-17T11:55:00.000Z'
1429
+ });
1430
+ seededStore.upsertLink({
1431
+ workspaceId,
1432
+ assetId: 'asset-image-ref-drift-1',
1433
+ taskId: 'task-image-ref-drift-1',
1434
+ role: 'image',
1435
+ updatedAt: '2026-03-17T11:55:00.000Z',
1436
+ deletedAt: null
1437
+ });
1438
+ const routes = createRoutes(core, { authConfig: { runtimeMode: 'local', enabled: false } });
1439
+ const match = matchRoute('POST', '/api/taskforce/sync/workspace/apply-local', routes);
1440
+ expect(match).toBeTruthy();
1441
+ const req = createMockReq('POST', '/api/taskforce/sync/workspace/apply-local', {
1442
+ changes: [
1443
+ {
1444
+ op: 'asset-upsert',
1445
+ path: 'workspaces/workspace-local-active/assets/images/hero.png',
1446
+ updatedAt: '2026-03-17T12:05:00.000Z',
1447
+ contentBase64: Buffer.from('cloud-hero').toString('base64'),
1448
+ assetId: 'asset-image-ref-drift-1',
1449
+ kind: 'image',
1450
+ mimeType: 'image/png',
1451
+ referenceNumber: 18,
1452
+ taskId: 'task-image-ref-drift-1',
1453
+ originalFilename: 'hero.png',
1454
+ linkRole: 'image'
1455
+ }
1456
+ ]
1457
+ }, { 'x-taskforce-workspace-id': workspaceId });
1458
+ const res = createMockRes();
1459
+ await match?.handler(req, res, {});
1460
+ expect(res.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1461
+ const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1462
+ const store = new WorkspaceAssetStore(reloadedCore.getDatabaseAdapter(), reloadedCore.getTenantId());
1463
+ const asset = store.get('asset-image-ref-drift-1', workspaceId);
1464
+ const syncEvents = reloadedCore.listSyncEvents(workspaceId, 20);
1465
+ expect(asset).toEqual(expect.objectContaining({
1466
+ assetId: 'asset-image-ref-drift-1',
1467
+ referenceNumber: 18
1468
+ }));
1469
+ expect(syncEvents).toEqual(expect.arrayContaining([
1470
+ expect.objectContaining({
1471
+ eventType: 'apply',
1472
+ status: 'error',
1473
+ errorMessage: 'Detected asset reference number mismatch during normal sync.',
1474
+ details: expect.objectContaining({
1475
+ assetId: 'asset-image-ref-drift-1',
1476
+ existingReferenceNumber: 12,
1477
+ incomingReferenceNumber: 18
1478
+ })
1479
+ })
1480
+ ]));
1481
+ });
1314
1482
  it('round-trips cloud attachment links for canonical markdown and file assets into local apply', async () => {
1315
1483
  const cloudProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-cloud-');
1316
1484
  const localProjectRoot = createIsolatedProjectRoot('taskforce-sync-cloud-link-roundtrip-local-');
@@ -1597,6 +1765,18 @@ describe('Server Routes Workspace Sync Integration', () => {
1597
1765
  const pushRes = createMockRes();
1598
1766
  await pushMatch?.handler(pushReq, pushRes, {});
1599
1767
  expect(pushRes.writeHead).toHaveBeenCalledWith(200, expect.any(Object));
1768
+ const pushBody = readJsonBody(pushRes);
1769
+ expect(Array.isArray(pushBody?.appliedTaskUpserts)).toBe(true);
1770
+ expect(pushBody?.appliedTaskUpserts).toEqual(expect.arrayContaining([
1771
+ expect.objectContaining({
1772
+ archived: false,
1773
+ task: expect.objectContaining({
1774
+ id: taskId,
1775
+ title: 'Offline provisional task',
1776
+ referenceNumber: 1
1777
+ })
1778
+ })
1779
+ ]));
1600
1780
  const persisted = core.getTask(taskId, workspaceId);
1601
1781
  expect(persisted?.referenceNumber).toBe(1);
1602
1782
  expect(Date.parse(String(persisted?.updatedAt || ''))).toBeGreaterThan(Date.parse(staleUpdatedAt));