@taskforcehq/taskforce 0.3.304 → 0.3.305

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 (138) hide show
  1. package/README.md +140 -247
  2. package/dist/TaskforceCore.js +165 -70
  3. package/dist/TaskforceCore.test.js +799 -90
  4. package/dist/compat/workspaceSyncCompat.js +6 -0
  5. package/dist/components/features/DocumentIndex.d.ts +1 -1
  6. package/dist/components/features/DocumentIndex.js +1 -1
  7. package/dist/components/features/DocumentViewer.d.ts +1 -1
  8. package/dist/components/features/DocumentViewer.js +32 -9
  9. package/dist/components/features/DocumentWorkspace.d.ts +5 -22
  10. package/dist/components/features/DocumentWorkspace.js +12 -155
  11. package/dist/components/features/DocumentWorkspace.test.js +193 -6
  12. package/dist/components/features/TaskSettings.js +29 -3
  13. package/dist/components/features/TaskSettings.test.js +42 -1
  14. package/dist/components/features/documentWorkspaceModel.d.ts +24 -0
  15. package/dist/components/features/documentWorkspaceModel.js +57 -0
  16. package/dist/components/features/useDocumentWorkspaceController.d.ts +34 -0
  17. package/dist/components/features/useDocumentWorkspaceController.js +113 -0
  18. package/dist/components/task/TaskForm.d.ts +8 -8
  19. package/dist/components/task/TaskForm.js +358 -221
  20. package/dist/components/task/TaskForm.test.js +456 -381
  21. package/dist/components/task/TaskKanban.js +2 -1
  22. package/dist/components/views/PlanComparisonPage.js +5 -8
  23. package/dist/components/views/PlanComparisonPage.test.js +51 -41
  24. package/dist/components/views/PlansPage.js +126 -84
  25. package/dist/components/views/PlansPage.test.js +308 -39
  26. package/dist/components/views/StandaloneLayout.js +28 -22
  27. package/dist/components/views/panels/FilterToolbar.test.js +6 -4
  28. package/dist/components/views/standalone/modals/AccountHubModal.d.ts +2 -14
  29. package/dist/components/views/standalone/modals/AccountHubModal.js +12 -14
  30. package/dist/components/views/standalone/modals/AccountHubModal.test.js +24 -1
  31. package/dist/config/envSchema.js +1 -1
  32. package/dist/core/AiProfileService.d.ts +6 -1
  33. package/dist/core/AiProfileService.js +161 -16
  34. package/dist/core/AiProfileService.test.js +3 -1
  35. package/dist/core/AiProfiles.test.js +200 -0
  36. package/dist/core/AuthTokenService.test.d.ts +1 -0
  37. package/dist/core/AuthTokenService.test.js +78 -0
  38. package/dist/core/EntitlementsPolicy.test.js +75 -13
  39. package/dist/core/PlanEntitlementService.d.ts +7 -1
  40. package/dist/core/PlanEntitlementService.js +117 -42
  41. package/dist/core/PlanVersionPolicy.test.js +19 -26
  42. package/dist/core/SignupMonetizationSettings.test.js +46 -21
  43. package/dist/core/SystemAdmin.test.js +212 -56
  44. package/dist/core/TaskTaxonomyValidation.test.js +53 -0
  45. package/dist/core/Taskforce.d.ts +3 -0
  46. package/dist/core/Taskforce.js +50 -10
  47. package/dist/core/WorkspacePlanMode.test.js +12 -12
  48. package/dist/core/shared.d.ts +2 -0
  49. package/dist/core/shared.js +11 -0
  50. package/dist/hooks/sync/auth.js +3 -1
  51. package/dist/hooks/sync/transfers.d.ts +2 -1
  52. package/dist/hooks/sync/transfers.js +8 -4
  53. package/dist/hooks/useSyncOrchestrator.d.ts +7 -4
  54. package/dist/hooks/useSyncOrchestrator.js +75 -7
  55. package/dist/hooks/useSyncOrchestrator.retry-closure.test.js +80 -10
  56. package/dist/hooks/useTaskMutations.d.ts +2 -1
  57. package/dist/hooks/useTaskMutations.js +12 -1
  58. package/dist/hooks/useTaskMutations.test.js +30 -1
  59. package/dist/hooks/useTaskforce.d.ts +5 -2
  60. package/dist/hooks/useTaskforce.js +17 -5
  61. package/dist/hooks/useTaskforce.sync-behavior.test.js +78 -0
  62. package/dist/hooks/useWorkspaceSyncController.d.ts +3 -1
  63. package/dist/hooks/useWorkspaceSyncController.js +11 -1
  64. package/dist/hooks/useWorkspaceSyncController.test.js +1 -0
  65. package/dist/mcp/adminWorkspaceRegistrar.d.ts +2 -0
  66. package/dist/mcp/adminWorkspaceRegistrar.js +154 -0
  67. package/dist/mcp/collaborationRegistrar.d.ts +2 -0
  68. package/dist/mcp/collaborationRegistrar.js +71 -0
  69. package/dist/mcp/documentAssetRegistrar.d.ts +2 -0
  70. package/dist/mcp/documentAssetRegistrar.js +346 -0
  71. package/dist/mcp/runtime.js +2362 -3530
  72. package/dist/mcp/runtime.test.js +159 -0
  73. package/dist/mcp/taskPlanningRegistrar.d.ts +2 -0
  74. package/dist/mcp/taskPlanningRegistrar.js +418 -0
  75. package/dist/mcp/toolCatalog.d.ts +35 -0
  76. package/dist/mcp/toolCatalog.js +3 -0
  77. package/dist/server/routes/admin.d.ts +1 -5
  78. package/dist/server/routes/admin.js +138 -61
  79. package/dist/server/routes/annotatedAttachments.d.ts +1 -1
  80. package/dist/server/routes/annotatedAttachments.js +1 -1
  81. package/dist/server/routes/auth.d.ts +1 -4
  82. package/dist/server/routes/auth.js +23 -63
  83. package/dist/server/routes/authSupport.d.ts +30 -0
  84. package/dist/server/routes/authSupport.js +81 -0
  85. package/dist/server/routes/billing.d.ts +1 -1
  86. package/dist/server/routes/billing.js +104 -188
  87. package/dist/server/routes/billing.test.js +214 -86
  88. package/dist/server/routes/documentReviews.d.ts +1 -1
  89. package/dist/server/routes/documentReviews.js +1 -1
  90. package/dist/server/routes/documents.d.ts +4 -2
  91. package/dist/server/routes/documents.js +73 -7
  92. package/dist/server/routes/primitives.d.ts +11 -0
  93. package/dist/server/routes/primitives.js +73 -0
  94. package/dist/server/routes/resources.d.ts +1 -1
  95. package/dist/server/routes/resources.js +1 -1
  96. package/dist/server/routes/shared.d.ts +1 -1
  97. package/dist/server/routes/sync.d.ts +1 -1
  98. package/dist/server/routes/sync.js +1 -1
  99. package/dist/server/routes/tasks.d.ts +1 -1
  100. package/dist/server/routes/tasks.js +1 -1
  101. package/dist/server/routes/workspaces.d.ts +1 -1
  102. package/dist/server/routes/workspaces.js +1 -1
  103. package/dist/server/routes.d.ts +2 -9
  104. package/dist/server/routes.js +76 -162
  105. package/dist/server/routes.test.js +672 -30
  106. package/dist/sync/contentSyncState.js +16 -7
  107. package/dist/sync/syncApplyHandlers.test.js +2 -0
  108. package/dist/sync/syncService.d.ts +1 -0
  109. package/dist/sync/syncService.js +8 -0
  110. package/dist/sync/syncService.test.js +14 -0
  111. package/dist/sync/workspaceSyncModel.d.ts +3 -0
  112. package/dist/sync/workspaceSyncModel.js +12 -3
  113. package/dist/sync/workspaceSyncModel.test.js +34 -0
  114. package/dist/sync/workspaceSyncState.d.ts +2 -0
  115. package/dist/sync/workspaceSyncState.js +1 -0
  116. package/dist/ui/assets/{AgentsModule-BLWVbuKP.js → AgentsModule-N2MnQBZk.js} +1 -1
  117. package/dist/ui/assets/{AnnotatedAttachmentWorkspace-BlS-cqnl.js → AnnotatedAttachmentWorkspace-BG6P_GVW.js} +1 -1
  118. package/dist/ui/assets/DocumentWorkspace-B3nV_Gc5.css +1 -0
  119. package/dist/ui/assets/DocumentWorkspace-DRbDMwh8.js +252 -0
  120. package/dist/ui/assets/{InitiativesModule-BjR6iBf9.js → InitiativesModule-Dhm7M8e7.js} +1 -1
  121. package/dist/ui/assets/PlansPage-Cq0zXj3I.js +1 -0
  122. package/dist/ui/assets/TaskSettings-ln0aF7xc.js +9 -0
  123. package/dist/ui/assets/{WorkflowsModule-DnFqdUME.js → WorkflowsModule-BcS4afRn.js} +1 -1
  124. package/dist/ui/assets/{index-Ii0B0rTO.css → index-C2-OXZV7.css} +1 -1
  125. package/dist/ui/assets/index-DcSI5F86.js +6 -0
  126. package/dist/ui/assets/{vendor-icons-I7ZwG1z_.js → vendor-icons-BSUaRwF8.js} +1 -1
  127. package/dist/ui/index.html +3 -3
  128. package/dist/utils/commercialLifecyclePresentation.d.ts +19 -0
  129. package/dist/utils/commercialLifecyclePresentation.js +196 -0
  130. package/package.json +8 -1
  131. package/scripts/check-cloud-mcp.mjs +83 -0
  132. package/scripts/playwright-auth.sh +5 -1
  133. package/scripts/run-billing-performance-smoke.sh +24 -0
  134. package/dist/ui/assets/DocumentWorkspace-BH_6DF6x.js +0 -250
  135. package/dist/ui/assets/DocumentWorkspace-C_8T8oz-.css +0 -1
  136. package/dist/ui/assets/PlansPage-CSDQ4sLa.js +0 -1
  137. package/dist/ui/assets/TaskSettings-CEDy34Jo.js +0 -9
  138. package/dist/ui/assets/index-DXUdtH1B.js +0 -6
