@taskforcehq/taskforce 0.3.328 → 0.3.329

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 (264) hide show
  1. package/dist/Taskforce.module.css +233 -34
  2. package/dist/TaskforceCore.js +237 -230
  3. package/dist/annotatedAttachments/AnnotatedAttachmentSessionStore.js +1 -1
  4. package/dist/cli.js +34 -0
  5. package/dist/cliCloudTargetGuard.d.ts +5 -0
  6. package/dist/cliCloudTargetGuard.js +11 -0
  7. package/dist/components/features/AnnotatedAttachmentWorkspace.js +24 -8
  8. package/dist/components/features/TaskSettings.d.ts +12 -1
  9. package/dist/components/features/TaskSettings.js +147 -40
  10. package/dist/components/features/agentCapabilities/AgentCapabilitiesPrototype.d.ts +1 -1
  11. package/dist/components/features/planning/PlanningModule.js +2 -5
  12. package/dist/components/features/planning/PlanningTaskOrderList.d.ts +1 -1
  13. package/dist/components/features/planning/PlanningTaskOrderList.js +17 -6
  14. package/dist/components/features/planning/planningTaskArrangement.d.ts +1 -0
  15. package/dist/components/features/planning/planningTaskArrangement.js +5 -5
  16. package/dist/components/features/taskforceAgentEditorModel.js +3 -1
  17. package/dist/components/task/TaskCard.d.ts +5 -0
  18. package/dist/components/task/TaskCard.js +65 -10
  19. package/dist/components/task/TaskImageReviews.js +14 -5
  20. package/dist/components/task/TaskKanban.d.ts +6 -1
  21. package/dist/components/task/TaskKanban.js +152 -6
  22. package/dist/components/task/TaskList.d.ts +5 -0
  23. package/dist/components/task/TaskList.js +3 -3
  24. package/dist/components/ui/PasswordInput.d.ts +6 -0
  25. package/dist/components/ui/PasswordInput.js +11 -0
  26. package/dist/components/ui/TopNoticeLayer.js +1 -1
  27. package/dist/components/views/AppShellHeader.js +32 -11
  28. package/dist/components/views/PlanComparisonPage.js +12 -0
  29. package/dist/components/views/StandaloneLayout.js +7 -31
  30. package/dist/components/views/WidgetView.js +3 -3
  31. package/dist/components/views/panels/PlanningDrawer.js +5 -12
  32. package/dist/components/views/standalone/modals/AccountSettingsModal.js +26 -17
  33. package/dist/config/envSchema.js +12 -0
  34. package/dist/config/localServiceRuntimeContract.d.ts +13 -0
  35. package/dist/config/localServiceRuntimeContract.js +135 -0
  36. package/dist/core/AiProfileService.js +46 -8
  37. package/dist/core/DeletedTaskLifecycleService.d.ts +1 -0
  38. package/dist/core/DeletedTaskLifecycleService.js +37 -6
  39. package/dist/core/TaskAttachmentLinksDurableMutationService.d.ts +3 -2
  40. package/dist/core/TaskAttachmentLinksDurableMutationService.js +33 -9
  41. package/dist/core/Taskforce.d.ts +62 -2
  42. package/dist/core/Taskforce.js +635 -32
  43. package/dist/core/types.d.ts +6 -0
  44. package/dist/hooks/sync/useDurableLocalOutboxDrain.d.ts +1 -1
  45. package/dist/hooks/sync/useDurableTaskCreateMutation.js +1 -1
  46. package/dist/hooks/sync/useDurableTaskMetadataMutation.d.ts +1 -0
  47. package/dist/hooks/sync/useDurableTaskMetadataMutation.js +1 -0
  48. package/dist/hooks/sync/useDurableTaskStatusMutation.d.ts +1 -0
  49. package/dist/hooks/tasks/taskHttpMutation.js +1 -0
  50. package/dist/hooks/tasks/useCloudTaskSearch.d.ts +12 -0
  51. package/dist/hooks/tasks/useCloudTaskSearch.js +71 -0
  52. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.d.ts +16 -0
  53. package/dist/hooks/tasks/useTaskDescriptionImageDraftRecovery.js +197 -0
  54. package/dist/hooks/tasks/useTaskEditorNavigation.d.ts +16 -3
  55. package/dist/hooks/tasks/useTaskEditorNavigation.js +72 -13
  56. package/dist/hooks/tasks/useTaskFormActions.d.ts +6 -0
  57. package/dist/hooks/tasks/useTaskFormActions.js +65 -12
  58. package/dist/hooks/ui/useSettingsModel.d.ts +24 -2
  59. package/dist/hooks/ui/useSettingsModel.js +13 -2
  60. package/dist/hooks/ui/useSettingsPersistence.d.ts +12 -1
  61. package/dist/hooks/ui/useSettingsPersistence.js +74 -47
  62. package/dist/hooks/useFilterSort.d.ts +7 -3
  63. package/dist/hooks/useFilterSort.js +15 -5
  64. package/dist/hooks/useTaskData.d.ts +10 -1
  65. package/dist/hooks/useTaskData.js +216 -7
  66. package/dist/hooks/useTaskMutations.d.ts +10 -2
  67. package/dist/hooks/useTaskMutations.js +223 -43
  68. package/dist/hooks/useTaskforce.d.ts +37 -9
  69. package/dist/hooks/useTaskforce.js +349 -39
  70. package/dist/hooks/useUiNotice.d.ts +4 -0
  71. package/dist/hooks/useUiNotice.js +4 -0
  72. package/dist/mcp/adminWorkspaceRegistrar.js +3 -3
  73. package/dist/mcp/documentAssetRegistrar.js +44 -35
  74. package/dist/mcp/documentHelpers.d.ts +1 -0
  75. package/dist/mcp/documentHelpers.js +7 -3
  76. package/dist/mcp/runtime.js +172 -42
  77. package/dist/mcp/taskPlanningRegistrar.js +41 -38
  78. package/dist/mcp/toolCallCompatibility.js +38 -1
  79. package/dist/mcp/toolCatalog.d.ts +1 -0
  80. package/dist/migrations/taskSchemaMigrations.js +12 -0
  81. package/dist/runtime/appGateDefinitions.d.ts +30 -6
  82. package/dist/runtime/appGateDefinitions.js +6 -6
  83. package/dist/runtime/appGates.d.ts +12 -13
  84. package/dist/runtime/appGates.js +29 -13
  85. package/dist/runtime/nodeAppGates.d.ts +8 -0
  86. package/dist/runtime/nodeAppGates.js +27 -0
  87. package/dist/server/cloudCollectionRead.d.ts +1 -1
  88. package/dist/server/cloudCollectionRead.js +2 -2
  89. package/dist/server/index.d.ts +3 -0
  90. package/dist/server/index.js +15 -3
  91. package/dist/server/localServiceLease.d.ts +3 -0
  92. package/dist/server/localServiceLease.js +9 -1
  93. package/dist/server/localWorkspaceSyncServerRuntime.js +10 -3
  94. package/dist/server/rateLimit.d.ts +1 -0
  95. package/dist/server/rateLimit.js +4 -0
  96. package/dist/server/realtimeSyncWs.js +36 -12
  97. package/dist/server/routes/admin.js +1 -0
  98. package/dist/server/routes/agentRoles.js +3 -2
  99. package/dist/server/routes/agentSkills.js +3 -2
  100. package/dist/server/routes/agents.js +118 -16
  101. package/dist/server/routes/annotatedAttachments.js +3 -2
  102. package/dist/server/routes/auth.js +3 -1
  103. package/dist/server/routes/durableTaskHttpRouters.d.ts +9 -2
  104. package/dist/server/routes/localService.js +2 -0
  105. package/dist/server/routes/primitives.d.ts +2 -0
  106. package/dist/server/routes/syncAuxRoutes.js +5 -2
  107. package/dist/server/routes/syncPullApplyRoutes.js +23 -4
  108. package/dist/server/routes/syncV3FeedRoutes.js +18 -16
  109. package/dist/server/routes/syncV3LocalCaptureRoutes.js +28 -2
  110. package/dist/server/routes/syncV3LocalFeedApplyRoutes.d.ts +1 -0
  111. package/dist/server/routes/syncV3LocalFeedApplyRoutes.js +26 -0
  112. package/dist/server/routes/syncV3OperationRoutes.js +10 -0
  113. package/dist/server/routes/tasks.js +103 -11
  114. package/dist/server/routes/workspaces.js +43 -1
  115. package/dist/server/workspaceDeletionAdmission.d.ts +23 -0
  116. package/dist/server/workspaceDeletionAdmission.js +50 -0
  117. package/dist/service/localServiceCli.js +16 -0
  118. package/dist/service/localServiceClientLifecycle.js +36 -10
  119. package/dist/services/codexAppServerModelGateway.d.ts +112 -0
  120. package/dist/services/codexAppServerModelGateway.js +438 -0
  121. package/dist/services/taskforceAgentMcpBridge.d.ts +7 -1
  122. package/dist/services/taskforceAgentMcpBridge.js +23 -3
  123. package/dist/shared/taskforceAgentDefinition.js +7 -1
  124. package/dist/shared/taskforceAgentModels.d.ts +1 -0
  125. package/dist/shared/taskforceAgentModels.js +30 -1
  126. package/dist/storage/postgresAdapter.d.ts +20 -3
  127. package/dist/storage/postgresAdapter.js +106 -14
  128. package/dist/storage/postgresWorker.js +13 -4
  129. package/dist/storage/postgresWorkerProtocol.d.ts +29 -0
  130. package/dist/storage/postgresWorkerProtocol.js +30 -0
  131. package/dist/sync/cloudSyncApi.d.ts +4 -1
  132. package/dist/sync/cloudSyncApi.js +7 -2
  133. package/dist/sync/contentSyncState.d.ts +1 -1
  134. package/dist/sync/coordinator/localSyncExecutionPermitStore.d.ts +1 -0
  135. package/dist/sync/coordinator/localSyncExecutionPermitStore.js +7 -1
  136. package/dist/sync/coordinator/localWorkspaceSyncRunner.js +6 -2
  137. package/dist/sync/engine/workspaceSyncEngineBrowserGatewayRemoteServices.js +20 -2
  138. package/dist/sync/engine/workspaceSyncEngineLocalServices.d.ts +4 -0
  139. package/dist/sync/engine/workspaceSyncEngineLocalServices.js +11 -0
  140. package/dist/sync/engine/workspaceSyncEngineRemoteServices.d.ts +11 -0
  141. package/dist/sync/engine/workspaceSyncEngineRemoteServices.js +2 -1
  142. package/dist/sync/engine/workspaceSyncEngineServerRemoteServices.js +31 -4
  143. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.d.ts +3 -1
  144. package/dist/sync/engine/workspaceSyncEngineServerRunnerOperations.js +25 -8
  145. package/dist/sync/engine/workspaceSyncEngineTransfers.d.ts +1 -1
  146. package/dist/sync/engine/workspaceSyncEngineTransfers.js +8 -3
  147. package/dist/sync/syncService.d.ts +2 -0
  148. package/dist/sync/syncService.js +4 -1
  149. package/dist/sync/v3/combinedFeedCapability.js +4 -0
  150. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.d.ts +2 -0
  151. package/dist/sync/v3/durableTaskAttachmentLinksMutationRouter.js +11 -1
  152. package/dist/sync/v3/durableTaskChecklistMutationRouter.d.ts +2 -0
  153. package/dist/sync/v3/durableTaskChecklistMutationRouter.js +4 -0
  154. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.d.ts +1 -0
  155. package/dist/sync/v3/durableTaskHttpBulkMutationRouter.js +30 -8
  156. package/dist/sync/v3/durableTaskHttpRootMutationRouter.d.ts +5 -3
  157. package/dist/sync/v3/durableTaskHttpRootMutationRouter.js +27 -2
  158. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.d.ts +2 -0
  159. package/dist/sync/v3/localTaskAttachmentLinksOutboxService.js +47 -7
  160. package/dist/sync/v3/localTaskCreateOutboxService.js +15 -6
  161. package/dist/sync/v3/localTaskMetadataClient.d.ts +1 -0
  162. package/dist/sync/v3/localTaskMetadataClient.js +1 -0
  163. package/dist/sync/v3/localTaskMetadataOutboxHandler.js +5 -1
  164. package/dist/sync/v3/localTaskMetadataOutboxService.d.ts +2 -0
  165. package/dist/sync/v3/localTaskMetadataOutboxService.js +7 -0
  166. package/dist/sync/v3/localTaskRestoreOutboxService.js +14 -6
  167. package/dist/sync/v3/localTaskStatusClient.d.ts +1 -0
  168. package/dist/sync/v3/localTaskStatusClient.js +6 -1
  169. package/dist/sync/v3/localTaskStatusOutboxService.d.ts +2 -0
  170. package/dist/sync/v3/localTaskStatusOutboxService.js +54 -13
  171. package/dist/sync/v3/syncDurabilityStore.d.ts +7 -0
  172. package/dist/sync/v3/syncDurabilityStore.js +31 -11
  173. package/dist/sync/v3/taskAttachmentLinksMutationService.d.ts +6 -4
  174. package/dist/sync/v3/taskAttachmentLinksMutationService.js +19 -10
  175. package/dist/sync/v3/taskChecklistMutationService.d.ts +4 -2
  176. package/dist/sync/v3/taskChecklistMutationService.js +4 -0
  177. package/dist/sync/v3/taskCreateMutationService.d.ts +2 -2
  178. package/dist/sync/v3/taskCreateMutationService.js +7 -4
  179. package/dist/sync/v3/taskMetadataMutationService.d.ts +3 -0
  180. package/dist/sync/v3/taskMetadataMutationService.js +4 -0
  181. package/dist/sync/v3/taskRestoreMutationService.d.ts +2 -2
  182. package/dist/sync/v3/taskRestoreMutationService.js +7 -4
  183. package/dist/sync/v3/taskStatusMutationService.d.ts +2 -0
  184. package/dist/sync/v3/taskStatusMutationService.js +4 -0
  185. package/dist/sync/v3/workflowAssignmentSync.d.ts +1 -0
  186. package/dist/sync/v3/workflowAssignmentSync.js +3 -0
  187. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.d.ts +1 -1
  188. package/dist/sync/v3/workspaceSyncV2ProjectionExclusions.js +13 -3
  189. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.d.ts +11 -0
  190. package/dist/sync/v3/workspaceSyncV3AttachmentApplyObligationStore.js +27 -10
  191. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.d.ts +2 -1
  192. package/dist/sync/v3/workspaceSyncV3AttachmentAssetHydration.js +52 -10
  193. package/dist/sync/v3/workspaceSyncV3CloudOperationHandler.js +9 -1
  194. package/dist/sync/v3/workspaceSyncV3CombinedFeedOrchestrator.js +8 -3
  195. package/dist/sync/v3/workspaceSyncV3CombinedLocalApply.js +51 -10
  196. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.d.ts +7 -0
  197. package/dist/sync/v3/workspaceSyncV3CombinedRepairBaseline.js +30 -18
  198. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.d.ts +9 -4
  199. package/dist/sync/v3/workspaceSyncV3CombinedRepairMaterialization.js +98 -47
  200. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.d.ts +10 -0
  201. package/dist/sync/v3/workspaceSyncV3CombinedRepairSnapshot.js +58 -21
  202. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.d.ts +3 -1
  203. package/dist/sync/v3/workspaceSyncV3CombinedWireContract.js +18 -0
  204. package/dist/sync/v3/workspaceSyncV3CoverageRegistry.js +6 -0
  205. package/dist/sync/v3/workspaceSyncV3FeedProfile.d.ts +1 -1
  206. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.d.ts +1 -0
  207. package/dist/sync/v3/workspaceSyncV3LegacyBoundaryAttestation.js +23 -10
  208. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.d.ts +3 -0
  209. package/dist/sync/v3/workspaceSyncV3NestedTaskApply.js +64 -13
  210. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.d.ts +9 -0
  211. package/dist/sync/v3/workspaceSyncV3NestedTaskCodecs.js +26 -0
  212. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.d.ts +15 -5
  213. package/dist/sync/v3/workspaceSyncV3NestedTaskProjection.js +50 -14
  214. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.d.ts +2 -1
  215. package/dist/sync/v3/workspaceSyncV3NestedTaskRepairSnapshot.js +11 -3
  216. package/dist/sync/v3/workspaceSyncV3OperationProtocol.d.ts +2 -0
  217. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.d.ts +2 -0
  218. package/dist/sync/v3/workspaceSyncV3OperatorDiagnostics.js +19 -7
  219. package/dist/sync/v3/workspaceSyncV3TaskCommentAudit.js +9 -2
  220. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.d.ts +7 -0
  221. package/dist/sync/v3/workspaceSyncV3TaskCoverCoverage.js +37 -0
  222. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.d.ts +6 -0
  223. package/dist/sync/v3/workspaceSyncV3TaskCoverWireContract.js +9 -0
  224. package/dist/sync/workspacePullFeed.js +2 -1
  225. package/dist/sync/workspaceRepair.d.ts +3 -0
  226. package/dist/sync/workspaceRepair.js +67 -6
  227. package/dist/types.d.ts +47 -1
  228. package/dist/types.js +6 -0
  229. package/dist/ui/assets/{AiIdentityRosterCard-MZs7lVED.js → AiIdentityRosterCard-BZM4aQnM.js} +1 -1
  230. package/dist/ui/assets/AiProfilesModule-BbAEWtHH.js +1 -0
  231. package/dist/ui/assets/AnnotatedAttachmentWorkspace-B68Wg-RV.js +3 -0
  232. package/dist/ui/assets/AnnotatedAttachmentWorkspace-TzYu7pkG.css +1 -0
  233. package/dist/ui/assets/{AssetTraySortControl-Kf1pelSy.js → AssetTraySortControl-C8Rztyal.js} +1 -1
  234. package/dist/ui/assets/{ContextAttachmentManager-B8boOs6Q.js → ContextAttachmentManager-Cj3c7X9y.js} +2 -2
  235. package/dist/ui/assets/{DocumentWorkspace-DXH4XUsB.js → DocumentWorkspace-B5uvzems.js} +1 -1
  236. package/dist/ui/assets/{EntityActivityTimeline-BtSTq0ON.js → EntityActivityTimeline-BVFvCB5R.js} +1 -1
  237. package/dist/ui/assets/PlanningModule-B5DWQOGz.js +1 -0
  238. package/dist/ui/assets/PlansPage-AY4cGWco.js +1 -0
  239. package/dist/ui/assets/TaskContextUpload-DZxTVesW.js +1 -0
  240. package/dist/ui/assets/TaskSettings-BeWAuyZj.js +12 -0
  241. package/dist/ui/assets/{TaskforceAgentsModule-BazdUwxZ.js → TaskforceAgentsModule-BifCYtjR.js} +6 -6
  242. package/dist/ui/assets/{WorkflowManagerModule-DWFspC-o.js → WorkflowManagerModule-DEr5di_r.js} +1 -1
  243. package/dist/ui/assets/index-I5zhdtgt.js +7 -0
  244. package/dist/ui/assets/index-VRFsx3wz.css +1 -0
  245. package/dist/ui/assets/{vendor-icons-BkFLXavV.js → vendor-icons-D9Lpw-j4.js} +1 -1
  246. package/dist/ui/index.html +3 -3
  247. package/dist/utils/taskActivity.d.ts +3 -2
  248. package/dist/utils/taskActivity.js +19 -2
  249. package/dist/utils/taskEditorReconciliation.d.ts +40 -0
  250. package/dist/utils/taskEditorReconciliation.js +217 -0
  251. package/dist/utils/taskNormalization.d.ts +1 -0
  252. package/dist/utils/taskNormalization.js +39 -0
  253. package/dist/utils/taskSearch.js +2 -2
  254. package/dist/utils/uiNotice.d.ts +4 -0
  255. package/package.json +16 -4
  256. package/dist/ui/assets/AiProfilesModule-C3i0LOxe.js +0 -1
  257. package/dist/ui/assets/AnnotatedAttachmentWorkspace-CBRZ8rca.css +0 -1
  258. package/dist/ui/assets/AnnotatedAttachmentWorkspace-DVZk5Tln.js +0 -3
  259. package/dist/ui/assets/PlanningModule-BPVyEa-f.js +0 -1
  260. package/dist/ui/assets/PlansPage-ChqgxALc.js +0 -1
  261. package/dist/ui/assets/TaskContextUpload-CB3ol0sN.js +0 -1
  262. package/dist/ui/assets/TaskSettings-g4ECKNOz.js +0 -12
  263. package/dist/ui/assets/index-BUplSsv_.js +0 -7
  264. package/dist/ui/assets/index-gz2EXuoK.css +0 -1
