@taskforcehq/taskforce 0.3.303 → 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 (165) hide show
  1. package/dist/Taskforce.module.css +102 -5
  2. package/dist/TaskforceCore.js +514 -212
  3. package/dist/TaskforceCore.test.js +1172 -133
  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/TaskStatusActions.d.ts +6 -1
  21. package/dist/components/task/TaskStatusActions.js +8 -3
  22. package/dist/components/task/TaskStatusActions.test.js +9 -0
  23. package/dist/components/task/taskKanbanDropRules.d.ts +58 -0
  24. package/dist/components/task/taskKanbanDropRules.js +187 -0
  25. package/dist/components/task/taskKanbanDropRules.test.d.ts +1 -0
  26. package/dist/components/task/taskKanbanDropRules.test.js +114 -0
  27. package/dist/components/ui/Modal.d.ts +1 -1
  28. package/dist/components/ui/Modal.js +1 -0
  29. package/dist/components/views/AppShellHeader.js +5 -1
  30. package/dist/components/views/AppShellHeader.test.js +4 -0
  31. package/dist/components/views/PlanComparisonPage.d.ts +3 -2
  32. package/dist/components/views/PlanComparisonPage.js +4 -2
  33. package/dist/components/views/PlanComparisonPage.test.js +4 -0
  34. package/dist/components/views/PlansPage.d.ts +5 -1
  35. package/dist/components/views/PlansPage.js +39 -12
  36. package/dist/components/views/PlansPage.test.js +59 -2
  37. package/dist/components/views/StandaloneLayout.js +113 -38
  38. package/dist/components/views/WidgetView.js +11 -2
  39. package/dist/components/views/standalone/modals/SyncStatusModal.js +1 -1
  40. package/dist/core/DeletedTaskLifecycleService.d.ts +2 -1
  41. package/dist/core/PlanEntitlementService.d.ts +3 -1
  42. package/dist/core/PlanEntitlementService.js +61 -11
  43. package/dist/core/SignupMonetizationSettings.test.js +37 -2
  44. package/dist/core/TaskActivityService.d.ts +4 -1
  45. package/dist/core/TaskActivityService.js +45 -0
  46. package/dist/core/TaskAuditTimeline.test.js +52 -1
  47. package/dist/core/Taskforce.d.ts +15 -11
  48. package/dist/core/Taskforce.js +43 -2
  49. package/dist/core/WorkspacePlanMode.test.js +65 -0
  50. package/dist/documentReviews/service.d.ts +1 -1
  51. package/dist/documentReviews/types.d.ts +1 -1
  52. package/dist/hooks/auth/useTaskforceAuthBootstrap.d.ts +48 -0
  53. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +219 -0
  54. package/dist/hooks/sync/auth.d.ts +37 -0
  55. package/dist/hooks/sync/auth.js +61 -0
  56. package/dist/hooks/sync/bootstrap.d.ts +34 -0
  57. package/dist/hooks/sync/bootstrap.js +52 -0
  58. package/dist/hooks/sync/orchestratorShared.d.ts +36 -0
  59. package/dist/hooks/sync/orchestratorShared.js +209 -0
  60. package/dist/hooks/sync/orchestratorShared.test.d.ts +1 -0
  61. package/dist/hooks/sync/orchestratorShared.test.js +49 -0
  62. package/dist/hooks/sync/realtime.d.ts +25 -0
  63. package/dist/hooks/sync/realtime.js +60 -0
  64. package/dist/hooks/sync/recovery.d.ts +69 -0
  65. package/dist/hooks/sync/recovery.js +118 -0
  66. package/dist/hooks/sync/transfers.d.ts +122 -0
  67. package/dist/hooks/sync/transfers.js +431 -0
  68. package/dist/hooks/sync/useDataVersionRefresh.d.ts +16 -0
  69. package/dist/hooks/sync/useDataVersionRefresh.js +73 -0
  70. package/dist/hooks/ui/useResourceExport.d.ts +3 -1
  71. package/dist/hooks/ui/useResourceExport.js +5 -2
  72. package/dist/hooks/ui/useWorkflowTemplates.js +11 -6
  73. package/dist/hooks/useRealtimeSync.js +2 -1
  74. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +8 -0
  75. package/dist/hooks/useSyncOrchestrator.context-assets.test.d.ts +1 -0
  76. package/dist/hooks/useSyncOrchestrator.context-assets.test.js +228 -0
  77. package/dist/hooks/useSyncOrchestrator.d.ts +5 -4
  78. package/dist/hooks/useSyncOrchestrator.js +314 -873
  79. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +568 -3
  80. package/dist/hooks/useTaskMutations.js +45 -6
  81. package/dist/hooks/useTaskMutations.test.js +147 -0
  82. package/dist/hooks/useTaskforce.d.ts +8 -2
  83. package/dist/hooks/useTaskforce.js +285 -812
  84. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  85. package/dist/hooks/useWorkspaceSyncController.js +22 -10
  86. package/dist/hooks/workspace/useTaskforceApiRouting.test.js +17 -0
  87. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +166 -0
  88. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +434 -0
  89. package/dist/hooks/workspace/workspaceLocalState.d.ts +19 -0
  90. package/dist/hooks/workspace/workspaceLocalState.js +100 -0
  91. package/dist/hooks/workspace/workspaceLocalState.test.d.ts +1 -0
  92. package/dist/hooks/workspace/workspaceLocalState.test.js +59 -0
  93. package/dist/localization/locales/en-US.js +2 -2
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +70 -0
  95. package/dist/mcp/canonicalAssetHelpers.js +259 -0
  96. package/dist/mcp/clientIntegrations.d.ts +12 -1
  97. package/dist/mcp/clientIntegrations.js +161 -6
  98. package/dist/mcp/clientIntegrations.test.js +76 -4
  99. package/dist/mcp/documentHelpers.d.ts +100 -0
  100. package/dist/mcp/documentHelpers.js +161 -0
  101. package/dist/mcp/runtime.js +174 -476
  102. package/dist/mcp/runtime.test.js +81 -8
  103. package/dist/mcp/taskAttachmentHelpers.d.ts +41 -0
  104. package/dist/mcp/taskAttachmentHelpers.js +60 -0
  105. package/dist/mcp/taskAttachmentTypes.d.ts +37 -0
  106. package/dist/mcp/taskAttachmentTypes.js +1 -0
  107. package/dist/plugins/vite.js +1 -1
  108. package/dist/runtime/appGateDefinitions.d.ts +1 -1
  109. package/dist/runtime/appGateDefinitions.js +1 -1
  110. package/dist/server/index.d.ts +1 -0
  111. package/dist/server/index.js +46 -3
  112. package/dist/server/index.test.js +12 -1
  113. package/dist/server/routes/admin.js +13 -19
  114. package/dist/server/routes/auth.js +37 -2
  115. package/dist/server/routes/billing.js +18 -67
  116. package/dist/server/routes/billing.test.js +43 -0
  117. package/dist/server/routes/shared.d.ts +30 -0
  118. package/dist/server/routes/shared.js +26 -0
  119. package/dist/server/routes/sync.integration.test.js +20 -0
  120. package/dist/server/routes/sync.js +110 -237
  121. package/dist/server/routes/tasks.js +49 -7
  122. package/dist/server/routes.js +8 -19
  123. package/dist/server/routes.test.js +243 -5
  124. package/dist/shared/taskActor.d.ts +10 -0
  125. package/dist/shared/taskActor.js +1 -0
  126. package/dist/sync/collaborationSyncPayload.js +4 -0
  127. package/dist/sync/collaborationSyncPayload.test.js +8 -0
  128. package/dist/sync/collaborationSyncState.d.ts +3 -1
  129. package/dist/sync/collaborationSyncState.js +19 -0
  130. package/dist/sync/contentSyncState.js +12 -10
  131. package/dist/sync/syncApplyHandlers.d.ts +87 -1
  132. package/dist/sync/syncApplyHandlers.js +370 -8
  133. package/dist/sync/syncApplyHandlers.test.d.ts +1 -0
  134. package/dist/sync/syncApplyHandlers.test.js +377 -0
  135. package/dist/sync/syncService.d.ts +8 -1
  136. package/dist/sync/syncService.js +36 -2
  137. package/dist/sync/syncService.test.js +48 -0
  138. package/dist/sync/workspacePullFeed.d.ts +1 -0
  139. package/dist/sync/workspacePullFeed.js +66 -59
  140. package/dist/sync/workspacePullFeed.test.js +72 -4
  141. package/dist/sync/workspaceSyncModel.d.ts +22 -1
  142. package/dist/sync/workspaceSyncModel.js +65 -0
  143. package/dist/sync/workspaceSyncModel.test.js +119 -0
  144. package/dist/types.d.ts +2 -10
  145. package/dist/ui/assets/AgentsModule-BLWVbuKP.js +1 -0
  146. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-C_TmXZwA.js → AnnotatedAttachmentWorkspace-BlS-cqnl.js} +1 -1
  147. package/dist/ui/assets/{DocumentWorkspace-C3GyzrWm.js → DocumentWorkspace-BH_6DF6x.js} +1 -1
  148. package/dist/ui/assets/{InitiativesModule-4-Rh2K2n.js → InitiativesModule-BjR6iBf9.js} +1 -1
  149. package/dist/ui/assets/{PlansPage-BlVC_lRq.css → PlansPage-C2dd2n1X.css} +1 -1
  150. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +1 -0
  151. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +9 -0
  152. package/dist/ui/assets/{WorkflowsModule-CGk9s3NJ.js → WorkflowsModule-DnFqdUME.js} +1 -1
  153. package/dist/ui/assets/index-DXUdtH1B.js +6 -0
  154. package/dist/ui/assets/index-Ii0B0rTO.css +1 -0
  155. package/dist/ui/assets/{vendor-icons-pWocEipT.js → vendor-icons-I7ZwG1z_.js} +1 -1
  156. package/dist/ui/index.html +4 -4
  157. package/dist/utils/taskActivity.js +4 -0
  158. package/dist/utils/taskActivity.test.js +25 -1
  159. package/package.json +3 -1
  160. package/scripts/playwright-auth.sh +4 -0
  161. package/dist/ui/assets/AgentsModule-CpsTmrIW.js +0 -1
  162. package/dist/ui/assets/PlansPage-BP7AYOqr.js +0 -1
  163. package/dist/ui/assets/TaskSettings-BOC5F6Ag.js +0 -8
  164. package/dist/ui/assets/index-CLIMgR6g.js +0 -6
  165. package/dist/ui/assets/index-DneETxcu.css +0 -1
@@ -0,0 +1,377 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { applyCollaborationSyncChanges, applyContentSyncChanges, applyPreparedTaskSyncSnapshot, applyTaskSyncDeletes, classifyWorkspaceSyncChanges, prepareTaskSyncSnapshotApply, } from './syncApplyHandlers.js';
3
+ function createCoreFixture(overrides = {}) {
4
+ return {
5
+ resolveTaskIdentifier: vi.fn().mockReturnValue(null),
6
+ getTask: vi.fn().mockReturnValue(null),
7
+ deleteTaskForSync: vi.fn(),
8
+ applyWorkspaceSyncSnapshot: vi.fn().mockReturnValue({
9
+ total: 1,
10
+ created: 0,
11
+ updated: 1,
12
+ archived: 0,
13
+ unarchived: 0,
14
+ skipped: 0,
15
+ }),
16
+ notifyDocumentMetadataMutation: vi.fn(),
17
+ getDocumentWatermark: vi.fn().mockReturnValue(null),
18
+ getTenantId: vi.fn().mockReturnValue('tenant-1'),
19
+ getDocumentReviewSession: vi.fn().mockReturnValue(null),
20
+ upsertDocumentReviewSessionForSync: vi.fn(),
21
+ listDocumentReviewCommentsForSync: vi.fn().mockReturnValue([]),
22
+ upsertDocumentReviewCommentForSync: vi.fn(),
23
+ deleteDocumentReviewSessionForSync: vi.fn(),
24
+ deleteDocumentReviewCommentForSync: vi.fn(),
25
+ getAnnotatedAttachmentSession: vi.fn().mockReturnValue(null),
26
+ upsertAnnotatedAttachmentSessionForSync: vi.fn(),
27
+ deleteAnnotatedAttachmentSessionForSync: vi.fn(),
28
+ ...overrides,
29
+ };
30
+ }
31
+ describe('syncApplyHandlers', () => {
32
+ it('classifies task and workstream changes, then prepares and applies the task snapshot pipeline', () => {
33
+ const diagnostics = [];
34
+ const workspaceId = 'workspace-1';
35
+ const applyModel = classifyWorkspaceSyncChanges({
36
+ workspaceId,
37
+ incomingChanges: [
38
+ {
39
+ op: 'workstream-upsert',
40
+ workstream: {
41
+ id: 'workstream-1',
42
+ title: 'Delivery',
43
+ description: null,
44
+ createdAt: '2026-04-10T09:00:00.000Z',
45
+ updatedAt: '2026-04-10T09:00:00.000Z',
46
+ },
47
+ },
48
+ {
49
+ op: 'upsert',
50
+ archived: false,
51
+ task: {
52
+ id: 'task-1',
53
+ title: 'Superseded task',
54
+ status: 'task',
55
+ category: 'general',
56
+ type: 'feature',
57
+ createdAt: '2026-04-10T09:00:00.000Z',
58
+ updatedAt: '2026-04-10T10:00:00.000Z',
59
+ referenceNumber: 7,
60
+ },
61
+ },
62
+ {
63
+ op: 'upsert',
64
+ archived: true,
65
+ task: {
66
+ id: 'task-2',
67
+ title: 'Archived task',
68
+ status: 'done',
69
+ category: 'general',
70
+ type: 'feature',
71
+ createdAt: '2026-04-10T09:00:00.000Z',
72
+ updatedAt: '2026-04-10T10:30:00.000Z',
73
+ isArchived: true,
74
+ },
75
+ },
76
+ {
77
+ op: 'delete',
78
+ taskId: 'task-1',
79
+ },
80
+ {
81
+ op: 'delete',
82
+ taskId: 'task-stale',
83
+ },
84
+ ],
85
+ normalizeWorkspaceSyncDocumentPath: (rawPath) => typeof rawPath === 'string' ? rawPath : null,
86
+ normalizeTaskAssetPath: (rawPath) => typeof rawPath === 'string' ? rawPath : null,
87
+ normalizeIncomingAiProfileSurfaceType: () => null,
88
+ normalizeTaskEventSyncPayload: () => null,
89
+ recordSyncDiagnostic: (targetWorkspaceId, eventType, message, details) => {
90
+ diagnostics.push({ workspaceId: targetWorkspaceId, eventType, message, details });
91
+ },
92
+ });
93
+ expect(applyModel.planning.incomingWorkstreams.get('workstream-1')).toEqual(expect.objectContaining({
94
+ id: 'workstream-1',
95
+ title: 'Delivery',
96
+ }));
97
+ expect(applyModel.taskMutations.upserts).toHaveLength(2);
98
+ expect(applyModel.taskMutations.deletes.get('task-1')).toBe('2026-04-10T11:00:00.000Z');
99
+ const core = createCoreFixture({
100
+ resolveTaskIdentifier: vi.fn().mockReturnValue({
101
+ id: 'task-local-7',
102
+ title: 'Local incumbent',
103
+ referenceNumber: 7,
104
+ }),
105
+ getTask: vi.fn().mockImplementation((taskId) => {
106
+ if (taskId === 'task-1') {
107
+ return {
108
+ id: 'task-1',
109
+ title: 'Superseded task',
110
+ createdAt: '2026-04-10T09:00:00.000Z',
111
+ updatedAt: '2026-04-10T10:00:00.000Z',
112
+ isArchived: false,
113
+ };
114
+ }
115
+ if (taskId === 'task-stale') {
116
+ return {
117
+ id: 'task-stale',
118
+ title: 'Keep local task',
119
+ createdAt: '2026-04-10T09:00:00.000Z',
120
+ updatedAt: '2026-04-10T09:30:00.000Z',
121
+ isArchived: false,
122
+ };
123
+ }
124
+ return null;
125
+ }),
126
+ });
127
+ const prepared = prepareTaskSyncSnapshotApply({
128
+ core,
129
+ workspaceId,
130
+ taskMutations: applyModel.taskMutations,
131
+ recordSyncDiagnostic: (targetWorkspaceId, eventType, message, details) => {
132
+ diagnostics.push({ workspaceId: targetWorkspaceId, eventType, message, details });
133
+ },
134
+ });
135
+ expect(prepared.tasks).toEqual([]);
136
+ expect(prepared.archived).toEqual([
137
+ expect.objectContaining({
138
+ id: 'task-2',
139
+ title: 'Archived task',
140
+ }),
141
+ ]);
142
+ expect(diagnostics).toEqual(expect.arrayContaining([
143
+ expect.objectContaining({
144
+ workspaceId,
145
+ eventType: 'apply',
146
+ message: 'Detected task reference number mismatch during normal sync.',
147
+ details: expect.objectContaining({
148
+ referenceLabel: 'T-7',
149
+ existingTaskId: 'task-local-7',
150
+ taskId: 'task-1',
151
+ }),
152
+ }),
153
+ ]));
154
+ const deleteCount = applyTaskSyncDeletes({
155
+ core,
156
+ workspaceId,
157
+ taskMutations: applyModel.taskMutations,
158
+ recordSyncDiagnostic: vi.fn(),
159
+ });
160
+ expect(deleteCount).toBe(2);
161
+ expect(core.deleteTaskForSync).toHaveBeenCalledTimes(1);
162
+ expect(core.deleteTaskForSync).toHaveBeenCalledWith('task-1', workspaceId, { persistBackup: false });
163
+ const result = applyPreparedTaskSyncSnapshot({
164
+ core,
165
+ workspaceId,
166
+ prepared,
167
+ });
168
+ expect(core.applyWorkspaceSyncSnapshot).toHaveBeenCalledWith({
169
+ tasks: [],
170
+ archived: [
171
+ expect.objectContaining({
172
+ id: 'task-2',
173
+ title: 'Archived task',
174
+ isArchived: true,
175
+ }),
176
+ ],
177
+ }, workspaceId);
178
+ expect(result).toEqual(expect.objectContaining({ total: 1 }));
179
+ });
180
+ it('applies document and asset changes through the content handler helpers', async () => {
181
+ const core = createCoreFixture();
182
+ const writeWorkspaceSyncDocument = vi.fn().mockResolvedValue(undefined);
183
+ const writeWorkspaceSyncBinaryAsset = vi.fn().mockResolvedValue(undefined);
184
+ const deleteWorkspaceSyncDocument = vi.fn();
185
+ const deleteWorkspaceSyncBinaryAsset = vi.fn();
186
+ await applyContentSyncChanges({
187
+ core,
188
+ workspaceId: 'workspace-1',
189
+ incomingDocuments: [
190
+ {
191
+ path: 'docs/spec.md',
192
+ updatedAt: '2026-04-10T10:00:00.000Z',
193
+ content: '# Spec',
194
+ assetId: 'asset-doc-1',
195
+ },
196
+ ],
197
+ incomingDocumentDeletes: new Map([
198
+ ['docs/stale.md', '2026-04-10T11:00:00.000Z'],
199
+ ]),
200
+ incomingAssets: [
201
+ {
202
+ path: 'assets/proof.png',
203
+ updatedAt: '2026-04-10T10:05:00.000Z',
204
+ contentBase64: Buffer.from('png-bytes').toString('base64'),
205
+ assetId: 'asset-image-1',
206
+ kind: 'image',
207
+ mimeType: 'image/png',
208
+ },
209
+ ],
210
+ incomingAssetDeletes: new Map([
211
+ ['assets/stale.png', '2026-04-10T11:05:00.000Z'],
212
+ ]),
213
+ basePath: '/tmp/taskforce-sync-content',
214
+ hasObjectStorageClient: () => false,
215
+ hasDocumentSyncMetadataDrift: () => false,
216
+ hasBinaryAssetSyncMetadataDrift: () => false,
217
+ getExistingDocument: () => null,
218
+ writeWorkspaceSyncDocument,
219
+ deleteWorkspaceSyncDocument,
220
+ writeWorkspaceSyncBinaryAsset,
221
+ deleteWorkspaceSyncBinaryAsset,
222
+ getExistingBinaryAsset: () => null,
223
+ recordSyncDiagnostic: vi.fn(),
224
+ });
225
+ expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md', assetId: 'asset-doc-1' }), 'workspace-1', { allowReferenceNumberReassignment: true });
226
+ expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png', assetId: 'asset-image-1' }), 'workspace-1', { allowReferenceNumberReassignment: true });
227
+ expect(deleteWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', 'docs/stale.md', 'workspace-1');
228
+ expect(deleteWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', 'assets/stale.png', 'workspace-1');
229
+ expect(core.notifyDocumentMetadataMutation).toHaveBeenNthCalledWith(1, 'workspace-1', ['docs/spec.md', 'assets/proof.png'], 'upsert');
230
+ expect(core.notifyDocumentMetadataMutation).toHaveBeenNthCalledWith(2, 'workspace-1', ['docs/stale.md', 'assets/stale.png'], 'delete');
231
+ });
232
+ it('only allows reference reassignment for content writes when the content changed or repair is explicit', async () => {
233
+ const core = createCoreFixture({
234
+ getDocumentWatermark: vi.fn().mockReturnValue(null),
235
+ });
236
+ const writeWorkspaceSyncDocument = vi.fn().mockResolvedValue(undefined);
237
+ const writeWorkspaceSyncBinaryAsset = vi.fn().mockResolvedValue(undefined);
238
+ await applyContentSyncChanges({
239
+ core,
240
+ workspaceId: 'workspace-1',
241
+ incomingDocuments: [
242
+ {
243
+ path: 'docs/spec.md',
244
+ updatedAt: '2026-04-10T10:00:00.000Z',
245
+ content: '# Spec',
246
+ assetId: 'asset-doc-1',
247
+ referenceNumber: 22,
248
+ },
249
+ ],
250
+ incomingDocumentDeletes: new Map(),
251
+ incomingAssets: [
252
+ {
253
+ path: 'assets/proof.png',
254
+ updatedAt: '2026-04-10T10:05:00.000Z',
255
+ contentBase64: Buffer.from('new-bytes').toString('base64'),
256
+ assetId: 'asset-image-1',
257
+ kind: 'image',
258
+ mimeType: 'image/png',
259
+ referenceNumber: 33,
260
+ },
261
+ ],
262
+ incomingAssetDeletes: new Map(),
263
+ basePath: '/tmp/taskforce-sync-content',
264
+ hasObjectStorageClient: () => false,
265
+ hasDocumentSyncMetadataDrift: () => true,
266
+ hasBinaryAssetSyncMetadataDrift: () => true,
267
+ getExistingDocument: () => ({
268
+ updatedAt: '2026-04-10T10:00:00.000Z',
269
+ contentHash: '6bb50a17e2dc2fc14caea95a15cd8572018b6d22d74fb7b9ad015b5a0a8a935e',
270
+ }),
271
+ writeWorkspaceSyncDocument,
272
+ deleteWorkspaceSyncDocument: vi.fn(),
273
+ writeWorkspaceSyncBinaryAsset,
274
+ deleteWorkspaceSyncBinaryAsset: vi.fn(),
275
+ getExistingBinaryAsset: () => ({
276
+ updatedAt: '2026-04-10T10:00:00.000Z',
277
+ contentSha256: 'old-hash',
278
+ }),
279
+ recordSyncDiagnostic: vi.fn(),
280
+ });
281
+ expect(writeWorkspaceSyncDocument).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'docs/spec.md' }), 'workspace-1', { allowReferenceNumberReassignment: false });
282
+ expect(writeWorkspaceSyncBinaryAsset).toHaveBeenCalledWith('/tmp/taskforce-sync-content', expect.objectContaining({ path: 'assets/proof.png' }), 'workspace-1', { allowReferenceNumberReassignment: true });
283
+ });
284
+ it('applies collaboration upserts and deletes through the collaboration handlers', () => {
285
+ const core = createCoreFixture();
286
+ applyCollaborationSyncChanges({
287
+ core,
288
+ workspaceId: 'workspace-1',
289
+ incomingDocumentReviewSessions: new Map([
290
+ ['session-1', {
291
+ id: 'session-1',
292
+ assetId: 'asset-doc-1',
293
+ documentId: 'doc-1',
294
+ documentVersion: 3,
295
+ title: 'Review round',
296
+ status: 'open',
297
+ comments: [],
298
+ createdByActorId: 'user-1',
299
+ updatedByActorId: 'user-1',
300
+ createdAt: '2026-04-10T09:00:00.000Z',
301
+ updatedAt: '2026-04-10T10:00:00.000Z',
302
+ deletedAt: null,
303
+ }],
304
+ ]),
305
+ incomingDocumentReviewSessionDeletes: new Map([
306
+ ['session-2', '2026-04-10T11:00:00.000Z'],
307
+ ]),
308
+ incomingDocumentReviewComments: new Map([
309
+ ['comment-1', {
310
+ id: 'comment-1',
311
+ sessionId: 'session-1',
312
+ body: 'Looks good',
313
+ anchor: { kind: 'document' },
314
+ order: 0,
315
+ authorActorId: 'user-1',
316
+ createdAt: '2026-04-10T09:05:00.000Z',
317
+ updatedAt: '2026-04-10T10:05:00.000Z',
318
+ deletedAt: null,
319
+ }],
320
+ ]),
321
+ incomingDocumentReviewCommentDeletes: new Map([
322
+ ['comment-2', '2026-04-10T11:05:00.000Z'],
323
+ ]),
324
+ incomingAnnotatedAttachmentSessions: new Map([
325
+ ['annotation-session-1', {
326
+ id: 'annotation-session-1',
327
+ workspaceId: 'workspace-1',
328
+ taskId: 'task-1',
329
+ baseImageAssetId: 'asset-image-1',
330
+ title: 'Annotations',
331
+ globalInstruction: 'Inspect issue',
332
+ annotations: [],
333
+ createdByActorId: 'user-1',
334
+ updatedByActorId: 'user-1',
335
+ createdAt: '2026-04-10T09:10:00.000Z',
336
+ updatedAt: '2026-04-10T10:10:00.000Z',
337
+ deletedAt: null,
338
+ }],
339
+ ]),
340
+ incomingAnnotatedAttachmentSessionDeletes: new Map([
341
+ ['annotation-session-2', '2026-04-10T11:10:00.000Z'],
342
+ ]),
343
+ recordSyncDiagnostic: vi.fn(),
344
+ createSyncRouteError: (message, details) => Object.assign(new Error(message), { details }),
345
+ });
346
+ expect(core.upsertDocumentReviewSessionForSync).toHaveBeenCalledWith(expect.objectContaining({
347
+ id: 'session-1',
348
+ workspaceId: 'workspace-1',
349
+ assetId: 'asset-doc-1',
350
+ }));
351
+ expect(core.deleteDocumentReviewSessionForSync).toHaveBeenCalledWith({
352
+ workspaceId: 'workspace-1',
353
+ sessionId: 'session-2',
354
+ deletedAt: '2026-04-10T11:00:00.000Z',
355
+ });
356
+ expect(core.upsertDocumentReviewCommentForSync).toHaveBeenCalledWith(expect.objectContaining({
357
+ id: 'comment-1',
358
+ sessionId: 'session-1',
359
+ workspaceId: 'workspace-1',
360
+ }));
361
+ expect(core.deleteDocumentReviewCommentForSync).toHaveBeenCalledWith({
362
+ workspaceId: 'workspace-1',
363
+ commentId: 'comment-2',
364
+ deletedAt: '2026-04-10T11:05:00.000Z',
365
+ });
366
+ expect(core.upsertAnnotatedAttachmentSessionForSync).toHaveBeenCalledWith(expect.objectContaining({
367
+ id: 'annotation-session-1',
368
+ workspaceId: 'workspace-1',
369
+ baseImageAssetId: 'asset-image-1',
370
+ }));
371
+ expect(core.deleteAnnotatedAttachmentSessionForSync).toHaveBeenCalledWith({
372
+ workspaceId: 'workspace-1',
373
+ sessionId: 'annotation-session-2',
374
+ deletedAt: '2026-04-10T11:10:00.000Z',
375
+ });
376
+ });
377
+ });
@@ -1,4 +1,4 @@
1
- import type { WorkspaceSyncPayload } from './workspaceSyncModel';
1
+ import type { WorkspaceSyncPayload, WorkspaceSyncTaskSnapshot } from './workspaceSyncModel';
2
2
  export declare function isTransientWorkspaceSyncStatus(status: number | null | undefined): boolean;
