fraim-framework 2.0.83 → 2.0.85

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.
@@ -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
+ }
@@ -13,7 +13,11 @@ class WorkflowParser {
13
13
  static parse(filePath) {
14
14
  if (!(0, fs_1.existsSync)(filePath))
15
15
  return null;
16
- const content = (0, fs_1.readFileSync)(filePath, 'utf-8');
16
+ let content = (0, fs_1.readFileSync)(filePath, 'utf-8');
17
+ // Strip optional UTF-8 BOM so frontmatter regex remains stable across editors.
18
+ if (content.charCodeAt(0) === 0xfeff) {
19
+ content = content.slice(1);
20
+ }
17
21
  // Try to extract JSON Metadata (frontmatter)
18
22
  const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
19
23
  if (metadataMatch) {