fraim-hub 2.0.204

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.
Files changed (120) hide show
  1. package/README.md +467 -0
  2. package/bin/fraim-hub.js +17 -0
  3. package/dist/src/ai-hub/brand-store.js +17 -0
  4. package/dist/src/ai-hub/catalog.js +412 -0
  5. package/dist/src/ai-hub/cert-store.js +70 -0
  6. package/dist/src/ai-hub/cli.js +212 -0
  7. package/dist/src/ai-hub/configured-agents.js +283 -0
  8. package/dist/src/ai-hub/conversation-store.js +428 -0
  9. package/dist/src/ai-hub/desktop-main.js +449 -0
  10. package/dist/src/ai-hub/hosts.js +1669 -0
  11. package/dist/src/ai-hub/hub-latest-version.js +52 -0
  12. package/dist/src/ai-hub/hub-launch-decision.js +57 -0
  13. package/dist/src/ai-hub/hub-runtime-file.js +43 -0
  14. package/dist/src/ai-hub/managed-browser.js +269 -0
  15. package/dist/src/ai-hub/manager-turns.js +64 -0
  16. package/dist/src/ai-hub/office-sideload.js +156 -0
  17. package/dist/src/ai-hub/openclaw-bridge.js +250 -0
  18. package/dist/src/ai-hub/preferences.js +201 -0
  19. package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
  20. package/dist/src/ai-hub/server.js +4013 -0
  21. package/dist/src/ai-hub/types.js +2 -0
  22. package/dist/src/ai-hub/url-safety.js +19 -0
  23. package/dist/src/api/admin/payments.js +33 -0
  24. package/dist/src/api/admin/sales-leads.js +21 -0
  25. package/dist/src/api/payment/create-session.js +338 -0
  26. package/dist/src/api/payment/dashboard-link.js +149 -0
  27. package/dist/src/api/payment/session-details.js +31 -0
  28. package/dist/src/api/payment/webhook.js +587 -0
  29. package/dist/src/api/sales/contact.js +44 -0
  30. package/dist/src/cli/api/get-provider-client.js +41 -0
  31. package/dist/src/cli/api/provider-client.js +107 -0
  32. package/dist/src/cli/commands/add-ide.js +462 -0
  33. package/dist/src/cli/fraim-hub.js +50 -0
  34. package/dist/src/cli/internal/device-flow-service.js +83 -0
  35. package/dist/src/cli/mcp/command-resolution.js +81 -0
  36. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
  37. package/dist/src/cli/mcp/ide-formats.js +317 -0
  38. package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
  39. package/dist/src/cli/mcp/mcp-server-registry.js +173 -0
  40. package/dist/src/cli/providers/local-provider-registry.js +165 -0
  41. package/dist/src/cli/providers/provider-registry.js +230 -0
  42. package/dist/src/cli/setup/claude-code-telemetry.js +59 -0
  43. package/dist/src/cli/setup/codex-local-config.js +37 -0
  44. package/dist/src/cli/setup/ide-detector.js +383 -0
  45. package/dist/src/cli/setup/ide-global-integration.js +100 -0
  46. package/dist/src/cli/setup/ide-invocation-surfaces.js +160 -0
  47. package/dist/src/cli/setup/mcp-config-generator.js +225 -0
  48. package/dist/src/cli/setup/provider-prompts.js +339 -0
  49. package/dist/src/cli/utils/managed-agent-paths.js +118 -0
  50. package/dist/src/cli/utils/script-sync-utils.js +221 -0
  51. package/dist/src/cli/utils/user-config.js +100 -0
  52. package/dist/src/cli/utils/version-utils.js +35 -0
  53. package/dist/src/config/ai-manager-hiring.js +121 -0
  54. package/dist/src/config/feature-flags.js +25 -0
  55. package/dist/src/config/persona-capability-bundles.js +277 -0
  56. package/dist/src/config/persona-hiring.js +270 -0
  57. package/dist/src/config/portfolio-slug-overrides.js +17 -0
  58. package/dist/src/config/pricing.js +37 -0
  59. package/dist/src/config/stripe.js +43 -0
  60. package/dist/src/core/brand-store.js +232 -0
  61. package/dist/src/core/quality-evidence.js +331 -0
  62. package/dist/src/core/utils/git-utils.js +194 -0
  63. package/dist/src/core/utils/ports.js +32 -0
  64. package/dist/src/core/utils/project-fraim-paths.js +220 -0
  65. package/dist/src/db/payment-repository.js +61 -0
  66. package/dist/src/first-run/types.js +93 -0
  67. package/dist/src/fraim/db-service.js +2409 -0
  68. package/dist/src/local-mcp-server/agent-token-prices.js +167 -0
  69. package/dist/src/local-mcp-server/learning-context-builder.js +975 -0
  70. package/dist/src/middleware/rate-limit.js +110 -0
  71. package/dist/src/models/payment.js +2 -0
  72. package/dist/src/routes/payment-routes.js +186 -0
  73. package/dist/src/routes/persona-catalog-routes.js +84 -0
  74. package/dist/src/services/dashboard-access.js +27 -0
  75. package/dist/src/services/email-service.js +951 -0
  76. package/dist/src/services/persona-entitlement-service.js +352 -0
  77. package/dist/src/services/workspace-identity.js +21 -0
  78. package/dist/src/types/analytics.js +2 -0
  79. package/dist/src/utils/payment-calculator.js +52 -0
  80. package/extensions/office-word/favicon.ico +0 -0
  81. package/extensions/office-word/icon-64.png +0 -0
  82. package/extensions/office-word/manifest.xml +33 -0
  83. package/extensions/office-word/taskpane.html +242 -0
  84. package/index.js +85 -0
  85. package/package.json +106 -0
  86. package/public/ai-hub/index.html +910 -0
  87. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  88. package/public/ai-hub/powerpoint-taskpane/index.html +236 -0
  89. package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
  90. package/public/ai-hub/review.css +444 -0
  91. package/public/ai-hub/script.js +11737 -0
  92. package/public/ai-hub/styles.css +5102 -0
  93. package/public/first-run/error-frame.js +100 -0
  94. package/public/first-run/index.html +35 -0
  95. package/public/first-run/script.js +739 -0
  96. package/public/first-run/styles.css +929 -0
  97. package/public/portfolio/ashley.html +523 -0
  98. package/public/portfolio/auditya.html +83 -0
  99. package/public/portfolio/banke.html +83 -0
  100. package/public/portfolio/beza.html +659 -0
  101. package/public/portfolio/careena.html +632 -0
  102. package/public/portfolio/casey.html +568 -0
  103. package/public/portfolio/celia.html +490 -0
  104. package/public/portfolio/deidre.html +642 -0
  105. package/public/portfolio/gautam.html +597 -0
  106. package/public/portfolio/hari.html +469 -0
  107. package/public/portfolio/huxley.html +1354 -0
  108. package/public/portfolio/index.html +741 -0
  109. package/public/portfolio/maestro.html +518 -0
  110. package/public/portfolio/mandy.html +590 -0
  111. package/public/portfolio/mona.html +597 -0
  112. package/public/portfolio/pam.html +887 -0
  113. package/public/portfolio/procella.html +107 -0
  114. package/public/portfolio/qasm.html +569 -0
  115. package/public/portfolio/ricardo.html +489 -0
  116. package/public/portfolio/sade.html +560 -0
  117. package/public/portfolio/sam.html +654 -0
  118. package/public/portfolio/sechar.html +580 -0
  119. package/public/portfolio/sreya.html +599 -0
  120. package/public/portfolio/swen.html +601 -0
