fraim-framework 2.0.171 → 2.0.174

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 (32) hide show
  1. package/dist/src/ai-hub/hosts.js +227 -6
  2. package/dist/src/ai-hub/server.js +1014 -35
  3. package/dist/src/cli/commands/add-ide.js +2 -0
  4. package/dist/src/cli/commands/cleanup-artifacts.js +39 -0
  5. package/dist/src/cli/commands/init-project.js +12 -5
  6. package/dist/src/cli/commands/sync.js +74 -7
  7. package/dist/src/cli/fraim.js +2 -0
  8. package/dist/src/cli/setup/ide-detector.js +6 -0
  9. package/dist/src/cli/utils/agent-adapters.js +40 -18
  10. package/dist/src/cli/utils/fraim-gitignore.js +13 -0
  11. package/dist/src/cli/utils/remote-sync.js +129 -53
  12. package/dist/src/cli/utils/user-config.js +12 -0
  13. package/dist/src/config/ai-manager-hiring.js +121 -0
  14. package/dist/src/config/compat.js +16 -0
  15. package/dist/src/config/feature-flags.js +25 -0
  16. package/dist/src/config/persona-capability-bundles.js +273 -0
  17. package/dist/src/config/persona-hiring.js +270 -0
  18. package/dist/src/config/portfolio-slug-overrides.js +17 -0
  19. package/dist/src/config/pricing.js +37 -0
  20. package/dist/src/config/stripe.js +43 -0
  21. package/dist/src/core/fraim-config-schema.generated.js +8 -2
  22. package/dist/src/core/utils/local-registry-resolver.js +26 -0
  23. package/dist/src/core/utils/project-fraim-paths.js +89 -2
  24. package/dist/src/first-run/session-service.js +9 -0
  25. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +298 -0
  26. package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
  27. package/dist/src/local-mcp-server/stdio-server.js +42 -7
  28. package/package.json +5 -1
  29. package/public/ai-hub/index.html +205 -89
  30. package/public/ai-hub/review.css +12 -0
  31. package/public/ai-hub/script.js +1720 -240
  32. package/public/ai-hub/styles.css +473 -6
@@ -25,8 +25,7 @@ const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPat
25
25
  const DEFAULT_THRESHOLD = 3.0;
26
26
  const AGING_HORIZON_DAYS = 7;
27
27
  const MAX_ENTRIES_SCANNED = 200;
28
- const BACKLOG_MIN = 5;
29
- const OLDEST_AGE_DAYS_TRIGGER = 3;
28
+ const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
30
29
  // ── Single source of truth for the learning-entry format contract (#533) ──────
31
30
  // The `## [P-…] <title>` entry format is a tight contract shared by three sides:
32
31
  // 1. EMIT — the synthesis jobs (sleep-on-learnings, organizational-learning-
@@ -309,51 +308,49 @@ function isUnsynthesizedRetrospective(filePath) {
309
308
  return false;
310
309
  }
311
310
  }
