@taskforcehq/taskforce 0.3.306 → 0.3.308

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 (232) 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.d.ts +20 -15
  38. package/dist/components/views/PlanComparisonPage.js +221 -82
  39. package/dist/components/views/PlanComparisonPage.test.js +268 -14
  40. package/dist/components/views/PlansPage.js +16 -10
  41. package/dist/components/views/PlansPage.test.js +4 -2
  42. package/dist/components/views/StandaloneLayout.js +256 -69
  43. package/dist/components/views/WidgetView.js +10 -8
  44. package/dist/components/views/panels/PlanningDrawer.d.ts +19 -6
  45. package/dist/components/views/panels/PlanningDrawer.js +259 -51
  46. package/dist/components/views/panels/PlanningDrawer.test.js +392 -4
  47. package/dist/config/envSchema.js +1 -0
  48. package/dist/core/AiProfileService.d.ts +7 -0
  49. package/dist/core/AiProfileService.js +460 -35
  50. package/dist/core/AiProfiles.test.js +648 -2
  51. package/dist/core/AttachmentLinkService.d.ts +3 -0
  52. package/dist/core/AttachmentLinkService.js +58 -13
  53. package/dist/core/GlobalSettingsService.js +30 -0
  54. package/dist/core/GlobalSettingsService.test.d.ts +1 -0
  55. package/dist/core/GlobalSettingsService.test.js +45 -0
  56. package/dist/core/McpTokenService.d.ts +12 -0
  57. package/dist/core/McpTokenService.js +44 -1
  58. package/dist/core/PlanEntitlementService.d.ts +20 -0
  59. package/dist/core/PlanEntitlementService.js +215 -19
  60. package/dist/core/PlanFeatureCatalog.test.js +55 -0
  61. package/dist/core/PlanningEntities.test.js +366 -4
  62. package/dist/core/TaskActivityService.d.ts +28 -1
  63. package/dist/core/TaskActivityService.js +227 -46
  64. package/dist/core/TaskActivitySync.test.d.ts +1 -0
  65. package/dist/core/TaskActivitySync.test.js +90 -0
  66. package/dist/core/TaskAuditTimeline.test.js +96 -0
  67. package/dist/core/Taskforce.d.ts +204 -26
  68. package/dist/core/Taskforce.ids.test.js +58 -0
  69. package/dist/core/Taskforce.js +781 -71
  70. package/dist/core/Taskforce.mcpAuth.test.js +109 -0
  71. package/dist/core/types.d.ts +10 -0
  72. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +2 -1
  73. package/dist/hooks/bootstrapTimeouts.d.ts +4 -0
  74. package/dist/hooks/bootstrapTimeouts.js +8 -0
  75. package/dist/hooks/bootstrapTimeouts.test.d.ts +1 -0
  76. package/dist/hooks/bootstrapTimeouts.test.js +12 -0
  77. package/dist/hooks/sync/orchestratorShared.d.ts +3 -3
  78. package/dist/hooks/sync/orchestratorShared.js +61 -1
  79. package/dist/hooks/sync/recovery.js +8 -0
  80. package/dist/hooks/sync/recovery.test.js +5 -5
  81. package/dist/hooks/sync/transfers.d.ts +2 -1
  82. package/dist/hooks/sync/transfers.js +36 -9
  83. package/dist/hooks/sync/transfers.test.js +534 -2
  84. package/dist/hooks/useFilterSort.js +13 -13
  85. package/dist/hooks/useFilterSort.test.js +87 -1
  86. package/dist/hooks/useSyncOrchestrator.js +50 -5
  87. package/dist/hooks/useTaskforce.d.ts +85 -50
  88. package/dist/hooks/useTaskforce.js +44 -14
  89. package/dist/hooks/useTaskforce.runtime-routing.test.js +94 -0
  90. package/dist/hooks/useTaskforce.sync-behavior.test.js +99 -0
  91. package/dist/hooks/useWorkspaceSyncController.js +7 -1
  92. package/dist/hooks/useWorkspaceSyncController.test.js +48 -0
  93. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +14 -7
  94. package/dist/mcp/canonicalAssetHelpers.d.ts +16 -4
  95. package/dist/mcp/canonicalAssetHelpers.js +18 -10
  96. package/dist/mcp/canonicalAssetHelpers.test.d.ts +1 -0
  97. package/dist/mcp/canonicalAssetHelpers.test.js +67 -0
  98. package/dist/mcp/clientIntegrations.d.ts +1 -0
  99. package/dist/mcp/clientIntegrations.js +25 -0
  100. package/dist/mcp/clientIntegrations.test.js +14 -3
  101. package/dist/mcp/clientProfileDefaults.js +3 -3
  102. package/dist/mcp/collaborationRegistrar.js +7 -7
  103. package/dist/mcp/documentAssetRegistrar.js +113 -7
  104. package/dist/mcp/runtime.d.ts +1 -0
  105. package/dist/mcp/runtime.js +969 -122
  106. package/dist/mcp/runtime.test.js +1083 -11
  107. package/dist/mcp/taskAttachmentTypes.d.ts +1 -1
  108. package/dist/mcp/taskPlanningRegistrar.js +48 -0
  109. package/dist/mcp/toolRegistry.d.ts +56 -0
  110. package/dist/mcp/toolRegistry.js +8 -0
  111. package/dist/mcp/toolRegistry.test.js +4 -0
  112. package/dist/migrations/taskSchemaMigrations.js +58 -15
  113. package/dist/server/aiProfileAvatarAssets.d.ts +49 -0
  114. package/dist/server/aiProfileAvatarAssets.js +319 -0
  115. package/dist/server/aiProfileAvatarAssets.test.d.ts +1 -0
  116. package/dist/server/aiProfileAvatarAssets.test.js +213 -0
  117. package/dist/server/annotatedAttachmentsRoutes.test.js +66 -7
  118. package/dist/server/index.js +4 -0
  119. package/dist/server/index.test.js +6 -1
  120. package/dist/server/routes/admin.js +328 -9
  121. package/dist/server/routes/annotatedAttachments.js +51 -0
  122. package/dist/server/routes/auth.d.ts +11 -0
  123. package/dist/server/routes/auth.js +189 -27
  124. package/dist/server/routes/authSupport.d.ts +5 -0
  125. package/dist/server/routes/authSupport.js +13 -1
  126. package/dist/server/routes/billing.d.ts +1 -0
  127. package/dist/server/routes/billing.js +116 -63
  128. package/dist/server/routes/billing.test.js +162 -11
  129. package/dist/server/routes/documents.js +10 -0
  130. package/dist/server/routes/shared.js +0 -10
  131. package/dist/server/routes/sync.integration.test.js +221 -0
  132. package/dist/server/routes/sync.js +13 -3
  133. package/dist/server/routes/syncSnapshotRoutes.js +2 -0
  134. package/dist/server/routes/tasks.js +12 -9
  135. package/dist/server/routes.js +87 -8
  136. package/dist/server/routes.test.js +972 -23
  137. package/dist/shared/aiProfileSurfaceType.d.ts +1 -1
  138. package/dist/shared/aiProfileSurfaceType.js +10 -8
  139. package/dist/storage/objectStorage.d.ts +1 -0
  140. package/dist/storage/objectStorage.js +2 -1
  141. package/dist/storage/objectStorageClient.d.ts +1 -0
  142. package/dist/storage/objectStorageClient.js +41 -8
  143. package/dist/storage/objectStorageClient.test.js +26 -1
  144. package/dist/storage/objectStorageConfig.js +3 -1
  145. package/dist/storage/objectStorageConfig.test.js +13 -1
  146. package/dist/sync/collaborationSyncPayload.js +2 -0
  147. package/dist/sync/collaborationSyncPayload.test.js +1 -0
  148. package/dist/sync/collaborationSyncState.d.ts +3 -3
  149. package/dist/sync/collaborationSyncState.js +19 -12
  150. package/dist/sync/contentSyncPayload.d.ts +5 -1
  151. package/dist/sync/contentSyncPayload.js +9 -0
  152. package/dist/sync/contentSyncPayload.test.js +39 -0
  153. package/dist/sync/contentSyncState.d.ts +5 -1
  154. package/dist/sync/contentSyncState.js +231 -37
  155. package/dist/sync/planningSyncPayload.js +5 -0
  156. package/dist/sync/planningSyncPayload.test.js +24 -0
  157. package/dist/sync/syncApplyHandlers.d.ts +4 -4
  158. package/dist/sync/syncApplyHandlers.js +89 -31
  159. package/dist/sync/syncApplyHandlers.test.js +184 -4
  160. package/dist/sync/syncResourceAdapters.d.ts +2 -0
  161. package/dist/sync/syncResourceAdapters.js +2 -0
  162. package/dist/sync/syncService.d.ts +1 -0
  163. package/dist/sync/syncService.js +13 -1
  164. package/dist/sync/syncService.test.js +39 -0
  165. package/dist/sync/workspacePullFeed.d.ts +4 -0
  166. package/dist/sync/workspacePullFeed.js +129 -70
  167. package/dist/sync/workspacePullFeed.test.js +49 -0
  168. package/dist/sync/workspaceSyncModel.d.ts +28 -6
  169. package/dist/sync/workspaceSyncModel.js +159 -45
  170. package/dist/sync/workspaceSyncModel.test.js +269 -3
  171. package/dist/types.d.ts +33 -2
  172. package/dist/ui/agent-logos/ChatGPT.png +0 -0
  173. package/dist/ui/agent-logos/Gemini CLI.jpeg +0 -0
  174. package/dist/ui/agent-logos/antigravity.jpeg +0 -0
  175. package/dist/ui/agent-logos/claude-code.jpeg +0 -0
  176. package/dist/ui/agent-logos/codex.jpeg +0 -0
  177. package/dist/ui/agent-logos/cursor.png +0 -0
  178. package/dist/ui/agent-logos/openclaw.jpeg +0 -0
  179. package/dist/ui/agent-logos/windsurf.png +0 -0
  180. package/dist/ui/assets/AgentsModule-BS4Pi_nC.js +1 -0
  181. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BVKew-mH.css +1 -0
  182. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CCckzWYZ.js +3 -0
  183. package/dist/ui/assets/ContextAttachmentManager-CfG_ER7C.js +3 -0
  184. package/dist/ui/assets/DocumentWorkspace-BVaWPA25.js +252 -0
  185. package/dist/ui/assets/DocumentWorkspace-D4rJKXzd.css +1 -0
  186. package/dist/ui/assets/EntityActivityTimeline-DSj0ThaK.js +1 -0
  187. package/dist/ui/assets/EntityActivityTimeline-DyqvvhuF.css +1 -0
  188. package/dist/ui/assets/{InitiativesModule-9E6ParrY.js → InitiativesModule-AxMkWtxH.js} +1 -1
  189. package/dist/ui/assets/PlansPage-B_z-D6Nh.css +1 -0
  190. package/dist/ui/assets/PlansPage-Ndt7mYfo.js +1 -0
  191. package/dist/ui/assets/TaskContextUpload-Dw4rTF4i.js +1 -0
  192. package/dist/ui/assets/TaskSettings-f2ga42_V.js +9 -0
  193. package/dist/ui/assets/{WorkflowsModule-BA7jcEpH.js → WorkflowsModule-E4UL3mov.js} +4 -4
  194. package/dist/ui/assets/documentReferences-BOUcJm6-.js +1 -0
  195. package/dist/ui/assets/index-BvAbqx5Q.css +1 -0
  196. package/dist/ui/assets/index-MAHKvFqp.js +4 -0
  197. package/dist/ui/assets/{vendor-icons-DuEd_65c.js → vendor-icons-B32hGuF2.js} +1 -1
  198. package/dist/ui/index.html +3 -3
  199. package/dist/ui/og-image.png +0 -0
  200. package/dist/ui/product/agent manager.png +0 -0
  201. package/dist/ui/product/agent_manager_02.png +0 -0
  202. package/dist/utils/aiProfileEvents.d.ts +7 -0
  203. package/dist/utils/aiProfileEvents.js +6 -0
  204. package/dist/utils/assignees.d.ts +6 -0
  205. package/dist/utils/assignees.js +8 -1
  206. package/dist/utils/assignees.test.d.ts +1 -0
  207. package/dist/utils/assignees.test.js +25 -0
  208. package/dist/utils/avatarDisplay.d.ts +1 -0
  209. package/dist/utils/avatarDisplay.js +15 -0
  210. package/dist/utils/contextFiles.d.ts +3 -1
  211. package/dist/utils/contextFiles.js +2 -1
  212. package/dist/utils/taskActivity.d.ts +7 -1
  213. package/dist/utils/taskActivity.js +63 -1
  214. package/dist/utils/taskActivity.test.js +23 -1
  215. package/dist/utils/taskSearch.d.ts +22 -0
  216. package/dist/utils/taskSearch.js +87 -0
  217. package/dist/utils/taskSearch.test.d.ts +1 -0
  218. package/dist/utils/taskSearch.test.js +69 -0
  219. package/dist/utils/taskUtils.js +10 -3
  220. package/dist/utils/taskUtils.test.d.ts +1 -0
  221. package/dist/utils/taskUtils.test.js +19 -0
  222. package/package.json +1 -1
  223. package/dist/ui/assets/AgentsModule-DrLawwNl.js +0 -1
  224. package/dist/ui/assets/AnnotatedAttachmentWorkspace-BaS2VwIr.css +0 -1
  225. package/dist/ui/assets/AnnotatedAttachmentWorkspace-Z9_whf7F.js +0 -3
  226. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +0 -252
  227. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +0 -1
  228. package/dist/ui/assets/PlansPage-CvfnMiWq.css +0 -1
  229. package/dist/ui/assets/PlansPage-DRXscYxH.js +0 -1
  230. package/dist/ui/assets/TaskSettings-DZX4jk7e.js +0 -9
  231. package/dist/ui/assets/index-9eT8e0Lu.css +0 -1
  232. package/dist/ui/assets/index-C6k75jr8.js +0 -6
