fraim-framework 2.0.87 → 2.0.89

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.
@@ -81,14 +81,14 @@ class InheritanceParser {
81
81
  }
82
82
  }
83
83
  /**
84
- * Resolve all import directives in content recursively
84
+ * Resolve all import and extends directives in content recursively
85
85
  *
86
- * @param content - Content with {{ import }} directives
86
+ * @param content - Content with {{ import }} or extends frontmatter
87
87
  * @param currentPath - Path of current file (for circular detection)
88
88
  * @param options - Resolution options
89
- * @returns Resolved content with all imports replaced
89
+ * @returns Resolved content with all inheritance applied
90
90
  *
91
- * @throws {InheritanceError} If circular import, path traversal, or max depth exceeded
91
+ * @throws {InheritanceError} If circular inheritance, path traversal, or max depth exceeded
92
92
  */
93
93
  async resolve(content, currentPath, options) {
94
94
  const depth = options.currentDepth || 0;
@@ -96,18 +96,53 @@ class InheritanceParser {
96
96
  const maxDepth = options.maxDepth || this.maxDepth;
97
97
  // Check depth limit
98
98
  if (depth > maxDepth) {
99
- throw new InheritanceError(`Max import depth exceeded (${maxDepth})`, currentPath);
99
+ throw new InheritanceError(`Max inheritance depth exceeded (${maxDepth})`, currentPath);
100
100
  }
101
- // Check circular imports (but allow importing the same path as parent)
101
+ // Check circular inheritance (but allow importing/extending the same path as parent)
102
102
  this.detectCircularImport(currentPath, visited, false);
103
103
  visited.add(currentPath);
104
- // Extract imports
105
- const imports = this.extractImports(content);
106
- if (imports.length === 0) {
107
- return content;
104
+ let resolvedContent = content;
105
+ // 1. Handle JSON frontmatter 'extends'
106
+ const metadataMatch = resolvedContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
107
+ if (metadataMatch) {
108
+ try {
109
+ const metadata = JSON.parse(metadataMatch[1]);
110
+ const extendsPath = metadata.extends;
111
+ if (extendsPath && typeof extendsPath === 'string') {
112
+ // Sanitize path
113
+ const sanitizedExtends = this.sanitizePath(extendsPath);
114
+ const isParentExtends = sanitizedExtends === currentPath;
115
+ // Fetch parent content
116
+ let parentContent;
117
+ try {
118
+ parentContent = await options.fetchParent(sanitizedExtends);
119
+ }
120
+ catch (error) {
121
+ throw new InheritanceError(`Failed to fetch extended parent content: ${sanitizedExtends}. ${error.message}`, sanitizedExtends);
122
+ }
123
+ // Recursively resolve parent
124
+ const parentVisited = isParentExtends ? new Set() : new Set(visited);
125
+ const resolvedParent = await this.resolve(parentContent, sanitizedExtends, {
126
+ ...options,
127
+ currentDepth: depth + 1,
128
+ visited: parentVisited
129
+ });
130
+ // Merge current content with resolved parent
131
+ resolvedContent = this.mergeContent(resolvedContent, resolvedParent);
132
+ }
133
+ }
134
+ catch (error) {
135
+ if (error instanceof SyntaxError) {
136
+ // Not JSON or invalid JSON, ignore extends logic but log it
137
+ console.warn(`[InheritanceParser] Failed to parse frontmatter for ${currentPath}: ${error.message}`);
138
+ }
139
+ else {
140
+ throw error;
141
+ }
142
+ }
108
143
  }
109
- // Resolve each import
110
- let resolved = content;
144
+ // 2. Handle {{ import: path }}
145
+ const imports = this.extractImports(resolvedContent);
111
146
  for (const importPath of imports) {
112
147
  // Sanitize path
113
148
  const sanitized = this.sanitizePath(importPath);
@@ -122,7 +157,6 @@ class InheritanceParser {
122
157
  throw new InheritanceError(`Failed to fetch parent content: ${sanitized}. ${error.message}`, sanitized);
123
158
  }
124
159
  // Recursively resolve parent imports
125
- // For parent imports, use a fresh visited set to allow the same path
126
160
  const parentVisited = isParentImport ? new Set() : new Set(visited);
127
161
  const resolvedParent = await this.resolve(parentContent, sanitized, {
128
162
  ...options,
@@ -131,20 +165,124 @@ class InheritanceParser {
131
165
  });
132
166
  // Replace import directive with resolved parent content
133
167
  const importDirective = `{{ import: ${importPath} }}`;
134
- resolved = resolved.replace(importDirective, resolvedParent);
168
+ resolvedContent = resolvedContent.replace(importDirective, resolvedParent);
135
169
  }
136
- return resolved;
170
+ return resolvedContent;
171
+ }
172
+ /**
173
+ * Merge two registry files (child override + parent base)
174
+ *
175
+ * Merging rules:
176
+ * 1. Metadata: JSON merge (child overrides parent)
177
+ * 2. Overview: Parent overview + child overview (if multi-para)
178
+ * 3. Phases: Phase override (child phase with same ID replaces parent phase)
179
+ */
180
+ mergeContent(child, parent) {
181
+ const childMatch = child.match(/^---\r?\n([\s\S]+?)\r?\n---/);
182
+ const parentMatch = parent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
183
+ if (!childMatch || !parentMatch)
184
+ return child;
185
+ // 1. Merge Metadata
186
+ const childMeta = JSON.parse(childMatch[1]);
187
+ const parentMeta = JSON.parse(parentMatch[1]);
188
+ const mergedMeta = { ...parentMeta, ...childMeta };
189
+ delete mergedMeta.extends; // Remove extends from final merged content
190
+ // 2. Extract Body (everything after frontmatter)
191
+ const childBody = this.stripRedundantParentImports(child.substring(childMatch[0].length).trim(), typeof childMeta.extends === 'string' ? childMeta.extends : undefined);
192
+ const parentBody = parent.substring(parentMatch[0].length).trim();
193
+ // 3. Parse Phases and Overview
194
+ const parsePhases = (body) => {
195
+ const phases = new Map();
196
+ const sections = body.split(/^##\s+Phase:\s+/m);
197
+ const overview = sections[0]?.trim() || '';
198
+ for (let i = 1; i < sections.length; i++) {
199
+ const section = sections[i];
200
+ if (!section.trim())
201
+ continue;
202
+ // Extract ID from first line: e.g. "implement-scoping (Primary)" -> "implement-scoping"
203
+ const firstLine = section.split(/\r?\n/)[0].trim();
204
+ const id = firstLine.split(/[ (]/)[0].trim().toLowerCase();
205
+ phases.set(id, `## Phase: ${section.trim()}`);
206
+ }
207
+ return { overview, phases };
208
+ };
209
+ const childParts = parsePhases(childBody);
210
+ const parentParts = parsePhases(parentBody);
211
+ // 4. Merge Overview: retain the parent framing, then append local overview additions.
212
+ const mergedOverview = childParts.overview
213
+ ? `${parentParts.overview}\n\n${childParts.overview}`.trim()
214
+ : parentParts.overview;
215
+ // 5. Merge Phases
216
+ const mergedPhases = new Map(parentParts.phases);
217
+ for (const [id, content] of childParts.phases.entries()) {
218
+ mergedPhases.set(id, content);
219
+ }
220
+ // 6. Reassemble
221
+ let finalContent = `---\n${JSON.stringify(mergedMeta, null, 2)}\n---\n\n`;
222
+ if (mergedOverview) {
223
+ finalContent += `${mergedOverview}\n\n`;
224
+ }
225
+ const addedPhases = new Set();
226
+ // First, add parent phases in order, using child overrides when present.
227
+ for (const id of parentParts.phases.keys()) {
228
+ if (mergedPhases.has(id)) {
229
+ finalContent += `${mergedPhases.get(id)}\n\n`;
230
+ addedPhases.add(id);
231
+ }
232
+ }
233
+ // Then append child-only phases in the order the child declared them.
234
+ for (const [id, content] of childParts.phases.entries()) {
235
+ if (!addedPhases.has(id)) {
236
+ finalContent += `${content}\n\n`;
237
+ addedPhases.add(id);
238
+ }
239
+ }
240
+ return finalContent.trim();
137
241
  }
138
242
  /**
139
243
  * Parse content and return detailed information about imports
140
244
  */
141
245
  parse(content) {
142
246
  const imports = this.extractImports(content);
247
+ const hasExtends = /^---\r?\n[\s\S]*?"extends":\s*"[^"]+"[\s\S]*?\r?\n---/m.test(content);
143
248
  return {
144
249
  content,
145
250
  imports,
146
- hasImports: imports.length > 0
251
+ hasImports: imports.length > 0 || hasExtends
147
252
  };
148
253
  }
254
+ normalizeImportRef(path) {
255
+ let normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
256
+ if (normalized.endsWith('.md')) {
257
+ normalized = normalized.slice(0, -3);
258
+ }
259
+ return normalized;
260
+ }
261
+ stripTypePrefix(path) {
262
+ return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
263
+ }
264
+ isEquivalentImportRef(left, right) {
265
+ const normalizedLeft = this.normalizeImportRef(left);
266
+ const normalizedRight = this.normalizeImportRef(right);
267
+ const strippedLeft = this.stripTypePrefix(normalizedLeft);
268
+ const strippedRight = this.stripTypePrefix(normalizedRight);
269
+ return normalizedLeft === normalizedRight ||
270
+ strippedLeft === strippedRight ||
271
+ normalizedLeft.endsWith(`/${strippedRight}`) ||
272
+ normalizedRight.endsWith(`/${strippedLeft}`) ||
273
+ strippedLeft.endsWith(`/${strippedRight}`) ||
274
+ strippedRight.endsWith(`/${strippedLeft}`);
275
+ }
276
+ stripRedundantParentImports(body, extendsPath) {
277
+ if (!extendsPath) {
278
+ return body;
279
+ }
280
+ return body
281
+ .replace(/\{\{\s*import:\s*([^\}]+)\s*\}\}\s*\r?\n?/g, (match, importPath) => {
282
+ return this.isEquivalentImportRef(importPath, extendsPath) ? '' : match;
283
+ })
284
+ .replace(/\n{3,}/g, '\n\n')
285
+ .trim();
286
+ }
149
287
  }
150
288
  exports.InheritanceParser = InheritanceParser;
@@ -5,16 +5,52 @@
5
5
  * Resolves registry file requests by checking for local overrides first,
6
6
  * then falling back to remote registry. Handles inheritance via InheritanceParser.
7
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  Object.defineProperty(exports, "__esModule", { value: true });
9
42
  exports.LocalRegistryResolver = void 0;
43
+ const fs = __importStar(require("fs"));
10
44
  const fs_1 = require("fs");
11
45
  const path_1 = require("path");
12
46
  const inheritance_parser_1 = require("./inheritance-parser");
47
+ const workflow_parser_1 = require("./workflow-parser");
13
48
  class LocalRegistryResolver {
14
49
  constructor(options) {
15
50
  this.workspaceRoot = options.workspaceRoot;
16
51
  this.remoteContentResolver = options.remoteContentResolver;
17
52
  this.parser = new inheritance_parser_1.InheritanceParser(options.maxDepth);
53
+ this.shouldFilter = options.shouldFilter;
18
54
  }
19
55
  /**
20
56
  * Check if a local override exists for the given path
@@ -22,9 +58,52 @@ class LocalRegistryResolver {
22
58
  hasLocalOverride(path) {
23
59
  const primaryPath = this.getOverridePath(path);
24
60
  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}`);
27
- return exists;
61
+ return (0, fs_1.existsSync)(primaryPath) || (0, fs_1.existsSync)(legacyPath);
62
+ }
63
+ /**
64
+ * Find the actual relative path for a registry item by searching subdirectories.
65
+ * Returns '{type}/{category}/{name}.md' or '{type}/{name}.md'.
66
+ */
67
+ async findRegistryPath(type, name) {
68
+ const baseName = `${name}.md`;
69
+ const searchDirs = [type];
70
+ for (const dir of searchDirs) {
71
+ // Check literal path first
72
+ const literal = `${dir}/${baseName}`;
73
+ if (this.hasLocalOverride(literal))
74
+ return literal;
75
+ // Deep search
76
+ const fullBaseDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', dir);
77
+ const legacyBaseDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'overrides', dir);
78
+ const found = this.searchFileRecursively(fullBaseDir, baseName) ||
79
+ this.searchFileRecursively(legacyBaseDir, baseName);
80
+ if (found) {
81
+ // Convert absolute back to relative
82
+ const rel = found.replace(/\\/g, '/');
83
+ const dirMarker = `/${dir}/`;
84
+ if (rel.includes(dirMarker)) {
85
+ return rel.substring(rel.indexOf(dirMarker) + 1);
86
+ }
87
+ }
88
+ }
89
+ return `${type}/${name}.md`;
90
+ }
91
+ searchFileRecursively(dir, fileName) {
92
+ if (!(0, fs_1.existsSync)(dir))
93
+ return null;
94
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
95
+ for (const entry of entries) {
96
+ const fullPath = (0, path_1.join)(dir, entry.name);
97
+ if (entry.isDirectory()) {
98
+ const found = this.searchFileRecursively(fullPath, fileName);
99
+ if (found)
100
+ return found;
101
+ }
102
+ else if (entry.name === fileName) {
103
+ return fullPath;
104
+ }
105
+ }
106
+ return null;
28
107
  }
29
108
  /**
30
109
  * Check if a locally synced skill/rule file exists for the given registry path.
@@ -37,14 +116,24 @@ class LocalRegistryResolver {
37
116
  * Get the full path to a local override file
38
117
  */
39
118
  getOverridePath(path) {
40
- return (0, path_1.join)(this.workspaceRoot, '.fraim/personalized-employee', path);
119
+ const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
120
+ // Personal overrides are in .fraim/personalized-employee/
121
+ // We don't need a redundant 'registry/' subfolder here as the path already includes type (e.g. jobs/)
122
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', normalized);
41
123
  }
42
124
  /**
43
125
  * Get the full path to a legacy local override file.
44
126
  * Kept for backward compatibility while migrating to personalized-employee.
45
127
  */
46
128
  getLegacyOverridePath(path) {
47
- return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', path);
129
+ const normalized = path.replace(/\\/g, '/').replace(/^\/+/, '');
130
+ const parts = normalized.split('/');
131
+ let lookupPath = normalized;
132
+ if (parts.length > 1 && (parts[0] === 'jobs' || parts[0] === 'workflows' || parts[0] === 'skills' || parts[0] === 'rules')) {
133
+ lookupPath = parts.slice(1).join('/');
134
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', 'overrides', parts[0], lookupPath);
135
+ }
136
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/overrides', normalized);
48
137
  }
49
138
  /**
50
139
  * Get the full path to a locally synced FRAIM file when available.
@@ -54,9 +143,58 @@ class LocalRegistryResolver {
54
143
  */
55
144
  getSyncedFilePath(path) {
56
145
  const normalizedPath = path.replace(/\\/g, '/').replace(/^\/+/, '');
146
+ // 1. Workflows: workflows/[role]/path -> .fraim/[role]/workflows/path
147
+ if (normalizedPath.startsWith('workflows/')) {
148
+ const parts = normalizedPath.split('/');
149
+ if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
150
+ const role = parts[1];
151
+ const subPath = parts.slice(2).join('/');
152
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'workflows', subPath);
153
+ }
154
+ // Fallback: Try ai-employee and ai-manager if no role prefix
155
+ const subPath = normalizedPath.substring('workflows/'.length);
156
+ const employeePath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-employee', 'workflows', subPath);
157
+ if (fs.existsSync(employeePath))
158
+ return employeePath;
159
+ const managerPath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-manager', 'workflows', subPath);
160
+ if (fs.existsSync(managerPath))
161
+ return managerPath;
162
+ // Fallback for non-role-prefixed (legacy or direct)
163
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', normalizedPath);
164
+ }
165
+ // 2. Jobs: jobs/[role]/path -> .fraim/[role]/jobs/path
166
+ if (normalizedPath.startsWith('jobs/')) {
167
+ const parts = normalizedPath.split('/');
168
+ if (parts.length >= 3 && (parts[1] === 'ai-employee' || parts[1] === 'ai-manager')) {
169
+ const role = parts[1];
170
+ const subPath = parts.slice(2).join('/');
171
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'jobs', subPath);
172
+ }
173
+ // Fallback: Try ai-employee and ai-manager if no role prefix
174
+ const subPath = normalizedPath.substring('jobs/'.length);
175
+ const employeePath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-employee', 'jobs', subPath);
176
+ if (fs.existsSync(employeePath))
177
+ return employeePath;
178
+ const managerPath = (0, path_1.join)(this.workspaceRoot, '.fraim', 'ai-manager', 'jobs', subPath);
179
+ if (fs.existsSync(managerPath))
180
+ return managerPath;
181
+ // Fallback
182
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', normalizedPath);
183
+ }
184
+ // 3. Rules: [role]/rules/path -> .fraim/[role]/rules/path
185
+ if (normalizedPath.includes('/rules/')) {
186
+ const role = normalizedPath.includes('/ai-manager/') ? 'ai-manager' : 'ai-employee';
187
+ // Extract the part after "rules/"
188
+ const rulesIdx = normalizedPath.indexOf('rules/');
189
+ const subPath = normalizedPath.substring(rulesIdx + 'rules/'.length);
190
+ return (0, path_1.join)(this.workspaceRoot, '.fraim', role, 'rules', subPath);
191
+ }
192
+ // 4. Skills: skills/path -> .fraim/ai-employee/skills/path (default to ai-employee)
57
193
  if (normalizedPath.startsWith('skills/')) {
58
- return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
194
+ const subPath = normalizedPath.substring('skills/'.length);
195
+ return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', 'skills', subPath);
59
196
  }