@@ -0,0 +1,975 @@
1
+ "use strict";
2
+ /**
3
+ * LearningContextBuilder builds the learning context section for a user.
4
+ *
5
+ * Lives in the local proxy so learning files are always read from the correct
6
+ * workspace root on the user's machine.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.CATEGORY_TO_FILETYPE = exports.LEARNING_PRIORITIES = void 0;
10
+ exports.learningEntryHeadingRegex = learningEntryHeadingRegex;
11
+ exports.computeEffectiveScore = computeEffectiveScore;
12
+ exports.buildLearningContextSection = buildLearningContextSection;
13
+ exports.buildTeamContextSection = buildTeamContextSection;
14
+ exports.resolveTeamContextFiles = resolveTeamContextFiles;
15
+ exports.isTeamContextKey = isTeamContextKey;
16
+ exports.resolveTeamContextFile = resolveTeamContextFile;
17
+ exports.readOrgBrand = readOrgBrand;
18
+ exports.resolveOrgBrandWriteDir = resolveOrgBrandWriteDir;
19
+ exports.countPreservedLearnings = countPreservedLearnings;
20
+ exports.readPreservedLearnings = readPreservedLearnings;
21
+ exports.applyLearningEntryChange = applyLearningEntryChange;
22
+ exports.isTruthyFlag = isTruthyFlag;
23
+ const fs_1 = require("fs");
24
+ const path_1 = require("path");
25
+ const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
26
+ const brand_store_1 = require("../core/brand-store");
27
+ const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/learnings').replace(/\/$/, '');
28
+ const DEFAULT_THRESHOLD = 3.0;
29
+ const AGING_HORIZON_DAYS = 7;
30
+ const MAX_ENTRIES_SCANNED = 200;
31
+ const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
32
+ // ── Single source of truth for the learning-entry format contract (#533) ──────
33
+ // The `## [P-…] <title>` entry format is a tight contract shared by three sides:
34
+ // 1. EMIT — the synthesis jobs (sleep-on-learnings, organizational-learning-
35
+ // synthesis) instruct the agent to write entries in this shape.
36
+ // 2. SCORE — the decay scorer below reads severity + Last seen + Recurrences to
37
+ // decide which learnings stay active and get injected into agent context.
38
+ // 3. READ — the counters and the Hub (readPreservedLearnings) display them.
39
+ // These previously drifted (each accepted a different priority set / heading
40
+ // level), which silently dropped P-CRITICAL learnings. Everything now derives
41
+ // from LEARNING_PRIORITIES, and a build-time contract test
42
+ // (tests/isolated/test-learning-format-contract.ts) asserts all three sides agree.
43
+ exports.LEARNING_PRIORITIES = ['P-CRITICAL', 'P-HIGH', 'P-MED', 'P-LOW'];
44
+ /** Canonical entry-heading matcher: `##`..`######` + `[P-X]` + optional title. */
45
+ function learningEntryHeadingRegex(flags = '') {
46
+ return new RegExp(`^#{2,}\\s+\\[(${exports.LEARNING_PRIORITIES.join('|')})\\]\\s*(.*)$`, flags);
47
+ }
48
+ function getLearningRoots(workspaceRoot) {
49
+ return {
50
+ globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
51
+ globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
52
+ managerCacheBase: (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', 'learnings'),
53
+ managerCacheDisplayBase: (0, project_fraim_paths_1.getUserFraimDisplayPath)('manager/learnings'),
54
+ repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
55
+ };
56
+ }
57
+ function resolvePersonalLearningFile(repoBase, managerCacheBase, managerCacheDisplayBase, globalBase, globalDisplayBase, fileName) {
58
+ const repoPath = (0, path_1.join)(repoBase, fileName);
59
+ if ((0, fs_1.existsSync)(repoPath)) {
60
+ return {
61
+ present: true,
62
+ path: repoPath,
63
+ displayPath: `${REPO_LEARNINGS_REL}/${fileName}`
64
+ };
65
+ }
66
+ const globalPath = (0, path_1.join)(globalBase, fileName);
67
+ if ((0, fs_1.existsSync)(globalPath)) {
68
+ return {
69
+ present: true,
70
+ path: globalPath,
71
+ displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}`
72
+ };
73
+ }
74
+ const managerCachePath = (0, path_1.join)(managerCacheBase, fileName);
75
+ if ((0, fs_1.existsSync)(managerCachePath)) {
76
+ return {
77
+ present: true,
78
+ path: managerCachePath,
79
+ displayPath: `${managerCacheDisplayBase.replace(/\/$/, '')}/${fileName}`
80
+ };
81
+ }
82
+ return { present: false, path: globalPath, displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}` };
83
+ }
84
+ function buildUserIdCandidates(userId) {
85
+ const candidates = new Set();
86
+ const trimmed = userId.trim();
87
+ if (trimmed)
88
+ candidates.add(trimmed);
89
+ const atIndex = trimmed.indexOf('@');
90
+ if (atIndex > 0)
91
+ candidates.add(trimmed.slice(0, atIndex));
92
+ return Array.from(candidates);
93
+ }
94
+ function countMatchingFilesByPrefix(dirPath, matcher) {
95
+ if (!(0, fs_1.existsSync)(dirPath))
96
+ return 0;
97
+ try {
98
+ return (0, fs_1.readdirSync)(dirPath).filter(matcher).length;
99
+ }
100
+ catch {
101
+ return 0;
102
+ }
103
+ }
104
+ function collectAvailableUserPrefixes(workspaceRoot, personalRoots) {
105
+ const prefixes = new Set();
106
+ const collect = (dirPath, extractor) => {
107
+ if (!(0, fs_1.existsSync)(dirPath))
108
+ return;
109
+ try {
110
+ for (const fileName of (0, fs_1.readdirSync)(dirPath)) {
111
+ const prefix = extractor(fileName);
112
+ if (prefix)
113
+ prefixes.add(prefix);
114
+ }
115
+ }
116
+ catch {
117
+ // Ignore unreadable directories.
118
+ }
119
+ };
120
+ for (const personalRoot of personalRoots) {
121
+ collect(personalRoot, (fileName) => {
122
+ if (!fileName.endsWith('.md') || fileName.startsWith('org-'))
123
+ return null;
124
+ const match = fileName.match(/^(.*?)-(preferences|manager-coaching|mistake-patterns|validated-patterns)\.md$/);
125
+ return match ? match[1] : null;
126
+ });
127
+ }
128
+ collect((0, path_1.join)(personalRoots[0], 'raw'), (fileName) => {
129
+ const match = fileName.match(/^(.*?)-\d{4}-\d{2}-\d{2}-.*\.md$/);
130
+ return match ? match[1] : null;
131
+ });
132
+ collect((0, path_1.join)(workspaceRoot, 'docs', 'retrospectives'), (fileName) => {
133
+ const match = fileName.match(/^(.*?)-\d{4}-\d{2}-\d{2}-.*\.md$/);
134
+ return match ? match[1] : null;
135
+ });
136
+ return prefixes;
137
+ }
138
+ function resolveLearningUserId(workspaceRoot, userId, roots) {
139
+ const candidates = buildUserIdCandidates(userId);
140
+ let bestCandidate = candidates[0] || userId;
141
+ let bestScore = -1;
142
+ for (const candidate of candidates) {
143
+ const score = ((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-preferences.md`)) ? 1 : 0) +
144
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
145
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
146
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.globalPersonalBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
147
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-preferences.md`)) ? 1 : 0) +
148
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
149
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
150
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.managerCacheBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
151
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-preferences.md`)) ? 1 : 0) +
152
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-manager-coaching.md`)) ? 1 : 0) +
153
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-mistake-patterns.md`)) ? 1 : 0) +
154
+ ((0, fs_1.existsSync)((0, path_1.join)(roots.repoLearningsBase, `${candidate}-validated-patterns.md`)) ? 1 : 0) +
155
+ countMatchingFilesByPrefix((0, path_1.join)(roots.repoLearningsBase, 'raw'), (fileName) => fileName.startsWith(`${candidate}-`)) +
156
+ countMatchingFilesByPrefix((0, path_1.join)(workspaceRoot, 'docs', 'retrospectives'), (fileName) => fileName.startsWith(`${candidate}-`) && fileName.endsWith('.md'));
157
+ if (score > bestScore) {
158
+ bestCandidate = candidate;
159
+ bestScore = score;
160
+ }
161
+ }
162
+ if (bestScore > 0)
163
+ return bestCandidate;
164
+ const availablePrefixes = collectAvailableUserPrefixes(workspaceRoot, [roots.repoLearningsBase, roots.managerCacheBase, roots.globalPersonalBase]);
165
+ if (availablePrefixes.size === 1) {
166
+ return Array.from(availablePrefixes)[0];
167
+ }
168
+ return bestCandidate;
169
+ }
170
+ function readWorkspaceConfig(workspaceRoot) {
171
+ try {
172
+ const configPath = (0, project_fraim_paths_1.getWorkspaceConfigPath)(workspaceRoot);
173
+ if ((0, fs_1.existsSync)(configPath)) {
174
+ return JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
175
+ }
176
+ }
177
+ catch {
178
+ // Fall through.
179
+ }
180
+ return null;
181
+ }
182
+ function getScoreThreshold(workspaceRoot) {
183
+ const config = readWorkspaceConfig(workspaceRoot);
184
+ const t = config?.learning?.scoreThreshold;
185
+ if (typeof t === 'number' && t > 0)
186
+ return t;
187
+ return DEFAULT_THRESHOLD;
188
+ }
189
+ /**
190
+ * Effective score for an L1 learning entry. The aging-risk count below uses
191
+ * this same decay model with `now` shifted forward.
192
+ *
193
+ * @param now Optional override for "now" (for forward-looking aging-risk
194
+ * calculations). Defaults to the current wall clock.
195
+ */
196
+ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()) {
197
+ const baseScore = severity === 'P-CRITICAL' ? 10 : severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
198
+ // Mistake patterns decay faster (90d) — they're tied to environments that change.
199
+ // Preferences, manager-coaching, and validated-patterns express durable judgment (180d half-life).
200
+ const halfLife = fileType === 'mistake-patterns' ? 90 : 180;
201
+ let daysSinceLastSeen = 0;
202
+ try {
203
+ const lastSeen = new Date(lastSeenDate);
204
+ daysSinceLastSeen = Math.max(0, (now.getTime() - lastSeen.getTime()) / (1000 * 60 * 60 * 24));
205
+ }
206
+ catch {
207
+ // Treat as fresh.
208
+ }
209
+ const decay = Math.pow(0.5, daysSinceLastSeen / halfLife);
210
+ const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
211
+ return baseScore * decay * recurrenceBoost;
212
+ }
213
+ function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns') {
214
+ const empty = { active: 0, dormant: 0, agingRisk: 0 };
215
+ if (!(0, fs_1.existsSync)(filePath))
216
+ return empty;
217
+ let content;
218
+ try {
219
+ content = (0, fs_1.readFileSync)(filePath, 'utf8');
220
+ }
221
+ catch {
222
+ return empty;
223
+ }
224
+ const lines = content.split(/\r?\n/);
225
+ const now = new Date();
226
+ const horizon = new Date(now.getTime() + AGING_HORIZON_DAYS * 86_400_000);
227
+ let active = 0;
228
+ let dormant = 0;
229
+ let agingRisk = 0;
230
+ let scanned = 0;
231
+ let inEntry = false;
232
+ let severity = null;
233
+ let lastSeen = '';
234
+ let recurrences = 1;
235
+ const flush = () => {
236
+ if (!severity)
237
+ return;
238
+ scanned++;
239
+ const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now);
240
+ if (today >= threshold) {
241
+ active++;
242
+ if (lastSeen) {
243
+ const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon);
244
+ if (future < threshold)
245
+ agingRisk++;
246
+ }
247
+ }
248
+ else {
249
+ dormant++;
250
+ }
251
+ };
252
+ for (const line of lines) {
253
+ if (scanned >= MAX_ENTRIES_SCANNED)
254
+ break;
255
+ const headerMatch = line.match(learningEntryHeadingRegex());
256
+ if (headerMatch) {
257
+ flush();
258
+ inEntry = true;
259
+ severity = headerMatch[1];
260
+ lastSeen = '';
261
+ recurrences = 1;
262
+ continue;
263
+ }
264
+ if (!inEntry)
265
+ continue;
266
+ const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
267
+ if (lastSeenMatch) {
268
+ lastSeen = lastSeenMatch[1].trim();
269
+ continue;
270
+ }
271
+ const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
272
+ if (recurrenceMatch) {
273
+ recurrences = parseInt(recurrenceMatch[1], 10);
274
+ }
275
+ }
276
+ flush();
277
+ return { active, dormant, agingRisk };
278
+ }
279
+ function readFrontmatter(content) {
280
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
281
+ if (!match)
282
+ return {};
283
+ const frontmatter = {};
284
+ for (const line of match[1].split(/\r?\n/)) {
285
+ const separator = line.indexOf(':');
286
+ if (separator === -1)
287
+ continue;
288
+ const key = line.slice(0, separator).trim();
289
+ const value = line.slice(separator + 1).trim();
290
+ if (key) {
291
+ frontmatter[key] = value;
292
+ }
293
+ }
294
+ return frontmatter;
295
+ }
296
+ function isRetrospectiveSynthesized(value) {
297
+ if (value === undefined)
298
+ return false;
299
+ const normalized = value.trim().toLowerCase();
300
+ if (!normalized)
301
+ return false;
302
+ return normalized !== 'false' && normalized !== 'no' && normalized !== '0' && normalized !== 'null';
303
+ }
304
+ function isUnsynthesizedRetrospective(filePath) {
305
+ try {
306
+ const content = (0, fs_1.readFileSync)(filePath, 'utf8');
307
+ const frontmatter = readFrontmatter(content);
308
+ return !isRetrospectiveSynthesized(frontmatter.synthesized);
309
+ }
310
+ catch {
311
+ return false;
312
+ }
313
+ }
314
+ function pendingL0SortKey(fileName) {
315
+ const timestamp = fileName.match(/\d{4}-\d{2}-\d{2}(?:T\d{2}-\d{2}-\d{2})?/);
316
+ return timestamp?.[0] || fileName;
317
+ }
318
+ function collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots) {
319
+ const sources = [];
320
+ const rawPath = (0, path_1.join)(roots.repoLearningsBase, 'raw');
321
+ if ((0, fs_1.existsSync)(rawPath)) {
322
+ try {
323
+ for (const fileName of (0, fs_1.readdirSync)(rawPath)) {
324
+ if (!fileName.startsWith(`${resolvedUserId}-`))
325
+ continue;
326
+ sources.push({
327
+ kind: 'coaching-moment',
328
+ displayPath: `${REPO_LEARNINGS_REL}/raw/${fileName}`,
329
+ sortKey: pendingL0SortKey(fileName)
330
+ });
331
+ }
332
+ }
333
+ catch {
334
+ // Ignore read failures.
335
+ }
336
+ }
337
+ const retrospectivesPath = (0, path_1.join)(workspaceRoot, 'docs', 'retrospectives');
338
+ if ((0, fs_1.existsSync)(retrospectivesPath)) {
339
+ try {
340
+ for (const fileName of (0, fs_1.readdirSync)(retrospectivesPath)) {
341
+ if (!fileName.startsWith(`${resolvedUserId}-`) || !fileName.endsWith('.md'))
342
+ continue;
343
+ if (!isUnsynthesizedRetrospective((0, path_1.join)(retrospectivesPath, fileName)))
344
+ continue;
345
+ sources.push({
346
+ kind: 'retrospective',
347
+ displayPath: `docs/retrospectives/${fileName}`,
348
+ sortKey: pendingL0SortKey(fileName)
349
+ });
350
+ }
351
+ }
352
+ catch {
353
+ // Ignore read failures.
354
+ }
355
+ }
356
+ return sources.sort((a, b) => b.sortKey.localeCompare(a.sortKey) || a.displayPath.localeCompare(b.displayPath));
357
+ }
358
+ /**
359
+ * Resolve an L2 org-scope learning file (issue #563): a repo-local override
360
+ * (`fraim/personalized-employee/learnings/`) wins, then the synced org cache
361
+ * (`~/.fraim/org/learnings/`). Org learnings are part of the shared org pack,
362
+ * so a synced copy must be readable by the agent even with no repo-local copy.
363
+ * Mirrors the org context/rules resolution order. Returns `present:false` with
364
+ * the repo path/display when neither tier has the file.
365
+ */
366
+ function resolveOrgLearningFile(repoLearningsBase, fileName) {
367
+ const repoPath = (0, path_1.join)(repoLearningsBase, fileName);
368
+ if ((0, fs_1.existsSync)(repoPath)) {
369
+ return { present: true, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
370
+ }
371
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', 'learnings', fileName);
372
+ if ((0, fs_1.existsSync)(cachePath)) {
373
+ return { present: true, path: cachePath, displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`) };
374
+ }
375
+ return { present: false, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
376
+ }
377
+ function buildLearningContextSection(workspaceRoot, userId, forJob) {
378
+ const roots = getLearningRoots(workspaceRoot);
379
+ const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
380
+ const threshold = getScoreThreshold(workspaceRoot);
381
+ // L2 org learnings resolve repo-local first, then the synced org cache (#563).
382
+ const l2Mistake = resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md');
383
+ const l2Pref = resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md');
384
+ const l2Coach = resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md');
385
+ const l2Validated = resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md');
386
+ const l2MistakePresent = l2Mistake.present;
387
+ const l2PrefPresent = l2Pref.present;
388
+ const l2CoachPresent = l2Coach.present;
389
+ const l2ValidatedPresent = l2Validated.present;
390
+ const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2Mistake.path, threshold, 'mistake-patterns') : null;
391
+ const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2Validated.path, threshold, 'validated-patterns') : null;
392
+ const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
393
+ const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
394
+ const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
395
+ const l1Validated = resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-validated-patterns.md`);
396
+ const l1MistakeStats = l1Mistake.present ? scanMistakePatternFile(l1Mistake.path, threshold, 'mistake-patterns') : null;
397
+ const l1ValidatedStats = l1Validated.present ? scanMistakePatternFile(l1Validated.path, threshold, 'validated-patterns') : null;
398
+ const pendingL0Sources = collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots);
399
+ const l0CoachingCount = pendingL0Sources.filter(source => source.kind === 'coaching-moment').length;
400
+ const l0RetroCount = pendingL0Sources.filter(source => source.kind === 'retrospective').length;
401
+ const hasL2 = l2MistakePresent || l2PrefPresent || l2CoachPresent || l2ValidatedPresent;
402
+ const hasL1 = l1Mistake.present || l1Pref.present || l1Coach.present || l1Validated.present;
403
+ const hasContent = hasL2 || hasL1 || l0CoachingCount > 0 || l0RetroCount > 0;
404
+ if (!hasContent)
405
+ return '';
406
+ let section = forJob
407
+ ? '\n\n## Learning Context for This Job\n\n'
408
+ : '\n\n## Learning Context\n\n';
409
+ if (hasL2) {
410
+ section += '### L2 - Org patterns\n';
411
+ if (l2MistakePresent)
412
+ section += `\`${l2Mistake.displayPath}\` (entries above score threshold)\n`;
413
+ if (l2PrefPresent)
414
+ section += `\`${l2Pref.displayPath}\` (all entries)\n`;
415
+ if (l2CoachPresent)
416
+ section += `\`${l2Coach.displayPath}\` (manager-facing; all entries)\n`;
417
+ if (l2ValidatedPresent)
418
+ section += `\`${l2Validated.displayPath}\` (entries above score threshold)\n`;
419
+ const l2DormantTotal = (l2MistakeStats?.dormant || 0) + (l2ValidatedStats?.dormant || 0);
420
+ if (l2DormantTotal > 0) {
421
+ section += `Dormant: ${l2DormantTotal} org pattern${l2DormantTotal !== 1 ? 's' : ''} below threshold\n`;
422
+ }
423
+ section += '\n';
424
+ }
425
+ if (hasL1) {
426
+ section += '### L1 - Your patterns\n';
427
+ if (l1Pref.present)
428
+ section += `\`${l1Pref.displayPath}\` (all entries)\n`;
429
+ if (l1Coach.present)
430
+ section += `\`${l1Coach.displayPath}\` (manager-facing; all entries)\n`;
431
+ if (l1Mistake.present)
432
+ section += `\`${l1Mistake.displayPath}\` (entries above score threshold)\n`;
433
+ if (l1Validated.present)
434
+ section += `\`${l1Validated.displayPath}\` (entries above score threshold)\n`;
435
+ const l1DormantTotal = (l1MistakeStats?.dormant || 0) + (l1ValidatedStats?.dormant || 0);
436
+ if (l1DormantTotal > 0) {
437
+ section += `Dormant: ${l1DormantTotal} personal pattern${l1DormantTotal !== 1 ? 's' : ''} below threshold\n`;
438
+ }
439
+ section += '\n';
440
+ }
441
+ if (l0CoachingCount > 0 || l0RetroCount > 0) {
442
+ const totalL0 = pendingL0Sources.length;
443
+ section += '### L0 - Your unprocessed signals\n';
444
+ if (l0CoachingCount > 0) {
445
+ section += `${l0CoachingCount} coaching moment${l0CoachingCount !== 1 ? 's' : ''} in \`${REPO_LEARNINGS_REL}/raw/${resolvedUserId}-*\`\n`;
446
+ }
447
+ if (l0RetroCount > 0) {
448
+ section += `${l0RetroCount} retrospective${l0RetroCount !== 1 ? 's' : ''} in \`docs/retrospectives/${resolvedUserId}-*\` with unsynthesized or missing \`synthesized\` frontmatter\n`;
449
+ }
450
+ section += 'Pending L0 Source Files:\n';
451
+ for (const source of pendingL0Sources) {
452
+ section += `- \`${source.displayPath}\`\n`;
453
+ }
454
+ section += 'Read these pending L0 source files before continuing; they are unsynthesized learnings for the current agent.\n';
455
+ if (totalL0 >= L0_SLEEP_ON_LEARNINGS_PROMPT_MIN) {
456
+ section += `This is ${totalL0} pending L0 file${totalL0 !== 1 ? 's' : ''}; to speed up future starts, run \`sleep-on-learnings\` to synthesize or archive them.\n`;
457
+ }
458
+ section += '\n';
459
+ }
460
+ if (forJob) {
461
+ if (hasL2 || hasL1) {
462
+ section += 'Use the relevant patterns and preferences in this job.\n';
463
+ if (l1Coach.present || l2CoachPresent) {
464
+ section += 'Treat manager-coaching as feedback for how the manager should continue or improve managing AI, not as agent instruction.\n';
465
+ }
466
+ }
467
+ }
468
+ else {
469
+ section += 'Use this synthesized learning context throughout the session.\n';
470
+ if (l1Coach.present || l2CoachPresent) {
471
+ section += 'Manager-coaching entries are manager-facing feedback, not instructions for the AI to follow.\n';
472
+ }
473
+ }
474
+ return section;
475
+ }
476
+ /**
477
+ * Resolve an organization/manager-scope context file (issue #563 order, R3.1):
478
+ * 1. repo-local override (`fraim/personalized-employee/…`) — wins when present
479
+ * 2. managed org cache (`~/.fraim/org/…`) — the synced shared org layer;
480
+ * eligible for org files only, never manager files (R3.3)
481
+ * 3. legacy machine-local (`~/.fraim/personalized-employee/…`)
482
+ */
483
+ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = true) {
484
+ try {
485
+ const repoPath = (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', relativePath);
486
+ if ((0, fs_1.existsSync)(repoPath)) {
487
+ return {
488
+ present: true,
489
+ displayPath: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/${relativePath}`)
490
+ };
491
+ }
492
+ if (orgCacheEligible) {
493
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
494
+ if ((0, fs_1.existsSync)(cachePath)) {
495
+ return {
496
+ present: true,
497
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`)
498
+ };
499
+ }
500
+ }
501
+ if (!orgCacheEligible) {
502
+ const managerCachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', relativePath);
503
+ if ((0, fs_1.existsSync)(managerCachePath)) {
504
+ return {
505
+ present: true,
506
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`)
507
+ };
508
+ }
509
+ }
510
+ const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
511
+ if ((0, fs_1.existsSync)(userPath)) {
512
+ return {
513
+ present: true,
514
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`personalized-employee/${relativePath}`)
515
+ };
516
+ }
517
+ }
518
+ catch {
519
+ // Fall through to absent.
520
+ }
521
+ return { present: false, displayPath: '' };
522
+ }
523
+ /**
524
+ * Resolve a project-scope context file. These are repo-local only — they
525
+ * describe a single engagement and never layer up to the user level.
526
+ */
527
+ function resolveProjectContextFile(workspaceRoot, relativePath) {
528
+ try {
529
+ const repoPath = (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', relativePath);
530
+ if ((0, fs_1.existsSync)(repoPath)) {
531
+ return {
532
+ present: true,
533
+ displayPath: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/${relativePath}`)
534
+ };
535
+ }
536
+ }
537
+ catch {
538
+ // Fall through to absent.
539
+ }
540
+ return { present: false, displayPath: '' };
541
+ }
542
+ /**
543
+ * Builds the team-context section pointing the agent at the org/manager/project
544
+ * context files that exist on disk (issue #512, three-layer Context
545
+ * Architecture). Mirrors `buildLearningContextSection`: pure, side-effect-free,
546
+ * lists only present files, and returns '' when none exist.
547
+ */
548
+ function buildTeamContextSection(workspaceRoot, forJob) {
549
+ // Organization layer: repo override, then org cache, then legacy machine-local.
550
+ // Manager files are personal and never resolve from the shared org cache (R3.3).
551
+ const orgContext = resolveOrgContextFile(workspaceRoot, 'context/org_context.md');
552
+ const managerContext = resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false);
553
+ const orgRules = resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md');
554
+ const managerRules = resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false);
555
+ // Project layer (repo-local only).
556
+ const projectContext = resolveProjectContextFile(workspaceRoot, 'context/project_context.md');
557
+ const projectBrief = resolveProjectContextFile(workspaceRoot, 'context/project_brief.md');
558
+ const projectRules = resolveProjectContextFile(workspaceRoot, 'rules/project_rules.md');
559
+ const projectQa = resolveProjectContextFile(workspaceRoot, 'context/project_qa.md');
560
+ const hasOrg = orgContext.present || managerContext.present || orgRules.present || managerRules.present;
561
+ const hasProject = projectContext.present || projectBrief.present || projectRules.present || projectQa.present;
562
+ if (!hasOrg && !hasProject)
563
+ return '';
564
+ let section = forJob
565
+ ? '\n\n## Team Context for This Job\n\n'
566
+ : '\n\n## Team Context\n\n';
567
+ if (hasOrg) {
568
+ section += '### Organization\n';
569
+ if (orgContext.present)
570
+ section += `\`${orgContext.displayPath}\`\n`;
571
+ if (managerContext.present)
572
+ section += `\`${managerContext.displayPath}\`\n`;
573
+ if (orgRules.present)
574
+ section += `\`${orgRules.displayPath}\`\n`;
575
+ if (managerRules.present)
576
+ section += `\`${managerRules.displayPath}\`\n`;
577
+ section += '\n';
578
+ }
579
+ if (hasProject) {
580
+ section += '### Project\n';
581
+ if (projectContext.present)
582
+ section += `\`${projectContext.displayPath}\`\n`;
583
+ if (projectBrief.present)
584
+ section += `\`${projectBrief.displayPath}\`\n`;
585
+ if (projectRules.present)
586
+ section += `\`${projectRules.displayPath}\`\n`;
587
+ if (projectQa.present)
588
+ section += `\`${projectQa.displayPath}\`\n`;
589
+ section += '\n';
590
+ }
591
+ section += 'Read the relevant context before doing work.\n';
592
+ return section;
593
+ }
594
+ /**
595
+ * Resolve presence + display paths for every three-layer Team Context file
596
+ * (org/manager/project). Reuses resolveOrgContextFile / resolveProjectContextFile
597
+ * so layering (repo-local shadows user-level for org/manager; project is
598
+ * repo-local only) matches buildTeamContextSection exactly.
599
+ */
600
+ function resolveTeamContextFiles(workspaceRoot) {
601
+ return {
602
+ orgContext: resolveOrgContextFile(workspaceRoot, 'context/org_context.md'),
603
+ managerContext: resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false),
604
+ orgRules: resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md'),
605
+ managerRules: resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false),
606
+ projectContext: resolveProjectContextFile(workspaceRoot, 'context/project_context.md'),
607
+ projectBrief: resolveProjectContextFile(workspaceRoot, 'context/project_brief.md'),
608
+ projectQa: resolveProjectContextFile(workspaceRoot, 'context/project_qa.md'),
609
+ projectRules: resolveProjectContextFile(workspaceRoot, 'rules/project_rules.md')
610
+ };
611
+ }
612
+ /** Map a key to its (relativePath, scope). Single source of truth. */
613
+ const TEAM_CONTEXT_FILE_MAP = {
614
+ org: { relativePath: 'context/org_context.md', scope: 'org' },
615
+ manager: { relativePath: 'context/manager_context.md', scope: 'org' },
616
+ orgRules: { relativePath: 'rules/org_rules.md', scope: 'org' },
617
+ managerRules: { relativePath: 'rules/manager_rules.md', scope: 'org' },
618
+ projectContext: { relativePath: 'context/project_context.md', scope: 'project' },
619
+ projectBrief: { relativePath: 'context/project_brief.md', scope: 'project' },
620
+ projectRules: { relativePath: 'rules/project_rules.md', scope: 'project' },
621
+ projectQa: { relativePath: 'context/project_qa.md', scope: 'project' }
622
+ };
623
+ function isTeamContextKey(value) {
624
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(TEAM_CONTEXT_FILE_MAP, value);
625
+ }
626
+ /**
627
+ * Resolve the read + write locations for one editable context file.
628
+ *
629
+ * - Org scope (org/manager/orgRules/managerRules): a repo-local copy under `fraim/…` wins for
630
+ * both read and write when present (so the edit updates the file that actually
631
+ * shadows user-level for this repo); otherwise read AND write target the
632
+ * user-level `~/.fraim/…` path (where org onboarding writes).
633
+ * - Project scope (projectContext/projectBrief/projectRules/projectQa): repo-local only.
634
+ */
635
+ function resolveTeamContextFile(workspaceRoot, key) {
636
+ const { relativePath, scope } = TEAM_CONTEXT_FILE_MAP[key];
637
+ const repoPath = (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', relativePath);
638
+ const repoDisplay = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/${relativePath}`);
639
+ if (scope === 'project') {
640
+ // Repo-local only — read and write are the same path.
641
+ return {
642
+ present: (0, fs_1.existsSync)(repoPath),
643
+ readPath: (0, fs_1.existsSync)(repoPath) ? repoPath : '',
644
+ writePath: repoPath,
645
+ displayPath: repoDisplay,
646
+ scope
647
+ };
648
+ }
649
+ // Org scope: repo-local override wins when present.
650
+ if ((0, fs_1.existsSync)(repoPath)) {
651
+ return {
652
+ present: true,
653
+ readPath: repoPath,
654
+ writePath: repoPath,
655
+ displayPath: repoDisplay,
656
+ scope
657
+ };
658
+ }
659
+ // Synced org cache (org files only, never manager files — R3.3). Reads
660
+ // resolve here, but the cache is managed content: it is never a write
661
+ // destination (R5.1), so writePath stays empty and the caller must route
662
+ // edits through the org backend's propose-and-approve flow.
663
+ if (key === 'org' || key === 'orgRules') {
664
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
665
+ if ((0, fs_1.existsSync)(cachePath)) {
666
+ return {
667
+ present: true,
668
+ readPath: cachePath,
669
+ writePath: '',
670
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`),
671
+ scope,
672
+ managedByOrgSync: true
673
+ };
674
+ }
675
+ }
676
+ if (key === 'manager' || key === 'managerRules') {
677
+ const managerCachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', relativePath);
678
+ if ((0, fs_1.existsSync)(managerCachePath)) {
679
+ return {
680
+ present: true,
681
+ readPath: managerCachePath,
682
+ writePath: '',
683
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`),
684
+ scope,
685
+ managedByManagerSync: true
686
+ };
687
+ }
688
+ }
689
+ const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
690
+ const userDisplay = (0, project_fraim_paths_1.getUserFraimDisplayPath)(`personalized-employee/${relativePath}`);
691
+ return {
692
+ present: (0, fs_1.existsSync)(userPath),
693
+ readPath: (0, fs_1.existsSync)(userPath) ? userPath : '',
694
+ // No repo-local override exists → write to the portable user-level path,
695
+ // matching where org onboarding persists these files.
696
+ writePath: userPath,
697
+ displayPath: userDisplay,
698
+ scope
699
+ };
700
+ }
701
+ // ---------------------------------------------------------------------------
702
+ // Issue #744 — Org brand (Hub cobranding).
703
+ //
704
+ // The brand descriptor (company name/logo/color) is stored ALONGSIDE company
705
+ // info, in the SAME org context storage as org_context.md (manager directive,
706
+ // spec R7.4), as `context/org_brand.json`. Read resolves with the same org-scope
707
+ // layering as org context (repo-local override → synced org cache → user-level);
708
+ // writes target the same writable location org context edits use.
709
+ // ---------------------------------------------------------------------------
710
+ /** Ordered directories to look for org_brand.json, mirroring org-context layering. */
711
+ function orgBrandCandidateDirs(workspaceRoot) {
712
+ return [
713
+ (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', 'context'),
714
+ (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', 'context'),
715
+ (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'context'),
716
+ ];
717
+ }
718
+ /** Read the resolved org brand, or null when none is set on this machine. */
719
+ function readOrgBrand(workspaceRoot) {
720
+ for (const dir of orgBrandCandidateDirs(workspaceRoot)) {
721
+ const brand = (0, brand_store_1.readBrandFromDir)(dir);
722
+ if (brand)
723
+ return brand;
724
+ }
725
+ return null;
726
+ }
727
+ /**
728
+ * Directory an org-brand edit should be written to. Mirrors org-context write
729
+ * resolution: the repo-local override dir when the repo carries its own org
730
+ * context, otherwise the portable user-level context dir (where org onboarding
731
+ * writes). Never the synced org cache (managed, overwritten on sync).
732
+ */
733
+ function resolveOrgBrandWriteDir(workspaceRoot) {
734
+ const loc = resolveTeamContextFile(workspaceRoot, 'org');
735
+ if (loc.writePath)
736
+ return (0, path_1.dirname)(loc.writePath);
737
+ return (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'context');
738
+ }
739
+ /**
740
+ * Count the number of `## [P-...]`-style entries inside a preserved learning
741
+ * file. Returns 0 when the file is absent or unreadable. Counts ALL entries
742
+ * (not score-gated) — the Brain summary reports preserved learnings, not the
743
+ * subset above the auto-load threshold.
744
+ */
745
+ function countLearningEntries(filePath) {
746
+ if (!(0, fs_1.existsSync)(filePath))
747
+ return 0;
748
+ let content;
749
+ try {
750
+ content = (0, fs_1.readFileSync)(filePath, 'utf8');
751
+ }
752
+ catch {
753
+ return 0;
754
+ }
755
+ let count = 0;
756
+ const headingRe = learningEntryHeadingRegex();
757
+ for (const line of content.split(/\r?\n/)) {
758
+ if (headingRe.test(line))
759
+ count++;
760
+ }
761
+ return count;
762
+ }
763
+ /**
764
+ * Count preserved learnings by scope for the Brain summary (R14). Organization
765
+ * = L2 org-* files; manager = the personal manager-coaching file (reverse
766
+ * mentoring); project = personal mistake/preferences/validated patterns. Raw =
767
+ * un-dismissed L0 signals still awaiting synthesis. Reuses the same root/file
768
+ * resolution as buildLearningContextSection so the counts line up with what the
769
+ * agent actually auto-loads.
770
+ */
771
+ function countPreservedLearnings(workspaceRoot, userId) {
772
+ const roots = getLearningRoots(workspaceRoot);
773
+ const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
774
+ // L2 organization-scope preserved files: repo-local override, then the
775
+ // synced org cache (#563), matching what buildLearningContextSection injects.
776
+ const organization = countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md').path) +
777
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md').path) +
778
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md').path) +
779
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md').path);
780
+ const resolve = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
781
+ // L1 manager-facing reverse-mentoring file.
782
+ const manager = countLearningEntries(resolve(`${resolvedUserId}-manager-coaching.md`).path);
783
+ // L1 personal work patterns (project scope).
784
+ const project = countLearningEntries(resolve(`${resolvedUserId}-mistake-patterns.md`).path) +
785
+ countLearningEntries(resolve(`${resolvedUserId}-preferences.md`).path) +
786
+ countLearningEntries(resolve(`${resolvedUserId}-validated-patterns.md`).path);
787
+ // L0 raw signals still awaiting synthesis (not dismissed).
788
+ let rawSignals = 0;
789
+ const rawDir = (0, path_1.join)(roots.repoLearningsBase, 'raw');
790
+ if ((0, fs_1.existsSync)(rawDir)) {
791
+ try {
792
+ for (const fileName of (0, fs_1.readdirSync)(rawDir)) {
793
+ if (!fileName.endsWith('.md'))
794
+ continue;
795
+ const fm = readFrontmatter((0, fs_1.readFileSync)((0, path_1.join)(rawDir, fileName), 'utf8'));
796
+ if (isTruthyFlag(fm.dismissed))
797
+ continue;
798
+ rawSignals++;
799
+ }
800
+ }
801
+ catch {
802
+ // ignore unreadable raw dir
803
+ }
804
+ }
805
+ return { organization, manager, project, rawSignals };
806
+ }
807
+ exports.CATEGORY_TO_FILETYPE = {
808
+ avoid: 'mistake-patterns',
809
+ preference: 'preferences',
810
+ repeat: 'validated-patterns',
811
+ coaching: 'manager-coaching',
812
+ };
813
+ const FILETYPE_TO_CATEGORY = {
814
+ 'mistake-patterns': 'avoid',
815
+ 'preferences': 'preference',
816
+ 'validated-patterns': 'repeat',
817
+ 'manager-coaching': 'coaching',
818
+ };
819
+ // Parse the `## [P-…] Title` entries (and their body prose) out of one learning
820
+ // file. Score/Last seen/Recurrences metadata lines are dropped — the Hub shows
821
+ // the human-readable learning, not the bookkeeping.
822
+ function parseLearningEntries(filePath, displayPath, category, level) {
823
+ if (!(0, fs_1.existsSync)(filePath))
824
+ return [];
825
+ let content;
826
+ try {
827
+ content = (0, fs_1.readFileSync)(filePath, 'utf8');
828
+ }
829
+ catch {
830
+ return [];
831
+ }
832
+ const out = [];
833
+ let current = null;
834
+ let bodyLines = [];
835
+ const flush = () => {
836
+ if (current) {
837
+ current.body = bodyLines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
838
+ out.push(current);
839
+ }
840
+ current = null;
841
+ bodyLines = [];
842
+ };
843
+ const headingRe = learningEntryHeadingRegex();
844
+ for (const line of content.split(/\r?\n/)) {
845
+ const header = line.match(headingRe);
846
+ if (header) {
847
+ flush();
848
+ current = { severity: header[1], title: header[2].trim(), body: '', source: displayPath, category, level };
849
+ continue;
850
+ }
851
+ if (!current)
852
+ continue;
853
+ if (line.trim() === '---') {
854
+ flush();
855
+ continue;
856
+ }
857
+ // Drop the bookkeeping lines — the Hub shows the human-readable learning,
858
+ // not the scoring metadata.
859
+ const t = line.trim();
860
+ if (/^\*\*(Score|Last seen|Recurrences|Technical trace|Users|First synthesized)\*\*:/i.test(t))
861
+ continue;
862
+ if (/^(First|Last) synthesized:/i.test(t))
863
+ continue;
864
+ bodyLines.push(line);
865
+ }
866
+ flush();
867
+ return out;
868
+ }
869
+ function levelDir(roots, level) {
870
+ if (level === 'machine') {
871
+ return { dir: roots.globalPersonalBase, displayBase: roots.globalPersonalDisplayBase.replace(/\/$/, '') };
872
+ }
873
+ return { dir: roots.repoLearningsBase, displayBase: REPO_LEARNINGS_REL };
874
+ }
875
+ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
876
+ const roots = getLearningRoots(workspaceRoot);
877
+ const out = [];
878
+ if (scope === 'org') {
879
+ for (const cat of ['avoid', 'preference', 'repeat', 'coaching']) {
880
+ const f = `org-${exports.CATEGORY_TO_FILETYPE[cat]}.md`;
881
+ out.push(...parseLearningEntries((0, path_1.join)(roots.repoLearningsBase, f), `${REPO_LEARNINGS_REL}/${f}`, cat, 'org'));
882
+ }
883
+ return out;
884
+ }
885
+ const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
886
+ const { dir, displayBase } = levelDir(roots, level);
887
+ const cats = scope === 'reverse' ? ['coaching'] : ['avoid', 'preference', 'repeat'];
888
+ for (const cat of cats) {
889
+ const f = `${resolvedUserId}-${exports.CATEGORY_TO_FILETYPE[cat]}.md`;
890
+ out.push(...parseLearningEntries((0, path_1.join)(dir, f), `${displayBase}/${f}`, cat, level));
891
+ }
892
+ return out;
893
+ }
894
+ function resolveLearningFilePath(workspaceRoot, userId, ref) {
895
+ const roots = getLearningRoots(workspaceRoot);
896
+ const fileType = exports.CATEGORY_TO_FILETYPE[ref.category];
897
+ if (ref.scope === 'org')
898
+ return (0, path_1.join)(roots.repoLearningsBase, `org-${fileType}.md`);
899
+ const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
900
+ const base = ref.level === 'project' ? roots.repoLearningsBase : roots.globalPersonalBase;
901
+ return (0, path_1.join)(base, `${resolvedUserId}-${fileType}.md`);
902
+ }
903
+ function titleCaseFromFileType(fileType) {
904
+ return fileType.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
905
+ }
906
+ function applyLearningEntryChange(workspaceRoot, userId, ref, action, payload) {
907
+ const filePath = resolveLearningFilePath(workspaceRoot, userId, ref);
908
+ const existing = (0, fs_1.existsSync)(filePath) ? (0, fs_1.readFileSync)(filePath, 'utf8') : '';
909
+ const nl = existing.includes('\r\n') ? '\r\n' : '\n';
910
+ const lines = existing.length ? existing.split(/\r?\n/) : [];
911
+ const headingRe = learningEntryHeadingRegex();
912
+ const starts = [];
913
+ lines.forEach((l, i) => { if (headingRe.test(l))
914
+ starts.push(i); });
915
+ const write = (text) => {
916
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(filePath), { recursive: true });
917
+ (0, fs_1.writeFileSync)(filePath, text.replace(/\s*$/, '') + nl, 'utf8');
918
+ };
919
+ if (action === 'add') {
920
+ const sev = (payload.severity || 'P-MED');
921
+ const title = (payload.title || '').trim() || 'Untitled learning';
922
+ const body = String(payload.body || '').trim();
923
+ const block = [`#### [${sev}] ${title}`, '', body, '', '---'].join(nl);
924
+ const fileType = exports.CATEGORY_TO_FILETYPE[ref.category];
925
+ const preamble = existing.trim()
926
+ ? existing.replace(/\s*$/, '')
927
+ : `# ${ref.scope === 'org' ? 'Org ' : ''}${titleCaseFromFileType(fileType)}`;
928
+ write(preamble + nl + nl + block);
929
+ return { ok: true, path: filePath };
930
+ }
931
+ const targetIdx = starts.findIndex((s) => {
932
+ const m = lines[s].match(headingRe);
933
+ return m && m[2].trim() === (payload.originalTitle || '').trim();
934
+ });
935
+ if (targetIdx === -1)
936
+ throw new Error(`Learning entry not found: "${payload.originalTitle}"`);
937
+ const start = starts[targetIdx];
938
+ const end = targetIdx + 1 < starts.length ? starts[targetIdx + 1] : lines.length;
939
+ if (action === 'delete') {
940
+ const next = [...lines.slice(0, start), ...lines.slice(end)];
941
+ write(next.join(nl).replace(/\n{3,}/g, '\n\n'));
942
+ return { ok: true, path: filePath };
943
+ }
944
+ // edit: keep the entry's metadata lines (Score/Last seen/Recurrences/...) intact,
945
+ // replace only the heading (priority + title) and the prose body.
946
+ const headerMatch = lines[start].match(headingRe);
947
+ const sev = (payload.severity || headerMatch[1]);
948
+ const title = (payload.title !== undefined ? payload.title : headerMatch[2]).trim();
949
+ const meta = [];
950
+ for (const l of lines.slice(start + 1, end)) {
951
+ const t = l.trim();
952
+ if (t === '---')
953
+ continue;
954
+ if (/^\*\*(Score|Last seen|Recurrences|Technical trace|Users|First synthesized)\*\*:/i.test(t) || /^(First|Last) synthesized:/i.test(t))
955
+ meta.push(l);
956
+ }
957
+ const body = String(payload.body !== undefined ? payload.body : '').trim();
958
+ const rebuilt = [`#### [${sev}] ${title}`, '', ...meta, ...(meta.length ? [''] : []), body, '', '---'];
959
+ const next = [...lines.slice(0, start), ...rebuilt, ...lines.slice(end)];
960
+ write(next.join(nl).replace(/\n{3,}/g, '\n\n'));
961
+ return { ok: true, path: filePath };
962
+ }
963
+ /**
964
+ * Interpret a frontmatter flag value (string) as a boolean. Mirrors how the
965
+ * synthesis jobs treat `dismissed:`/`blessed:` — only an explicit truthy value
966
+ * counts as set.
967
+ */
968
+ function isTruthyFlag(value) {
969
+ if (value === undefined)
970
+ return false;
971
+ const normalized = value.trim().toLowerCase();
972
+ if (!normalized)
973
+ return false;
974
+ return normalized === 'true' || normalized === 'yes' || normalized === '1';
975
+ }