@taskforcehq/taskforce 0.3.305 → 0.3.306

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 (337) hide show
  1. package/dist/Taskforce.module.css +295 -89
  2. package/dist/TaskforceCore.js +215 -43
  3. package/dist/TaskforceCore.test.js +433 -28
  4. package/dist/assets/fonts/CourierPrime-Bold.woff2 +0 -0
  5. package/dist/assets/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  6. package/dist/assets/fonts/CourierPrime-Italic.woff2 +0 -0
  7. package/dist/assets/fonts/CourierPrime-Regular.woff2 +0 -0
  8. package/dist/assets/fonts/Noir_medium.woff2 +0 -0
  9. package/dist/assets/fonts/Noir_regular.woff2 +0 -0
  10. package/dist/assets/fonts/RussoOne-Regular.woff2 +0 -0
  11. package/dist/assets/fonts/SpecialElite-Regular.woff2 +0 -0
  12. package/dist/compat/workspaceSyncCompat.js +46 -2
  13. package/dist/compat/workspaceSyncCompat.test.js +38 -1
  14. package/dist/components/context/ContextAttachmentManager.d.ts +14 -0
  15. package/dist/components/context/ContextAttachmentManager.js +549 -0
  16. package/dist/components/features/AgentsModule.d.ts +10 -1
  17. package/dist/components/features/AgentsModule.js +260 -15
  18. package/dist/components/features/AgentsModule.test.js +255 -5
  19. package/dist/components/features/DocumentViewer.d.ts +5 -1
  20. package/dist/components/features/DocumentViewer.js +60 -3
  21. package/dist/components/features/DocumentWorkspace.js +1 -1
  22. package/dist/components/features/DocumentWorkspace.test.js +33 -0
  23. package/dist/components/features/documentWorkspaceModel.d.ts +1 -1
  24. package/dist/components/task/TaskCard.js +19 -28
  25. package/dist/components/task/TaskCard.test.js +38 -3
  26. package/dist/components/task/TaskContextUpload.js +4 -526
  27. package/dist/components/task/TaskForm.js +85 -90
  28. package/dist/components/task/TaskForm.test.js +110 -8
  29. package/dist/components/task/TaskKanban.d.ts +2 -1
  30. package/dist/components/task/TaskKanban.js +94 -13
  31. package/dist/components/task/TaskKanban.test.js +36 -0
  32. package/dist/components/ui/EditableAvatarButton.d.ts +18 -0
  33. package/dist/components/ui/EditableAvatarButton.js +18 -0
  34. package/dist/components/views/PlanComparisonPage.d.ts +9 -1
  35. package/dist/components/views/PlanComparisonPage.js +70 -14
  36. package/dist/components/views/PlanComparisonPage.test.js +238 -0
  37. package/dist/components/views/PlansPage.js +127 -36
  38. package/dist/components/views/PlansPage.test.js +294 -12
  39. package/dist/components/views/StandaloneLayout.js +283 -533
  40. package/dist/components/views/panels/PlanningDrawer.d.ts +8 -1
  41. package/dist/components/views/panels/PlanningDrawer.js +18 -11
  42. package/dist/components/views/panels/PlanningDrawer.test.d.ts +1 -0
  43. package/dist/components/views/panels/PlanningDrawer.test.js +141 -0
  44. package/dist/components/views/planningScope.d.ts +20 -0
  45. package/dist/components/views/planningScope.js +25 -0
  46. package/dist/components/views/planningScope.test.d.ts +1 -0
  47. package/dist/components/views/planningScope.test.js +60 -0
  48. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +26 -1
  49. package/dist/components/views/standalone/modals/AccountHubModal.js +76 -2
  50. package/dist/components/views/standalone/modals/AccountHubModal.test.js +20 -3
  51. package/dist/components/views/standalone/modals/AvatarImageManagerModal.d.ts +20 -0
  52. package/dist/components/views/standalone/modals/AvatarImageManagerModal.js +127 -0
  53. package/dist/components/views/standalone/modals/EditProfileModal.d.ts +2 -9
  54. package/dist/components/views/standalone/modals/EditProfileModal.js +3 -2
  55. package/dist/components/views/standalone/modals/SyncEnableWarningModal.d.ts +9 -0
  56. package/dist/components/views/standalone/modals/SyncEnableWarningModal.js +6 -0
  57. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.d.ts +1 -0
  58. package/dist/components/views/standalone/modals/SyncEnableWarningModal.test.js +24 -0
  59. package/dist/components/views/standalone/modals/SyncStatusModal.d.ts +2 -1
  60. package/dist/components/views/standalone/modals/SyncStatusModal.js +2 -2
  61. package/dist/core/AiProfileService.d.ts +39 -2
  62. package/dist/core/AiProfileService.js +639 -73
  63. package/dist/core/AiProfileService.test.js +19 -1
  64. package/dist/core/AiProfiles.test.js +566 -7
  65. package/dist/core/AttachmentLinkService.js +57 -15
  66. package/dist/core/AuthIdentityService.d.ts +107 -0
  67. package/dist/core/AuthIdentityService.js +284 -0
  68. package/dist/core/AuthTokenService.d.ts +4 -0
  69. package/dist/core/AuthTokenService.js +34 -6
  70. package/dist/core/AuthTokenService.test.js +3 -3
  71. package/dist/core/EntitlementsPolicy.test.js +2 -2
  72. package/dist/core/GlobalSettingsService.js +115 -6
  73. package/dist/core/PlanEntitlementService.d.ts +23 -2
  74. package/dist/core/PlanEntitlementService.js +343 -57
  75. package/dist/core/PlanFeatureCatalog.test.d.ts +1 -0
  76. package/dist/core/PlanFeatureCatalog.test.js +84 -0
  77. package/dist/core/PlanVersionPolicy.test.js +35 -2
  78. package/dist/core/PlanningEntities.test.js +52 -0
  79. package/dist/core/SignupMonetizationSettings.test.js +199 -27
  80. package/dist/core/SyncReconciliationService.js +2 -65
  81. package/dist/core/SystemAdmin.test.js +42 -42
  82. package/dist/core/TaskAttachmentsCanonical.test.js +51 -1
  83. package/dist/core/Taskforce.d.ts +225 -14
  84. package/dist/core/Taskforce.js +765 -192
  85. package/dist/core/Taskforce.listTasksSlim.test.d.ts +1 -0
  86. package/dist/core/Taskforce.listTasksSlim.test.js +91 -0
  87. package/dist/core/UserProfileAvatarDrafts.test.js +30 -3
  88. package/dist/core/WorkspaceLifecycleService.js +0 -5
  89. package/dist/core/WorkspacePermissions.test.js +25 -1
  90. package/dist/core/WorkspacePlanMode.test.js +117 -16
  91. package/dist/core/shared.d.ts +1 -1
  92. package/dist/core/shared.js +5 -1
  93. package/dist/core/types.d.ts +68 -2
  94. package/dist/hooks/auth/useTaskforceAuthBootstrap.js +26 -1
  95. package/dist/hooks/sync/bootstrap.js +2 -0
  96. package/dist/hooks/sync/controlPlane.d.ts +37 -0
  97. package/dist/hooks/sync/controlPlane.js +78 -0
  98. package/dist/hooks/sync/controlPlane.test.d.ts +1 -0
  99. package/dist/hooks/sync/controlPlane.test.js +121 -0
  100. package/dist/hooks/sync/lifecyclePolicy.d.ts +45 -0
  101. package/dist/hooks/sync/lifecyclePolicy.js +93 -0
  102. package/dist/hooks/sync/lifecyclePolicy.test.d.ts +1 -0
  103. package/dist/hooks/sync/lifecyclePolicy.test.js +124 -0
  104. package/dist/hooks/sync/orchestratorShared.d.ts +10 -1
  105. package/dist/hooks/sync/orchestratorShared.js +68 -47
  106. package/dist/hooks/sync/orchestratorShared.test.js +50 -1
  107. package/dist/hooks/sync/pullLifecycle.d.ts +31 -0
  108. package/dist/hooks/sync/pullLifecycle.js +24 -0
  109. package/dist/hooks/sync/pullLifecycle.test.d.ts +1 -0
  110. package/dist/hooks/sync/pullLifecycle.test.js +80 -0
  111. package/dist/hooks/sync/recovery.d.ts +16 -2
  112. package/dist/hooks/sync/recovery.js +171 -53
  113. package/dist/hooks/sync/recovery.test.d.ts +1 -0
  114. package/dist/hooks/sync/recovery.test.js +292 -0
  115. package/dist/hooks/sync/transfers.d.ts +15 -3
  116. package/dist/hooks/sync/transfers.js +179 -62
  117. package/dist/hooks/sync/transfers.test.d.ts +1 -0
  118. package/dist/hooks/sync/transfers.test.js +396 -0
  119. package/dist/hooks/sync/useRecentSyncEvents.d.ts +21 -0
  120. package/dist/hooks/sync/useRecentSyncEvents.js +92 -0
  121. package/dist/hooks/sync/useRecentSyncEvents.test.d.ts +1 -0
  122. package/dist/hooks/sync/useRecentSyncEvents.test.js +124 -0
  123. package/dist/hooks/sync/useSyncStatusActions.d.ts +30 -0
  124. package/dist/hooks/sync/useSyncStatusActions.js +88 -0
  125. package/dist/hooks/sync/useSyncStatusActions.test.d.ts +1 -0
  126. package/dist/hooks/sync/useSyncStatusActions.test.js +133 -0
  127. package/dist/hooks/sync/useSyncStatusControls.d.ts +25 -0
  128. package/dist/hooks/sync/useSyncStatusControls.js +60 -0
  129. package/dist/hooks/sync/useSyncStatusControls.test.d.ts +1 -0
  130. package/dist/hooks/sync/useSyncStatusControls.test.js +88 -0
  131. package/dist/hooks/useSyncOrchestrator.aiProfiles.test.js +12 -0
  132. package/dist/hooks/useSyncOrchestrator.d.ts +9 -2
  133. package/dist/hooks/useSyncOrchestrator.js +359 -178
  134. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +407 -0
  135. package/dist/hooks/useTaskData.js +8 -3
  136. package/dist/hooks/useTaskData.test.js +45 -0
  137. package/dist/hooks/useTaskforce.d.ts +60 -2
  138. package/dist/hooks/useTaskforce.js +549 -240
  139. package/dist/hooks/useTaskforce.runtime-routing.test.d.ts +1 -0
  140. package/dist/hooks/useTaskforce.runtime-routing.test.js +152 -0
  141. package/dist/hooks/useTaskforce.sync-behavior.test.js +2380 -212
  142. package/dist/hooks/useWorkspaceSyncController.d.ts +1 -0
  143. package/dist/hooks/useWorkspaceSyncController.js +6 -1
  144. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.d.ts +16 -0
  145. package/dist/hooks/workspace/useTaskforceWorkspaceBootstrap.js +316 -35
  146. package/dist/localization/locales/en-US.d.ts +0 -1
  147. package/dist/localization/locales/en-US.js +0 -1
  148. package/dist/localization/locales/es-419.js +0 -1
  149. package/dist/localization/locales/pt-BR.js +0 -1
  150. package/dist/mcp/documentAssetRegistrar.js +3 -3
  151. package/dist/mcp/runtime.d.ts +2 -0
  152. package/dist/mcp/runtime.js +131 -64
  153. package/dist/mcp/runtime.test.js +291 -12
  154. package/dist/migrations/taskSchemaMigrations.d.ts +1 -1
  155. package/dist/migrations/taskSchemaMigrations.js +171 -61
  156. package/dist/migrations/taskSchemaMigrations.test.js +15 -0
  157. package/dist/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  158. package/dist/resources/templates/workflows/collaborate.yaml +1 -1
  159. package/dist/resources/templates/workflows/evaluate.yaml +2 -2
  160. package/dist/resources/templates/workflows/plan.yaml +1 -1
  161. package/dist/resources/templates/workflows/review.yaml +2 -2
  162. package/dist/server/annotatedAttachmentsRoutes.test.js +3 -3
  163. package/dist/server/auth/providers/apple.d.ts +33 -0
  164. package/dist/server/auth/providers/apple.js +82 -0
  165. package/dist/server/auth/providers/appleClientSecret.d.ts +37 -0
  166. package/dist/server/auth/providers/appleClientSecret.js +65 -0
  167. package/dist/server/auth/providers/github.d.ts +26 -0
  168. package/dist/server/auth/providers/github.js +86 -0
  169. package/dist/server/auth/providers/google.d.ts +21 -0
  170. package/dist/server/auth/providers/google.js +56 -0
  171. package/dist/server/auth/providers/types.d.ts +21 -0
  172. package/dist/server/auth/providers/types.js +1 -0
  173. package/dist/server/auth.d.ts +2 -0
  174. package/dist/server/auth.js +6 -3
  175. package/dist/server/documentReviewRoutes.test.js +36 -3
  176. package/dist/server/index.cookieProxy.test.js +1 -0
  177. package/dist/server/index.d.ts +12 -0
  178. package/dist/server/index.js +120 -9
  179. package/dist/server/index.rateLimit.test.js +3 -0
  180. package/dist/server/index.test.js +106 -3
  181. package/dist/server/routes/admin.d.ts +3 -0
  182. package/dist/server/routes/admin.js +272 -19
  183. package/dist/server/routes/auth.d.ts +15 -2
  184. package/dist/server/routes/auth.js +994 -57
  185. package/dist/server/routes/authSupport.d.ts +1 -1
  186. package/dist/server/routes/authSupport.js +1 -1
  187. package/dist/server/routes/billing.js +361 -35
  188. package/dist/server/routes/billing.test.js +592 -12
  189. package/dist/server/routes/documents.d.ts +1 -0
  190. package/dist/server/routes/documents.js +172 -70
  191. package/dist/server/routes/shared.d.ts +28 -2
  192. package/dist/server/routes/shared.js +69 -4
  193. package/dist/server/routes/sync.integration.test.js +437 -39
  194. package/dist/server/routes/sync.js +61 -605
  195. package/dist/server/routes/syncAuxRoutes.d.ts +15 -0
  196. package/dist/server/routes/syncAuxRoutes.js +91 -0
  197. package/dist/server/routes/syncAuxRoutes.test.d.ts +1 -0
  198. package/dist/server/routes/syncAuxRoutes.test.js +158 -0
  199. package/dist/server/routes/syncPullApplyRoutes.d.ts +36 -0
  200. package/dist/server/routes/syncPullApplyRoutes.js +204 -0
  201. package/dist/server/routes/syncPushRoutes.d.ts +24 -0
  202. package/dist/server/routes/syncPushRoutes.js +212 -0
  203. package/dist/server/routes/syncRouteGuards.d.ts +36 -0
  204. package/dist/server/routes/syncRouteGuards.js +67 -0
  205. package/dist/server/routes/syncRouteGuards.test.d.ts +1 -0
  206. package/dist/server/routes/syncRouteGuards.test.js +94 -0
  207. package/dist/server/routes/syncRouteTypes.d.ts +13 -0
  208. package/dist/server/routes/syncRouteTypes.js +1 -0
  209. package/dist/server/routes/syncSnapshotRoutes.d.ts +66 -0
  210. package/dist/server/routes/syncSnapshotRoutes.js +180 -0
  211. package/dist/server/routes/tasks.js +43 -1
  212. package/dist/server/routes/workspaces.js +36 -5
  213. package/dist/server/routes.js +60 -3
  214. package/dist/server/routes.test.js +1824 -171
  215. package/dist/services/workflowExportSnapshots.test.js +2 -0
  216. package/dist/shared/aiProfileSeatScope.d.ts +5 -0
  217. package/dist/shared/aiProfileSeatScope.js +21 -0
  218. package/dist/shared/passwordPolicy.d.ts +13 -0
  219. package/dist/shared/passwordPolicy.js +84 -0
  220. package/dist/shared/passwordPolicy.test.d.ts +1 -0
  221. package/dist/shared/passwordPolicy.test.js +34 -0
  222. package/dist/storage/workspaceAssetStore.d.ts +15 -3
  223. package/dist/storage/workspaceAssetStore.js +132 -44
  224. package/dist/storage/workspaceAssetStore.test.js +93 -6
  225. package/dist/styles/fonts.css +70 -0
  226. package/dist/sync/collaborationSyncPayload.d.ts +6 -1
  227. package/dist/sync/collaborationSyncPayload.js +17 -11
  228. package/dist/sync/collaborationSyncPayload.test.js +13 -1
  229. package/dist/sync/contentSyncState.d.ts +4 -8
  230. package/dist/sync/contentSyncState.js +17 -70
  231. package/dist/sync/syncApplyHandlers.d.ts +7 -0
  232. package/dist/sync/syncApplyHandlers.js +28 -13
  233. package/dist/sync/syncApplyHandlers.test.js +70 -3
  234. package/dist/sync/syncFieldSemantics.d.ts +12 -0
  235. package/dist/sync/syncFieldSemantics.js +55 -0
  236. package/dist/sync/syncResourceAdapters.d.ts +44 -0
  237. package/dist/sync/syncResourceAdapters.js +77 -0
  238. package/dist/sync/syncService.d.ts +5 -0
  239. package/dist/sync/syncService.js +52 -0
  240. package/dist/sync/syncService.test.js +57 -3
  241. package/dist/sync/taskSyncChangeKey.d.ts +2 -0
  242. package/dist/sync/taskSyncChangeKey.js +143 -0
  243. package/dist/sync/taskSyncPayload.js +3 -8
  244. package/dist/sync/workspacePullFeed.js +4 -3
  245. package/dist/sync/workspacePullFeed.test.js +43 -0
  246. package/dist/sync/workspaceRepair.js +5 -2
  247. package/dist/sync/workspaceSyncModel.d.ts +45 -0
  248. package/dist/sync/workspaceSyncModel.js +183 -28
  249. package/dist/sync/workspaceSyncModel.test.js +291 -12
  250. package/dist/sync/workspaceSyncState.d.ts +17 -0
  251. package/dist/sync/workspaceSyncState.js +2 -1
  252. package/dist/sync/workspaceSyncSurface.js +3 -8
  253. package/dist/types.d.ts +17 -0
  254. package/dist/ui/assets/AgentsModule-DrLawwNl.js +1 -0
  255. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BG6P_GVW.js → AnnotatedAttachmentWorkspace-Z9_whf7F.js} +1 -1
  256. package/dist/ui/assets/CourierPrime-Bold-BuLj_dpw.woff2 +0 -0
  257. package/dist/ui/assets/CourierPrime-BoldItalic-BrK2pUkZ.woff2 +0 -0
  258. package/dist/ui/assets/CourierPrime-Italic-DFUZK5Ey.woff2 +0 -0
  259. package/dist/ui/assets/CourierPrime-Regular-BsKphzVf.woff2 +0 -0
  260. package/dist/ui/assets/DocumentWorkspace-B03MaSkw.js +252 -0
  261. package/dist/ui/assets/DocumentWorkspace-Dhdso07p.css +1 -0
  262. package/dist/ui/assets/{InitiativesModule-Dhm7M8e7.js → InitiativesModule-9E6ParrY.js} +1 -1
  263. package/dist/ui/assets/Noir_medium-bxQwKGzB.woff2 +0 -0
  264. package/dist/ui/assets/Noir_regular-ojf2kxlG.woff2 +0 -0
  265. package/dist/ui/assets/PlansPage-CvfnMiWq.css +1 -0
  266. package/dist/ui/assets/PlansPage-DRXscYxH.js +1 -0
  267. package/dist/ui/assets/RussoOne-Regular-Cu_qq_qC.woff2 +0 -0
  268. package/dist/ui/assets/SpecialElite-Regular-Di6gSvAS.woff2 +0 -0
  269. package/dist/ui/assets/{TaskSettings-ln0aF7xc.js → TaskSettings-DZX4jk7e.js} +1 -1
  270. package/dist/ui/assets/{WorkflowsModule-BcS4afRn.js → WorkflowsModule-BA7jcEpH.js} +1 -1
  271. package/dist/ui/assets/index-9eT8e0Lu.css +1 -0
  272. package/dist/ui/assets/index-C6k75jr8.js +6 -0
  273. package/dist/ui/assets/{vendor-icons-BSUaRwF8.js → vendor-icons-DuEd_65c.js} +1 -1
  274. package/dist/ui/fonts/CourierPrime-Bold.ttf +0 -0
  275. package/dist/ui/fonts/CourierPrime-Bold.woff2 +0 -0
  276. package/dist/ui/fonts/CourierPrime-BoldItalic.ttf +0 -0
  277. package/dist/ui/fonts/CourierPrime-BoldItalic.woff2 +0 -0
  278. package/dist/ui/fonts/CourierPrime-Italic.ttf +0 -0
  279. package/dist/ui/fonts/CourierPrime-Italic.woff2 +0 -0
  280. package/dist/ui/fonts/CourierPrime-Regular.ttf +0 -0
  281. package/dist/ui/fonts/CourierPrime-Regular.woff2 +0 -0
  282. package/dist/ui/fonts/Noir_medium.otf +0 -0
  283. package/dist/ui/fonts/Noir_medium.woff2 +0 -0
  284. package/dist/ui/fonts/Noir_regular.otf +0 -0
  285. package/dist/ui/fonts/Noir_regular.woff2 +0 -0
  286. package/dist/ui/fonts/RussoOne-Regular.woff2 +0 -0
  287. package/dist/ui/fonts/SpecialElite-Regular.ttf +0 -0
  288. package/dist/ui/fonts/SpecialElite-Regular.woff2 +0 -0
  289. package/dist/ui/index.html +3 -3
  290. package/dist/utils/accountProfileSummaryCache.d.ts +5 -0
  291. package/dist/utils/assignees.d.ts +2 -0
  292. package/dist/utils/assignees.js +2 -0
  293. package/dist/utils/avatarUpload.d.ts +27 -0
  294. package/dist/utils/avatarUpload.js +132 -0
  295. package/dist/utils/avatarUpload.test.d.ts +1 -0
  296. package/dist/utils/avatarUpload.test.js +40 -0
  297. package/dist/utils/bootstrapTrace.d.ts +7 -0
  298. package/dist/utils/bootstrapTrace.js +29 -0
  299. package/dist/utils/commercialLifecyclePresentation.js +3 -0
  300. package/dist/utils/contextAssetEvents.d.ts +2 -0
  301. package/dist/utils/syncEventDetails.d.ts +3 -0
  302. package/dist/utils/syncEventDetails.js +64 -0
  303. package/dist/utils/syncEventDetails.test.d.ts +1 -0
  304. package/dist/utils/syncEventDetails.test.js +23 -0
  305. package/dist/utils/syncEventPresentation.d.ts +15 -0
  306. package/dist/utils/syncEventPresentation.js +129 -0
  307. package/dist/utils/syncEventPresentation.test.d.ts +1 -0
  308. package/dist/utils/syncEventPresentation.test.js +64 -0
  309. package/dist/utils/syncStatusPresentation.d.ts +72 -0
  310. package/dist/utils/syncStatusPresentation.js +217 -0
  311. package/dist/utils/syncStatusPresentation.test.d.ts +1 -0
  312. package/dist/utils/syncStatusPresentation.test.js +164 -0
  313. package/dist/utils/taskActivity.d.ts +2 -0
  314. package/dist/utils/taskActivity.js +228 -22
  315. package/dist/utils/taskActivity.test.js +119 -8
  316. package/dist/utils/workspaceSyncPresentation.d.ts +4 -0
  317. package/dist/utils/workspaceSyncPresentation.js +68 -2
  318. package/dist/utils/workspaceSyncPresentation.test.js +36 -1
  319. package/package.json +3 -2
  320. package/scripts/export-sqlite-to-postgres.mjs +7 -9
  321. package/scripts/run-e2e-realtime.sh +17 -3
  322. package/scripts/run-smoke.sh +17 -5
  323. package/scripts/run-soak.sh +17 -5
  324. package/src/resources/templates/workflow-sources/workflowDocs.mjs +6 -6
  325. package/src/resources/templates/workflows/collaborate.yaml +1 -1
  326. package/src/resources/templates/workflows/evaluate.yaml +2 -2
  327. package/src/resources/templates/workflows/plan.yaml +1 -1
  328. package/src/resources/templates/workflows/review.yaml +2 -2
  329. package/dist/ui/assets/AgentsModule-N2MnQBZk.js +0 -1
  330. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +0 -1
  331. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +0 -252
  332. package/dist/ui/assets/PlansPage-C2dd2n1X.css +0 -1
  333. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +0 -1
  334. package/dist/ui/assets/RussoOne-Regular-C3BxZIj7.ttf +0 -0
  335. package/dist/ui/assets/index-C2-OXZV7.css +0 -1
  336. package/dist/ui/assets/index-DcSI5F86.js +0 -6
  337. package/scripts/migrate-planning-model.ts +0 -233
