@taskforcehq/taskforce 0.3.306 → 0.3.307

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 (215) hide show
  1. package/README.md +0 -2
  2. package/dist/Taskforce.module.css +317 -27
  3. package/dist/TaskforceCore.js +1 -0
  4. package/dist/TaskforceCore.test.js +178 -0
  5. package/dist/components/activity/EntityActivityTimeline.d.ts +34 -0
  6. package/dist/components/activity/EntityActivityTimeline.js +81 -0
  7. package/dist/components/context/ContextAttachmentManager.js +15 -3
  8. package/dist/components/features/AgentsModule.d.ts +5 -1
  9. package/dist/components/features/AgentsModule.js +142 -39
  10. package/dist/components/features/AgentsModule.test.js +260 -11
  11. package/dist/components/features/AnnotatedAttachmentWorkspace.d.ts +3 -1
  12. package/dist/components/features/AnnotatedAttachmentWorkspace.js +118 -4
  13. package/dist/components/features/AnnotatedAttachmentWorkspace.test.js +52 -0
  14. package/dist/components/features/DocumentIndex.d.ts +2 -2
  15. package/dist/components/features/DocumentIndex.js +3 -4
  16. package/dist/components/features/DocumentViewer.d.ts +3 -1
  17. package/dist/components/features/DocumentViewer.js +6 -5
  18. package/dist/components/features/DocumentWorkspace.d.ts +10 -2
  19. package/dist/components/features/DocumentWorkspace.js +15 -4
  20. package/dist/components/features/DocumentWorkspace.test.js +20 -3
  21. package/dist/components/features/TaskSettings.d.ts +2 -0
  22. package/dist/components/features/TaskSettings.js +83 -78
  23. package/dist/components/features/TaskSettings.test.js +171 -82
  24. package/dist/components/task/TaskActionHeader.js +11 -5
  25. package/dist/components/task/TaskActionHeader.test.js +10 -0
  26. package/dist/components/task/TaskCard.js +6 -2
  27. package/dist/components/task/TaskContextUpload.test.js +3 -3
  28. package/dist/components/task/TaskForm.js +14 -64
  29. package/dist/components/task/TaskKanban.js +0 -8
  30. package/dist/components/task/TaskList.js +5 -1
  31. package/dist/components/task/TaskList.test.js +4 -0
  32. package/dist/components/ui/HierarchyActionBadge.d.ts +10 -0
  33. package/dist/components/ui/HierarchyActionBadge.js +5 -0
  34. package/dist/components/views/AppShellHeader.d.ts +3 -1
  35. package/dist/components/views/AppShellHeader.js +27 -11
  36. package/dist/components/views/AppShellHeader.test.js +18 -1
  37. package/dist/components/views/PlanComparisonPage.js +9 -1
  38. package/dist/components/views/PlanComparisonPage.test.js +19 -0
  39. package/dist/components/views/PlansPage.js +1 -1
  40. package/dist/components/views/StandaloneLayout.js +256 -69
  41. package/dist/components/views/WidgetView.js +10 -8
  42. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  43. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  44. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  45. package/dist/config/envSchema.js +1 -0
  46. package/dist/core/AiProfileService.d.ts +7 -0
  47. package/dist/core/AiProfileService.js +460 -35
  48. package/dist/core/AiProfiles.test.js +648 -2
  49. package/dist/core/AttachmentLinkService.d.ts +3 -0
  50. package/dist/core/AttachmentLinkService.js +58 -13
  51. package/dist/core/GlobalSettingsService.js +12 -0
  52. package/dist/core/McpTokenService.d.ts +12 -0
  53. package/dist/core/McpTokenService.js +44 -1
  54. package/dist/core/PlanningEntities.test.js +366 -4
  55. package/dist/core/TaskActivityService.d.ts +28 -1
  56. package/dist/core/TaskActivityService.js +227 -46
  57. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  58. package/dist/core/TaskActivitySync.test.js +90 -0
  59. package/dist/core/TaskAuditTimeline.test.js +96 -0
  60. package/dist/core/Taskforce.d.ts +190 -26
  61. package/dist/core/Taskforce.ids.test.js +58 -0
  62. package/dist/core/Taskforce.js +775 -71
  63. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  64. package/dist/core/types.d.ts +7 -0
  65. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  66. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  67. package/dist/hooks/bootstrapTimeouts.js +8 -0
  68. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  69. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  70. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  71. package/dist/hooks/sync/orchestratorShared.js +61 -1
  72. package/dist/hooks/sync/recovery.js +8 -0
  73. package/dist/hooks/sync/recovery.test.js +5 -5
  74. package/dist/hooks/sync/transfers.d.ts +2 -1
  75. package/dist/hooks/sync/transfers.js +36 -9
  76. package/dist/hooks/sync/transfers.test.js +534 -2
  77. package/dist/hooks/useFilterSort.js +13 -13
  78. package/dist/hooks/useFilterSort.test.js +87 -1
  79. package/dist/hooks/useSyncOrchestrator.js +50 -5
  80. package/dist/hooks/useTaskforce.d.ts +85 -50
  81. package/dist/hooks/useTaskforce.js +44 -14
  82. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  83. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  84. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  85. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  86. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  87. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  88. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  89. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  90. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  91. package/dist/mcp/clientIntegrations.d.ts +1 -0
  92. package/dist/mcp/clientIntegrations.js +25 -0
  93. package/dist/mcp/clientIntegrations.test.js +14 -3
  94. package/dist/mcp/clientProfileDefaults.js +3 -3
  95. package/dist/mcp/collaborationRegistrar.js +7 -7
  96. package/dist/mcp/documentAssetRegistrar.js +113 -7
  97. package/dist/mcp/runtime.d.ts +1 -0
  98. package/dist/mcp/runtime.js +969 -122
  99. package/dist/mcp/runtime.test.js +1083 -11
  100. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  101. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  102. package/dist/mcp/toolRegistry.d.ts +56 -0
  103. package/dist/mcp/toolRegistry.js +8 -0
  104. package/dist/mcp/toolRegistry.test.js +4 -0
  105. package/dist/migrations/taskSchemaMigrations.js +21 -0
  106. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  107. package/dist/server/aiProfileAvatarAssets.js +319 -0
  108. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  109. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  110. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  111. package/dist/server/index.js +4 -0
  112. package/dist/server/index.test.js +6 -1
  113. package/dist/server/routes/admin.js +187 -9
  114. package/dist/server/routes/annotatedAttachments.js +51 -0
  115. package/dist/server/routes/auth.d.ts +11 -0
  116. package/dist/server/routes/auth.js +189 -27
  117. package/dist/server/routes/authSupport.d.ts +4 -0
  118. package/dist/server/routes/authSupport.js +12 -1
  119. package/dist/server/routes/billing.js +11 -0
  120. package/dist/server/routes/billing.test.js +3 -0
  121. package/dist/server/routes/documents.js +10 -0
  122. package/dist/server/routes/shared.js +0 -10
  123. package/dist/server/routes/sync.integration.test.js +221 -0
  124. package/dist/server/routes/sync.js +13 -3
  125. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  126. package/dist/server/routes/tasks.js +12 -9
  127. package/dist/server/routes.js +87 -8
  128. package/dist/server/routes.test.js +890 -22
  129. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  130. package/dist/shared/aiProfileSurfaceType.js +10 -8
  131. package/dist/storage/objectStorage.d.ts +1 -0
  132. package/dist/storage/objectStorage.js +2 -1
  133. package/dist/storage/objectStorageClient.d.ts +1 -0
  134. package/dist/storage/objectStorageClient.js +41 -8
  135. package/dist/storage/objectStorageClient.test.js +26 -1
  136. package/dist/storage/objectStorageConfig.js +3 -1
  137. package/dist/storage/objectStorageConfig.test.js +13 -1
  138. package/dist/sync/collaborationSyncPayload.js +2 -0
  139. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  140. package/dist/sync/collaborationSyncState.d.ts +3 -3
  141. package/dist/sync/collaborationSyncState.js +19 -12
  142. package/dist/sync/contentSyncPayload.d.ts +5 -1
  143. package/dist/sync/contentSyncPayload.js +9 -0
  144. package/dist/sync/contentSyncPayload.test.js +39 -0
  145. package/dist/sync/contentSyncState.d.ts +5 -1
  146. package/dist/sync/contentSyncState.js +231 -37
  147. package/dist/sync/planningSyncPayload.js +5 -0
  148. package/dist/sync/planningSyncPayload.test.js +24 -0
  149. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  150. package/dist/sync/syncApplyHandlers.js +89 -31
  151. package/dist/sync/syncApplyHandlers.test.js +184 -4
  152. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  153. package/dist/sync/syncResourceAdapters.js +2 -0
  154. package/dist/sync/syncService.d.ts +1 -0
  155. package/dist/sync/syncService.js +13 -1
  156. package/dist/sync/syncService.test.js +39 -0
  157. package/dist/sync/workspacePullFeed.d.ts +4 -0
  158. package/dist/sync/workspacePullFeed.js +129 -70
  159. package/dist/sync/workspacePullFeed.test.js +49 -0
  160. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  161. package/dist/sync/workspaceSyncModel.js +159 -45
  162. package/dist/sync/workspaceSyncModel.test.js +269 -3
  163. package/dist/types.d.ts +33 -2
  164. package/dist/ui/assets/AgentsModule-2y82_3DO.js +1 -0
  165. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BTLZZCiQ.js +3 -0
  166. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  167. package/dist/ui/assets/ContextAttachmentManager-D2epuNnG.js +3 -0
  168. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  169. package/dist/ui/assets/DocumentWorkspace-sVa-3Do6.js +252 -0
  170. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  171. package/dist/ui/assets/EntityActivityTimeline-Zods27y_.js +1 -0
  172. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-D4XseTwW.js} +1 -1
  173. package/dist/ui/assets/PlansPage-D5AcbO0L.js +1 -0
  174. package/dist/ui/assets/PlansPage-Dvqsz5zc.css +1 -0
  175. package/dist/ui/assets/TaskContextUpload-uC5qx4Bv.js +1 -0
  176. package/dist/ui/assets/TaskSettings-CqvPAsTv.js +9 -0
  177. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-oOA9bcdK.js} +4 -4
  178. package/dist/ui/assets/documentReferences-DPZuTgHh.js +1 -0
  179. package/dist/ui/assets/index-607WPo_X.js +4 -0
  180. package/dist/ui/assets/index-CYyodXsg.css +1 -0
  181. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  182. package/dist/ui/index.html +3 -3
  183. package/dist/ui/product/agent manager.png +0 -0
  184. package/dist/ui/product/agent_manager_02.png +0 -0
  185. package/dist/utils/aiProfileEvents.d.ts +7 -0
  186. package/dist/utils/aiProfileEvents.js +6 -0
  187. package/dist/utils/assignees.d.ts +6 -0
  188. package/dist/utils/assignees.js +8 -1
  189. package/dist/utils/assignees.test.d.ts +1 -0
  190. package/dist/utils/assignees.test.js +25 -0
  191. package/dist/utils/avatarDisplay.d.ts +1 -0
  192. package/dist/utils/avatarDisplay.js +15 -0
  193. package/dist/utils/contextFiles.d.ts +3 -1
  194. package/dist/utils/contextFiles.js +2 -1
  195. package/dist/utils/taskActivity.d.ts +7 -1
  196. package/dist/utils/taskActivity.js +63 -1
  197. package/dist/utils/taskActivity.test.js +23 -1
  198. package/dist/utils/taskSearch.d.ts +22 -0
  199. package/dist/utils/taskSearch.js +87 -0
  200. package/dist/utils/taskSearch.test.d.ts +1 -0
  201. package/dist/utils/taskSearch.test.js +69 -0
  202. package/dist/utils/taskUtils.js +10 -3
  203. package/dist/utils/taskUtils.test.d.ts +1 -0
  204. package/dist/utils/taskUtils.test.js +19 -0
  205. package/package.json +1 -1
  206. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  207. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  208. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  209. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  210. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  211. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  212. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  213. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  214. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  215. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -2,6 +2,7 @@ import type { DatabaseAdapter } from '../storage/databaseAdapter.js';
