fraim 2.0.360-next.0 → 2.0.360-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.
- package/dist/registry/domains.json +159 -0
- package/dist/registry/scripts/domains/catalog.js +112 -0
- package/dist/registry/scripts/learning/entry-authoring.js +63 -25
- package/dist/registry/scripts/learning/run-application.js +12 -2
- package/dist/registry/scripts/learning/run-publication.js +192 -16
- package/dist/registry/scripts/learning-usage/local-runtime.js +1 -3
- package/dist/src/cli/learning/runtime.js +2 -0
- package/dist/src/cli/utils/remote-sync.js +7 -0
- package/dist/src/cli/utils/script-sync-utils.js +4 -0
- package/dist/src/config/learning-domains.js +24 -108
- package/dist/src/core/ai-mentor.js +7 -0
- package/dist/src/core/utils/domain-rule-assembler.js +206 -0
- package/dist/src/core/utils/local-registry-resolver.js +135 -32
- package/dist/src/fraim/db-service.js +1 -1
- package/dist/src/local-mcp-server/agent-token-prices.js +25 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +94 -36
- package/dist/src/local-mcp-server/stdio-server.js +122 -39
- package/dist/src/mcp/tool-schemas.js +2 -2
- package/dist/src/middleware/logger.js +1 -45
- package/dist/src/services/mcp-service.js +35 -16
- package/dist/src/services/registry-service.js +4 -0
- package/package.json +3 -2
- package/dist/registry/scripts/learning/learning-domains.json +0 -80
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.TEAM_CONTEXT_RELATIVE_PATHS = exports.generateLearningEntryId = exports.learningEntryIdLineRegex = exports.learningEntryHeadingRegex = exports.LEARNING_ENTRY_ID_REGEX = exports.LEARNING_PRIORITIES = exports.DEFAULT_THRESHOLD = void 0;
|
|
4
|
+
exports.storedLearningDomains = storedLearningDomains;
|
|
4
5
|
exports.getScoreThreshold = getScoreThreshold;
|
|
5
6
|
exports.computeEffectiveScore = computeEffectiveScore;
|
|
6
7
|
exports.getLearningInputHomes = getLearningInputHomes;
|
|
@@ -40,6 +41,13 @@ const brand_store_1 = require("../core/brand-store");
|
|
|
40
41
|
// cache, which silently returned nothing once that cache stopped existing.
|
|
41
42
|
const pack_home_1 = require("../cli/utils/pack-home");
|
|
42
43
|
const learning_domains_1 = require("../config/learning-domains");
|
|
44
|
+
const entry_authoring_1 = require("../../registry/scripts/learning/entry-authoring");
|
|
45
|
+
/** Catalog-backed allowlist for existing files, including retired storage identities. */
|
|
46
|
+
function storedLearningDomains(effectiveCatalog) {
|
|
47
|
+
const configured = effectiveCatalog?.schemaVersion === 2 && effectiveCatalog.domains && typeof effectiveCatalog.domains === 'object'
|
|
48
|
+
? Object.keys(effectiveCatalog.domains).filter(domain => /^[a-z][a-z-]*$/.test(domain) && !['global', 'constructor', 'prototype'].includes(domain)) : [];
|
|
49
|
+
return [...new Set([...learning_domains_1.STORED_LEARNING_DOMAINS, ...configured])];
|
|
50
|
+
}
|
|
43
51
|
const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
|
|
44
52
|
/**
|
|
45
53
|
* The score at or above which an entry is injected. Exported so a test asserts
|
|
@@ -535,7 +543,7 @@ const CONTEXT_CATEGORIES = {
|
|
|
535
543
|
validated: { ft: 'validated-patterns', label: '(all entries)' },
|
|
536
544
|
};
|
|
537
545
|
/** Resolve the files listed in injected context, in delivery order. */
|
|
538
|
-
function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain) {
|
|
546
|
+
function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain, effectiveCatalog) {
|
|
539
547
|
// Domain axis (issue #806) plus the connect/job single-injection contract (issue
|
|
540
548
|
// #860). Session startup stays broad enough for stable preferences plus L0, while
|
|
541
549
|
// job contexts add the remaining global categories and may append the current job's
|
|
@@ -569,11 +577,25 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
569
577
|
const collect = (cats, tiersFor, fallbackLevel) => {
|
|
570
578
|
const globalFiles = [];
|
|
571
579
|
const domainFiles = [];
|
|
572
|
-
const push = (into, key, tiers) => {
|
|
580
|
+
const push = (into, key, tiers, storageDomain = 'global', includeGlobal = true) => {
|
|
573
581
|
if (tiers.length > 1)
|
|
574
582
|
multiTier = true;
|
|
575
583
|
const meta = CONTEXT_CATEGORIES[key];
|
|
576
584
|
for (const tier of tiers) {
|
|
585
|
+
let selected;
|
|
586
|
+
try {
|
|
587
|
+
const content = (0, fs_1.readFileSync)(tier.path, 'utf8');
|
|
588
|
+
const explicitProvenance = /^\*\*(?:Domain|Originating job)\*\*:/m.test(content);
|
|
589
|
+
if (storageDomain !== 'global' || explicitProvenance)
|
|
590
|
+
selected = (0, entry_authoring_1.selectStoredLearningEntries)(content, storageDomain, activeDomain ?? 'global', includeGlobal, effectiveCatalog);
|
|
591
|
+
else if (!includeGlobal)
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
selected = { entries: [], unresolved: 1 };
|
|
596
|
+
}
|
|
597
|
+
if (selected && !selected.entries.length && !selected.unresolved)
|
|
598
|
+
continue;
|
|
577
599
|
into.push({
|
|
578
600
|
path: tier.path,
|
|
579
601
|
displayPath: tier.displayPath,
|
|
@@ -581,6 +603,7 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
581
603
|
label: meta.label,
|
|
582
604
|
isCoach: key === 'coach',
|
|
583
605
|
level: tier.level ?? fallbackLevel,
|
|
606
|
+
...(selected ? { selectedEntries: selected.entries, unresolved: selected.unresolved } : {}),
|
|
584
607
|
});
|
|
585
608
|
if (key === 'coach')
|
|
586
609
|
coachPresent = true;
|
|
@@ -591,7 +614,11 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
591
614
|
}
|
|
592
615
|
if (activeDomain) {
|
|
593
616
|
for (const key of cats.domain) {
|
|
594
|
-
|
|
617
|
+
if (sessionCats.includes(key))
|
|
618
|
+
push(domainFiles, key, tiersFor(CONTEXT_CATEGORIES[key].ft), 'global', false);
|
|
619
|
+
for (const storageDomain of storedLearningDomains(effectiveCatalog)) {
|
|
620
|
+
push(domainFiles, key, tiersFor(CONTEXT_CATEGORIES[key].ft, storageDomain), storageDomain);
|
|
621
|
+
}
|
|
595
622
|
}
|
|
596
623
|
}
|
|
597
624
|
return [...globalFiles, ...domainFiles];
|
|
@@ -600,16 +627,28 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
600
627
|
const l1 = collect({ global: l1GlobalCats, domain: l1DomainCats }, (ft, dom) => resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, dom ? `${resolvedUserId}-${dom}-${ft}.md` : `${resolvedUserId}-${ft}.md`), 'manager');
|
|
601
628
|
return { l2, l1, multiTier, coachPresent };
|
|
602
629
|
}
|
|
603
|
-
function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
630
|
+
function buildLearningContextSection(workspaceRoot, userId, forJob, domain, effectiveCatalog) {
|
|
604
631
|
const roots = getLearningRoots(workspaceRoot);
|
|
605
632
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
606
|
-
const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain);
|
|
633
|
+
const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain, effectiveCatalog);
|
|
607
634
|
const multiTier = resolved.multiTier;
|
|
608
635
|
const toListed = (f) => ({
|
|
609
636
|
displayPath: f.displayPath,
|
|
610
637
|
label: f.label,
|
|
611
638
|
isCoach: f.isCoach,
|
|
639
|
+
selectedEntries: f.selectedEntries,
|
|
640
|
+
unresolved: f.unresolved,
|
|
612
641
|
});
|
|
642
|
+
const renderLearningFile = (f) => {
|
|
643
|
+
if (!f.selectedEntries)
|
|
644
|
+
return `\`${f.displayPath}\` ${f.label}\n`;
|
|
645
|
+
let result = '';
|
|
646
|
+
if (f.selectedEntries.length)
|
|
647
|
+
result += `Selected entries from \`${f.displayPath}\` ${f.isCoach ? '(manager-facing feedback)' : ''}:\n${f.selectedEntries.join('\n\n')}\n`;
|
|
648
|
+
if (f.unresolved)
|
|
649
|
+
result += `needs-domain: ${f.unresolved} historical entr${f.unresolved === 1 ? 'y' : 'ies'} in \`${f.displayPath}\` require original job provenance or an explicit manager domain decision. Retained for review; not standing guidance.\n`;
|
|
650
|
+
return result;
|
|
651
|
+
};
|
|
613
652
|
const l2Files = resolved.l2.map(toListed);
|
|
614
653
|
const l1Files = resolved.l1.map(toListed);
|
|
615
654
|
const pendingL0Sources = collectPendingL0SourceFiles(workspaceRoot, userId.trim(), roots);
|
|
@@ -628,13 +667,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
628
667
|
if (hasL2) {
|
|
629
668
|
section += '### L2 - Org patterns\n';
|
|
630
669
|
for (const f of l2Files)
|
|
631
|
-
section +=
|
|
670
|
+
section += renderLearningFile(f);
|
|
632
671
|
section += '\n';
|
|
633
672
|
}
|
|
634
673
|
if (hasL1) {
|
|
635
674
|
section += '### L1 - Your patterns\n';
|
|
636
675
|
for (const f of l1Files)
|
|
637
|
-
section +=
|
|
676
|
+
section += renderLearningFile(f);
|
|
638
677
|
section += '\n';
|
|
639
678
|
}
|
|
640
679
|
if (l0CoachingCount > 0 || l0RetroCount > 0) {
|
|
@@ -662,7 +701,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
662
701
|
const coachPresent = resolved.coachPresent;
|
|
663
702
|
if (forJob) {
|
|
664
703
|
if (hasL2 || hasL1) {
|
|
665
|
-
section += 'Read the listed
|
|
704
|
+
section += 'Read the listed global learning files before continuing and apply the selected domain entries shown above. Source paths for selected or needs-domain entries are provenance, not instructions to load the whole file. Do not apply omitted or unresolved entries.\n';
|
|
666
705
|
if (multiTier) {
|
|
667
706
|
section += 'A family listed at more than one level is combined, not replaced. Stable IDs identify lessons; equal titles do not establish identity. Conflicting copies require explicit reconciliation: do not choose a revision by scope, timestamp, count, or title. Preserve approved decisions and useful refinements; unresolved conflicting claims are not authority for action.\n';
|
|
668
707
|
}
|
|
@@ -672,7 +711,7 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
672
711
|
}
|
|
673
712
|
}
|
|
674
713
|
else if (hasL2 || hasL1) {
|
|
675
|
-
section += 'Read the listed synthesized learning files before continuing, then use this synthesized learning context throughout the session.\n';
|
|
714
|
+
section += 'Read the listed synthesized learning files before continuing, then use this synthesized learning context throughout the session. Selected entry source paths are provenance; do not read omitted entries or apply needs-domain material.\n';
|
|
676
715
|
if (multiTier) {
|
|
677
716
|
section += 'A family listed at more than one level is combined, not replaced. Stable IDs identify lessons; equal titles do not establish identity. Conflicting copies require explicit reconciliation: do not choose a revision by scope, timestamp, count, or title. Preserve approved decisions and useful refinements; unresolved conflicting claims are not authority for action.\n';
|
|
678
717
|
}
|
|
@@ -1034,7 +1073,7 @@ function readLearningEntryTitles(filePath) {
|
|
|
1034
1073
|
* manager-coaching entries that field used to hold, so the tile labelled
|
|
1035
1074
|
* "Manager" reports the level rather than a category.
|
|
1036
1075
|
*/
|
|
1037
|
-
function countPreservedLearnings(workspaceRoot, userId) {
|
|
1076
|
+
function countPreservedLearnings(workspaceRoot, userId, effectiveCatalog) {
|
|
1038
1077
|
const roots = getLearningRoots(workspaceRoot);
|
|
1039
1078
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1040
1079
|
// Distinct entry titles across every level holding this company family, so a
|
|
@@ -1086,7 +1125,7 @@ function countPreservedLearnings(workspaceRoot, userId) {
|
|
|
1086
1125
|
let reverseMentoring = coachByLevel.manager + coachByLevel.project;
|
|
1087
1126
|
// #806: domain-scoped files fold into the same per-level counts so the Brain
|
|
1088
1127
|
// reconciles with what the loader injects for a job.
|
|
1089
|
-
for (const domain of
|
|
1128
|
+
for (const domain of storedLearningDomains(effectiveCatalog)) {
|
|
1090
1129
|
for (const ft of ORG_TYPES)
|
|
1091
1130
|
organization += countOrgFamily(`org-${domain}-${ft}.md`);
|
|
1092
1131
|
for (const ft of PERSONAL_WORK_TYPES) {
|
|
@@ -1297,11 +1336,11 @@ const FILETYPE_TO_CATEGORY = {
|
|
|
1297
1336
|
* #806) present in a directory, in a deterministic domain order. Used by the Hub
|
|
1298
1337
|
* read/count paths so domain learnings appear alongside the flat global files.
|
|
1299
1338
|
*/
|
|
1300
|
-
function listDomainLearningFiles(dir, prefix, fileType) {
|
|
1339
|
+
function listDomainLearningFiles(dir, prefix, fileType, effectiveCatalog) {
|
|
1301
1340
|
if (!(0, fs_1.existsSync)(dir))
|
|
1302
1341
|
return [];
|
|
1303
1342
|
const out = [];
|
|
1304
|
-
for (const domain of
|
|
1343
|
+
for (const domain of storedLearningDomains(effectiveCatalog)) {
|
|
1305
1344
|
const fileName = `${prefix}-${domain}-${fileType}.md`;
|
|
1306
1345
|
if ((0, fs_1.existsSync)((0, path_1.join)(dir, fileName)))
|
|
1307
1346
|
out.push({ fileName, domain });
|
|
@@ -1404,7 +1443,7 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1404
1443
|
}
|
|
1405
1444
|
// `Id` is listed here as well as matched above so a malformed id line is still
|
|
1406
1445
|
// treated as bookkeeping rather than leaking into the displayed lesson.
|
|
1407
|
-
if (/^\*\*(Id|Score|Technical trace|Users|First synthesized)\*\*:/i.test(t))
|
|
1446
|
+
if (/^\*\*(Id|Score|Technical trace|Users|First recorded|First synthesized|Originating job|Sources|Domain|Domain basis|Domain history)\*\*:/i.test(t))
|
|
1408
1447
|
continue;
|
|
1409
1448
|
if (/^(First|Last) synthesized:/i.test(t))
|
|
1410
1449
|
continue;
|
|
@@ -1516,7 +1555,7 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1516
1555
|
// Newest first: the manager is most often checking what they just said no to.
|
|
1517
1556
|
return out.sort((a, b) => String(b.rejectedAt).localeCompare(String(a.rejectedAt)));
|
|
1518
1557
|
}
|
|
1519
|
-
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
|
|
1558
|
+
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine', effectiveCatalog) {
|
|
1520
1559
|
const roots = getLearningRoots(workspaceRoot);
|
|
1521
1560
|
const usageStats = resolveUsageStats();
|
|
1522
1561
|
// Only the human library reads these observations. An unavailable store must
|
|
@@ -1565,8 +1604,8 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1565
1604
|
out.push(...parseTiers(resolveOrgLearningFileTiers(roots.repoLearningsBase, f), cat));
|
|
1566
1605
|
// #806: domain-scoped org learnings — union from both locations.
|
|
1567
1606
|
const allDomainFiles = [
|
|
1568
|
-
...listDomainLearningFiles(roots.repoLearningsBase, 'org', fileType).map((d) => ({ ...d, base: roots.repoLearningsBase })),
|
|
1569
|
-
...listDomainLearningFiles((0, path_1.join)((0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot, 'learnings'), 'org', fileType)
|
|
1607
|
+
...listDomainLearningFiles(roots.repoLearningsBase, 'org', fileType, effectiveCatalog).map((d) => ({ ...d, base: roots.repoLearningsBase })),
|
|
1608
|
+
...listDomainLearningFiles((0, path_1.join)((0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot, 'learnings'), 'org', fileType, effectiveCatalog)
|
|
1570
1609
|
.map((d) => ({ ...d, base: (0, path_1.join)((0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot, 'learnings') })),
|
|
1571
1610
|
];
|
|
1572
1611
|
const seenDomainFile = new Set();
|
|
@@ -1590,7 +1629,7 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1590
1629
|
const cats = scope === 'reverse' ? ['coaching'] : ['avoid', 'preference', 'repeat'];
|
|
1591
1630
|
for (const cat of cats) {
|
|
1592
1631
|
const fileType = exports.CATEGORY_TO_FILETYPE[cat];
|
|
1593
|
-
for (const domain of ['global', ...
|
|
1632
|
+
for (const domain of ['global', ...storedLearningDomains(effectiveCatalog)]) {
|
|
1594
1633
|
const domainPart = domain === 'global' ? '' : `-${domain}`;
|
|
1595
1634
|
for (const storageTier of ['hot', 'cold']) {
|
|
1596
1635
|
const suffix = storageTier === 'cold' ? COLD_FILE_SUFFIX : '';
|
|
@@ -1607,39 +1646,39 @@ function isGlobalDomain(domain) {
|
|
|
1607
1646
|
return !domain || domain === 'global';
|
|
1608
1647
|
}
|
|
1609
1648
|
/** Normalize a caller-supplied domain to one of the known domains, or 'global'. */
|
|
1610
|
-
function normalizeLearningDomain(domain) {
|
|
1649
|
+
function normalizeLearningDomain(domain, effectiveCatalog) {
|
|
1611
1650
|
if (isGlobalDomain(domain))
|
|
1612
1651
|
return 'global';
|
|
1613
|
-
const found =
|
|
1652
|
+
const found = storedLearningDomains(effectiveCatalog).find((d) => d === domain);
|
|
1614
1653
|
return found ?? 'global';
|
|
1615
1654
|
}
|
|
1616
|
-
function learningFileName(userId, fileType, domain) {
|
|
1655
|
+
function learningFileName(userId, fileType, domain, effectiveCatalog) {
|
|
1617
1656
|
// Issue #1755 / #754: normalize here rather than trusting the caller. The HTTP
|
|
1618
1657
|
// layer already allowlists the domain, but this function builds a path and a
|
|
1619
1658
|
// direct caller passing `../../escape` would otherwise write outside the
|
|
1620
1659
|
// learnings root. Anything not in the known set resolves to the global file.
|
|
1621
|
-
const safe = normalizeLearningDomain(domain);
|
|
1660
|
+
const safe = normalizeLearningDomain(domain, effectiveCatalog);
|
|
1622
1661
|
return safe === 'global' ? `${userId}-${fileType}.md` : `${userId}-${safe}-${fileType}.md`;
|
|
1623
1662
|
}
|
|
1624
|
-
function resolveLearningFilePath(workspaceRoot, userId, ref) {
|
|
1663
|
+
function resolveLearningFilePath(workspaceRoot, userId, ref, effectiveCatalog) {
|
|
1625
1664
|
const roots = getLearningRoots(workspaceRoot);
|
|
1626
1665
|
const fileType = exports.CATEGORY_TO_FILETYPE[ref.category];
|
|
1627
1666
|
if (ref.scope === 'org') {
|
|
1628
1667
|
// Write org entries to the org pack home — same location share-with-others uses.
|
|
1629
1668
|
// The Hub read path already merges pack home + repo-local, so edits land where
|
|
1630
1669
|
// the agent reads them without needing a repo commit.
|
|
1631
|
-
return (0, path_1.join)((0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot, 'learnings', learningFileName('org', fileType, ref.domain));
|
|
1670
|
+
return (0, path_1.join)((0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot, 'learnings', learningFileName('org', fileType, ref.domain, effectiveCatalog));
|
|
1632
1671
|
}
|
|
1633
1672
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
1634
1673
|
// Issue #1070: for the 'machine' level use the configured manager home.
|
|
1635
1674
|
const base = ref.level === 'project' ? roots.repoLearningsBase : roots.managerHomeBase;
|
|
1636
|
-
return (0, path_1.join)(base, learningFileName(resolvedUserId, fileType, ref.domain));
|
|
1675
|
+
return (0, path_1.join)(base, learningFileName(resolvedUserId, fileType, ref.domain, effectiveCatalog));
|
|
1637
1676
|
}
|
|
1638
1677
|
function titleCaseFromFileType(fileType) {
|
|
1639
1678
|
return fileType.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
1640
1679
|
}
|
|
1641
|
-
function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
|
|
1642
|
-
const filePath = resolveLearningFilePath(workspaceRoot, userId, ref);
|
|
1680
|
+
function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload, effectiveCatalog) {
|
|
1681
|
+
const filePath = resolveLearningFilePath(workspaceRoot, userId, ref, effectiveCatalog);
|
|
1643
1682
|
const existing = (0, fs_1.existsSync)(filePath) ? (0, fs_1.readFileSync)(filePath, 'utf8') : '';
|
|
1644
1683
|
const nl = existing.includes('\r\n') ? '\r\n' : '\n';
|
|
1645
1684
|
const lines = existing.length ? existing.split(/\r?\n/) : [];
|
|
@@ -1651,13 +1690,28 @@ function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
|
|
|
1651
1690
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
|
|
1652
1691
|
(0, fs_1.writeFileSync)(filePath, text.replace(/\s*$/, '') + nl, 'utf8');
|
|
1653
1692
|
};
|
|
1693
|
+
const metadataLine = /^\*\*(Id|Score|Last seen|Recurrences|Technical trace|Users|First recorded|First synthesized|Originating job|Sources|Domain|Domain basis|Domain history)\*\*:/i;
|
|
1694
|
+
const setManagerDomain = (metadata, domain) => {
|
|
1695
|
+
const version = effectiveCatalog?.mapVersion ?? learning_domains_1.LEARNING_DOMAIN_MAP_VERSION;
|
|
1696
|
+
if (!Number.isInteger(version) || version < 1)
|
|
1697
|
+
throw new Error('Cannot record a learning domain decision: restore the domain catalog first.');
|
|
1698
|
+
const oldDomain = metadata.find(line => /^\*\*Domain\*\*:/i.test(line.trim()))?.trim().replace(/^\*\*Domain\*\*:\s*/i, '');
|
|
1699
|
+
const oldBasis = metadata.find(line => /^\*\*Domain basis\*\*:/i.test(line.trim()))?.trim().replace(/^\*\*Domain basis\*\*:\s*/i, '');
|
|
1700
|
+
const retained = metadata.filter(line => !/^\*\*(Domain|Domain basis)\*\*:/i.test(line.trim()));
|
|
1701
|
+
if (oldDomain && (oldDomain !== domain || oldBasis !== `manager; map ${version}`)) {
|
|
1702
|
+
retained.push(`**Domain history**: ${oldDomain}; ${oldBasis ?? 'basis not recorded'}`);
|
|
1703
|
+
}
|
|
1704
|
+
return [...retained, `**Domain**: ${domain}`, `**Domain basis**: manager; map ${version}`];
|
|
1705
|
+
};
|
|
1654
1706
|
if (action === 'add') {
|
|
1655
1707
|
const sev = (payload.severity || 'P-MED');
|
|
1656
1708
|
const title = (payload.title || '').trim() || 'Untitled learning';
|
|
1657
|
-
const
|
|
1658
|
-
const
|
|
1709
|
+
const suppliedLines = String(payload.body || '').trim().split(/\r?\n/);
|
|
1710
|
+
const addDomain = normalizeLearningDomain(ref.domain, effectiveCatalog);
|
|
1711
|
+
const metadata = setManagerDomain(suppliedLines.filter(line => metadataLine.test(line.trim())), addDomain);
|
|
1712
|
+
const body = suppliedLines.filter(line => !metadataLine.test(line.trim())).join(nl).trim();
|
|
1713
|
+
const block = [`#### [${sev}] ${title}`, '', ...metadata, '', body, '', '---'].join(nl);
|
|
1659
1714
|
const fileType = exports.CATEGORY_TO_FILETYPE[ref.category];
|
|
1660
|
-
const addDomain = normalizeLearningDomain(ref.domain);
|
|
1661
1715
|
const preamble = existing.trim()
|
|
1662
1716
|
? existing.replace(/\s*$/, '')
|
|
1663
1717
|
: `# ${ref.scope === 'org' ? 'Org ' : ''}${titleCaseFromFileType(fileType)}${addDomain === 'global' ? '' : ` (${addDomain})`}`;
|
|
@@ -1701,18 +1755,22 @@ function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
|
|
|
1701
1755
|
const t = l.trim();
|
|
1702
1756
|
if (t === '---')
|
|
1703
1757
|
continue;
|
|
1704
|
-
if (
|
|
1758
|
+
if (metadataLine.test(t) || /^(First|Last) synthesized:/i.test(t))
|
|
1705
1759
|
meta.push(l);
|
|
1706
1760
|
}
|
|
1707
|
-
const
|
|
1708
|
-
const
|
|
1761
|
+
const originalBody = lines.slice(start + 1, end).filter(line => line.trim() !== '---' && !metadataLine.test(line.trim()) && !/^(First|Last) synthesized:/i.test(line.trim())).join(nl).trim();
|
|
1762
|
+
const body = payload.body === undefined ? originalBody : String(payload.body).split(/\r?\n/).filter(line => !metadataLine.test(line.trim())).join(nl).trim();
|
|
1763
|
+
const targetDomain = payload.targetDomain === undefined ? undefined : normalizeLearningDomain(payload.targetDomain, effectiveCatalog);
|
|
1764
|
+
// A normal prose edit is not authority to reclassify an ambiguous legacy entry.
|
|
1765
|
+
// An explicit choice records a manager decision even when its filename is unchanged.
|
|
1766
|
+
const updatedMeta = targetDomain !== undefined && targetDomain === payload.targetDomain ? setManagerDomain(meta, targetDomain) : meta;
|
|
1767
|
+
const rebuilt = [`#### [${sev}] ${title}`, '', ...updatedMeta, ...(updatedMeta.length ? [''] : []), body, '', '---'];
|
|
1709
1768
|
// Issue #1755 R2/R4: a domain change is a move between files, because the domain
|
|
1710
1769
|
// IS the filename. Cut the rebuilt block from the source file and append it to the
|
|
1711
1770
|
// destination, so the entry keeps its id, score and history rather than being
|
|
1712
1771
|
// re-created as a new entry with no past.
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
const destPath = resolveLearningFilePath(workspaceRoot, userId, { ...ref, domain: targetDomain });
|
|
1772
|
+
if (targetDomain !== undefined && targetDomain !== normalizeLearningDomain(ref.domain, effectiveCatalog)) {
|
|
1773
|
+
const destPath = resolveLearningFilePath(workspaceRoot, userId, { ...ref, domain: targetDomain }, effectiveCatalog);
|
|
1716
1774
|
const destExisting = (0, fs_1.existsSync)(destPath) ? (0, fs_1.readFileSync)(destPath, 'utf8') : '';
|
|
1717
1775
|
const destNl = destExisting.includes('\r\n') ? '\r\n' : nl;
|
|
1718
1776
|
const fileTypeForHeader = exports.CATEGORY_TO_FILETYPE[ref.category];
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
/**
|
|
4
|
-
* FRAIM Local MCP Server - STDIO Version
|
|
5
|
-
*
|
|
6
|
-
* Proxy that:
|
|
7
|
-
* 1. Accepts MCP requests via stdin/stdout
|
|
8
|
-
* 2. Proxies to remote FRAIM server
|
|
9
|
-
* 3. Performs template substitution:
|
|
10
|
-
* - Proxy config variables: {{proxy.config.path.to.value}}
|
|
11
|
-
* - Platform-specific actions: {{proxy.action.get_issue}}, {{proxy.action.create_pr}}, etc.
|
|
12
|
-
* 4. Automatically detects and injects machine/repo info for fraim_connect
|
|
13
|
-
* 5. Substitutes {{proxy.delivery.*}} templates based on user's workingStyle
|
|
14
|
-
* (PR or Conversation from ~/.fraim/config.json). Delivery phases live
|
|
15
|
-
* server-side in the workflow; the proxy just fills in mode-specific content.
|
|
16
|
-
*/
|
|
17
3
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
4
|
if (k2 === undefined) k2 = k;
|
|
19
5
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -54,6 +40,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
54
40
|
exports.FraimLocalMCPServer = exports.FraimTemplateEngine = void 0;
|
|
55
41
|
exports.parseExecutionModeFromResponse = parseExecutionModeFromResponse;
|
|
56
42
|
exports.buildExecutionModeForwardUrl = buildExecutionModeForwardUrl;
|
|
43
|
+
const domain_rule_assembler_1 = require("../core/utils/domain-rule-assembler");
|
|
44
|
+
/**
|
|
45
|
+
* FRAIM Local MCP Server - STDIO Version
|
|
46
|
+
*
|
|
47
|
+
* Proxy that:
|
|
48
|
+
* 1. Accepts MCP requests via stdin/stdout
|
|
49
|
+
* 2. Proxies to remote FRAIM server
|
|
50
|
+
* 3. Performs template substitution:
|
|
51
|
+
* - Proxy config variables: {{proxy.config.path.to.value}}
|
|
52
|
+
* - Platform-specific actions: {{proxy.action.get_issue}}, {{proxy.action.create_pr}}, etc.
|
|
53
|
+
* 4. Automatically detects and injects machine/repo info for fraim_connect
|
|
54
|
+
* 5. Substitutes {{proxy.delivery.*}} templates based on user's workingStyle
|
|
55
|
+
* (PR or Conversation from ~/.fraim/config.json). Delivery phases live
|
|
56
|
+
* server-side in the workflow; the proxy just fills in mode-specific content.
|
|
57
|
+
*/
|
|
57
58
|
const fs_1 = require("fs");
|
|
58
59
|
const path_1 = require("path");
|
|
59
60
|
const os_1 = require("os");
|
|
@@ -80,7 +81,7 @@ const usage_collector_js_1 = require("./usage-collector.js");
|
|
|
80
81
|
const otlp_metrics_receiver_js_1 = require("./otlp-metrics-receiver.js");
|
|
81
82
|
const token_adapter_registry_js_1 = require("./token-adapter-registry.js");
|
|
82
83
|
const learning_context_builder_js_1 = require("./learning-context-builder.js");
|
|
83
|
-
const
|
|
84
|
+
const catalog_1 = require("../../registry/scripts/domains/catalog");
|
|
84
85
|
const skill_include_dedup_js_1 = require("./skill-include-dedup.js");
|
|
85
86
|
const git_utils_1 = require("../core/utils/git-utils");
|
|
86
87
|
/**
|
|
@@ -1289,6 +1290,10 @@ class FraimLocalMCPServer {
|
|
|
1289
1290
|
* The Hub run pointer bridges CLI and MCP session IDs, which are unrelated.
|
|
1290
1291
|
*/
|
|
1291
1292
|
recordRecoveryState(args, sessionId, acceptedNext) {
|
|
1293
|
+
// Instruction restoration is a read, including after terminal/review states.
|
|
1294
|
+
// Do not rebind the original execution to the requesting transport session.
|
|
1295
|
+
if (args.status === 'starting' && args.requestInstructions === true)
|
|
1296
|
+
return;
|
|
1292
1297
|
if (!sessionId || sessionId === 'unknown'
|
|
1293
1298
|
|| typeof args.jobName !== 'string' || !args.jobName
|
|
1294
1299
|
|| typeof args.currentPhase !== 'string' || !args.currentPhase
|
|
@@ -1382,6 +1387,8 @@ class FraimLocalMCPServer {
|
|
|
1382
1387
|
}
|
|
1383
1388
|
/** Called only after the response has passed every rejecting contract. */
|
|
1384
1389
|
acceptPhaseTracking(args, sessionId, nextPhase) {
|
|
1390
|
+
if (args.status === 'starting' && args.requestInstructions === true)
|
|
1391
|
+
return '';
|
|
1385
1392
|
this.recordRecoveryState(args, sessionId, { nextPhase });
|
|
1386
1393
|
if (!sessionId || sessionId === 'unknown' || !args.jobName || !args.currentPhase) {
|
|
1387
1394
|
return args.status === 'complete' || args.status === 'failure'
|
|
@@ -1689,6 +1696,23 @@ class FraimLocalMCPServer {
|
|
|
1689
1696
|
if (request.method !== 'tools/call' || typeof toolName !== 'string') {
|
|
1690
1697
|
return this.processResponseWithHydration(response, requestSessionId);
|
|
1691
1698
|
}
|
|
1699
|
+
// A remote catalog miss does not mean an authorized personalized file is
|
|
1700
|
+
// missing. Keep this exception tied to the file lookup producer's exact
|
|
1701
|
+
// error; authentication, execution and workflow rejections stay authoritative.
|
|
1702
|
+
const remoteFileMissing = toolName === 'get_fraim_file'
|
|
1703
|
+
&& typeof args.path === 'string'
|
|
1704
|
+
&& response.error?.code === -32603
|
|
1705
|
+
&& response.error.message === `File not found: ${args.path}. Check for typos in the file name or file path.`;
|
|
1706
|
+
if (response.result?.isError || response.result?.personaLock || (response.error && !remoteFileMissing))
|
|
1707
|
+
return response;
|
|
1708
|
+
// Connect creates the session: its response, rather than the sessionless
|
|
1709
|
+
// request, authorizes dependent catalog, include and rule reads. Keep it
|
|
1710
|
+
// request-local so a later rejected reconnect cannot change another call.
|
|
1711
|
+
if (toolName === 'fraim_connect') {
|
|
1712
|
+
const connectedSessionId = response.result?.structuredContent?.sessionId ?? response.result?.sessionId;
|
|
1713
|
+
if (typeof connectedSessionId === 'string' && connectedSessionId.trim())
|
|
1714
|
+
requestSessionId = connectedSessionId;
|
|
1715
|
+
}
|
|
1692
1716
|
const resolver = this.getRegistryResolver(requestSessionId);
|
|
1693
1717
|
let finalizedResponse = response;
|
|
1694
1718
|
// 1. Resolve top-level override if this tool maps to a specific file.
|
|
@@ -1702,20 +1726,18 @@ class FraimLocalMCPServer {
|
|
|
1702
1726
|
registryPath = await resolver.findRegistryPath('jobs', args.job);
|
|
1703
1727
|
if (registryPath) {
|
|
1704
1728
|
const localOverride = resolver.hasPersonalizedOverride(registryPath);
|
|
1705
|
-
|
|
1706
|
-
if (response.error) {
|
|
1707
|
-
this.log(`[req:${requestId}] Remote error for ${registryPath}: code=${response.error.code}`);
|
|
1708
|
-
}
|
|
1709
|
-
if (localOverride || remoteFailed) {
|
|
1729
|
+
if (localOverride || remoteFileMissing) {
|
|
1710
1730
|
try {
|
|
1711
1731
|
// Every configured personalized layer takes priority over the remote baseline.
|
|
1712
|
-
const resolved = await resolver.resolveFile(registryPath).catch(() => null);
|
|
1732
|
+
const resolved = await resolver.resolveFile(registryPath, { includeMetadata: false }).catch(() => null);
|
|
1713
1733
|
if (resolved?.content) {
|
|
1714
1734
|
const newResponse = {
|
|
1715
1735
|
...response,
|
|
1716
1736
|
result: {
|
|
1717
1737
|
...response.result,
|
|
1718
|
-
content:
|
|
1738
|
+
content: response.result?.content?.length
|
|
1739
|
+
? response.result.content.map((block, index) => index === 0 ? { type: 'text', text: resolved.content } : block)
|
|
1740
|
+
: [{ type: 'text', text: resolved.content }]
|
|
1719
1741
|
}
|
|
1720
1742
|
};
|
|
1721
1743
|
delete newResponse.error;
|
|
@@ -1728,6 +1750,8 @@ class FraimLocalMCPServer {
|
|
|
1728
1750
|
}
|
|
1729
1751
|
}
|
|
1730
1752
|
}
|
|
1753
|
+
if (finalizedResponse.error || finalizedResponse.result?.isError)
|
|
1754
|
+
return finalizedResponse;
|
|
1731
1755
|
// 2. Resolve includes within the content (for all registry tools)
|
|
1732
1756
|
finalizedResponse = await this.resolveIncludesInResponse(finalizedResponse, requestSessionId, requestId);
|
|
1733
1757
|
// 3. After fraim_connect succeeds, capture user email and inject learning context.
|
|
@@ -1768,11 +1792,13 @@ class FraimLocalMCPServer {
|
|
|
1768
1792
|
finalizedResponse.result.content[0].text = responseText;
|
|
1769
1793
|
}
|
|
1770
1794
|
}
|
|
1795
|
+
let catalogRead;
|
|
1771
1796
|
// 4. After get_fraim_job succeeds, inject learning context with job-focus frame.
|
|
1772
1797
|
if (toolName === 'get_fraim_job' && !finalizedResponse.error) {
|
|
1773
1798
|
const text = finalizedResponse.result?.content?.[0]?.text;
|
|
1774
1799
|
if (typeof text === 'string') {
|
|
1775
|
-
|
|
1800
|
+
catalogRead = (0, domain_rule_assembler_1.withRuleDeliveryTimeout)(resolver.resolveFile('domains.json', { includeMetadata: false }));
|
|
1801
|
+
finalizedResponse.result.content[0].text = await this.appendJobContext(text, args.job, requestSessionId, requestId, catalogRead);
|
|
1776
1802
|
}
|
|
1777
1803
|
// Issue #1345: forward executionMode from get_fraim_job response to Hub so
|
|
1778
1804
|
// the chip reads from run data (no local file read, no separate endpoint needed).
|
|
@@ -1788,16 +1814,23 @@ class FraimLocalMCPServer {
|
|
|
1788
1814
|
}
|
|
1789
1815
|
}
|
|
1790
1816
|
}
|
|
1817
|
+
finalizedResponse = await this.addRuleDelivery(request, finalizedResponse, requestSessionId, catalogRead);
|
|
1791
1818
|
return this.processResponseWithHydration(finalizedResponse, requestSessionId);
|
|
1792
1819
|
}
|
|
1793
1820
|
/** Keep context delivery and offer attribution together for local and remote jobs. */
|
|
1794
|
-
async appendJobContext(text, jobName, requestSessionId, requestId) {
|
|
1821
|
+
async appendJobContext(text, jobName, requestSessionId, requestId, catalogRead) {
|
|
1795
1822
|
const userEmail = this.ensureEngine().getUserEmail();
|
|
1823
|
+
const workspaceRoot = this.findProjectRoot() || process.cwd();
|
|
1824
|
+
let effectiveCatalog;
|
|
1825
|
+
try {
|
|
1826
|
+
const source = await (catalogRead ?? (0, domain_rule_assembler_1.withRuleDeliveryTimeout)(this.getRegistryResolver(requestSessionId).resolveFile('domains.json', { includeMetadata: false })));
|
|
1827
|
+
effectiveCatalog = JSON.parse(source.content);
|
|
1828
|
+
}
|
|
1829
|
+
catch { /* Rule delivery returns the source warning; retain available global learning. */ }
|
|
1796
1830
|
if (!userEmail)
|
|
1797
1831
|
return text;
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
1800
|
-
const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true, jobDomain);
|
|
1832
|
+
const jobDomain = effectiveCatalog ? await this.resolveJobDomainForJob(jobName, requestSessionId, effectiveCatalog) : null;
|
|
1833
|
+
const learningSection = (0, learning_context_builder_js_1.buildLearningContextSection)(workspaceRoot, userEmail, true, jobDomain, effectiveCatalog);
|
|
1801
1834
|
const teamContextSection = (0, learning_context_builder_js_1.buildTeamContextSection)(workspaceRoot, true);
|
|
1802
1835
|
if (learningSection || teamContextSection) {
|
|
1803
1836
|
text += `\n\n---` + learningSection + teamContextSection;
|
|
@@ -1812,7 +1845,7 @@ class FraimLocalMCPServer {
|
|
|
1812
1845
|
* context must never break a job response, and an unknown job never guesses a
|
|
1813
1846
|
* wrong domain.
|
|
1814
1847
|
*/
|
|
1815
|
-
async resolveJobDomainForJob(jobName, requestSessionId) {
|
|
1848
|
+
async resolveJobDomainForJob(jobName, requestSessionId, effectiveCatalog) {
|
|
1816
1849
|
try {
|
|
1817
1850
|
const resolver = this.getRegistryResolver(requestSessionId);
|
|
1818
1851
|
if (!resolver)
|
|
@@ -1820,12 +1853,9 @@ class FraimLocalMCPServer {
|
|
|
1820
1853
|
const registryPath = await resolver.findRegistryPath('jobs', jobName);
|
|
1821
1854
|
if (!registryPath)
|
|
1822
1855
|
return null;
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
// to personalized content, hiding its frontmatter from the domain parser.
|
|
1827
|
-
const resolved = await resolver.resolveFile(registryPath, { includeMetadata: false }).catch(() => null);
|
|
1828
|
-
return (0, learning_domains_js_1.resolveDomainForJob)(registryPath, resolved?.content ?? null);
|
|
1856
|
+
const catalog = effectiveCatalog ?? JSON.parse((await (0, domain_rule_assembler_1.withRuleDeliveryTimeout)(resolver.resolveFile('domains.json', { includeMetadata: false }))).content);
|
|
1857
|
+
const selected = (0, catalog_1.selectCatalogDomain)(catalog, registryPath);
|
|
1858
|
+
return selected.selection === 'mapped' && selected.domain !== 'global' ? selected.domain : null;
|
|
1829
1859
|
}
|
|
1830
1860
|
catch {
|
|
1831
1861
|
return null;
|
|
@@ -1862,7 +1892,51 @@ class FraimLocalMCPServer {
|
|
|
1862
1892
|
}
|
|
1863
1893
|
return errors;
|
|
1864
1894
|
}
|
|
1865
|
-
async
|
|
1895
|
+
async addRuleDelivery(request, response, sessionId, catalogRead) {
|
|
1896
|
+
if (response.error || response.result?.isError || response.result?.personaLock || !response.result)
|
|
1897
|
+
return response;
|
|
1898
|
+
const tool = request.params?.name;
|
|
1899
|
+
const args = request.params?.arguments || {};
|
|
1900
|
+
let jobPath = null;
|
|
1901
|
+
if (tool === 'get_fraim_job' || tool === 'seekMentoring') {
|
|
1902
|
+
const context = await this.getMentor(sessionId).getRuleDeliveryContext(args.job || args.jobName);
|
|
1903
|
+
if (!context)
|
|
1904
|
+
return response;
|
|
1905
|
+
jobPath = context.jobPath;
|
|
1906
|
+
if (tool === 'seekMentoring' && !(args.status === 'starting' &&
|
|
1907
|
+
(args.currentPhase === 'starting' || args.currentPhase === context.initialPhase || args.requestInstructions === true)))
|
|
1908
|
+
return response;
|
|
1909
|
+
}
|
|
1910
|
+
else if (tool !== 'fraim_connect')
|
|
1911
|
+
return response;
|
|
1912
|
+
// Reuse authenticated connect policy only as the remote fallback. The
|
|
1913
|
+
// ordinary resolver still checks every effective local/overlay source first.
|
|
1914
|
+
const remoteFiles = new Map();
|
|
1915
|
+
if (tool === 'fraim_connect') {
|
|
1916
|
+
const prior = response.result.structuredContent?.ruleAssembly;
|
|
1917
|
+
const block = Number.isInteger(prior?.contentBlockIndex) && prior.contentBlockIndex >= 0
|
|
1918
|
+
? response.result.content?.[prior.contentBlockIndex] : undefined;
|
|
1919
|
+
const artifact = Array.isArray(prior?.artifacts) && prior.artifacts.length === 1 ? prior.artifacts[0] : undefined;
|
|
1920
|
+
const prefix = '## Applicable rules\n\n### rules/constitution.md\n\n';
|
|
1921
|
+
if (prior?.complete === true && prior.selection === 'connect' && prior.domain === 'global'
|
|
1922
|
+
&& Array.isArray(prior.warnings) && prior.warnings.length === 0
|
|
1923
|
+
&& artifact?.path === 'rules/constitution.md' && block?.type === 'text'
|
|
1924
|
+
&& typeof block.text === 'string' && block.text.startsWith(prefix)) {
|
|
1925
|
+
const body = block.text.slice(prefix.length);
|
|
1926
|
+
if ((0, crypto_1.createHash)('sha256').update(body).digest('hex') === artifact?.hash)
|
|
1927
|
+
remoteFiles.set(artifact.path, body);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
const resolver = this.getRegistryResolver(sessionId, remoteFiles);
|
|
1931
|
+
const result = (0, domain_rule_assembler_1.attachRuleAssembly)(response.result, await (0, domain_rule_assembler_1.assembleDomainRules)({ jobPath,
|
|
1932
|
+
resolveFile: path => path === 'domains.json' && catalogRead ? catalogRead : resolver.resolveFile(path, { includeMetadata: false, stripMcpHeader: true }) }));
|
|
1933
|
+
const metadata = result.structuredContent?.ruleAssembly;
|
|
1934
|
+
for (const warning of metadata?.warnings ?? []) {
|
|
1935
|
+
this.logError('Rule delivery warning: ' + JSON.stringify(warning));
|
|
1936
|
+
}
|
|
1937
|
+
return { ...response, result };
|
|
1938
|
+
}
|
|
1939
|
+
async finalizeLocalToolTextResponse(request, requestSessionId, requestId, text, catalogRead) {
|
|
1866
1940
|
const response = {
|
|
1867
1941
|
jsonrpc: '2.0',
|
|
1868
1942
|
id: request.id,
|
|
@@ -1870,7 +1944,8 @@ class FraimLocalMCPServer {
|
|
|
1870
1944
|
content: [{ type: 'text', text }]
|
|
1871
1945
|
}
|
|
1872
1946
|
};
|
|
1873
|
-
const
|
|
1947
|
+
const withRules = await this.addRuleDelivery(request, response, requestSessionId, catalogRead);
|
|
1948
|
+
const withResolvedIncludes = await this.resolveIncludesInResponse(withRules, requestSessionId, requestId);
|
|
1874
1949
|
return this.processResponseWithHydration(withResolvedIncludes, requestSessionId);
|
|
1875
1950
|
}
|
|
1876
1951
|
rewriteProxyTokensInText(text) {
|
|
@@ -2015,7 +2090,7 @@ class FraimLocalMCPServer {
|
|
|
2015
2090
|
/**
|
|
2016
2091
|
* Initialize the LocalRegistryResolver for override resolution
|
|
2017
2092
|
*/
|
|
2018
|
-
getRegistryResolver(requestSessionId) {
|
|
2093
|
+
getRegistryResolver(requestSessionId, remoteFiles) {
|
|
2019
2094
|
const projectRoot = this.findProjectRoot();
|
|
2020
2095
|
this.log(`🔍 getRegistryResolver: projectRoot = ${projectRoot}`);
|
|
2021
2096
|
// Determine effective FRAIM dir using shadow semantics
|
|
@@ -2042,6 +2117,8 @@ class FraimLocalMCPServer {
|
|
|
2042
2117
|
orgCacheRoot: (0, pack_home_1.resolvePackHome)('org', { migrateLegacy: false }).contentRoot,
|
|
2043
2118
|
shouldFilter: (content) => this.isStub(content),
|
|
2044
2119
|
remoteContentResolver: async (path) => {
|
|
2120
|
+
if (remoteFiles?.has(path))
|
|
2121
|
+
return remoteFiles.get(path);
|
|
2045
2122
|
// Fetch parent content from remote for inheritance
|
|
2046
2123
|
this.log(`🔄 Remote content resolver: fetching ${path}`);
|
|
2047
2124
|
this.log(`🔄 Fetching raw file content: ${path}`);
|
|
@@ -2820,8 +2897,9 @@ class FraimLocalMCPServer {
|
|
|
2820
2897
|
responseText = `${mentor.getCompactPhaseAuthority()}\n\n${responseText}`;
|
|
2821
2898
|
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.`;
|
|
2822
2899
|
}
|
|
2823
|
-
|
|
2824
|
-
|
|
2900
|
+
const catalogRead = (0, domain_rule_assembler_1.withRuleDeliveryTimeout)(this.getRegistryResolver(requestSessionId).resolveFile('domains.json', { includeMetadata: false }));
|
|
2901
|
+
responseText = await this.appendJobContext(responseText, name, requestSessionId, requestId, catalogRead);
|
|
2902
|
+
return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, responseText, catalogRead);
|
|
2825
2903
|
}
|
|
2826
2904
|
}
|
|
2827
2905
|
catch (error) {
|
|
@@ -3150,6 +3228,11 @@ class FraimLocalMCPServer {
|
|
|
3150
3228
|
}
|
|
3151
3229
|
const toolName = request.params?.name;
|
|
3152
3230
|
let args = request.params?.arguments || {};
|
|
3231
|
+
if (toolName === 'seekMentoring' && args.status === 'starting' && args.requestInstructions === true) {
|
|
3232
|
+
// A context read must not publish a phase-start event to execution projections.
|
|
3233
|
+
this.mentoringResponseCache?.delete(request.id);
|
|
3234
|
+
return;
|
|
3235
|
+
}
|
|
3153
3236
|
const requestSessionId = this.extractSessionIdFromRequest(request);
|
|
3154
3237
|
if (toolName && requestSessionId) {
|
|
3155
3238
|
const success = !response.error && !response.result?.isError;
|