@@ -61,6 +61,7 @@ import { getImageReferenceLabel } from '../utils/imageReferences.js';
61
61
  import { getCanonicalDocumentName } from '../utils/documentNames.js';
62
62
  import { isPathInsideRoot, resolveStorageKeyPathInsideRoot } from '../utils/pathSafety.js';
63
63
  import { DEFAULT_TASKFORCE_THEME } from '../utils/theme.js';
64
+ import { normalizeTaskSearchQuery } from '../utils/taskSearch.js';
64
65
  import { ensureAssigneeTaskSchema, ensureAnnotatedAttachmentSessionsSchema, ensureAuthIdentitySchema, backfillPlanningAttachmentAssetIdentities, ensureBillingSchema, ensureDocumentReviewSessionsSchema, ensureDocumentReferenceSchema, ensureEntitlementsSchema, ensurePlanningReferenceSchema, ensureScheduleTaskSchema, ensureWorkstreamTaskOrderSchema, ensureTaskAssetsSchema, ensureTaskApproachRemoved, ensureAgentRolesSchema, ensureTaskforceAgentsSchema, ensureTaskforceAgentSkillsSchema, ensureTaskReferenceSchema, ensureTenantSchema, ensureWorkflowSchema, ensureWorkspaceAssetsSchema, ensureWorkspaceSchema, ensureLocalSyncCoordinatorSchema, ensureWorkspaceSyncDurabilitySchema } from '../migrations/taskSchemaMigrations.js';
