fraim-framework 2.0.86 → 2.0.88

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.
@@ -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;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPortConflictError = createPortConflictError;
4
+ exports.startListening = startListening;
5
+ exports.logStartupError = logStartupError;
6
+ function createPortConflictError(port) {
7
+ const error = new Error(`Port ${port} is already in use. Another process is already listening on http://localhost:${port}. Stop that process or set FRAIM_MCP_PORT/PORT to a different port.`);
8
+ error.code = 'EADDRINUSE';
9
+ return error;
10
+ }
11
+ async function startListening(app, port, onListening) {
12
+ await new Promise((resolve, reject) => {
13
+ const server = app.listen(port);
14
+ server.once('listening', () => {
15
+ onListening();
16
+ resolve();
17
+ });
18
+ server.once('error', (error) => {
19
+ if (error?.code === 'EADDRINUSE') {
20
+ reject(createPortConflictError(port));
21
+ return;
22
+ }
23
+ reject(error);
24
+ });
25
+ });
26
+ }
27
+ function logStartupError(error) {
28
+ const maybePortError = error;
29
+ if (maybePortError?.code === 'EADDRINUSE') {
30
+ console.error(`[FRAIM PORT] ${maybePortError.message}`);
31
+ return;
32
+ }
33
+ console.error('❌ Failed to start FRAIM MCP Server:', error);
34
+ }
@@ -8,6 +8,7 @@ exports.generateRuleStub = generateRuleStub;
8
8
  exports.parseRegistryJob = parseRegistryJob;
9
9
  exports.parseRegistrySkill = parseRegistrySkill;
10
10
  exports.parseRegistryRule = parseRegistryRule;