312
- /** Oldest mtime-age in days across this user's L0 signals. 0 if none. */
313
- function computeOldestL0AgeDays(workspaceRoot, userId) {
314
- const learningsBase = (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot);
315
- const now = Date.now();
316
- let oldest = 0;
317
- const consider = (filePath) => {
318
- try {
319
- const st = (0, fs_1.statSync)(filePath);
320
- const ageDays = Math.floor((now - st.mtimeMs) / (1000 * 60 * 60 * 24));
321
- if (ageDays > oldest)
322
- oldest = ageDays;
323
- }
324
- catch {
325
- // ignore
326
- }
327
- };
328
- const rawDir = (0, path_1.join)(learningsBase, 'raw');
329
- if ((0, fs_1.existsSync)(rawDir)) {
311
+ function pendingL0SortKey(fileName) {
312
+ const timestamp = fileName.match(/\d{4}-\d{2}-\d{2}(?:T\d{2}-\d{2}-\d{2})?/);
313
+ return timestamp?.[0] || fileName;
314
+ }
315
+ function collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots) {
316
+ const sources = [];
317
+ const rawPath = (0, path_1.join)(roots.repoLearningsBase, 'raw');
318
+ if ((0, fs_1.existsSync)(rawPath)) {
330
319
  try {
331
- for (const f of (0, fs_1.readdirSync)(rawDir)) {
332
- if (!f.startsWith(`${userId}-`))
320
+ for (const fileName of (0, fs_1.readdirSync)(rawPath)) {
321
+ if (!fileName.startsWith(`${resolvedUserId}-`))
333
322
  continue;
334
- consider((0, path_1.join)(rawDir, f));
323
+ sources.push({
324
+ kind: 'coaching-moment',
325
+ displayPath: `${REPO_LEARNINGS_REL}/raw/${fileName}`,
326
+ sortKey: pendingL0SortKey(fileName)
327
+ });
335
328
  }
336
329
  }
337
330
  catch {
338
- // ignore
331
+ // Ignore read failures.
339
332
  }
340
333
  }
341
- const retroDir = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
342
- if ((0, fs_1.existsSync)(retroDir)) {
334
+ const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
335
+ if ((0, fs_1.existsSync)(retrospectivesPath)) {
343
336
  try {
344
- for (const f of (0, fs_1.readdirSync)(retroDir)) {
345
- if (!f.startsWith(`${userId}-`) || !f.endsWith('.md'))
337
+ for (const fileName of (0, fs_1.readdirSync)(retrospectivesPath)) {
338
+ if (!fileName.startsWith(`${resolvedUserId}-`) || !fileName.endsWith('.md'))
346
339
  continue;
347
- if (!isUnsynthesizedRetrospective((0, path_1.join)(retroDir, f)))
340
+ if (!isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, fileName)))
348
341
  continue;
349
- consider((0, path_1.join)(retroDir, f));
342
+ sources.push({
343
+ kind: 'retrospective',
344
+ displayPath: `docs/retrospectives/${fileName}`,
345
+ sortKey: pendingL0SortKey(fileName)
346
+ });
350
347
  }
351
348
  }
352
349
  catch {
353
- // ignore
350
+ // Ignore read failures.
354
351
  }
355
352
  }
356
- return oldest;
353
+ return sources.sort((a, b) => b.sortKey.localeCompare(a.sortKey) || a.displayPath.localeCompare(b.displayPath));
357
354
  }
358
355
  /**
359
356
  * Resolve an L2 org-scope learning file (issue #563): a repo-local override
@@ -395,28 +392,9 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
395
392
  const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
396
393
  const l1MistakeStats = l1Mistake.present ? scanMistakePatternFile(l1Mistake.path, threshold, 'mistake-patterns') : null;
397
394
  const l1ValidatedStats = l1Validated.present ? scanMistakePatternFile(l1Validated.path, threshold, 'validated-patterns') : null;
398
- let l0CoachingCount = 0;
399
- const rawPath = (0, path_1.join)(roots.repoLearningsBase, 'raw');
400
- if ((0, fs_1.existsSync)(rawPath)) {
401
- try {
402
- l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${resolvedUserId}-`)).length;
403
- }
404
- catch {
405
- // Ignore read failures.
406
- }
407
- }
408
- let l0RetroCount = 0;
409
- const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
410
- if ((0, fs_1.existsSync)(retrospectivesPath)) {
411
- try {
412
- l0RetroCount = (0, fs_1.readdirSync)(retrospectivesPath)
413
- .filter(f => f.startsWith(`${resolvedUserId}-`) && f.endsWith('.md'))
414
- .filter(f => isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, f))).length;
415
- }
416
- catch {
417
- // Ignore read failures.
418
- }
419
- }
395
+ const pendingL0Sources = collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots);
396
+ const l0CoachingCount = pendingL0Sources.filter(source => source.kind === 'coaching-moment').length;
397
+ const l0RetroCount = pendingL0Sources.filter(source => source.kind === 'retrospective').length;
420
398
  const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent || l2ValidatedPresent;
421
399
  const hasL1 = l1Mistake.present || l1Pref.present || l1Coach.present || l1Validated.present;
422
400
  const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
@@ -458,6 +436,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
458
436
  section += '\n';
459
437
  }
460
438
  if (l0CoachingCount > 0 || l0RetroCount > 0) {
439
+ const totalL0 = pendingL0Sources.length;
461
440
  section += '### L0 - Your unprocessed signals\n';
462
441
  if (l0CoachingCount > 0) {
463
442
  section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${REPO_LEARNINGS_REL}/raw/${resolvedUserId}-*\`\n`;
@@ -465,12 +444,16 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
465
444
  if (l0RetroCount > 0) {
466
445
  section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${resolvedUserId}-*\` with unsynthesized or missing \`synthesized\` frontmatter\n`;
467
446
  }
447
+ section += 'Pending L0 Source Files:\n';
448
+ for (const source of pendingL0Sources) {
449
+ section += `- \`${source.displayPath}\`\n`;
450
+ }
451
+ section += 'Read these pending L0 source files before continuing; they are unsynthesized learnings for the current agent.\n';
452
+ if (totalL0 >= L0_SLEEP_ON_LEARNINGS_PROMPT_MIN) {
453
+ section += `This is ${totalL0} pending L0 file${totalL0 !== 1 ? 's' : ''}; to speed up future starts, run \`sleep-on-learnings\` to synthesize or archive them.\n`;
454
+ }
468
455
  section += '\n';
469
456
  }
470
- const totalL0 = l0CoachingCount + l0RetroCount;
471
- const oldestAgeDays = totalL0 > 0 ? computeOldestL0AgeDays(workspaceRoot, resolvedUserId) : 0;
472
- const agingRisk = l1MistakeStats?.agingRisk ?? 0;
473
- const backlogTriggered = totalL0 >= BACKLOG_MIN || (oldestAgeDays >= OLDEST_AGE_DAYS_TRIGGER && totalL0 > 0);
474
457
  if (forJob) {
475
458
  if (hasL2 || hasL1) {
476
459
  section += 'Use the relevant patterns and preferences in this job.\n';
@@ -478,23 +461,12 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
478
461
  section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
479
462
  }
480
463
  }
481
- if (backlogTriggered) {
482
- section += '\n';
483
- section += `Warning: ${totalL0} unprocessed signals pending. Consider running \`sleep-on-learnings\` before starting today's work.\n`;
484
- section += renderBacklogDetail(oldestAgeDays, agingRisk);
485
- }
486
464
  }
487
465
  else {
488
466
  section += 'Use this synthesized learning context throughout the session.\n';
489
467
  if (l1Coach.present || l2CoachPresent) {
490
468
  section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
491
469
  }
492
- if (backlogTriggered) {
493
- section += '\n';
494
- section += `Warning: synthesis overdue with ${totalL0} unprocessed signals.\n`;
495
- section += 'Run `sleep-on-learnings` before starting today\'s work.\n';
496
- section += renderBacklogDetail(oldestAgeDays, agingRisk);
497
- }
498
470
  }
499
471
  return section;
500
472
  }
@@ -960,15 +932,3 @@ function isTruthyFlag(value) {
960
932
  return false;
961
933
  return normalized === 'true' || normalized === 'yes' || normalized === '1';
962
934
  }
963
- function renderBacklogDetail(oldestAgeDays, agingRisk) {
964
- if (oldestAgeDays <= 0 && agingRisk <= 0)
965
- return '';
966
- const parts = [];
967
- if (oldestAgeDays > 0)
968
- parts.push(`oldest ${oldestAgeDays}d`);
969
- parts.push('debrief takes ~3 minutes');
970
- if (agingRisk > 0) {
971
- parts.push(`${agingRisk} high-score pattern${agingRisk !== 1 ? 's' : ''} aging out within ${AGING_HORIZON_DAYS}d`);
972
- }
973
- return `Detail: ${parts.join('; ')}.\n`;
974
- }
@@ -570,7 +570,7 @@ class FraimLocalMCPServer {
570
570
  while (currentDir !== root) {
571
571
  const fraimDir = (0, project_fraim_paths_1.getWorkspaceConfigPath)(currentDir).replace(/[\\/]config\.json$/, '');
572
572
  this.log(` Checking: ${fraimDir}`);
573
- if ((0, fs_1.existsSync)(fraimDir)) {
573
+ if ((0, project_fraim_paths_1.workspaceFraimExists)(currentDir)) {
574
574
  // Skip the home directory FRAIM dir and continue searching for a project-specific one
575
575
  if (homeDir && currentDir === homeDir) {
576
576
  this.log(`Skipping home directory ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)()}, continuing search...`);
@@ -640,8 +640,8 @@ class FraimLocalMCPServer {
640
640
  return false;
641
641
  }
642
642
  }
643
- getLocalCatalogMetadataPath(projectRoot) {
644
- return (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(projectRoot), FraimLocalMCPServer.CONNECT_SYNC_METADATA_PATH);
643
+ getLocalCatalogMetadataPath(_projectRoot) {
644
+ return (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), FraimLocalMCPServer.CONNECT_SYNC_METADATA_PATH);
645
645
  }
646
646
  readLocalCatalogMetadata(projectRoot) {
647
647
  const metadataPath = this.getLocalCatalogMetadataPath(projectRoot);
@@ -657,6 +657,7 @@ class FraimLocalMCPServer {
657
657
  }
658
658
  writeLocalCatalogMetadata(projectRoot) {
659
659
  const metadataPath = this.getLocalCatalogMetadataPath(projectRoot);
660
+ (0, fs_1.mkdirSync)((0, project_fraim_paths_1.getUserFraimDirPath)(), { recursive: true });
660
661
  const metadata = {
661
662
  localVersion: this.localVersion,
662
663
  mode: this.shouldUseLocalSyncTarget() ? 'local' : 'remote',
@@ -1838,10 +1839,12 @@ class FraimLocalMCPServer {
1838
1839
  }
1839
1840
  const resolvedRequestStr = requestSubstitution.content;
1840
1841
  const finalRequest = JSON.parse(resolvedRequestStr);
1841
- const response = await axios_1.default.post(`${this.remoteUrl}/mcp`, finalRequest, {
1842
- headers,
1843
- timeout: 30000
1844
- });
1842
+ const canRetryTransientRemoteReset = request.method === 'initialize' ||
1843
+ request.method === 'tools/list' ||
1844
+ request.method === 'resources/list' ||
1845
+ request.method === 'prompts/list' ||
1846
+ (request.method === 'tools/call' && request.params?.name === 'fraim_connect');
1847
+ const response = await this.postRemoteMcpRequest(finalRequest, headers, requestId, canRetryTransientRemoteReset);
1845
1848
  return response.data;
1846
1849
  }
1847
1850
  catch (error) {
@@ -1884,6 +1887,38 @@ class FraimLocalMCPServer {
1884
1887
  };
1885
1888
  }
1886
1889
  }
1890
+ isTransientRemoteReset(error) {
1891
+ if (error?.response) {
1892
+ return false;
1893
+ }
1894
+ const code = String(error?.code || '');
1895
+ const message = String(error?.message || '');
1896
+ return ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EPIPE', 'socket hang up'].some((needle) => code.includes(needle) || message.includes(needle));
1897
+ }
1898
+ async postRemoteMcpRequest(requestBody, headers, requestId, retryTransientReset) {
1899
+ let lastError;
1900
+ const attempts = retryTransientReset ? 3 : 1;
1901
+ for (let attempt = 1; attempt <= attempts; attempt++) {
1902
+ try {
1903
+ return await axios_1.default.post(`${this.remoteUrl}/mcp`, requestBody, {
1904
+ headers: {
1905
+ ...headers,
1906
+ Connection: 'close'
1907
+ },
1908
+ timeout: 30000
1909
+ });
1910
+ }
1911
+ catch (error) {
1912
+ lastError = error;
1913
+ if (!retryTransientReset || !this.isTransientRemoteReset(error) || attempt === attempts) {
1914
+ throw error;
1915
+ }
1916
+ this.logError(`[req:${requestId}] Remote request reset; retrying (${attempt + 1}/${attempts})`);
1917
+ await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
1918
+ }
1919
+ }
1920
+ throw lastError;
1921
+ }
1887
1922
  /**
1888
1923
  * Try to request workspace roots from MCP client
1889
1924
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.171",
3
+ "version": "2.0.174",
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": {
@@ -16,6 +16,8 @@
16
16
  "test-all": "npm run test && npm run test:isolated tests/isolated/test-*.ts && npm run test:ui",
17
17
  "test": "node scripts/test-with-server.js",
18
18
  "test:isolated": "node scripts/test-isolated.js",
19
+ "test:evals": "node scripts/evals/run-promptfoo-evals.cjs --suite all",
20
+ "test:evals:smoke": "node scripts/evals/run-promptfoo-evals.cjs --suite all --tag smoke",
19
21
  "test:smoke": "node scripts/test-with-server.js --tags=smoke",
20
22
  "test:coverage": "node scripts/test-with-server.js --tags=smoke --coverage",
21
23
  "test:stripe": "node scripts/test-with-server.js tests/test-stripe-payment-complete.ts",
@@ -110,6 +112,7 @@
110
112
  "node-cron": "^4.2.1",
111
113
  "playwright": "^1.58.2",
112
114
  "pptxgenjs": "^4.0.1",
115
+ "promptfoo": "^0.121.17",
113
116
  "puppeteer": "^24.36.1",
114
117
  "qrcode": "^1.5.4",
115
118
  "sharp": "^0.34.5",
@@ -122,6 +125,7 @@
122
125
  "dist/src/ai-hub/",
123
126
  "dist/src/first-run/",
124
127
  "dist/src/core/",
128
+ "dist/src/config/",
125
129
  "bin/fraim.js",
126
130
  "bin/fraim-mcp.js",
127
131
  "public/ai-hub/",