fraim-framework 2.0.84 → 2.0.86

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/bin/fraim-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM MCP Server - Smart Entry Point
@@ -16,6 +16,7 @@ const version_utils_1 = require("../utils/version-utils");
16
16
  const ide_detector_1 = require("../setup/ide-detector");
17
17
  const codex_local_config_1 = require("../setup/codex-local-config");
18
18
  const provider_registry_1 = require("../providers/provider-registry");
19
+ const fraim_gitignore_1 = require("../utils/fraim-gitignore");
19
20
  const promptForJiraProjectKey = async (jiraBaseUrl) => {
20
21
  console.log(chalk_1.default.blue('\nšŸŽ« Jira Project Configuration'));
21
22
  console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
@@ -159,13 +160,16 @@ const runInitProject = async () => {
159
160
  fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
160
161
  console.log(chalk_1.default.green('Created .fraim/config.json'));
161
162
  }
162
- ['workflows'].forEach((dir) => {
163
+ ['workflows', 'ai-employee/jobs', 'ai-employee/skills', 'ai-manager/jobs', 'personalized-employee'].forEach((dir) => {
163
164
  const dirPath = path_1.default.join(fraimDir, dir);
164
165
  if (!fs_1.default.existsSync(dirPath)) {
165
166
  fs_1.default.mkdirSync(dirPath, { recursive: true });
166
167
  console.log(chalk_1.default.green(`Created .fraim/${dir}`));
167
168
  }
168
169
  });
170
+ if ((0, fraim_gitignore_1.ensureFraimSyncedContentIgnored)(projectRoot)) {
171
+ console.log(chalk_1.default.green('Updated .gitignore with FRAIM synced content ignore rules'));
172
+ }
169
173
  if (!process.env.FRAIM_SKIP_SYNC) {
170
174
  await (0, sync_1.runSync)({});
171
175
  }
@@ -16,7 +16,8 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
16
16
  const projectRoot = process.cwd();
17
17
  const fraimDir = path_1.default.join(projectRoot, '.fraim');
18
18
  const configPath = path_1.default.join(fraimDir, 'config.json');
19
- const overridesDir = path_1.default.join(fraimDir, 'overrides');
19
+ const personalizedDir = path_1.default.join(fraimDir, 'personalized-employee');
20
+ const legacyOverridesDir = path_1.default.join(fraimDir, 'overrides');
20
21
  // Validate .fraim directory exists
21
22
  if (!fs_1.default.existsSync(fraimDir)) {
22
23
  console.log(chalk_1.default.red('āŒ .fraim/ directory not found. Run "fraim setup" or "fraim init-project" first.'));
@@ -42,20 +43,23 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
42
43
  console.log(chalk_1.default.blue('šŸ“‹ Overridable FRAIM Registry Paths:\n'));
43
44
  // Get list of existing overrides
44
45
  const existingOverrides = new Set();
45
- if (fs_1.default.existsSync(overridesDir)) {
46
- const scanDir = (dir, base = '') => {
47
- const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
48
- for (const entry of entries) {
49
- const relativePath = path_1.default.join(base, entry.name);
50
- if (entry.isDirectory()) {
51
- scanDir(path_1.default.join(dir, entry.name), relativePath);
52
- }
53
- else {
54
- existingOverrides.add(relativePath.replace(/\\/g, '/'));
55
- }
46
+ const scanDir = (dir, base = '') => {
47
+ const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ const relativePath = path_1.default.join(base, entry.name);
50
+ if (entry.isDirectory()) {
51
+ scanDir(path_1.default.join(dir, entry.name), relativePath);
56
52
  }
57
- };
58
- scanDir(overridesDir);
53
+ else {
54
+ existingOverrides.add(relativePath.replace(/\\/g, '/'));
55
+ }
56
+ }
57
+ };
58
+ if (fs_1.default.existsSync(personalizedDir)) {
59
+ scanDir(personalizedDir);
60
+ }
61
+ if (fs_1.default.existsSync(legacyOverridesDir)) {
62
+ scanDir(legacyOverridesDir);
59
63
  }
60
64
  // Handle --rules flag
61
65
  if (options.rules) {
@@ -197,6 +201,6 @@ exports.listOverridableCommand = new commander_1.Command('list-overridable')
197
201
  console.log(chalk_1.default.gray(' • Use "fraim override <path> --copy" to copy current content'));
198
202
  console.log(chalk_1.default.gray(' • Use --job-category <category> to see category-specific items'));
199
203
  console.log(chalk_1.default.gray(' • Use --rules to see all overridable rules'));
200
- console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/overrides/'));
204
+ console.log(chalk_1.default.gray(' • Overrides are stored in .fraim/personalized-employee/'));
201
205
  }
202
206
  });
@@ -39,8 +39,8 @@ exports.overrideCommand = new commander_1.Command('override')
39
39
  console.log(chalk_1.default.red('āŒ Must specify either --inherit or --copy.'));
40
40
  process.exit(1);
41
41
  }
42
- // Create overrides directory structure
43
- const overridePath = path_1.default.join(fraimDir, 'overrides', registryPath);
42
+ // Create personalized override directory structure
43
+ const overridePath = path_1.default.join(fraimDir, 'personalized-employee', registryPath);
44
44
  const overrideDir = path_1.default.dirname(overridePath);
45
45
  if (!fs_1.default.existsSync(overrideDir)) {
46
46
  fs_1.default.mkdirSync(overrideDir, { recursive: true });
@@ -124,6 +124,13 @@ exports.overrideCommand = new commander_1.Command('override')
124
124
  toolName = 'get_fraim_workflow';
125
125
  toolArgs = { workflow: workflowName };
126
126
  }
127
+ else if (registryPath.startsWith('jobs/')) {
128
+ // e.g., "jobs/product-building/feature-specification.md" -> "feature-specification"
129
+ const parts = registryPath.split('/');
130
+ const jobName = parts[parts.length - 1].replace('.md', '');
131
+ toolName = 'get_fraim_job';
132
+ toolArgs = { job: jobName };
133
+ }
127
134
  else {
128
135
  toolName = 'get_fraim_file';
129
136
  toolArgs = { path: registryPath };
@@ -46,11 +46,19 @@ const path_1 = __importDefault(require("path"));
46
46
  const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
47
47
  const version_utils_1 = require("../utils/version-utils");
48
48
  const platform_detection_1 = require("../utils/platform-detection");
49
- const get_provider_client_1 = require("../api/get-provider-client");
49
+ const provider_client_1 = require("../api/provider-client");
50
50
  const provider_prompts_1 = require("../setup/provider-prompts");
51
51
  const provider_registry_1 = require("../providers/provider-registry");
52
52
  const init_project_1 = require("./init-project");
53
53
  const script_sync_utils_1 = require("../utils/script-sync-utils");
54
+ function parseModeOption(mode) {
55
+ if (mode === 'conversational' || mode === 'integrated' || mode === 'split') {
56
+ return mode;
57
+ }
58
+ console.log(chalk_1.default.red(`āŒ Invalid mode "${mode}"`));
59
+ console.log(chalk_1.default.yellow('šŸ’” Valid values: integrated, split, conversational'));
60
+ process.exit(1);
61
+ }
54
62
  const promptForFraimKey = async () => {
55
63
  console.log(chalk_1.default.blue('šŸ”‘ FRAIM Key Setup'));
56
64
  console.log('FRAIM requires a valid API key to access workflows and features.\n');
@@ -293,22 +301,28 @@ const runSetup = async (options) => {
293
301
  // Get FRAIM key
294
302
  fraimKey = options.key || await promptForFraimKey();
295
303
  console.log(chalk_1.default.green('āœ… FRAIM key accepted\n'));
296
- // Ask for mode preference
297
- mode = await promptForMode();
304
+ // Ask for mode preference (or use explicit option)
305
+ mode = options.mode ? parseModeOption(options.mode) : await promptForMode();
306
+ // Parse provider CLI flags on first-time setup too
307
+ const parsed = await parseLegacyOptions(options, fraimKey);
308
+ requestedProviders = parsed.requestedProviders;
309
+ providedTokens = parsed.providedTokens;
310
+ providedConfigs = parsed.providedConfigs;
298
311
  }
312
+ const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
299
313
  // Handle platform tokens based on mode
300
314
  if (mode === 'integrated') {
301
315
  let providersToSetup = requestedProviders;
302
316
  // If no specific providers requested and not an update, ask user
303
317
  if (!isUpdate && providersToSetup.length === 0) {
304
- providersToSetup = await (0, provider_prompts_1.promptForProviders)((0, get_provider_client_1.getProviderClient)());
318
+ providersToSetup = await (0, provider_prompts_1.promptForProviders)(providerClient);
305
319
  }
306
320
  // Get credentials for each provider
307
321
  for (const providerId of providersToSetup) {
308
322
  if (!tokens[providerId]) {
309
323
  try {
310
324
  // Use provided tokens if available, otherwise prompt
311
- const creds = await (0, provider_prompts_1.promptForProviderCredentials)((0, get_provider_client_1.getProviderClient)(), providerId, providedTokens[providerId], providedConfigs[providerId]);
325
+ const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, providerId, providedTokens[providerId], providedConfigs[providerId]);
312
326
  tokens[providerId] = creds.token;
313
327
  if (creds.config) {
314
328
  configs[providerId] = creds.config;
@@ -336,12 +350,12 @@ const runSetup = async (options) => {
336
350
  console.log(chalk_1.default.blue('\nšŸ”€ Split Mode Configuration'));
337
351
  console.log(chalk_1.default.gray('Configure separate platforms for code hosting and issue tracking.\n'));
338
352
  // Get code repository platform
339
- const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)((0, get_provider_client_1.getProviderClient)(), 'code');
353
+ const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'code');
340
354
  // Get code repository credentials
341
355
  if (!tokens[codeRepoProvider]) {
342
356
  try {
343
357
  // Use provided tokens if available, otherwise prompt
344
- const creds = await (0, provider_prompts_1.promptForProviderCredentials)((0, get_provider_client_1.getProviderClient)(), codeRepoProvider, providedTokens[codeRepoProvider], providedConfigs[codeRepoProvider]);
358
+ const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, codeRepoProvider, providedTokens[codeRepoProvider], providedConfigs[codeRepoProvider]);
345
359
  tokens[codeRepoProvider] = creds.token;
346
360
  if (creds.config) {
347
361
  configs[codeRepoProvider] = creds.config;
@@ -354,12 +368,12 @@ const runSetup = async (options) => {
354
368
  }
355
369
  }
356
370
  // Get issue tracking platform
357
- const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)((0, get_provider_client_1.getProviderClient)(), 'issues');
371
+ const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'issues');
358
372
  // Get issue tracking credentials (if different from code repo)
359
373
  if (!tokens[issueProvider]) {
360
374
  try {
361
375
  // Use provided tokens if available, otherwise prompt
362
- const creds = await (0, provider_prompts_1.promptForProviderCredentials)((0, get_provider_client_1.getProviderClient)(), issueProvider, providedTokens[issueProvider], providedConfigs[issueProvider]);
376
+ const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, issueProvider, providedTokens[issueProvider], providedConfigs[issueProvider]);
363
377
  tokens[issueProvider] = creds.token;
364
378
  if (creds.config) {
365
379
  configs[issueProvider] = creds.config;
@@ -474,6 +488,7 @@ exports.runSetup = runSetup;
474
488
  exports.setupCommand = new commander_1.Command('setup')
475
489
  .description('Complete global FRAIM setup with platform configuration')
476
490
  .option('--key <key>', 'FRAIM API key')
491
+ .option('--mode <mode>', 'Usage mode: integrated | split | conversational')
477
492
  .option('--ide <ides>', 'Configure specific IDEs');
478
493
  // Track initialization promise for CLI entry point
479
494
  exports.setupCommandInitialization = null;
@@ -63,6 +63,24 @@ function loadUserApiKey() {
63
63
  return undefined;
64
64
  }
65
65
  }
66
+ function updateVersionInConfig(fraimDir) {
67
+ const configPath = path_1.default.join(fraimDir, 'config.json');
68
+ if (!fs_1.default.existsSync(configPath)) {
69
+ return;
70
+ }
71
+ try {
72
+ const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
73
+ const newVersion = (0, version_utils_1.getFraimVersion)();
74
+ if (currentConfig.version !== newVersion) {
75
+ currentConfig.version = newVersion;
76
+ fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
77
+ console.log(chalk_1.default.green(`āœ… Updated FRAIM version to ${newVersion} in config.`));
78
+ }
79
+ }
80
+ catch (e) {
81
+ console.warn(chalk_1.default.yellow('āš ļø Could not update version in config.json.'));
82
+ }
83
+ }
66
84
  const runSync = async (options) => {
67
85
  const projectRoot = process.cwd();
68
86
  const config = (0, config_loader_1.loadFraimConfig)();
@@ -88,7 +106,8 @@ const runSync = async (options) => {
88
106
  skipUpdates: true
89
107
  });
90
108
  if (result.success) {
91
- console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, ${result.coachingSynced} coaching files, and ${result.docsSynced} docs from local server`));
109
+ console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from local server`));
110
+ updateVersionInConfig(fraimDir);
92
111
  return;
93
112
  }
94
113
  console.error(chalk_1.default.red(`āŒ Local sync failed: ${result.error}`));
@@ -96,12 +115,18 @@ const runSync = async (options) => {
96
115
  process.exit(1);
97
116
  }
98
117
  // Path 2: Remote sync with API key
99
- const apiKey = loadUserApiKey() || config.apiKey || process.env.FRAIM_API_KEY;
118
+ let apiKey = loadUserApiKey() || config.apiKey || process.env.FRAIM_API_KEY;
100
119
  if (!apiKey) {
101
- console.error(chalk_1.default.red('āŒ No API key configured. Cannot sync.'));
102
- console.error(chalk_1.default.yellow('šŸ’” Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json or .fraim/config.json'));
103
- console.error(chalk_1.default.yellow('šŸ’” Or use --local to sync from a locally running FRAIM server.'));
104
- process.exit(1);
120
+ if (process.env.TEST_MODE === 'true') {
121
+ console.log(chalk_1.default.yellow('āš ļø TEST_MODE: No API key configured. Using test placeholder key.'));
122
+ apiKey = 'test-mode-key';
123
+ }
124
+ else {
125
+ console.error(chalk_1.default.red('āŒ No API key configured. Cannot sync.'));
126
+ console.error(chalk_1.default.yellow('šŸ’” Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json or .fraim/config.json'));
127
+ console.error(chalk_1.default.yellow('šŸ’” Or use --local to sync from a locally running FRAIM server.'));
128
+ process.exit(1);
129
+ }
105
130
  }
106
131
  console.log(chalk_1.default.blue('šŸ”„ Syncing FRAIM workflows from remote server...'));
107
132
  const result = await syncFromRemote({
@@ -115,27 +140,13 @@ const runSync = async (options) => {
115
140
  console.error(chalk_1.default.yellow('šŸ’” Check your API key and network connection.'));
116
141
  if (process.env.TEST_MODE === 'true') {
117
142
  console.log(chalk_1.default.yellow('āš ļø TEST_MODE: Continuing without remote sync (server may be unavailable).'));
143
+ updateVersionInConfig(fraimDir);
118
144
  return;
119
145
  }
120
146
  process.exit(1);
121
147
  }
122
- console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.scriptsSynced} scripts, ${result.coachingSynced} coaching files, and ${result.docsSynced} docs from remote`));
123
- // Update version in config.json
124
- const configPath = path_1.default.join(fraimDir, 'config.json');
125
- if (fs_1.default.existsSync(configPath)) {
126
- try {
127
- const currentConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
128
- const newVersion = (0, version_utils_1.getFraimVersion)();
129
- if (currentConfig.version !== newVersion) {
130
- currentConfig.version = newVersion;
131
- fs_1.default.writeFileSync(configPath, JSON.stringify(currentConfig, null, 2));
132
- console.log(chalk_1.default.green(`āœ… Updated FRAIM version to ${newVersion} in config.`));
133
- }
134
- }
135
- catch (e) {
136
- console.warn(chalk_1.default.yellow('āš ļø Could not update version in config.json.'));
137
- }
138
- }
148
+ console.log(chalk_1.default.green(`āœ… Successfully synced ${result.workflowsSynced} workflows, ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
149
+ updateVersionInConfig(fraimDir);
139
150
  };
140
151
  exports.runSync = runSync;
141
152
  async function checkAndUpdateCLI() {
@@ -166,8 +166,9 @@ function checkOverrideSyntaxValid() {
166
166
  category: 'workflows',
167
167
  critical: false,
168
168
  run: async () => {
169
- const overridesDir = path_1.default.join(process.cwd(), '.fraim', 'overrides');
170
- if (!fs_1.default.existsSync(overridesDir)) {
169
+ const personalizedDir = path_1.default.join(process.cwd(), '.fraim', 'personalized-employee');
170
+ const legacyOverridesDir = path_1.default.join(process.cwd(), '.fraim', 'overrides');
171
+ if (!fs_1.default.existsSync(personalizedDir) && !fs_1.default.existsSync(legacyOverridesDir)) {
171
172
  return {
172
173
  status: 'passed',
173
174
  message: 'No overrides (not required)',
@@ -192,10 +193,17 @@ function checkOverrideSyntaxValid() {
192
193
  }
193
194
  }
194
195
  };
195
- scanDir(overridesDir);
196
+ if (fs_1.default.existsSync(personalizedDir)) {
197
+ scanDir(personalizedDir);
198
+ }
199
+ if (fs_1.default.existsSync(legacyOverridesDir)) {
200
+ scanDir(legacyOverridesDir);
201
+ }
196
202
  // Validate each override
197
203
  for (const override of overrides) {
198
- const overridePath = path_1.default.join(overridesDir, override);
204
+ const primaryPath = path_1.default.join(personalizedDir, override);
205
+ const legacyPath = path_1.default.join(legacyOverridesDir, override);
206
+ const overridePath = fs_1.default.existsSync(primaryPath) ? primaryPath : legacyPath;
199
207
  const content = fs_1.default.readFileSync(overridePath, 'utf-8');
200
208
  const parseResult = parser.parse(content);
201
209
  if (parseResult.hasImports) {
@@ -0,0 +1,46 @@
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.ensureFraimSyncedContentIgnored = exports.FRAIM_SYNC_GITIGNORE_ENTRIES = exports.FRAIM_SYNC_GITIGNORE_END = exports.FRAIM_SYNC_GITIGNORE_START = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
10
+ exports.FRAIM_SYNC_GITIGNORE_END = '# END FRAIM SYNCED CONTENT';
11
+ exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
12
+ '.fraim/workflows/',
13
+ '.fraim/docs/',
14
+ '.fraim/ai-employee/',
15
+ '.fraim/ai-manager/'
16
+ ];
17
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
18
+ /**
19
+ * Ensures the repo .gitignore contains a managed FRAIM synced-content block.
20
+ * Returns true when the file was modified.
21
+ */
22
+ const ensureFraimSyncedContentIgnored = (projectRoot) => {
23
+ const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
24
+ const existingRaw = fs_1.default.existsSync(gitignorePath)
25
+ ? fs_1.default.readFileSync(gitignorePath, 'utf8')
26
+ : '';
27
+ const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
28
+ const normalized = existingRaw.replace(/\r\n/g, '\n');
29
+ const managedBlock = [
30
+ exports.FRAIM_SYNC_GITIGNORE_START,
31
+ '# Synced by fraim init-project (generated content)',
32
+ ...exports.FRAIM_SYNC_GITIGNORE_ENTRIES,
33
+ exports.FRAIM_SYNC_GITIGNORE_END
34
+ ].join('\n');
35
+ const blockPattern = new RegExp(`${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
36
+ const hasManagedBlock = blockPattern.test(normalized);
37
+ const updatedNormalized = hasManagedBlock
38
+ ? normalized.replace(blockPattern, `${managedBlock}\n`)
39
+ : `${normalized.trimEnd()}${normalized.trimEnd().length > 0 ? '\n\n' : ''}${managedBlock}\n`;
40
+ if (updatedNormalized !== normalized) {
41
+ fs_1.default.writeFileSync(gitignorePath, updatedNormalized.replace(/\n/g, newline), 'utf8');
42
+ return true;
43
+ }
44
+ return false;
45
+ };
46
+ exports.ensureFraimSyncedContentIgnored = ensureFraimSyncedContentIgnored;
@@ -27,8 +27,11 @@ async function syncFromRemote(options) {
27
27
  return {
28
28
  success: false,
29
29
  workflowsSynced: 0,
30
+ employeeJobsSynced: 0,
31
+ managerJobsSynced: 0,
32
+ skillsSynced: 0,
33
+ rulesSynced: 0,
30
34
  scriptsSynced: 0,
31
- coachingSynced: 0,
32
35
  docsSynced: 0,
33
36
  error: 'FRAIM_API_KEY not set'
34
37
  };
@@ -49,8 +52,11 @@ async function syncFromRemote(options) {
49
52
  return {
50
53
  success: false,
51
54
  workflowsSynced: 0,
55
+ employeeJobsSynced: 0,
56
+ managerJobsSynced: 0,
57
+ skillsSynced: 0,
58
+ rulesSynced: 0,
52
59
  scriptsSynced: 0,
53
- coachingSynced: 0,
54
60
  docsSynced: 0,
55
61
  error: 'No files received'
56
62
  };
@@ -73,6 +79,72 @@ async function syncFromRemote(options) {
73
79
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
74
80
  console.log(chalk_1.default.gray(` + ${file.path}`));
75
81
  }
82
+ // Sync job stubs to role-specific folders under .fraim
83
+ const allJobFiles = files.filter(f => f.type === 'job');
84
+ const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
85
+ const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
86
+ const employeeJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'jobs');
87
+ if (!(0, fs_1.existsSync)(employeeJobsDir)) {
88
+ (0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
89
+ }
90
+ cleanDirectory(employeeJobsDir);
91
+ for (const file of jobFiles) {
92
+ const filePath = (0, path_1.join)(employeeJobsDir, file.path);
93
+ const fileDir = (0, path_1.dirname)(filePath);
94
+ if (!(0, fs_1.existsSync)(fileDir)) {
95
+ (0, fs_1.mkdirSync)(fileDir, { recursive: true });
96
+ }
97
+ (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
98
+ console.log(chalk_1.default.gray(` + ai-employee/jobs/${file.path}`));
99
+ }
100
+ // Sync ai-manager job stubs to .fraim/ai-manager/jobs/
101
+ const managerJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'jobs');
102
+ if (!(0, fs_1.existsSync)(managerJobsDir)) {
103
+ (0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
104
+ }
105
+ cleanDirectory(managerJobsDir);
106
+ for (const file of managerJobFiles) {
107
+ const managerRelativePath = file.path.replace(/^ai-manager\//, '');
108
+ const filePath = (0, path_1.join)(managerJobsDir, managerRelativePath);
109
+ const fileDir = (0, path_1.dirname)(filePath);
110
+ if (!(0, fs_1.existsSync)(fileDir)) {
111
+ (0, fs_1.mkdirSync)(fileDir, { recursive: true });
112
+ }
113
+ (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
114
+ console.log(chalk_1.default.gray(` + ai-manager/jobs/${managerRelativePath}`));
115
+ }
116
+ // Sync full skill files to .fraim/ai-employee/skills/
117
+ const skillFiles = files.filter(f => f.type === 'skill');
118
+ const skillsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'skills');
119
+ if (!(0, fs_1.existsSync)(skillsDir)) {
120
+ (0, fs_1.mkdirSync)(skillsDir, { recursive: true });
121
+ }
122
+ cleanDirectory(skillsDir);
123
+ for (const file of skillFiles) {
124
+ const filePath = (0, path_1.join)(skillsDir, file.path);
125
+ const fileDir = (0, path_1.dirname)(filePath);
126
+ if (!(0, fs_1.existsSync)(fileDir)) {
127
+ (0, fs_1.mkdirSync)(fileDir, { recursive: true });
128
+ }
129
+ (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
130
+ console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path}`));
131
+ }
132
+ // Sync full rule files to .fraim/ai-employee/rules/
133
+ const ruleFiles = files.filter(f => f.type === 'rule');
134
+ const rulesDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'rules');
135
+ if (!(0, fs_1.existsSync)(rulesDir)) {
136
+ (0, fs_1.mkdirSync)(rulesDir, { recursive: true });
137
+ }
138
+ cleanDirectory(rulesDir);
139
+ for (const file of ruleFiles) {
140
+ const filePath = (0, path_1.join)(rulesDir, file.path);
141
+ const fileDir = (0, path_1.dirname)(filePath);
142
+ if (!(0, fs_1.existsSync)(fileDir)) {
143
+ (0, fs_1.mkdirSync)(fileDir, { recursive: true });
144
+ }
145
+ (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
146
+ console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path}`));
147
+ }
76
148
  // Sync scripts to user directory
77
149
  const scriptFiles = files.filter(f => f.type === 'script');
78
150
  const userDir = (0, script_sync_utils_1.getUserFraimDir)();
@@ -92,22 +164,6 @@ async function syncFromRemote(options) {
92
164
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
93
165
  console.log(chalk_1.default.gray(` + ${file.path}`));
94
166
  }
95
- // Sync coaching files to .fraim/coaching-moments/
96
- const coachingFiles = files.filter(f => f.type === 'coaching');
97
- const coachingDir = (0, path_1.join)(options.projectRoot, '.fraim', 'coaching-moments');
98
- if (!(0, fs_1.existsSync)(coachingDir)) {
99
- (0, fs_1.mkdirSync)(coachingDir, { recursive: true });
100
- }
101
- cleanDirectory(coachingDir);
102
- for (const file of coachingFiles) {
103
- const filePath = (0, path_1.join)(coachingDir, file.path);
104
- const fileDir = (0, path_1.dirname)(filePath);
105
- if (!(0, fs_1.existsSync)(fileDir)) {
106
- (0, fs_1.mkdirSync)(fileDir, { recursive: true });
107
- }
108
- (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
109
- console.log(chalk_1.default.gray(` + coaching-moments/${file.path}`));
110
- }
111
167
  // Sync docs to .fraim/docs/
112
168
  const docsFiles = files.filter(f => f.type === 'docs');
113
169
  const docsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'docs');
@@ -124,12 +180,14 @@ async function syncFromRemote(options) {
124
180
  (0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
125
181
  console.log(chalk_1.default.gray(` + docs/${file.path}`));
126
182
  }
127
- console.log(chalk_1.default.green(`\nāœ… Synced ${workflowFiles.length} workflows, ${scriptFiles.length} scripts, ${coachingFiles.length} coaching files, and ${docsFiles.length} docs from remote`));
128
183
  return {
129
184
  success: true,
130
185
  workflowsSynced: workflowFiles.length,
186
+ employeeJobsSynced: jobFiles.length,
187
+ managerJobsSynced: managerJobFiles.length,
188
+ skillsSynced: skillFiles.length,
189
+ rulesSynced: ruleFiles.length,
131
190
  scriptsSynced: scriptFiles.length,
132
- coachingSynced: coachingFiles.length,
133
191
  docsSynced: docsFiles.length
134
192
  };
135
193
  }
@@ -138,8 +196,11 @@ async function syncFromRemote(options) {
138
196
  return {
139
197
  success: false,
140
198
  workflowsSynced: 0,
199
+ employeeJobsSynced: 0,
200
+ managerJobsSynced: 0,
201
+ skillsSynced: 0,
202
+ rulesSynced: 0,
141
203
  scriptsSynced: 0,
142
- coachingSynced: 0,
143
204
  docsSynced: 0,
144
205
  error: error.message
145
206
  };
@@ -20,22 +20,54 @@ class LocalRegistryResolver {
20
20
  * Check if a local override exists for the given path
21
21
  */
22
22
  hasLocalOverride(path) {
23
- const overridePath = this.getOverridePath(path);
24
- const exists = (0, fs_1.existsSync)(overridePath);
25
- console.error(`[LocalRegistryResolver] hasLocalOverride(${path}) -> ${overridePath} -> ${exists}`);
23
+ const primaryPath = this.getOverridePath(path);
24
+ const legacyPath = this.getLegacyOverridePath(path);
25
+ const exists = (0, fs_1.existsSync)(primaryPath) || (0, fs_1.existsSync)(legacyPath);
26
+ console.error(`[LocalRegistryResolver] hasLocalOverride(${path}) -> primary: ${primaryPath}, legacy: ${legacyPath}, exists: ${exists}`);
26
27
  return exists;
27
28
  }
29
+ /**
30
+ * Check if a locally synced skill/rule file exists for the given registry path.
31
+ */
32
+ hasSyncedLocalFile(path) {
33
+ const syncedPath = this.getSyncedFilePath(path);
34
+ return !!syncedPath && (0, fs_1.existsSync)(syncedPath);
35
+ }
28
36
  /**
29
37
  * Get the full path to a local override file
30
38
  */
31
39
  getOverridePath(path) {
40
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/personalized-employee', path);
41
+ }
42
+ /**
43
+ * Get the full path to a legacy local override file.
44
+ * Kept for backward compatibility while migrating to personalized-employee.
45
+ */
46
+ getLegacyOverridePath(path) {
32
47
  return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', path);
33
48
  }
49
+ /**
50
+ * Get the full path to a locally synced FRAIM file when available.
51
+ * Skills and rules are synced under role-based folders:
52
+ * - skills/* -> .fraim/ai-employee/skills/*
53
+ * - rules/* -> .fraim/ai-employee/rules/*
54
+ */
55
+ getSyncedFilePath(path) {
56
+ const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
57
+ if (normalizedPath.startsWith('skills/')) {
58
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
59
+ }
60
+ if (normalizedPath.startsWith('rules/')) {
61
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
62
+ }
63
+ return null;
64
+ }
34
65
  /**
35
66
  * Read local override file content
36
67
  */
37
68
  readLocalOverride(path) {
38
- const overridePath = this.getOverridePath(path);
69
+ const primaryPath = this.getOverridePath(path);
70
+ const overridePath = (0, fs_1.existsSync)(primaryPath) ? primaryPath : this.getLegacyOverridePath(path);
39
71
  try {
40
72
  return (0, fs_1.readFileSync)(overridePath, 'utf-8');
41
73
  }
@@ -43,6 +75,37 @@ class LocalRegistryResolver {
43
75
  throw new Error(`Failed to read local override: ${path}. ${error.message}`);
44
76
  }
45
77
  }
78
+ /**
79
+ * Read locally synced skill/rule file content.
80
+ */
81
+ readSyncedLocalFile(path) {
82
+ const syncedPath = this.getSyncedFilePath(path);
83
+ if (!syncedPath || !(0, fs_1.existsSync)(syncedPath)) {
84
+ return null;
85
+ }
86
+ try {
87
+ return (0, fs_1.readFileSync)(syncedPath, 'utf-8');
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ stripMcpHeader(content) {
94
+ const trimmed = content.trimStart();
95
+ if (!trimmed.startsWith('#')) {
96
+ return content;
97
+ }
98
+ const separator = '\n---\n';
99
+ const separatorIndex = content.indexOf(separator);
100
+ if (separatorIndex === -1) {
101
+ return content;
102
+ }
103
+ const headerBlock = content.slice(0, separatorIndex);
104
+ if (!headerBlock.includes('** Path:**')) {
105
+ return content;
106
+ }
107
+ return content.slice(separatorIndex + separator.length).trimStart();
108
+ }
46
109
  /**
47
110
  * Resolve inheritance in local override content
48
111
  */
@@ -86,20 +149,32 @@ class LocalRegistryResolver {
86
149
  * Resolve a registry file request
87
150
  *
88
151
  * Resolution order:
89
- * 1. Check for local override in .fraim/overrides/
90
- * 2. If found, read and resolve inheritance
91
- * 3. If not found, fetch from remote
152
+ * 1. Check for local override in .fraim/personalized-employee/
153
+ * 2. Fallback to .fraim/overrides/ (legacy)
154
+ * 3. If found, read and resolve inheritance
155
+ * 4. If not found, fetch from remote
92
156
  *
93
157
  * @param path - Registry path (e.g., "workflows/product-building/spec.md")
94
158
  * @returns Resolved file with metadata
95
159
  */
96
- async resolveFile(path) {
160
+ async resolveFile(path, options = {}) {
97
161
  console.error(`[LocalRegistryResolver] ===== resolveFile called for: ${path} =====`);
162
+ const includeMetadata = options.includeMetadata ?? true;
163
+ const stripMcpHeader = options.stripMcpHeader ?? false;
98
164
  // Check for local override
99
165
  if (!this.hasLocalOverride(path)) {
166
+ const syncedLocalContent = this.readSyncedLocalFile(path);
167
+ if (syncedLocalContent !== null) {
168
+ return {
169
+ content: syncedLocalContent,
170
+ source: 'local',
171
+ inherited: false
172
+ };
173
+ }
100
174
  // No override, fetch from remote
101
175
  try {
102
- const content = await this.remoteContentResolver(path);
176
+ const rawContent = await this.remoteContentResolver(path);
177
+ const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
103
178
  return {
104
179
  content,
105
180
  source: 'remote',
@@ -118,7 +193,8 @@ class LocalRegistryResolver {
118
193
  catch (error) {
119
194
  // If local read fails, fall back to remote
120
195
  console.warn(`Local override read failed, falling back to remote: ${path}`);
121
- const content = await this.remoteContentResolver(path);
196
+ const rawContent = await this.remoteContentResolver(path);
197
+ const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
122
198
  return {
123
199
  content,
124
200
  source: 'remote',
@@ -154,12 +230,93 @@ class LocalRegistryResolver {
154
230
  imports: resolved.imports.length > 0 ? resolved.imports : undefined
155
231
  };
156
232
  // Add metadata comment
157
- const metadata = this.generateMetadata(result);
158
- if (metadata) {
159
- result.metadata = metadata;
160
- result.content = metadata + result.content;
233
+ if (includeMetadata) {
234
+ const metadata = this.generateMetadata(result);
235
+ if (metadata) {
236
+ result.metadata = metadata;
237
+ result.content = metadata + result.content;
238
+ }
161
239
  }
162
240
  return result;
163
241
  }
242
+ sanitizeIncludePath(path) {
243
+ const trimmed = path.trim().replace(/\\/g, '/');
244
+ if (!trimmed)
245
+ return null;
246
+ if (trimmed.includes('..'))
247
+ return null;
248
+ if (trimmed.startsWith('/'))
249
+ return null;
250
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed))
251
+ return null;
252
+ return trimmed;
253
+ }
254
+ async resolveIncludesInternal(content, options) {
255
+ if (options.depth >= options.maxDepth) {
256
+ return content;
257
+ }
258
+ const includePattern = /\{\{include:([^}]+)\}\}/g;
259
+ const matches = Array.from(content.matchAll(includePattern));
260
+ if (matches.length === 0) {
261
+ return content;
262
+ }
263
+ const resolvedByPath = new Map();
264
+ for (const match of matches) {
265
+ const includePath = this.sanitizeIncludePath(match[1]);
266
+ if (!includePath)
267
+ continue;
268
+ if (resolvedByPath.has(includePath))
269
+ continue;
270
+ if (options.cache.has(includePath)) {
271
+ resolvedByPath.set(includePath, options.cache.get(includePath));
272
+ continue;
273
+ }
274
+ if (options.stack.has(includePath)) {
275
+ continue;
276
+ }
277
+ try {
278
+ options.stack.add(includePath);
279
+ const resolved = await this.resolveFile(includePath, {
280
+ includeMetadata: false,
281
+ stripMcpHeader: true
282
+ });
283
+ const resolvedContent = await this.resolveIncludesInternal(resolved.content, {
284
+ depth: options.depth + 1,
285
+ maxDepth: options.maxDepth,
286
+ stack: options.stack,
287
+ cache: options.cache
288
+ });
289
+ options.cache.set(includePath, resolvedContent);
290
+ resolvedByPath.set(includePath, resolvedContent);
291
+ }
292
+ catch (error) {
293
+ console.warn(`Failed to resolve include ${includePath}: ${error.message}`);
294
+ }
295
+ finally {
296
+ options.stack.delete(includePath);
297
+ }
298
+ }
299
+ return content.replace(includePattern, (fullMatch, includeRef) => {
300
+ const includePath = this.sanitizeIncludePath(includeRef);
301
+ if (!includePath)
302
+ return fullMatch;
303
+ return resolvedByPath.get(includePath) ?? fullMatch;
304
+ });
305
+ }
306
+ /**
307
+ * Resolve {{include:path}} directives using local override precedence:
308
+ * 1. .fraim/personalized-employee/
309
+ * 2. .fraim/overrides/ (legacy)
310
+ * 3. remote resolver
311
+ */
312
+ async resolveIncludes(content, maxDepth = LocalRegistryResolver.MAX_INCLUDE_DEPTH) {
313
+ return this.resolveIncludesInternal(content, {
314
+ depth: 0,
315
+ maxDepth,
316
+ stack: new Set(),
317
+ cache: new Map()
318
+ });
319
+ }
164
320
  }
165
321
  exports.LocalRegistryResolver = LocalRegistryResolver;
322
+ LocalRegistryResolver.MAX_INCLUDE_DEPTH = 10;
@@ -2,6 +2,48 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.generateWorkflowStub = generateWorkflowStub;
4
4
  exports.parseRegistryWorkflow = parseRegistryWorkflow;
5
+ exports.generateJobStub = generateJobStub;
6
+ exports.generateSkillStub = generateSkillStub;
7
+ exports.generateRuleStub = generateRuleStub;
8
+ exports.parseRegistryJob = parseRegistryJob;
9
+ exports.parseRegistrySkill = parseRegistrySkill;
10
+ exports.parseRegistryRule = parseRegistryRule;
11
+ function extractSection(content, headingPatterns) {
12
+ for (const heading of headingPatterns) {
13
+ const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
14
+ const match = content.match(pattern);
15
+ if (match?.[1]) {
16
+ const section = match[1].trim();
17
+ if (section.length > 0)
18
+ return section;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ function extractLeadParagraph(content) {
24
+ const withoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/m, '');
25
+ const lines = withoutFrontmatter.split(/\r?\n/);
26
+ const paragraphLines = [];
27
+ let started = false;
28
+ for (const rawLine of lines) {
29
+ const line = rawLine.trim();
30
+ if (!started) {
31
+ if (!line || line.startsWith('#'))
32
+ continue;
33
+ if (line.startsWith('-') || /^\d+\./.test(line))
34
+ continue;
35
+ started = true;
36
+ }
37
+ if (!line)
38
+ break;
39
+ if (line.startsWith('#'))
40
+ break;
41
+ paragraphLines.push(line);
42
+ }
43
+ if (paragraphLines.length === 0)
44
+ return null;
45
+ return paragraphLines.join(' ').trim();
46
+ }
5
47
  /**
6
48
  * Generates a lightweight markdown stub for a workflow.
7
49
  * These stubs are committed to the user's repo for discoverability.
@@ -33,3 +75,100 @@ function parseRegistryWorkflow(content) {
33
75
  : [];
34
76
  return { intent, principles };
35
77
  }
78
+ /**
79
+ * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
80
+ */
81
+ function generateJobStub(jobName, _jobPath, intent, outcome) {
82
+ return `# FRAIM Job: ${jobName}
83
+
84
+ ## Intent
85
+ ${intent}
86
+
87
+ ## Outcome
88
+ ${outcome}
89
+
90
+ ---
91
+
92
+ > [!IMPORTANT]
93
+ > **For AI Agents:** Do NOT attempt to execute this job based on the Intent/Outcome above.
94
+ > This stub is for discoverability only. The actual job has multiple phases with
95
+ > detailed steps, validation criteria, and required skills.
96
+ >
97
+ > To get full phase-by-phase instructions, call:
98
+ > \`get_fraim_job({ job: "${jobName}" })\`
99
+ >
100
+ > Then follow all phases using \`seekMentoring\` at each phase transition.
101
+ `;
102
+ }
103
+ /**
104
+ * Generates a lightweight markdown stub for a skill.
105
+ */
106
+ function generateSkillStub(skillName, skillPath, intent, outcome) {
107
+ return `# FRAIM Skill: ${skillName}
108
+
109
+ ## Intent
110
+ ${intent}
111
+
112
+ ## Outcome
113
+ ${outcome}
114
+
115
+ ---
116
+
117
+ > [!IMPORTANT]
118
+ > **For AI Agents:** This is a skill stub for discoverability.
119
+ > To retrieve the complete skill instructions, call:
120
+ > \`get_fraim_file({ path: "skills/${skillPath}" })\`
121
+ `;
122
+ }
123
+ /**
124
+ * Generates a lightweight markdown stub for a rule.
125
+ */
126
+ function generateRuleStub(ruleName, rulePath, intent, outcome) {
127
+ return `# FRAIM Rule: ${ruleName}
128
+
129
+ ## Intent
130
+ ${intent}
131
+
132
+ ## Outcome
133
+ ${outcome}
134
+
135
+ ---
136
+
137
+ > [!IMPORTANT]
138
+ > **For AI Agents:** This is a rule stub for discoverability.
139
+ > To retrieve the complete rule instructions, call:
140
+ > \`get_fraim_file({ path: "rules/${rulePath}" })\`
141
+ `;
142
+ }
143
+ /**
144
+ * Parses a job file from the registry to extract its intent and outcome for the stub.
145
+ */
146
+ function parseRegistryJob(content) {
147
+ const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
148
+ const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
149
+ const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
150
+ const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
151
+ return { intent, outcome };
152
+ }
153
+ /**
154
+ * Parses a skill file from the registry to extract intent and expected outcome for stubs.
155
+ */
156
+ function parseRegistrySkill(content) {
157
+ const intent = extractSection(content, ['intent', 'skill intent']) ||
158
+ extractLeadParagraph(content) ||
159
+ 'Apply the skill correctly using the provided inputs and constraints.';
160
+ const outcome = extractSection(content, ['outcome', 'expected behavior', 'skill output']) ||
161
+ 'Produce the expected skill output while following skill guardrails.';
162
+ return { intent, outcome };
163
+ }
164
+ /**
165
+ * Parses a rule file from the registry to extract intent and expected behavior for stubs.
166
+ */
167
+ function parseRegistryRule(content) {
168
+ const intent = extractSection(content, ['intent']) ||
169
+ extractLeadParagraph(content) ||
170
+ 'Follow this rule when executing related FRAIM workflows and jobs.';
171
+ const outcome = extractSection(content, ['outcome', 'expected behavior', 'principles']) ||
172
+ 'Consistently apply this rule throughout execution.';
173
+ return { intent, outcome };
174
+ }
@@ -790,6 +790,45 @@ class FraimLocalMCPServer {
790
790
  }
791
791
  return this.applyAgentFallbackForUnresolvedProxy(processedResponse);
792
792
  }
793
+ shouldResolveIncludes(toolName) {
794
+ return toolName === 'get_fraim_workflow' ||
795
+ toolName === 'get_fraim_job' ||
796
+ toolName === 'get_fraim_file' ||
797
+ toolName === 'seekMentoring';
798
+ }
799
+ async resolveIncludesInResponse(response, requestSessionId) {
800
+ if (!response.result?.content || !Array.isArray(response.result.content)) {
801
+ return response;
802
+ }
803
+ const resolver = this.getRegistryResolver(requestSessionId);
804
+ const transformedContent = [];
805
+ for (const block of response.result.content) {
806
+ if (block?.type !== 'text' || typeof block.text !== 'string') {
807
+ transformedContent.push(block);
808
+ continue;
809
+ }
810
+ const resolvedText = await resolver.resolveIncludes(block.text);
811
+ transformedContent.push({
812
+ ...block,
813
+ text: resolvedText
814
+ });
815
+ }
816
+ return {
817
+ ...response,
818
+ result: {
819
+ ...response.result,
820
+ content: transformedContent
821
+ }
822
+ };
823
+ }
824
+ async finalizeToolResponse(request, response, requestSessionId) {
825
+ let finalizedResponse = response;
826
+ const toolName = request.params?.name;
827
+ if (request.method === 'tools/call' && typeof toolName === 'string' && this.shouldResolveIncludes(toolName)) {
828
+ finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId);
829
+ }
830
+ return this.processResponseWithHydration(finalizedResponse, requestSessionId);
831
+ }
793
832
  rewriteProxyTokensInText(text) {
794
833
  const tokens = new Set();
795
834
  const rewritten = text.replace(/\{\{\s*proxy\.([^}]+?)\s*\}\}/g, (_match, proxyPath) => {
@@ -861,35 +900,19 @@ class FraimLocalMCPServer {
861
900
  remoteContentResolver: async (path) => {
862
901
  // Fetch parent content from remote for inheritance
863
902
  this.log(`šŸ”„ Remote content resolver: fetching ${path}`);
864
- let request;
865
- if (path.startsWith('workflows/')) {
866
- // Extract workflow name from path: workflows/category/name.md -> name
867
- const pathParts = path.replace('workflows/', '').replace('.md', '').split('/');
868
- const workflowName = pathParts[pathParts.length - 1]; // Get last part (name)
869
- this.log(`šŸ”„ Fetching workflow: ${workflowName}`);
870
- request = {
871
- jsonrpc: '2.0',
872
- id: (0, crypto_1.randomUUID)(),
873
- method: 'tools/call',
874
- params: {
875
- name: 'get_fraim_workflow',
876
- arguments: { workflow: workflowName }
877
- }
878
- };
879
- }
880
- else {
881
- // For non-workflow files (templates, rules, etc.), use get_fraim_file
882
- this.log(`šŸ”„ Fetching file: ${path}`);
883
- request = {
884
- jsonrpc: '2.0',
885
- id: (0, crypto_1.randomUUID)(),
886
- method: 'tools/call',
887
- params: {
888
- name: 'get_fraim_file',
889
- arguments: { path }
903
+ this.log(`šŸ”„ Fetching raw file content: ${path}`);
904
+ const request = {
905
+ jsonrpc: '2.0',
906
+ id: (0, crypto_1.randomUUID)(),
907
+ method: 'tools/call',
908
+ params: {
909
+ name: 'get_fraim_file',
910
+ arguments: {
911
+ path,
912
+ _internalRaw: true
890
913
  }
891
- };
892
- }
914
+ }
915
+ };
893
916
  this.applyRequestSessionId(request, requestSessionId);
894
917
  const response = await this.proxyToRemote(request);
895
918
  if (response.error) {
@@ -943,6 +966,38 @@ class FraimLocalMCPServer {
943
966
  // Default to product-building for unknown workflows
944
967
  return 'product-building';
945
968
  }
969
+ /**
970
+ * Find local override path for a job name by scanning override roots.
971
+ * Returns a registry-style path like jobs/<category>/<name>.md when found.
972
+ */
973
+ findJobOverridePath(jobName) {
974
+ const projectRoot = this.findProjectRoot();
975
+ if (!projectRoot)
976
+ return null;
977
+ const candidates = [
978
+ (0, path_1.join)(projectRoot, '.fraim', 'personalized-employee', 'jobs'),
979
+ (0, path_1.join)(projectRoot, '.fraim', 'overrides', 'jobs')
980
+ ];
981
+ for (const jobsRoot of candidates) {
982
+ if (!(0, fs_1.existsSync)(jobsRoot))
983
+ continue;
984
+ try {
985
+ const categories = (0, fs_1.readdirSync)(jobsRoot, { withFileTypes: true })
986
+ .filter(entry => entry.isDirectory())
987
+ .map(entry => entry.name);
988
+ for (const category of categories) {
989
+ const jobFilePath = (0, path_1.join)(jobsRoot, category, `${jobName}.md`);
990
+ if ((0, fs_1.existsSync)(jobFilePath)) {
991
+ return `jobs/${category}/${jobName}.md`;
992
+ }
993
+ }
994
+ }
995
+ catch {
996
+ // Best effort scan only.
997
+ }
998
+ }
999
+ return null;
1000
+ }
946
1001
  /**
947
1002
  * Process template substitution in MCP response
948
1003
  */
@@ -1305,7 +1360,7 @@ class FraimLocalMCPServer {
1305
1360
  }
1306
1361
  // Proxy initialize to remote server first
1307
1362
  const response = await this.proxyToRemote(request);
1308
- const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
1363
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1309
1364
  // After successful initialization, load config
1310
1365
  if (!processedResponse.error) {
1311
1366
  // Load config immediately for compatibility, then request roots so
@@ -1318,9 +1373,10 @@ class FraimLocalMCPServer {
1318
1373
  this.log(`šŸ“¤ ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1319
1374
  return processedResponse;
1320
1375
  }
1321
- // Intercept get_fraim_workflow and get_fraim_file for override resolution
1376
+ // Intercept get_fraim_workflow, get_fraim_job, and get_fraim_file for override resolution
1322
1377
  if (request.method === 'tools/call' &&
1323
1378
  (request.params?.name === 'get_fraim_workflow' ||
1379
+ request.params?.name === 'get_fraim_job' ||
1324
1380
  request.params?.name === 'get_fraim_file')) {
1325
1381
  try {
1326
1382
  const toolName = request.params.name;
@@ -1359,7 +1415,41 @@ class FraimLocalMCPServer {
1359
1415
  }
1360
1416
  };
1361
1417
  // Apply template substitution
1362
- const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
1418
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1419
+ this.log(`šŸ“¤ ${request.method} → OK`);
1420
+ return processedResponse;
1421
+ }
1422
+ }
1423
+ }
1424
+ else if (toolName === 'get_fraim_job') {
1425
+ const jobName = args.job;
1426
+ if (!jobName) {
1427
+ this.log('āš ļø No job name provided in get_fraim_job');
1428
+ }
1429
+ else {
1430
+ // Determine job path by scanning local override roots.
1431
+ requestedPath = this.findJobOverridePath(jobName) || `jobs/product-building/${jobName}.md`;
1432
+ this.log(`šŸ” Checking for override: ${requestedPath}`);
1433
+ const resolver = this.getRegistryResolver(requestSessionId);
1434
+ const hasOverride = resolver.hasLocalOverride(requestedPath);
1435
+ this.log(`šŸ” hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1436
+ if (hasOverride) {
1437
+ this.log(`āœ… Local override found: ${requestedPath}`);
1438
+ const resolved = await resolver.resolveFile(requestedPath);
1439
+ this.log(`šŸ“ Override resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1440
+ const response = {
1441
+ jsonrpc: '2.0',
1442
+ id: request.id,
1443
+ result: {
1444
+ content: [
1445
+ {
1446
+ type: 'text',
1447
+ text: resolved.content
1448
+ }
1449
+ ]
1450
+ }
1451
+ };
1452
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1363
1453
  this.log(`šŸ“¤ ${request.method} → OK`);
1364
1454
  return processedResponse;
1365
1455
  }
@@ -1378,6 +1468,27 @@ class FraimLocalMCPServer {
1378
1468
  else {
1379
1469
  this.log(`šŸ” Checking for override: ${requestedPath}`);
1380
1470
  const resolver = this.getRegistryResolver(requestSessionId);
1471
+ const isLocalFirstSyncedPath = requestedPath.startsWith('skills/') || requestedPath.startsWith('rules/');
1472
+ if (isLocalFirstSyncedPath && resolver.hasSyncedLocalFile(requestedPath)) {
1473
+ this.log(`āœ… Synced local file found: ${requestedPath}`);
1474
+ const resolved = await resolver.resolveFile(requestedPath, { includeMetadata: false });
1475
+ this.log(`šŸ“ Synced local file resolved (source: ${resolved.source}, inherited: ${resolved.inherited})`);
1476
+ const response = {
1477
+ jsonrpc: '2.0',
1478
+ id: request.id,
1479
+ result: {
1480
+ content: [
1481
+ {
1482
+ type: 'text',
1483
+ text: resolved.content
1484
+ }
1485
+ ]
1486
+ }
1487
+ };
1488
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1489
+ this.log(`šŸ“¤ ${request.method} → OK`);
1490
+ return processedResponse;
1491
+ }
1381
1492
  const hasOverride = resolver.hasLocalOverride(requestedPath);
1382
1493
  this.log(`šŸ” hasLocalOverride(${requestedPath}) = ${hasOverride}`);
1383
1494
  if (hasOverride) {
@@ -1398,7 +1509,7 @@ class FraimLocalMCPServer {
1398
1509
  }
1399
1510
  };
1400
1511
  // Apply template substitution
1401
- const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
1512
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1402
1513
  this.log(`šŸ“¤ ${request.method} → OK`);
1403
1514
  return processedResponse;
1404
1515
  }
@@ -1414,7 +1525,7 @@ class FraimLocalMCPServer {
1414
1525
  }
1415
1526
  // Proxy to remote server
1416
1527
  const response = await this.proxyToRemote(request);
1417
- const processedResponse = await this.processResponseWithHydration(response, requestSessionId);
1528
+ const processedResponse = await this.finalizeToolResponse(request, response, requestSessionId);
1418
1529
  this.log(`šŸ“¤ ${request.method} → ${processedResponse.error ? 'ERROR' : 'OK'}`);
1419
1530
  return processedResponse;
1420
1531
  }
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.84",
3
+ "version": "2.0.86",
4
4
  "description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "test-all": "npm run test && npm run test:isolated && npm run test:ui",
17
17
  "test": "node scripts/test-with-server.js",
18
18
  "test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
19
+ "test:smoke": "node scripts/test-with-server.js tests/test-*.ts --tags=smoke",
19
20
  "test:ui": "playwright test",
20
21
  "test:ui:headed": "playwright test --headed",
21
22
  "start:fraim": "tsx src/fraim-mcp-server.ts",