fraim 2.0.264 → 2.0.266

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.
@@ -673,6 +673,19 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
673
673
  child.on('error', (err) => {
674
674
  spawnError = err;
675
675
  });
676
+ const handleStdinError = (err) => {
677
+ if (settled)
678
+ return;
679
+ const msg = err.message || String(err);
680
+ settle({
681
+ status: 'error',
682
+ phase: 'spawn',
683
+ message: `Failed to start MCP server: ${msg}`,
684
+ suggestion: 'Verify that the MCP server command is installed and accessible on PATH.',
685
+ details: { command, args, error: (0, fraim_mcp_diagnostics_1.sanitizeDiagnosticText)(msg, knownSecrets) }
686
+ });
687
+ };
688
+ child.stdin?.on('error', handleStdinError);
676
689
  const handleStdoutLine = (line) => {
677
690
  const trimmed = line.trim();
678
691
  if (!trimmed)
@@ -804,14 +817,20 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
804
817
  clientInfo: { name: 'fraim-doctor', version: '1.0' }
805
818
  }
806
819
  }) + '\n';
807
- child.stdin?.write(initMsg);
820
+ child.stdin?.write(initMsg, (err) => {
821
+ if (err)
822
+ handleStdinError(err);
823
+ });
808
824
  const toolsMsg = JSON.stringify({
809
825
  jsonrpc: '2.0',
810
826
  id: 2,
811
827
  method: 'tools/list',
812
828
  params: {}
813
829
  }) + '\n';
814
- child.stdin?.write(toolsMsg);
830
+ child.stdin?.write(toolsMsg, (err) => {
831
+ if (err)
832
+ handleStdinError(err);
833
+ });
815
834
  }
