fraim-framework 2.0.163 → 2.0.165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ORG_CACHE_MANAGED_HEADER = exports.ORG_SYNC_METADATA_FILE = exports.ORG_CACHE_DIRNAME = void 0;
7
+ exports.getOrgCacheDir = getOrgCacheDir;
8
+ exports.readOrgCacheMetadata = readOrgCacheMetadata;
9
+ exports.getOrgCacheAgeHours = getOrgCacheAgeHours;
10
+ exports.syncOrgCache = syncOrgCache;
11
+ /**
12
+ * Org cache sync (issue #563).
13
+ *
14
+ * Materializes the organization's shared context/rules/learnings into the
15
+ * managed read-only cache at ~/.fraim/org/, from either backend:
16
+ * - git: shallow snapshot of the customer-owned org repo (R7)
17
+ * - fraim-cloud: GET /api/org/pack from the FRAIM server (R6)
18
+ *
19
+ * Cache files are managed content: marked, overwritten on every sync, and
20
+ * never a write target for agents (R2.2). A failed refresh never throws:
21
+ * an existing cache is served stale with its age (R2.3, R4.3).
22
+ */
23
+ const axios_1 = __importDefault(require("axios"));
24
+ const fs_1 = __importDefault(require("fs"));
25
+ const path_1 = __importDefault(require("path"));
26
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
27
+ const user_config_1 = require("./user-config");
28
+ const git_org_sync_1 = require("./git-org-sync");
29
+ exports.ORG_CACHE_DIRNAME = 'org';
30
+ exports.ORG_SYNC_METADATA_FILE = '.org-sync-metadata.json';
31
+ exports.ORG_CACHE_MANAGED_HEADER = '<!-- FRAIM_ORG_SYNC_MANAGED_CONTENT -->';
32
+ /** Subdirectories of the org pack that sync into the cache (spec R7.1). */
33
+ const ORG_PACK_DIRS = ['context', 'rules', 'learnings'];
34
+ /**
35
+ * Pack files must stay inside the three org pack directories. Applied to
36
+ * every relativePath before it touches the filesystem, so a compromised
37
+ * backend response can never write outside the cache directory.
38
+ */
39
+ const SAFE_PACK_RELATIVE_PATH = /^(context|rules|learnings)\/[\w.-]+\.md$/;
40
+ function getOrgCacheDir() {
41
+ return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.ORG_CACHE_DIRNAME);
42
+ }
43
+ function readOrgCacheMetadata() {
44
+ try {
45
+ const metadataPath = path_1.default.join(getOrgCacheDir(), exports.ORG_SYNC_METADATA_FILE);
46
+ if (!fs_1.default.existsSync(metadataPath))
47
+ return null;
48
+ return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ function getOrgCacheAgeHours() {
55
+ const metadata = readOrgCacheMetadata();
56
+ if (!metadata?.syncedAt)
57
+ return null;
58
+ const syncedAt = Date.parse(metadata.syncedAt);
59
+ if (Number.isNaN(syncedAt))
60
+ return null;
61
+ return Math.max(0, (Date.now() - syncedAt) / 3_600_000);
62
+ }
63
+ function decorateManagedOrgFile(content, backend) {
64
+ const normalized = content.replace(/^/, '');
65
+ if (normalized.startsWith(exports.ORG_CACHE_MANAGED_HEADER))
66
+ return normalized;
67
+ const writePath = backend === 'git'
68
+ ? 'open a pull request against your organization repo'
69
+ : 'update it through the organization-onboarding flow';
70
+ const marker = [
71
+ exports.ORG_CACHE_MANAGED_HEADER,
72
+ '> [!IMPORTANT]',
73
+ '> Synced from your organization\'s shared context. Local edits are overwritten on the next sync.',
74
+ `> To change this content, ${writePath}.`,
75
+ ''
76
+ ].join('\n');
77
+ return `${marker}\n${normalized}`;
78
+ }
79
+ function collectGitPackFiles(snapshotDir) {
80
+ const files = [];
81
+ for (const dirName of ORG_PACK_DIRS) {
82
+ const dirPath = path_1.default.join(snapshotDir, dirName);
83
+ if (!fs_1.default.existsSync(dirPath))
84
+ continue;
85
+ for (const entry of fs_1.default.readdirSync(dirPath, { withFileTypes: true })) {
86
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
87
+ continue;
88
+ files.push({
89
+ relativePath: `${dirName}/${entry.name}`,
90
+ content: fs_1.default.readFileSync(path_1.default.join(dirPath, entry.name), 'utf8')
91
+ });
92
+ }
93
+ }
94
+ return files;
95
+ }
96
+ async function fetchCloudPack(remoteUrl, apiKey) {
97
+ const response = await axios_1.default.get(`${remoteUrl.replace(/\/$/, '')}/api/org/pack`, {
98
+ headers: { 'x-api-key': apiKey },
99
+ timeout: 30_000
100
+ });
101
+ const files = Array.isArray(response.data?.files) ? response.data.files : [];
102
+ return {
103
+ files: files.filter((f) => typeof f?.relativePath === 'string' &&
104
+ SAFE_PACK_RELATIVE_PATH.test(f.relativePath) &&
105
+ typeof f?.content === 'string'),
106
+ version: String(response.data?.version ?? '0')
107
+ };
108
+ }
109
+ /** Replace the cache contents atomically: stage fully, then swap. */
110
+ function materializeCache(files, metadata) {
111
+ const cacheDir = getOrgCacheDir();
112
+ const stagingDir = `${cacheDir}.staging-${process.pid}`;
113
+ fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
114
+ fs_1.default.mkdirSync(stagingDir, { recursive: true });
115
+ for (const file of files) {
116
+ if (!SAFE_PACK_RELATIVE_PATH.test(file.relativePath))
117
+ continue;
118
+ const destination = path_1.default.join(stagingDir, file.relativePath);
119
+ fs_1.default.mkdirSync(path_1.default.dirname(destination), { recursive: true });
120
+ fs_1.default.writeFileSync(destination, decorateManagedOrgFile(file.content, metadata.backend));
121
+ }
122
+ fs_1.default.writeFileSync(path_1.default.join(stagingDir, exports.ORG_SYNC_METADATA_FILE), JSON.stringify(metadata, null, 2));
123
+ fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
124
+ fs_1.default.renameSync(stagingDir, cacheDir);
125
+ }
126
+ /**
127
+ * New-machine onboarding (R1.3, AC1): when this machine has an API key but no
128
+ * organization block yet, probe the FRAIM server. If the account owns org
129
+ * artifacts, persist the fraim-cloud organization block so `fraim setup` +
130
+ * `fraim sync` is the complete second-machine flow. Quietly resolves null on
131
+ * any failure or when the account has no org.
132
+ */
133
+ async function discoverCloudOrganization(options) {
134
+ try {
135
+ const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
136
+ if (!apiKey || apiKey === 'local-dev' || apiKey === 'test-mode-key')
137
+ return null;
138
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
139
+ const pack = await fetchCloudPack(remoteUrl, apiKey);
140
+ if (pack.files.length === 0)
141
+ return null;
142
+ (0, user_config_1.writeUserFraimConfig)({ organization: { backend: 'fraim-cloud' } });
143
+ return (0, user_config_1.getOrganizationConfig)();
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ }
149
+ function failureOutcome(error) {
150
+ const message = error instanceof Error ? error.message : String(error);
151
+ const existing = readOrgCacheMetadata();
152
+ if (existing) {
153
+ return {
154
+ status: 'stale',
155
+ metadata: existing,
156
+ ageHours: getOrgCacheAgeHours() ?? 0,
157
+ error: message
158
+ };
159
+ }
160
+ return { status: 'absent', error: message };
161
+ }
162
+ /**
163
+ * Refresh the org cache from the configured backend (R4.1). Never throws:
164
+ * returns 'disabled' (no org configured), 'synced', 'stale' (refresh failed,
165
+ * existing cache served with age), or 'absent' (refresh failed, no cache).
166
+ */
167
+ async function syncOrgCache(options) {
168
+ let organization = (0, user_config_1.getOrganizationConfig)();
169
+ if (!organization) {
170
+ organization = await discoverCloudOrganization(options);
171
+ if (!organization)
172
+ return { status: 'disabled' };
173
+ }
174
+ try {
175
+ if (organization.backend === 'git') {
176
+ const snapshot = (0, git_org_sync_1.fetchOrgRepoSnapshot)(organization.gitUrl);
177
+ try {
178
+ const metadata = {
179
+ version: snapshot.sha,
180
+ backend: 'git',
181
+ source: organization.gitUrl,
182
+ syncedAt: new Date().toISOString()
183
+ };
184
+ materializeCache(collectGitPackFiles(snapshot.dir), metadata);
185
+ return { status: 'synced', metadata };
186
+ }
187
+ finally {
188
+ snapshot.cleanup();
189
+ }
190
+ }
191
+ const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
192
+ const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
193
+ if (!apiKey)
194
+ throw new Error('No FRAIM API key available for the fraim-cloud org backend.');
195
+ const pack = await fetchCloudPack(remoteUrl, apiKey);
196
+ const metadata = {
197
+ version: pack.version,
198
+ backend: 'fraim-cloud',
199
+ source: remoteUrl,
200
+ syncedAt: new Date().toISOString()
201
+ };
202
+ materializeCache(pack.files, metadata);
203
+ return { status: 'synced', metadata };
204
+ }
205
+ catch (error) {
206
+ return failureOutcome(error);
207
+ }
208
+ }
@@ -9,7 +9,11 @@ exports.buildInitProjectSummary = buildInitProjectSummary;
9
9
  exports.printInitProjectSummary = printInitProjectSummary;
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
11
  const ONBOARDING_VIDEO_PLAYLIST_URL = 'https://fraimworks.ai/resources.html#videos';
12
- function formatModeLabel(mode) {
12
+ function formatModeLabel(result) {
13
+ if (result.mode === 'conversational' && result.repositoryDetected) {
14
+ return 'Pending project onboarding';
15
+ }
16
+ const mode = result.mode;
13
17
  switch (mode) {
14
18
  case 'conversational':
15
19
  return 'Conversational';
@@ -19,7 +23,11 @@ function formatModeLabel(mode) {
19
23
  return 'Integrated';
20
24
  }
21
25
  }
22
- function getModeSpecificNextStep(mode) {
26
+ function getModeSpecificNextStep(result) {
27
+ if (result.mode === 'conversational' && result.repositoryDetected) {
28
+ return `The agent will create fraim/config.json during onboarding, preserve the detected repository identity, then focus on project context, validation commands, durable project rules, and any optional automation choices. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
29
+ }
30
+ const mode = result.mode;
23
31
  switch (mode) {
24
32
  case 'conversational':
25
33
  return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable project rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
@@ -53,7 +61,8 @@ function buildInitProjectSummary(result) {
53
61
  return {
54
62
  status: 'FRAIM project initialized.',
55
63
  fields: {
56
- mode: formatModeLabel(result.mode),
64
+ mode: formatModeLabel(result),
65
+ projectContext: result.repositoryDetected ? 'repository-backed' : 'local-folder',
57
66
  project: result.projectName,
58
67
  repositoryDetection: result.repositoryDetected ? 'detected' : 'not detected',
59
68
  issueTracking: result.issueTrackingDetected ? 'detected' : 'not detected',
@@ -64,19 +73,23 @@ function buildInitProjectSummary(result) {
64
73
  warnings: [...result.warnings],
65
74
  nextStep: {
66
75
  prompt: 'Tell your AI agent "Onboard this project".',
67
- explanation: getModeSpecificNextStep(result.mode)
76
+ explanation: getModeSpecificNextStep(result)
68
77
  }
69
78
  };
70
79
  }
71
80
  function printInitProjectSummary(result) {
72
81
  const summary = buildInitProjectSummary(result);
73
- const showRepositoryDetails = result.mode !== 'conversational';
82
+ const showRepositoryDetection = result.mode !== 'conversational' || result.repositoryDetected;
83
+ const showIssueTrackingDetails = result.mode !== 'conversational';
74
84
  console.log(chalk_1.default.green(`\n${summary.status}`));
75
85
  console.log(chalk_1.default.blue('Project summary:'));
76
- console.log(chalk_1.default.gray(` Mode: ${summary.fields.mode}`));
86
+ console.log(chalk_1.default.gray(` Automation mode: ${summary.fields.mode}`));
87
+ console.log(chalk_1.default.gray(` Project context: ${summary.fields.projectContext}`));
77
88
  console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
78
- if (showRepositoryDetails) {
89
+ if (showRepositoryDetection) {
79
90
  console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
91
+ }
92
+ if (showIssueTrackingDetails) {
80
93
  console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
81
94
  }
82
95
  console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readUserFraimConfig = readUserFraimConfig;
7
+ exports.writeUserFraimConfig = writeUserFraimConfig;
8
+ exports.getOrganizationConfig = getOrganizationConfig;
9
+ /**
10
+ * Typed read/write helpers for the user-level FRAIM config (~/.fraim/config.json),
11
+ * including the organization block introduced by issue #563.
12
+ *
13
+ * The user-level config is machine-scoped and distinct from the workspace
14
+ * config (fraim/config.json). Honors the FRAIM_USER_DIR override used by tests.
15
+ */
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
19
+ function getUserConfigPath() {
20
+ return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json');
21
+ }
22
+ function readUserFraimConfig() {
23
+ try {
24
+ const configPath = getUserConfigPath();
25
+ if (!fs_1.default.existsSync(configPath))
26
+ return {};
27
+ const parsed = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
28
+ return parsed && typeof parsed === 'object' ? parsed : {};
29
+ }
30
+ catch {
31
+ return {};
32
+ }
33
+ }
34
+ /**
35
+ * Shallow-merge a patch into the user config and persist it. Existing keys
36
+ * not named in the patch are preserved. Setting a key to undefined removes it.
37
+ */
38
+ function writeUserFraimConfig(patch) {
39
+ const current = readUserFraimConfig();
40
+ const merged = { ...current, ...patch };
41
+ for (const key of Object.keys(patch)) {
42
+ if (patch[key] === undefined)
43
+ delete merged[key];
44
+ }
45
+ const dir = (0, project_fraim_paths_1.getUserFraimDirPath)();
46
+ fs_1.default.mkdirSync(dir, { recursive: true });
47
+ fs_1.default.writeFileSync(getUserConfigPath(), JSON.stringify(merged, null, 2));
48
+ return merged;
49
+ }
50
+ /**
51
+ * Resolve the validated organization configuration, or null when no valid
52
+ * organization is configured on this machine (R1.1).
53
+ */
54
+ function getOrganizationConfig() {
55
+ const raw = readUserFraimConfig().organization;
56
+ if (!raw || typeof raw !== 'object')
57
+ return null;
58
+ if (raw.backend === 'git') {
59
+ const gitUrl = typeof raw.gitUrl === 'string' ? raw.gitUrl.trim() : '';
60
+ if (!gitUrl)
61
+ return null;
62
+ return { backend: 'git', gitUrl, id: raw.id };
63
+ }
64
+ if (raw.backend === 'fraim-cloud') {
65
+ return { backend: 'fraim-cloud', id: raw.id };
66
+ }
67
+ return null;
68
+ }
@@ -296,6 +296,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
296
296
  },
297
297
  "instanceUrl": {
298
298
  "kind": "string"
299
+ },
300
+ "accessScript": {
301
+ "kind": "string"
299
302
  }
300
303
  }
301
304
  },
@@ -390,6 +393,12 @@ exports.FRAIM_CONFIG_SCHEMA = {
390
393
  },
391
394
  "instructions": {
392
395
  "kind": "string"
396
+ },
397
+ "accessScript": {
398
+ "kind": "string"
399
+ },
400
+ "purpose": {
401
+ "kind": "string"
393
402
  }
394
403
  }
395
404
  }
@@ -588,6 +597,7 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
588
597
  "integrations.itsm",
589
598
  "integrations.itsm.provider",
590
599
  "integrations.itsm.instanceUrl",
600
+ "integrations.itsm.accessScript",
591
601
  "integrations.identity",
592
602
  "integrations.identity.provider",
593
603
  "automation",
@@ -341,20 +341,40 @@ function computeOldestL0AgeDays(workspaceRoot, userId) {
341
341
  }
342
342
  return oldest;
343
343
  }
344
+ /**
345
+ * Resolve an L2 org-scope learning file (issue #563): a repo-local override
346
+ * (`fraim/personalized-employee/learnings/`) wins, then the synced org cache
347
+ * (`~/.fraim/org/learnings/`). Org learnings are part of the shared org pack,
348
+ * so a synced copy must be readable by the agent even with no repo-local copy.
349
+ * Mirrors the org context/rules resolution order. Returns `present:false` with
350
+ * the repo path/display when neither tier has the file.
351
+ */
352
+ function resolveOrgLearningFile(repoLearningsBase, fileName) {
353
+ const repoPath = (0, path_1.join)(repoLearningsBase, fileName);
354
+ if ((0, fs_1.existsSync)(repoPath)) {
355
+ return { present: true, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
356
+ }
357
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', 'learnings', fileName);
358
+ if ((0, fs_1.existsSync)(cachePath)) {
359
+ return { present: true, path: cachePath, displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`) };
360
+ }
361
+ return { present: false, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
362
+ }
344
363
  function buildLearningContextSection(workspaceRoot, userId, forJob) {
345
364
  const roots = getLearningRoots(workspaceRoot);
346
365
  const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
347
366
  const threshold = getScoreThreshold(workspaceRoot);
348
- const l2MistakePath = (0, path_1.join)(roots.repoLearningsBase, 'org-mistake-patterns.md');
349
- const l2PrefPath = (0, path_1.join)(roots.repoLearningsBase, 'org-preferences.md');
350
- const l2CoachPath = (0, path_1.join)(roots.repoLearningsBase, 'org-manager-coaching.md');
351
- const l2ValidatedPath = (0, path_1.join)(roots.repoLearningsBase, 'org-validated-patterns.md');
352
- const l2MistakePresent = (0, fs_1.existsSync)(l2MistakePath);
353
- const l2PrefPresent = (0, fs_1.existsSync)(l2PrefPath);
354
- const l2CoachPresent = (0, fs_1.existsSync)(l2CoachPath);
355
- const l2ValidatedPresent = (0, fs_1.existsSync)(l2ValidatedPath);
356
- const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2MistakePath, threshold, 'mistake-patterns') : null;
357
- const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2ValidatedPath, threshold, 'validated-patterns') : null;
367
+ // L2 org learnings resolve repo-local first, then the synced org cache (#563).
368
+ const l2Mistake = resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md');
369
+ const l2Pref = resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md');
370
+ const l2Coach = resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md');
371
+ const l2Validated = resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md');
372
+ const l2MistakePresent = l2Mistake.present;
373
+ const l2PrefPresent = l2Pref.present;
374
+ const l2CoachPresent = l2Coach.present;
375
+ const l2ValidatedPresent = l2Validated.present;
376
+ const l2MistakeStats = l2MistakePresent ? scanMistakePatternFile(l2Mistake.path, threshold, 'mistake-patterns') : null;
377
+ const l2ValidatedStats = l2ValidatedPresent ? scanMistakePatternFile(l2Validated.path, threshold, 'validated-patterns') : null;
358
378
  const l1Mistake = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-mistake-patterns.md`);
359
379
  const l1Pref = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-preferences.md`);
360
380
  const l1Coach = resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, `${resolvedUserId}-manager-coaching.md`);
@@ -394,13 +414,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
394
414
  if (hasL2) {
395
415
  section += '### L2 - Org patterns\n';
396
416
  if (l2MistakePresent)
397
- section += `\`${REPO_LEARNINGS_REL}/org-mistake-patterns.md\` (entries above score threshold)\n`;
417
+ section += `\`${l2Mistake.displayPath}\` (entries above score threshold)\n`;
398
418
  if (l2PrefPresent)
399
- section += `\`${REPO_LEARNINGS_REL}/org-preferences.md\` (all entries)\n`;
419
+ section += `\`${l2Pref.displayPath}\` (all entries)\n`;
400
420
  if (l2CoachPresent)
401
- section += `\`${REPO_LEARNINGS_REL}/org-manager-coaching.md\` (manager-facing; all entries)\n`;
421
+ section += `\`${l2Coach.displayPath}\` (manager-facing; all entries)\n`;
402
422
  if (l2ValidatedPresent)
403
- section += `\`${REPO_LEARNINGS_REL}/org-validated-patterns.md\` (entries above score threshold)\n`;
423
+ section += `\`${l2Validated.displayPath}\` (entries above score threshold)\n`;
404
424
  const l2DormantTotal = (l2MistakeStats?.dormant || 0) + (l2ValidatedStats?.dormant || 0);
405
425
  if (l2DormantTotal > 0) {
406
426
  section += `Dormant: ${l2DormantTotal} org pattern${l2DormantTotal !== 1 ? 's' : ''} below threshold\n`;
@@ -465,12 +485,13 @@ function buildLearningContextSection(workspaceRoot, userId, forJob) {
465
485
  return section;
466
486
  }
467
487
  /**
468
- * Resolve an organization/manager-scope context file. These are user-level and
469
- * portable across repos, but a repo-local copy (under `fraim/…`) wins when
470
- * presentmirroring how personal learnings layer (repo-local shadows
471
- * user-level) in `resolvePersonalLearningFile`.
488
+ * Resolve an organization/manager-scope context file (issue #563 order, R3.1):
489
+ * 1. repo-local override (`fraim/personalized-employee/…`) wins when present
490
+ * 2. managed org cache (`~/.fraim/org/…`) the synced shared org layer;
491
+ * eligible for org files only, never manager files (R3.3)
492
+ * 3. legacy machine-local (`~/.fraim/personalized-employee/…`)
472
493
  */
473
- function resolveOrgContextFile(workspaceRoot, relativePath) {
494
+ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = true) {
474
495
  try {
475
496
  const repoPath = (0, path_1.join)((0, project_fraim_paths_1.getWorkspaceFraimDir)(workspaceRoot), 'personalized-employee', relativePath);
476
497
  if ((0, fs_1.existsSync)(repoPath)) {
@@ -479,6 +500,15 @@ function resolveOrgContextFile(workspaceRoot, relativePath) {
479
500
  displayPath: (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`personalized-employee/${relativePath}`)
480
501
  };
481
502
  }
503
+ if (orgCacheEligible) {
504
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
505
+ if ((0, fs_1.existsSync)(cachePath)) {
506
+ return {
507
+ present: true,
508
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`)
509
+ };
510
+ }
511
+ }
482
512
  const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
483
513
  if ((0, fs_1.existsSync)(userPath)) {
484
514
  return {
@@ -518,11 +548,12 @@ function resolveProjectContextFile(workspaceRoot, relativePath) {
518
548
  * lists only present files, and returns '' when none exist.
519
549
  */
520
550
  function buildTeamContextSection(workspaceRoot, forJob) {
521
- // Organization layer (user-level, portable; repo-local override wins).
551
+ // Organization layer: repo override, then org cache, then legacy machine-local.
552
+ // Manager files are personal and never resolve from the shared org cache (R3.3).
522
553
  const orgContext = resolveOrgContextFile(workspaceRoot, 'context/org_context.md');
523
- const managerContext = resolveOrgContextFile(workspaceRoot, 'context/manager_context.md');
554
+ const managerContext = resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false);
524
555
  const orgRules = resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md');
525
- const managerRules = resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md');
556
+ const managerRules = resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false);
526
557
  // Project layer (repo-local only).
527
558
  const projectContext = resolveProjectContextFile(workspaceRoot, 'context/project_context.md');
528
559
  const projectBrief = resolveProjectContextFile(workspaceRoot, 'context/project_brief.md');
@@ -571,9 +602,9 @@ function buildTeamContextSection(workspaceRoot, forJob) {
571
602
  function resolveTeamContextFiles(workspaceRoot) {
572
603
  return {
573
604
  orgContext: resolveOrgContextFile(workspaceRoot, 'context/org_context.md'),
574
- managerContext: resolveOrgContextFile(workspaceRoot, 'context/manager_context.md'),
605
+ managerContext: resolveOrgContextFile(workspaceRoot, 'context/manager_context.md', false),
575
606
  orgRules: resolveOrgContextFile(workspaceRoot, 'rules/org_rules.md'),
576
- managerRules: resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md'),
607
+ managerRules: resolveOrgContextFile(workspaceRoot, 'rules/manager_rules.md', false),
577
608
  projectContext: resolveProjectContextFile(workspaceRoot, 'context/project_context.md'),
578
609
  projectBrief: resolveProjectContextFile(workspaceRoot, 'context/project_brief.md'),
579
610
  projectQa: resolveProjectContextFile(workspaceRoot, 'context/project_qa.md'),
@@ -627,6 +658,23 @@ function resolveTeamContextFile(workspaceRoot, key) {
627
658
  scope
628
659
  };
629
660
  }
661
+ // Synced org cache (org files only, never manager files — R3.3). Reads
662
+ // resolve here, but the cache is managed content: it is never a write
663
+ // destination (R5.1), so writePath stays empty and the caller must route
664
+ // edits through the org backend's propose-and-approve flow.
665
+ if (key === 'org' || key === 'orgRules') {
666
+ const cachePath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'org', relativePath);
667
+ if ((0, fs_1.existsSync)(cachePath)) {
668
+ return {
669
+ present: true,
670
+ readPath: cachePath,
671
+ writePath: '',
672
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/${relativePath}`),
673
+ scope,
674
+ managedByOrgSync: true
675
+ };
676
+ }
677
+ }
630
678
  const userPath = (0, path_1.join)((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', relativePath);
631
679
  const userDisplay = (0, project_fraim_paths_1.getUserFraimDisplayPath)(`personalized-employee/${relativePath}`);
632
680
  return {
@@ -674,11 +722,12 @@ function countLearningEntries(filePath) {
674
722
  function countPreservedLearnings(workspaceRoot, userId) {
675
723
  const roots = getLearningRoots(workspaceRoot);
676
724
  const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
677
- // L2 organization-scope preserved files (repo-local org-* files).
678
- const organization = countLearningEntries((0, path_1.join)(roots.repoLearningsBase, 'org-mistake-patterns.md')) +
679
- countLearningEntries((0, path_1.join)(roots.repoLearningsBase, 'org-preferences.md')) +
680
- countLearningEntries((0, path_1.join)(roots.repoLearningsBase, 'org-manager-coaching.md')) +
681
- countLearningEntries((0, path_1.join)(roots.repoLearningsBase, 'org-validated-patterns.md'));
725
+ // L2 organization-scope preserved files: repo-local override, then the
726
+ // synced org cache (#563), matching what buildLearningContextSection injects.
727
+ const organization = countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-mistake-patterns.md').path) +
728
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-preferences.md').path) +
729
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-manager-coaching.md').path) +
730
+ countLearningEntries(resolveOrgLearningFile(roots.repoLearningsBase, 'org-validated-patterns.md').path);
682
731
  const resolve = (fileName) => resolvePersonalLearningFile(roots.repoLearningsBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, fileName);
683
732
  // L1 manager-facing reverse-mentoring file.
684
733
  const manager = countLearningEntries(resolve(`${resolvedUserId}-manager-coaching.md`).path);
@@ -415,6 +415,7 @@ class FraimLocalMCPServer {
415
415
  this.mentoringResponseCache = null;
416
416
  this.connectSyncInFlight = null;
417
417
  this.latestConnectSyncWarning = null;
418
+ this.orgCacheRefreshInFlight = false;
418
419
  this.writer = writer || process.stdout.write.bind(process.stdout);
419
420
  this.remoteUrl = process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
420
421
  this.apiKey = this.loadApiKey();
@@ -748,6 +749,35 @@ class FraimLocalMCPServer {
748
749
  finally {
749
750
  this.connectSyncInFlight = null;
750
751
  }
752
+ this.maybeRefreshOrgCache(String(requestId));
753
+ }
754
+ /**
755
+ * Issue #563 (R4.2): opportunistically refresh the managed org cache
756
+ * (~/.fraim/org/) at session bootstrap when it is missing or older than
757
+ * 24 hours. Fire-and-forget: session start never blocks on the org
758
+ * backend, and a failed refresh leaves the existing cache serving stale
759
+ * with its age (R4.3).
760
+ */
761
+ maybeRefreshOrgCache(requestId) {
762
+ if (this.orgCacheRefreshInFlight)
763
+ return;
764
+ this.orgCacheRefreshInFlight = true;
765
+ void Promise.resolve().then(() => __importStar(require('../cli/utils/org-pack-sync'))).then(async ({ getOrgCacheAgeHours, syncOrgCache }) => {
766
+ const ageHours = getOrgCacheAgeHours();
767
+ if (ageHours !== null && ageHours < 24)
768
+ return;
769
+ const outcome = await syncOrgCache();
770
+ if (outcome.status !== 'disabled') {
771
+ const version = outcome.metadata ? ` (version ${outcome.metadata.version.slice(0, 12)})` : '';
772
+ this.log(`[req:${requestId}] Org cache refresh at connect: ${outcome.status}${version}`);
773
+ }
774
+ })
775
+ .catch((error) => {
776
+ this.logError(`[req:${requestId}] Org cache refresh failed: ${error?.message || error}`);
777
+ })
778
+ .finally(() => {
779
+ this.orgCacheRefreshInFlight = false;
780
+ });
751
781
  }
752
782
  /**
753
783
  * Automatically detect machine information
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework - Smart Entry Point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-framework",
3
- "version": "2.0.163",
3
+ "version": "2.0.165",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -104,7 +104,6 @@
104
104
  "@types/prompts": "^2.4.9",
105
105
  "@types/semver": "^7.7.1",
106
106
  "fast-glob": "^3.3.3",
107
- "html-to-docx": "^1.8.0",
108
107
  "markdown-it": "^14.1.1",
109
108
  "markdown-it-highlightjs": "^4.3.0",
110
109
  "playwright": "^1.58.2",
@@ -9,7 +9,7 @@
9
9
  <!-- Detect Electron + host platform before CSS paints so html.electron / html.mac /
10
10
  html.win / html.linux rules apply on first render (no flash, correct native chrome). -->
11
11
  <script>(function(){var h=document.documentElement,ua=navigator.userAgent;if(/Electron/.test(ua))h.classList.add('electron');var p=(navigator.userAgentData&&navigator.userAgentData.platform)||navigator.platform||'';if(/mac/i.test(p)||/Mac|iPhone|iPad|iPod/.test(ua))h.classList.add('mac');else if(/win/i.test(p)||/Windows/.test(ua))h.classList.add('win');else if(/linux/i.test(p)||/Linux|X11/.test(ua))h.classList.add('linux');})();</script>
12
- <link rel="stylesheet" href="./styles.css">
12
+ <link rel="stylesheet" href="./styles.css?v=conv-panels-20260611b">
13
13
  <link rel="stylesheet" href="./review.css">
14
14
  </head>
15
15
  <body>
@@ -363,7 +363,7 @@
363
363
  <summary>
364
364
  <span class="panel-summary-copy">
365
365
  <span class="panel-kicker">Micro-manage</span>
366
- <span class="panel-summary-text">Raw host details</span>
366
+ <span class="panel-summary-text">Raw host events and tool calls</span>
367
367
  </span>
368
368
  </summary>
369
369
  <pre class="micro-log" id="micro-log"></pre>