3
3
  export interface EnsureWorkspaceReadyInput {
4
4
  cloudAuthConfigured: boolean;
@@ -77,10 +77,16 @@ export interface WorkspacePushSyncResult {
77
77
  currentAnnotatedAttachmentSessionIds: Set<string>;
78
78
  deleteAnnotatedAttachmentSessionIds: Set<string>;
79
79
  currentAnnotatedAttachmentSessionWatermarks: Map<string, string>;
80
+ currentTaskEventWatermarks: Map<string, string>;
80
81
  /** Watermarks for every task upsert that was successfully pushed. */
81
82
  pushedWatermarks: Map<string, string>;
82
83
  /** Server-emitted realtime mutation event ids for this push apply. */
83
84
  emittedEventIds: string[];
85
+ /** Authoritative task snapshots returned immediately by the push route for incoming task upserts. */
86
+ appliedTaskUpserts: Array<{
87
+ archived: boolean;
88
+ task: WorkspaceSyncTaskSnapshot;
89
+ }>;
84
90
  }
85
91
  export declare function runWorkspacePushSyncService(input: WorkspacePushSyncInput): Promise<WorkspacePushSyncResult>;
86
92
  export type WorkspacePullFailureKind = 'pull_http' | 'apply_auth' | 'apply_all_candidates' | 'apply_payload' | 'exception';
@@ -113,6 +119,7 @@ export interface WorkspacePullSyncResult {
113
119
  pulledDeleteTaskIds: Set<string>;
114
120
  pulledActiveUpserts: boolean;
115
121
  pulledArchivedUpserts: boolean;
122
+ pulledTaskEventUpserts?: boolean;
116
123
  documentDecryptFailures?: string[];
117
124
  emittedEventIds?: string[];
118
125
  serverDiagnostics?: Record<string, unknown> | null;
@@ -187,8 +187,10 @@ export async function runWorkspacePushSyncService(input) {
187
187
  currentAnnotatedAttachmentSessionIds: input.payload.currentAnnotatedAttachmentSessionIds,
188
188
  deleteAnnotatedAttachmentSessionIds: input.payload.deleteAnnotatedAttachmentSessionIds,
189
189
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
190
+ currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
190
191
  pushedWatermarks: new Map(),
191
- emittedEventIds: []
192
+ emittedEventIds: [],
193
+ appliedTaskUpserts: []
192
194
  };
193
195
  }
194
196
  const pushData = asObjectRecord(pushResult.data);
@@ -197,6 +199,23 @@ export async function runWorkspacePushSyncService(input) {
197
199
  .map((entry) => String(entry || '').trim())
198
200
  .filter(Boolean)))
