fraim 2.0.117 → 2.0.120

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.
@@ -268,7 +268,8 @@ const listSupportedIDEs = () => {
268
268
  console.log(chalk_1.default.gray(` Config: ${ide.configPath}\n`));
269
269
  });
270
270
  console.log(chalk_1.default.yellow('šŸ’” Use "fraim add-ide --ide <name>" to configure a specific IDE'));
271
- console.log(chalk_1.default.yellow(' Example: fraim add-ide --ide claude'));
271
+ console.log(chalk_1.default.yellow(' Example: fraim add-ide --ide claude-code'));
272
+ console.log(chalk_1.default.yellow(' Anthropic aliases: claude, claude-code, claude-desktop, claude-cowork'));
272
273
  };
273
274
  const promptForIDESelection = async (availableIDEs, tokens) => {
274
275
  console.log(chalk_1.default.green(`āœ… Found ${availableIDEs.length} IDEs that can be configured:\n`));
@@ -356,7 +357,7 @@ const runAddIDE = async (options) => {
356
357
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
357
358
  if (detectedIDEs.length === 0) {
358
359
  console.log(chalk_1.default.yellow('āš ļø No supported IDEs detected on your system.'));
359
- console.log(chalk_1.default.gray('Supported IDEs: Claude, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
360
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
360
361
  console.log(chalk_1.default.blue('\nšŸ’” Install an IDE and run this command again.'));
361
362
  return;
362
363
  }
@@ -420,16 +421,21 @@ const runAddIDE = async (options) => {
420
421
  });
421
422
  }
422
423
  if (results.successful.length > 0) {
424
+ const { describeConfiguredInvocationSurfaces } = await Promise.resolve().then(() => __importStar(require('../setup/ide-global-integration')));
425
+ const successfulIDEs = idesToConfigure.filter(ide => results.successful.includes(ide.name));
426
+ const invocationSummaries = describeConfiguredInvocationSurfaces(successfulIDEs);
423
427
  console.log(chalk_1.default.blue('\nšŸ”„ Next steps:'));
424
428
  console.log(chalk_1.default.cyan(' 1. Restart your configured IDEs'));
425
- console.log(chalk_1.default.cyan(' 2. In any FRAIM-enabled repo, tell your AI agent: "Onboard this project"'));
429
+ invocationSummaries.forEach((summary, index) => {
430
+ console.log(chalk_1.default.cyan(` ${index + 2}. ${summary}`));
431
+ });
426
432
  console.log(chalk_1.default.blue('\nšŸ’” Use "fraim doctor --test-mcp" to verify the configuration.'));
427
433
  }
428
434
  };
429
435
  exports.runAddIDE = runAddIDE;
430
436
  exports.addIDECommand = new commander_1.Command('add-ide')
431
437
  .description('Add FRAIM configuration to additional IDEs')
432
- .option('--ide <name>', 'Configure specific IDE (claude, claude-code, antigravity, kiro, cursor, vscode, codex, windsurf)')
438
+ .option('--ide <name>', 'Configure specific IDE (claude, claude-code, claude-desktop, claude-cowork, antigravity, kiro, cursor, vscode, codex, windsurf)')
433
439
  .option('--all', 'Configure all detected IDEs')
434
440
  .option('--list', 'List all supported IDEs and their detection status')
435
441
  .action(exports.runAddIDE);
@@ -681,12 +681,12 @@ const runSetup = async (options) => {
681
681
  else {
682
682
  console.log(chalk_1.default.cyan('\n Get started — open your AI tool and:'));
683
683
  }
684
- // Check if Claude is among configured IDEs
685
- const hasClaude = configuredIDEs.some(ide => ide.name.toLowerCase().includes('claude'));
686
- if (hasClaude) {
687
- console.log(chalk_1.default.white(' /fraim ') + chalk_1.default.gray('(Claude Code) Browse all jobs'));
688
- }
689
- console.log(chalk_1.default.white(' "What can FRAIM help me with?" ') + chalk_1.default.gray('Works in any AI tool'));
684
+ const { describeConfiguredInvocationSurfaces } = await Promise.resolve().then(() => __importStar(require('../setup/ide-global-integration')));
685
+ const invocationSummaries = describeConfiguredInvocationSurfaces(configuredIDEs);
686
+ invocationSummaries.forEach((summary) => {
687
+ console.log(chalk_1.default.white(` ${summary}`));
688
+ });
689
+ console.log(chalk_1.default.white(' "What can FRAIM help me with?"'));
690
690
  console.log(chalk_1.default.gray('\n Just tell your AI what you need — FRAIM will find the right job.'));
691
691
  if (mode !== 'conversational') {
692
692
  console.log(chalk_1.default.cyan('\n To set up FRAIM in a specific project:'));
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveManagedCommand = exports.getPortableNpxCommand = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
10
+ const getPortableNodeRoot = () => path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'node');
11
+ const getPortableNpxCandidates = () => {
12
+ const nodeRoot = getPortableNodeRoot();
13
+ if (process.platform === 'win32') {
14
+ const candidates = [path_1.default.join(nodeRoot, 'npx.cmd')];
15
+ if (fs_1.default.existsSync(nodeRoot)) {
16
+ const extractedDirs = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
17
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
18
+ .sort((a, b) => b.name.localeCompare(a.name));
19
+ for (const entry of extractedDirs) {
20
+ candidates.push(path_1.default.join(nodeRoot, entry.name, 'npx.cmd'));
21
+ }
22
+ }
23
+ return candidates;
24
+ }
25
+ return [
26
+ path_1.default.join(nodeRoot, 'bin', 'npx'),
27
+ path_1.default.join(nodeRoot, 'npx')
28
+ ];
29
+ };
30
+ const getPortableNpxCommand = () => {
31
+ for (const candidate of getPortableNpxCandidates()) {
32
+ if (fs_1.default.existsSync(candidate)) {
33
+ return candidate;
34
+ }
35
+ }
36
+ return null;
37
+ };
38
+ exports.getPortableNpxCommand = getPortableNpxCommand;
39
+ const resolveManagedCommand = (command) => {
40
+ if (command !== 'npx') {
41
+ return command;
42
+ }
43
+ return (0, exports.getPortableNpxCommand)() || command;
44
+ };
45
+ exports.resolveManagedCommand = resolveManagedCommand;
@@ -6,6 +6,7 @@ exports.IDE_FORMATS = exports.CodexFormat = exports.WindsurfFormat = exports.Cla
6
6
  exports.getIDEFormat = getIDEFormat;
7
7
  const mcp_server_registry_1 = require("./mcp-server-registry");
8
8
  const provider_registry_1 = require("../providers/provider-registry");
9
+ const command_resolution_1 = require("./command-resolution");
9
10
  // Standard format (Cursor, Kiro, Antigravity, etc.)
10
11
  class StandardFormat {
11
12
  constructor() {
@@ -160,7 +161,7 @@ class WindsurfFormat {
160
161
  if (tokenEnvVar && server.headers?.Authorization) {
161
162
  const token = server.headers.Authorization.replace('Bearer ', '');
162
163
  mcpServers[key] = {
163
- command: 'npx',
164
+ command: (0, command_resolution_1.resolveManagedCommand)('npx'),
164
165
  args: ['-y', '@modelcontextprotocol/server-fetch', server.url],
165
166
  env: {
166
167
  [tokenEnvVar]: token
@@ -9,13 +9,14 @@ exports.isProviderServer = isProviderServer;
9
9
  exports.isHTTPServer = isHTTPServer;
10
10
  exports.buildAllBaseServers = buildAllBaseServers;
11
11
  const provider_registry_1 = require("../providers/provider-registry");
12
+ const command_resolution_1 = require("./command-resolution");
12
13
  exports.BASE_MCP_SERVERS = [
13
14
  {
14
15
  id: 'git',
15
16
  name: 'Git',
16
17
  description: 'Git repository operations (commit, branch, merge, etc.)',
17
18
  buildServer: () => ({
18
- command: 'npx',
19
+ command: (0, command_resolution_1.resolveManagedCommand)('npx'),
19
20
  args: ['-y', '@cyanheads/git-mcp-server']
20
21
  })
21
22
  },
@@ -24,7 +25,7 @@ exports.BASE_MCP_SERVERS = [
24
25
  name: 'Playwright',
25
26
  description: 'Browser automation and testing',
26
27
  buildServer: () => ({
27
- command: 'npx',
28
+ command: (0, command_resolution_1.resolveManagedCommand)('npx'),
28
29
  args: ['-y', '@playwright/mcp']
29
30
  })
30
31
  },
@@ -33,7 +34,7 @@ exports.BASE_MCP_SERVERS = [
33
34
  name: 'FRAIM',
34
35
  description: 'FRAIM job orchestration and mentoring',
35
36
  buildServer: (fraimKey) => ({
36
- command: 'npx',
37
+ command: (0, command_resolution_1.resolveManagedCommand)('npx'),
37
38
  args: ['-y', 'fraim-framework@latest', 'mcp'],
38
39
  env: {
39
40
  // Include API key for IDE configs (Codex, VSCode, etc.)
@@ -119,7 +120,7 @@ function buildStdioServer(mcpConfig, token, config) {
119
120
  }
120
121
  }
121
122
  return {
122
- command: mcpConfig.command,
123
+ command: (0, command_resolution_1.resolveManagedCommand)(mcpConfig.command),
123
124
  args: (mcpConfig.args || []).map(resolveTemplate),
124
125
  env
125
126
  };
@@ -55,6 +55,10 @@ const normalizePlatformTokens = (tokenInput) => {
55
55
  };
56
56
  const promptForIDESelection = async (detectedIDEs, tokenInput) => {
57
57
  const tokens = normalizePlatformTokens(tokenInput);
58
+ if (process.env.FRAIM_NON_INTERACTIVE) {
59
+ console.log(chalk_1.default.yellow(`\nā„¹ļø Non-interactive mode: configuring all detected IDEs (${detectedIDEs.length})`));
60
+ return detectedIDEs;
61
+ }
58
62
  console.log(chalk_1.default.green(`āœ… Found ${detectedIDEs.length} IDEs that can be configured with FRAIM:\n`));
59
63
  detectedIDEs.forEach((ide, index) => {
60
64
  const configExists = fs_1.default.existsSync((0, ide_detector_1.expandPath)(ide.configPath));
@@ -253,6 +257,10 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
253
257
  console.log(chalk_1.default.gray('Supported IDEs: Claude, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
254
258
  console.log(chalk_1.default.blue('\nšŸ’” You can install an IDE and run setup again later.'));
255
259
  console.log(chalk_1.default.gray(' Or continue with manual MCP configuration.'));
260
+ if (process.env.FRAIM_NON_INTERACTIVE) {
261
+ console.log(chalk_1.default.yellow('ā„¹ļø Non-interactive mode: skipping IDE configuration because no supported IDEs were detected.'));
262
+ return;
263
+ }
256
264
  const continueAnyway = await (0, prompts_1.default)({
257
265
  type: 'confirm',
258
266
  name: 'continue',
@@ -60,28 +60,33 @@ exports.IDE_CONFIGS = [
60
60
  configPath: '~/.claude.json',
61
61
  configFormat: 'json',
62
62
  configType: 'claude-code',
63
+ invocationProfile: 'claude-slash',
63
64
  detectMethod: detectClaude,
65
+ aliases: ['claude-code', 'claude code', 'claude code cli', 'code tab'],
64
66
  alternativePaths: [
65
67
  '~/.claude/settings.json'
66
68
  ],
67
- description: 'Anthropic Claude Code CLI tool'
69
+ description: 'Anthropic Claude Code local surfaces (CLI and Desktop Code tab)'
68
70
  },
69
71
  {
70
72
  name: 'Claude Desktop / Cowork',
71
73
  configPath: '~/AppData/Roaming/Claude/claude_desktop_config.json',
72
74
  configFormat: 'json',
73
75
  configType: 'claude',
76
+ invocationProfile: 'launch-phrase',
74
77
  detectMethod: detectClaude,
78
+ aliases: ['claude', 'claude-desktop', 'claude desktop', 'claude-cowork', 'cowork', 'claude cowork'],
75
79
  alternativePaths: [
76
80
  '~/Library/Application Support/Claude/claude_desktop_config.json'
77
81
  ],
78
- description: 'Anthropic Claude Desktop and Cowork application'
82
+ description: 'Anthropic Claude Desktop chat and Cowork surfaces'
79
83
  },
80
84
  {
81
85
  name: 'Antigravity',
82
86
  configPath: '~/.gemini/antigravity/mcp_config.json',
83
87
  configFormat: 'json',
84
88
  configType: 'standard',
89
+ invocationProfile: 'instructions-only',
85
90
  detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
86
91
  description: 'Google Gemini Antigravity IDE'
87
92
  },
@@ -90,6 +95,7 @@ exports.IDE_CONFIGS = [
90
95
  configPath: '~/.kiro/settings/mcp.json',
91
96
  configFormat: 'json',
92
97
  configType: 'kiro',
98
+ invocationProfile: 'kiro-hashtag',
93
99
  detectMethod: () => fs_1.default.existsSync(expandPath('~/.kiro')),
94
100
  description: 'Kiro AI-powered IDE'
95
101
  },
@@ -98,6 +104,7 @@ exports.IDE_CONFIGS = [
98
104
  configPath: '~/.cursor/mcp.json',
99
105
  configFormat: 'json',
100
106
  configType: 'kiro',
107
+ invocationProfile: 'cursor-mention',
101
108
  detectMethod: detectCursor,
102
109
  alternativePaths: [
103
110
  '~/Library/Application Support/Cursor/mcp.json',
@@ -115,6 +122,7 @@ exports.IDE_CONFIGS = [
115
122
  : '~/.config/Code/User/mcp.json',
116
123
  configFormat: 'json',
117
124
  configType: 'vscode',
125
+ invocationProfile: 'vscode-prompt',
118
126
  detectMethod: detectVSCode,
119
127
  alternativePaths: [
120
128
  '~/Library/Application Support/Code/User/mcp.json',
@@ -128,6 +136,7 @@ exports.IDE_CONFIGS = [
128
136
  configPath: '~/.codex/config.toml',
129
137
  configFormat: 'toml',
130
138
  configType: 'codex',
139
+ invocationProfile: 'codex-skill',
131
140
  detectMethod: () => fs_1.default.existsSync(expandPath('~/.codex')),
132
141
  description: 'Codex AI development environment'
133
142
  },
@@ -136,6 +145,7 @@ exports.IDE_CONFIGS = [
136
145
  configPath: '~/.codeium/windsurf/mcp_config.json',
137
146
  configFormat: 'json',
138
147
  configType: 'windsurf',
148
+ invocationProfile: 'windsurf-command',
139
149
  detectMethod: detectWindsurf,
140
150
  alternativePaths: [
141
151
  '~/Library/Application Support/Windsurf/mcp_config.json',
@@ -173,7 +183,9 @@ const getAllSupportedIDEs = () => {
173
183
  };
174
184
  exports.getAllSupportedIDEs = getAllSupportedIDEs;
175
185
  const findIDEByName = (name) => {
176
- return exports.IDE_CONFIGS.find(ide => ide.name.toLowerCase().includes(name.toLowerCase()) ||
177
- name.toLowerCase().includes(ide.name.toLowerCase()));
186
+ const normalized = name.toLowerCase();
187
+ return exports.IDE_CONFIGS.find(ide => ide.name.toLowerCase().includes(normalized) ||
188
+ normalized.includes(ide.name.toLowerCase()) ||
189
+ ide.aliases?.some(alias => alias.includes(normalized) || normalized.includes(alias)));
178
190
  };
179
191
  exports.findIDEByName = findIDEByName;
@@ -3,122 +3,62 @@ 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.describeConfiguredInvocationSurfaces = describeConfiguredInvocationSurfaces;
6
7
  exports.installSlashCommands = installSlashCommands;
7
8
  exports.installGlobalRules = installGlobalRules;
8
9
  /**
9
10
  * IDE Global Integration
10
11
  *
11
- * Installs FRAIM slash commands and global rules into user-level IDE
12
- * configuration directories. These enable FRAIM to be discoverable
13
- * in any project without requiring fraim init-project.
14
- *
15
- * Part of: User-Level FRAIM Artifacts with Local Shadow Semantics
12
+ * Installs user-level FRAIM invocation artifacts for supported IDEs.
13
+ * These surfaces make FRAIM discoverable without requiring `fraim init-project`.
16
14
  */
17
15
  const fs_1 = __importDefault(require("fs"));
18
- const path_1 = __importDefault(require("path"));
19
16
  const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
20
18
  const chalk_1 = __importDefault(require("chalk"));
21
- const FRAIM_SLASH_COMMAND_CONTENT = `The user wants to run FRAIM. The requested job or topic is: $ARGUMENTS
22
-
23
- Follow this process:
24
-
25
- 1. **If no argument was given** (the line above ends with ": "):
26
- Call \`list_fraim_jobs()\` to discover available jobs. Present the results to the user grouped by business function (the server returns jobs organized by category — use those categories as group headings). For each group, list 3-5 of the most impactful jobs with a one-line description.
27
-
28
- After listing, suggest 2-3 starting points based on what seems most relevant:
29
- - If in a code repo: suggest jobs from engineering/product-building categories
30
- - If no repo context: suggest jobs from marketing, fundraising, or business categories
31
-
32
- Ask the user which job they want to run, then proceed to step 2.
33
-
34
- 2. **Find the match**: from the list returned by \`list_fraim_jobs()\`, find the job whose name matches or closely resembles the argument. If no job matches, search for a matching skill by calling \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` — try common categories like \`engineering/\`, \`marketing/\`, \`business/\`, \`product-management/\`, \`ai-tools/\`. Confirm the match with the user.
35
-
36
- 3. **Load the full content**:
37
- - For jobs: call \`get_fraim_job({ job: "<matched-job-name>" })\` — never execute from stub content.
38
- - For skills: the content from \`get_fraim_file\` is the full skill. Use it directly.
39
-
40
- 4. **Execute**: for jobs, follow the phased instructions returned by \`get_fraim_job\`, using \`seekMentoring\` at phase transitions where indicated. For skills, apply the skill steps to the user's current context.
41
- `;
42
- const FRAIM_CURSOR_RULES_CONTENT = `# FRAIM Rules
43
-
44
- This workspace uses FRAIM (Framework for Rigor-based AI Management).
45
-
46
- ## Discovery
47
- - Before acting on any user request, check if a FRAIM job matches the intent.
48
- - Call \`list_fraim_jobs()\` to discover available jobs.
49
- - Call \`get_fraim_job({ job: "<job-name>" })\` to get full phased instructions.
50
-
51
- ## Execution
52
- - Jobs are FRAIM's primary execution units — structured, multi-phase workflows.
53
- - Skills are reusable capabilities that jobs compose.
54
- - Rules are always-on constraints and conventions.
55
- - Follow phased instructions and use \`seekMentoring\` at phase transitions.
56
-
57
- ## Principles
58
- - Never execute from job stubs — always load full instructions via MCP.
59
- - Follow the constitution: integrity, correctness, contribution, completeness.
60
- `;
19
+ const ide_invocation_surfaces_1 = require("./ide-invocation-surfaces");
20
+ function describeConfiguredInvocationSurfaces(installedIDEs) {
21
+ return installedIDEs.map((ide) => (0, ide_invocation_surfaces_1.describeInvocationSurface)(ide.name, ide.invocationProfile));
22
+ }
61
23
  /**
62
24
  * Install the FRAIM slash command for Claude Code at the user level.
63
- * Writes to ~/.claude/commands/fraim.md.
64
- * Does NOT overwrite if the file already exists.
65
- *
66
- * @param homeDir - Override for home directory (for testing)
25
+ * Writes to ~/.claude/commands/fraim.md and does not overwrite existing files.
67
26
  */
68
27
  async function installSlashCommands(homeDir) {
69
28
  const home = homeDir || os_1.default.homedir();
70
29
  const claudeDir = path_1.default.join(home, '.claude');
71
- // Only install if Claude Code is installed (indicated by .claude/ existing)
72
30
  if (!fs_1.default.existsSync(claudeDir)) {
73
31
  return;
74
32
  }
75
- const commandsDir = path_1.default.join(claudeDir, 'commands');
76
- const slashCommandPath = path_1.default.join(commandsDir, 'fraim.md');
77
- // Do not overwrite existing file
78
- if (fs_1.default.existsSync(slashCommandPath)) {
79
- console.log(chalk_1.default.gray(' Claude slash command already exists — skipping'));
80
- return;
81
- }
82
- fs_1.default.mkdirSync(commandsDir, { recursive: true });
83
- fs_1.default.writeFileSync(slashCommandPath, FRAIM_SLASH_COMMAND_CONTENT, 'utf8');
84
- console.log(chalk_1.default.green(' āœ… Installed Claude slash command (~/.claude/commands/fraim.md)'));
33
+ installFileIfMissing(path_1.default.join(claudeDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildClaudeSlashCommandContent)(), 'Claude slash command (~/.claude/commands/fraim.md)');
85
34
  }
86
35
  /**
87
- * Install FRAIM global rules/instructions for supported IDEs.
36
+ * Install FRAIM invocation artifacts for non-Claude IDEs.
88
37
  * Supports: Cursor, Codex, Windsurf, Kiro
89
- * Does NOT overwrite if files already exist.
90
- *
91
- * @param homeDir - Override for home directory (for testing)
38
+ * Does not overwrite existing files.
92
39
  */
93
40
  async function installGlobalRules(homeDir) {
94
41
  const home = homeDir || os_1.default.homedir();
95
- // Cursor: ~/.cursor/rules/fraim-rules.md
96
42
  const cursorDir = path_1.default.join(home, '.cursor');
97
43
  if (fs_1.default.existsSync(cursorDir)) {
98
- installRuleFile(path_1.default.join(cursorDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Cursor global rules (~/.cursor/rules/fraim-rules.md)');
44
+ installFileIfMissing(path_1.default.join(cursorDir, 'rules', 'fraim.mdc'), (0, ide_invocation_surfaces_1.buildCursorMentionRuleContent)(), 'Cursor FRAIM rule (~/.cursor/rules/fraim.mdc)');
99
45
  }
100
- // Codex: ~/.codex/instructions.md
101
46
  const codexDir = path_1.default.join(home, '.codex');
102
47
  if (fs_1.default.existsSync(codexDir)) {
103
- installRuleFile(path_1.default.join(codexDir, 'instructions.md'), FRAIM_CURSOR_RULES_CONTENT, 'Codex global instructions (~/.codex/instructions.md)');
48
+ installFileIfMissing(path_1.default.join(codexDir, 'skills', 'fraim', 'SKILL.md'), (0, ide_invocation_surfaces_1.buildCodexSkillContent)(), 'Codex FRAIM skill (~/.codex/skills/fraim/SKILL.md)');
104
49
  }
105
- // Windsurf: ~/.codeium/windsurf/rules/fraim-rules.md
106
50
  const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
107
51
  if (fs_1.default.existsSync(windsurfDir)) {
108
- installRuleFile(path_1.default.join(windsurfDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Windsurf global rules (~/.codeium/windsurf/rules/fraim-rules.md)');
52
+ installFileIfMissing(path_1.default.join(windsurfDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)(), 'Windsurf FRAIM command (~/.codeium/windsurf/commands/fraim.md)');
109
53
  }
110
- // Kiro: ~/.kiro/rules/fraim-rules.md
111
54
  const kiroDir = path_1.default.join(home, '.kiro');
112
55
  if (fs_1.default.existsSync(kiroDir)) {
113
- installRuleFile(path_1.default.join(kiroDir, 'rules', 'fraim-rules.md'), FRAIM_CURSOR_RULES_CONTENT, 'Kiro global rules (~/.kiro/rules/fraim-rules.md)');
56
+ installFileIfMissing(path_1.default.join(kiroDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildKiroCommandContent)(), 'Kiro FRAIM command (~/.kiro/commands/fraim.md)');
114
57
  }
115
58
  }
116
- /**
117
- * Install a rule file if it doesn't already exist.
118
- */
119
- function installRuleFile(filePath, content, displayName) {
59
+ function installFileIfMissing(filePath, content, displayName) {
120
60
  if (fs_1.default.existsSync(filePath)) {
121
- console.log(chalk_1.default.gray(` ${displayName.split('(')[0].trim()} already exist — skipping`));
61
+ console.log(chalk_1.default.gray(` ${displayName.split('(')[0].trim()} already exists — skipping`));
122
62
  return;
123
63
  }
124
64
  fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true });
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FRAIM_INVOCATION_BODY = exports.CURSOR_MDC_FRONTMATTER = exports.FRAIM_LAUNCH_PHRASE = void 0;
4
+ exports.buildClaudeSlashCommandContent = buildClaudeSlashCommandContent;
5
+ exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
6
+ exports.buildCodexSkillContent = buildCodexSkillContent;
7
+ exports.buildWindsurfCommandContent = buildWindsurfCommandContent;
8
+ exports.buildKiroCommandContent = buildKiroCommandContent;
9
+ exports.describeInvocationSurface = describeInvocationSurface;
10
+ exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
11
+ exports.CURSOR_MDC_FRONTMATTER = `---
12
+ description: FRAIM discovery and execution contract
13
+ alwaysApply: true
14
+ ---`;
15
+ exports.FRAIM_INVOCATION_BODY = `Follow this process:
16
+
17
+ 1. **If the user did not specify a FRAIM job or topic**:
18
+ 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.
19
+
20
+ 2. **Find the match**:
21
+ Match the user's request to a FRAIM job from \`list_fraim_jobs()\`. If no job matches, try a likely FRAIM skill with \`get_fraim_file({ path: "skills/<likely-category>/<argument>.md" })\` and confirm the match with the user.
22
+
23
+ 3. **Load the full content**:
24
+ - For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
25
+ - For skills, use the content returned by \`get_fraim_file(...)\`.
26
+
27
+ 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
+ `;
31
+ function buildClaudeSlashCommandContent() {
32
+ return exports.FRAIM_INVOCATION_BODY;
33
+ }
34
+ function buildCursorMentionRuleContent() {
35
+ return `${exports.CURSOR_MDC_FRONTMATTER}
36
+
37
+ # FRAIM
38
+
39
+ ${exports.FRAIM_INVOCATION_BODY}
40
+ `;
41
+ }
42
+ function buildCodexSkillContent() {
43
+ return `# FRAIM
44
+
45
+ ${exports.FRAIM_INVOCATION_BODY}`;
46
+ }
47
+ function buildWindsurfCommandContent() {
48
+ return `# FRAIM
49
+
50
+ ${exports.FRAIM_INVOCATION_BODY}`;
51
+ }
52
+ function buildKiroCommandContent() {
53
+ return `# FRAIM
54
+
55
+ ${exports.FRAIM_INVOCATION_BODY}`;
56
+ }
57
+ function describeInvocationSurface(ideName, invocationProfile) {
58
+ switch (invocationProfile) {
59
+ case 'claude-slash':
60
+ return ideName === 'Claude Code'
61
+ ? 'Claude Code (CLI/Desktop): /fraim'
62
+ : `${ideName}: /fraim`;
63
+ case 'launch-phrase':
64
+ return `${ideName}: "${exports.FRAIM_LAUNCH_PHRASE}"`;
65
+ case 'cursor-mention':
66
+ return `${ideName}: @fraim`;
67
+ case 'vscode-prompt':
68
+ return `${ideName}: /fraim via workspace prompt`;
69
+ case 'codex-skill':
70
+ return `${ideName}: /fraim, $fraim`;
71
+ case 'windsurf-command':
72
+ return `${ideName}: /fraim`;
73
+ case 'kiro-hashtag':
74
+ return `${ideName}: #fraim`;
75
+ case 'instructions-only':
76
+ default:
77
+ return `${ideName}: natural language`;
78
+ }
79
+ }
@@ -6,15 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ensureAgentAdapterFiles = ensureAgentAdapterFiles;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const ide_invocation_surfaces_1 = require("../setup/ide-invocation-surfaces");
9
10
  const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
10
11
  const START_MARKER = '<!-- FRAIM_AGENT_ADAPTER_START -->';
11
12
  const END_MARKER = '<!-- FRAIM_AGENT_ADAPTER_END -->';
12
13
  const CURSOR_RULE_PATH = path_1.default.join('.cursor', 'rules', 'fraim.mdc');
13
14
  const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fraim.md');
14
- const CURSOR_FRONTMATTER = `---
15
- description: FRAIM discovery and execution contract
16
- alwaysApply: true
17
- ---`;
15
+ const VSCODE_FRAIM_PROMPT_PATH = path_1.default.join('.github', 'prompts', 'fraim.prompt.md');
16
+ const CODEX_FRAIM_SKILL_PATH = path_1.default.join('.codex', 'skills', 'fraim', 'SKILL.md');
17
+ const WINDSURF_FRAIM_COMMAND_PATH = path_1.default.join('.windsurf', 'commands', 'fraim.md');
18
+ const KIRO_FRAIM_COMMAND_PATH = path_1.default.join('.kiro', 'commands', 'fraim.md');
18
19
  function buildManagedSection(body) {
19
20
  return `${START_MARKER}
20
21
  ${body.trim()}
@@ -40,7 +41,7 @@ function mergeCursorRule(existingContent, managedSection) {
40
41
  const normalized = existingContent.replace(/\r\n/g, '\n').trimStart();
41
42
  const bodyWithoutLeadingFrontmatter = normalized.replace(/^(?:---\n[\s\S]*?\n---(?:\n+)?)*/, '');
42
43
  const mergedBody = mergeManagedSection(bodyWithoutLeadingFrontmatter, managedSection).trim();
43
- return `${CURSOR_FRONTMATTER}\n\n${mergedBody}\n`;
44
+ return `${ide_invocation_surfaces_1.CURSOR_MDC_FRONTMATTER}\n\n${mergedBody}\n`;
44
45
  }
45
46
  function getAdapterFiles() {
46
47
  const fraimRoot = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)().replace(/\/$/, '');
@@ -70,15 +71,9 @@ This repository uses FRAIM.
70
71
  > **Job stubs are for discovery only.** When a user @mentions or references any file under \`${employeeJobsPath}/\` or \`${managerJobsPath}/\`, do NOT attempt to execute the job from the stub content. The stub only shows intent and phase names. Always call \`get_fraim_job({ job: "<job-name>" })\` first to get the full phased instructions before doing any work.
71
72
  `);
72
73
  const cursorManagedBody = buildManagedSection(`
73
- Use FRAIM as the repo's execution framework.
74
+ # FRAIM
74
75
 
75
- - Discover available jobs, skills, and rules under \`${fraimRoot}/\`.
76
- - Jobs are the primary execution units; treat them like first-class workflows.
77
- - Skills are reusable capability modules jobs compose.
78
- - Rules are always-on constraints.
79
- - Repo-specific overrides and learnings under \`${personalizedRootPath}/\` take precedence.
80
- - Choose a relevant job from the stubs, then call \`get_fraim_job(...)\` for the full phased instructions.
81
- - **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
76
+ ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}
82
77
  `);
83
78
  const copilotBody = buildManagedSection(`
84
79
  ## FRAIM
@@ -89,7 +84,7 @@ Use FRAIM as the repo's execution framework.
89
84
  - FRAIM rules are always-on constraints and conventions.
90
85
  - Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
91
86
  - Use the stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
92
- - **Job stubs are for discovery only.** Never execute a job from stub content — always call \`get_fraim_job({ job: "<job-name>" })\` first.
87
+ - **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
93
88
  `);
94
89
  const fraimReadme = `# FRAIM Catalog
95
90
 
@@ -103,25 +98,20 @@ This directory is the repository-visible FRAIM surface.
103
98
 
104
99
  Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools.
105
100
  `;
106
- const claudeCommand = `The user wants to run FRAIM. The requested job or topic is: $ARGUMENTS
101
+ const vscodePrompt = `# FRAIM
107
102
 
108
- Follow this process:
109
-
110
- 1. **If no argument was given** (the line above ends with ": "): scan all stub files under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\`. List each by filename and its Intent line. Ask the user which job they want to run, then proceed to step 2.
111
-
112
- 2. **Find the job**: search \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` for a stub whose filename (without \`.md\`) matches or closely resembles the argument. Read the stub's Intent/Outcome to confirm it matches what the user wants.
113
-
114
- 3. **Load the full job**: call \`get_fraim_job({ job: "<matched-job-name>" })\` — never execute from stub content.
115
-
116
- 4. **Execute**: follow the phased instructions returned by \`get_fraim_job\`, using \`seekMentoring\` at phase transitions where indicated.
117
- `;
103
+ ${ide_invocation_surfaces_1.FRAIM_INVOCATION_BODY}`;
118
104
  return [
119
105
  { path: 'AGENTS.md', content: markdownBody },
120
106
  { path: 'CLAUDE.md', content: markdownBody },
121
107
  { path: path_1.default.join('.github', 'copilot-instructions.md'), content: copilotBody },
122
108
  { path: CURSOR_RULE_PATH, content: cursorManagedBody },
109
+ { path: VSCODE_FRAIM_PROMPT_PATH, content: vscodePrompt },
123
110
  { path: path_1.default.join(project_fraim_paths_1.WORKSPACE_FRAIM_DIRNAME, 'README.md'), content: fraimReadme },
124
- { path: CLAUDE_FRAIM_COMMAND_PATH, content: claudeCommand }
111
+ { path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSlashCommandContent)() },
112
+ { path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)() },
113
+ { path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)() },
114
+ { path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)() }
125
115
  ];
126
116
  }
127
117
  function ensureAgentAdapterFiles(projectRoot) {
@@ -135,7 +125,12 @@ function ensureAgentAdapterFiles(projectRoot) {
135
125
  const existing = fs_1.default.existsSync(fullPath) ? fs_1.default.readFileSync(fullPath, 'utf8') : '';
136
126
  const next = file.path === CURSOR_RULE_PATH
137
127
  ? mergeCursorRule(existing, file.content)
138
- : file.path.endsWith('README.md') || file.path === CLAUDE_FRAIM_COMMAND_PATH
128
+ : file.path.endsWith('README.md')
129
+ || file.path === VSCODE_FRAIM_PROMPT_PATH
130
+ || file.path === CLAUDE_FRAIM_COMMAND_PATH
131
+ || file.path === CODEX_FRAIM_SKILL_PATH
132
+ || file.path === WINDSURF_FRAIM_COMMAND_PATH
133
+ || file.path === KIRO_FRAIM_COMMAND_PATH
139
134
  ? file.content
140
135
  : mergeManagedSection(existing, file.content);
141
136
  if (existing !== next) {
@@ -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",
3
- "version": "2.0.117",
3
+ "version": "2.0.120",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {