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.
@@ -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 the FRAIM slash command for Claude Code at the user level.
25
- * Writes to ~/.claude/commands/fraim.md and does not overwrite existing files.
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, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildClaudeSlashCommandContent)(), 'Claude slash command (~/.claude/commands/fraim.md)');
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 exports.FRAIM_INVOCATION_BODY;
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: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSlashCommandContent)() },
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 getScoreThreshold(workspaceRoot) {
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
- const config = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
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 to default.
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
- function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType) {
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 countMistakePatternEntries(filePath, threshold) {
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 { active: 0, dormant: 0 };
145
+ return empty;
146
+ let content;
52
147
  try {
53
- const content = (0, fs_1.readFileSync)(filePath, 'utf8');
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 { active: 0, dormant: 0 };
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 l2MistakeCounts = l2MistakePresent ? countMistakePatternEntries(l2MistakePath, threshold) : null;
137
- const l1MistakePath = (0, path_1.join)(learningsBase, `${userId}-mistake-patterns.md`);
138
- const l1PrefPath = (0, path_1.join)(learningsBase, `${userId}-preferences.md`);
139
- const l1CoachPath = (0, path_1.join)(learningsBase, `${userId}-manager-coaching.md`);
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 l1MistakeCounts = l1MistakePresent ? countMistakePatternEntries(l1MistakePath, threshold) : null;
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(`${userId}-`)).length;
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(`${userId}-`) && f.endsWith('.md'))
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 (l2MistakeCounts && l2MistakeCounts.dormant > 0) {
183
- section += `Dormant: ${l2MistakeCounts.dormant} org pattern${l2MistakeCounts.dormant !== 1 ? 's' : ''} below threshold\n`;
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}/${userId}-preferences.md\` (all entries)\n`;
355
+ section += `\`${LEARNINGS_REL}/${resolvedUserId}-preferences.md\` (all entries)\n`;
191
356
  if (l1CoachPresent)
192
- section += `\`${LEARNINGS_REL}/${userId}-manager-coaching.md\` (all entries)\n`;
357
+ section += `\`${LEARNINGS_REL}/${resolvedUserId}-manager-coaching.md\` (all entries)\n`;
193
358
  if (l1MistakePresent)
194
- section += `\`${LEARNINGS_REL}/${userId}-mistake-patterns.md\` (entries above score threshold)\n`;
195
- if (l1MistakeCounts && l1MistakeCounts.dormant > 0) {
196
- section += `Dormant: ${l1MistakeCounts.dormant} personal pattern${l1MistakeCounts.dormant !== 1 ? 's' : ''} below threshold\n`;
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/${userId}-*\`\n`;
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/${userId}-*\` with \`synthesized: false\` or missing\n`;
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 (totalL0 >= 5) {
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 (totalL0 >= 5) {
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.119",
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": {