fraim-framework 2.0.201 → 2.0.203

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.
@@ -18,7 +18,7 @@ function getFraimVersion() {
18
18
  if (fs_1.default.existsSync(pkgPath)) {
19
19
  try {
20
20
  const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
21
- if (pkg.name === 'fraim-framework') {
21
+ if (pkg.name === 'fraim-framework' || pkg.name === 'fraim') {
22
22
  return pkg.version;
23
23
  }
24
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.201",
3
+ "version": "2.0.203",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -6439,21 +6439,20 @@ function renderFirstRunLanding(mode) {
6439
6439
  // Company · Manager · Projects. It ADDS UI around the existing surface and
6440
6440
  // removes no existing ids or handlers.
6441
6441
  //
6442
- // Reads the extended bootstrap (firstRun, teamContext, brain, personas).
6443
- // Per-project employee/job assignments and the project list live in
6444
- // localStorage (the same model as conversations); the workspace conversation
6445
- // pane reuses the existing run/poll/coaching code paths verbatim.
6446
- // ===========================================================================
6447
-
6448
- const STORAGE_KEY_PROJECTS_512 = 'fraim.aiHub.projects.v1';
6449
- const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
6442
+ // Reads the extended bootstrap (firstRun, teamContext, brain, personas).
6443
+ // Project tabs are server-owned local-machine state; per-project employee/job
6444
+ // assignments remain UI-local. The workspace conversation pane reuses the
6445
+ // existing run/poll/coaching code paths verbatim.
6446
+ // ===========================================================================
6447
+
6448
+ const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
6450
6449
 
6451
6450
  const tf = {
6452
6451
  enabled: false,
6453
6452
  area: 'projects', // company | manager | projects | brain | connected
6454
6453
  projectView: 'overview', // overview | workspace
6455
6454
  activeProjectId: null,
6456
- projects: [], // [{ id, name, intent, brief }] — roster derived from hired personas, not stored
6455
+ projects: [], // server-owned [{ id, name, folderPath, intent, brief }]
6457
6456
  assignments: {}, // { [projectId]: [{ id, employeeKey, jobName, jobId }] }
6458
6457
  // #726: canonical paths just removed this session. A delete switches workspaces first,
6459
6458
  // and that switch's (possibly cached) bootstrap merge would otherwise re-add the project
@@ -6488,7 +6487,7 @@ function tfPersonaByKey(key) { return tfPersonas().find((p) => p.key === key) ||
6488
6487
  function tfProjectTeamKeys() { return tfHiredPersonas().map((p) => p.key); }
6489
6488
 
6490
6489
  // ---------------------------------------------------------------------------
6491
- // Persistence (projects + assignments, mirrors the conversation model)
6490
+ // Persistence (server-owned projects + client-local assignments)
6492
6491
  // ---------------------------------------------------------------------------
6493
6492
  function tfCanonicalProjectPath(folderPath) {
6494
6493
  const value = String(folderPath || '').replace(/\\/g, '/').replace(/\/+$/, '');
@@ -6538,30 +6537,9 @@ function tfApplyUniqueProjectIds(projects) {
6538
6537
  });
6539
6538
  }
6540
6539
 
6541
- function tfMergeProjects(projects) {
6542
- const merged = new Map();
6543
- const add = (project) => {
6544
- const normalized = tfNormalizeProject(project);
6545
- if (!normalized) return;
6546
- const key = tfCanonicalProjectPath(normalized.folderPath);
6547
- // #726: never re-add a project removed this session (stale/cached bootstrap merge).
6548
- if (tf.removedPaths && tf.removedPaths.has(key)) return;
6549
- const existing = merged.get(key);
6550
- if (existing) {
6551
- const next = { ...existing, ...normalized, folderPath: normalized.folderPath || existing.folderPath };
6552
- merged.set(key, next);
6553
- } else {
6554
- merged.set(key, normalized);
6555
- }
6556
- };
6557
- for (const project of tf.projects || []) add(project);
6558
- for (const project of projects || []) add(project);
6559
- tf.projects = tfApplyUniqueProjectIds(Array.from(merged.values()));
6560
- }
6561
-
6562
- function tfProjectForPath(folderPath) {
6563
- const key = tfCanonicalProjectPath(folderPath);
6564
- return tf.projects.find((project) => tfCanonicalProjectPath(project.folderPath || project.path || project.folder) === key) || null;
6540
+ function tfProjectForPath(folderPath) {
6541
+ const key = tfCanonicalProjectPath(folderPath);
6542
+ return tf.projects.find((project) => tfCanonicalProjectPath(project.folderPath || project.path || project.folder) === key) || null;
6565
6543
  }
6566
6544
 
6567
6545
  function tfEnsureCurrentProject() {
@@ -6576,34 +6554,41 @@ function tfEnsureCurrentProject() {
6576
6554
  name: friendlyProjectShortName(state.projectPath),
6577
6555
  folderPath: state.projectPath,
6578
6556
  }, state.projectPath);
6579
- tfMergeProjects([current]);
6580
- const resolved = tfProjectForPath(state.projectPath);
6581
- if (!tf.activeProjectId || !tf.projects.some((project) => project.id === tf.activeProjectId)) {
6582
- tf.activeProjectId = resolved ? resolved.id : tf.projects[0]?.id || null;
6583
- }
6584
- return resolved;
6585
- }
6586
-
6587
- function tfLoadStorage() {
6588
- try {
6589
- const stored = JSON.parse(window.localStorage.getItem(STORAGE_KEY_PROJECTS_512) || '[]');
6590
- const normalized = Array.isArray(stored) ? stored.map((project) => tfNormalizeProject(project)).filter(Boolean) : [];
6591
- tf.projects = tfApplyUniqueProjectIds(normalized);
6592
- }
6593
- catch { tf.projects = []; }
6594
- try { tf.assignments = JSON.parse(window.localStorage.getItem(STORAGE_KEY_ASSIGNMENTS_512) || '{}'); }
6595
- catch { tf.assignments = {}; }
6596
- }
6597
- function tfPersistProjects(options) {
6598
- try { window.localStorage.setItem(STORAGE_KEY_PROJECTS_512, JSON.stringify(tf.projects)); } catch {}
6599
- if (!state.projectPath) return;
6600
- const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
6601
- requestJson('/api/ai-hub/projects', {
6602
- method: 'PUT',
6603
- headers: { 'Content-Type': 'application/json' },
6604
- body: JSON.stringify({ projectPath: state.projectPath, projects: tf.projects, reviveRemovedPaths }),
6605
- }).catch((error) => console.warn('[512] Could not persist projects to local disk:', error));
6606
- }
6557
+ let resolved = existingForPath;
6558
+ if (!resolved && current && !(tf.removedPaths && tf.removedPaths.has(tfCanonicalProjectPath(current.folderPath)))) {
6559
+ // Fallback only for the server bootstrap's active project path. General
6560
+ // project-list ownership remains in bootstrap.projects / /api/ai-hub/projects.
6561
+ tf.projects = tfApplyUniqueProjectIds([...(tf.projects || []), current]);
6562
+ resolved = tfProjectForPath(state.projectPath);
6563
+ }
6564
+ if (!tf.activeProjectId || !tf.projects.some((project) => project.id === tf.activeProjectId)) {
6565
+ tf.activeProjectId = resolved ? resolved.id : tf.projects[0]?.id || null;
6566
+ }
6567
+ return resolved;
6568
+ }
6569
+
6570
+ function tfClearLegacyProjectStorage() {
6571
+ try { window.localStorage.removeItem('fraim.aiHub.projects.v1'); } catch {}
6572
+ }
6573
+
6574
+ function tfLoadClientState() {
6575
+ tf.projects = [];
6576
+ tfClearLegacyProjectStorage();
6577
+ try { tf.assignments = JSON.parse(window.localStorage.getItem(STORAGE_KEY_ASSIGNMENTS_512) || '{}'); }
6578
+ catch { tf.assignments = {}; }
6579
+ }
6580
+
6581
+ async function tfSaveProjectsToServer(projects, options) {
6582
+ if (!state.projectPath) throw new Error('No active project path available.');
6583
+ const reviveRemovedPaths = options && Array.isArray(options.reviveRemovedPaths) ? options.reviveRemovedPaths : [];
6584
+ const payload = await requestJson('/api/ai-hub/projects', {
6585
+ method: 'PUT',
6586
+ headers: { 'Content-Type': 'application/json' },
6587
+ body: JSON.stringify({ projectPath: state.projectPath, projects: Array.isArray(projects) ? projects : [], reviveRemovedPaths }),
6588
+ });
6589
+ tfAdoptServerProjects(payload && payload.projects);
6590
+ return payload;
6591
+ }
6607
6592
  function tfPersistAssignments() {
6608
6593
  try { window.localStorage.setItem(STORAGE_KEY_ASSIGNMENTS_512, JSON.stringify(tf.assignments)); } catch {}
6609
6594
  }
@@ -7037,9 +7022,9 @@ function tfConfirmProjectRemove(proj, cardEl) {
7037
7022
  });
7038
7023
  }
7039
7024
 
7040
- // R10: adopt a server projects list as the authoritative tf.projects a REPLACE,
7041
- // not a tfMergeProjects union, so a later tfPersistProjects PUT cannot re-add a
7042
- // removed project from stale in-memory state.
7025
+ // R10: adopt a server projects list as the authoritative tf.projects - a REPLACE,
7026
+ // not a client-side union, so stale in-memory/browser state cannot re-add a
7027
+ // removed project.
7043
7028
  function tfAdoptServerProjects(projects) {
7044
7029
  const normalized = (Array.isArray(projects) ? projects : [])
7045
7030
  .map((project) => tfNormalizeProject(project))
@@ -7099,8 +7084,7 @@ async function tfRemoveProject(proj) {
7099
7084
  if (!tf.projects.some((p) => p.id === tf.activeProjectId)) {
7100
7085
  tf.activeProjectId = tf.projects[0] ? tf.projects[0].id : null;
7101
7086
  }
7102
- tfPersistProjects();
7103
- tfRenderProjectTabs();
7087
+ tfRenderProjectTabs();
7104
7088
  tfRenderOverview();
7105
7089
  if (typeof renderRail === 'function') renderRail();
7106
7090
  showStatus('"' + (proj.name || 'Project') + '" was removed from the Hub. Its files on disk are untouched.', false);
@@ -10287,26 +10271,36 @@ async function tfCreateProject() {
10287
10271
  brief: intent,
10288
10272
  team: tf.npSelectedEmployees.slice(),
10289
10273
  };
10290
- tf.projects.push(project);
10291
- tf.assignments[id] = [];
10292
- // #726: re-creating a project at a previously-removed path lifts the client guard
10293
- // (the server revive is handled via reviveRemovedPaths below).
10294
- tf.removedPaths.delete(tfCanonicalProjectPath(folderPath));
10295
- tfPersistProjects({ reviveRemovedPaths: [folderPath] });
10296
- tfPersistAssignments();
10297
- tf.activeProjectId = id;
10298
- tfCloseNewProject();
10299
- tfRenderProjectTabs();
10300
-
10301
- // Switch the active workspace to this project's folder so all runs,
10302
- // context reads, and learnings scope to the right place.
10303
- await tfSwitchProjectFolder(folderPath);
10274
+ const removedKey = tfCanonicalProjectPath(folderPath);
10275
+ try {
10276
+ await tfSaveProjectsToServer([...(tf.projects || []), project], { reviveRemovedPaths: [folderPath] });
10277
+ tf.removedPaths.delete(removedKey);
10278
+ } catch (error) {
10279
+ showStatus('Could not create project: ' + (error && error.message ? error.message : 'request failed.'), true);
10280
+ return;
10281
+ }
10282
+ const savedProject = tfProjectForPath(folderPath) || project;
10283
+ if (!tf.assignments[savedProject.id]) tf.assignments[savedProject.id] = [];
10284
+ tfPersistAssignments();
10285
+ tf.activeProjectId = savedProject.id;
10286
+ tfCloseNewProject();
10287
+ tfRenderProjectTabs();
10288
+
10289
+ // Switch the active workspace to this project's folder so all runs,
10290
+ // context reads, and learnings scope to the right place.
10291
+ await tfSwitchProjectFolder(folderPath);
10292
+ const currentProject = tfProjectForPath(folderPath) || savedProject;
10293
+ if (currentProject) {
10294
+ tf.activeProjectId = currentProject.id;
10295
+ if (!tf.assignments[currentProject.id]) tf.assignments[currentProject.id] = [];
10296
+ tfPersistAssignments();
10297
+ }
10304
10298
 
10305
10299
  // #521 fix: re-render the new project's tree + Brief/Learnings top sections so
10306
10300
  // they reflect THIS project (the folder switch cleared the project-scoped
10307
10301
  // context cache; this re-fetches it) instead of leaving the previous project's
10308
10302
  // brief on screen ("random stuff in the brief").
10309
- tf._lastWorkspaceProject = id;
10303
+ tf._lastWorkspaceProject = currentProject ? currentProject.id : savedProject.id;
10310
10304
  tfRenderTree();
10311
10305
  tfRenderProjectContextTop();
10312
10306
 
@@ -10371,12 +10365,11 @@ async function tfSwitchProjectFolder(folderPath) {
10371
10365
  } else {
10372
10366
  await conversationsPromise;
10373
10367
  }
10374
- tfMergeProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10375
- const currentProject = tfEnsureCurrentProject();
10376
- if (currentProject) tf.activeProjectId = currentProject.id;
10377
- tfPersistProjects();
10378
- tfRefreshAfterBootstrap();
10379
- }
10368
+ tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10369
+ const currentProject = tfEnsureCurrentProject();
10370
+ if (currentProject) tf.activeProjectId = currentProject.id;
10371
+ tfRefreshAfterBootstrap();
10372
+ }
10380
10373
 
10381
10374
  // ---------------------------------------------------------------------------
10382
10375
  // Assign-job modal (job-first with locks + per-employee filter)
@@ -10939,20 +10932,19 @@ function tfInitShell() {
10939
10932
  document.body.classList.add('hub-shell');
10940
10933
  const tabs = document.getElementById('hub-tabs');
10941
10934
  if (tabs) tabs.hidden = false;
10942
- tfLoadStorage();
10943
- tfMergeProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10935
+ tfLoadClientState();
10936
+ tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10944
10937
  tfWireShell();
10945
10938
  tfPopulateAccountMenu(); // #533: show the real account identity, not "SM"
10946
10939
  // Seed a project from the loaded folder so the workspace reuses the existing
10947
10940
  // conversation machinery for that real project path.
10948
10941
  const currentProject = tfEnsureCurrentProject();
10949
10942
  if (currentProject && !tf.assignments[currentProject.id]) tf.assignments[currentProject.id] = [];
10950
- if (currentProject) {
10951
- tf.activeProjectId = currentProject.id;
10952
- tfPersistProjects();
10953
- } else if (tf.projects.length && !tf.activeProjectId) {
10954
- tf.activeProjectId = tf.projects[0].id;
10955
- }
10943
+ if (currentProject) {
10944
+ tf.activeProjectId = currentProject.id;
10945
+ } else if (tf.projects.length && !tf.activeProjectId) {
10946
+ tf.activeProjectId = tf.projects[0].id;
10947
+ }
10956
10948
  tfRenderProjectTabs();
10957
10949
  tfShowArea('projects');
10958
10950
  // Issue #578: wire deployment modal buttons.
@@ -10974,12 +10966,11 @@ function tfInitShell() {
10974
10966
  }
10975
10967
 
10976
10968
  // Refresh shell-derived UI after a bootstrap reload (e.g. project picked).
10977
- function tfRefreshAfterBootstrap() {
10978
- if (!tf.enabled) return;
10979
- tfMergeProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10980
- tfEnsureCurrentProject();
10981
- tfPersistProjects();
10982
- tfRenderProjectTabs();
10969
+ function tfRefreshAfterBootstrap() {
10970
+ if (!tf.enabled) return;
10971
+ tfAdoptServerProjects(state.bootstrap && Array.isArray(state.bootstrap.projects) ? state.bootstrap.projects : []);
10972
+ tfEnsureCurrentProject();
10973
+ tfRenderProjectTabs();
10983
10974
  if (tf.area === 'company') tfRenderCompany();
10984
10975
  else if (tf.area === 'manager') tfRenderManager();
10985
10976
  else if (tf.area === 'brain') tfRenderBrain();