@@ -348,6 +348,59 @@ describe('TaskforceCore taxonomy validation', () => {
348
348
  core.close();
349
349
  fs.rmSync(projectRoot, { recursive: true, force: true });
350
350
  });
351
+ it('migrates legacy config-backed taxonomy data once and no longer depends on config for runtime state', () => {
352
+ const projectRoot = makeTempProjectRoot();
353
+ fs.mkdirSync(path.join(projectRoot, '.taskforce'), { recursive: true });
354
+ const configPath = path.join(projectRoot, '.taskforce', 'config.json');
355
+ fs.writeFileSync(configPath, JSON.stringify({
356
+ categories: [{ value: 'legacy', label: 'Legacy' }],
357
+ types: [{ value: 'legacy-type', label: 'Legacy Type' }],
358
+ priorities: [{ value: 9, label: 'Legacy Priority', color: 'red-500', icon: 'AlertTriangle' }],
359
+ taxonomies: [{ id: 'legacy-taxonomy', label: 'Legacy Taxonomy', widgetType: 'select', isSystem: false, multiSelect: false, options: [] }]
360
+ }, null, 2));
361
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
362
+ const migratedState = core.getTaxonomyState('default');
363
+ expect((migratedState.categories || []).map((category) => (typeof category === 'string' ? category : category.value))).toEqual(['default', 'legacy']);
364
+ expect((migratedState.types || []).map((type) => type.value)).toEqual(['legacy-type']);
365
+ expect((migratedState.priorities || []).map((priority) => priority.value)).toEqual([9]);
366
+ expect((migratedState.taxonomies || []).map((taxonomy) => taxonomy.id)).toEqual(['legacy-taxonomy']);
367
+ core.close();
368
+ fs.writeFileSync(configPath, JSON.stringify({ checklistDropdownEnabled: true }, null, 2));
369
+ const reopened = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
370
+ const persistedState = reopened.getTaxonomyState('default');
371
+ expect((persistedState.categories || []).map((category) => (typeof category === 'string' ? category : category.value))).toEqual(['default', 'legacy']);
372
+ expect((persistedState.types || []).map((type) => type.value)).toEqual(['legacy-type']);
373
+ expect((persistedState.priorities || []).map((priority) => priority.value)).toEqual([9]);
374
+ expect((persistedState.taxonomies || []).map((taxonomy) => taxonomy.id)).toEqual(['legacy-taxonomy']);
375
+ reopened.close();
376
+ fs.rmSync(projectRoot, { recursive: true, force: true });
377
+ });
378
+ it('does not seed new workspaces from legacy config-backed taxonomy definitions', () => {
379
+ const projectRoot = makeTempProjectRoot();
380
+ fs.mkdirSync(path.join(projectRoot, '.taskforce'), { recursive: true });
381
+ const configPath = path.join(projectRoot, '.taskforce', 'config.json');
382
+ fs.writeFileSync(configPath, JSON.stringify({
383
+ categories: [{ value: 'legacy', label: 'Legacy' }],
384
+ types: [{ value: 'legacy-type', label: 'Legacy Type' }],
385
+ priorities: [{ value: 9, label: 'Legacy Priority' }],
386
+ taxonomies: [{ id: 'legacy-taxonomy', label: 'Legacy Taxonomy' }]
387
+ }, null, 2));
388
+ const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
389
+ core.createWorkspace({ workspaceId: 'workspace-new', name: 'New Workspace' });
390
+ const state = core.getTaxonomyState('workspace-new');
391
+ expect((state.categories || []).map((category) => (typeof category === 'string' ? category : category.value))).toEqual(['default']);
392
+ expect((state.types || []).map((type) => type.value)).toEqual([
393
+ 'task',
394
+ 'deliverable',
395
+ 'issue',
396
+ 'idea',
397
+ 'review'
398
+ ]);
399
+ expect((state.priorities || []).map((priority) => priority.value)).toEqual([1, 2, 3]);
400
+ expect(state.taxonomies || []).toEqual([]);
401
+ core.close();
402
+ fs.rmSync(projectRoot, { recursive: true, force: true });
403
+ });
351
404
  it('preserves existing first-class taxonomy sections when taxonomy state updates are partial', () => {
352
405
  const projectRoot = makeTempProjectRoot();
353
406
  const core = new TaskforceCore({ projectRoot, storagePath: '.taskforce' });
@@ -911,6 +911,7 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
911
911
  getAccountAccessStatus(accountId: string): import("./types.js").AccountAccessStatus;
912
912
  getEffectivePlanVersion(accountId: string): import("./types.js").PlanVersionSnapshot;
913
913
  getAccountEntitlement(accountId: string): import("./types.js").AccountEntitlementSnapshot;
914
+ countAiProfileUsage(workspaceId: string): import("./types.js").SeatUsageSnapshot;
914
915
  countWorkspaceSeatUsage(workspaceId: string, options?: {
915
916
  includeInvited?: boolean;
916
917
  }): import("./types.js").SeatUsageSnapshot;
@@ -968,6 +969,8 @@ export declare class TaskforceCore implements TaskforceSyncCapable {
968
969
  private resolveDefaultTypeValue;
969
970
  private getLegacyTaxonomiesConfig;
970
971
  private getLegacyPrioritiesConfig;
972
+ private readLegacyWorkspaceTaxonomyStateFromConfig;
973
+ private migrateLegacyWorkspaceTaxonomyStateIfNeeded;
971
974
  private buildEmergencyWorkspaceTaxonomyState;
972
975
  private getConfiguredDefaultStarterTaxonomyPackId;
973
976
  private getResolvedStarterWorkspaceTaxonomyState;
@@ -79,7 +79,7 @@ const DEFAULT_PLAN_FEATURE_CATALOG = [
79
79
  featureKey: DOCUMENT_CLOUD_STORAGE_FEATURE_KEY,
80
80
  label: 'Document Cloud Storage',
81
81
  description: 'Allow cloud document storage with an optional storage allowance in MB.',
82
- allowedAccessModes: ['enabled', 'disabled'],
82
+ allowedAccessModes: ['enabled', 'limited', 'disabled'],
83
83
  configTemplate: {
84
84
  [DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY]: 1024
85
85
  }
@@ -1261,6 +1261,9 @@ export class TaskforceCore {
1261
1261
  getAccountEntitlement(accountId) {
1262
1262
  return this.planEntitlementService.getAccountEntitlement(accountId);
1263
1263
  }
1264
+ countAiProfileUsage(workspaceId) {
1265
+ return this.planEntitlementService.countAiProfileUsage(workspaceId);
1266
+ }
1264
1267
  countWorkspaceSeatUsage(workspaceId, options = {}) {
1265
1268
  return this.planEntitlementService.countWorkspaceSeatUsage(workspaceId, options);
1266
1269
  }
@@ -2414,6 +2417,42 @@ export class TaskforceCore {
2414
2417
  }
2415
2418
  return [];
2416
2419
  }
2420
+ readLegacyWorkspaceTaxonomyStateFromConfig() {
2421
+ const legacyCategories = this.getLegacyCategoriesConfig();
2422
+ const legacyTypes = this.getLegacyTypesConfig();
2423
+ const legacyPriorities = this.getLegacyPrioritiesConfig();
2424
+ const legacyTaxonomies = this.getLegacyTaxonomiesConfig();
2425
+ if (legacyCategories.length === 0
2426
+ && legacyTypes.length === 0
2427
+ && legacyPriorities.length === 0
2428
+ && legacyTaxonomies.length === 0) {
2429
+ return null;
2430
+ }
2431
+ return normalizeWorkspaceTaxonomyStatePayload({
2432
+ categories: legacyCategories,
2433
+ types: legacyTypes,
2434
+ priorities: legacyPriorities,
2435
+ taxonomies: legacyTaxonomies
2436
+ });
2437
+ }
2438
+ migrateLegacyWorkspaceTaxonomyStateIfNeeded(workspaceId) {
2439
+ const normalizedWorkspaceId = this.normalizeWorkspaceId(workspaceId);
2440
+ if (normalizedWorkspaceId !== 'default') {
2441
+ return null;
2442
+ }
2443
+ const stored = this.readStoredWorkspaceTaxonomyState(normalizedWorkspaceId);
2444
+ if (Object.keys(stored.state).length > 0) {
2445
+ return stored;
2446
+ }
2447
+ const legacyState = this.readLegacyWorkspaceTaxonomyStateFromConfig();
2448
+ if (!legacyState)
2449
+ return null;
2450
+ const persisted = this.writeWorkspaceTaxonomyState(legacyState, normalizedWorkspaceId);
2451
+ return {
2452
+ state: persisted.state,
2453
+ updatedAt: persisted.updatedAt
2454
+ };
2455
+ }
2417
2456
  buildEmergencyWorkspaceTaxonomyState() {
2418
2457
  const emergencyPack = getEmergencyStarterTaxonomyLibraryPack();
2419
2458
  return {
@@ -2454,17 +2493,13 @@ export class TaskforceCore {
2454
2493
  }
2455
2494
  getEffectiveWorkspaceTaxonomyState(workspaceId) {
2456
2495
  const { state } = this.readStoredWorkspaceTaxonomyState(workspaceId);
2457
- const legacyCategories = this.getLegacyCategoriesConfig();
2458
- const legacyTypes = this.getLegacyTypesConfig();
2459
- const legacyPriorities = this.getLegacyPrioritiesConfig();
2460
- const legacyTaxonomies = this.getLegacyTaxonomiesConfig();
2461
2496
  const starterState = this.getResolvedStarterWorkspaceTaxonomyState();
2462
2497
  const emergencyState = this.buildEmergencyWorkspaceTaxonomyState();
2463
2498
  return normalizeWorkspaceTaxonomyStatePayload({
2464
- categories: state.categories || (legacyCategories.length > 0 ? legacyCategories : starterState.categories) || emergencyState.categories,
2465
- types: state.types || (legacyTypes.length > 0 ? legacyTypes : starterState.types) || emergencyState.types,
2466
- priorities: state.priorities || (legacyPriorities.length > 0 ? legacyPriorities : starterState.priorities) || emergencyState.priorities,
2467
- taxonomies: state.taxonomies || (legacyTaxonomies.length > 0 ? legacyTaxonomies : starterState.taxonomies) || emergencyState.taxonomies
2499
+ categories: state.categories || starterState.categories || emergencyState.categories,
2500
+ types: state.types || starterState.types || emergencyState.types,
2501
+ priorities: state.priorities || starterState.priorities || emergencyState.priorities,
2502
+ taxonomies: state.taxonomies || starterState.taxonomies || emergencyState.taxonomies
2468
2503
  });
2469
2504
  }
2470
2505
  ensureWorkspaceTaxonomyStateSeeded(workspaceId) {
@@ -2473,6 +2508,10 @@ export class TaskforceCore {
2473
2508
  if (Object.keys(stored.state).length > 0) {
2474
2509
  return stored;
2475
2510
  }
2511
+ const migratedLegacy = this.migrateLegacyWorkspaceTaxonomyStateIfNeeded(normalizedWorkspaceId);
2512
+ if (migratedLegacy) {
2513
+ return migratedLegacy;
2514
+ }
2476
2515
  const seededState = this.getEffectiveWorkspaceTaxonomyState(normalizedWorkspaceId);
2477
2516
  const persisted = this.writeWorkspaceTaxonomyState(seededState, normalizedWorkspaceId);
2478
2517
  return {
@@ -4912,7 +4951,8 @@ export class TaskforceCore {
4912
4951
  aiProfileRowToRecord: (row) => this.aiProfileRowToRecord(row),
4913
4952
  generateEntityId: (prefix) => this.generateEntityId(prefix),
4914
4953
  issueProfileToken: () => `tfp_${randomBytes(24).toString('hex')}`,
4915
- createRuleError: (message, statusCode = 400, code = 'TASK_RULE_VIOLATION') => new TaskforceRuleError(message, statusCode, code),
4954
+ countAiProfileUsage: (workspaceId) => this.countAiProfileUsage(workspaceId),
4955
+ createRuleError: (message, statusCode = 400, code = 'TASK_RULE_VIOLATION', details) => new TaskforceRuleError(message, statusCode, code, details),
4916
4956
  };
4917
4957
  }
4918
4958
  resolveWorkspaceActor(workspaceId, actorRef) {
@@ -20,30 +20,30 @@ function openCore(projectRoot) {
20
20
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21
21
  `).run('default', planVersionId, 'month', `free:${planVersionId}:month`, 'free', 1, 0, null, now, now);
22
22
  };
23
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter' });
24
- addFreeMapping('starter-v1');
25
- core.upsertPlanCatalog({ planId: 'starter', displayName: 'Starter', status: 'active' });
23
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free' });
24
+ addFreeMapping('free-v1');
25
+ core.upsertPlanCatalog({ planId: 'free', displayName: 'Free', status: 'active' });
26
26
  core.updatePlanVersion({
27
- planId: 'starter',
27
+ planId: 'free',
28
28
  versionNumber: 1,
29
29
  seatLimit: 1
30
30
  });
31
31
  core.updatePlanVersionFeatures({
32
- planVersionId: 'starter-v1',
32
+ planVersionId: 'free-v1',
33
33
  features: [
34
34
  { featureKey: 'workspace.switching', access: 'enabled' }
35
35
  ]
36
36
  });
37
- core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab' });
38
- addFreeMapping('collab-v1');
39
- core.upsertPlanCatalog({ planId: 'collab', displayName: 'Collab', status: 'active' });
37
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5' });
38
+ addFreeMapping('team-5-v1');
39
+ core.upsertPlanCatalog({ planId: 'team-5', displayName: 'Team 5', status: 'active' });
40
40
  core.updatePlanVersion({
41
- planId: 'collab',
41
+ planId: 'team-5',
42
42
  versionNumber: 1,
43
43
  seatLimit: 5
44
44
  });
45
45
  core.updatePlanVersionFeatures({
46
- planVersionId: 'collab-v1',
46
+ planVersionId: 'team-5-v1',
47
47
  features: [
48
48
  { featureKey: 'collaboration.invites', access: 'enabled' },
49
49
  { featureKey: 'collaboration.team_management', access: 'enabled' }
@@ -233,7 +233,7 @@ describe('TaskforceCore account-level plan mode and invite revoke', () => {
233
233
  billing: {
234
234
  onboardingPolicy: {
235
235
  mode: 'auto_grant_free',
236
- planVersionId: 'starter-v1'
236
+ planVersionId: 'free-v1'
237
237
  }
238
238
  }
239
239
  });
@@ -266,7 +266,7 @@ describe('TaskforceCore account-level plan mode and invite revoke', () => {
266
266
  expect(accepted.workspaceId).toBe('workspace-invite');
267
267
  expect(core.hasAccountEntitlement('invited-user')).toBe(true);
268
268
  expect(core.getAccountEntitlement('invited-user')).toEqual(expect.objectContaining({
269
- planVersionId: 'starter-v1',
269
+ planVersionId: 'free-v1',
270
270
  state: 'active'
271
271
  }));
272
272
  expect(core.getAccountAccessStatus('invited-user')).toEqual(expect.objectContaining({
@@ -17,8 +17,10 @@
17
17
  import type { McpAccessTokenScope, PlanFeatureCatalogItem } from './types.js';
18
18
  export declare const DOCUMENT_CLOUD_STORAGE_FEATURE_KEY = "context.uploads";
19
19
  export declare const WORKSPACES_FEATURE_KEY = "workspace.switching";
20
+ export declare const AI_PROFILES_FEATURE_KEY = "collaboration.ai_profiles";
20
21
  export declare const DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY = "storageLimitMb";
21
22
  export declare const WORKSPACES_LIMIT_CONFIG_KEY = "maxWorkspaces";
23
+ export declare const AI_PROFILES_LIMIT_CONFIG_KEY = "maxAiProfiles";
22
24
  export declare const MCP_ACCESS_TOKEN_MAX_ACTIVE_PER_USER_WORKSPACE = 10;
23
25
  export declare const MCP_ACCESS_TOKEN_DEFAULT_PAGE_SIZE = 20;
24
26
  export declare const MCP_ACCESS_TOKEN_MAX_PAGE_SIZE = 100;
@@ -18,8 +18,10 @@ import { createHash, pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto';
18
18
  // === SECTION: Constants ===
19
19
  export const DOCUMENT_CLOUD_STORAGE_FEATURE_KEY = 'context.uploads';
20
20
  export const WORKSPACES_FEATURE_KEY = 'workspace.switching';
21
+ export const AI_PROFILES_FEATURE_KEY = 'collaboration.ai_profiles';
21
22
  export const DOCUMENT_CLOUD_STORAGE_LIMIT_CONFIG_KEY = 'storageLimitMb';
22
23
  export const WORKSPACES_LIMIT_CONFIG_KEY = 'maxWorkspaces';
24
+ export const AI_PROFILES_LIMIT_CONFIG_KEY = 'maxAiProfiles';
23
25
  export const MCP_ACCESS_TOKEN_MAX_ACTIVE_PER_USER_WORKSPACE = 10;
24
26
  export const MCP_ACCESS_TOKEN_DEFAULT_PAGE_SIZE = 20;
25
27
  export const MCP_ACCESS_TOKEN_MAX_PAGE_SIZE = 100;
@@ -50,6 +52,15 @@ export const DEFAULT_PLAN_FEATURE_CATALOG = [
50
52
  configTemplate: {
51
53
  [WORKSPACES_LIMIT_CONFIG_KEY]: 1
52
54
  }
55
+ },
56
+ {
57
+ featureKey: AI_PROFILES_FEATURE_KEY,
58
+ label: 'AI Profiles',
59
+ description: 'Allow registered AI collaborator profiles with an optional per-workspace limit.',
60
+ allowedAccessModes: ['enabled', 'limited', 'disabled'],
61
+ configTemplate: {
62
+ [AI_PROFILES_LIMIT_CONFIG_KEY]: 1
63
+ }
53
64
  }
54
65
  ];
55
66
  export const PLAN_FEATURE_DEPENDENCIES = Object.freeze(DEFAULT_PLAN_FEATURE_CATALOG.reduce((acc, feature) => {
@@ -49,7 +49,9 @@ export function createSyncAuthCoordinator({ currentWorkspaceId, workspaceSyncPha
49
49
  if (ensured.transient || isTransientWorkspaceSyncStatus(ensured.statusCode)) {
50
50
  scheduleWorkspaceSyncRetry(ensured.retryAfterMs);
51
51
  }
52
- else if (workspaceSyncPhase === 'active') {
52
+ else if (workspaceSyncPhase === 'active'
53
+ || workspaceSyncPhase === 'attach-cloud'
54
+ || workspaceSyncPhase === 'provision-local') {
53
55
  persistWorkspaceSyncPatchSafely({ phase: 'error', lastErrorMessage: message });
54
56
  }
55
57
  return false;
@@ -74,6 +74,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
74
74
  workspacePendingSignatureRef: MutableRefObject<string>;
75
75
  workspaceLastPushedTaskIdsRef: MutableRefObject<Set<string>>;
76
76
  workspaceDeletedTaskIdsRef: MutableRefObject<Set<string>>;
77
+ workspaceDeletedTaskWatermarksRef: MutableRefObject<Map<string, string>>;
77
78
  workspacePullCursorRef: MutableRefObject<string | null>;
78
79
  workspaceLastPushedWatermarksRef: MutableRefObject<Map<string, string>>;
79
80
  workspaceLastPushedInitiativeIdsRef: MutableRefObject<Set<string>>;
@@ -109,7 +110,7 @@ interface CreateWorkspaceTransferCoordinatorArgs {
109
110
  setTasks: Dispatch<SetStateAction<any[]>>;
110
111
  setArchivedTasks: Dispatch<SetStateAction<any[]>>;
111
112
  }
112
- export declare function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, taxonomyStateFingerprint, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }: CreateWorkspaceTransferCoordinatorArgs): {
113
+ export declare function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, taxonomyStateFingerprint, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }: CreateWorkspaceTransferCoordinatorArgs): {
113
114
  pushWorkspaceChangesToCloud: (signature: string, options?: {
114
115
  forceAllAiProfiles?: boolean;
115
116
  repairMode?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { applyLocalWorkspaceChanges } from '../../sync/cloudSyncApi';
2
2
  import { isTransientWorkspaceSyncStatus, runWorkspacePullSyncService, runWorkspacePushSyncService } from '../../sync/syncService';
3
3
  import { logSyncDebug, savePersistedSyncWatermarks, isValidWorkspaceKey } from './orchestratorShared';
4
- export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, taxonomyStateFingerprint, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }) {
4
+ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtimeMode, workspaceCloudSyncEnabled, currentWorkspaceId, workspaceSyncPhase, taxonomyStateFingerprint, isAuthenticated, checkAuthSession, resolveCloudAuthUrl, ensureCloudWorkspaceReadyForSync, ensureWorkspaceSyncReady, handleSyncAuthFailure, clearWorkspaceRetry, clearWorkspaceIssue, isWorkspaceRetryPending, scheduleWorkspaceSyncRetry, persistWorkspaceSyncPatchSafely, persistWorkspaceSyncPatchBestEffort, reportSyncEvent, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, reportWorkspaceWindowContention, refreshWorkspaceSyncMarkdownDocumentsSnapshot, refreshWorkspaceSyncAiProfilesSnapshot, refreshWorkspaceSyncAssetsSnapshot, refreshWorkspaceSyncDocumentReviewSessionsSnapshot, refreshWorkspaceSyncAnnotatedAttachmentSessionsSnapshot, refreshLocalWorkspaceTaskCollections, buildWorkspaceSyncPayload, buildWorkspaceSyncSignature, seedWorkspaceSnapshotPushBaseline, shouldSuppressAttachBootstrapPush, recordSuppressedEventIds, workspaceRepairModeRef, workspaceForceFullAiProfilePushRef, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, workspaceLastPushedWatermarksRef, workspaceLastPushedInitiativeIdsRef, workspaceLastPushedInitiativeWatermarksRef, workspaceLastPushedWorkstreamIdsRef, workspaceLastPushedWorkstreamWatermarksRef, workspaceLastPushedAiProfileIdsRef, workspaceLastPushedAiProfileWatermarksRef, workspaceLastPushedDocumentPathsRef, workspaceLastPushedDocumentWatermarksRef, workspaceLastPushedAssetPathsRef, workspaceLastPushedAssetWatermarksRef, workspaceLastPushedDocumentReviewSessionIdsRef, workspaceLastPushedDocumentReviewSessionWatermarksRef, workspaceLastPushedDocumentReviewCommentIdsRef, workspaceLastPushedDocumentReviewCommentWatermarksRef, workspaceLastPushedAnnotatedAttachmentSessionIdsRef, workspaceLastPushedAnnotatedAttachmentSessionWatermarksRef, workspaceLastPushedTaskEventWatermarksRef, workspaceCaptureAttachBootstrapBaselineRef, workspaceStartupFullReconcileRanRef, workspaceFullReconcileInFlightRef, bootstrapLastProgressAtRef, realtimeSuppressedEventQueueRef, setWorkspaceSyncBusy, setUserGlobalSyncStatus, setUserGlobalSyncError, setWorkspaceLastErrorMessage, setWorkspaceLastErrorAt, setWorkspaceLastPullAt, setWorkspaceLastPushAt, setWorkspaceSyncPendingChanges, setTasks, setArchivedTasks }) {
5
5
  const pushWorkspaceChangesToCloud = async (signature, options) => {
6
6
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled)
7
7
  return false;
@@ -115,6 +115,7 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
115
115
  if (pushResult.deleteTaskIds.size > 0) {
116
116
  for (const taskId of pushResult.deleteTaskIds) {
117
117
  workspaceDeletedTaskIdsRef.current.delete(taskId);
118
+ workspaceDeletedTaskWatermarksRef.current.delete(taskId);
118
119
  workspaceLastPushedWatermarksRef.current.delete(taskId);
119
120
  }
120
121
  }
@@ -214,12 +215,15 @@ export function createWorkspaceTransferCoordinator({ cloudAuthConfigured, runtim
214
215
  setWorkspaceSyncBusy(workspacePullInFlightRef.current);
215
216
  releaseWorkspaceSyncLease(currentWorkspaceId);
216
217
  const pendingSignature = workspacePendingSignatureRef.current;
218
+ const latestSignature = buildWorkspaceSyncSignature();
217
219
  if (pendingSignature
218
- && pendingSignature !== workspaceLastPushedSignatureRef.current
219
- && !workspacePullInFlightRef.current) {
220
+ && latestSignature
221
+ && latestSignature !== workspaceLastPushedSignatureRef.current
222
+ && !workspacePullInFlightRef.current
223
+ && !isWorkspaceRetryPending()) {
220
224
  workspacePendingSignatureRef.current = '';
221
225
  window.setTimeout(() => {
222
- void pushWorkspaceChangesToCloud(pendingSignature);
226
+ void pushWorkspaceChangesToCloud(latestSignature);
223
227
  }, 120);
224
228
  }
225
229
  }
@@ -1,5 +1,5 @@
1
- import type { InitiativeItem, TaskItem, TaskforceTheme, WorkstreamItem } from '../types';
2
- import { type WorkspaceSyncPhase } from '../sync/workspaceSyncState';
1
+ import type { DeletedTaskRecord, InitiativeItem, TaskItem, TaskforceTheme, WorkstreamItem } from '../types';
2
+ import { type WorkspaceSyncPhase, type WorkspaceSyncSetupIntent } from '../sync/workspaceSyncState';
3
3
  import type { WorkspaceTaxonomyState } from '../shared/taxonomyState.js';
4
4
  import type { SupportedLocale } from '../localization';
5
5
  import { normalizeWorkspaceSyncAiProfileSnapshotEntry } from './sync/orchestratorShared';
@@ -16,6 +16,7 @@ export interface UseSyncOrchestratorInput {
16
16
  realtimeSyncEnabled: boolean;
17
17
  tasks: TaskItem[];
18
18
  archivedTasks: TaskItem[];
19
+ deletedTasks?: DeletedTaskRecord[];
19
20
  initiatives: InitiativeItem[];
20
21
  workstreams: WorkstreamItem[];
21
22
  taxonomies: unknown[];
@@ -53,6 +54,7 @@ export declare function useSyncOrchestrator(input: UseSyncOrchestratorInput): {
53
54
  workspaceLastSuccessfulSyncAt: string | null;
54
55
  workspaceCloudSyncEnabled: boolean;
55
56
  workspaceSyncPhase: WorkspaceSyncPhase;
57
+ workspaceSyncSetupIntent: WorkspaceSyncSetupIntent;
56
58
  workspaceSyncStatus: WorkspaceSyncStatus;
57
59
  workspaceSyncSummary: string;
58
60
  workspaceSyncRecommendedAction: string;
@@ -67,14 +69,15 @@ export declare function useSyncOrchestrator(input: UseSyncOrchestratorInput): {
67
69
  workspacePendingSignatureRef: import("react").RefObject<string>;
68
70
  workspaceLastPushedTaskIdsRef: import("react").RefObject<Set<string>>;
69
71
  workspaceDeletedTaskIdsRef: import("react").RefObject<Set<string>>;
72
+ workspaceDeletedTaskWatermarksRef: import("react").RefObject<Map<string, string>>;
70
73
  workspacePullCursorRef: import("react").RefObject<string | null>;
71
74
  clearWorkspaceRetry: () => void;
72
75
  persistWorkspaceSyncPatch: (patch: Partial<import("../sync/workspaceSyncState").WorkspaceSyncStateV2>) => Promise<import("../sync/workspaceSyncState").WorkspaceSyncStateV2>;
73
76
  loadWorkspaceSyncState: () => Promise<void>;
74
- applyWorkspaceSyncStateSnapshot: (nextState: Partial<import("../sync/workspaceSyncState").WorkspaceSyncStateV2> & {
77
+ applyWorkspaceSyncStateSnapshot: (nextState: Parameters<(nextState: Partial<import("../sync/workspaceSyncState").WorkspaceSyncStateV2> & {
75
78
  enabled: boolean;
76
79
  phase?: WorkspaceSyncPhase | null;
77
- }) => void;
80
+ }) => void>[0]) => void;
78
81
  syncUserGlobalSettings: (options?: {
79
82
  preferCloudOnFirstSync?: boolean;
80
83
  }) => Promise<void>;
@@ -86,7 +86,7 @@ const EMPTY_WORKSPACE_SYNC_REFERENCE_SNAPSHOT_STATUS = {
86
86
  };
87
87
  export { normalizeWorkspaceSyncAiProfileSnapshotEntry };
88
88
  export function useSyncOrchestrator(input) {
89
- const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, authSessionResolved, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, initiatives, workstreams, taxonomies, taxonomyState, setupState, globalTheme, locale, globalWeekStartsOn, themeUseGlobalDefault, setTasks, setArchivedTasks, setAuthBlocked, setIsAuthenticated, checkAuthSession, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn, fetchPlanningEntities } = input;
89
+ const { currentWorkspaceId, cloudAuthConfigured, runtimeMode, authSessionResolved, isAuthenticated, authUserId, projectName, resolveCloudAuthUrl, resolveWebSocketUrl, realtimeSyncEnabled, tasks, archivedTasks, deletedTasks = [], initiatives, workstreams, taxonomies, taxonomyState, setupState, globalTheme, locale, globalWeekStartsOn, themeUseGlobalDefault, setTasks, setArchivedTasks, setAuthBlocked, setIsAuthenticated, checkAuthSession, setGlobalTheme, setCurrentTheme, setSetupState, setLocale, setGlobalWeekStartsOn, fetchPlanningEntities } = input;
90
90
  const taxonomyStateFingerprint = useMemo(() => buildTaxonomyStateFingerprint(taxonomyState), [taxonomyState]);
91
91
  // === SECTION: State initialization ===
92
92
  const [userGlobalSyncStatus, setUserGlobalSyncStatus] = useState('disconnected');
@@ -97,6 +97,7 @@ export function useSyncOrchestrator(input) {
97
97
  const [workspaceLastErrorMessage, setWorkspaceLastErrorMessage] = useState(null);
98
98
  const [workspaceCloudSyncEnabled, setWorkspaceCloudSyncEnabled] = useState(false);
99
99
  const [workspaceSyncPhase, setWorkspaceSyncPhase] = useState('idle');
100
+ const [workspaceSyncSetupIntent, setWorkspaceSyncSetupIntent] = useState(null);
100
101
  const [workspaceSyncBusy, setWorkspaceSyncBusy] = useState(false);
101
102
  const [workspaceSyncPendingChanges, setWorkspaceSyncPendingChanges] = useState(0);
102
103
  const [workspaceSyncMarkdownDocuments, setWorkspaceSyncMarkdownDocuments] = useState([]);
@@ -253,14 +254,25 @@ export function useSyncOrchestrator(input) {
253
254
  }));
254
255
  }, []);
255
256
  const workspaceSyncReferenceSnapshotsReady = Object.values(workspaceSyncReferenceSnapshotStatus).every(Boolean);
256
- const { workspaceRetryAt, isWorkspaceRetryPending, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspacePullCursorRef, clearWorkspaceRetry, scheduleWorkspaceRetry, persistWorkspaceSyncPatch, applyWorkspaceSyncPatchLocally, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease } = useWorkspaceSyncController({
257
+ const { workspaceRetryAt, isWorkspaceRetryPending, workspacePushInFlightRef, workspacePullInFlightRef, workspaceLastPushedSignatureRef, workspacePendingSignatureRef, workspaceLastPushedTaskIdsRef, workspaceDeletedTaskIdsRef, workspaceDeletedTaskWatermarksRef, workspacePullCursorRef, clearWorkspaceRetry, scheduleWorkspaceRetry, persistWorkspaceSyncPatch, applyWorkspaceSyncPatchLocally, loadWorkspaceSyncState, applyWorkspaceSyncStateSnapshot: applyWorkspaceSyncStateSnapshotBase, saveWorkspaceCloudSyncSettings: saveWorkspaceCloudSyncSettingsBase, acquireWorkspaceSyncLease, releaseWorkspaceSyncLease, forceClearWorkspaceSyncLease } = useWorkspaceSyncController({
257
258
  currentWorkspaceId,
258
259
  setWorkspaceCloudSyncEnabled,
259
260
  setWorkspaceSyncPhase,
261
+ setWorkspaceSyncSetupIntent,
260
262
  setWorkspaceLastPullAt,
261
263
  setWorkspaceLastPushAt,
262
264
  setWorkspaceLastErrorMessage
263
265
  });
266
+ const applyWorkspaceSyncStateSnapshot = useCallback((nextState) => {
267
+ if (nextState.phase === 'attach-cloud' || nextState.phase === 'provision-local') {
268
+ lastBootstrapPhaseRef.current = nextState.phase;
269
+ if (bootstrapPhaseStartedAtRef.current === null) {
270
+ bootstrapPhaseStartedAtRef.current = Date.now();
271
+ }
272
+ bootstrapLastProgressAtRef.current = null;
273
+ }
274
+ applyWorkspaceSyncStateSnapshotBase(nextState);
275
+ }, [applyWorkspaceSyncStateSnapshotBase]);
264
276
  const workspaceLastPushedWatermarksRef = useRef(new Map());
265
277
  const workspaceLastPushedInitiativeIdsRef = useRef(new Set());
266
278
  const workspaceLastPushedInitiativeWatermarksRef = useRef(new Map());
@@ -286,6 +298,7 @@ export function useSyncOrchestrator(input) {
286
298
  const workspaceCaptureAttachBootstrapBaselineRef = useRef(false);
287
299
  const workspaceDocumentsRefreshIntervalMs = 30_000;
288
300
  const workspacePullIntervalMs = realtimeSyncEnabled ? 30_000 : 15_000;
301
+ const workspacePushWatchdogIntervalMs = Math.max(30_000, workspacePullIntervalMs);
289
302
  const workspaceLastSuccessfulSyncAt = useMemo(() => {
290
303
  const pullMs = workspaceLastPullAt ? Date.parse(workspaceLastPullAt) : NaN;
291
304
  const pushMs = workspaceLastPushAt ? Date.parse(workspaceLastPushAt) : NaN;
@@ -560,7 +573,8 @@ export function useSyncOrchestrator(input) {
560
573
  isAuthenticated,
561
574
  resolveCloudAuthUrl,
562
575
  workspaceId: currentWorkspaceId,
563
- workspaceName: projectName || currentWorkspaceId
576
+ workspaceName: projectName || currentWorkspaceId,
577
+ allowProvisionOnMismatch: workspaceSyncPhase !== 'attach-cloud'
564
578
  });
565
579
  }, [
566
580
  cloudAuthConfigured,
@@ -568,7 +582,8 @@ export function useSyncOrchestrator(input) {
568
582
  isAuthenticated,
569
583
  resolveCloudAuthUrl,
570
584
  currentWorkspaceId,
571
- projectName
585
+ projectName,
586
+ workspaceSyncPhase
572
587
  ]);
573
588
  useEffect(() => {
574
589
  workspaceTasksRef.current = tasks || [];
@@ -576,6 +591,14 @@ export function useSyncOrchestrator(input) {
576
591
  useEffect(() => {
577
592
  workspaceArchivedTasksRef.current = archivedTasks || [];
578
593
  }, [archivedTasks]);
594
+ useEffect(() => {
595
+ workspaceDeletedTaskWatermarksRef.current = new Map((deletedTasks || [])
596
+ .map((entry) => [
597
+ String(entry?.taskId || '').trim(),
598
+ String(entry?.deletedAt || '').trim()
599
+ ])
600
+ .filter(([taskId, deletedAt]) => taskId.length > 0 && Number.isFinite(Date.parse(deletedAt))));
601
+ }, [deletedTasks, workspaceDeletedTaskWatermarksRef]);
579
602
  useEffect(() => {
580
603
  workspaceInitiativesRef.current = initiatives || [];
581
604
  }, [initiatives]);
@@ -595,6 +618,7 @@ export function useSyncOrchestrator(input) {
595
618
  taskEvents: flattenTaskEvents(workspaceTasksRef.current, workspaceArchivedTasksRef.current),
596
619
  lastPushedTaskIds: workspaceLastPushedTaskIdsRef.current,
597
620
  pendingDeletedTaskIds: workspaceDeletedTaskIdsRef.current,
621
+ pendingDeletedTaskWatermarks: workspaceDeletedTaskWatermarksRef.current,
598
622
  initiatives: workspaceInitiativesRef.current,
599
623
  workstreams: workspaceWorkstreamsRef.current,
600
624
  taxonomies: taxonomies || [],
@@ -1175,6 +1199,7 @@ export function useSyncOrchestrator(input) {
1175
1199
  workspacePendingSignatureRef,
1176
1200
  workspaceLastPushedTaskIdsRef,
1177
1201
  workspaceDeletedTaskIdsRef,
1202
+ workspaceDeletedTaskWatermarksRef,
1178
1203
  workspacePullCursorRef,
1179
1204
  workspaceLastPushedWatermarksRef,
1180
1205
  workspaceLastPushedInitiativeIdsRef,
@@ -1561,6 +1586,41 @@ export function useSyncOrchestrator(input) {
1561
1586
  workspacePushInFlightRef,
1562
1587
  workspacePullInFlightRef
1563
1588
  ]);
1589
+ useEffect(() => {
1590
+ if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
1591
+ return;
1592
+ if (workspaceSyncPhase !== 'active')
1593
+ return;
1594
+ if (!workspaceSyncReferenceSnapshotsReady)
1595
+ return;
1596
+ const intervalId = window.setInterval(() => {
1597
+ if (workspacePushInFlightRef.current || workspacePullInFlightRef.current)
1598
+ return;
1599
+ if (isWorkspaceRetryPending())
1600
+ return;
1601
+ const signature = workspacePendingSignatureRef.current || buildWorkspaceSyncSignature();
1602
+ if (!signature || signature === workspaceLastPushedSignatureRef.current)
1603
+ return;
1604
+ const pendingPayload = buildWorkspaceSyncPayload();
1605
+ setWorkspaceSyncPendingChanges(pendingPayload.changes.length);
1606
+ if (pendingPayload.changes.length === 0)
1607
+ return;
1608
+ void pushWorkspaceChangesToCloud(signature);
1609
+ }, workspacePushWatchdogIntervalMs);
1610
+ return () => window.clearInterval(intervalId);
1611
+ }, [
1612
+ cloudAuthConfigured,
1613
+ runtimeMode,
1614
+ workspaceCloudSyncEnabled,
1615
+ authSessionResolved,
1616
+ workspaceSyncPhase,
1617
+ workspaceSyncReferenceSnapshotsReady,
1618
+ buildWorkspaceSyncSignature,
1619
+ buildWorkspaceSyncPayload,
1620
+ pushWorkspaceChangesToCloud,
1621
+ isWorkspaceRetryPending,
1622
+ workspacePushWatchdogIntervalMs
1623
+ ]);
1564
1624
  useEffect(() => {
1565
1625
  if (cloudAuthConfigured && runtimeMode === 'local' && workspaceCloudSyncEnabled)
1566
1626
  return;
@@ -1575,21 +1635,27 @@ export function useSyncOrchestrator(input) {
1575
1635
  useEffect(() => {
1576
1636
  if (!cloudAuthConfigured || runtimeMode !== 'local' || !workspaceCloudSyncEnabled || !authSessionResolved)
1577
1637
  return;
1578
- if (workspaceSyncPhase === 'provision-local')
1638
+ if (workspaceSyncPhase === 'provision-local' || workspaceSyncPhase === 'error')
1579
1639
  return;
1580
1640
  if (isWorkspaceRetryPending())
1581
1641
  return;
1582
- void pullWorkspaceChangesFromCloud();
1642
+ const shouldKickOffPull = workspaceSyncPhase === 'attach-cloud' || !workspaceLastPullAt;
1643
+ if (shouldKickOffPull) {
1644
+ void pullWorkspaceChangesFromCloud();
1645
+ }
1583
1646
  const intervalId = window.setInterval(() => {
1584
1647
  void pullWorkspaceChangesFromCloud();
1585
1648
  }, workspacePullIntervalMs);
1586
- return () => window.clearInterval(intervalId);
1649
+ return () => {
1650
+ window.clearInterval(intervalId);
1651
+ };
1587
1652
  }, [
1588
1653
  cloudAuthConfigured,
1589
1654
  runtimeMode,
1590
1655
  workspaceCloudSyncEnabled,
1591
1656
  authSessionResolved,
1592
1657
  workspaceSyncPhase,
1658
+ workspaceLastPullAt,
1593
1659
  isWorkspaceRetryPending,
1594
1660
  pullWorkspaceChangesFromCloud,
1595
1661
  workspacePullIntervalMs
@@ -1718,6 +1784,7 @@ export function useSyncOrchestrator(input) {
1718
1784
  workspaceLastSuccessfulSyncAt,
1719
1785
  workspaceCloudSyncEnabled,
1720
1786
  workspaceSyncPhase,
1787
+ workspaceSyncSetupIntent,
1721
1788
  workspaceSyncStatus,
1722
1789
  workspaceSyncSummary,
1723
1790
  workspaceSyncRecommendedAction,
@@ -1732,6 +1799,7 @@ export function useSyncOrchestrator(input) {
1732
1799
  workspacePendingSignatureRef,
1733
1800
  workspaceLastPushedTaskIdsRef,
1734
1801
  workspaceDeletedTaskIdsRef,
1802
+ workspaceDeletedTaskWatermarksRef,
1735
1803
  workspacePullCursorRef,
1736
1804
  clearWorkspaceRetry,
1737
1805
  persistWorkspaceSyncPatch,