fraim-framework 2.0.119 → 2.0.122
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/cli/mcp/command-resolution.js +35 -2
- package/dist/src/cli/setup/ide-global-integration.js +5 -3
- package/dist/src/cli/setup/ide-invocation-surfaces.js +18 -4
- package/dist/src/cli/utils/agent-adapters.js +4 -1
- package/dist/src/core/quality-evidence.js +3 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +257 -72
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.resolveManagedCommand = exports.getPortableNpxCommand = void 0;
|
|
6
|
+
exports.resolveManagedCommand = exports.getSystemCommandPath = exports.getPortableNpxCommand = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
@@ -36,10 +36,43 @@ const getPortableNpxCommand = () => {
|
|
|
36
36
|
return null;
|
|
37
37
|
};
|
|
38
38
|
exports.getPortableNpxCommand = getPortableNpxCommand;
|
|
39
|
+
const getPathEntries = () => {
|
|
40
|
+
const rawPath = process.env.PATH || '';
|
|
41
|
+
return rawPath
|
|
42
|
+
.split(path_1.default.delimiter)
|
|
43
|
+
.map((entry) => entry.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
};
|
|
46
|
+
const getSystemCommandCandidates = (command) => {
|
|
47
|
+
if (!command || path_1.default.isAbsolute(command)) {
|
|
48
|
+
return command ? [command] : [];
|
|
49
|
+
}
|
|
50
|
+
const commandNames = process.platform === 'win32'
|
|
51
|
+
? command.includes('.')
|
|
52
|
+
? [command]
|
|
53
|
+
: [command, `${command}.cmd`, `${command}.exe`, `${command}.bat`, `${command}.com`]
|
|
54
|
+
: [command];
|
|
55
|
+
return getPathEntries().flatMap((entry) => commandNames.map((name) => path_1.default.join(entry, name)));
|
|
56
|
+
};
|
|
57
|
+
const getSystemCommandPath = (command) => {
|
|
58
|
+
for (const candidate of getSystemCommandCandidates(command)) {
|
|
59
|
+
try {
|
|
60
|
+
const stats = fs_1.default.statSync(candidate);
|
|
61
|
+
if (stats.isFile()) {
|
|
62
|
+
return candidate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Ignore missing or inaccessible PATH entries and keep scanning.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
exports.getSystemCommandPath = getSystemCommandPath;
|
|
39
72
|
const resolveManagedCommand = (command) => {
|
|
40
73
|
if (command !== 'npx') {
|
|
41
74
|
return command;
|
|
42
75
|
}
|
|
43
|
-
return (0, exports.getPortableNpxCommand)() || command;
|
|
76
|
+
return (0, exports.getPortableNpxCommand)() || (0, exports.getSystemCommandPath)(command) || command;
|
|
44
77
|
};
|
|
45
78
|
exports.resolveManagedCommand = resolveManagedCommand;
|
|
@@ -21,8 +21,9 @@ function describeConfiguredInvocationSurfaces(installedIDEs) {
|
|
|
21
21
|
return installedIDEs.map((ide) => (0, ide_invocation_surfaces_1.describeInvocationSurface)(ide.name, ide.invocationProfile));
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Install
|
|
25
|
-
* Writes to ~/.claude/
|
|
24
|
+
* Install Claude FRAIM discovery artifacts at the user level.
|
|
25
|
+
* Writes a skill to ~/.claude/skills/fraim/SKILL.md and a compatibility command
|
|
26
|
+
* to ~/.claude/commands/fraim.md. Existing user files are preserved.
|
|
26
27
|
*/
|
|
27
28
|
async function installSlashCommands(homeDir) {
|
|
28
29
|
const home = homeDir || os_1.default.homedir();
|
|
@@ -30,7 +31,8 @@ async function installSlashCommands(homeDir) {
|
|
|
30
31
|
if (!fs_1.default.existsSync(claudeDir)) {
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
|
-
installFileIfMissing(path_1.default.join(claudeDir, '
|
|
34
|
+
installFileIfMissing(path_1.default.join(claudeDir, 'skills', 'fraim', 'SKILL.md'), (0, ide_invocation_surfaces_1.buildClaudeSkillContent)(), 'Claude FRAIM skill (~/.claude/skills/fraim/SKILL.md)');
|
|
35
|
+
installFileIfMissing(path_1.default.join(claudeDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)(), 'Claude compatibility command (~/.claude/commands/fraim.md)');
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Install FRAIM invocation artifacts for non-Claude IDEs.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FRAIM_INVOCATION_BODY = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
|
|
4
|
+
exports.buildClaudeSkillContent = buildClaudeSkillContent;
|
|
5
|
+
exports.buildClaudeCommandShimContent = buildClaudeCommandShimContent;
|
|
4
6
|
exports.buildClaudeSlashCommandContent = buildClaudeSlashCommandContent;
|
|
5
7
|
exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
|
|
6
8
|
exports.buildCodexSkillContent = buildCodexSkillContent;
|
|
@@ -12,7 +14,7 @@ exports.CURSOR_MDC_FRONTMATTER = `---
|
|
|
12
14
|
description: FRAIM discovery and execution contract
|
|
13
15
|
alwaysApply: true
|
|
14
16
|
---`;
|
|
15
|
-
exports.FRAIM_INVOCATION_BODY = `Follow this process:
|
|
17
|
+
exports.FRAIM_INVOCATION_BODY = `Follow this process:
|
|
16
18
|
|
|
17
19
|
1. **If the user did not specify a FRAIM job or topic**:
|
|
18
20
|
Call \`list_fraim_jobs()\` to discover available jobs. Present the results grouped by the categories returned by the server. For each group, list 3-5 of the most relevant jobs with a one-line description.
|
|
@@ -25,11 +27,23 @@ exports.FRAIM_INVOCATION_BODY = `Follow this process:
|
|
|
25
27
|
- For skills, use the content returned by \`get_fraim_file(...)\`.
|
|
26
28
|
|
|
27
29
|
4. **Execute**:
|
|
28
|
-
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
29
|
-
- For skills, apply the skill steps directly to the user's current context.
|
|
30
|
+
- For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
|
|
31
|
+
- For skills, apply the skill steps directly to the user's current context.
|
|
30
32
|
`;
|
|
33
|
+
function buildClaudeSkillContent() {
|
|
34
|
+
return `# FRAIM
|
|
35
|
+
|
|
36
|
+
${exports.FRAIM_INVOCATION_BODY}`;
|
|
37
|
+
}
|
|
38
|
+
function buildClaudeCommandShimContent() {
|
|
39
|
+
return `# FRAIM Compatibility Command
|
|
40
|
+
|
|
41
|
+
Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
|
|
42
|
+
|
|
43
|
+
${exports.FRAIM_INVOCATION_BODY}`;
|
|
44
|
+
}
|
|
31
45
|
function buildClaudeSlashCommandContent() {
|
|
32
|
-
return
|
|
46
|
+
return buildClaudeCommandShimContent();
|
|
33
47
|
}
|
|
34
48
|
function buildCursorMentionRuleContent() {
|
|
35
49
|
return `${exports.CURSOR_MDC_FRONTMATTER}
|
|
@@ -12,6 +12,7 @@ const START_MARKER = '<!-- FRAIM_AGENT_ADAPTER_START -->';
|
|
|
12
12
|
const END_MARKER = '<!-- FRAIM_AGENT_ADAPTER_END -->';
|
|
13
13
|
const CURSOR_RULE_PATH = path_1.default.join('.cursor', 'rules', 'fraim.mdc');
|
|
14
14
|
const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fraim.md');
|
|
15
|
+
const CLAUDE_FRAIM_SKILL_PATH = path_1.default.join('.claude', 'skills', 'fraim', 'SKILL.md');
|
|
15
16
|
const VSCODE_FRAIM_PROMPT_PATH = path_1.default.join('.github', 'prompts', 'fraim.prompt.md');
|
|
16
17
|
const CODEX_FRAIM_SKILL_PATH = path_1.default.join('.codex', 'skills', 'fraim', 'SKILL.md');
|
|
17
18
|
const WINDSURF_FRAIM_COMMAND_PATH = path_1.default.join('.windsurf', 'commands', 'fraim.md');
|
|
@@ -108,7 +109,8 @@ ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}`;
|
|
|
108
109
|
{ path: CURSOR_RULE_PATH, content: cursorManagedBody },
|
|
109
110
|
{ path: VSCODE_FRAIM_PROMPT_PATH, content: vscodePrompt },
|
|
110
111
|
{ path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme },
|
|
111
|
-
{ path:
|
|
112
|
+
{ path: CLAUDE_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSkillContent)() },
|
|
113
|
+
{ path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)() },
|
|
112
114
|
{ path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)() },
|
|
113
115
|
{ path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)() },
|
|
114
116
|
{ path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)() }
|
|
@@ -127,6 +129,7 @@ function ensureAgentAdapterFiles(projectRoot) {
|
|
|
127
129
|
? mergeCursorRule(existing, file.content)
|
|
128
130
|
: file.path.endsWith('README.md')
|
|
129
131
|
|| file.path === VSCODE_FRAIM_PROMPT_PATH
|
|
132
|
+
|| file.path === CLAUDE_FRAIM_SKILL_PATH
|
|
130
133
|
|| file.path === CLAUDE_FRAIM_COMMAND_PATH
|
|
131
134
|
|| file.path === CODEX_FRAIM_SKILL_PATH
|
|
132
135
|
|| file.path === WINDSURF_FRAIM_COMMAND_PATH
|
|
@@ -38,6 +38,7 @@ exports.QUALITY_REGISTRY = {
|
|
|
38
38
|
// Business Strategy
|
|
39
39
|
'review-business-strategy': { stage: 'business-strategy', enforced: true },
|
|
40
40
|
'business-plan-creation': { stage: 'business-strategy', enforced: false },
|
|
41
|
+
'branding-quality-audit': { stage: 'branding', enforced: true },
|
|
41
42
|
// Product Quality
|
|
42
43
|
'code-quality-assessment': { stage: 'product-quality', enforced: true },
|
|
43
44
|
// Test Quality
|
|
@@ -63,6 +64,7 @@ exports.STAGE_CATEGORY_MAP = Object.keys(exports.QUALITY_REGISTRY).reduce((acc,
|
|
|
63
64
|
exports.STAGE_DISPLAY_NAMES = {
|
|
64
65
|
'customer-development': 'Customer Development',
|
|
65
66
|
'business-strategy': 'Business Strategy',
|
|
67
|
+
'branding': 'Branding',
|
|
66
68
|
'product-quality': 'Product Quality',
|
|
67
69
|
'test-quality': 'Test Quality',
|
|
68
70
|
'fundraising': 'Fundraising',
|
|
@@ -74,6 +76,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
74
76
|
exports.ALL_STAGE_CATEGORIES = [
|
|
75
77
|
'customer-development',
|
|
76
78
|
'business-strategy',
|
|
79
|
+
'branding',
|
|
77
80
|
'product-quality',
|
|
78
81
|
'test-quality',
|
|
79
82
|
'fundraising',
|
|
@@ -6,37 +6,130 @@
|
|
|
6
6
|
* workspace root on the user's machine.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.computeEffectiveScore = computeEffectiveScore;
|
|
9
10
|
exports.buildLearningContextSection = buildLearningContextSection;
|
|
10
11
|
const fs_1 = require("fs");
|
|
11
12
|
const path_1 = require("path");
|
|
12
13
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
13
14
|
const LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
|
|
14
15
|
const DEFAULT_THRESHOLD = 3.0;
|
|
16
|
+
const AGING_HORIZON_DAYS = 7;
|
|
17
|
+
const MAX_ENTRIES_SCANNED = 200;
|
|
18
|
+
const BACKLOG_MIN = 5;
|
|
19
|
+
const OLDEST_AGE_DAYS_TRIGGER = 3;
|
|
15
20
|
function getLearningsBase(workspaceRoot) {
|
|
16
21
|
return (0, path_1.join)(workspaceRoot, LEARNINGS_REL);
|
|
17
22
|
}
|
|
18
|
-
function
|
|
23
|
+
function buildUserIdCandidates(userId) {
|
|
24
|
+
const candidates = new Set();
|
|
25
|
+
const trimmed = userId.trim();
|
|
26
|
+
if (trimmed)
|
|
27
|
+
candidates.add(trimmed);
|
|
28
|
+
const atIndex = trimmed.indexOf('@');
|
|
29
|
+
if (atIndex > 0)
|
|
30
|
+
candidates.add(trimmed.slice(0, atIndex));
|
|
31
|
+
return Array.from(candidates);
|
|
32
|
+
}
|
|
33
|
+
function countMatchingFilesByPrefix(dirPath, matcher) {
|
|
34
|
+
if (!(0, fs_1.existsSync)(dirPath))
|
|
35
|
+
return 0;
|
|
36
|
+
try {
|
|
37
|
+
return (0, fs_1.readdirSync)(dirPath).filter(matcher).length;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function collectAvailableUserPrefixes(workspaceRoot, learningsBase) {
|
|
44
|
+
const prefixes = new Set();
|
|
45
|
+
const collect = (dirPath, extractor) => {
|
|
46
|
+
if (!(0, fs_1.existsSync)(dirPath))
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
for (const fileName of (0, fs_1.readdirSync)(dirPath)) {
|
|
50
|
+
const prefix = extractor(fileName);
|
|
51
|
+
if (prefix)
|
|
52
|
+
prefixes.add(prefix);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Ignore unreadable directories.
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
collect(learningsBase, (fileName) => {
|
|
60
|
+
if (!fileName.endsWith('.md') || fileName.startsWith('org-'))
|
|
61
|
+
return null;
|
|
62
|
+
const match = fileName.match(/^(.*?)-(preferences|manager-coaching|mistake-patterns)\.md$/);
|
|
63
|
+
return match ? match[1] : null;
|
|
64
|
+
});
|
|
65
|
+
collect((0, path_1.join)(learningsBase, 'raw'), (fileName) => {
|
|
66
|
+
const match = fileName.match(/^(.*?)-\d{4}-\d{2}-\d{2}-.*\.md$/);
|
|
67
|
+
return match ? match[1] : null;
|
|
68
|
+
});
|
|
69
|
+
collect((0, path_1.join)(workspaceRoot, 'docs', 'retrospectives'), (fileName) => {
|
|
70
|
+
const match = fileName.match(/^(.*?)-\d{4}-\d{2}-\d{2}-.*\.md$/);
|
|
71
|
+
return match ? match[1] : null;
|
|
72
|
+
});
|
|
73
|
+
return prefixes;
|
|
74
|
+
}
|
|
75
|
+
function resolveLearningUserId(workspaceRoot, userId) {
|
|
76
|
+
const learningsBase = getLearningsBase(workspaceRoot);
|
|
77
|
+
const candidates = buildUserIdCandidates(userId);
|
|
78
|
+
let bestCandidate = candidates[0] || userId;
|
|
79
|
+
let bestScore = -1;
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
const score = ((0, fs_1.existsSync)((0, path_1.join)(learningsBase, `${candidate}-preferences.md`)) ? 1 : 0) +
|
|
82
|
+
((0, fs_1.existsSync)((0, path_1.join)(learningsBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
|
|
83
|
+
((0, fs_1.existsSync)((0, path_1.join)(learningsBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
|
|
84
|
+
countMatchingFilesByPrefix((0, path_1.join)(learningsBase, 'raw'), (fileName) => fileName.startsWith(`${candidate}-`)) +
|
|
85
|
+
countMatchingFilesByPrefix((0, path_1.join)(workspaceRoot, 'docs', 'retrospectives'), (fileName) => fileName.startsWith(`${candidate}-`) && fileName.endsWith('.md'));
|
|
86
|
+
if (score > bestScore) {
|
|
87
|
+
bestCandidate = candidate;
|
|
88
|
+
bestScore = score;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (bestScore > 0)
|
|
92
|
+
return bestCandidate;
|
|
93
|
+
const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot, learningsBase);
|
|
94
|
+
if (availablePrefixes.size === 1) {
|
|
95
|
+
return Array.from(availablePrefixes)[0];
|
|
96
|
+
}
|
|
97
|
+
return bestCandidate;
|
|
98
|
+
}
|
|
99
|
+
function readWorkspaceConfig(workspaceRoot) {
|
|
19
100
|
try {
|
|
20
101
|
const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
|
|
21
102
|
if ((0, fs_1.existsSync)(configPath)) {
|
|
22
|
-
|
|
23
|
-
const t = config?.learning?.scoreThreshold;
|
|
24
|
-
if (typeof t === 'number' && t > 0)
|
|
25
|
-
return t;
|
|
103
|
+
return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
26
104
|
}
|
|
27
105
|
}
|
|
28
106
|
catch {
|
|
29
|
-
// Fall through
|
|
107
|
+
// Fall through.
|
|
30
108
|
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function getScoreThreshold(workspaceRoot) {
|
|
112
|
+
const config = readWorkspaceConfig(workspaceRoot);
|
|
113
|
+
const t = config?.learning?.scoreThreshold;
|
|
114
|
+
if (typeof t === 'number' && t > 0)
|
|
115
|
+
return t;
|
|
31
116
|
return DEFAULT_THRESHOLD;
|
|
32
117
|
}
|
|
33
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Effective score for an L1 learning entry. The aging-risk count below uses
|
|
120
|
+
* this same decay model with `now` shifted forward.
|
|
121
|
+
*
|
|
122
|
+
* @param now Optional override for "now" (for forward-looking aging-risk
|
|
123
|
+
* calculations). Defaults to the current wall clock.
|
|
124
|
+
*/
|
|
125
|
+
function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()) {
|
|
34
126
|
const baseScore = severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
|
|
127
|
+
// Mistake patterns decay faster (90d) — they're tied to environments that change.
|
|
128
|
+
// Preferences, manager-coaching, and validated-patterns express durable judgment (180d half-life).
|
|
35
129
|
const halfLife = fileType === 'mistake-patterns' ? 90 : 180;
|
|
36
130
|
let daysSinceLastSeen = 0;
|
|
37
131
|
try {
|
|
38
132
|
const lastSeen = new Date(lastSeenDate);
|
|
39
|
-
const now = new Date();
|
|
40
133
|
daysSinceLastSeen = Math.max(0, (now.getTime() - lastSeen.getTime()) / (1000 * 60 * 60 * 24));
|
|
41
134
|
}
|
|
42
135
|
catch {
|
|
@@ -46,55 +139,71 @@ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType) {
|
|
|
46
139
|
const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
|
|
47
140
|
return baseScore * decay * recurrenceBoost;
|
|
48
141
|
}
|
|
49
|
-
function
|
|
142
|
+
function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns') {
|
|
143
|
+
const empty = { active: 0, dormant: 0, agingRisk: 0 };
|
|
50
144
|
if (!(0, fs_1.existsSync)(filePath))
|
|
51
|
-
return
|
|
145
|
+
return empty;
|
|
146
|
+
let content;
|
|
52
147
|
try {
|
|
53
|
-
|
|
54
|
-
const lines = content.split('\n');
|
|
55
|
-
let active = 0;
|
|
56
|
-
let dormant = 0;
|
|
57
|
-
let inEntry = false;
|
|
58
|
-
let currentSeverity = null;
|
|
59
|
-
let currentLastSeen = '';
|
|
60
|
-
let currentRecurrences = 1;
|
|
61
|
-
const processCurrentEntry = () => {
|
|
62
|
-
if (!currentSeverity)
|
|
63
|
-
return;
|
|
64
|
-
const score = computeEffectiveScore(currentSeverity, currentLastSeen, currentRecurrences, 'mistake-patterns');
|
|
65
|
-
if (score >= threshold)
|
|
66
|
-
active++;
|
|
67
|
-
else
|
|
68
|
-
dormant++;
|
|
69
|
-
};
|
|
70
|
-
for (const line of lines) {
|
|
71
|
-
const headerMatch = line.match(/^## \[(P-HIGH|P-MED|P-LOW)\]/);
|
|
72
|
-
if (headerMatch) {
|
|
73
|
-
processCurrentEntry();
|
|
74
|
-
inEntry = true;
|
|
75
|
-
currentSeverity = headerMatch[1];
|
|
76
|
-
currentLastSeen = '';
|
|
77
|
-
currentRecurrences = 1;
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (!inEntry)
|
|
81
|
-
continue;
|
|
82
|
-
const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
|
|
83
|
-
if (lastSeenMatch) {
|
|
84
|
-
currentLastSeen = lastSeenMatch[1].trim();
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
|
|
88
|
-
if (recurrenceMatch) {
|
|
89
|
-
currentRecurrences = parseInt(recurrenceMatch[1], 10);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
processCurrentEntry();
|
|
93
|
-
return { active, dormant };
|
|
148
|
+
content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
94
149
|
}
|
|
95
150
|
catch {
|
|
96
|
-
return
|
|
151
|
+
return empty;
|
|
97
152
|
}
|
|
153
|
+
const lines = content.split(/\r?\n/);
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const horizon = new Date(now.getTime() + AGING_HORIZON_DAYS * 86_400_000);
|
|
156
|
+
let active = 0;
|
|
157
|
+
let dormant = 0;
|
|
158
|
+
let agingRisk = 0;
|
|
159
|
+
let scanned = 0;
|
|
160
|
+
let inEntry = false;
|
|
161
|
+
let severity = null;
|
|
162
|
+
let lastSeen = '';
|
|
163
|
+
let recurrences = 1;
|
|
164
|
+
const flush = () => {
|
|
165
|
+
if (!severity)
|
|
166
|
+
return;
|
|
167
|
+
scanned++;
|
|
168
|
+
const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now);
|
|
169
|
+
if (today >= threshold) {
|
|
170
|
+
active++;
|
|
171
|
+
if (lastSeen) {
|
|
172
|
+
const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon);
|
|
173
|
+
if (future < threshold)
|
|
174
|
+
agingRisk++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
dormant++;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (scanned >= MAX_ENTRIES_SCANNED)
|
|
183
|
+
break;
|
|
184
|
+
const headerMatch = line.match(/^## \[(P-HIGH|P-MED|P-LOW)\]/);
|
|
185
|
+
if (headerMatch) {
|
|
186
|
+
flush();
|
|
187
|
+
inEntry = true;
|
|
188
|
+
severity = headerMatch[1];
|
|
189
|
+
lastSeen = '';
|
|
190
|
+
recurrences = 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!inEntry)
|
|
194
|
+
continue;
|
|
195
|
+
const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
|
|
196
|
+
if (lastSeenMatch) {
|
|
197
|
+
lastSeen = lastSeenMatch[1].trim();
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
|
|
201
|
+
if (recurrenceMatch) {
|
|
202
|
+
recurrences = parseInt(recurrenceMatch[1], 10);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
flush();
|
|
206
|
+
return { active, dormant, agingRisk };
|
|
98
207
|
}
|
|
99
208
|
function readFrontmatter(content) {
|
|
100
209
|
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
@@ -124,28 +233,81 @@ function isUnsynthesizedRetrospective(filePath) {
|
|
|
124
233
|
return false;
|
|
125
234
|
}
|
|
126
235
|
}
|
|
236
|
+
/** Oldest mtime-age in days across this user's L0 signals. 0 if none. */
|
|
237
|
+
function computeOldestL0AgeDays(workspaceRoot, userId) {
|
|
238
|
+
const learningsBase = getLearningsBase(workspaceRoot);
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
let oldest = 0;
|
|
241
|
+
const consider = (filePath) => {
|
|
242
|
+
try {
|
|
243
|
+
const st = (0, fs_1.statSync)(filePath);
|
|
244
|
+
const ageDays = Math.floor((now - st.mtimeMs) / (1000 * 60 * 60 * 24));
|
|
245
|
+
if (ageDays > oldest)
|
|
246
|
+
oldest = ageDays;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// ignore
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const rawDir = (0, path_1.join)(learningsBase, 'raw');
|
|
253
|
+
if ((0, fs_1.existsSync)(rawDir)) {
|
|
254
|
+
try {
|
|
255
|
+
for (const f of (0, fs_1.readdirSync)(rawDir)) {
|
|
256
|
+
if (!f.startsWith(`${userId}-`))
|
|
257
|
+
continue;
|
|
258
|
+
consider((0, path_1.join)(rawDir, f));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const retroDir = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
|
|
266
|
+
if ((0, fs_1.existsSync)(retroDir)) {
|
|
267
|
+
try {
|
|
268
|
+
for (const f of (0, fs_1.readdirSync)(retroDir)) {
|
|
269
|
+
if (!f.startsWith(`${userId}-`) || !f.endsWith('.md'))
|
|
270
|
+
continue;
|
|
271
|
+
if (!isUnsynthesizedRetrospective((0, path_1.join)(retroDir, f)))
|
|
272
|
+
continue;
|
|
273
|
+
consider((0, path_1.join)(retroDir, f));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return oldest;
|
|
281
|
+
}
|
|
127
282
|
function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
128
283
|
const learningsBase = getLearningsBase(workspaceRoot);
|
|
284
|
+
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId);
|
|
129
285
|
const threshold = getScoreThreshold(workspaceRoot);
|
|
130
286
|
const l2MistakePath = (0, path_1.join)(learningsBase, 'org-mistake-patterns.md');
|
|
131
287
|
const l2PrefPath = (0, path_1.join)(learningsBase, 'org-preferences.md');
|
|
132
288
|
const l2CoachPath = (0, path_1.join)(learningsBase, 'org-manager-coaching.md');
|
|
289
|
+
const l2ValidatedPath = (0, path_1.join)(learningsBase, 'org-validated-patterns.md');
|
|
133
290
|
const l2MistakePresent = (0, fs_1.existsSync)(l2MistakePath);
|
|
134
291
|
const l2PrefPresent = (0, fs_1.existsSync)(l2PrefPath);
|
|
135
292
|
const l2CoachPresent = (0, fs_1.existsSync)(l2CoachPath);
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
293
|
+
const l2ValidatedPresent = (0, fs_1.existsSync)(l2ValidatedPath);
|
|
294
|
+
const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2MistakePath, threshold, 'mistake-patterns') : null;
|
|
295
|
+
const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2ValidatedPath, threshold, 'validated-patterns') : null;
|
|
296
|
+
const l1MistakePath = (0, path_1.join)(learningsBase, `${resolvedUserId}-mistake-patterns.md`);
|
|
297
|
+
const l1PrefPath = (0, path_1.join)(learningsBase, `${resolvedUserId}-preferences.md`);
|
|
298
|
+
const l1CoachPath = (0, path_1.join)(learningsBase, `${resolvedUserId}-manager-coaching.md`);
|
|
299
|
+
const l1ValidatedPath = (0, path_1.join)(learningsBase, `${resolvedUserId}-validated-patterns.md`);
|
|
140
300
|
const l1MistakePresent = (0, fs_1.existsSync)(l1MistakePath);
|
|
141
301
|
const l1PrefPresent = (0, fs_1.existsSync)(l1PrefPath);
|
|
142
302
|
const l1CoachPresent = (0, fs_1.existsSync)(l1CoachPath);
|
|
143
|
-
const
|
|
303
|
+
const l1ValidatedPresent = (0, fs_1.existsSync)(l1ValidatedPath);
|
|
304
|
+
const l1MistakeStats = l1MistakePresent ? scanMistakePatternFile(l1MistakePath, threshold, 'mistake-patterns') : null;
|
|
305
|
+
const l1ValidatedStats = l1ValidatedPresent ? scanMistakePatternFile(l1ValidatedPath, threshold, 'validated-patterns') : null;
|
|
144
306
|
let l0CoachingCount = 0;
|
|
145
307
|
const rawPath = (0, path_1.join)(learningsBase, 'raw');
|
|
146
308
|
if ((0, fs_1.existsSync)(rawPath)) {
|
|
147
309
|
try {
|
|
148
|
-
l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${
|
|
310
|
+
l0CoachingCount = (0, fs_1.readdirSync)(rawPath).filter(f => f.startsWith(`${resolvedUserId}-`)).length;
|
|
149
311
|
}
|
|
150
312
|
catch {
|
|
151
313
|
// Ignore read failures.
|
|
@@ -156,15 +318,15 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
156
318
|
if ((0, fs_1.existsSync)(retrospectivesPath)) {
|
|
157
319
|
try {
|
|
158
320
|
l0RetroCount = (0, fs_1.readdirSync)(retrospectivesPath)
|
|
159
|
-
.filter(f => f.startsWith(`${
|
|
321
|
+
.filter(f => f.startsWith(`${resolvedUserId}-`) && f.endsWith('.md'))
|
|
160
322
|
.filter(f => isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, f))).length;
|
|
161
323
|
}
|
|
162
324
|
catch {
|
|
163
325
|
// Ignore read failures.
|
|
164
326
|
}
|
|
165
327
|
}
|
|
166
|
-
const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent;
|
|
167
|
-
const hasL1 = l1MistakePresent || l1PrefPresent || l1CoachPresent;
|
|
328
|
+
const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent || l2ValidatedPresent;
|
|
329
|
+
const hasL1 = l1MistakePresent || l1PrefPresent || l1CoachPresent || l1ValidatedPresent;
|
|
168
330
|
const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
|
|
169
331
|
if (!hasContent)
|
|
170
332
|
return '';
|
|
@@ -179,51 +341,74 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
|
|
|
179
341
|
section += `\`${LEARNINGS_REL}/org-preferences.md\` (all entries)\n`;
|
|
180
342
|
if (l2CoachPresent)
|
|
181
343
|
section += `\`${LEARNINGS_REL}/org-manager-coaching.md\` (all entries)\n`;
|
|
182
|
-
if (
|
|
183
|
-
section +=
|
|
344
|
+
if (l2ValidatedPresent)
|
|
345
|
+
section += `\`${LEARNINGS_REL}/org-validated-patterns.md\` (entries above score threshold)\n`;
|
|
346
|
+
const l2DormantTotal = (l2MistakeStats?.dormant || 0) + (l2ValidatedStats?.dormant || 0);
|
|
347
|
+
if (l2DormantTotal > 0) {
|
|
348
|
+
section += `Dormant: ${l2DormantTotal} org pattern${l2DormantTotal !== 1 ? 's' : ''} below threshold\n`;
|
|
184
349
|
}
|
|
185
350
|
section += '\n';
|
|
186
351
|
}
|
|
187
352
|
if (hasL1) {
|
|
188
353
|
section += '### L1 - Your patterns\n';
|
|
189
354
|
if (l1PrefPresent)
|
|
190
|
-
section += `\`${LEARNINGS_REL}/${
|
|
355
|
+
section += `\`${LEARNINGS_REL}/${resolvedUserId}-preferences.md\` (all entries)\n`;
|
|
191
356
|
if (l1CoachPresent)
|
|
192
|
-
section += `\`${LEARNINGS_REL}/${
|
|
357
|
+
section += `\`${LEARNINGS_REL}/${resolvedUserId}-manager-coaching.md\` (all entries)\n`;
|
|
193
358
|
if (l1MistakePresent)
|
|
194
|
-
section += `\`${LEARNINGS_REL}/${
|
|
195
|
-
if (
|
|
196
|
-
section +=
|
|
359
|
+
section += `\`${LEARNINGS_REL}/${resolvedUserId}-mistake-patterns.md\` (entries above score threshold)\n`;
|
|
360
|
+
if (l1ValidatedPresent)
|
|
361
|
+
section += `\`${LEARNINGS_REL}/${resolvedUserId}-validated-patterns.md\` (entries above score threshold)\n`;
|
|
362
|
+
const l1DormantTotal = (l1MistakeStats?.dormant || 0) + (l1ValidatedStats?.dormant || 0);
|
|
363
|
+
if (l1DormantTotal > 0) {
|
|
364
|
+
section += `Dormant: ${l1DormantTotal} personal pattern${l1DormantTotal !== 1 ? 's' : ''} below threshold\n`;
|
|
197
365
|
}
|
|
198
366
|
section += '\n';
|
|
199
367
|
}
|
|
200
368
|
if (l0CoachingCount > 0 || l0RetroCount > 0) {
|
|
201
369
|
section += '### L0 - Your unprocessed signals\n';
|
|
202
370
|
if (l0CoachingCount > 0) {
|
|
203
|
-
section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${LEARNINGS_REL}/raw/${
|
|
371
|
+
section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${LEARNINGS_REL}/raw/${resolvedUserId}-*\`\n`;
|
|
204
372
|
}
|
|
205
373
|
if (l0RetroCount > 0) {
|
|
206
|
-
section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${
|
|
374
|
+
section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${resolvedUserId}-*\` with \`synthesized: false\` or missing\n`;
|
|
207
375
|
}
|
|
208
376
|
section += '\n';
|
|
209
377
|
}
|
|
210
378
|
const totalL0 = l0CoachingCount + l0RetroCount;
|
|
379
|
+
const oldestAgeDays = totalL0 > 0 ? computeOldestL0AgeDays(workspaceRoot, resolvedUserId) : 0;
|
|
380
|
+
const agingRisk = l1MistakeStats?.agingRisk ?? 0;
|
|
381
|
+
const backlogTriggered = totalL0 >= BACKLOG_MIN || (oldestAgeDays >= OLDEST_AGE_DAYS_TRIGGER && totalL0 > 0);
|
|
211
382
|
if (forJob) {
|
|
212
383
|
if (hasL2 || hasL1) {
|
|
213
384
|
section += 'Use the relevant patterns, preferences, and coaching signals in this job.\n';
|
|
214
385
|
}
|
|
215
|
-
if (
|
|
386
|
+
if (backlogTriggered) {
|
|
216
387
|
section += '\n';
|
|
217
388
|
section += `Warning: ${totalL0} unprocessed signals pending. Consider running \`end-of-day-debrief\` before starting today's work.\n`;
|
|
389
|
+
section += renderBacklogDetail(oldestAgeDays, agingRisk);
|
|
218
390
|
}
|
|
219
391
|
}
|
|
220
392
|
else {
|
|
221
393
|
section += 'Use this synthesized learning context throughout the session.\n';
|
|
222
|
-
if (
|
|
394
|
+
if (backlogTriggered) {
|
|
223
395
|
section += '\n';
|
|
224
396
|
section += `Warning: synthesis overdue with ${totalL0} unprocessed signals.\n`;
|
|
225
397
|
section += 'Run `end-of-day-debrief` before starting today\'s work.\n';
|
|
398
|
+
section += renderBacklogDetail(oldestAgeDays, agingRisk);
|
|
226
399
|
}
|
|
227
400
|
}
|
|
228
401
|
return section;
|
|
229
402
|
}
|
|
403
|
+
function renderBacklogDetail(oldestAgeDays, agingRisk) {
|
|
404
|
+
if (oldestAgeDays <= 0 && agingRisk <= 0)
|
|
405
|
+
return '';
|
|
406
|
+
const parts = [];
|
|
407
|
+
if (oldestAgeDays > 0)
|
|
408
|
+
parts.push(`oldest ${oldestAgeDays}d`);
|
|
409
|
+
parts.push('debrief takes ~3 minutes');
|
|
410
|
+
if (agingRisk > 0) {
|
|
411
|
+
parts.push(`${agingRisk} high-score pattern${agingRisk !== 1 ? 's' : ''} aging out within ${AGING_HORIZON_DAYS}d`);
|
|
412
|
+
}
|
|
413
|
+
return `Detail: ${parts.join('; ')}.\n`;
|
|
414
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.122",
|
|
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": {
|