2
2
  export type AttachmentLinkInput = string | {
3
3
  path: string;
4
4
  fsPath?: string;
5
+ assetId?: string | null;
5
6
  caption?: string;
6
7
  displayName?: string;
7
8
  originalFilename?: string;
@@ -12,4 +13,6 @@ export interface AttachmentLinkServiceDeps {
12
13
  tenantId: string;
13
14
  normalizeWorkspaceId: (workspaceId: string) => string;
14
15
  }
16
+ export type AttachmentLinkTargetType = 'task' | 'initiative' | 'workstream';
17
+ export declare function syncCanonicalAttachmentLinksForTargetService(deps: AttachmentLinkServiceDeps, targetType: AttachmentLinkTargetType, targetId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
15
18
  export declare function syncCanonicalTaskAttachmentLinksService(deps: AttachmentLinkServiceDeps, taskId: string, workspaceId: string, attachments: AttachmentLinkInput[]): void;
@@ -63,6 +63,25 @@ function normalizeAttachmentStorageKey(pathValue, fsPathValue) {
63
63
  }
64
64
  return null;
65
65
  }
66
+ function extractAttachmentAssetId(attachment) {
67
+ const explicitAssetId = typeof attachment === 'string'
68
+ ? ''
69
+ : String(attachment.assetId || '').trim();
70
+ if (explicitAssetId)
71
+ return explicitAssetId;
72
+ const pathValue = typeof attachment === 'string'
73
+ ? attachment.trim()
74
+ : String(attachment.path || '').trim();
75
+ const match = pathValue.match(/^\/api\/taskforce\/documents\/([^/?#]+)\/content(?:[?#].*)?$/);
76
+ if (!match)
77
+ return null;
78
+ try {
79
+ return decodeURIComponent(match[1]).trim() || null;
80
+ }
81
+ catch {
82
+ return match[1].trim() || null;
83
+ }
84
+ }
66
85
  function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachment) {
67
86
  const storageKey = String(storageKeyRaw || '').trim();
68
87
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
@@ -124,25 +143,31 @@ function ensureExternalReferenceAsset(deps, storageKeyRaw, workspaceId, attachme
124
143
  `).run(assetId, deps.tenantId, normalizedWorkspaceId, 'file', logicalName, logicalName, mimeType, 'external', storageKey, urlHash, Buffer.byteLength(storageKey, 'utf8'), null, null, 1, 'clean', null, null, null, timestamp, timestamp);
125
144
  return assetId;
126
145
  }
127
- export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceId, attachments) {
128
- const normalizedTaskId = String(taskId || '').trim();
146
+ export function syncCanonicalAttachmentLinksForTargetService(deps, targetType, targetId, workspaceId, attachments) {
147
+ const normalizedTargetId = String(targetId || '').trim();
129
148
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
130
- if (!normalizedTaskId)
149
+ if (!normalizedTargetId)
131
150
  return;
132
151
  const supportsTargetType = supportsWorkspaceAssetLinkTargetType(deps.db);
152
+ const safeTargetType = targetType === 'initiative' || targetType === 'workstream'
153
+ ? targetType
154
+ : 'task';
155
+ const nowIso = new Date().toISOString();
133
156
  if (supportsTargetType) {
134
157
  deps.db.prepare(`
135
158
  UPDATE workspace_asset_links
136
159
  SET deleted_at = ?, updated_at = ?
137
- WHERE tenant_id = ? AND workspace_id = ? AND target_type = 'task' AND task_id = ? AND deleted_at IS NULL
138
- `).run(new Date().toISOString(), new Date().toISOString(), deps.tenantId, normalizedWorkspaceId, normalizedTaskId);
160
+ WHERE tenant_id = ? AND workspace_id = ? AND target_type = ? AND task_id = ? AND deleted_at IS NULL
161
+ `).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, safeTargetType, normalizedTargetId);
139
162
  }
140
163
  else {
164
+ if (safeTargetType !== 'task')
165
+ return;
141
166
  deps.db.prepare(`
142
167
  UPDATE workspace_asset_links
143
168
  SET deleted_at = ?, updated_at = ?
144
169
  WHERE tenant_id = ? AND workspace_id = ? AND task_id = ? AND deleted_at IS NULL
145
- `).run(new Date().toISOString(), new Date().toISOString(), deps.tenantId, normalizedWorkspaceId, normalizedTaskId);
170
+ `).run(nowIso, nowIso, deps.tenantId, normalizedWorkspaceId, normalizedTargetId);
146
171
  }
147
172
  const selectAsset = deps.db.prepare(`
148
173
  SELECT asset_id, kind
@@ -150,11 +175,17 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
150
175
  WHERE tenant_id = ? AND workspace_id = ? AND storage_key = ? AND deleted_at IS NULL
151
176
  LIMIT 1
152
177
  `);
178
+ const selectAssetById = deps.db.prepare(`
179
+ SELECT asset_id, kind
180
+ FROM workspace_assets
181
+ WHERE tenant_id = ? AND workspace_id = ? AND asset_id = ? AND deleted_at IS NULL
182
+ LIMIT 1
183
+ `);
153
184
  const upsertLink = supportsTargetType
154
185
  ? deps.db.prepare(`
155
186
  INSERT INTO workspace_asset_links (
156
187
  tenant_id, workspace_id, asset_id, target_type, task_id, role, display_name, sort_order, created_at, updated_at, deleted_at
157
- ) VALUES (?, ?, ?, 'task', ?, ?, ?, ?, ?, ?, NULL)
188
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
158
189
  ON CONFLICT (tenant_id, workspace_id, asset_id, task_id, role) DO UPDATE SET
159
190
  target_type = excluded.target_type,
160
191
  display_name = excluded.display_name,
@@ -174,16 +205,27 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
174
205
  `);
175
206
  attachments.forEach((attachment, index) => {
176
207
  const storageKey = normalizeAttachmentStorageKey(typeof attachment === 'string' ? attachment : attachment.path, typeof attachment === 'string' ? null : attachment.fsPath);
177
- if (!storageKey)
208
+ const attachmentAssetId = extractAttachmentAssetId(attachment);
209
+ if (!storageKey && !attachmentAssetId)
178
210
  return;
179
- const row = selectAsset.get(deps.tenantId, normalizedWorkspaceId, storageKey);
211
+ const row = (storageKey
212
+ ? selectAsset.get(deps.tenantId, normalizedWorkspaceId, storageKey)
213
+ : null)
214
+ || (attachmentAssetId
215
+ ? selectAssetById.get(deps.tenantId, normalizedWorkspaceId, attachmentAssetId)
216
+ : null);
217
+ const isExternalStorageKey = typeof storageKey === 'string' && /^https?:\/\//i.test(storageKey);
218
+ const hasExplicitFileStorageKey = typeof storageKey === 'string'
219
+ && typeof attachment !== 'string'
220
+ && typeof attachment.fsPath === 'string'
221
+ && attachment.fsPath.trim().length > 0;
180
222
  const assetId = String(row?.asset_id || '').trim()
181
- || ((/^https?:\/\//i.test(storageKey) || (typeof attachment !== 'string' && typeof attachment.fsPath === 'string' && attachment.fsPath.trim().length > 0))
223
+ || (isExternalStorageKey || hasExplicitFileStorageKey
182
224
  ? ensureExternalReferenceAsset(deps, storageKey, normalizedWorkspaceId, attachment)
183
225
  : '');
184
226
  if (!assetId)
185
227
  return;
186
- const role = /^https?:\/\//i.test(storageKey)
228
+ const role = isExternalStorageKey
187
229
  ? 'reference'
188
230
  : String(row?.kind || '').trim() === 'image'
189
231
  ? 'image'
@@ -192,8 +234,11 @@ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceI
192
234
  ? null
193
235
  : (typeof attachment === 'string'
194
236
  ? null
195
- : buildAttachmentDisplayName(attachment, /^https?:\/\//i.test(storageKey) ? null : path.basename(storageKey)));
237
+ : buildAttachmentDisplayName(attachment, isExternalStorageKey || !storageKey ? null : path.basename(storageKey)));
196
238
  const timestamp = typeof attachment === 'string' ? new Date().toISOString() : (attachment.timestamp || new Date().toISOString());
197
- upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId, normalizedTaskId, role, displayName, index, timestamp, timestamp);
239
+ upsertLink.run(deps.tenantId, normalizedWorkspaceId, assetId, safeTargetType, normalizedTargetId, role, displayName, index, timestamp, timestamp);
198
240
  });
199
241
  }
242
+ export function syncCanonicalTaskAttachmentLinksService(deps, taskId, workspaceId, attachments) {
243
+ syncCanonicalAttachmentLinksForTargetService(deps, 'task', taskId, workspaceId, attachments);
244
+ }
@@ -194,6 +194,12 @@ export function getGlobalSettingsService(deps) {
194
194
  : {}),
195
195
  ...(typeof global.site.pricingPageEnabled === 'boolean'
196
196
  ? { pricingPageEnabled: global.site.pricingPageEnabled }
197
+ : {}),
198
+ ...(typeof global.site.animatedBackgroundEnabled === 'boolean'
199
+ ? { animatedBackgroundEnabled: global.site.animatedBackgroundEnabled }
200
+ : {}),
201
+ ...(global.site.siteTheme === 'light' || global.site.siteTheme === 'dark'
202
+ ? { siteTheme: global.site.siteTheme }
197
203
  : {})
198
204
  }
199
205
  }
@@ -407,6 +413,12 @@ export function updateGlobalSettingsService(deps, settings) {
407
413
  : {}),
408
414
  ...(typeof incoming.pricingPageEnabled === 'boolean'
409
415
  ? { pricingPageEnabled: incoming.pricingPageEnabled }
416
+ : {}),
417
+ ...(typeof incoming.animatedBackgroundEnabled === 'boolean'
418
+ ? { animatedBackgroundEnabled: incoming.animatedBackgroundEnabled }
419
+ : {}),
420
+ ...(incoming.siteTheme === 'light' || incoming.siteTheme === 'dark'
421
+ ? { siteTheme: incoming.siteTheme }
410
422
  : {})
411
423
  };
412
424
  }
@@ -60,6 +60,11 @@ export declare class McpTokenService {
60
60
  workspaceId: string;
61
61
  tokenId: string;
62
62
  }): McpAccessTokenRecord;
63
+ deleteMcpAccessToken(input: {
64
+ actorUserId: string;
65
+ workspaceId: string;
66
+ tokenId: string;
67
+ }): boolean;
63
68
  regenerateMcpAccessToken(input: {
64
69
  actorUserId: string;
65
70
  workspaceId: string;
@@ -111,6 +116,13 @@ export declare class McpTokenService {
111
116
  grantId: string;
112
117
  }): McpOAuthConnectorGrantRecord;
113
118
  authenticateMcpOAuthAccessToken(token: string, workspaceId?: string): McpOAuthAccessTokenAuthResult | null;
119
+ bindMcpOAuthConnectorGrantAiProfile(input: {
120
+ grantId: string;
121
+ workspaceId: string;
122
+ profileId: string;
123
+ profileToken: string;
124
+ profileName: string;
125
+ }): boolean;
114
126
  recordMcpOAuthConnectorGrantUsage(input: {
115
127
  grantId: string;
116
128
  workspaceId: string;
@@ -518,6 +518,23 @@ export class McpTokenService {
518
518
  }
519
519
  return this.rowToMcpAccessTokenRecord(updated);
520
520
  }
521
+ deleteMcpAccessToken(input) {
522
+ this.getActiveWorkspaceMemberForMcp(input.actorUserId, input.workspaceId);
523
+ const row = this.getMcpAccessTokenRow(input.tokenId, input.workspaceId);
524
+ if (!row) {
525
+ throw new TaskforceRuleError('MCP token not found', 404, 'MCP_TOKEN_NOT_FOUND');
526
+ }
527
+ this.assertCanManageOwnMcpAccessToken(input.actorUserId, row);
528
+ const status = this.resolveMcpAccessTokenStatus(row);
529
+ if (status === 'active') {
530
+ throw new TaskforceRuleError('Active MCP tokens must be revoked before deletion', 400, 'MCP_TOKEN_DELETE_ACTIVE');
531
+ }
532
+ const result = this.db.prepare(`
533
+ DELETE FROM mcp_access_tokens
534
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
535
+ `).run(this.tenantId, input.workspaceId, row.id);
536
+ return Number(result.changes || 0) > 0;
537
+ }
521
538
  regenerateMcpAccessToken(input) {
522
539
  this.getActiveWorkspaceMemberForMcp(input.actorUserId, input.workspaceId);
523
540
  const existing = this.getMcpAccessTokenRow(input.tokenId, input.workspaceId);
@@ -872,7 +889,8 @@ export class McpTokenService {
872
889
  if (!parsed)
873
890
  return null;
874
891
  const row = this.db.prepare(`
875
- SELECT id, provider, client_id, user_id, workspace_id, scopes_json, token_hash, status, revoked_at
892
+ SELECT id, provider, client_id, user_id, workspace_id, scopes_json, token_hash, status, revoked_at,
893
+ ai_profile_id, ai_profile_token, ai_profile_name
876
894
  FROM mcp_oauth_connector_grants
877
895
  WHERE tenant_id = ?
878
896
  AND token_prefix = ?
@@ -902,8 +920,33 @@ export class McpTokenService {
902
920
  provider: this.normalizeMcpOAuthProvider(row.provider),
903
921
  scopes,
904
922
  status: this.resolveMcpOAuthGrantStatus(row),
923
+ aiProfileId: row?.ai_profile_id ? String(row.ai_profile_id) : null,
924
+ aiProfileToken: row?.ai_profile_token ? String(row.ai_profile_token) : null,
925
+ aiProfileName: row?.ai_profile_name ? String(row.ai_profile_name) : null,
905
926
  };
906
927
  }
928
+ bindMcpOAuthConnectorGrantAiProfile(input) {
929
+ const grantId = String(input.grantId || '').trim();
930
+ const workspaceId = String(input.workspaceId || '').trim();
931
+ const profileId = String(input.profileId || '').trim();
932
+ const profileToken = String(input.profileToken || '').trim();
933
+ const profileName = String(input.profileName || '').trim();
934
+ if (!grantId || !workspaceId || !profileId || !profileToken || !profileName)
935
+ return false;
936
+ const now = new Date().toISOString();
937
+ const result = this.db.prepare(`
938
+ UPDATE mcp_oauth_connector_grants
939
+ SET ai_profile_id = ?,
940
+ ai_profile_token = ?,
941
+ ai_profile_name = ?,
942
+ updated_at = ?
943
+ WHERE tenant_id = ?
944
+ AND workspace_id = ?
945
+ AND id = ?
946
+ AND status = 'active'
947
+ `).run(profileId, profileToken, profileName, now, this.tenantId, workspaceId, grantId);
948
+ return Number(result?.changes || 0) > 0;
949
+ }
907
950
  recordMcpOAuthConnectorGrantUsage(input) {
908
951
  const grantId = String(input.grantId || '').trim();
909
952
  const workspaceId = String(input.workspaceId || '').trim();
@@ -22,13 +22,33 @@ describe('TaskforceCore planning entity persistence', () => {
22
22
  const initiative = core.createInitiative({
23
23
  title: 'Launch initiative',
24
24
  comments: [{ id: 'comment-1', author: 'user', text: 'Kickoff note', timestamp: '2026-01-01T00:00:00.000Z' }],
25
- attachments: [{ path: '/tmp/launch-brief.md', caption: 'Launch brief', timestamp: '2026-01-01T00:00:00.000Z' }],
25
+ attachments: [{
26
+ path: '/tmp/launch-brief.md',
27
+ fsPath: 'workspaces/workspace-1/assets/documents/asset-launch-brief.md',
28
+ caption: 'Launch brief',
29
+ displayName: 'Launch Brief',
30
+ originalFilename: 'launch-brief.md',
31
+ assetId: 'asset-launch-brief',
32
+ referenceNumber: 12,
33
+ referenceLabel: 'D-12',
34
+ timestamp: '2026-01-01T00:00:00.000Z',
35
+ }],
26
36
  });
27
37
  const workstream = core.createWorkstream({
28
38
  title: 'Billing workstream',
29
39
  initiativeId: initiative.id,
30
40
  comments: [{ id: 'comment-2', author: 'user', text: 'Billing note', timestamp: '2026-01-01T00:00:00.000Z' }],
31
- attachments: [{ path: '/tmp/billing-brief.md', caption: 'Billing brief', timestamp: '2026-01-01T00:00:00.000Z' }],
41
+ attachments: [{
42
+ path: '/tmp/billing-brief.md',
43
+ fsPath: 'workspaces/workspace-1/assets/documents/asset-billing-brief.md',
44
+ caption: 'Billing brief',
45
+ displayName: 'Billing Brief',
46
+ originalFilename: 'billing-brief.md',
47
+ assetId: 'asset-billing-brief',
48
+ referenceNumber: 13,
49
+ referenceLabel: 'D-13',
50
+ timestamp: '2026-01-01T00:00:00.000Z',
51
+ }],
32
52
  });
33
53
  const task = core.createTask({
34
54
  title: 'Adjust billing copy',
@@ -43,11 +63,29 @@ describe('TaskforceCore planning entity persistence', () => {
43
63
  const persistedTask = core.getTask(task.id);
44
64
  expect(persistedInitiative?.referenceLabel).toBe('IN-1');
45
65
  expect(persistedInitiative?.comments?.[0]?.text).toBe('Kickoff note');
46
- expect(persistedInitiative?.attachments?.[0]).toMatchObject({ path: '/tmp/launch-brief.md' });
66
+ expect(persistedInitiative?.attachments?.[0]).toMatchObject({
67
+ path: '/tmp/launch-brief.md',
68
+ fsPath: 'workspaces/workspace-1/assets/documents/asset-launch-brief.md',
69
+ caption: 'Launch brief',
70
+ displayName: 'Launch Brief',
71
+ originalFilename: 'launch-brief.md',
72
+ assetId: 'asset-launch-brief',
73
+ referenceNumber: 12,
74
+ referenceLabel: 'D-12',
75
+ });
47
76
  expect(persistedWorkstream?.referenceLabel).toBe('WS-1');
48
77
  expect(persistedWorkstream?.initiativeId).toBe(initiative.id);
49
78
  expect(persistedWorkstream?.comments?.[0]?.text).toBe('Billing note');
50
- expect(persistedWorkstream?.attachments?.[0]).toMatchObject({ path: '/tmp/billing-brief.md' });
79
+ expect(persistedWorkstream?.attachments?.[0]).toMatchObject({
80
+ path: '/tmp/billing-brief.md',
81
+ fsPath: 'workspaces/workspace-1/assets/documents/asset-billing-brief.md',
82
+ caption: 'Billing brief',
83
+ displayName: 'Billing Brief',
84
+ originalFilename: 'billing-brief.md',
85
+ assetId: 'asset-billing-brief',
86
+ referenceNumber: 13,
87
+ referenceLabel: 'D-13',
88
+ });
51
89
  expect(persistedTask?.workstreamId).toBe(workstream.id);
52
90
  expect(persistedTask?.checklistItems?.map((item) => item.title)).toEqual([
53
91
  'Update docs',
@@ -92,6 +130,328 @@ describe('TaskforceCore planning entity persistence', () => {
92
130
  expect(clearedTask?.workstreamId).toBeNull();
93
131
  core.close();
94
132
  });
133
+ it('syncs canonical attachment links for initiatives and workstreams like task attachments do', () => {
134
+ const core = createCore();
135
+ const workspaceId = 'default';
136
+ const assetStore = core.getWorkspaceAssetStore();
137
+ assetStore.upsertAsset({
138
+ assetId: 'asset-initiative-doc',
139
+ workspaceId,
140
+ kind: 'document',
141
+ logicalName: 'Initiative context',
142
+ originalFilename: 'initiative-context.md',
143
+ mimeType: 'text/markdown',
144
+ storageProvider: 'local',
145
+ storageKey: 'workspaces/default/assets/documents/initiative-context.md',
146
+ contentSha256: 'sha-init',
147
+ sizeBytes: 120,
148
+ documentId: 'doc-initiative-context',
149
+ version: 1,
150
+ isLatest: 1,
151
+ });
152
+ assetStore.upsertAsset({
153
+ assetId: 'asset-workstream-doc',
154
+ workspaceId,
155
+ kind: 'document',
156
+ logicalName: 'Workstream context',
157
+ originalFilename: 'workstream-context.md',
158
+ mimeType: 'text/markdown',
159
+ storageProvider: 'local',
160
+ storageKey: 'workspaces/default/assets/documents/workstream-context.md',
161
+ contentSha256: 'sha-workstream',
162
+ sizeBytes: 140,
163
+ documentId: 'doc-workstream-context',
164
+ version: 1,
165
+ isLatest: 1,
166
+ });
167
+ const initiative = core.createInitiative({
168
+ title: 'Platform',
169
+ attachments: [{
170
+ path: '/api/taskforce/documents/asset-initiative-doc/content',
171
+ fsPath: 'workspaces/default/assets/documents/initiative-context.md',
172
+ caption: 'Initiative context',
173
+ assetId: 'asset-initiative-doc',
174
+ timestamp: '2026-01-01T00:00:00.000Z',
175
+ }],
176
+ }, workspaceId);
177
+ const workstream = core.createWorkstream({
178
+ title: 'Sync',
179
+ initiativeId: initiative.id,
180
+ attachments: [{
181
+ path: '/api/taskforce/documents/asset-workstream-doc/content',
182
+ fsPath: 'workspaces/default/assets/documents/workstream-context.md',
183
+ caption: 'Workstream context',
184
+ assetId: 'asset-workstream-doc',
185
+ timestamp: '2026-01-01T00:00:00.000Z',
186
+ }],
187
+ }, workspaceId);
188
+ expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
189
+ expect.objectContaining({
190
+ assetId: 'asset-initiative-doc',
191
+ targetType: 'initiative',
192
+ targetId: initiative.id,
193
+ role: 'attachment',
194
+ })
195
+ ]);
196
+ expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
197
+ expect.objectContaining({
198
+ assetId: 'asset-workstream-doc',
199
+ targetType: 'workstream',
200
+ targetId: workstream.id,
201
+ role: 'attachment',
202
+ })
203
+ ]);
204
+ core.updateInitiative(initiative.id, { attachments: [] }, workspaceId);
205
+ core.updateWorkstream(workstream.id, { attachments: [] }, workspaceId);
206
+ expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([]);
207
+ expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId, { includeDeleted: true })).toEqual([
208
+ expect.objectContaining({
209
+ assetId: 'asset-initiative-doc',
210
+ deletedAt: expect.any(String),
211
+ })
212
+ ]);
213
+ expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([]);
214
+ expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId, { includeDeleted: true })).toEqual([
215
+ expect.objectContaining({
216
+ assetId: 'asset-workstream-doc',
217
+ deletedAt: expect.any(String),
218
+ })
219
+ ]);
220
+ core.close();
221
+ });
222
+ it('syncs sparse canonical document attachment links by asset id for planning entities', () => {
223
+ const core = createCore();
224
+ const workspaceId = 'default';
225
+ const assetStore = core.getWorkspaceAssetStore();
226
+ assetStore.upsertAsset({
227
+ assetId: 'asset-sparse-initiative-doc',
228
+ workspaceId,
229
+ kind: 'document',
230
+ logicalName: 'Sparse initiative context',
231
+ originalFilename: 'sparse-initiative.md',
232
+ mimeType: 'text/markdown',
233
+ storageProvider: 'local',
234
+ storageKey: 'workspaces/default/assets/documents/sparse-initiative.md',
235
+ contentSha256: 'sha-sparse-initiative',
236
+ sizeBytes: 120,
237
+ documentId: 'doc-sparse-initiative',
238
+ version: 1,
239
+ isLatest: 1,
240
+ });
241
+ assetStore.upsertAsset({
242
+ assetId: 'asset-sparse-workstream-doc',
243
+ workspaceId,
244
+ kind: 'document',
245
+ logicalName: 'Sparse workstream context',
246
+ originalFilename: 'sparse-workstream.md',
247
+ mimeType: 'text/markdown',
248
+ storageProvider: 'local',
249
+ storageKey: 'workspaces/default/assets/documents/sparse-workstream.md',
250
+ contentSha256: 'sha-sparse-workstream',
251
+ sizeBytes: 140,
252
+ documentId: 'doc-sparse-workstream',
253
+ version: 1,
254
+ isLatest: 1,
255
+ });
256
+ const initiative = core.createInitiative({
257
+ title: 'Platform',
258
+ attachments: [{
259
+ path: '/api/taskforce/documents/asset-sparse-initiative-doc/content',
260
+ assetId: 'asset-sparse-initiative-doc',
261
+ caption: 'Sparse initiative context',
262
+ referenceLabel: 'D-101',
263
+ timestamp: '2026-01-01T00:00:00.000Z',
264
+ }],
265
+ }, workspaceId);
266
+ const workstream = core.createWorkstream({
267
+ title: 'Sync',
268
+ initiativeId: initiative.id,
269
+ attachments: [{
270
+ path: '/api/taskforce/documents/asset-sparse-workstream-doc/content',
271
+ assetId: 'asset-sparse-workstream-doc',
272
+ caption: 'Sparse workstream context',
273
+ referenceLabel: 'D-102',
274
+ timestamp: '2026-01-01T00:00:00.000Z',
275
+ }],
276
+ }, workspaceId);
277
+ expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
278
+ expect.objectContaining({
279
+ assetId: 'asset-sparse-initiative-doc',
280
+ targetType: 'initiative',
281
+ targetId: initiative.id,
282
+ role: 'attachment',
283
+ })
284
+ ]);
285
+ expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
286
+ expect.objectContaining({
287
+ assetId: 'asset-sparse-workstream-doc',
288
+ targetType: 'workstream',
289
+ targetId: workstream.id,
290
+ role: 'attachment',
291
+ })
292
+ ]);
293
+ core.close();
294
+ });
295
+ it('preserves planning attachments when sync upserts omit attachment fields', () => {
296
+ const core = createCore();
297
+ const workspaceId = 'default';
298
+ const assetStore = core.getWorkspaceAssetStore();
299
+ assetStore.upsertAsset({
300
+ assetId: 'asset-preserved-initiative-doc',
301
+ workspaceId,
302
+ kind: 'document',
303
+ logicalName: 'Preserved initiative context',
304
+ originalFilename: 'preserved-initiative.md',
305
+ mimeType: 'text/markdown',
306
+ storageProvider: 'local',
307
+ storageKey: 'workspaces/default/assets/documents/preserved-initiative.md',
308
+ contentSha256: 'sha-preserved-initiative',
309
+ sizeBytes: 120,
310
+ documentId: 'doc-preserved-initiative',
311
+ version: 1,
312
+ isLatest: 1,
313
+ });
314
+ assetStore.upsertAsset({
315
+ assetId: 'asset-preserved-workstream-doc',
316
+ workspaceId,
317
+ kind: 'document',
318
+ logicalName: 'Preserved workstream context',
319
+ originalFilename: 'preserved-workstream.md',
320
+ mimeType: 'text/markdown',
321
+ storageProvider: 'local',
322
+ storageKey: 'workspaces/default/assets/documents/preserved-workstream.md',
323
+ contentSha256: 'sha-preserved-workstream',
324
+ sizeBytes: 140,
325
+ documentId: 'doc-preserved-workstream',
326
+ version: 1,
327
+ isLatest: 1,
328
+ });
329
+ const initiative = core.createInitiative({
330
+ title: 'Platform',
331
+ attachments: [{
332
+ path: '/api/taskforce/documents/asset-preserved-initiative-doc/content',
333
+ assetId: 'asset-preserved-initiative-doc',
334
+ caption: 'Preserved initiative context',
335
+ timestamp: '2026-01-01T00:00:00.000Z',
336
+ }],
337
+ }, workspaceId);
338
+ const workstream = core.createWorkstream({
339
+ title: 'Sync',
340
+ initiativeId: initiative.id,
341
+ attachments: [{
342
+ path: '/api/taskforce/documents/asset-preserved-workstream-doc/content',
343
+ assetId: 'asset-preserved-workstream-doc',
344
+ caption: 'Preserved workstream context',
345
+ timestamp: '2026-01-01T00:00:00.000Z',
346
+ }],
347
+ }, workspaceId);
348
+ core.upsertInitiativeForSync({
349
+ id: initiative.id,
350
+ referenceNumber: initiative.referenceNumber,
351
+ title: 'Platform updated elsewhere',
352
+ description: initiative.description,
353
+ ownerId: initiative.ownerId,
354
+ createdAt: initiative.createdAt,
355
+ updatedAt: '2026-01-01T00:05:00.000Z',
356
+ order: initiative.order,
357
+ isArchived: false,
358
+ }, workspaceId);
359
+ core.upsertWorkstreamForSync({
360
+ id: workstream.id,
361
+ referenceNumber: workstream.referenceNumber,
362
+ initiativeId: initiative.id,
363
+ title: 'Sync updated elsewhere',
364
+ description: workstream.description,
365
+ ownerId: workstream.ownerId,
366
+ createdAt: workstream.createdAt,
367
+ updatedAt: '2026-01-01T00:05:00.000Z',
368
+ order: workstream.order,
369
+ isArchived: false,
370
+ }, workspaceId);
371
+ expect(core.getInitiative(initiative.id, workspaceId)?.attachments).toEqual([
372
+ expect.objectContaining({
373
+ assetId: 'asset-preserved-initiative-doc',
374
+ caption: 'Preserved initiative context',
375
+ })
376
+ ]);
377
+ expect(core.getWorkstream(workstream.id, workspaceId)?.attachments).toEqual([
378
+ expect.objectContaining({
379
+ assetId: 'asset-preserved-workstream-doc',
380
+ caption: 'Preserved workstream context',
381
+ })
382
+ ]);
383
+ expect(assetStore.listLinksForTarget('initiative', initiative.id, workspaceId)).toEqual([
384
+ expect.objectContaining({
385
+ assetId: 'asset-preserved-initiative-doc',
386
+ deletedAt: null,
387
+ })
388
+ ]);
389
+ expect(assetStore.listLinksForTarget('workstream', workstream.id, workspaceId)).toEqual([
390
+ expect.objectContaining({
391
+ assetId: 'asset-preserved-workstream-doc',
392
+ deletedAt: null,
393
+ })
394
+ ]);
395
+ core.close();
396
+ });
397
+ it('rejects stale planning sync upserts so older empty snapshots cannot clear newer attachments', () => {
398
+ const core = createCore();
399
+ const workspaceId = 'default';
400
+ const initiative = core.createInitiative({
401
+ title: 'Cloud initiative',
402
+ updatedAt: '2026-01-01T00:10:00.000Z',
403
+ attachments: [{
404
+ path: '/api/taskforce/documents/asset-newer-initiative-doc/content',
405
+ assetId: 'asset-newer-initiative-doc',
406
+ caption: 'Newer initiative doc',
407
+ timestamp: '2026-01-01T00:10:00.000Z',
408
+ }],
409
+ }, workspaceId);
410
+ const workstream = core.createWorkstream({
411
+ title: 'Cloud workstream',
412
+ initiativeId: initiative.id,
413
+ updatedAt: '2026-01-01T00:10:00.000Z',
414
+ attachments: [{
415
+ path: '/api/taskforce/documents/asset-newer-workstream-doc/content',
416
+ assetId: 'asset-newer-workstream-doc',
417
+ caption: 'Newer workstream doc',
418
+ timestamp: '2026-01-01T00:10:00.000Z',
419
+ }],
420
+ }, workspaceId);
421
+ core.upsertInitiativeForSync({
422
+ ...initiative,
423
+ title: 'Stale initiative snapshot',
424
+ updatedAt: '2026-01-01T00:05:00.000Z',
425
+ attachments: [],
426
+ }, workspaceId);
427
+ core.upsertWorkstreamForSync({
428
+ ...workstream,
429
+ title: 'Stale workstream snapshot',
430
+ updatedAt: '2026-01-01T00:05:00.000Z',
431
+ attachments: [],
432
+ }, workspaceId);
433
+ expect(core.getInitiative(initiative.id, workspaceId)).toEqual(expect.objectContaining({
434
+ title: 'Cloud initiative',
435
+ updatedAt: '2026-01-01T00:10:00.000Z',
436
+ attachments: [
437
+ expect.objectContaining({
438
+ assetId: 'asset-newer-initiative-doc',
439
+ caption: 'Newer initiative doc',
440
+ })
441
+ ],
442
+ }));
443
+ expect(core.getWorkstream(workstream.id, workspaceId)).toEqual(expect.objectContaining({
444
+ title: 'Cloud workstream',
445
+ updatedAt: '2026-01-01T00:10:00.000Z',
446
+ attachments: [
447
+ expect.objectContaining({
448
+ assetId: 'asset-newer-workstream-doc',
449
+ caption: 'Newer workstream doc',
450
+ })
451
+ ],
452
+ }));
453
+ core.close();
454
+ });
95
455
  it('builds lightweight planning bootstrap summaries without hydrating full task details', () => {
96
456
  const core = createCore();
97
457
  const initiative = core.createInitiative({ title: 'Launch initiative' });
@@ -127,6 +487,8 @@ describe('TaskforceCore planning entity persistence', () => {
127
487
  const billingSummary = bootstrap.workstreamTaskSummaries.find((entry) => entry.workstreamId === billingWorkstream.id);
128
488
  expect(bootstrap.initiatives.map((entry) => entry.id)).toEqual([initiative.id]);
129
489
  expect(bootstrap.workstreams.map((entry) => entry.id)).toEqual([deliveryWorkstream.id, billingWorkstream.id]);
490
+ expect(bootstrap.initiatives.every((entry) => entry.activity === undefined && entry.entityEvents === undefined)).toBe(true);
491
+ expect(bootstrap.workstreams.every((entry) => entry.activity === undefined && entry.entityEvents === undefined)).toBe(true);
130
492
  expect(deliverySummary).toMatchObject({
131
493
  workstreamId: deliveryWorkstream.id,
132
494
  taskCount: 2,