197
+ // 5. Rules: rules/path -> .fraim/ai-employee/rules/path (default to ai-employee)
60
198
  if (normalizedPath.startsWith('rules/')) {
61
199
  return (0, path_1.join)(this.workspaceRoot, '.fraim/ai-employee', normalizedPath);
62
200
  }
@@ -84,12 +222,55 @@ class LocalRegistryResolver {
84
222
  return null;
85
223
  }
86
224
  try {
87
- return (0, fs_1.readFileSync)(syncedPath, 'utf-8');
225
+ const content = (0, fs_1.readFileSync)(syncedPath, 'utf-8');
226
+ if (this.shouldFilter && this.shouldFilter(content)) {
227
+ return null;
228
+ }
229
+ return content;
88
230
  }
89
231
  catch {
90
232
  return null;
91
233
  }
92
234
  }
235
+ normalizeRegistryPath(path) {
236
+ const normalized = path.trim().replace(/\\/g, '/').replace(/^\/+/, '');
237
+ return normalized.endsWith('.md') ? normalized : `${normalized}.md`;
238
+ }
239
+ stripTypePrefix(path) {
240
+ return path.replace(/^(jobs|workflows|skills|rules|templates)\//, '');
241
+ }
242
+ areEquivalentRegistryPaths(left, right) {
243
+ const normalizedLeft = this.normalizeRegistryPath(left);
244
+ const normalizedRight = this.normalizeRegistryPath(right);
245
+ const strippedLeft = this.stripTypePrefix(normalizedLeft);
246
+ const strippedRight = this.stripTypePrefix(normalizedRight);
247
+ return normalizedLeft === normalizedRight ||
248
+ strippedLeft === strippedRight ||
249
+ normalizedLeft.endsWith(`/${strippedRight}`) ||
250
+ normalizedRight.endsWith(`/${strippedLeft}`) ||
251
+ strippedLeft.endsWith(`/${strippedRight}`) ||
252
+ strippedRight.endsWith(`/${strippedLeft}`);
253
+ }
254
+ async fetchRemoteWithFallback(...paths) {
255
+ const candidates = new Set();
256
+ for (const path of paths) {
257
+ if (!path)
258
+ continue;
259
+ const normalized = this.normalizeRegistryPath(path);
260
+ candidates.add(normalized);
261
+ candidates.add(this.stripTypePrefix(normalized));
262
+ }
263
+ let lastError = null;
264
+ for (const candidate of candidates) {
265
+ try {
266
+ return await this.remoteContentResolver(candidate);
267
+ }
268
+ catch (error) {
269
+ lastError = error;
270
+ }
271
+ }
272
+ throw lastError || new Error(`Failed to fetch remote content for ${paths.join(', ')}`);
273
+ }
93
274
  stripMcpHeader(content) {
94
275
  const trimmed = content.trimStart();
95
276
  if (!trimmed.startsWith('#')) {
@@ -115,10 +296,44 @@ class LocalRegistryResolver {
115
296
  if (!parseResult.hasImports) {
116
297
  return { content, imports: [] };
117
298
  }
118
- // Resolve imports
299
+ // Resolve inheritance
119
300
  try {
120
301
  const resolved = await this.parser.resolve(content, currentPath, {
121
- fetchParent: this.remoteContentResolver,
302
+ fetchParent: async (path) => {
303
+ const normalized = this.normalizeRegistryPath(path);
304
+ const normalizedCurrent = this.normalizeRegistryPath(currentPath);
305
+ // If the import points back to the current file, skip local resolution and fetch
306
+ // from the next layer (remote/global) instead.
307
+ if (this.areEquivalentRegistryPaths(normalized, normalizedCurrent)) {
308
+ return this.fetchRemoteWithFallback(normalizedCurrent, normalized);
309
+ }
310
+ // Otherwise, resolve through the normal local-first path.
311
+ try {
312
+ let targetPath = normalized;
313
+ const hasKnownPrefix = targetPath.startsWith('jobs/') || targetPath.startsWith('workflows/') ||
314
+ targetPath.startsWith('skills/') || targetPath.startsWith('rules/') || targetPath.startsWith('templates/');
315
+ if (!hasKnownPrefix) {
316
+ // Try to guess type from currentPath or just try jobs/workflows
317
+ const type = normalizedCurrent.startsWith('jobs/') ? 'jobs' : (normalizedCurrent.startsWith('workflows/') ? 'workflows' : null);
318
+ if (type) {
319
+ try {
320
+ targetPath = await this.findRegistryPath(type, normalized.replace(/\.md$/, ''));
321
+ }
322
+ catch {
323
+ // Ignore and use original normalized
324
+ }
325
+ }
326
+ }
327
+ const res = await this.resolveFile(targetPath, {
328
+ includeMetadata: false,
329
+ stripMcpHeader: true
330
+ });
331
+ return res.content;
332
+ }
333
+ catch (error) {
334
+ throw error;
335
+ }
336
+ },
122
337
  maxDepth: 5
123
338
  });
124
339
  return {
@@ -158,7 +373,6 @@ class LocalRegistryResolver {
158
373
  * @returns Resolved file with metadata
159
374
  */
160
375
  async resolveFile(path, options = {}) {
161
- console.error(`[LocalRegistryResolver] ===== resolveFile called for: ${path} =====`);
162
376
  const includeMetadata = options.includeMetadata ?? true;
163
377
  const stripMcpHeader = options.stripMcpHeader ?? false;
164
378
  // Check for local override
@@ -171,9 +385,9 @@ class LocalRegistryResolver {
171
385
  inherited: false
172
386
  };
173
387
  }
174
- // No override, fetch from remote
388
+ // No useful override or synced content, fetch from remote
175
389
  try {
176
- const rawContent = await this.remoteContentResolver(path);
390
+ const rawContent = await this.fetchRemoteWithFallback(path);
177
391
  const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
178
392
  return {
179
393
  content,
@@ -189,11 +403,15 @@ class LocalRegistryResolver {
189
403
  let localContent;
190
404
  try {
191
405
  localContent = this.readLocalOverride(path);
406
+ if (this.shouldFilter && this.shouldFilter(localContent)) {
407
+ console.warn(`[LocalRegistryResolver] Local override for ${path} filtered (e.g. stub), falling back to remote.`);
408
+ throw new Error('CONTENT_FILTERED');
409
+ }
192
410
  }
193
411
  catch (error) {
194
412
  // If local read fails, fall back to remote
195
413
  console.warn(`Local override read failed, falling back to remote: ${path}`);
196
- const rawContent = await this.remoteContentResolver(path);
414
+ const rawContent = await this.fetchRemoteWithFallback(path);
197
415
  const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
198
416
  return {
199
417
  content,
@@ -204,18 +422,11 @@ class LocalRegistryResolver {
204
422
  // Resolve inheritance
205
423
  let resolved;
206
424
  try {
207
- console.error(`[LocalRegistryResolver] Resolving inheritance for ${path}`);
208
- console.error(`[LocalRegistryResolver] Local content length: ${localContent.length} chars`);
209
- console.error(`[LocalRegistryResolver] Local content preview: ${localContent.substring(0, 200)}`);
210
425
  resolved = await this.resolveInheritance(localContent, path);
211
- console.error(`[LocalRegistryResolver] Inheritance resolved: ${resolved.imports.length} imports`);
212
- console.error(`[LocalRegistryResolver] Resolved content length: ${resolved.content.length} chars`);
213
- console.error(`[LocalRegistryResolver] Resolved content preview: ${resolved.content.substring(0, 200)}`);
214
426
  }
215
427
  catch (error) {
216
- // If inheritance resolution fails, fall back to remote
217
- console.error(`❌ Inheritance resolution failed for ${path}:`, error);
218
- const content = await this.remoteContentResolver(path);
428
+ const rawContent = await this.fetchRemoteWithFallback(path);
429
+ const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
219
430
  return {
220
431
  content,
221
432
  source: 'remote',
@@ -317,6 +528,99 @@ class LocalRegistryResolver {
317
528
  cache: new Map()
318
529
  });
319
530
  }
531
+ /**
532
+ * Implement RegistryResolver: Resolve raw file content
533
+ */
534
+ async getFile(path) {
535
+ try {
536
+ const result = await this.resolveFile(path, {
537
+ includeMetadata: false,
538
+ stripMcpHeader: true
539
+ });
540
+ return result.content;
541
+ }
542
+ catch {
543
+ return null;
544
+ }
545
+ }
546
+ /**
547
+ * Implement RegistryResolver: Parse and resolve a workflow or job
548
+ */
549
+ async getWorkflow(name, preferredType) {
550
+ const types = preferredType
551
+ ? [preferredType === 'job' ? 'jobs' : 'workflows']
552
+ : ['workflows', 'jobs'];
553
+ for (const typeDir of types) {
554
+ try {
555
+ const path = await this.findRegistryPath(typeDir, name);
556
+ const resolved = await this.resolveFile(path, {
557
+ includeMetadata: false,
558
+ stripMcpHeader: true
559
+ });
560
+ if (resolved) {
561
+ return workflow_parser_1.WorkflowParser.parseContent(resolved.content, name, path);
562
+ }
563
+ }
564
+ catch (err) {
565
+ // Continue to next type if not successful
566
+ continue;
567
+ }
568
+ }
569
+ // Fallback for names that might already include type/category
570
+ try {
571
+ const finalPath = name.endsWith('.md') ? name : `${name}.md`;
572
+ const resolved = await this.resolveFile(finalPath, {
573
+ includeMetadata: false,
574
+ stripMcpHeader: true
575
+ });
576
+ return workflow_parser_1.WorkflowParser.parseContent(resolved.content, name, finalPath);
577
+ }
578
+ catch {
579
+ return null;
580
+ }
581
+ }
582
+ /**
583
+ * Implement RegistryResolver: List available items
584
+ * Note: For proxy, this is a merged view of local and potentially remote.
585
+ * Remote discovery is handled separately in list_fraim_jobs/workflows.
586
+ */
587
+ async listItems(type) {
588
+ // Current LocalRegistryResolver doesn't maintain a full list of remote items,
589
+ // so it primarily returns local overrides.
590
+ const items = [];
591
+ const dirs = type === 'job' ? ['jobs'] : (type === 'workflow' ? ['workflows'] : ['jobs', 'workflows']);
592
+ for (const dir of dirs) {
593
+ const localDir = (0, path_1.join)(this.workspaceRoot, '.fraim', 'personalized-employee', dir);
594
+ if (fs.existsSync(localDir)) {
595
+ const relPaths = this.collectLocalMarkdownPaths(localDir);
596
+ for (const rel of relPaths) {
597
+ items.push({
598
+ name: rel.replace(/\.md$/, ''),
599
+ path: `${dir}/${rel}`,
600
+ type: dir === 'jobs' ? 'job' : 'workflow'
601
+ });
602
+ }
603
+ }
604
+ }
605
+ return items;
606
+ }
607
+ collectLocalMarkdownPaths(dir, currentRel = '') {
608
+ const results = [];
609
+ if (!fs.existsSync(dir))
610
+ return results;
611
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
612
+ for (const entry of entries) {
613
+ const rel = currentRel ? `${currentRel}/${entry.name}` : entry.name;
614
+ const full = (0, path_1.join)(dir, entry.name);
615
+ if (entry.isDirectory()) {
616
+ results.push(...this.collectLocalMarkdownPaths(full, rel));
617
+ }
618
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
619
+ results.push(rel);
620
+ }
621
+ }
622
+ return results;
623
+ }
320
624
  }
321
625
  exports.LocalRegistryResolver = LocalRegistryResolver;
322
626
  LocalRegistryResolver.MAX_INCLUDE_DEPTH = 10;