fraim 2.0.210 → 2.0.212
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.
|
@@ -130,7 +130,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
130
130
|
personaKey: 'ricardo',
|
|
131
131
|
bundleId: 'persona-ricardo-core',
|
|
132
132
|
catalogMetadata: buildCatalogMetadata('ricardo', ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management']),
|
|
133
|
-
protectedJobs: ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management', 'interview-coordination', 'offer-and-close-management'],
|
|
133
|
+
protectedJobs: ['role-intake-and-scorecard', 'job-posting-and-sourcing', 'candidate-pipeline-management', 'sourcing-funnel-recap', 'interview-coordination', 'offer-and-close-management'],
|
|
134
134
|
protectedAliases: ['recruiting', 'recruiter', 'hiring'],
|
|
135
135
|
defaultHireMode: 'job',
|
|
136
136
|
lockCopy: 'Hire RECardo to unlock full-cycle recruiting work for this request.'
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* workspace root on the user's machine.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.CATEGORY_TO_FILETYPE = exports.LEARNING_PRIORITIES = void 0;
|
|
9
|
+
exports.CATEGORY_TO_FILETYPE = exports.BRAIN_LEARNING_FILETYPE_TO_REGION = exports.LEARNING_PRIORITIES = void 0;
|
|
10
10
|
exports.learningEntryHeadingRegex = learningEntryHeadingRegex;
|
|
11
11
|
exports.computeEffectiveScore = computeEffectiveScore;
|
|
12
12
|
exports.buildLearningContextSection = buildLearningContextSection;
|
|
@@ -17,6 +17,7 @@ exports.resolveTeamContextFile = resolveTeamContextFile;
|
|
|
17
17
|
exports.readOrgBrand = readOrgBrand;
|
|
18
18
|
exports.resolveOrgBrandWriteDir = resolveOrgBrandWriteDir;
|
|
19
19
|
exports.countPreservedLearnings = countPreservedLearnings;
|
|
20
|
+
exports.collectBrainLearningDots = collectBrainLearningDots;
|
|
20
21
|
exports.readPreservedLearnings = readPreservedLearnings;
|
|
21
22
|
exports.applyLearningEntryChange = applyLearningEntryChange;
|
|
22
23
|
exports.isTruthyFlag = isTruthyFlag;
|
|
@@ -385,13 +386,14 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
385
386
|
coach: { ft: 'manager-coaching', gated: false, label: '(manager-facing; all entries)' },
|
|
386
387
|
validated: { ft: 'validated-patterns', gated: true, label: '(entries above score threshold)' },
|
|
387
388
|
};
|
|
388
|
-
// Domain axis (issue #806
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
const
|
|
389
|
+
// Domain axis (issue #806): session startup stays broad enough for stable
|
|
390
|
+
// preferences plus L0, while job contexts load the full global set and may
|
|
391
|
+
// append the current job's domain files below the global files.
|
|
392
|
+
const sessionCats = ['pref'];
|
|
393
|
+
const jobL2Cats = ['mistake', 'pref', 'coach', 'validated'];
|
|
394
|
+
const jobL1Cats = ['pref', 'coach', 'mistake', 'validated'];
|
|
395
|
+
const l2Cats = forJob ? jobL2Cats : sessionCats;
|
|
396
|
+
const l1Cats = forJob ? jobL1Cats : sessionCats;
|
|
395
397
|
const activeDomain = forJob && domain ? domain : null;
|
|
396
398
|
const dormantOf = (meta, filePath) => meta.gated ? scanMistakePatternFile(filePath, threshold, meta.ft).dormant : 0;
|
|
397
399
|
// Resolve one tier (L2 org or L1 personal) into ordered global + domain files.
|
|
@@ -475,14 +477,14 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
475
477
|
const coachPresent = l1.coachPresent || l2.coachPresent;
|
|
476
478
|
if (forJob) {
|
|
477
479
|
if (hasL2 || hasL1) {
|
|
478
|
-
section += '
|
|
480
|
+
section += 'Read the listed synthesized learning files before continuing, then apply the relevant patterns and preferences in this job.\n';
|
|
479
481
|
if (coachPresent) {
|
|
480
482
|
section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
|
|
481
483
|
}
|
|
482
484
|
}
|
|
483
485
|
}
|
|
484
|
-
else {
|
|
485
|
-
section += '
|
|
486
|
+
else if (hasL2 || hasL1) {
|
|
487
|
+
section += 'Read the listed synthesized learning files before continuing, then use this synthesized learning context throughout the session.\n';
|
|
486
488
|
if (coachPresent) {
|
|
487
489
|
section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
|
|
488
490
|
}
|
|
@@ -842,6 +844,118 @@ function countPreservedLearnings(workspaceRoot, userId) {
|
|
|
842
844
|
rawSignals,
|
|
843
845
|
};
|
|
844
846
|
}
|
|
847
|
+
// ── Issue #833: Brain learning-dot projection ──────────────────────────────
|
|
848
|
+
// Personal learning signals the FRAIM brain overlays on the SVG. Pending L0
|
|
849
|
+
// signals (raw coaching moments + unsynthesized retros) cluster in the
|
|
850
|
+
// temporal-memory region; processed L1/L2 learnings map to a fixed brain region
|
|
851
|
+
// per learning type (the approved Option A mapping). A read projection over the
|
|
852
|
+
// same files buildLearningContextSection injects — no new storage.
|
|
853
|
+
/** Fixed learning-type → brain-region mapping (issue #833, approved spec). */
|
|
854
|
+
exports.BRAIN_LEARNING_FILETYPE_TO_REGION = {
|
|
855
|
+
'mistake-patterns': 'prefrontal-cortex', // rule following / constraint enforcement
|
|
856
|
+
'preferences': 'right-parietal-experience', // experience / audience fit
|
|
857
|
+
'validated-patterns': 'left-parietal-analysis', // engineering / evaluation judgment
|
|
858
|
+
'manager-coaching': 'right-frontal-strategy', // strategy / framing (reverse mentoring)
|
|
859
|
+
};
|
|
860
|
+
const BRAIN_LEARNING_FILETYPE_LABEL = {
|
|
861
|
+
'mistake-patterns': 'Mistake pattern',
|
|
862
|
+
'preferences': 'Preference',
|
|
863
|
+
'validated-patterns': 'Validated pattern',
|
|
864
|
+
'manager-coaching': 'Manager coaching',
|
|
865
|
+
};
|
|
866
|
+
const PENDING_KIND_LABEL = {
|
|
867
|
+
'coaching-moment': 'Coaching moment',
|
|
868
|
+
'retrospective': 'Retrospective',
|
|
869
|
+
};
|
|
870
|
+
// Parse `## [P-…] Title` entries out of a learning file WITH their effective
|
|
871
|
+
// score + last-seen (unlike parseLearningEntries, which drops that bookkeeping
|
|
872
|
+
// for the Hub text display). Used only by the brain-dot projection.
|
|
873
|
+
function parseBrainLearningEntries(filePath, fileType) {
|
|
874
|
+
if (!(0, fs_1.existsSync)(filePath))
|
|
875
|
+
return [];
|
|
876
|
+
let content;
|
|
877
|
+
try {
|
|
878
|
+
content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
const out = [];
|
|
884
|
+
const headingRe = learningEntryHeadingRegex();
|
|
885
|
+
const now = new Date();
|
|
886
|
+
let current = null;
|
|
887
|
+
const flush = () => {
|
|
888
|
+
if (!current)
|
|
889
|
+
return;
|
|
890
|
+
const rawScore = current.lastSeen
|
|
891
|
+
? computeEffectiveScore(current.severity, current.lastSeen, current.recurrences, fileType, now)
|
|
892
|
+
: null;
|
|
893
|
+
out.push({
|
|
894
|
+
title: current.title,
|
|
895
|
+
severity: current.severity,
|
|
896
|
+
score: rawScore === null ? null : Math.round(rawScore * 10) / 10,
|
|
897
|
+
lastSeen: current.lastSeen || null,
|
|
898
|
+
});
|
|
899
|
+
current = null;
|
|
900
|
+
};
|
|
901
|
+
for (const line of content.split(/\r?\n/)) {
|
|
902
|
+
const header = line.match(headingRe);
|
|
903
|
+
if (header) {
|
|
904
|
+
flush();
|
|
905
|
+
current = { severity: header[1], title: header[2].trim(), lastSeen: '', recurrences: 1 };
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (!current)
|
|
909
|
+
continue;
|
|
910
|
+
const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
|
|
911
|
+
if (lastSeenMatch) {
|
|
912
|
+
current.lastSeen = lastSeenMatch[1].trim();
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
|
|
916
|
+
if (recurrenceMatch) {
|
|
917
|
+
current.recurrences = parseInt(recurrenceMatch[1], 10);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
flush();
|
|
921
|
+
return out;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Build the brain learning-dot projection (issue #833) for one user: pending L0
|
|
925
|
+
* signals + processed L1 (personal) and L2 (org) learnings grouped by file type
|
|
926
|
+
* and mapped to a fixed brain region. Reuses the same root/user resolution as
|
|
927
|
+
* buildLearningContextSection so the dots line up with the auto-loaded context.
|
|
928
|
+
*/
|
|
929
|
+
function collectBrainLearningDots(workspaceRoot, userId) {
|
|
930
|
+
const roots = getLearningRoots(workspaceRoot);
|
|
931
|
+
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
932
|
+
const pendingSignals = collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots).map((s) => ({
|
|
933
|
+
type: s.kind,
|
|
934
|
+
typeLabel: PENDING_KIND_LABEL[s.kind],
|
|
935
|
+
fileName: s.displayPath.split('/').pop() || s.displayPath,
|
|
936
|
+
displayPath: s.displayPath,
|
|
937
|
+
}));
|
|
938
|
+
const resolvePersonal = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
|
|
939
|
+
const processed = [];
|
|
940
|
+
const fileTypes = ['mistake-patterns', 'preferences', 'validated-patterns', 'manager-coaching'];
|
|
941
|
+
for (const fileType of fileTypes) {
|
|
942
|
+
const region = exports.BRAIN_LEARNING_FILETYPE_TO_REGION[fileType];
|
|
943
|
+
const typeLabel = BRAIN_LEARNING_FILETYPE_LABEL[fileType];
|
|
944
|
+
// L1 personal
|
|
945
|
+
const personal = resolvePersonal(`${resolvedUserId}-${fileType}.md`);
|
|
946
|
+
const personalEntries = parseBrainLearningEntries(personal.path, fileType);
|
|
947
|
+
if (personalEntries.length) {
|
|
948
|
+
processed.push({ type: fileType, typeLabel, region, scope: 'personal', displayPath: personal.displayPath, entries: personalEntries });
|
|
949
|
+
}
|
|
950
|
+
// L2 org
|
|
951
|
+
const org = resolveOrgLearningFile(roots.repoLearningsBase, `org-${fileType}.md`);
|
|
952
|
+
const orgEntries = parseBrainLearningEntries(org.path, fileType);
|
|
953
|
+
if (orgEntries.length) {
|
|
954
|
+
processed.push({ type: fileType, typeLabel, region, scope: 'org', displayPath: org.displayPath, entries: orgEntries });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return { pending: { count: pendingSignals.length, signals: pendingSignals }, processed };
|
|
958
|
+
}
|
|
845
959
|
exports.CATEGORY_TO_FILETYPE = {
|
|
846
960
|
avoid: 'mistake-patterns',
|
|
847
961
|
preference: 'preferences',
|
|
@@ -63,7 +63,7 @@ class TelemetryMiddleware {
|
|
|
63
63
|
// session-active gate is exempted.
|
|
64
64
|
// Issue #701/#744 - hosted AI Hub HTTP APIs are browser/gateway routes,
|
|
65
65
|
// not MCP tool calls. They are still authenticated by API key/session.
|
|
66
|
-
const exemptPaths = ['/health', '/admin', '/api/registry/sync', '/api/registry/github-workflows', '/api/registry/topology/personalized', '/api/registry/topology/usage-heatmap', '/api/org/', '/api/manager/', '/api/ai-hub/', '/api/payment/', '/api/sales/', '/api/signup', '/api/providers', '/api/analytics', '/analytics', '/api/auth/', '/api/account/'];
|
|
66
|
+
const exemptPaths = ['/health', '/admin', '/api/registry/sync', '/api/registry/github-workflows', '/api/registry/topology/personalized', '/api/registry/topology/usage-heatmap', '/api/registry/topology/learnings-dots', '/api/org/', '/api/manager/', '/api/ai-hub/', '/api/payment/', '/api/sales/', '/api/signup', '/api/providers', '/api/analytics', '/analytics', '/api/auth/', '/api/account/'];
|
|
67
67
|
if (exemptPaths.some(p => req.path.startsWith(p))) {
|
|
68
68
|
return next();
|
|
69
69
|
}
|
|
@@ -4,7 +4,7 @@ exports.EmailService = void 0;
|
|
|
4
4
|
const resend_1 = require("resend");
|
|
5
5
|
class EmailService {
|
|
6
6
|
constructor() {
|
|
7
|
-
const apiKey = process.env.RESEND_API_KEY;
|
|
7
|
+
const apiKey = process.env.RESEND_API_KEY || (process.env.FRAIM_SUPPRESS_EMAILS === 'true' ? 'test-resend-key' : undefined);
|
|
8
8
|
if (!apiKey) {
|
|
9
9
|
throw new Error('RESEND_API_KEY environment variable is required');
|
|
10
10
|
}
|
|
@@ -13,16 +13,20 @@ class EmailService {
|
|
|
13
13
|
this.fromEmail = process.env.RESEND_FROM_EMAIL || 'FRAIM <onboarding@resend.dev>';
|
|
14
14
|
this.baseUrl = (process.env.BASE_URL || 'https://fraimworks.ai').replace(/\/$/, '');
|
|
15
15
|
}
|
|
16
|
+
static shouldSuppressEmail() {
|
|
17
|
+
return process.env.FRAIM_SUPPRESS_EMAILS === 'true'
|
|
18
|
+
|| (process.env.NODE_ENV === 'test' && process.env.ALLOW_REAL_EMAILS_IN_TEST !== 'true');
|
|
19
|
+
}
|
|
16
20
|
/** Wrapper around resend.emails.send — no-ops in test mode to avoid burning real API quota. */
|
|
17
21
|
async sendEmail(payload) {
|
|
18
|
-
if (
|
|
22
|
+
if (EmailService.shouldSuppressEmail()) {
|
|
19
23
|
console.log(`[TEST] Email suppressed — would send to ${Array.isArray(payload.to) ? payload.to.join(', ') : payload.to}: "${payload.subject}"`);
|
|
20
24
|
return { data: { id: 'test-suppressed' }, error: null, headers: null };
|
|
21
25
|
}
|
|
22
26
|
return this.sendWithResend(payload);
|
|
23
27
|
}
|
|
24
28
|
async sendWithResend(payload) {
|
|
25
|
-
if (
|
|
29
|
+
if (EmailService.shouldSuppressEmail()) {
|
|
26
30
|
console.log(`[TEST] Email suppressed - would send to ${Array.isArray(payload.to) ? payload.to.join(', ') : payload.to}: "${payload.subject}"`);
|
|
27
31
|
return { data: { id: 'test-suppressed' }, error: null, headers: null };
|
|
28
32
|
}
|