11
+ const STUB_MARKER = '<!-- FRAIM_DISCOVERY_STUB -->';
11
12
  function extractSection(content, headingPatterns) {
12
13
  for (const heading of headingPatterns) {
13
14
  const pattern = new RegExp(`(?:^|\\n)#{2,3}\\s*${heading}\\s*\\n([\\s\\S]*?)(?=\\n#{2,3}\\s|$)`, 'i');
@@ -49,7 +50,8 @@ function extractLeadParagraph(content) {
49
50
  * These stubs are committed to the user's repo for discoverability.
50
51
  */
51
52
  function generateWorkflowStub(workflowName, workflowPath, intent, principles) {
52
- return `# FRAIM Workflow: ${workflowName}
53
+ return `${STUB_MARKER}
54
+ # FRAIM Workflow: ${workflowName}
53
55
 
54
56
  > [!IMPORTANT]
55
57
  > This is a **FRAIM-managed workflow stub**.
@@ -78,8 +80,9 @@ function parseRegistryWorkflow(content) {
78
80
  /**
79
81
  * Coaching stubs are discoverability artifacts and should be resolved with get_fraim_file.
80
82
  */
81
- function generateJobStub(jobName, _jobPath, intent, outcome) {
82
- return `# FRAIM Job: ${jobName}
83
+ function generateJobStub(jobName, _jobPath, intent, outcome, steps) {
84
+ return `${STUB_MARKER}
85
+ # FRAIM Job: ${jobName}
83
86
 
84
87
  ## Intent
85
88
  ${intent}
@@ -87,6 +90,9 @@ ${intent}
87
90
  ## Outcome
88
91
  ${outcome}
89
92
 
93
+ ## Steps
94
+ ${steps}
95
+
90
96
  ---
91
97
 
92
98
  > [!IMPORTANT]
@@ -103,19 +109,21 @@ ${outcome}
103
109
  /**
104
110
  * Generates a lightweight markdown stub for a skill.
105
111
  */
106
- function generateSkillStub(skillName, skillPath, intent, outcome) {
107
- return `# FRAIM Skill: ${skillName}
112
+ function generateSkillStub(skillName, skillPath, skillInput, skillOutput) {
113
+ return `${STUB_MARKER}
114
+ # FRAIM Skill: ${skillName}
108
115
 
109
- ## Intent
110
- ${intent}
116
+ ## Skill Input
117
+ ${skillInput}
111
118
 
112
- ## Outcome
113
- ${outcome}
119
+ ## Skill Output
120
+ ${skillOutput}
114
121
 
115
122
  ---
116
123
 
117
124
  > [!IMPORTANT]
118
- > **For AI Agents:** This is a skill stub for discoverability.
125
+ > **For AI Agents:** This is a discoverability stub for the skill.
126
+ > All execution details must be fetched from MCP before use.
119
127
  > To retrieve the complete skill instructions, call:
120
128
  > \`get_fraim_file({ path: "skills/${skillPath}" })\`
121
129
  `;
@@ -123,52 +131,51 @@ ${outcome}
123
131
  /**
124
132
  * Generates a lightweight markdown stub for a rule.
125
133
  */
126
- function generateRuleStub(ruleName, rulePath, intent, outcome) {
127
- return `# FRAIM Rule: ${ruleName}
134
+ function generateRuleStub(ruleName, rulePath, intent) {
135
+ return `${STUB_MARKER}
136
+ # FRAIM Rule: ${ruleName}
128
137
 
129
138
  ## Intent
130
139
  ${intent}
131
140
 
132
- ## Outcome
133
- ${outcome}
134
-
135
141
  ---
136
142
 
137
143
  > [!IMPORTANT]
138
- > **For AI Agents:** This is a rule stub for discoverability.
144
+ > **For AI Agents:** This is a discoverability stub for the rule.
145
+ > All rule details must be fetched from MCP before use.
139
146
  > To retrieve the complete rule instructions, call:
140
147
  > \`get_fraim_file({ path: "rules/${rulePath}" })\`
141
148
  `;
142
149
  }
143
150
  /**
144
- * Parses a job file from the registry to extract its intent and outcome for the stub.
151
+ * Parses a job file from the registry to extract its intent, outcome, and steps for the stub.
145
152
  */
146
153
  function parseRegistryJob(content) {
147
154
  const intentMatch = content.match(/##\s*intent\s+([\s\S]*?)(?=\n##|$)/i);
148
155
  const outcomeMatch = content.match(/##\s*outcome\s+([\s\S]*?)(?=\n##|$)/i);
156
+ const stepsMatch = content.match(/##\s*steps\s+([\s\S]*?)(?=\n##|$)/i);
149
157
  const intent = intentMatch ? intentMatch[1].trim() : 'No intent defined.';
150
158
  const outcome = outcomeMatch ? outcomeMatch[1].trim() : 'No outcome defined.';
151
- return { intent, outcome };
159
+ const steps = stepsMatch ? stepsMatch[1].trim() : 'Use get_fraim_job to retrieve the full phase-by-phase execution steps.';
160
+ return { intent, outcome, steps };
152
161
  }
153
162
  /**
154
163
  * Parses a skill file from the registry to extract intent and expected outcome for stubs.
155
164
  */
156
165
  function parseRegistrySkill(content) {
157
- const intent = extractSection(content, ['intent', 'skill intent']) ||
166
+ const skillInput = extractSection(content, ['skill input', 'input', 'intent', 'skill intent']) ||
158
167
  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 };
168
+ 'Use this skill only when the task matches the skill trigger, context, and required inputs.';
169
+ const skillOutput = extractSection(content, ['skill output', 'output', 'outcome', 'expected behavior']) ||
170
+ 'Produce the concrete deliverable or decision this skill is designed to return.';
171
+ return { skillInput, skillOutput };
163
172
  }
164
173
  /**
165
- * Parses a rule file from the registry to extract intent and expected behavior for stubs.
174
+ * Parses a rule file from the registry to extract intent for stubs.
166
175
  */
167
176
  function parseRegistryRule(content) {
168
177
  const intent = extractSection(content, ['intent']) ||
169
178
  extractLeadParagraph(content) ||
170
179
  '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 };
180
+ return { intent };
174
181
  }