fraim 2.0.359-next.0 → 2.0.359-next.2

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.
@@ -90,7 +90,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
90
90
  personaKey: 'gautam',
91
91
  bundleId: 'persona-gautam-core',
92
92
  catalogMetadata: buildCatalogMetadata('gautam', ['analyze-revenue-system', 'build-gtm-motion', 'domain-registration-research']),
93
- protectedJobs: ['marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'domain-registration-research', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck', 'notebooklm-content-generation'],
93
+ protectedJobs: ['marketing-strategy-definition', 'product-launch-management', 'evangelist-content-development', 'funnel-analysis', 'growth-loop-design', 'analyze-revenue-system', 'design-gtm-system', 'build-gtm-motion', 'build-gtm-stack', 'plan-gtm-team', 'marketing-content-creation', 'ppc-campaign-management', 'paid-social-strategy', 'ad-performance-analysis', 'tracking-and-attribution-setup', 'developer-advocacy', 'seo-strategy', 'aeo-strategy', 'marketing-analytics-review', 'linkedin-company-page-setup', 'x-account-setup', 'domain-registration-research', 'social-engagement-campaign', 'thought-leadership-engagement', 'promo-video-creation', 'linkedin-carousel-from-deck', 'notebooklm-content-generation'],
94
94
  protectedAliases: ['gtm', 'marketing'],
95
95
  defaultHireMode: 'job',
96
96
  lockCopy: 'Hire GauTaM to unlock go-to-market and paid media work for this request.'
@@ -278,6 +278,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
278
278
  catalogMetadata: buildCatalogMetadata('aida', ['create-ai-agent', 'author-ai-evals', 'enable-web-mcp']),
279
279
  protectedJobs: [
280
280
  'create-ai-agent',
281
+ 'aeo-optimization',
281
282
  'author-ai-evals',
282
283
  'enable-web-mcp',
283
284
  'evaluate-ai-agent',
@@ -287,7 +288,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
287
288
  ],
288
289
  protectedAliases: ['ai-engineering', 'agent-engineering', 'ai-agents'],
289
290
  defaultHireMode: 'job',
290
- lockCopy: 'Hire AIDa to unlock AI agent design, MCP enablement, eval authoring, and agent evaluation work for this request.'
291
+ lockCopy: 'Hire AIDa to unlock AI agent design, MCP enablement, evals, and answer-engine optimization for this request.'
291
292
  }
292
293
  };
293
294
  const PROTECTED_JOB_TO_PERSONA = new Map();
@@ -24,7 +24,7 @@ exports.PERSONA_HIRE_CATALOG = {
24
24
  role: 'AI Developer',
25
25
  emoji: '\u{1F9E0}',
26
26
  gradient: 'linear-gradient(135deg, #4f46e5 0%, #06b6d4 50%, #10b981 100%)',
27
- blurb: 'Designs production AI agents, connects them to tools and data, and proves their behavior with evals before deployment.',
27
+ blurb: 'Builds and evaluates production AI agents, connects tools and data, and implements tested answer-engine optimization for websites.',
28
28
  jobPriceCents: 1290, // placeholder — product owner approval required before launch
29
29
  fulltimePriceCents: 5990, // placeholder — product owner approval required before launch
30
30
  },
@@ -96,7 +96,7 @@ exports.PERSONA_HIRE_CATALOG = {
96
96
  role: 'GTM & Marketing Manager',
97
97
  emoji: '📣',
98
98
  gradient: 'linear-gradient(135deg, #f97316 0%, #f59e0b 100%)',
99
- blurb: 'Defines marketing strategy, ships content, runs launches, and owns the brand voice in market.',
99
+ blurb: 'Defines marketing and answer-engine visibility strategy, ships content, runs launches, and owns the brand voice in market.',
100
100
  jobPriceCents: 890,
101
101
  fulltimePriceCents: 4990,
102
102
  },
