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.
- package/dist/src/cli/commands/add-ide.js +10 -4
- package/dist/src/cli/commands/setup.js +6 -6
- package/dist/src/cli/mcp/command-resolution.js +45 -0
- package/dist/src/cli/mcp/ide-formats.js +2 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +5 -4
- package/dist/src/cli/setup/auto-mcp-setup.js +8 -0
- package/dist/src/cli/setup/ide-detector.js +16 -4
- package/dist/src/cli/setup/ide-global-integration.js +18 -78
- package/dist/src/cli/setup/ide-invocation-surfaces.js +79 -0
- package/dist/src/cli/utils/agent-adapters.js +22 -27
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
console.log(chalk_1.default.white(
|
|
688
|
-
}
|
|
689
|
-
console.log(chalk_1.default.white(' "What can FRAIM help me with?"
|
|
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
|
|
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
|
|
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
|
-
|
|
177
|
-
|
|
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
|
|
12
|
-
*
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
36
|
+
* Install FRAIM invocation artifacts for non-Claude IDEs.
|
|
88
37
|
* Supports: Cursor, Codex, Windsurf, Kiro
|
|
89
|
-
* Does
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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 `${
|
|
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
|
-
|
|
74
|
+
# FRAIM
|
|
74
75
|
|
|
75
|
-
|
|
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
|
|
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
|
|
101
|
+
const vscodePrompt = `# FRAIM
|
|
107
102
|
|
|
108
|
-
|
|
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:
|
|
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')
|
|
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
|
|
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
|
+
}
|