fraim 2.0.337 → 2.0.338

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.
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ // Read the shared author's current entry format for identity and update guards.
4
+ function learningEntryBlocks(body) {
5
+ const headings = [...body.matchAll(/^#{2,6}\s+\[P-[A-Z]+\].*$/gm)];
6
+ return headings.map((heading, index) => {
7
+ const start = heading.index;
8
+ const next = headings[index + 1]?.index ?? body.length;
9
+ const ownDepth = heading[0].match(/^#+/)[0].length;
10
+ const afterHeading = start + heading[0].length;
11
+ const shallower = [...body.slice(afterHeading, next).matchAll(/^(#{1,6})\s+.*$/gm)].find(m => m[1].length <= ownDepth);
12
+ const limit = shallower ? afterHeading + shallower.index : next;
13
+ const text = body.slice(start, limit).trimEnd();
14
+ return { start, end: start + text.length, text, id: text.match(/^\*\*Id\*\*:\s*(L-[a-f0-9]{10})\s*$/m)?.[1] ?? null };
15
+ });
16
+ }
17
+
18
+ function learningEntryMetadata(content) {
19
+ const metadata = {};
20
+ for (const match of content.matchAll(/^\*\*(Recurrences|First recorded|First synthesized)\*\*:\s*([^\r\n]+)$/gm)) {
21
+ if (metadata[match[1]] !== undefined && metadata[match[1]] !== match[2]) throw new Error('entry-metadata-conflict');
22
+ metadata[match[1]] = match[2];
23
+ }
24
+ return metadata;
25
+ }
26
+
27
+ module.exports = { learningEntryMetadata, learningEntryBlocks };
@@ -0,0 +1,78 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "mapVersion": 1,
4
+ "domains": [
5
+ "engineering",
6
+ "product",
7
+ "go-to-market",
8
+ "people-ops",
9
+ "finance",
10
+ "legal",
11
+ "compliance",
12
+ "customer",
13
+ "company-building",
14
+ "operations"
15
+ ],
16
+ "jobDomainMap": {
17
+ "product-building": "engineering",
18
+ "dev-ops": "engineering",
19
+ "quality-assurance": "engineering",
20
+ "migration": "engineering",
21
+ "replicate": "engineering",
22
+ "reviewer": "engineering",
23
+ "security": "engineering",
24
+ "delivery-ops": "engineering",
25
+ "salesforce": "engineering",
26
+ "ai-engineering": "engineering",
27
+ "product-management": "product",
28
+ "customer-development": "product",
29
+ "brainstorming": "product",
30
+ "marketing": "go-to-market",
31
+ "branding": "go-to-market",
32
+ "sales": "go-to-market",
33
+ "go-to-market": "go-to-market",
34
+ "business-development": "go-to-market",
35
+ "commercial-ops": "go-to-market",
36
+ "people-ops": "people-ops",
37
+ "inclusion-ops": "people-ops",
38
+ "career": "people-ops",
39
+ "finance": "finance",
40
+ "invoicing": "finance",
41
+ "banking": "finance",
42
+ "legal": "legal",
43
+ "compliance": "compliance",
44
+ "customer-success": "customer",
45
+ "customer-communication": "customer",
46
+ "company-creation": "company-building",
47
+ "bootstrap": "company-building",
48
+ "fundraising": "company-building",
49
+ "business-ops": "operations",
50
+ "procurement": "operations",
51
+ "project-management": "operations"
52
+ },
53
+ "globalJobCategories": [
54
+ "manager",
55
+ "fraim",
56
+ "1-1",
57
+ "coaching",
58
+ "delegation",
59
+ "hiring",
60
+ "productivity",
61
+ "project-setup",
62
+ "setup-guardrails",
63
+ "team-learning",
64
+ "verification"
65
+ ],
66
+ "domainVoice": {
67
+ "engineering": "Precise and technical; file paths, commands, and test names are appropriate.",
68
+ "product": "Product and user-outcome language; behavior and flow, not code symbols.",
69
+ "go-to-market": "Marketing and sales language; campaigns, funnels, messaging, audience.",
70
+ "people-ops": "Plain HR and recruiting language; no engineering jargon.",
71
+ "finance": "Finance and accounting language; figures, close, forecasts.",
72
+ "legal": "Contract and IP language; obligations, clauses, risk.",
73
+ "compliance": "Controls, evidence, and regulation language.",
74
+ "customer": "Customer-relationship and support language.",
75
+ "company-building": "Founder and operations language; setup, entity, funding.",
76
+ "operations": "Business-operations language; process, vendors, planning, coordination."
77
+ }
78
+ }
@@ -113,12 +113,14 @@ function defaultCloneDir(layer) {
113
113
  * Resolve where this layer's details live. Never throws and never touches the
114
114
  * network; call `ensurePackClone` separately when the git working copy has to
115
115
  * be present and current.
116
+ * Read-only inventory callers set `migrateLegacy: false` to suppress the
117
+ * otherwise backwards-compatible flat org layout migration.
116
118
  */
117
- function resolvePackHome(layer) {
119
+ function resolvePackHome(layer, options = {}) {
118
120
  const config = (0, user_config_1.getLayerBackend)(layer);
119
121
  const standard = standardHome(layer);
120
122
  const make = (root, extra) => {
121
- if (layer === 'org')
123
+ if (layer === 'org' && options.migrateLegacy !== false)
122
124
  migrateOrgFlatContent(root);
123
125
  return { layer, root, contentRoot: path_1.default.join(root, 'personalized-employee'), ...extra };
124
126
  };
@@ -12,130 +12,27 @@
12
12
  * the current job's-domain learnings only, so a marketing job no longer loads
13
13
  * engineering learnings.
14
14
  *
15
- * This module is the single source of truth for the domain list, the
16
- * registry-category -> domain map, the per-domain authoring voice (R3), and the
17
- * resolver used at the injection sites. It is pure (no I/O) and mirrors the
15
+ * The shared registry/scripts/learning/learning-domains.json owns the domain list, the
16
+ * registry-category -> domain map, and the per-domain authoring voice (R3).
17
+ * This module provides the pure resolver used at the injection sites, following the
18
18
  * config-module convention of `src/config/portfolio-slug-overrides.ts`.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.DOMAIN_VOICE = exports.GLOBAL_JOB_CATEGORIES = exports.JOB_DOMAIN_MAP = exports.LEARNING_DOMAINS = void 0;
21
+ exports.DOMAIN_VOICE = exports.GLOBAL_JOB_CATEGORIES = exports.JOB_DOMAIN_MAP = exports.LEARNING_DOMAINS = exports.LEARNING_DOMAIN_MAP_VERSION = void 0;
22
22
  exports.isLearningDomain = isLearningDomain;
23
23
  exports.jobCategoryFromRegistryPath = jobCategoryFromRegistryPath;
24
24
  exports.resolveJobDomain = resolveJobDomain;
25
25
  exports.domainFromJobFrontmatter = domainFromJobFrontmatter;
26
26
  exports.resolveDomainForJob = resolveDomainForJob;
27
- /** The coarse learning domains. `global` is represented by the absence of a domain. */
28
- exports.LEARNING_DOMAINS = [
29
- 'engineering',
30
- 'product',
31
- 'go-to-market',
32
- 'people-ops',
33
- 'finance',
34
- 'legal',
35
- 'compliance',
36
- 'customer',
37
- 'company-building',
38
- 'operations',
39
- ];
27
+ const learningDomainData = require('../../registry/scripts/learning/learning-domains.json');
28
+ exports.LEARNING_DOMAIN_MAP_VERSION = learningDomainData.mapVersion;
29
+ exports.LEARNING_DOMAINS = Object.freeze(learningDomainData.domains);
30
+ exports.JOB_DOMAIN_MAP = Object.freeze(learningDomainData.jobDomainMap);
31
+ exports.GLOBAL_JOB_CATEGORIES = new Set(learningDomainData.globalJobCategories);
32
+ exports.DOMAIN_VOICE = Object.freeze(learningDomainData.domainVoice);
40
33
  function isLearningDomain(value) {
41
34
  return typeof value === 'string' && exports.LEARNING_DOMAINS.includes(value);
42
35
  }
43
- /**
44
- * Registry job category -> learning domain. Seeded from the registry taxonomy and
45
- * the `PERSONA_CAPABILITY_BUNDLES` groupings. Every domain-scoped category maps
46
- * here; cross-cutting categories are listed in `GLOBAL_JOB_CATEGORIES` instead.
47
- *
48
- * A build-time test (`tests/test-unit-learning-domains.ts`) asserts that every
49
- * registry job category (from `registry/jobs/**`) is covered by exactly one of
50
- * this map or `GLOBAL_JOB_CATEGORIES`, so the taxonomy cannot silently drift as
51
- * new job categories are added.
52
- */
53
- exports.JOB_DOMAIN_MAP = {
54
- // engineering
55
- 'product-building': 'engineering',
56
- 'dev-ops': 'engineering',
57
- 'quality-assurance': 'engineering',
58
- 'migration': 'engineering',
59
- 'replicate': 'engineering',
60
- 'reviewer': 'engineering',
61
- 'security': 'engineering',
62
- 'delivery-ops': 'engineering',
63
- 'salesforce': 'engineering',
64
- 'ai-engineering': 'engineering',
65
- // product
66
- 'product-management': 'product',
67
- 'customer-development': 'product',
68
- 'brainstorming': 'product',
69
- // go-to-market
70
- 'marketing': 'go-to-market',
71
- 'branding': 'go-to-market',
72
- 'sales': 'go-to-market',
73
- 'go-to-market': 'go-to-market',
74
- 'business-development': 'go-to-market',
75
- 'commercial-ops': 'go-to-market',
76
- // people-ops
77
- 'people-ops': 'people-ops',
78
- 'inclusion-ops': 'people-ops',
79
- 'career': 'people-ops',
80
- // finance
81
- 'finance': 'finance',
82
- 'invoicing': 'finance',
83
- 'banking': 'finance',
84
- // legal
85
- 'legal': 'legal',
86
- // compliance
87
- 'compliance': 'compliance',
88
- // customer
89
- 'customer-success': 'customer',
90
- 'customer-communication': 'customer',
91
- // company-building
92
- 'company-creation': 'company-building',
93
- 'bootstrap': 'company-building',
94
- 'fundraising': 'company-building',
95
- // operations
96
- 'business-ops': 'operations',
97
- 'procurement': 'operations',
98
- 'project-management': 'operations',
99
- };
100
- /**
101
- * Cross-cutting job categories that intentionally resolve to `global` (no domain).
102
- * Management and framework-meta learnings apply regardless of the work domain, so
103
- * they are never hidden from any job. Listed explicitly so the completeness test
104
- * can tell an intentional global category apart from a forgotten mapping.
105
- */
106
- exports.GLOBAL_JOB_CATEGORIES = new Set([
107
- // ai-employee cross-cutting
108
- 'manager',
109
- 'fraim',
110
- // ai-manager (all management categories)
111
- '1-1',
112
- 'coaching',
113
- 'delegation',
114
- 'hiring',
115
- 'productivity',
116
- 'project-setup',
117
- 'setup-guardrails',
118
- 'team-learning',
119
- 'verification',
120
- ]);
121
- /**
122
- * One-line authoring voice per domain (R3). Consumed as documentation and by the
123
- * synthesis jobs/skills (`sleep-on-learnings`, `organizational-learning-synthesis`,
124
- * `l1-signal-classification`) so a domain-specific learning is written in that
125
- * domain's language and stays human-auditable.
126
- */
127
- exports.DOMAIN_VOICE = {
128
- 'engineering': 'Precise and technical; file paths, commands, and test names are appropriate.',
129
- 'product': 'Product and user-outcome language; behavior and flow, not code symbols.',
130
- 'go-to-market': 'Marketing and sales language; campaigns, funnels, messaging, audience.',
131
- 'people-ops': 'Plain HR and recruiting language; no engineering jargon.',
132
- 'finance': 'Finance and accounting language; figures, close, forecasts.',
133
- 'legal': 'Contract and IP language; obligations, clauses, risk.',
134
- 'compliance': 'Controls, evidence, and regulation language.',
135
- 'customer': 'Customer-relationship and support language.',
136
- 'company-building': 'Founder and operations language; setup, entity, funding.',
137
- 'operations': 'Business-operations language; process, vendors, planning, coordination.',
138
- };
139
36
  /**
140
37
  * Extract the registry job category from a job's registry path.
141
38
  *
@@ -102,11 +102,17 @@ class LocalRegistryResolver {
102
102
  (0, path_1.join)(this.managerCacheRoot, dir),
103
103
  (0, path_1.join)(this.orgCacheRoot, dir),
104
104
  (0, path_1.join)(this.workspaceRoot, 'registry', dir),
105
+ // Installed projects have categorized job stubs in the synced roles,
106
+ // without a source registry checkout. Keep authored overrides first.
107
+ ...(type === 'jobs' ? [this.getFraimPath('ai-employee', dir), this.getFraimPath('ai-manager', dir)] : []),
105
108
  ];
106
109
  for (const root of searchRoots) {
107
110
  const found = this.searchFileRecursively(root, baseName);
108
111
  if (!found)
109
112
  continue;
113
+ const syncedRole = ['ai-employee', 'ai-manager'].find(role => root === this.getFraimPath(role, dir));
114
+ if (syncedRole)
115
+ return `${dir}/${syncedRole}/${found.slice(root.length + 1).replace(/\\/g, '/')}`;
110
116
  // Convert absolute back to a registry-relative path.
111
117
  const rel = found.replace(/\\/g, '/');
112
118
  const dirMarker = `/${dir}/`;
@@ -1612,17 +1612,8 @@ class FraimLocalMCPServer {
1612
1612
  // 4. After get_fraim_job succeeds, inject learning context with job-focus frame.
1613
1613
  if (toolName === 'get_fraim_job' && !finalizedResponse.error) {
1614
1614
  const text = finalizedResponse.result?.content?.[0]?.text;
1615
- const userEmail = this.ensureEngine().getUserEmail();
1616
- if (typeof text === 'string' && userEmail) {
1617
- const workspaceRoot = this.findProjectRoot() || process.cwd();
1618
- const jobDomain = await this.resolveJobDomainForJob(args.job, requestSessionId);
1619
- const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true, jobDomain);
1620
- const teamContextSection = (0, learning_context_builder_js_1.buildTeamContextSection)(workspaceRoot, true);
1621
- if (learningSection || teamContextSection) {
1622
- finalizedResponse.result.content[0].text = text + `\n\n---` + learningSection + teamContextSection;
1623
- this.log(`[req:${requestId}] Injected job-focus learning/team context for ${userEmail} (domain: ${jobDomain ?? 'global'})`);
1624
- }
1625
- this.recordUsageOffers(workspaceRoot, userEmail, true, jobDomain, String(args.job ?? 'unknown'), requestId);
1615
+ if (typeof text === 'string') {
1616
+ finalizedResponse.result.content[0].text = await this.appendJobContext(text, args.job, requestSessionId, requestId);
1626
1617
  }
1627
1618
  // Issue #1345: forward executionMode from get_fraim_job response to Hub so
1628
1619
  // the chip reads from run data (no local file read, no separate endpoint needed).
@@ -1640,6 +1631,22 @@ class FraimLocalMCPServer {
1640
1631
  }
1641
1632
  return this.processResponseWithHydration(finalizedResponse, requestSessionId);
1642
1633
  }
1634
+ /** Keep context delivery and offer attribution together for local and remote jobs. */
1635
+ async appendJobContext(text, jobName, requestSessionId, requestId) {
1636
+ const userEmail = this.ensureEngine().getUserEmail();
1637
+ if (!userEmail)
1638
+ return text;
1639
+ const workspaceRoot = this.findProjectRoot() || process.cwd();
1640
+ const jobDomain = await this.resolveJobDomainForJob(jobName, requestSessionId);
1641
+ const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true, jobDomain);
1642
+ const teamContextSection = (0, learning_context_builder_js_1.buildTeamContextSection)(workspaceRoot, true);
1643
+ if (learningSection || teamContextSection) {
1644
+ text += `\n\n---` + learningSection + teamContextSection;
1645
+ this.log(`[req:${requestId}] Injected job-focus learning/team context for ${userEmail} (domain: ${jobDomain ?? 'global'})`);
1646
+ }
1647
+ this.recordUsageOffers(workspaceRoot, userEmail, true, jobDomain, String(jobName ?? 'unknown'), requestId);
1648
+ return text;
1649
+ }
1643
1650
  /**
1644
1651
  * Issue #1103 R1, R2 and R3: record what this context injection delivered.
1645
1652
  *
@@ -2627,18 +2634,7 @@ class FraimLocalMCPServer {
2627
2634
  responseText = `${mentor.getCompactPhaseAuthority()}\n\n${responseText}`;
2628
2635
  responseText += `\n\n---\n\n**Job ID:** \`${jobId}\`\n\n**This job has phases.** Use \`seekMentoring\` with the jobId above to get phase-specific instructions.`;
2629
2636
  }
2630
- // Inject local learning context for job requests (RFC 177).
2631
- const userEmail = this.ensureEngine().getUserEmail();
2632
- if (userEmail) {
2633
- const workspaceRoot = this.findProjectRoot() || process.cwd();
2634
- const jobDomain = await this.resolveJobDomainForJob(name, requestSessionId);
2635
- const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true, jobDomain);
2636
- const teamContextSection = (0, learning_context_builder_js_1.buildTeamContextSection)(workspaceRoot, true);
2637
- if (learningSection || teamContextSection) {
2638
- responseText += `\n\n---` + learningSection + teamContextSection;
2639
- this.log(`✅ Injected job-focus learning/team context for ${userEmail} (local override path, domain: ${jobDomain ?? 'global'})`);
2640
- }
2641
- }
2637
+ responseText = await this.appendJobContext(responseText, name, requestSessionId, requestId);
2642
2638
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, responseText);
2643
2639
  }
2644
2640
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.337",
3
+ "version": "2.0.338",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "fraim": "bin/fraim.js"
8
8
  },
9
9
  "files": [
10
+ "dist/registry/scripts/learning/learning-domains.json",
11
+ "dist/registry/scripts/learning/entry-format.js",
10
12
  "dist/src/local-mcp-server/",
11
13
  "dist/src/cli/",
12
14
  "!dist/src/cli/fraim-hub.js",