@@ -80,6 +80,11 @@ describe('AiProfileService', () => {
80
80
  color: '#0ea5e9',
81
81
  surfaceType: 'coding_tool',
82
82
  providerMetadata: { source: 'sync' },
83
+ archivedAt: null,
84
+ archivedBy: null,
85
+ archivedReason: null,
86
+ mergedIntoProfileId: null,
87
+ rosterStateUpdatedAt: '2026-03-22T10:00:00.000Z',
83
88
  createdAt: '2026-03-22T10:00:00.000Z',
84
89
  updatedAt: '2026-03-22T10:05:00.000Z',
85
90
  lastActiveAt: '2026-03-22T10:05:00.000Z'
@@ -131,6 +136,11 @@ describe('AiProfileService', () => {
131
136
  color: profile.color,
132
137
  surface_type: profile.surfaceType,
133
138
  provider_metadata_json: JSON.stringify(profile.providerMetadata),
139
+ archived_at: profile.archivedAt,
140
+ archived_by: profile.archivedBy,
141
+ archived_reason: profile.archivedReason,
142
+ merged_into_profile_id: profile.mergedIntoProfileId,
143
+ roster_state_updated_at: profile.rosterStateUpdatedAt,
134
144
  created_at: profile.createdAt,
135
145
  updated_at: profile.updatedAt,
136
146
  last_active_at: profile.lastActiveAt
@@ -155,12 +165,20 @@ describe('AiProfileService', () => {
155
165
  const upsertSql = statements.find((statement) => statement.includes('INSERT INTO ai_profiles'));
156
166
  expect(upsertSql).toBeTruthy();
157
167
  expect(upsertSql).toContain('id, tenant_id, workspace_id, profile_token, name, username, icon, color,');
158
- expect(upsertSql).toContain('surface_type, provider_metadata_json, created_at, updated_at, last_active_at');
168
+ expect(upsertSql).toContain('surface_type, seat_scope, provider_metadata_json,');
169
+ expect(upsertSql).toContain('archived_at, archived_by, archived_reason, merged_into_profile_id,');
170
+ expect(upsertSql).toContain('roster_state_updated_at, created_at, updated_at, last_active_at');
159
171
  expect(upsertSql).toContain('ON CONFLICT(id) DO UPDATE SET');
160
172
  expect(upsertSql).toContain('workspace_id = excluded.workspace_id');
161
173
  expect(upsertSql).toContain('profile_token = excluded.profile_token');
162
174
  expect(upsertSql).toContain('surface_type = excluded.surface_type');
175
+ expect(upsertSql).toContain('seat_scope = excluded.seat_scope');
163
176
  expect(upsertSql).toContain('provider_metadata_json = excluded.provider_metadata_json');
177
+ expect(upsertSql).toContain('archived_at = excluded.archived_at');
178
+ expect(upsertSql).toContain('archived_by = excluded.archived_by');
179
+ expect(upsertSql).toContain('archived_reason = excluded.archived_reason');
180
+ expect(upsertSql).toContain('merged_into_profile_id = excluded.merged_into_profile_id');
181
+ expect(upsertSql).toContain('roster_state_updated_at = excluded.roster_state_updated_at');
164
182
  expect(upsertSql).toContain('last_active_at = excluded.last_active_at');
165
183
  });
166
184
  });
@@ -98,6 +98,128 @@ describe('TaskforceCore ai profiles', () => {
98
98
  expect(second.profile.icon).toBe('Sparkles');
99
99
  expect(second.profile.color).toBe('#0ea5e9');
100
100
  });
101
+ it('updates ai profile avatar urls without changing the registered icon fallback', () => {
102
+ const projectRoot = createProjectRoot();
103
+ createdRoots.push(projectRoot);
104
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
105
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
106
+ const resolved = core.resolveAiProfile({
107
+ workspaceId: 'workspace-1',
108
+ name: 'Codex',
109
+ icon: 'Bot',
110
+ color: '#0ea5e9'
111
+ });
112
+ const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
113
+ const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
114
+ const updated = core.updateAiProfileAvatar({
115
+ workspaceId: 'workspace-1',
116
+ profileId: resolved.profile.id,
117
+ avatarUrl,
118
+ avatarSourceUrl
119
+ });
120
+ expect(updated.avatarUrl).toBe(avatarUrl);
121
+ expect(updated.avatarSourceUrl).toBe(avatarSourceUrl);
122
+ expect(updated.icon).toBe('Bot');
123
+ expect(core.listAiProfiles('workspace-1')[0]?.avatarUrl).toBe(avatarUrl);
124
+ expect(core.listAiProfiles('workspace-1')[0]?.avatarSourceUrl).toBe(avatarSourceUrl);
125
+ const cleared = core.updateAiProfileAvatar({
126
+ workspaceId: 'workspace-1',
127
+ profileId: resolved.profile.id,
128
+ avatarUrl: null,
129
+ avatarSourceUrl: null
130
+ });
131
+ expect(cleared.avatarUrl).toBeNull();
132
+ expect(cleared.avatarSourceUrl).toBeNull();
133
+ });
134
+ it('preserves synced ai profile avatars when metadata-only sync payloads omit avatar fields', () => {
135
+ const projectRoot = createProjectRoot();
136
+ createdRoots.push(projectRoot);
137
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
138
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
139
+ const resolved = core.resolveAiProfile({
140
+ workspaceId: 'workspace-1',
141
+ name: 'Codex',
142
+ icon: 'Bot',
143
+ color: '#0ea5e9'
144
+ });
145
+ const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
146
+ const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
147
+ const updated = core.updateAiProfileAvatar({
148
+ workspaceId: 'workspace-1',
149
+ profileId: resolved.profile.id,
150
+ avatarUrl,
151
+ avatarSourceUrl
152
+ });
153
+ const { avatarUrl: _avatarUrl, avatarSourceUrl: _avatarSourceUrl, ...metadataOnlyProfile } = updated;
154
+ const metadataUpdatedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
155
+ const replayed = core.upsertAiProfileForSync({
156
+ ...metadataOnlyProfile,
157
+ name: 'Codex Metadata Refresh',
158
+ updatedAt: metadataUpdatedAt,
159
+ lastActiveAt: metadataUpdatedAt
160
+ });
161
+ expect(replayed.name).toBe('Codex Metadata Refresh');
162
+ expect(replayed.avatarUrl).toBe(avatarUrl);
163
+ expect(replayed.avatarSourceUrl).toBe(avatarSourceUrl);
164
+ });
165
+ it('clears synced ai profile avatars when sync payloads explicitly send null avatar fields', () => {
166
+ const projectRoot = createProjectRoot();
167
+ createdRoots.push(projectRoot);
168
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
169
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
170
+ const resolved = core.resolveAiProfile({
171
+ workspaceId: 'workspace-1',
172
+ name: 'Codex',
173
+ icon: 'Bot',
174
+ color: '#0ea5e9'
175
+ });
176
+ const updated = core.updateAiProfileAvatar({
177
+ workspaceId: 'workspace-1',
178
+ profileId: resolved.profile.id,
179
+ avatarUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp',
180
+ avatarSourceUrl: '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp'
181
+ });
182
+ const clearedAt = new Date(Date.parse(updated.updatedAt) + 1000).toISOString();
183
+ const cleared = core.upsertAiProfileForSync({
184
+ ...updated,
185
+ avatarUrl: null,
186
+ avatarSourceUrl: null,
187
+ updatedAt: clearedAt,
188
+ lastActiveAt: clearedAt
189
+ });
190
+ expect(cleared.avatarUrl).toBeNull();
191
+ expect(cleared.avatarSourceUrl).toBeNull();
192
+ });
193
+ it('repairs missing synced ai profile avatars when timestamps match', () => {
194
+ const projectRoot = createProjectRoot();
195
+ createdRoots.push(projectRoot);
196
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
197
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
198
+ const resolved = core.resolveAiProfile({
199
+ workspaceId: 'workspace-1',
200
+ name: 'Codex',
201
+ icon: 'Bot',
202
+ color: '#0ea5e9'
203
+ });
204
+ const avatarUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Favatar.webp';
205
+ const avatarSourceUrl = '/api/taskforce/context/workspaces%2Fworkspace-1%2Fassets%2Fimages%2Fai-profiles%2Fprofile%2Favatar%2Fsource.webp';
206
+ const updated = core.updateAiProfileAvatar({
207
+ workspaceId: 'workspace-1',
208
+ profileId: resolved.profile.id,
209
+ avatarUrl,
210
+ avatarSourceUrl
211
+ });
212
+ core.db.prepare(`
213
+ UPDATE ai_profiles
214
+ SET avatar_url = NULL,
215
+ avatar_source_url = NULL
216
+ WHERE id = ?
217
+ `).run(updated.id);
218
+ const repaired = core.upsertAiProfileForSync(updated);
219
+ expect(repaired.updatedAt).toBe(updated.updatedAt);
220
+ expect(repaired.avatarUrl).toBe(avatarUrl);
221
+ expect(repaired.avatarSourceUrl).toBe(avatarSourceUrl);
222
+ });
101
223
  it('stores expanded ai profile fields and preserves them when omitted on re-resolve', () => {
102
224
  const projectRoot = createProjectRoot();
103
225
  createdRoots.push(projectRoot);
@@ -533,12 +655,12 @@ describe('TaskforceCore ai profiles', () => {
533
655
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
534
656
  core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
535
657
  enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
536
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Automation Codex' });
658
+ const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Automation Codex' }, { trustedSeatScope: 'cloud_metered' });
537
659
  const second = core.resolveAiProfile({
538
660
  workspaceId: 'workspace-1',
539
661
  name: 'Automation Codex',
540
- role: 'Assistant'
541
- });
662
+ role: 'Assistant',
663
+ }, { trustedSeatScope: 'cloud_metered' });
542
664
  expect(second.created).toBe(false);
543
665
  expect(second.profile.id).toBe(first.profile.id);
544
666
  expect(second.profile.role).toBe('Assistant');
@@ -550,10 +672,10 @@ describe('TaskforceCore ai profiles', () => {
550
672
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
551
673
  core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
552
674
  enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
553
- const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' });
675
+ const first = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex' }, { trustedSeatScope: 'cloud_metered' });
554
676
  expect(first.created).toBe(true);
555
677
  try {
556
- core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude' });
678
+ core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Claude' }, { trustedSeatScope: 'cloud_metered' });
557
679
  throw new Error('Expected AI profile creation to be blocked');
558
680
  }
559
681
  catch (error) {
@@ -568,6 +690,157 @@ describe('TaskforceCore ai profiles', () => {
568
690
  });
569
691
  }
570
692
  });
693
+ it('counts only cloud-metered ai profiles toward the seat limit', () => {
694
+ const projectRoot = createProjectRoot();
695
+ createdRoots.push(projectRoot);
696
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
697
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
698
+ enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
699
+ const local = core.resolveAiProfile({
700
+ workspaceId: 'workspace-1',
701
+ name: 'Local Codex',
702
+ }, { trustedSeatScope: 'local_unmetered' });
703
+ const cloud = core.resolveAiProfile({
704
+ workspaceId: 'workspace-1',
705
+ name: 'Cloud Codex',
706
+ }, { trustedSeatScope: 'cloud_metered' });
707
+ const extraLocal = core.resolveAiProfile({
708
+ workspaceId: 'workspace-1',
709
+ name: 'Another Local Agent',
710
+ }, { trustedSeatScope: 'local_unmetered' });
711
+ expect(local.profile.seatScope).toBe('local_unmetered');
712
+ expect(cloud.profile.seatScope).toBe('cloud_metered');
713
+ expect(extraLocal.profile.seatScope).toBe('local_unmetered');
714
+ expect(core.countAiProfileUsage('workspace-1')).toEqual({
715
+ used: 1,
716
+ limit: 1,
717
+ remaining: 0
718
+ });
719
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(3);
720
+ });
721
+ it('honors configured ai profile limits even when legacy plan data stores the feature as enabled', () => {
722
+ const projectRoot = createProjectRoot();
723
+ createdRoots.push(projectRoot);
724
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
725
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
726
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
727
+ core.ensureBootstrapUserAccess({
728
+ userId: 'owner-u1',
729
+ workspaceId: 'workspace-1',
730
+ role: 'owner',
731
+ email: 'owner-u1@example.com'
732
+ });
733
+ core.updatePlanVersionFeatures({
734
+ planVersionId: 'free-v1',
735
+ features: [
736
+ { featureKey: 'collaboration.ai_profiles', access: 'enabled', config: { maxAiProfiles: 20 } }
737
+ ]
738
+ });
739
+ core.db.prepare(`
740
+ UPDATE plan_feature_matrix
741
+ SET access = 'enabled',
742
+ config_json = ?
743
+ WHERE tenant_id = ? AND plan_version_id = ? AND feature_key = ?
744
+ `).run(JSON.stringify({ maxAiProfiles: 20 }), 'default', 'free-v1', 'collaboration.ai_profiles');
745
+ core.updateAccountEntitlement({
746
+ accountId: 'owner-u1',
747
+ planVersionId: 'free-v1',
748
+ entitlementState: 'active',
749
+ actorUserId: 'system-admin',
750
+ actorRole: 'system_admin'
751
+ });
752
+ core.resolveAiProfile({
753
+ workspaceId: 'workspace-1',
754
+ name: 'Cloud Codex',
755
+ }, { trustedSeatScope: 'cloud_metered' });
756
+ expect(core.countAiProfileUsage('workspace-1')).toEqual({
757
+ used: 1,
758
+ limit: 20,
759
+ remaining: 19
760
+ });
761
+ });
762
+ it('blocks cloud promotion when a local ai profile would exceed the seat limit', () => {
763
+ const projectRoot = createProjectRoot();
764
+ createdRoots.push(projectRoot);
765
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
766
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
767
+ enableAiProfileSeatLimit(core, 'workspace-1', 'owner-u1', 1);
768
+ core.resolveAiProfile({
769
+ workspaceId: 'workspace-1',
770
+ name: 'Cloud Bot',
771
+ }, { trustedSeatScope: 'cloud_metered' });
772
+ const local = core.resolveAiProfile({
773
+ workspaceId: 'workspace-1',
774
+ name: 'Hybrid Bot',
775
+ }, { trustedSeatScope: 'local_unmetered' });
776
+ try {
777
+ core.resolveAiProfile({
778
+ workspaceId: 'workspace-1',
779
+ name: 'Hybrid Bot',
780
+ }, { trustedSeatScope: 'cloud_metered' });
781
+ throw new Error('Expected cloud promotion to be blocked');
782
+ }
783
+ catch (error) {
784
+ expect(error.message).toContain('No AI seats are available for this workspace');
785
+ expect(error.code).toBe('AI_PROFILE_LIMIT_REACHED');
786
+ }
787
+ const refreshed = core.getAiProfileByToken({
788
+ workspaceId: 'workspace-1',
789
+ profileToken: local.profile.profileToken
790
+ });
791
+ expect(refreshed?.seatScope).toBe('local_unmetered');
792
+ });
793
+ it('keeps local and cloud MCP profiles separate when they share the same name', () => {
794
+ const projectRoot = createProjectRoot();
795
+ createdRoots.push(projectRoot);
796
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
797
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
798
+ const cloud = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
799
+ const local = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
800
+ expect(local.created).toBe(true);
801
+ expect(local.profile.id).not.toBe(cloud.profile.id);
802
+ expect(local.profile.seatScope).toBe('local_unmetered');
803
+ expect(cloud.profile.seatScope).toBe('cloud_metered');
804
+ expect(core.countAiProfileUsage('workspace-1')).toEqual({
805
+ used: 1,
806
+ limit: null,
807
+ remaining: null
808
+ });
809
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(2);
810
+ });
811
+ it('claims a legacy unscoped profile for the trusted runtime instead of creating a duplicate', () => {
812
+ const projectRoot = createProjectRoot();
813
+ createdRoots.push(projectRoot);
814
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
815
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
816
+ const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex' });
817
+ expect(legacy.profile.seatScope ?? null).toBeNull();
818
+ const claimed = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'local_unmetered' });
819
+ expect(claimed.created).toBe(false);
820
+ expect(claimed.profile.id).toBe(legacy.profile.id);
821
+ expect(claimed.profile.seatScope).toBe('local_unmetered');
822
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
823
+ });
824
+ it('reactivates a retired legacy unscoped profile for the trusted runtime instead of recreating it', () => {
825
+ const projectRoot = createProjectRoot();
826
+ createdRoots.push(projectRoot);
827
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
828
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
829
+ const legacy = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex' });
830
+ expect(legacy.profile.seatScope ?? null).toBeNull();
831
+ core.archiveAiProfile({
832
+ workspaceId: 'workspace-1',
833
+ profileId: legacy.profile.id,
834
+ archivedBy: 'owner-1',
835
+ reason: 'manual_archive'
836
+ });
837
+ const reactivated = core.resolveAiProfile({ workspaceId: 'workspace-1', name: 'Retired Legacy Codex', surfaceType: 'coding_tool' }, { trustedSeatScope: 'cloud_metered' });
838
+ expect(reactivated.created).toBe(false);
839
+ expect(reactivated.profile.id).toBe(legacy.profile.id);
840
+ expect(reactivated.profile.archivedAt).toBeNull();
841
+ expect(reactivated.profile.seatScope).toBe('cloud_metered');
842
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
843
+ });
571
844
  it('merges two duplicate profiles, reassigning task assignees and comment authors', () => {
572
845
  const projectRoot = createProjectRoot();
573
846
  createdRoots.push(projectRoot);
@@ -593,16 +866,302 @@ describe('TaskforceCore ai profiles', () => {
593
866
  mergeId: second.profile.id
594
867
  });
595
868
  expect(merged.id).toBe(first.profile.id);
596
- // Duplicate profile should be gone
869
+ // Duplicate profile should leave the active roster but remain archived for history and sync.
597
870
  const profiles = core.listAiProfiles('workspace-1');
598
871
  expect(profiles.some((p) => p.id === second.profile.id)).toBe(false);
599
872
  expect(profiles.some((p) => p.id === first.profile.id)).toBe(true);
873
+ const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
874
+ const archivedDuplicate = archivedProfiles.find((p) => p.id === second.profile.id);
875
+ expect(archivedDuplicate).toMatchObject({
876
+ id: second.profile.id,
877
+ archivedReason: 'merged_duplicate',
878
+ mergedIntoProfileId: first.profile.id,
879
+ archivedBy: first.profile.id
880
+ });
881
+ expect(archivedDuplicate?.archivedAt).toBeTruthy();
600
882
  // Task assignee should point to the kept profile
601
883
  const updatedTask = core.getTask(task.id, 'workspace-1');
602
884
  expect(updatedTask?.assignee).toBe(first.profile.id);
603
885
  // Comment author should point to the kept profile
604
886
  expect(updatedTask?.comments?.[0]?.author).toBe(first.profile.id);
605
887
  });
888
+ it('archives AI profiles out of the active roster while preserving history and freeing seats', () => {
889
+ const projectRoot = createProjectRoot();
890
+ createdRoots.push(projectRoot);
891
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
892
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
893
+ enableAiProfileSeatLimit(core, 'workspace-1', 'owner-1', 1);
894
+ const first = core.resolveAiProfile({
895
+ workspaceId: 'workspace-1',
896
+ name: 'Codex',
897
+ surfaceType: 'coding_tool'
898
+ });
899
+ const archived = core.archiveAiProfile({
900
+ workspaceId: 'workspace-1',
901
+ profileId: first.profile.id,
902
+ archivedBy: 'owner-1',
903
+ reason: 'manual_archive'
904
+ });
905
+ expect(archived.id).toBe(first.profile.id);
906
+ expect(archived.archivedReason).toBe('manual_archive');
907
+ expect(archived.archivedBy).toBe('owner-1');
908
+ expect(archived.archivedAt).toBeTruthy();
909
+ expect(core.listAiProfiles('workspace-1')).toEqual([]);
910
+ expect(core.listAssignableActors('workspace-1').some((actor) => actor.id === first.profile.id)).toBe(false);
911
+ const historicalActor = core.resolveWorkspaceActor('workspace-1', first.profile.id);
912
+ expect(historicalActor?.id).toBe(first.profile.id);
913
+ expect(historicalActor?.label).toBe('Codex');
914
+ expect(historicalActor?.meta).toEqual(expect.objectContaining({
915
+ archivedReason: 'manual_archive'
916
+ }));
917
+ const second = core.resolveAiProfile({
918
+ workspaceId: 'workspace-1',
919
+ name: 'Claude',
920
+ surfaceType: 'coding_tool'
921
+ });
922
+ expect(second.created).toBe(true);
923
+ const archivedProfiles = core.listAiProfiles('workspace-1', { includeArchived: true });
924
+ expect(archivedProfiles.map((profile) => profile.name)).toEqual(['Claude', 'Codex']);
925
+ const archivedEntry = archivedProfiles.find((profile) => profile.id === first.profile.id);
926
+ expect(archivedEntry?.archivedReason).toBe('manual_archive');
927
+ expect(archivedEntry?.rosterStateUpdatedAt).toBe(archivedEntry?.archivedAt);
928
+ });
929
+ it('keeps an archived AI profile archived when sync replays an equal-watermark active copy', () => {
930
+ const projectRoot = createProjectRoot();
931
+ createdRoots.push(projectRoot);
932
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
933
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
934
+ const resolved = core.resolveAiProfile({
935
+ workspaceId: 'workspace-1',
936
+ name: 'Codex T-714 Verifier',
937
+ surfaceType: 'coding_tool'
938
+ });
939
+ const archived = core.archiveAiProfile({
940
+ workspaceId: 'workspace-1',
941
+ profileId: resolved.profile.id,
942
+ archivedBy: 'owner-1',
943
+ reason: 'manual_archive'
944
+ });
945
+ const replayedActive = core.upsertAiProfileForSync({
946
+ ...archived,
947
+ archivedAt: null,
948
+ archivedBy: null,
949
+ archivedReason: null,
950
+ mergedIntoProfileId: null,
951
+ updatedAt: archived.updatedAt
952
+ });
953
+ expect(replayedActive.archivedAt).toBeTruthy();
954
+ expect(core.listAiProfiles('workspace-1')).toEqual([]);
955
+ const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
956
+ .find((profile) => profile.id === archived.id);
957
+ expect(stored?.archivedAt).toBe(archived.archivedAt);
958
+ expect(stored?.archivedReason).toBe('manual_archive');
959
+ });
960
+ it('accepts newer synced seat scope for the same AI profile id', () => {
961
+ const projectRoot = createProjectRoot();
962
+ createdRoots.push(projectRoot);
963
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
964
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
965
+ const contaminated = core.resolveAiProfile({
966
+ workspaceId: 'workspace-1',
967
+ name: 'Codex',
968
+ surfaceType: 'coding_tool'
969
+ }, { trustedSeatScope: 'cloud_metered' });
970
+ const repairedAt = new Date(Date.parse(contaminated.profile.updatedAt) + 1000).toISOString();
971
+ const repaired = core.upsertAiProfileForSync({
972
+ ...contaminated.profile,
973
+ seatScope: 'local_unmetered',
974
+ updatedAt: repairedAt,
975
+ lastActiveAt: repairedAt
976
+ });
977
+ expect(repaired.id).toBe(contaminated.profile.id);
978
+ expect(repaired.seatScope).toBe('local_unmetered');
979
+ const staleReplay = core.upsertAiProfileForSync({
980
+ ...contaminated.profile,
981
+ seatScope: 'cloud_metered',
982
+ updatedAt: contaminated.profile.updatedAt
983
+ });
984
+ expect(staleReplay.seatScope).toBe('local_unmetered');
985
+ });
986
+ it('keeps an archived AI profile archived when sync replays a newer metadata update with an older lifecycle timestamp', () => {
987
+ const projectRoot = createProjectRoot();
988
+ createdRoots.push(projectRoot);
989
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
990
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
991
+ const resolved = core.resolveAiProfile({
992
+ workspaceId: 'workspace-1',
993
+ name: 'Antigravity/jCodeMunch',
994
+ surfaceType: 'coding_tool'
995
+ });
996
+ const archived = core.archiveAiProfile({
997
+ workspaceId: 'workspace-1',
998
+ profileId: resolved.profile.id,
999
+ archivedBy: 'owner-1',
1000
+ reason: 'manual_archive'
1001
+ });
1002
+ const replayedActive = core.upsertAiProfileForSync({
1003
+ ...archived,
1004
+ description: 'Cloud-side metadata changed after the archive write was missed.',
1005
+ archivedAt: null,
1006
+ archivedBy: null,
1007
+ archivedReason: null,
1008
+ mergedIntoProfileId: null,
1009
+ rosterStateUpdatedAt: resolved.profile.rosterStateUpdatedAt,
1010
+ updatedAt: '2099-03-22T10:30:00.000Z'
1011
+ });
1012
+ expect(replayedActive.archivedAt).toBeTruthy();
1013
+ const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
1014
+ .find((profile) => profile.id === archived.id);
1015
+ expect(stored?.archivedAt).toBe(archived.archivedAt);
1016
+ expect(stored?.description).toBe('Cloud-side metadata changed after the archive write was missed.');
1017
+ });
1018
+ it('repairs stale archived roster lifecycle timestamps before applying active sync snapshots', () => {
1019
+ const projectRoot = createProjectRoot();
1020
+ createdRoots.push(projectRoot);
1021
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1022
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1023
+ const resolved = core.resolveAiProfile({
1024
+ workspaceId: 'workspace-1',
1025
+ name: 'Codex',
1026
+ surfaceType: 'coding_tool'
1027
+ });
1028
+ const archived = core.archiveAiProfile({
1029
+ workspaceId: 'workspace-1',
1030
+ profileId: resolved.profile.id,
1031
+ archivedBy: 'owner-1',
1032
+ reason: 'manual_archive'
1033
+ });
1034
+ core.db.prepare(`
1035
+ UPDATE ai_profiles
1036
+ SET roster_state_updated_at = ?
1037
+ WHERE tenant_id = ? AND id = ?
1038
+ `).run(resolved.profile.createdAt, 'default', archived.id);
1039
+ const replayedActive = core.upsertAiProfileForSync({
1040
+ ...archived,
1041
+ description: 'Cloud active snapshot with newer metadata but no lifecycle marker.',
1042
+ archivedAt: null,
1043
+ archivedBy: null,
1044
+ archivedReason: null,
1045
+ mergedIntoProfileId: null,
1046
+ rosterStateUpdatedAt: null,
1047
+ updatedAt: '2099-03-22T10:30:00.000Z'
1048
+ });
1049
+ expect(replayedActive.archivedAt).toBe(archived.archivedAt);
1050
+ expect(replayedActive.rosterStateUpdatedAt).toBe(archived.archivedAt);
1051
+ const stored = core.listAiProfiles('workspace-1', { includeArchived: true })
1052
+ .find((profile) => profile.id === archived.id);
1053
+ expect(stored?.archivedAt).toBe(archived.archivedAt);
1054
+ expect(stored?.description).toBe('Cloud active snapshot with newer metadata but no lifecycle marker.');
1055
+ expect(core.listAiProfiles('workspace-1')).toEqual([]);
1056
+ });
1057
+ it('allows sync to reactivate an archived AI profile when lifecycle timestamp is newer', () => {
1058
+ const projectRoot = createProjectRoot();
1059
+ createdRoots.push(projectRoot);
1060
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1061
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1062
+ const resolved = core.resolveAiProfile({
1063
+ workspaceId: 'workspace-1',
1064
+ name: 'Claude Code',
1065
+ surfaceType: 'coding_tool'
1066
+ });
1067
+ const archived = core.archiveAiProfile({
1068
+ workspaceId: 'workspace-1',
1069
+ profileId: resolved.profile.id,
1070
+ archivedBy: 'owner-1',
1071
+ reason: 'manual_archive'
1072
+ });
1073
+ const reactivated = core.upsertAiProfileForSync({
1074
+ ...archived,
1075
+ archivedAt: null,
1076
+ archivedBy: null,
1077
+ archivedReason: null,
1078
+ mergedIntoProfileId: null,
1079
+ rosterStateUpdatedAt: '2099-03-22T10:45:00.000Z',
1080
+ updatedAt: '2099-03-22T10:45:00.000Z'
1081
+ });
1082
+ expect(reactivated.archivedAt).toBeNull();
1083
+ expect(core.listAiProfiles('workspace-1').map((profile) => profile.id)).toEqual([resolved.profile.id]);
1084
+ });
1085
+ it('reactivates an archived AI profile when resolveAiProfile is called explicitly', () => {
1086
+ const projectRoot = createProjectRoot();
1087
+ createdRoots.push(projectRoot);
1088
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1089
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1090
+ const initial = core.resolveAiProfile({
1091
+ workspaceId: 'workspace-1',
1092
+ name: 'Codex',
1093
+ surfaceType: 'coding_tool',
1094
+ }, { trustedSeatScope: 'cloud_metered' });
1095
+ core.archiveAiProfile({
1096
+ workspaceId: 'workspace-1',
1097
+ profileId: initial.profile.id,
1098
+ archivedBy: 'owner-1',
1099
+ reason: 'manual_archive'
1100
+ });
1101
+ const reactivatedByName = core.resolveAiProfile({
1102
+ workspaceId: 'workspace-1',
1103
+ name: 'Codex',
1104
+ surfaceType: 'coding_tool'
1105
+ }, { trustedSeatScope: 'cloud_metered' });
1106
+ expect(reactivatedByName.created).toBe(false);
1107
+ expect(reactivatedByName.profile.id).toBe(initial.profile.id);
1108
+ expect(reactivatedByName.profile.archivedAt).toBeNull();
1109
+ expect(reactivatedByName.profile.archivedReason).toBeNull();
1110
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1111
+ core.archiveAiProfile({
1112
+ workspaceId: 'workspace-1',
1113
+ profileId: initial.profile.id,
1114
+ archivedBy: 'owner-1',
1115
+ reason: 'manual_archive'
1116
+ });
1117
+ const reactivatedByToken = core.resolveAiProfile({
1118
+ workspaceId: 'workspace-1',
1119
+ name: 'Codex',
1120
+ profileToken: initial.profile.profileToken,
1121
+ surfaceType: 'coding_tool'
1122
+ }, { trustedSeatScope: 'cloud_metered' });
1123
+ expect(reactivatedByToken.created).toBe(false);
1124
+ expect(reactivatedByToken.profile.id).toBe(initial.profile.id);
1125
+ expect(reactivatedByToken.profile.profileToken).toBe(initial.profile.profileToken);
1126
+ expect(reactivatedByToken.profile.archivedAt).toBeNull();
1127
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1128
+ });
1129
+ it('rejects archiving an AI profile that still owns open tasks', () => {
1130
+ const projectRoot = createProjectRoot();
1131
+ createdRoots.push(projectRoot);
1132
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
1133
+ core.createWorkspace({ workspaceId: 'workspace-1', name: 'Workspace 1' });
1134
+ const profile = core.resolveAiProfile({
1135
+ workspaceId: 'workspace-1',
1136
+ name: 'Antigravity',
1137
+ surfaceType: 'agent'
1138
+ });
1139
+ core.createTask({
1140
+ title: 'Still assigned',
1141
+ status: 'in-progress',
1142
+ assignee: profile.profile.id
1143
+ }, { workspaceId: 'workspace-1' });
1144
+ try {
1145
+ core.archiveAiProfile({
1146
+ workspaceId: 'workspace-1',
1147
+ profileId: profile.profile.id,
1148
+ archivedBy: 'owner-1',
1149
+ reason: 'manual_archive'
1150
+ });
1151
+ expect.unreachable('Expected archiveAiProfile to reject open-task ownership');
1152
+ }
1153
+ catch (error) {
1154
+ expect(error.code).toBe('AI_PROFILE_HAS_OPEN_TASKS');
1155
+ expect(error.statusCode).toBe(409);
1156
+ expect(error.details).toEqual({
1157
+ workspaceId: 'workspace-1',
1158
+ profileId: profile.profile.id,
1159
+ openTaskCount: 1
1160
+ });
1161
+ }
1162
+ expect(core.listAiProfiles('workspace-1')).toHaveLength(1);
1163
+ expect(core.listAiProfiles('workspace-1', { includeArchived: true })[0]?.archivedAt).toBeNull();
1164
+ });
606
1165
  it('canonicalizes AI names to profile ids when creating tasks and comments', () => {
607
1166
  const projectRoot = createProjectRoot();
608
1167
  createdRoots.push(projectRoot);
@@ -658,7 +1217,7 @@ describe('TaskforceCore ai profiles', () => {
658
1217
  const reloadedCore = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
659
1218
  const reloaded = reloadedCore.getTask(task.id, 'workspace-1');
660
1219
  const profiles = reloadedCore.listAiProfiles('workspace-1');
661
- expect(profiles.map((profile) => profile.name)).toEqual(['Claude', 'Antigravity']);
1220
+ expect(profiles.map((profile) => profile.name)).toEqual(['Antigravity', 'Claude']);
662
1221
  const survivingAntigravityId = profiles.find((profile) => profile.name === 'Antigravity')?.id;
663
1222
  expect(reloaded?.createdBy).toBe(claude.profile.id);
664
1223
  expect(reloaded?.assignee).toBe(survivingAntigravityId);