199
201
  : [];
202
+ const appliedTaskUpserts = Array.isArray(pushData.appliedTaskUpserts)
203
+ ? pushData.appliedTaskUpserts
204
+ .map((entry) => {
205
+ const source = entry && typeof entry === 'object' ? entry : {};
206
+ const task = source.task && typeof source.task === 'object'
207
+ ? source.task
208
+ : null;
209
+ const taskId = String(task?.id || '').trim();
210
+ if (!task || !taskId)
211
+ return null;
212
+ return {
213
+ archived: source.archived === true,
214
+ task
215
+ };
216
+ })
217
+ .filter((entry) => Boolean(entry))
218
+ : [];
200
219
  return {
201
220
  success: true,
202
221
  syncedAt: new Date().toISOString(),
@@ -226,8 +245,10 @@ export async function runWorkspacePushSyncService(input) {
226
245
  currentAnnotatedAttachmentSessionIds: input.payload.currentAnnotatedAttachmentSessionIds,
227
246
  deleteAnnotatedAttachmentSessionIds: input.payload.deleteAnnotatedAttachmentSessionIds,
228
247
  currentAnnotatedAttachmentSessionWatermarks: input.payload.currentAnnotatedAttachmentSessionWatermarks,
248
+ currentTaskEventWatermarks: input.payload.currentTaskEventWatermarks,
229
249
  pushedWatermarks: input.payload.pushedWatermarks,
230
- emittedEventIds
250
+ emittedEventIds,
251
+ appliedTaskUpserts
231
252
  };
232
253
  }
233
254
  export async function runWorkspacePullSyncService(input) {
@@ -240,6 +261,7 @@ export async function runWorkspacePullSyncService(input) {
240
261
  const pulledDeleteTaskIds = new Set();
241
262
  let pulledActiveUpserts = false;
242
263
  let pulledArchivedUpserts = false;
264
+ let pulledTaskEventUpserts = false;
243
265
  const documentDecryptFailures = new Set();
244
266
  const emittedEventIds = new Set();
245
267
  const pulledAnnotatedAttachmentSessionIds = new Set();
@@ -280,6 +302,7 @@ export async function runWorkspacePullSyncService(input) {
280
302
  pulledDeleteTaskIds,
281
303
  pulledActiveUpserts,
282
304
  pulledArchivedUpserts,
305
+ pulledTaskEventUpserts,
283
306
  documentDecryptFailures: Array.from(documentDecryptFailures),
284
307
  serverDiagnostics: null
285
308
  };
@@ -309,6 +332,10 @@ export async function runWorkspacePullSyncService(input) {
309
332
  pulledAnnotatedAttachmentSessionIds.add(sessionId);
310
333
  continue;
311
334
  }
335
+ if (change.op === 'task-event-upsert') {
336
+ pulledTaskEventUpserts = true;
337
+ continue;
338
+ }
312
339
  if (change.op === 'document-upsert') {
313
340
  if (typeof change.content === 'string' && typeof process !== 'undefined' && process.env?.TASKFORCE_SYNC_KEY) {
314
341
  try {
@@ -371,6 +398,7 @@ export async function runWorkspacePullSyncService(input) {
371
398
  pulledDeleteTaskIds,
372
399
  pulledActiveUpserts,
373
400
  pulledArchivedUpserts,
401
+ pulledTaskEventUpserts,
374
402
  documentDecryptFailures: Array.from(documentDecryptFailures),
375
403
  serverDiagnostics
376
404
  };
@@ -398,6 +426,7 @@ export async function runWorkspacePullSyncService(input) {
398
426
  pulledDeleteTaskIds,
399
427
  pulledActiveUpserts,
400
428
  pulledArchivedUpserts,
429
+ pulledTaskEventUpserts,
401
430
  documentDecryptFailures: Array.from(documentDecryptFailures),
402
431
  emittedEventIds: Array.from(emittedEventIds),
403
432
  serverDiagnostics: lastServerDiagnostics
@@ -419,6 +448,7 @@ export async function runWorkspacePullSyncService(input) {
419
448
  pulledDeleteTaskIds,
420
449
  pulledActiveUpserts,
421
450
  pulledArchivedUpserts,
451
+ pulledTaskEventUpserts,
422
452
  documentDecryptFailures: Array.from(documentDecryptFailures),
423
453
  emittedEventIds: Array.from(emittedEventIds),
424
454
  serverDiagnostics: lastServerDiagnostics
@@ -469,6 +499,7 @@ export async function runWorkspacePullSyncService(input) {
469
499
  pulledDeleteTaskIds,
470
500
  pulledActiveUpserts,
471
501
  pulledArchivedUpserts,
502
+ pulledTaskEventUpserts,
472
503
  documentDecryptFailures: Array.from(documentDecryptFailures),
473
504
  emittedEventIds: Array.from(emittedEventIds),
474
505
  serverDiagnostics: lastServerDiagnostics
@@ -497,6 +528,7 @@ export async function runWorkspacePullSyncService(input) {
497
528
  pulledDeleteTaskIds,
498
529
  pulledActiveUpserts,
499
530
  pulledArchivedUpserts,
531
+ pulledTaskEventUpserts,
500
532
  documentDecryptFailures: Array.from(documentDecryptFailures),
501
533
  emittedEventIds: Array.from(emittedEventIds),
502
534
  serverDiagnostics: lastServerDiagnostics
@@ -515,6 +547,7 @@ export async function runWorkspacePullSyncService(input) {
515
547
  pulledDeleteTaskIds,
516
548
  pulledActiveUpserts,
517
549
  pulledArchivedUpserts,
550
+ pulledTaskEventUpserts,
518
551
  documentDecryptFailures: Array.from(documentDecryptFailures),
519
552
  emittedEventIds: Array.from(emittedEventIds),
520
553
  serverDiagnostics: lastServerDiagnostics
@@ -535,6 +568,7 @@ export async function runWorkspacePullSyncService(input) {
535
568
  pulledDeleteTaskIds,
536
569
  pulledActiveUpserts,
537
570
  pulledArchivedUpserts,
571
+ pulledTaskEventUpserts,
538
572
  documentDecryptFailures: Array.from(documentDecryptFailures),
539
573
  emittedEventIds: Array.from(emittedEventIds),
540
574
  serverDiagnostics: null
@@ -37,6 +37,7 @@ function buildPayload(overrides = {}) {
37
37
  currentAnnotatedAttachmentSessionIds: new Set(),
38
38
  deleteAnnotatedAttachmentSessionIds: new Set(),
39
39
  currentAnnotatedAttachmentSessionWatermarks: new Map(),
40
+ currentTaskEventWatermarks: new Map(),
40
41
  pushedWatermarks: new Map(),
41
42
  ...overrides
42
43
  };
@@ -500,6 +501,53 @@ describe('syncService', () => {
500
501
  delete process.env.TASKFORCE_SYNC_KEY;
501
502
  }
502
503
  });
504
+ it('tracks event-only task pulls so callers can refresh visible task history', async () => {
505
+ vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
506
+ ok: true,
507
+ status: 200,
508
+ data: {
509
+ success: true,
510
+ changes: [{
511
+ op: 'task-event-upsert',
512
+ event: {
513
+ id: 'task-event-1',
514
+ taskId: 'task-1',
515
+ workspaceId: 'ws-1',
516
+ action: 'task-status-changed',
517
+ actor: 'ai-profile-1',
518
+ actorType: 'ai',
519
+ createdAt: '2026-04-13T10:05:00.000Z',
520
+ details: {
521
+ changes: {
522
+ status: {
523
+ from: 'in-progress',
524
+ to: 'done'
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }],
530
+ nextCursor: null,
531
+ hasMore: false
532
+ }
533
+ });
534
+ vi.mocked(applyLocalWorkspaceChanges).mockResolvedValueOnce({
535
+ ok: true,
536
+ failures: [],
537
+ appliedCount: 1,
538
+ data: { success: true }
539
+ });
540
+ const res = await runWorkspacePullSyncService({
541
+ resolveCloudAuthUrl: mockResolveCloudAuthUrl,
542
+ workspaceId: 'ws-1',
543
+ cursor: null,
544
+ bootstrap: false
545
+ });
546
+ expect(res.success).toBe(true);
547
+ expect(res.pulledTaskEventUpserts).toBe(true);
548
+ expect(res.pulledActiveUpserts).toBe(false);
549
+ expect(res.pulledArchivedUpserts).toBe(false);
550
+ });
503
551
  it('zero-change bootstrap pull (no changes, hasMore false) returns success true', async () => {
504
552
  vi.mocked(pullWorkspaceChanges).mockResolvedValueOnce({
505
553
  ok: true,
@@ -18,6 +18,7 @@ export interface WorkspacePullFeedResult {
18
18
  diagnostics: {
19
19
  totalEntries: number;
20
20
  pageEntries: number;
21
+ skippedHydrationEntries: number;
21
22
  documentIndexCount: number;
22
23
  assetIndexCount: number;
23
24
  indexMs: number;