816
835
  catch {
817
836
  // stdin may not be writable if the process exited immediately
@@ -53,8 +53,8 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
53
53
  pam: {
54
54
  personaKey: 'pam',
55
55
  bundleId: 'persona-pam-core',
56
- catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'technical-design', 'project-plan-creation']),
57
- protectedJobs: ['feature-specification', 'technical-design', 'experiment-tracking', 'project-plan-creation', 'implementation-feature-review', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
56
+ catalogMetadata: buildCatalogMetadata('pam', ['feature-specification', 'project-plan-creation', 'implementation-feature-review']),
57
+ protectedJobs: ['feature-specification', 'send-newsletter', 'send-thank-you-notes', 'experiment-tracking', 'project-plan-creation', 'implementation-feature-review', 'scrum-sprint-planning', 'mvp-validation-plan', 'sprint-planning', 'customer-prospect-discovery', 'interview-preparation', 'participant-recruitment', 'process-interview-notes', 'review-customer-development', 'triage-customer-needs'],
58
58
  protectedAliases: ['product-management', 'product-spec'],
59
59
  defaultHireMode: 'job',
60
60
  lockCopy: 'Hire PaM to unlock product-management work for this request.'
@@ -63,7 +63,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
63
63
  personaKey: 'swen',
64
64
  bundleId: 'persona-swen-core',
65
65
  catalogMetadata: buildCatalogMetadata('swen', ['feature-implementation', 'technical-design', 'code-refactoring']),
66
- protectedJobs: ['feature-implementation', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
66
+ protectedJobs: ['feature-implementation', 'technical-design', 'implementation-design-review', 'code-refactoring', 'pr-iteration', 'mobile-app-development', 'mcp-server-creation', 'cloud-application-deployment', 'cloud-cost-optimization', 'cloud-performance-diagnosis', 'route-llm-spend-to-cloud-credits', 'set-up-cloud-cost-alerts', 'gitlabs-to-github', 'system-migration', 'cross-cloud-migration', 'data-pipeline-design', 'data-quality-monitoring', 'data-platform-architecture', 'write-dev-docs', 'database-schema-design', 'create-architecture', 'project-scaffolding', 'codebase-analysis-and-ideation', 'github-org-setup', 'google-workspace-setup', 'mobile-app-rejection-response', 'mobile-app-submission', 'publish-mcp-app', 'application-replication-workflow'],
67
67
  protectedAliases: ['software-engineering', 'implementation'],
68
68
  defaultHireMode: 'job',
69
69
  lockCopy: 'Hire SWEn to unlock software-engineering delivery for this request.'
@@ -117,7 +117,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
117
117
  personaKey: 'ashley',
118
118
  bundleId: 'persona-ashley-core',
119
119
  catalogMetadata: buildCatalogMetadata('ashley', ['chief-of-staff-briefing', 'executive-assistant', 'analyze-transcript']),
120
- protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'send-newsletter', 'send-thank-you-notes', 'analyze-transcript', 'travel-planning', 'travel-disruption-rebooking'],
120
+ protectedJobs: ['chief-of-staff-briefing', 'calendar-triage', 'meeting-preparation', 'executive-assistant', 'analyze-transcript', 'travel-planning', 'travel-disruption-rebooking'],
121
121
  protectedAliases: ['executive-assistant', 'operations-assistant'],
122
122
  defaultHireMode: 'job',
123
123
  lockCopy: 'Hire AshLey to unlock executive-assistant work for this request.'
@@ -273,6 +273,7 @@ for (const bundle of Object.values(exports.PERSONA_CAPABILITY_BUNDLES)) {
273
273
  // as "free") so the Hub attributes them to FRAIMworker, but they are never
274
274
  // hire-gated because FRAIMworker is not a purchasable persona.
275
275
  const GENERIC_WORKER_OWNED_JOBS = new Set([
276
+ 'create-hub-configured-agent',
276
277
  'contribute-to-fraim',
277
278
  'file-fraim-issue',
278
279
  'praise-fraim',
@@ -90,6 +90,13 @@ function getLearningRoots(workspaceRoot) {
90
90
  repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
91
91
  };
92
92
  }
93
+ function sameResolvedPath(a, b) {
94
+ return (0, path_1.resolve)(a) === (0, path_1.resolve)(b);
95
+ }
96
+ function portablePersonalBaseIsLive(globalBase) {
97
+ return sameResolvedPath((0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, 'learnings'), globalBase)
98
+ || !sameResolvedPath(globalBase, (0, project_fraim_paths_1.getUserFraimLearningsDir)());
99
+ }
93
100
  function resolvePersonalLearningFile(repoBase, managerCacheBase, managerCacheDisplayBase, globalBase, globalDisplayBase, fileName) {
94
101
  const repoPath = (0, path_1.join)(repoBase, fileName);
95
102
  if ((0, fs_1.existsSync)(repoPath)) {
@@ -100,14 +107,14 @@ function resolvePersonalLearningFile(repoBase, managerCacheBase, managerCacheDis
100
107
  };
101
108
  }
102
109
  const globalPath = (0, path_1.join)(globalBase, fileName);
103
- if ((0, fs_1.existsSync)(globalPath)) {
110
+ const managerCachePath = (0, path_1.join)(managerCacheBase, fileName);
111
+ if (portablePersonalBaseIsLive(globalBase) && (0, fs_1.existsSync)(globalPath)) {
104
112
  return {
105
113
  present: true,
106
114
  path: globalPath,
107
115
  displayPath: `${globalDisplayBase.replace(/\/$/, '')}/${fileName}`
108
116
  };
109
117
  }
110
- const managerCachePath = (0, path_1.join)(managerCacheBase, fileName);
111
118
  if ((0, fs_1.existsSync)(managerCachePath)) {
112
119
  return {
113
120
  present: true,
@@ -140,7 +147,9 @@ function resolvePersonalLearningFileTiers(repoBase, managerCacheBase, managerCac
140
147
  });
141
148
  }
142
149
  const globalPath = (0, path_1.join)(globalBase, fileName);
143
- if ((0, fs_1.existsSync)(globalPath)) {
150
+ if (portablePersonalBaseIsLive(globalBase) &&
151
+ (0, fs_1.existsSync)(globalPath) &&
152
+ !tiers.some((tier) => sameResolvedPath(tier.path, globalPath))) {
144
153
  tiers.push({
145
154
  level: 'manager',
146
155
  path: globalPath,
@@ -405,10 +414,25 @@ function collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots) {
405
414
  }
406
415
  return sources.sort((a, b) => b.sortKey.localeCompare(a.sortKey) || a.displayPath.localeCompare(b.displayPath));
407
416
  }
417
+ /**
418
+ * Issue #1078 fix: derive the org display path from contentRoot rather than
419
+ * hardcoding the pre-migration flat layout. After migrateOrgFlatContent runs,
420
+ * contentRoot is ~/.fraim/org/personalized-employee/ — not ~/.fraim/org/.
421
+ * The relative portion after the ~/.fraim/ prefix is computed dynamically so
422
+ * it cannot drift from the read path again.
423
+ */
424
+ function orgCacheDisplayPath(fileName) {
425
+ const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
426
+ const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', fileName);
427
+ const rel = cachePath.startsWith(fraimDir)
428
+ ? cachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
429
+ : cachePath.replace(/\\/g, '/');
430
+ return (0, project_fraim_paths_1.getUserFraimDisplayPath)(rel);
431
+ }
408
432
  /**
409
433
  * Resolve an L2 org-scope learning file (issue #563): a repo-local override
410
434
  * (`fraim/personalized-employee/learnings/`) wins, then the synced org cache
411
- * (`~/.fraim/org/learnings/`). Org learnings are part of the shared org pack,
435
+ * (contentRoot/learnings/). Org learnings are part of the shared org pack,
412
436
  * so a synced copy must be readable by the agent even with no repo-local copy.
413
437
  * Mirrors the org context/rules resolution order. Returns `present:false` with
414
438
  * the repo path/display when neither tier has the file.
@@ -420,7 +444,7 @@ function resolveOrgLearningFile(repoLearningsBase, fileName) {
420
444
  }
421
445
  const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', fileName);
422
446
  if ((0, fs_1.existsSync)(cachePath)) {
423
- return { present: true, path: cachePath, displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`) };
447
+ return { present: true, path: cachePath, displayPath: orgCacheDisplayPath(fileName) };
424
448
  }
425
449
  return { present: false, path: repoPath, displayPath: `${REPO_LEARNINGS_REL}/${fileName}` };
426
450
  }
@@ -436,7 +460,7 @@ function resolveOrgLearningFileTiers(repoLearningsBase, fileName) {
436
460
  tiers.push({
437
461
  level: 'org',
438
462
  path: cachePath,
439
- displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`org/learnings/${fileName}`)
463
+ displayPath: orgCacheDisplayPath(fileName)
440
464
  });
441
465
  }
442
466
  const repoPath = (0, path_1.join)(repoLearningsBase, fileName);
@@ -1158,11 +1182,20 @@ function collectBrainLearningDots(workspaceRoot, userId) {
1158
1182
  if (coldEntries.length) {
1159
1183
  processed.push({ type: fileType, typeLabel, region, scope: 'cold', displayPath: coldResolved.displayPath, entries: coldEntries });
1160
1184
  }
1161
- // L2 org
1162
- const org = resolveOrgLearningFile(roots.repoLearningsBase, `org-${fileType}.md`);
1163
- const orgEntries = parseBrainLearningEntries(org.path, fileType);
1164
- if (orgEntries.length) {
1165
- processed.push({ type: fileType, typeLabel, region, scope: 'org', displayPath: org.displayPath, entries: orgEntries });
1185
+ // L2 org — issue #1078 fix: use tiered resolver + merge so synced org home
1186
+ // and repo-local org-*.md are both included, same as buildLearningContextSection.
1187
+ const orgTiers = resolveOrgLearningFileTiers(roots.repoLearningsBase, `org-${fileType}.md`);
1188
+ if (orgTiers.length > 0) {
1189
+ // Merge entries across tiers; deduplicate by title (last tier wins = repo-local wins).
1190
+ const allOrgEntries = orgTiers.flatMap(t => parseBrainLearningEntries(t.path, fileType));
1191
+ const byTitle = new Map();
1192
+ for (const e of allOrgEntries)
1193
+ byTitle.set(e.title.trim().toLowerCase(), e);
1194
+ const mergedOrgEntries = [...byTitle.values()];
1195
+ if (mergedOrgEntries.length) {
1196
+ // Use the first (highest-precedence = synced org home) tier's displayPath.
1197
+ processed.push({ type: fileType, typeLabel, region, scope: 'org', displayPath: orgTiers[0].displayPath, entries: mergedOrgEntries });
1198
+ }
1166
1199
  }
1167
1200
  }
1168
1201
  return { pending: { count: pendingSignals.length, signals: pendingSignals }, processed };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.264",
3
+ "version": "2.0.266",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {