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.
- package/dist/src/ai-hub/hosts.js +227 -6
- package/dist/src/ai-hub/server.js +1014 -35
- package/dist/src/cli/commands/add-ide.js +2 -0
- package/dist/src/cli/commands/cleanup-artifacts.js +39 -0
- package/dist/src/cli/commands/init-project.js +12 -5
- package/dist/src/cli/commands/sync.js +74 -7
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-detector.js +6 -0
- package/dist/src/cli/utils/agent-adapters.js +40 -18
- package/dist/src/cli/utils/fraim-gitignore.js +13 -0
- package/dist/src/cli/utils/remote-sync.js +129 -53
- package/dist/src/cli/utils/user-config.js +12 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/compat.js +16 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +273 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/fraim-config-schema.generated.js +8 -2
- package/dist/src/core/utils/local-registry-resolver.js +26 -0
- package/dist/src/core/utils/project-fraim-paths.js +89 -2
- package/dist/src/first-run/session-service.js +9 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +298 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
- package/dist/src/local-mcp-server/stdio-server.js +42 -7
- package/package.json +5 -1
- package/public/ai-hub/index.html +205 -89
- package/public/ai-hub/review.css +12 -0
- package/public/ai-hub/script.js +1720 -240
- 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
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
332
|
-
if (!
|
|
320
|
+
for (const fileName of (0, fs_1.readdirSync)(rawPath)) {
|
|
321
|
+
if (!fileName.startsWith(`${resolvedUserId}-`))
|
|
333
322
|
continue;
|
|
334
|
-
|
|
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
|
-
//
|
|
331
|
+
// Ignore read failures.
|
|
339
332
|
}
|
|
340
333
|
}
|
|
341
|
-
const
|
|
342
|
-
if ((0, fs_1.existsSync)(
|
|
334
|
+
const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
|
|
335
|
+
if ((0, fs_1.existsSync)(retrospectivesPath)) {
|
|
343
336
|
try {
|
|
344
|
-
for (const
|
|
345
|
-
if (!
|
|
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)(
|
|
340
|
+
if (!isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, fileName)))
|
|
348
341
|
continue;
|
|
349
|
-
|
|
342
|
+
sources.push({
|
|
343
|
+
kind: 'retrospective',
|
|
344
|
+
displayPath: `docs/retrospectives/${fileName}`,
|
|
345
|
+
sortKey: pendingL0SortKey(fileName)
|
|
346
|
+
});
|
|
350
347
|
}
|
|
351
348
|
}
|
|
352
349
|
catch {
|
|
353
|
-
//
|
|
350
|
+
// Ignore read failures.
|
|
354
351
|
}
|
|
355
352
|
}
|
|
356
|
-
return
|
|
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
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
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,
|
|
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(
|
|
644
|
-
return (0, path_1.join)((0, project_fraim_paths_1.
|
|
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
|
|
1842
|
-
|
|
1843
|
-
|
|
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.
|
|
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/",
|