@@ -1,4 +1,4 @@
1
- function normalizeTaskEventActor(deps, actorRef, workspaceId) {
1
+ function normalizeEntityEventActor(deps, actorRef, workspaceId) {
2
2
  const actorProfile = deps.resolveWorkspaceActor(workspaceId, actorRef);
3
3
  const actor = String(actorRef || '').trim() || 'system';
4
4
  if (actor === 'system') {
@@ -10,25 +10,48 @@ function normalizeTaskEventActor(deps, actorRef, workspaceId) {
10
10
  actorProfile
11
11
  };
12
12
  }
13
- function normalizeTaskEventDetails(details) {
13
+ function normalizeEntityEventDetails(details) {
14
14
  if (!details || typeof details !== 'object' || Array.isArray(details))
15
15
  return {};
16
16
  return details;
17
17
  }
18
- function mapTaskEventRow(deps, row) {
18
+ function normalizeEntityType(value) {
19
+ return value === 'initiative' || value === 'workstream' ? value : 'task';
20
+ }
21
+ function mapEntityEventRow(deps, row) {
19
22
  const workspaceId = deps.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
20
23
  const actor = String(row.actor || 'system').trim() || 'system';
24
+ const entityType = normalizeEntityType(row.entity_type);
25
+ const entityId = String(row.entity_id || row.task_id || '').trim();
21
26
  let details = {};
22
27
  try {
23
- details = normalizeTaskEventDetails(row.details_json ? JSON.parse(String(row.details_json)) : {});
28
+ details = normalizeEntityEventDetails(row.details_json ? JSON.parse(String(row.details_json)) : {});
24
29
  }
25
30
  catch {
26
31
  details = {};
27
32
  }
33
+ if (entityType === 'task') {
34
+ return {
35
+ id: String(row.id || '').trim(),
36
+ taskId: entityId,
37
+ workspaceId,
38
+ entityType,
39
+ entityId,
40
+ action: String(row.action || '').trim(),
41
+ actor,
42
+ actorType: row.actor_type === 'ai' || row.actor_type === 'human' || row.actor_type === 'system'
43
+ ? row.actor_type
44
+ : 'system',
45
+ createdAt: String(row.created_at || ''),
46
+ details,
47
+ actorProfile: actor === 'system' ? null : deps.resolveWorkspaceActor(workspaceId, actor)
48
+ };
49
+ }
28
50
  return {
29
51
  id: String(row.id || '').trim(),
30
- taskId: String(row.task_id || '').trim(),
31
52
  workspaceId,
53
+ entityType,
54
+ entityId,
32
55
  action: String(row.action || '').trim(),
33
56
  actor,
34
57
  actorType: row.actor_type === 'ai' || row.actor_type === 'human' || row.actor_type === 'system'
@@ -39,16 +62,124 @@ function mapTaskEventRow(deps, row) {
39
62
  actorProfile: actor === 'system' ? null : deps.resolveWorkspaceActor(workspaceId, actor)
40
63
  };
41
64
  }
65
+ function mapEntityEventSyncRow(deps, row) {
66
+ const workspaceId = deps.normalizeWorkspaceId(typeof row.workspace_id === 'string' ? row.workspace_id : undefined);
67
+ const entityType = normalizeEntityType(row.entity_type);
68
+ const entityId = String(row.entity_id || row.task_id || '').trim();
69
+ let details = {};
70
+ try {
71
+ details = normalizeEntityEventDetails(row.details_json ? JSON.parse(String(row.details_json)) : {});
72
+ }
73
+ catch {
74
+ details = {};
75
+ }
76
+ if (entityType === 'task') {
77
+ return {
78
+ id: String(row.id || '').trim(),
79
+ taskId: entityId,
80
+ workspaceId,
81
+ entityType: 'task',
82
+ entityId,
83
+ action: String(row.action || '').trim(),
84
+ actor: String(row.actor || 'system').trim() || 'system',
85
+ actorType: row.actor_type === 'ai' || row.actor_type === 'human' || row.actor_type === 'system'
86
+ ? row.actor_type
87
+ : 'system',
88
+ createdAt: String(row.created_at || ''),
89
+ details,
90
+ // Sync only needs the stable event payload. Avoid actor hydration so
91
+ // large workspace snapshots don't blow up the Postgres worker buffer.
92
+ actorProfile: null
93
+ };
94
+ }
95
+ return {
96
+ id: String(row.id || '').trim(),
97
+ workspaceId,
98
+ entityType,
99
+ entityId,
100
+ action: String(row.action || '').trim(),
101
+ actor: String(row.actor || 'system').trim() || 'system',
102
+ actorType: row.actor_type === 'ai' || row.actor_type === 'human' || row.actor_type === 'system'
103
+ ? row.actor_type
104
+ : 'system',
105
+ createdAt: String(row.created_at || ''),
106
+ details,
107
+ // Sync only needs the stable event payload. Avoid actor hydration so
108
+ // large workspace snapshots don't blow up the Postgres worker buffer.
109
+ actorProfile: null
110
+ };
111
+ }
42
112
  export function listWorkspaceTaskEventsService(deps, workspaceId) {
43
113
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
44
114
  const rows = deps.db.prepare(`
45
- SELECT id, task_id, workspace_id, action, actor, actor_type, details_json, created_at
46
- FROM task_events
115
+ SELECT id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
116
+ FROM entity_events
117
+ WHERE tenant_id = ?
118
+ AND workspace_id = ?
119
+ AND entity_type = 'task'
120
+ ORDER BY created_at ASC, id ASC
121
+ `).all(deps.tenantId, normalizedWorkspaceId);
122
+ return rows.map((row) => mapEntityEventRow(deps, row));
123
+ }
124
+ export function listWorkspaceEntityEventsService(deps, workspaceId) {
125
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
126
+ const rows = deps.db.prepare(`
127
+ SELECT id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
128
+ FROM entity_events
129
+ WHERE tenant_id = ?
130
+ AND workspace_id = ?
131
+ ORDER BY created_at ASC, id ASC
132
+ `).all(deps.tenantId, normalizedWorkspaceId);
133
+ return rows.map((row) => mapEntityEventRow(deps, row));
134
+ }
135
+ export function listWorkspaceEntityEventsForSyncService(deps, workspaceId) {
136
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
137
+ const rows = deps.db.prepare(`
138
+ SELECT id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
139
+ FROM entity_events
47
140
  WHERE tenant_id = ?
48
141
  AND workspace_id = ?
49
142
  ORDER BY created_at ASC, id ASC
50
143
  `).all(deps.tenantId, normalizedWorkspaceId);
51
- return rows.map((row) => mapTaskEventRow(deps, row));
144
+ return rows.map((row) => mapEntityEventSyncRow(deps, row));
145
+ }
146
+ export function listWorkspaceTaskEventsForSyncService(deps, workspaceId) {
147
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
148
+ const rows = deps.db.prepare(`
149
+ SELECT id, entity_id AS task_id, workspace_id, action, actor, actor_type, details_json, created_at
150
+ FROM entity_events
151
+ WHERE tenant_id = ?
152
+ AND workspace_id = ?
153
+ AND entity_type = 'task'
154
+ ORDER BY created_at ASC, id ASC
155
+ `).all(deps.tenantId, normalizedWorkspaceId);
156
+ return rows.map((row) => mapEntityEventSyncRow(deps, { ...row, entity_type: 'task', entity_id: row.task_id }));
157
+ }
158
+ export function getTaskEventForSyncService(deps, input) {
159
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
160
+ const row = deps.db.prepare(`
161
+ SELECT id, entity_id AS task_id, workspace_id, action, actor, actor_type, details_json, created_at
162
+ FROM entity_events
163
+ WHERE tenant_id = ?
164
+ AND workspace_id = ?
165
+ AND entity_type = 'task'
166
+ AND entity_id = ?
167
+ AND id = ?
168
+ LIMIT 1
169
+ `).get(deps.tenantId, normalizedWorkspaceId, String(input.taskId || '').trim(), String(input.eventId || '').trim());
170
+ return row ? mapEntityEventSyncRow(deps, { ...row, entity_type: 'task', entity_id: row.task_id }) : null;
171
+ }
172
+ export function getEntityEventForSyncService(deps, input) {
173
+ const normalizedWorkspaceId = deps.normalizeWorkspaceId(input.workspaceId);
174
+ const row = deps.db.prepare(`
175
+ SELECT id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
176
+ FROM entity_events
177
+ WHERE tenant_id = ?
178
+ AND workspace_id = ?
179
+ AND id = ?
180
+ LIMIT 1
181
+ `).get(deps.tenantId, normalizedWorkspaceId, String(input.eventId || '').trim());
182
+ return row ? mapEntityEventSyncRow(deps, row) : null;
52
183
  }
53
184
  export function replaceTaskCommentsService(deps, taskId, workspaceId, comments) {
54
185
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
@@ -73,19 +204,24 @@ export function replaceTaskCommentsService(deps, taskId, workspaceId, comments)
73
204
  });
74
205
  }
75
206
  }
76
- export function listTaskEventsInternalService(deps, taskId, workspaceId) {
207
+ export function listEntityEventsInternalService(deps, entityType, entityId, workspaceId) {
77
208
  const rows = deps.db.prepare(`
78
- SELECT id, task_id, workspace_id, action, actor, actor_type, details_json, created_at
79
- FROM task_events
209
+ SELECT id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
210
+ FROM entity_events
80
211
  WHERE tenant_id = ?
81
212
  AND workspace_id = ?
82
- AND task_id = ?
213
+ AND entity_type = ?
214
+ AND entity_id = ?
83
215
  ORDER BY created_at ASC, id ASC
84
- `).all(deps.tenantId, workspaceId, taskId);
85
- return rows.map((row) => mapTaskEventRow(deps, row));
216
+ `).all(deps.tenantId, workspaceId, entityType, entityId);
217
+ return rows.map((row) => mapEntityEventRow(deps, row));
86
218
  }
87
- export function buildTaskActivityService(comments, taskEvents) {
88
- const visibleTaskEvents = taskEvents.filter((event) => event.action !== 'task-comment-added');
219
+ export function listTaskEventsInternalService(deps, taskId, workspaceId) {
220
+ return listEntityEventsInternalService(deps, 'task', taskId, workspaceId);
221
+ }
222
+ export function buildEntityActivityService(comments, entityEvents, options) {
223
+ const hiddenActions = new Set(options?.hiddenActions || []);
224
+ const visibleEvents = entityEvents.filter((event) => !hiddenActions.has(event.action));
89
225
  return [
90
226
  ...comments.map((comment) => ({
91
227
  id: `comment:${comment.id}`,
@@ -93,7 +229,7 @@ export function buildTaskActivityService(comments, taskEvents) {
93
229
  timestamp: comment.timestamp,
94
230
  comment
95
231
  })),
96
- ...visibleTaskEvents.map((event) => ({
232
+ ...visibleEvents.map((event) => ({
97
233
  id: `event:${event.id}`,
98
234
  type: 'event',
99
235
  timestamp: event.createdAt,
@@ -108,68 +244,113 @@ export function buildTaskActivityService(comments, taskEvents) {
108
244
  return String(left.id || '').localeCompare(String(right.id || ''));
109
245
  });
110
246
  }
111
- export function addTaskEventService(deps, input) {
247
+ export function buildTaskActivityService(comments, taskEvents) {
248
+ return buildEntityActivityService(comments, taskEvents, {
249
+ hiddenActions: ['task-comment-added']
250
+ });
251
+ }
252
+ export function addEntityEventService(deps, input) {
112
253
  const action = String(input.action || '').trim();
113
254
  if (!action)
114
255
  return;
115
- const taskId = String(input.taskId || '').trim();
116
- if (!taskId)
256
+ const entityId = String(input.entityId || '').trim();
257
+ if (!entityId)
117
258
  return;
259
+ const entityType = normalizeEntityType(input.entityType);
118
260
  const workspaceId = deps.normalizeWorkspaceId(input.workspaceId);
119
261
  const createdAt = String(input.createdAt || '').trim() || new Date().toISOString();
120
- const { actor, actorType } = normalizeTaskEventActor(deps, input.actor, workspaceId);
262
+ const { actor, actorType } = normalizeEntityEventActor(deps, input.actor, workspaceId);
121
263
  deps.db.prepare(`
122
- INSERT INTO task_events (
123
- id, tenant_id, workspace_id, task_id, action, actor, actor_type, details_json, created_at
124
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
125
- `).run(deps.generateEntityId('task-event'), deps.tenantId, workspaceId, taskId, action, actor, actorType, JSON.stringify(normalizeTaskEventDetails(input.details)), createdAt);
264
+ INSERT INTO entity_events (
265
+ id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
266
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
267
+ `).run(deps.generateEntityId('entity-event'), deps.tenantId, workspaceId, entityType, entityId, action, actor, actorType, JSON.stringify(normalizeEntityEventDetails(input.details)), createdAt);
126
268
  }
127
- export function replaceTaskEventsService(deps, taskId, workspaceId, taskEvents) {
269
+ export function addTaskEventService(deps, input) {
270
+ addEntityEventService(deps, {
271
+ entityType: 'task',
272
+ entityId: input.taskId,
273
+ workspaceId: input.workspaceId,
274
+ action: input.action,
275
+ actor: input.actor,
276
+ createdAt: input.createdAt,
277
+ details: input.details,
278
+ });
279
+ }
280
+ export function replaceEntityEventsService(deps, entityType, entityId, workspaceId, entityEvents) {
128
281
  const normalizedWorkspaceId = deps.normalizeWorkspaceId(workspaceId);
129
- deps.db.prepare('DELETE FROM task_events WHERE tenant_id = ? AND workspace_id = ? AND task_id = ?')
130
- .run(deps.tenantId, normalizedWorkspaceId, taskId);
131
- if (taskEvents.length === 0)
282
+ deps.db.prepare('DELETE FROM entity_events WHERE tenant_id = ? AND workspace_id = ? AND entity_type = ? AND entity_id = ?')
283
+ .run(deps.tenantId, normalizedWorkspaceId, entityType, entityId);
284
+ if (entityEvents.length === 0)
132
285
  return;
133
- const insertTaskEvent = deps.db.prepare(`
134
- INSERT INTO task_events (
135
- id, tenant_id, workspace_id, task_id, action, actor, actor_type, details_json, created_at
136
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
286
+ const insertEntityEvent = deps.db.prepare(`
287
+ INSERT INTO entity_events (
288
+ id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
289
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
137
290
  `);
138
- for (const event of taskEvents) {
139
- insertTaskEvent.run(event.id, deps.tenantId, normalizedWorkspaceId, taskId, event.action, event.actor, event.actorType, JSON.stringify(normalizeTaskEventDetails(event.details)), event.createdAt);
291
+ for (const event of entityEvents) {
292
+ insertEntityEvent.run(event.id, deps.tenantId, normalizedWorkspaceId, event.entityType, event.entityId, event.action, event.actor, event.actorType, JSON.stringify(normalizeEntityEventDetails(event.details)), event.createdAt);
140
293
  }
141
294
  }
142
- export function upsertTaskEventForSyncService(deps, event) {
295
+ export function replaceTaskEventsService(deps, taskId, workspaceId, taskEvents) {
296
+ replaceEntityEventsService(deps, 'task', taskId, workspaceId, taskEvents.map((event) => ({
297
+ ...event,
298
+ entityType: 'task',
299
+ entityId: String(event.entityId || event.taskId || taskId).trim(),
300
+ taskId: String(event.taskId || taskId).trim(),
301
+ })));
302
+ }
303
+ export function upsertEntityEventForSyncService(deps, event) {
143
304
  const workspaceId = deps.normalizeWorkspaceId(event.workspaceId);
305
+ const entityType = normalizeEntityType(event.entityType);
144
306
  const normalizedEvent = {
145
307
  id: String(event.id || '').trim(),
146
- taskId: String(event.taskId || '').trim(),
147
308
  workspaceId,
309
+ entityType,
310
+ entityId: String(event.entityId || (entityType === 'task' ? event.taskId : '') || '').trim(),
148
311
  action: String(event.action || '').trim(),
149
312
  actor: String(event.actor || 'system').trim() || 'system',
150
313
  actorType: event.actorType === 'ai' || event.actorType === 'human' || event.actorType === 'system'
151
314
  ? event.actorType
152
315
  : 'system',
153
316
  createdAt: String(event.createdAt || '').trim() || new Date().toISOString(),
154
- details: normalizeTaskEventDetails(event.details)
317
+ details: normalizeEntityEventDetails(event.details)
155
318
  };
156
319
  deps.db.prepare(`
157
- INSERT INTO task_events (
158
- id, tenant_id, workspace_id, task_id, action, actor, actor_type, details_json, created_at
159
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
320
+ INSERT INTO entity_events (
321
+ id, tenant_id, workspace_id, entity_type, entity_id, action, actor, actor_type, details_json, created_at
322
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
160
323
  ON CONFLICT (id) DO UPDATE SET
161
324
  workspace_id = excluded.workspace_id,
162
- task_id = excluded.task_id,
325
+ entity_type = excluded.entity_type,
326
+ entity_id = excluded.entity_id,
163
327
  action = excluded.action,
164
328
  actor = excluded.actor,
165
329
  actor_type = excluded.actor_type,
166
330
  details_json = excluded.details_json,
167
331
  created_at = excluded.created_at
168
- `).run(normalizedEvent.id, deps.tenantId, workspaceId, normalizedEvent.taskId, normalizedEvent.action, normalizedEvent.actor, normalizedEvent.actorType, JSON.stringify(normalizedEvent.details), normalizedEvent.createdAt);
332
+ `).run(normalizedEvent.id, deps.tenantId, workspaceId, normalizedEvent.entityType, normalizedEvent.entityId, normalizedEvent.action, normalizedEvent.actor, normalizedEvent.actorType, JSON.stringify(normalizedEvent.details), normalizedEvent.createdAt);
333
+ const actorProfile = normalizedEvent.actor === 'system'
334
+ ? null
335
+ : deps.resolveWorkspaceActor(workspaceId, normalizedEvent.actor);
336
+ if (normalizedEvent.entityType === 'task') {
337
+ return {
338
+ ...normalizedEvent,
339
+ entityType: 'task',
340
+ taskId: normalizedEvent.entityId,
341
+ actorProfile,
342
+ };
343
+ }
169
344
  return {
170
345
  ...normalizedEvent,
171
- actorProfile: normalizedEvent.actor === 'system'
172
- ? null
173
- : deps.resolveWorkspaceActor(workspaceId, normalizedEvent.actor)
346
+ entityType: normalizedEvent.entityType,
347
+ actorProfile,
174
348
  };
175
349
  }
350
+ export function upsertTaskEventForSyncService(deps, event) {
351
+ return upsertEntityEventForSyncService(deps, {
352
+ ...event,
353
+ entityType: 'task',
354
+ entityId: event.taskId,
355
+ });
356
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { TaskforceCore } from './Taskforce';
6
+ function makeCore() {
7
+ const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'taskforce-activity-sync-'));
8
+ const core = new TaskforceCore({
9
+ projectRoot,
10
+ storagePath: path.join(projectRoot, '.taskforce')
11
+ });
12
+ return {
13
+ core,
14
+ cleanup: () => {
15
+ core.close();
16
+ fs.rmSync(projectRoot, { recursive: true, force: true });
17
+ }
18
+ };
19
+ }
20
+ describe('TaskforceCore sync activity reads', () => {
21
+ it('lists workspace entity events for sync without hydrating actor profiles', () => {
22
+ const { core, cleanup } = makeCore();
23
+ try {
24
+ const task = core.createTask({ title: 'Sync task event seed', createdBy: 'user-1' }, {
25
+ workspaceId: 'workspace-a',
26
+ actorRef: 'user-1'
27
+ });
28
+ const initiative = core.createInitiative({ title: 'Initiative seed' }, 'workspace-a', {
29
+ actorRef: 'user-1'
30
+ });
31
+ core.recordTaskEvent({
32
+ taskId: task.id,
33
+ workspaceId: 'workspace-a',
34
+ action: 'task-updated',
35
+ actor: 'user-1',
36
+ createdAt: '2026-04-28T10:00:00.000Z',
37
+ details: { changes: { assignee: { from: 'user-1', to: 'ai-1' } } }
38
+ });
39
+ core.updateInitiative(initiative.id, { description: 'Updated initiative body' }, 'workspace-a', {
40
+ actorRef: 'user-1'
41
+ });
42
+ vi.spyOn(core, 'resolveWorkspaceActor').mockImplementation(() => {
43
+ throw new Error('sync snapshot should not hydrate actor profiles');
44
+ });
45
+ const events = core.listEntityEventsForSync({ workspaceId: 'workspace-a' });
46
+ expect(events.map((event) => event.action)).toEqual(expect.arrayContaining([
47
+ 'task-created',
48
+ 'task-updated',
49
+ 'initiative-created',
50
+ 'initiative-updated',
51
+ ]));
52
+ expect(events.every((event) => event.actorProfile === null)).toBe(true);
53
+ }
54
+ finally {
55
+ cleanup();
56
+ }
57
+ });
58
+ it('looks up one entity event for sync without hydrating actor profiles', () => {
59
+ const { core, cleanup } = makeCore();
60
+ try {
61
+ const workstream = core.createWorkstream({ title: 'Single sync event seed' }, 'workspace-a', {
62
+ actorRef: 'user-1'
63
+ });
64
+ core.updateWorkstream(workstream.id, {
65
+ description: 'Workstream moved forward'
66
+ }, 'workspace-a', {
67
+ actorRef: 'user-1'
68
+ });
69
+ const target = core.getWorkstream(workstream.id, 'workspace-a')?.entityEvents?.find((event) => event.action === 'workstream-updated');
70
+ expect(target).toBeTruthy();
71
+ vi.spyOn(core, 'resolveWorkspaceActor').mockImplementation(() => {
72
+ throw new Error('single sync lookup should not hydrate actor profiles');
73
+ });
74
+ const syncEvent = core.getEntityEventForSync({
75
+ workspaceId: 'workspace-a',
76
+ eventId: target.id,
77
+ });
78
+ expect(syncEvent).toEqual(expect.objectContaining({
79
+ id: target.id,
80
+ entityType: 'workstream',
81
+ entityId: workstream.id,
82
+ action: 'workstream-updated',
83
+ actorProfile: null,
84
+ }));
85
+ }
86
+ finally {
87
+ cleanup();
88
+ }
89
+ });
90
+ });
@@ -183,6 +183,102 @@ describe('TaskforceCore task audit timeline', () => {
183
183
  cleanup();
184
184
  }
185
185
  });
186
+ it('records initiative and workstream activity events and exposes them on hydrated planning entities', () => {
187
+ const { core, cleanup } = makeCore();
188
+ try {
189
+ core.createWorkspace({ workspaceId: 'workspace-a', name: 'Workspace A' });
190
+ const initiative = core.createInitiative({ title: 'Planning initiative' }, 'workspace-a', { actorRef: 'user' });
191
+ const nestedInitiative = core.createInitiative({ title: 'Nested initiative' }, 'workspace-a', { actorRef: 'user' });
192
+ const workstream = core.createWorkstream({
193
+ title: 'Planning workstream',
194
+ initiativeId: initiative.id,
195
+ }, 'workspace-a', { actorRef: 'user' });
196
+ const siblingWorkstream = core.createWorkstream({
197
+ title: 'Sibling workstream',
198
+ initiativeId: initiative.id,
199
+ }, 'workspace-a', { actorRef: 'user' });
200
+ core.updateInitiative(initiative.id, {
201
+ description: 'Refined initiative description',
202
+ attachments: [{ path: 'https://example.com/initiative-spec.md', caption: 'initiative spec' }],
203
+ }, 'workspace-a', { actorRef: 'user' });
204
+ core.archiveInitiative(initiative.id, 'workspace-a', { actorRef: 'user' });
205
+ core.unarchiveInitiative(initiative.id, 'workspace-a', { actorRef: 'user' });
206
+ core.updateWorkstream(workstream.id, {
207
+ initiativeId: nestedInitiative.id,
208
+ }, 'workspace-a', { actorRef: 'user' });
209
+ core.archiveWorkstream(workstream.id, 'workspace-a', { actorRef: 'user' });
210
+ core.unarchiveWorkstream(workstream.id, 'workspace-a', { actorRef: 'user' });
211
+ const linkedTask = core.createTask({
212
+ title: 'Linked task',
213
+ createdBy: 'user',
214
+ workstreamId: workstream.id,
215
+ }, { workspaceId: 'workspace-a', actorRef: 'user' });
216
+ core.updateTask(linkedTask.id, { workstreamId: siblingWorkstream.id }, 'workspace-a', { actorRef: 'user' });
217
+ core.updateTask(linkedTask.id, { workstreamId: null }, 'workspace-a', { actorRef: 'user' });
218
+ const hydratedInitiative = core.getInitiative(initiative.id, 'workspace-a');
219
+ const hydratedWorkstream = core.getWorkstream(workstream.id, 'workspace-a');
220
+ const hydratedSiblingWorkstream = core.getWorkstream(siblingWorkstream.id, 'workspace-a');
221
+ const initiativeActions = (hydratedInitiative?.activity || [])
222
+ .filter((item) => item.type === 'event')
223
+ .map((item) => item.event.action);
224
+ const workstreamRelationshipEvent = (hydratedWorkstream?.activity || [])
225
+ .filter((item) => item.type === 'event')
226
+ .map((item) => item.event)
227
+ .find((event) => event.action === 'workstream-relationship-changed');
228
+ const workstreamActions = (hydratedWorkstream?.activity || [])
229
+ .filter((item) => item.type === 'event')
230
+ .map((item) => item.event.action);
231
+ const siblingWorkstreamActions = (hydratedSiblingWorkstream?.activity || [])
232
+ .filter((item) => item.type === 'event')
233
+ .map((item) => item.event.action);
234
+ const workstreamTaskAddedEvent = (hydratedWorkstream?.activity || [])
235
+ .filter((item) => item.type === 'event')
236
+ .map((item) => item.event)
237
+ .find((event) => event.action === 'workstream-task-added');
238
+ const workstreamTaskRemovedEvent = (hydratedWorkstream?.activity || [])
239
+ .filter((item) => item.type === 'event')
240
+ .map((item) => item.event)
241
+ .find((event) => event.action === 'workstream-task-removed');
242
+ expect(initiativeActions).toEqual(expect.arrayContaining([
243
+ 'initiative-created',
244
+ 'initiative-updated',
245
+ 'initiative-archived',
246
+ 'initiative-unarchived',
247
+ ]));
248
+ expect(workstreamActions).toEqual(expect.arrayContaining([
249
+ 'workstream-created',
250
+ 'workstream-relationship-changed',
251
+ 'workstream-task-added',
252
+ 'workstream-task-removed',
253
+ 'workstream-archived',
254
+ 'workstream-unarchived',
255
+ ]));
256
+ expect(siblingWorkstreamActions).toEqual(expect.arrayContaining([
257
+ 'workstream-created',
258
+ 'workstream-task-added',
259
+ 'workstream-task-removed',
260
+ ]));
261
+ expect(workstreamRelationshipEvent?.details?.changes?.initiativeId).toEqual({
262
+ from: initiative.id,
263
+ to: nestedInitiative.id,
264
+ });
265
+ expect(workstreamTaskAddedEvent?.details?.taskId).toBe(linkedTask.id);
266
+ expect(workstreamTaskAddedEvent?.details?.taskTitle).toBe('Linked task');
267
+ expect(workstreamTaskAddedEvent?.details?.changes?.workstreamId).toEqual({
268
+ from: null,
269
+ to: workstream.id,
270
+ });
271
+ expect(workstreamTaskRemovedEvent?.details?.changes?.workstreamId).toEqual({
272
+ from: workstream.id,
273
+ to: siblingWorkstream.id,
274
+ });
275
+ expect(hydratedInitiative?.entityEvents?.every((event) => event.entityType === 'initiative')).toBe(true);
276
+ expect(hydratedWorkstream?.entityEvents?.every((event) => event.entityType === 'workstream')).toBe(true);
277
+ }
278
+ finally {
279
+ cleanup();
280
+ }
281
+ });
186
282
  it('repairs legacy comments tables so raw submitted authors can be stored', () => {
187
283
  const { core, cleanup } = makeCore();
188
284
  try {