65
66
  import { repairTaskUpdatedAtOnlySyncDrift } from '../migrations/taskUpdatedAtSyncDriftMigration.js';
66
67
  export class TaskforceRuleError extends Error {
@@ -3158,7 +3159,17 @@ export class TaskforceCore {
3158
3159
  return ['manualComplexityEnabled'];
3159
3160
  }
3160
3161
  getUserBackedProjectConfigKeys() {
3161
- return ['theme', 'jsonBackupEnabled', 'checklistDropdownEnabled', 'showTaskCardStatusLabel'];
3162
+ return [
3163
+ 'theme',
3164
+ 'jsonBackupEnabled',
3165
+ 'checklistDropdownEnabled',
3166
+ 'showTaskCardStatusLabel',
3167
+ 'showTaskCardDescription',
3168
+ 'showTaskCardAttachments',
3169
+ 'showTaskCardChecklistProgress',
3170
+ 'showTaskCardLatestActivity',
3171
+ 'tintTaskCardActivityBackground',
3172
+ ];
3162
3173
  }
3163
3174
  isWorkspaceBackedProjectConfigKey(key) {
3164
3175
  return this.getWorkspaceBackedProjectConfigKeys().includes(key);
@@ -3201,6 +3212,21 @@ export class TaskforceCore {
3201
3212
  if (typeof source.showTaskCardStatusLabel === 'boolean') {
3202
3213
  next.showTaskCardStatusLabel = source.showTaskCardStatusLabel;
3203
3214
  }
3215
+ if (typeof source.showTaskCardDescription === 'boolean') {
3216
+ next.showTaskCardDescription = source.showTaskCardDescription;
3217
+ }
3218
+ if (typeof source.showTaskCardAttachments === 'boolean') {
3219
+ next.showTaskCardAttachments = source.showTaskCardAttachments;
3220
+ }
3221
+ if (typeof source.showTaskCardChecklistProgress === 'boolean') {
3222
+ next.showTaskCardChecklistProgress = source.showTaskCardChecklistProgress;
3223
+ }
3224
+ if (typeof source.showTaskCardLatestActivity === 'boolean') {
3225
+ next.showTaskCardLatestActivity = source.showTaskCardLatestActivity;
3226
+ }
3227
+ if (typeof source.tintTaskCardActivityBackground === 'boolean') {
3228
+ next.tintTaskCardActivityBackground = source.tintTaskCardActivityBackground;
3229
+ }
3204
3230
  return next;
3205
3231
  }
3206
3232
  stripPersistedProjectConfigKeys(config) {
@@ -4019,7 +4045,7 @@ export class TaskforceCore {
4019
4045
  AND identity.entity_type = 'initiative'
4020
4046
  AND identity.entity_id = i.id
4021
4047
  WHERE i.tenant_id = ? AND i.workspace_id = ?
4022
- ORDER BY COALESCE(i.sort_order, 2147483647), i.created_at ASC
4048
+ ORDER BY COALESCE(i.sort_order, 2147483647), i.created_at ASC, i.id ASC
4023
4049
  `).all(this.tenantId, normalizedWorkspaceId);
4024
4050
  return rows.map((row) => this.rowToInitiative(row));
4025
4051
  }
@@ -4326,7 +4352,7 @@ export class TaskforceCore {
4326
4352
  AND identity.entity_type = 'workstream'
4327
4353
  AND identity.entity_id = w.id
4328
4354
  WHERE w.tenant_id = ? AND w.workspace_id = ?
4329
- ORDER BY COALESCE(w.sort_order, 2147483647), w.created_at ASC
4355
+ ORDER BY COALESCE(w.sort_order, 2147483647), w.created_at ASC, w.id ASC
4330
4356
  `).all(this.tenantId, normalizedWorkspaceId);
4331
4357
  return rows.map((row) => this.rowToWorkstream(row));
4332
4358
  }
@@ -5493,6 +5519,7 @@ export class TaskforceCore {
5493
5519
  generateEntityId: (prefix) => this.generateEntityId(prefix),
5494
5520
  normalizeWorkspaceId: (workspaceId) => this.normalizeWorkspaceId(workspaceId),
5495
5521
  resolveWorkspaceActor: (workspaceId, actorRef) => this.resolveWorkspaceActor(workspaceId, actorRef),
5522
+ resolveWorkspaceActors: (workspaceId, actorRefs) => this.resolveWorkspaceActors(workspaceId, actorRefs),
5496
5523
  sanitizeOrderInDay: (value) => this.sanitizeOrderInDay(value),
5497
5524
  sanitizeDateOnly: (value) => this.sanitizeDateOnly(value),
5498
5525
  normalizeTaskEventActor: (actorRef, workspaceId) => this.normalizeTaskEventActor(actorRef, workspaceId),
@@ -5572,6 +5599,489 @@ export class TaskforceCore {
5572
5599
  config: this.readConfig()
5573
5600
  };
5574
5601
  }
5602
+ listTaskSummaries(workspaceId, archived) {
5603
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5604
+ const rows = this.db.prepare(`
5605
+ WITH ranked_events AS (
5606
+ SELECT e.entity_id,
5607
+ e.id,
5608
+ e.action,
5609
+ e.actor,
5610
+ e.actor_type,
5611
+ e.details_json,
5612
+ e.created_at,
5613
+ ROW_NUMBER() OVER (
5614
+ PARTITION BY e.entity_id
5615
+ ORDER BY e.created_at DESC, e.id DESC
5616
+ ) AS event_rank
5617
+ FROM entity_events e
5618
+ JOIN tasks scoped_task
5619
+ ON scoped_task.tenant_id = e.tenant_id
5620
+ AND scoped_task.workspace_id = e.workspace_id
5621
+ AND scoped_task.id = e.entity_id
5622
+ WHERE e.tenant_id = ? AND e.workspace_id = ?
5623
+ AND e.entity_type = 'task'
5624
+ AND scoped_task.is_archived = ?
5625
+ -- Comment activity is represented by the comment itself; the
5626
+ -- paired audit event is intentionally hidden by taskActivity.
5627
+ AND e.action <> 'task-comment-added'
5628
+ -- Durable text-only autosaves retain an empty task-updated event
5629
+ -- for stable replay identity, but full task activity hides it.
5630
+ -- Match that visibility rule before ranking so an internal event
5631
+ -- cannot displace the latest meaningful card activity. REPLACE is
5632
+ -- intentionally used instead of dialect-specific JSON operators.
5633
+ AND NOT (
5634
+ e.action = 'task-updated'
5635
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"saveSource":"autosave"%'
5636
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"changes":{}%'
5637
+ )
5638
+ )
5639
+ SELECT t.id, t.reference_number, t.local_reference_number, t.title, t.status, t.priority,
5640
+ t.complexity, t.type, t.category, t.created_at, t.updated_at, t.completed_at,
5641
+ t.canceled_reason, t.is_archived, t.scheduled_date, t.due_date,
5642
+ t.scheduled_week_key, t.order_in_day, t.workstream_order, t.workstream_id,
5643
+ t.card_cover_asset_id, t.taxonomies, t.created_by, t.assignee,
5644
+ event.id AS latest_event_id,
5645
+ event.action AS latest_event_action,
5646
+ event.actor AS latest_event_actor,
5647
+ event.actor_type AS latest_event_actor_type,
5648
+ event.details_json AS latest_event_details_json,
5649
+ event.created_at AS latest_event_at
5650
+ FROM tasks t
5651
+ LEFT JOIN ranked_events event
5652
+ ON event.entity_id = t.id AND event.event_rank = 1
5653
+ WHERE t.tenant_id = ? AND t.workspace_id = ? AND t.is_archived = ?
5654
+ ORDER BY ${archived ? 't.completed_at DESC, t.created_at DESC' : 't.created_at DESC'}
5655
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0, this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5656
+ const comments = this.db.prepare(`
5657
+ WITH ranked_comments AS (
5658
+ SELECT c.task_id,
5659
+ c.id,
5660
+ c.text,
5661
+ c.author,
5662
+ c.timestamp,
5663
+ COUNT(*) OVER (PARTITION BY c.task_id) AS count,
5664
+ MAX(c.timestamp) OVER (PARTITION BY c.task_id) AS revision,
5665
+ ROW_NUMBER() OVER (
5666
+ PARTITION BY c.task_id
5667
+ ORDER BY c.timestamp DESC, c.id DESC
5668
+ ) AS comment_rank
5669
+ FROM comments c
5670
+ JOIN tasks t
5671
+ ON t.tenant_id = c.tenant_id
5672
+ AND t.workspace_id = c.workspace_id
5673
+ AND t.id = c.task_id
5674
+ WHERE c.tenant_id = ? AND c.workspace_id = ? AND t.is_archived = ?
5675
+ )
5676
+ SELECT task_id, id, text, author, timestamp, count, revision
5677
+ FROM ranked_comments
5678
+ WHERE comment_rank = 1
5679
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5680
+ const checklist = this.db.prepare(`
5681
+ SELECT c.task_id, COUNT(*) AS count,
5682
+ SUM(CASE WHEN c.is_completed = 1 THEN 1 ELSE 0 END) AS completed_count,
5683
+ MAX(COALESCE(c.updated_at, c.created_at)) AS revision
5684
+ FROM checklist_items c
5685
+ JOIN tasks t ON t.tenant_id = c.tenant_id AND t.workspace_id = c.workspace_id AND t.id = c.task_id
5686
+ WHERE c.tenant_id = ? AND c.workspace_id = ? AND t.is_archived = ?
5687
+ GROUP BY c.task_id
5688
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5689
+ const attachments = this.db.prepare(`
5690
+ SELECT l.task_id, COUNT(a.asset_id) AS count,
5691
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN l.asset_id ELSE NULL END) AS cover_asset_id,
5692
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.storage_provider ELSE NULL END) AS cover_storage_provider,
5693
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.storage_key ELSE NULL END) AS cover_storage_key,
5694
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN a.original_filename ELSE NULL END) AS cover_original_filename,
5695
+ MAX(CASE WHEN l.asset_id = t.card_cover_asset_id THEN COALESCE(l.display_name, a.logical_name) ELSE NULL END) AS cover_display_name,
5696
+ MAX(CASE
5697
+ WHEN COALESCE(a.updated_at, '') > COALESCE(l.updated_at, '') THEN a.updated_at
5698
+ ELSE l.updated_at
5699
+ END) AS revision
5700
+ FROM workspace_asset_links l
5701
+ JOIN tasks t ON t.tenant_id = l.tenant_id AND t.workspace_id = l.workspace_id AND t.id = l.task_id
5702
+ JOIN workspace_assets a ON a.tenant_id = l.tenant_id AND a.workspace_id = l.workspace_id AND a.asset_id = l.asset_id AND a.deleted_at IS NULL
5703
+ WHERE l.tenant_id = ? AND l.workspace_id = ? AND t.is_archived = ? AND l.target_type = 'task' AND l.deleted_at IS NULL
5704
+ GROUP BY l.task_id
5705
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5706
+ const relationships = this.db.prepare(`
5707
+ SELECT task_id, COUNT(*) AS count, MAX(updated_at) AS revision
5708
+ FROM (
5709
+ SELECT r.source_task_id AS task_id, r.updated_at
5710
+ FROM task_relationships r
5711
+ JOIN tasks t ON t.tenant_id = r.tenant_id AND t.workspace_id = r.workspace_id AND t.id = r.source_task_id
5712
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL AND t.is_archived = ?
5713
+ UNION ALL
5714
+ SELECT r.target_task_id AS task_id, r.updated_at
5715
+ FROM task_relationships r
5716
+ JOIN tasks t ON t.tenant_id = r.tenant_id AND t.workspace_id = r.workspace_id AND t.id = r.target_task_id
5717
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5718
+ AND r.target_task_id <> r.source_task_id AND t.is_archived = ?
5719
+ ) scoped_relationships
5720
+ GROUP BY task_id
5721
+ `).all(this.tenantId, normalizedWorkspaceId, archived ? 1 : 0, this.tenantId, normalizedWorkspaceId, archived ? 1 : 0);
5722
+ const countByTask = (entries) => new Map(entries.map((entry) => [String(entry.task_id || ''), Number(entry.count || 0)]));
5723
+ const commentCountByTask = countByTask(comments);
5724
+ const attachmentCountByTask = countByTask(attachments);
5725
+ const commentRevisionByTask = new Map(comments.map((entry) => [String(entry.task_id || ''), String(entry.revision || '')]));
5726
+ const attachmentRevisionByTask = new Map(attachments.map((entry) => [String(entry.task_id || ''), String(entry.revision || '')]));
5727
+ const coverAttachmentByTask = new Map(attachments.flatMap((entry) => {
5728
+ const taskId = String(entry.task_id || '');
5729
+ const assetId = String(entry.cover_asset_id || '').trim();
5730
+ const storageKey = String(entry.cover_storage_key || '').trim();
5731
+ const storageProvider = String(entry.cover_storage_provider || 'local').trim();
5732
+ if (!taskId || !assetId || !storageKey)
5733
+ return [];
5734
+ const isExternalHttpReference = storageProvider === 'external' && /^https?:\/\//i.test(storageKey);
5735
+ return [[taskId, {
5736
+ assetId,
5737
+ path: isExternalHttpReference
5738
+ ? storageKey
5739
+ : storageProvider === 'external'
5740
+ ? `/api/taskforce/context-link?path=${encodeURIComponent(storageKey)}`
5741
+ : `/api/taskforce/context/${encodeURIComponent(storageKey)}`,
5742
+ ...(isExternalHttpReference ? {} : { fsPath: storageKey }),
5743
+ displayName: entry.cover_display_name ? String(entry.cover_display_name) : undefined,
5744
+ originalFilename: entry.cover_original_filename ? String(entry.cover_original_filename) : undefined,
5745
+ linkRole: 'image',
5746
+ taskId,
5747
+ }]];
5748
+ }));
5749
+ const checklistByTask = new Map(checklist.map((entry) => [String(entry.task_id || ''), {
5750
+ total: Number(entry.count || 0),
5751
+ completed: Number(entry.completed_count || 0),
5752
+ revision: String(entry.revision || ''),
5753
+ }]));
5754
+ const relationshipCountByTask = countByTask(relationships);
5755
+ const relationshipRevisionByTask = new Map(relationships.map((entry) => [
5756
+ String(entry.task_id || ''),
5757
+ String(entry.revision || ''),
5758
+ ]));
5759
+ const latestActivityByTask = new Map();
5760
+ const latestActivityRevisionByTask = new Map();
5761
+ const considerLatestActivity = (taskId, item) => {
5762
+ const existing = latestActivityByTask.get(taskId);
5763
+ const nextTimestamp = Date.parse(String(item.timestamp || ''));
5764
+ const existingTimestamp = existing ? Date.parse(String(existing.timestamp || '')) : Number.NaN;
5765
+ const isNewer = !existing
5766
+ || (Number.isFinite(nextTimestamp) && !Number.isFinite(existingTimestamp))
5767
+ || (Number.isFinite(nextTimestamp) && Number.isFinite(existingTimestamp) && nextTimestamp > existingTimestamp)
5768
+ || (nextTimestamp === existingTimestamp && item.id.localeCompare(existing.id) > 0);
5769
+ if (!isNewer)
5770
+ return;
5771
+ latestActivityByTask.set(taskId, item);
5772
+ latestActivityRevisionByTask.set(taskId, `${item.timestamp}:${item.id}`);
5773
+ };
5774
+ for (const entry of comments) {
5775
+ const taskId = String(entry.task_id || '').trim();
5776
+ const commentId = String(entry.id || '').trim();
5777
+ const timestamp = String(entry.timestamp || '');
5778
+ if (!taskId || !commentId)
5779
+ continue;
5780
+ considerLatestActivity(taskId, {
5781
+ id: `comment:${commentId}`,
5782
+ type: 'comment',
5783
+ timestamp,
5784
+ comment: {
5785
+ text: String(entry.text || ''),
5786
+ author: String(entry.author || 'user').trim() || 'user',
5787
+ },
5788
+ });
5789
+ }
5790
+ for (const row of rows) {
5791
+ const taskId = String(row.id || '').trim();
5792
+ const eventId = String(row.latest_event_id || '').trim();
5793
+ const timestamp = String(row.latest_event_at || '');
5794
+ if (!taskId || !eventId)
5795
+ continue;
5796
+ let details = {};
5797
+ try {
5798
+ const parsed = row.latest_event_details_json ? JSON.parse(String(row.latest_event_details_json)) : {};
5799
+ details = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
5800
+ }
5801
+ catch {
5802
+ details = {};
5803
+ }
5804
+ considerLatestActivity(taskId, {
5805
+ id: `event:${eventId}`,
5806
+ type: 'event',
5807
+ timestamp,
5808
+ event: {
5809
+ action: String(row.latest_event_action || ''),
5810
+ actor: String(row.latest_event_actor || 'system').trim() || 'system',
5811
+ actorType: row.latest_event_actor_type === 'human' || row.latest_event_actor_type === 'ai'
5812
+ ? row.latest_event_actor_type
5813
+ : 'system',
5814
+ details,
5815
+ },
5816
+ });
5817
+ }
5818
+ return rows.map((row) => {
5819
+ const id = String(row.id || '').trim();
5820
+ const scheduledDate = this.sanitizeDateOnly(row.scheduled_date);
5821
+ const dueDate = this.sanitizeDateOnly(row.due_date);
5822
+ const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
5823
+ const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
5824
+ let taxonomies = {};
5825
+ try {
5826
+ taxonomies = JSON.parse(row.taxonomies || '{}');
5827
+ }
5828
+ catch {
5829
+ taxonomies = {};
5830
+ }
5831
+ const checklistCounts = checklistByTask.get(id) || { total: 0, completed: 0, revision: '' };
5832
+ const commentCount = commentCountByTask.get(id) || 0;
5833
+ const attachmentCount = attachmentCountByTask.get(id) || 0;
5834
+ const relationshipCount = relationshipCountByTask.get(id) || 0;
5835
+ const detailRevision = JSON.stringify([
5836
+ String(row.updated_at || row.created_at || ''),
5837
+ commentCount,
5838
+ commentRevisionByTask.get(id) || '',
5839
+ checklistCounts.total,
5840
+ checklistCounts.completed,
5841
+ checklistCounts.revision,
5842
+ attachmentCount,
5843
+ attachmentRevisionByTask.get(id) || '',
5844
+ relationshipCount,
5845
+ relationshipRevisionByTask.get(id) || '',
5846
+ latestActivityRevisionByTask.get(id) || '',
5847
+ ]);
5848
+ return {
5849
+ id,
5850
+ referenceNumber,
5851
+ localReferenceNumber,
5852
+ referenceLabel: deriveTaskReferenceLabel({ referenceNumber, localReferenceNumber }),
5853
+ title: String(row.title || ''),
5854
+ status: this.normalizeStatusValue(row.status),
5855
+ priority: this.normalizePriorityValue(row.priority),
5856
+ complexity: Number(row.complexity || 3),
5857
+ type: String(row.type || 'task'),
5858
+ category: String(row.category || 'default'),
5859
+ createdAt: String(row.created_at || ''),
5860
+ updatedAt: row.updated_at ? String(row.updated_at) : undefined,
5861
+ completedAt: row.completed_at ? String(row.completed_at) : null,
5862
+ canceledReason: row.canceled_reason ? String(row.canceled_reason) : null,
5863
+ isArchived: row.is_archived === 1 || row.is_archived === true,
5864
+ scheduledDate,
5865
+ dueDate,
5866
+ scheduledWeekKey: row.scheduled_week_key || (scheduledDate ? this.dateOnlyToIsoWeekKey(scheduledDate) : null),
5867
+ orderInDay: this.sanitizeOrderInDay(row.order_in_day),
5868
+ workstreamOrder: this.sanitizeOrderInDay(row.workstream_order),
5869
+ workstreamId: this.sanitizeWorkstreamId(row.workstream_id),
5870
+ cardCoverAssetId: typeof row.card_cover_asset_id === 'string' && row.card_cover_asset_id.trim()
5871
+ ? row.card_cover_asset_id.trim()
5872
+ : null,
5873
+ cardCoverAttachment: coverAttachmentByTask.get(id) || null,
5874
+ taxonomies,
5875
+ createdBy: this.normalizeCreatedBy(row.created_by, normalizedWorkspaceId, { preserveGenericAi: true }),
5876
+ assignee: this.sanitizeAssignee(row.assignee, normalizedWorkspaceId),
5877
+ isSummary: true,
5878
+ commentCount,
5879
+ checklistCount: checklistCounts.total,
5880
+ completedChecklistCount: checklistCounts.completed,
5881
+ attachmentCount,
5882
+ relationshipCount,
5883
+ latestActivity: latestActivityByTask.get(id) || null,
5884
+ detailRevision,
5885
+ };
5886
+ });
5887
+ }
5888
+ listActiveTaskSummaries(workspaceId) {
5889
+ return { tasks: this.listTaskSummaries(workspaceId, false) };
5890
+ }
5891
+ listArchiveSummaries(workspaceId) {
5892
+ return { archived: this.listTaskSummaries(workspaceId, true) };
5893
+ }
5894
+ getTaskDetailRevision(id, workspaceId) {
5895
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5896
+ const taskId = String(id || '').trim();
5897
+ if (!taskId)
5898
+ return null;
5899
+ const row = this.db.prepare(`
5900
+ WITH target_task AS (
5901
+ SELECT id, created_at, updated_at
5902
+ FROM tasks
5903
+ WHERE tenant_id = ? AND workspace_id = ? AND id = ?
5904
+ ),
5905
+ comment_stats AS (
5906
+ SELECT COUNT(*) AS count, MAX(c.timestamp) AS revision
5907
+ FROM comments c
5908
+ JOIN target_task t ON t.id = c.task_id
5909
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5910
+ ),
5911
+ latest_comment AS (
5912
+ SELECT c.id, c.timestamp
5913
+ FROM comments c
5914
+ JOIN target_task t ON t.id = c.task_id
5915
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5916
+ ORDER BY c.timestamp DESC, c.id DESC
5917
+ LIMIT 1
5918
+ ),
5919
+ checklist_stats AS (
5920
+ SELECT COUNT(*) AS count,
5921
+ SUM(CASE WHEN c.is_completed = 1 THEN 1 ELSE 0 END) AS completed_count,
5922
+ MAX(COALESCE(c.updated_at, c.created_at)) AS revision
5923
+ FROM checklist_items c
5924
+ JOIN target_task t ON t.id = c.task_id
5925
+ WHERE c.tenant_id = ? AND c.workspace_id = ?
5926
+ ),
5927
+ attachment_stats AS (
5928
+ SELECT COUNT(a.asset_id) AS count,
5929
+ MAX(CASE
5930
+ WHEN COALESCE(a.updated_at, '') > COALESCE(l.updated_at, '') THEN a.updated_at
5931
+ ELSE l.updated_at
5932
+ END) AS revision
5933
+ FROM workspace_asset_links l
5934
+ JOIN target_task t ON t.id = l.task_id
5935
+ JOIN workspace_assets a
5936
+ ON a.tenant_id = l.tenant_id
5937
+ AND a.workspace_id = l.workspace_id
5938
+ AND a.asset_id = l.asset_id
5939
+ AND a.deleted_at IS NULL
5940
+ WHERE l.tenant_id = ? AND l.workspace_id = ?
5941
+ AND l.target_type = 'task' AND l.deleted_at IS NULL
5942
+ ),
5943
+ relationship_rows AS (
5944
+ SELECT r.updated_at
5945
+ FROM task_relationships r
5946
+ JOIN target_task t ON t.id = r.source_task_id
5947
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5948
+ UNION ALL
5949
+ SELECT r.updated_at
5950
+ FROM task_relationships r
5951
+ JOIN target_task t ON t.id = r.target_task_id
5952
+ WHERE r.tenant_id = ? AND r.workspace_id = ? AND r.deleted_at IS NULL
5953
+ AND r.target_task_id <> r.source_task_id
5954
+ ),
5955
+ relationship_stats AS (
5956
+ SELECT COUNT(*) AS count, MAX(updated_at) AS revision
5957
+ FROM relationship_rows
5958
+ ),
5959
+ latest_event AS (
5960
+ SELECT e.id, e.created_at
5961
+ FROM entity_events e
5962
+ JOIN target_task t ON t.id = e.entity_id
5963
+ WHERE e.tenant_id = ? AND e.workspace_id = ?
5964
+ AND e.entity_type = 'task'
5965
+ AND e.action <> 'task-comment-added'
5966
+ AND NOT (
5967
+ e.action = 'task-updated'
5968
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"saveSource":"autosave"%'
5969
+ AND REPLACE(COALESCE(e.details_json, ''), ' ', '') LIKE '%"changes":{}%'
5970
+ )
5971
+ ORDER BY e.created_at DESC, e.id DESC
5972
+ LIMIT 1
5973
+ )
5974
+ SELECT t.created_at, t.updated_at,
5975
+ comments.count AS comment_count, comments.revision AS comment_revision,
5976
+ latest_comment.id AS latest_comment_id,
5977
+ latest_comment.timestamp AS latest_comment_at,
5978
+ checklist.count AS checklist_count,
5979
+ checklist.completed_count AS completed_checklist_count,
5980
+ checklist.revision AS checklist_revision,
5981
+ attachments.count AS attachment_count,
5982
+ attachments.revision AS attachment_revision,
5983
+ relationships.count AS relationship_count,
5984
+ relationships.revision AS relationship_revision,
5985
+ latest_event.id AS latest_event_id,
5986
+ latest_event.created_at AS latest_event_at
5987
+ FROM target_task t
5988
+ CROSS JOIN comment_stats comments
5989
+ CROSS JOIN checklist_stats checklist
5990
+ CROSS JOIN attachment_stats attachments
5991
+ CROSS JOIN relationship_stats relationships
5992
+ LEFT JOIN latest_comment ON 1 = 1
5993
+ LEFT JOIN latest_event ON 1 = 1
5994
+ `).get(this.tenantId, normalizedWorkspaceId, taskId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId, this.tenantId, normalizedWorkspaceId);
5995
+ if (!row)
5996
+ return null;
5997
+ const latestCommentId = String(row.latest_comment_id || '').trim();
5998
+ const latestCommentAt = String(row.latest_comment_at || '');
5999
+ const latestEventId = String(row.latest_event_id || '').trim();
6000
+ const latestEventAt = String(row.latest_event_at || '');
6001
+ const commentTimestamp = Date.parse(latestCommentAt);
6002
+ const eventTimestamp = Date.parse(latestEventAt);
6003
+ const commentActivityId = latestCommentId ? `comment:${latestCommentId}` : '';
6004
+ const eventActivityId = latestEventId ? `event:${latestEventId}` : '';
6005
+ const latestActivityRevision = !latestEventId
6006
+ || (latestCommentId && ((Number.isFinite(commentTimestamp) && !Number.isFinite(eventTimestamp))
6007
+ || commentTimestamp > eventTimestamp
6008
+ || (commentTimestamp === eventTimestamp && commentActivityId.localeCompare(eventActivityId) > 0)))
6009
+ ? (latestCommentId ? `${latestCommentAt}:${commentActivityId}` : '')
6010
+ : `${latestEventAt}:${eventActivityId}`;
6011
+ return JSON.stringify([
6012
+ String(row.updated_at || row.created_at || ''),
6013
+ Number(row.comment_count || 0),
6014
+ String(row.comment_revision || ''),
6015
+ Number(row.checklist_count || 0),
6016
+ Number(row.completed_checklist_count || 0),
6017
+ String(row.checklist_revision || ''),
6018
+ Number(row.attachment_count || 0),
6019
+ String(row.attachment_revision || ''),
6020
+ Number(row.relationship_count || 0),
6021
+ String(row.relationship_revision || ''),
6022
+ latestActivityRevision,
6023
+ ]);
6024
+ }
6025
+ getTaskDetailSnapshot(id, workspaceId) {
6026
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6027
+ return this.db.transaction(() => {
6028
+ if (this.db.provider === 'postgres') {
6029
+ this.db.exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');
6030
+ }
6031
+ const task = this.getTask(id, normalizedWorkspaceId);
6032
+ if (!task)
6033
+ return null;
6034
+ const detailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6035
+ return detailRevision ? { ...task, detailRevision } : null;
6036
+ })();
6037
+ }
6038
+ updateTaskIfDetailRevisionMatches(id, expectedDetailRevision, updates, workspaceId, options) {
6039
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6040
+ const updated = this.db.transaction(() => {
6041
+ if (this.db.provider === 'postgres') {
6042
+ this.db.exec('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE');
6043
+ }
6044
+ const currentDetailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6045
+ if (currentDetailRevision === null)
6046
+ return null;
6047
+ if (currentDetailRevision !== expectedDetailRevision) {
6048
+ throw new TaskforceRuleError('Task changed after this editor was loaded. Reload the latest task before saving.', 409, 'TASK_DETAIL_REVISION_CONFLICT', { currentDetailRevision });
6049
+ }
6050
+ const task = this.updateTask(id, updates, normalizedWorkspaceId, {
6051
+ actorRef: options?.actorRef,
6052
+ saveSource: options?.saveSource,
6053
+ persistBackup: false,
6054
+ emitRealtime: false,
6055
+ });
6056
+ if (!task)
6057
+ return null;
6058
+ const detailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6059
+ return detailRevision ? { ...task, detailRevision } : null;
6060
+ })();
6061
+ if (updated) {
6062
+ this.persistBackup();
6063
+ this.emitRealtimeMutation({
6064
+ workspaceId: normalizedWorkspaceId,
6065
+ entityType: 'task',
6066
+ changeType: 'upsert',
6067
+ entityIds: [updated.id],
6068
+ });
6069
+ }
6070
+ return updated;
6071
+ }
6072
+ assertTaskDetailRevisionForUpdate(id, expectedDetailRevision, workspaceId) {
6073
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
6074
+ const lockSql = this.db.provider === 'postgres'
6075
+ ? `SELECT id FROM tasks WHERE tenant_id = ? AND workspace_id = ? AND id = ? FOR UPDATE`
6076
+ : `SELECT id FROM tasks WHERE tenant_id = ? AND workspace_id = ? AND id = ?`;
6077
+ const task = this.db.prepare(lockSql).get(this.tenantId, normalizedWorkspaceId, id);
6078
+ if (!task)
6079
+ throw new TaskforceRuleError('Task not found.', 404, 'TASK_NOT_FOUND');
6080
+ const currentDetailRevision = this.getTaskDetailRevision(id, normalizedWorkspaceId);
6081
+ if (currentDetailRevision !== expectedDetailRevision) {
6082
+ throw new TaskforceRuleError('Task changed after this editor was loaded. Reload the latest task before saving.', 409, 'TASK_DETAIL_REVISION_CONFLICT', { currentDetailRevision });
6083
+ }
6084
+ }
5575
6085
  /**
5576
6086
  * Filter-pushdown listing used by read-only surfaces that don't need the
5577
6087
  * full task graph (MCP list_tasks, CLI summaries). Avoids the per-task
@@ -5750,41 +6260,53 @@ export class TaskforceCore {
5750
6260
  };
5751
6261
  });
5752
6262
  }
5753
- searchTaskSummaries(query, scope = 'all', workspaceId) {
6263
+ searchTaskSummaries(query, scope = 'all', workspaceId, options) {
5754
6264
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
5755
- const normalizedQuery = String(query || '').trim();
5756
- if (!normalizedQuery)
6265
+ const normalizedQuery = normalizeTaskSearchQuery(String(query || ''));
6266
+ if (!normalizedQuery.raw)
5757
6267
  return [];
5758
- const exactReferenceNumber = parseTaskReference(normalizedQuery);
5759
- const exactLocalReferenceNumber = parseLocalTaskReference(normalizedQuery);
5760
- const term = `%${normalizedQuery}%`;
6268
+ const exactReferenceNumber = parseTaskReference(normalizedQuery.raw);
6269
+ const exactLocalReferenceNumber = parseLocalTaskReference(normalizedQuery.raw);
6270
+ const searchableText = `
6271
+ COALESCE(title, '') || ' ' ||
6272
+ COALESCE(description, '') || ' ' ||
6273
+ COALESCE(id, '') || ' ' ||
6274
+ CASE WHEN reference_number IS NULL THEN '' ELSE 'T-' || CAST(reference_number AS TEXT) END || ' ' ||
6275
+ CASE WHEN local_reference_number IS NULL THEN '' ELSE 'LT-' || CAST(local_reference_number AS TEXT) END || ' ' ||
6276
+ COALESCE(category, '') || ' ' ||
6277
+ COALESCE(type, '') || ' ' ||
6278
+ COALESCE(CAST(priority AS TEXT), '') || ' ' ||
6279
+ COALESCE(status, '') || ' ' ||
6280
+ COALESCE(assignee, '') || ' ' ||
6281
+ COALESCE(canceled_reason, '')
6282
+ `;
6283
+ const escapedTerms = normalizedQuery.tokens.map((token) => (`%${token.replace(/[\\%_]/g, '\\$&')}%`));
5761
6284
  const clauses = [
5762
6285
  'tenant_id = ?',
5763
6286
  'workspace_id = ?',
5764
- '(title LIKE ? OR description LIKE ? OR id LIKE ? OR reference_number = ? OR local_reference_number = ?)',
6287
+ ...escapedTerms.map(() => `LOWER(${searchableText}) LIKE ? ESCAPE '\\'`),
5765
6288
  ];
5766
6289
  const params = [
5767
6290
  this.tenantId,
5768
6291
  normalizedWorkspaceId,
5769
- term,
5770
- term,
5771
- term,
5772
- exactReferenceNumber || -1,
5773
- exactLocalReferenceNumber || -1,
6292
+ ...escapedTerms,
5774
6293
  ];
5775
6294
  if (scope === 'active')
5776
6295
  clauses.push('is_archived = 0');
5777
6296
  if (scope === 'archive')
5778
6297
  clauses.push('is_archived = 1');
6298
+ const limit = Math.max(1, Math.min(51, Math.trunc(options?.limit ?? 50)));
6299
+ const offset = Math.max(0, Math.trunc(options?.offset ?? 0));
5779
6300
  const rows = this.db.prepare(`
5780
- SELECT id, reference_number, local_reference_number, title, status, assignee, category, priority
6301
+ SELECT id, reference_number, local_reference_number, title, status, assignee, category, priority, is_archived
5781
6302
  FROM tasks
5782
6303
  WHERE ${clauses.join(' AND ')}
5783
6304
  ORDER BY
5784
6305
  CASE WHEN id = ? OR reference_number = ? OR local_reference_number = ? THEN 0 ELSE 1 END,
5785
- created_at DESC
5786
- LIMIT 50
5787
- `).all(...params, normalizedQuery, exactReferenceNumber || -1, exactLocalReferenceNumber || -1);
6306
+ created_at DESC,
6307
+ id DESC
6308
+ LIMIT ? OFFSET ?
6309
+ `).all(...params, normalizedQuery.raw, exactReferenceNumber || -1, exactLocalReferenceNumber || -1, limit, offset);
5788
6310
  return rows.map((row) => {
5789
6311
  const referenceNumber = this.sanitizeTaskReferenceNumber(row.reference_number);
5790
6312
  const localReferenceNumber = this.sanitizeTaskReferenceNumber(row.local_reference_number);
@@ -5798,6 +6320,7 @@ export class TaskforceCore {
5798
6320
  assignee: String(row.assignee || '').trim() || null,
5799
6321
  category: String(row.category || ''),
5800
6322
  priority: this.normalizePriorityValue(row.priority),
6323
+ isArchived: row.is_archived === 1 || row.is_archived === true,
5801
6324
  };
5802
6325
  });
5803
6326
  }
@@ -7229,6 +7752,20 @@ export class TaskforceCore {
7229
7752
  runTaskMetadataOwnershipTransaction: (input) => this.runTaskMetadataOwnershipTransaction(input),
7230
7753
  };
7231
7754
  }
7755
+ buildUnresolvedWorkspaceActor(raw) {
7756
+ const normalized = raw.toLowerCase();
7757
+ const unresolvedKind = (normalized.startsWith('ai-profile-') || this.isKnownAiModelName(normalized)) ? 'ai' : 'human';
7758
+ return {
7759
+ id: raw,
7760
+ kind: unresolvedKind,
7761
+ label: this.formatActorLabel(raw),
7762
+ username: this.normalizeAiProfileUsername(raw, raw),
7763
+ icon: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_ICON : 'User',
7764
+ color: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_COLOR : '#22c55e',
7765
+ isNamed: true,
7766
+ meta: { unresolved: true }
7767
+ };
7768
+ }
7232
7769
  resolveWorkspaceActor(workspaceId, actorRef) {
7233
7770
  const raw = String(actorRef || '').trim();
7234
7771
  const normalized = raw.toLowerCase();
@@ -7258,17 +7795,81 @@ export class TaskforceCore {
7258
7795
  if (workspaceUser) {
7259
7796
  return this.workspaceUserToActor(workspaceUser);
7260
7797
  }
7261
- const unresolvedKind = (normalized.startsWith('ai-profile-') || this.isKnownAiModelName(normalized)) ? 'ai' : 'human';
7262
- return {
7263
- id: raw,
7264
- kind: unresolvedKind,
7265
- label: this.formatActorLabel(raw),
7266
- username: this.normalizeAiProfileUsername(raw, raw),
7267
- icon: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_ICON : 'User',
7268
- color: unresolvedKind === 'ai' ? GENERIC_AI_ACTOR_COLOR : '#22c55e',
7269
- isNamed: true,
7270
- meta: { unresolved: true }
7271
- };
7798
+ return this.buildUnresolvedWorkspaceActor(raw);
7799
+ }
7800
+ resolveWorkspaceActors(workspaceId, actorRefs) {
7801
+ const rawActorRefs = [...new Set(actorRefs
7802
+ .map((actorRef) => String(actorRef || '').trim())
7803
+ .filter(Boolean))];
7804
+ const resolvedActors = new Map();
7805
+ const lookupActorRefs = [];
7806
+ for (const raw of rawActorRefs) {
7807
+ const normalized = raw.toLowerCase();
7808
+ if (normalized === 'unassigned' || normalized === 'none' || normalized === 'null') {
7809
+ resolvedActors.set(raw, null);
7810
+ }
7811
+ else if (this.isHumanActorReference(raw)) {
7812
+ resolvedActors.set(raw, this.buildHumanActorFallback());
7813
+ }
7814
+ else if (this.isGenericAiActorReference(raw)) {
7815
+ resolvedActors.set(raw, this.buildGenericAiActor());
7816
+ }
7817
+ else {
7818
+ lookupActorRefs.push(raw);
7819
+ }
7820
+ }
7821
+ if (lookupActorRefs.length === 0)
7822
+ return resolvedActors;
7823
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
7824
+ const aiProfileRows = this.db.prepare(`
7825
+ SELECT *
7826
+ FROM ai_profiles
7827
+ WHERE tenant_id = ?
7828
+ AND workspace_id = ?
7829
+ ORDER BY COALESCE(last_active_at, updated_at, created_at) DESC, id ASC
7830
+ `).all(this.tenantId, normalizedWorkspaceId);
7831
+ const aiProfileRowsById = new Map(aiProfileRows
7832
+ .map((row) => [String(row.id || '').trim(), row])
7833
+ .filter(([id]) => Boolean(id)));
7834
+ const aiProfileRowsByAlias = new Map();
7835
+ for (const row of aiProfileRows) {
7836
+ const aliases = new Set([String(row.name || '').trim().toLowerCase(), String(row.username || '').trim().toLowerCase()]);
7837
+ for (const alias of aliases) {
7838
+ if (!alias)
7839
+ continue;
7840
+ const matches = aiProfileRowsByAlias.get(alias) ?? [];
7841
+ matches.push(row);
7842
+ aiProfileRowsByAlias.set(alias, matches);
7843
+ }
7844
+ }
7845
+ const workspaceUsersById = new Map(this.listWorkspaceUsers(normalizedWorkspaceId)
7846
+ .map((user) => [user.userId, user]));
7847
+ for (const raw of lookupActorRefs) {
7848
+ const directAiProfileRow = aiProfileRowsById.get(raw);
7849
+ if (directAiProfileRow) {
7850
+ resolvedActors.set(raw, this.aiProfileToActor(this.aiProfileRowToRecord(directAiProfileRow)));
7851
+ continue;
7852
+ }
7853
+ const aliasMatches = aiProfileRowsByAlias.get(raw.toLowerCase()) ?? [];
7854
+ const aliasRowsById = new Map(aliasMatches
7855
+ .map((row) => [String(row.id || '').trim(), row])
7856
+ .filter(([id]) => Boolean(id)));
7857
+ const canonicalAliasIds = [...new Set(aliasMatches
7858
+ .map((row) => this.resolveCanonicalAiProfileId(aliasRowsById, String(row.id || '').trim()))
7859
+ .filter(Boolean))];
7860
+ if (canonicalAliasIds.length === 1) {
7861
+ const canonicalAiProfileRow = aiProfileRowsById.get(canonicalAliasIds[0]);
7862
+ if (canonicalAiProfileRow) {
7863
+ resolvedActors.set(raw, this.aiProfileToActor(this.aiProfileRowToRecord(canonicalAiProfileRow)));
7864
+ continue;
7865
+ }
7866
+ }
7867
+ const workspaceUser = workspaceUsersById.get(raw);
7868
+ resolvedActors.set(raw, workspaceUser
7869
+ ? this.workspaceUserToActor(workspaceUser)
7870
+ : this.buildUnresolvedWorkspaceActor(raw));
7871
+ }
7872
+ return resolvedActors;
7272
7873
  }
7273
7874
  listAssignableActors(workspaceId) {
7274
7875
  const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
@@ -9716,9 +10317,11 @@ export class TaskforceCore {
9716
10317
  return null;
9717
10318
  const patchFields = Object.keys(patch);
9718
10319
  const archivedNestedContentWrite = options?.allowArchivedNestedContentSyncWrite === true
9719
- && patchFields.some((field) => ['comments', 'attachments', 'checklistItems'].includes(field))
10320
+ && patchFields.some((field) => [
10321
+ 'comments', 'attachments', 'checklistItems', 'cardCoverAssetId',
10322
+ ].includes(field))
9720
10323
  && patchFields.every((field) => [
9721
- 'id', 'comments', 'attachments', 'checklistItems',
10324
+ 'id', 'comments', 'attachments', 'checklistItems', 'cardCoverAssetId',
9722
10325
  ].includes(field));
9723
10326
  if (existing.isArchived && !archivedNestedContentWrite) {
9724
10327
  throw new TaskforceRuleError('Archived tasks are read-only. Unarchive first to make changes.', 409, 'TASK_ARCHIVED_READ_ONLY');