@@ -115,7 +115,7 @@ class InheritanceParser {
115
115
  // Fetch parent content
116
116
  let parentContent;
117
117
  try {
118
- parentContent = await options.fetchParent(sanitizedExtends);
118
+ parentContent = await options.fetchParent(sanitizedExtends, currentPath);
119
119
  }
120
120
  catch (error) {
121
121
  throw new InheritanceError(`Failed to fetch extended parent content: ${sanitizedExtends}. ${error.message}`, sanitizedExtends);
@@ -151,7 +151,7 @@ class InheritanceParser {
151
151
  // Fetch parent content
152
152
  let parentContent;
153
153
  try {
154
- parentContent = await options.fetchParent(sanitized);
154
+ parentContent = await options.fetchParent(sanitized, currentPath);
155
155
  }
156
156
  catch (error) {
157
157
  throw new InheritanceError(`Failed to fetch parent content: ${sanitized}. ${error.message}`, sanitized);
@@ -249,11 +249,21 @@ class InheritanceParser {
249
249
  */
250
250
  parse(content) {
251
251
  const imports = this.extractImports(content);
252
- const hasExtends = /^---\r?\n[\s\S]*?"extends":\s*"[^"]+"[\s\S]*?\r?\n---/m.test(content);
252
+ const metadataMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
253
+ if (metadataMatch) {
254
+ try {
255
+ const parent = JSON.parse(metadataMatch[1]).extends;
256
+ if (typeof parent === 'string' && parent && !imports.includes(parent))
257
+ imports.unshift(parent);
258
+ }
259
+ catch {
260
+ // Invalid/non-JSON frontmatter has no executable extends directive.
261
+ }
262
+ }
253
263
  return {
254
264
  content,
255
265
  imports,
256
- hasImports: imports.length > 0 || hasExtends
266
+ hasImports: imports.length > 0
257
267
  };
258
268
  }
259
269
  normalizeImportRef(path) {
@@ -102,7 +102,7 @@ class LocalRegistryResolver {
102
102
  * Returns '{type}/{category}/{name}.md' or '{type}/{name}.md'.
103
103
  */
104
104
  async findRegistryPath(type, name) {
105
- const baseName = `${name}.md`;
105
+ const baseName = `${name.split(/[\\/]/).pop()}.md`;
106
106
  const searchDirs = [type];
107
107
  for (const dir of searchDirs) {
108
108
  // Check literal path first
@@ -126,7 +126,9 @@ class LocalRegistryResolver {
126
126
  ...(type !== 'skills' ? [this.getSyncedCatalogRoot('ai-manager', dir)] : []),
127
127
  ];
128
128
  for (const root of searchRoots) {
129
- const found = this.searchFileRecursively(root, baseName);
129
+ const namedPath = (0, path_1.join)(root, `${name}.md`);
130
+ const contained = (0, path_1.resolve)(namedPath).startsWith((0, path_1.resolve)(root) + path_1.sep);
131
+ const found = contained && (0, fs_1.existsSync)(namedPath) ? namedPath : this.searchFileRecursively(root, baseName);
130
132
  if (!found)
131
133
  continue;
132
134
  const syncedRole = ['ai-employee', 'ai-manager'].find(role => root === this.getSyncedCatalogRoot(role, dir));
@@ -415,7 +417,7 @@ class LocalRegistryResolver {
415
417
  /**
416
418
  * Resolve inheritance in local override content
417
419
  */
418
- async resolveInheritance(content, currentPath) {
420
+ async resolveInheritance(content, currentPath, personalized = true) {
419
421
  // Check if content has imports
420
422
  const parseResult = this.parser.parse(content);
421
423
  if (!parseResult.hasImports) {
@@ -423,13 +425,14 @@ class LocalRegistryResolver {
423
425
  }
424
426
  // Resolve inheritance
425
427
  try {
428
+ const personalizedPaths = new Set(personalized ? [this.normalizeRegistryPath(currentPath)] : []);
426
429
  const resolved = await this.parser.resolve(content, currentPath, {
427
- fetchParent: async (path) => {
430
+ fetchParent: async (path, requestingPath = currentPath) => {
428
431
  const normalized = this.normalizeRegistryPath(path);
429
- const normalizedCurrent = this.normalizeRegistryPath(currentPath);
432
+ const normalizedCurrent = this.normalizeRegistryPath(requestingPath);
430
433
  // If the import points back to the current file, skip local resolution and fetch
431
434
  // from the next layer (remote/global) instead.
432
- if (this.areEquivalentRegistryPaths(normalized, normalizedCurrent)) {
435
+ if (personalizedPaths.has(normalizedCurrent) && this.areEquivalentRegistryPaths(normalized, normalizedCurrent)) {
433
436
  return this.fetchRemoteWithFallback(normalizedCurrent, normalized);
434
437
  }
435
438
  // Otherwise, resolve through the normal local-first path.
@@ -449,17 +452,21 @@ class LocalRegistryResolver {
449
452
  }
450
453
  }
451
454
  }
452
- const res = await this.resolveFile(targetPath, {
455
+ // Fetch the selected layer's raw parent. The parser owns recursion,
456
+ // including its visited set and depth limit; restarting resolveFile
457
+ // here loses both guards for multi-file inheritance cycles.
458
+ const res = await this.resolveFileInternal(targetPath, {
453
459
  includeMetadata: false,
454
460
  stripMcpHeader: true
455
- });
461
+ }, false);
462
+ if (res.personalized)
463
+ personalizedPaths.add(normalized);
456
464
  return res.content;
457
465
  }
458
466
  catch (error) {
459
467
  throw error;
460
468
  }
461
- },
462
- maxDepth: 5
469
+ }
463
470
  });
464
471
  return {
465
472
  content: resolved,
@@ -497,8 +504,25 @@ class LocalRegistryResolver {
497
504
  * @returns Resolved file with metadata
498
505
  */
499
506
  async resolveFile(path, options = {}) {
507
+ return this.resolveFileInternal(path, options, true);
508
+ }
509
+ async resolveFileInternal(path, options, applyInheritance) {
500
510
  const includeMetadata = options.includeMetadata ?? true;
501
511
  const stripMcpHeader = options.stripMcpHeader ?? false;
512
+ const resolveContent = (content, personalized = true) => applyInheritance
513
+ ? this.resolveInheritance(content, path, personalized)
514
+ : Promise.resolve({ content, imports: [] });
515
+ const baselineResult = async (rawContent, source) => {
516
+ const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
517
+ const resolved = await resolveContent(content, false);
518
+ return {
519
+ content: resolved.content,
520
+ source,
521
+ personalized: false,
522
+ inherited: resolved.imports.length > 0,
523
+ imports: resolved.imports.length > 0 ? resolved.imports : undefined
524
+ };
525
+ };
502
526
  // Check for local override
503
527
  if (!this.hasLocalOverride(path)) {
504
528
  // Issue #1002: the manager's writable home (precedence 2 — above the
@@ -509,7 +533,7 @@ class LocalRegistryResolver {
509
533
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
510
534
  let resolvedManagerLocal;
511
535
  try {
512
- resolvedManagerLocal = await this.resolveInheritance(managerLocalContent, path);
536
+ resolvedManagerLocal = await resolveContent(managerLocalContent);
513
537
  }
514
538
  catch {
515
539
  resolvedManagerLocal = { content: managerLocalContent, imports: [] };
@@ -529,7 +553,7 @@ class LocalRegistryResolver {
529
553
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
530
554
  let resolvedManagerOverlay;
531
555
  try {
532
- resolvedManagerOverlay = await this.resolveInheritance(managerOverlayContent, path);
556
+ resolvedManagerOverlay = await resolveContent(managerOverlayContent);
533
557
  }
534
558
  catch {
535
559
  resolvedManagerOverlay = { content: managerOverlayContent, imports: [] };
@@ -549,7 +573,7 @@ class LocalRegistryResolver {
549
573
  // Issue #1867: apply inheritance so an extends-based override merges with its parent.
550
574
  let resolvedOrgOverlay;
551
575
  try {
552
- resolvedOrgOverlay = await this.resolveInheritance(orgOverlayContent, path);
576
+ resolvedOrgOverlay = await resolveContent(orgOverlayContent);
553
577
  }
554
578
  catch {
555
579
  resolvedOrgOverlay = { content: orgOverlayContent, imports: [] };
@@ -566,32 +590,16 @@ class LocalRegistryResolver {
566
590
  const syncedLocalContent = this.readSyncedLocalFile(path);
567
591
  if (syncedLocalContent !== null) {
568
592
  // Synced machine baseline, not personalized.
569
- return {
570
- content: syncedLocalContent,
571
- source: 'local',
572
- personalized: false,
573
- inherited: false
574
- };
593
+ return baselineResult(syncedLocalContent, 'local');
575
594
  }
576
595
  const workspaceRegistryContent = this.readWorkspaceRegistryFile(path);
577
596
  if (workspaceRegistryContent !== null) {
578
- return {
579
- content: workspaceRegistryContent,
580
- source: 'local',
581
- personalized: false,
582
- inherited: false
583
- };
597
+ return baselineResult(workspaceRegistryContent, 'local');
584
598
  }
585
599
  // No useful override or synced content, fetch from remote
586
600
  try {
587
601
  const rawContent = await this.fetchRemoteWithFallback(path);
588
- const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
589
- return {
590
- content,
591
- source: 'remote',
592
- personalized: false,
593
- inherited: false
594
- };
602
+ return await baselineResult(rawContent, 'remote');
595
603
  }
596
604
  catch (error) {
597
605
  throw new Error(`Failed to fetch from remote: ${path}. ${error.message}`);
@@ -610,28 +618,16 @@ class LocalRegistryResolver {
610
618
  // If local read fails, fall back to remote
611
619
  console.warn(`Local override read failed, falling back to remote: ${path}`);
612
620
  const rawContent = await this.fetchRemoteWithFallback(path);
613
- const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
614
- return {
615
- content,
616
- source: 'remote',
617
- personalized: false,
618
- inherited: false
619
- };
621
+ return baselineResult(rawContent, 'remote');
620
622
  }
621
623
  // Resolve inheritance
622
624
  let resolved;
623
625
  try {
624
- resolved = await this.resolveInheritance(localContent, path);
626
+ resolved = await resolveContent(localContent);
625
627
  }
626
628
  catch (error) {
627
629
  const rawContent = await this.fetchRemoteWithFallback(path);
628
- const content = stripMcpHeader ? this.stripMcpHeader(rawContent) : rawContent;
629
- return {
630
- content,
631
- source: 'remote',
632
- personalized: false,
633
- inherited: false
634
- };
630
+ return baselineResult(rawContent, 'remote');
635
631
  }
636
632
  // Build result — a local override hit means this came from the
637
633
  // personalized-employee layer (the manager taught/customized it).
@@ -9,6 +9,7 @@ const path_1 = require("path");
9
9
  const crypto_1 = __importDefault(require("crypto"));
10
10
  const stub_generator_1 = require("../core/utils/stub-generator");
11
11
  const job_parser_1 = require("../core/utils/job-parser");
12
+ const inheritance_parser_1 = require("../core/utils/inheritance-parser");
12
13
  const pack_home_1 = require("../cli/utils/pack-home");
13
14
  class RegistryService {
14
15
  constructor() {
@@ -600,18 +601,38 @@ class RegistryService {
600
601
  if (!metadata)
601
602
  return null;
602
603
  try {
603
- return (0, fs_1.readFileSync)(metadata.fullPath, 'utf8');
604
+ return await this.resolveContent(metadata);
604
605
  }
605
- catch (e) {
606
+ catch (error) {
607
+ if (error instanceof inheritance_parser_1.InheritanceError)
608
+ throw error;
606
609
  return null;
607
610
  }
608
611
  }
612
+ /** Resolve shipped inheritance before parsing jobs or expanding includes. */
613
+ async resolveContent(metadata) {
614
+ const content = (0, fs_1.readFileSync)(metadata.fullPath, 'utf8');
615
+ const parser = new inheritance_parser_1.InheritanceParser();
616
+ if (!parser.parse(content).hasImports)
617
+ return content;
618
+ return parser.resolve(content, metadata.path, {
619
+ fetchParent: async (parentPath) => {
620
+ const parent = this.findFileByPath(parentPath);
621
+ if (!parent)
622
+ throw new Error(`Inherited registry file not found: ${parentPath}`);
623
+ // Leave recursion to InheritanceParser so cycles and depth are
624
+ // checked across the entire chain, rather than per fetch.
625
+ return (0, fs_1.readFileSync)(parent.fullPath, 'utf8');
626
+ }
627
+ });
628
+ }
609
629
  async getJob(name) {
610
630
  const allMatches = Array.from(this.fileIndex.values()).filter(f => f.name === name && f.type === 'job');
611
631
  if (allMatches.length === 0)
612
632
  return null;
613
633
  const metadata = allMatches.find(f => !f.isStub) || allMatches[0];
614
- const def = job_parser_1.JobParser.parse(metadata.fullPath);
634
+ const content = await this.resolveContent(metadata);
635
+ const def = job_parser_1.JobParser.parseContent(content, name, metadata.fullPath);
615
636
  if (def && !def.metadata.type) {
616
637
  def.metadata.type = 'job';
617
638
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.359-next.0",
3
+ "version": "2.0.359-next.2",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {