fraim-hub 2.0.350 → 2.0.351

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.
@@ -16,7 +16,6 @@ const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
16
16
  const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
17
17
  const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
18
18
  const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
19
- const hub_app_materializer_1 = require("./hub-app-materializer");
20
19
  function isDesktopRuntimeComplete() {
21
20
  if (![(0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimNpxShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimMcpLatestLauncherPath)()]
22
21
  .every(file => fs_1.default.existsSync(file)))
@@ -68,19 +67,11 @@ async function repairInstallerIdeMcpEntries(options = {}) {
68
67
  process.env.FRAIM_NON_INTERACTIVE = previousNonInteractive;
69
68
  }
70
69
  }
71
- async function warmInstallerCliCache() {
72
- const cliShim = (0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)();
73
- const result = await (0, hub_app_materializer_1.runNpmInstall)(cliShim, ['--version']);
74
- if (result.status !== 0) {
75
- throw new Error(`FRAIM CLI bootstrap exited with code ${result.status ?? 'unknown'}.`);
76
- }
77
- }
78
70
  async function runDesktopRuntimeSetup(logger = console) {
79
71
  await (0, managed_node_runtime_1.ensureManagedNpm)({
80
72
  onProgress: (message) => logger.log(`[fraim-installer-setup] ${message}`),
81
73
  });
82
74
  (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
83
- await warmInstallerCliCache();
84
75
  }
85
76
  async function runDesktopInstallerSetup(logger = console) {
86
77
  await runDesktopRuntimeSetup(logger);
@@ -232,9 +232,26 @@ function runPackage(plan, cacheDir) {
232
232
  const plan = resolvePackage();
233
233
  const versionCacheDir = path.join(CACHE_ROOT, plan.packageName + '-' + plan.version.replace(/[^a-zA-Z0-9._-]/g, '_'));
234
234
  pruneCaches(versionCacheDir, Date.now());
235
+
236
+ /**
237
+ * True when npx materialized the package into this cache. A non-zero exit from a package
238
+ * that DID install is the CLI's own result, not a cache problem, and must be returned verbatim.
239
+ */
240
+ function packageMaterialized(cacheDir) {
241
+ try {
242
+ const npxDir = path.join(cacheDir, '_npx');
243
+ return fs.readdirSync(npxDir).some(function (entry) {
244
+ return fs.existsSync(path.join(npxDir, entry, 'node_modules', plan.packageName));
245
+ });
246
+ } catch (error) {
247
+ return false;
248
+ }
249
+ }
250
+
235
251
  let status = runPackage(plan, versionCacheDir);
236
252
 
237
- if (status !== 0 && process.platform === 'win32') {
253
+ // Retry ONLY when npx could not materialize the package: the corrupt-cache case this exists for.
254
+ if (status !== 0 && process.platform === 'win32' && !packageMaterialized(versionCacheDir)) {
238
255
  const retryCache = path.join(CACHE_ROOT, 'retry-' + Date.now() + '-' + process.pid);
239
256
  console.error('[fraim-mcp-launcher] Package execution failed; retrying once with a fresh npm cache.');
240
257
  status = runPackage(plan, retryCache);
@@ -70,6 +70,8 @@ const script_sync_utils_1 = require("../utils/script-sync-utils");
70
70
  const USER_LEVEL_RUNTIME_DEPS = {
71
71
  'node-edge-tts': '*', // used by scripts/author-audio.js
72
72
  'adm-zip': '^0.6.0', // used by scripts/communication/*.js (DOCX review layer) — matches root/packages/* manifests
73
+ 'fast-glob': '^3.3.3', // used by scripts/exec-with-timeout.ts (Issue #1839)
74
+ 'tree-kill': '^1.2.2', // used by scripts/exec-with-timeout.ts (Issue #1839)
73
75
  };
74
76
  /**
75
77
  * Ensure the user-level FRAIM directory structure exists.
@@ -47,7 +47,6 @@ const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPat
47
47
  * would keep passing if this default ever changed.
48
48
  */
49
49
  exports.DEFAULT_THRESHOLD = 3.0;
50
- const AGING_HORIZON_DAYS = 7;
51
50
  const MAX_ENTRIES_SCANNED = 200;
52
51
  const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
53
52
  // ── Single source of truth for the learning-entry format contract (#533) ──────
@@ -306,11 +305,10 @@ function getScoreThreshold(workspaceRoot) {
306
305
  return exports.DEFAULT_THRESHOLD;
307
306
  }
308
307
  /**
309
- * Effective score for an L1 learning entry. The aging-risk count below uses
310
- * this same decay model with `now` shifted forward.
308
+ * Effective score for an L1 learning entry. Score is display-only and does
309
+ * not gate delivery; the tier file is the authoritative delivery gate.
311
310
  *
312
- * @param now Optional override for "now" (for forward-looking aging-risk
313
- * calculations). Defaults to the current wall clock.
311
+ * @param now Optional override for "now". Defaults to the current wall clock.
314
312
  */
315
313
  function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()) {
316
314
  const baseScore = severity === 'P-CRITICAL' ? 10 : severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
@@ -330,72 +328,6 @@ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, no
330
328
  const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
331
329
  return baseScore * decay * recurrenceBoost;
332
330
  }
333
- function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns') {
334
- const empty = { active: 0, dormant: 0, agingRisk: 0 };
335
- if (!(0, fs_1.existsSync)(filePath))
336
- return empty;
337
- let content;
338
- try {
339
- content = (0, fs_1.readFileSync)(filePath, 'utf8');
340
- }
341
- catch {
342
- return empty;
343
- }
344
- const lines = content.split(/\r?\n/);
345
- const now = new Date();
346
- const horizon = new Date(now.getTime() + AGING_HORIZON_DAYS * 86_400_000);
347
- let active = 0;
348
- let dormant = 0;
349
- let agingRisk = 0;
350
- let scanned = 0;
351
- let inEntry = false;
352
- let severity = null;
353
- let lastSeen = '';
354
- let recurrences = 1;
355
- const flush = () => {
356
- if (!severity)
357
- return;
358
- scanned++;
359
- const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now);
360
- if (today >= threshold) {
361
- active++;
362
- if (lastSeen) {
363
- const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon);
364
- if (future < threshold)
365
- agingRisk++;
366
- }
367
- }
368
- else {
369
- dormant++;
370
- }
371
- };
372
- for (const line of lines) {
373
- if (scanned >= MAX_ENTRIES_SCANNED)
374
- break;
375
- const headerMatch = line.match((0, learning_entry_format_1.learningEntryHeadingRegex)());
376
- if (headerMatch) {
377
- flush();
378
- inEntry = true;
379
- severity = headerMatch[1];
380
- lastSeen = '';
381
- recurrences = 1;
382
- continue;
383
- }
384
- if (!inEntry)
385
- continue;
386
- const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
387
- if (lastSeenMatch) {
388
- lastSeen = lastSeenMatch[1].trim();
389
- continue;
390
- }
391
- const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
392
- if (recurrenceMatch) {
393
- recurrences = parseInt(recurrenceMatch[1], 10);
394
- }
395
- }
396
- flush();
397
- return { active, dormant, agingRisk };
398
- }
399
331
  /** Historical counters are only read for the human library view. */
400
332
  function resolveUsageStats() {
401
333
  try {
@@ -597,10 +529,10 @@ function mergeLearningEntriesAcrossTiers(tiers) {
597
529
  return tiers.flatMap(tier => tier.entries);
598
530
  }
599
531
  const CONTEXT_CATEGORIES = {
600
- mistake: { ft: 'mistake-patterns', gated: true, label: '(entries above score threshold)' },
601
- pref: { ft: 'preferences', gated: false, label: '(all entries)' },
602
- coach: { ft: 'manager-coaching', gated: false, label: '(manager-facing; all entries)' },
603
- validated: { ft: 'validated-patterns', gated: true, label: '(entries above score threshold)' },
532
+ mistake: { ft: 'mistake-patterns', label: '(all entries)' },
533
+ pref: { ft: 'preferences', label: '(all entries)' },
534
+ coach: { ft: 'manager-coaching', label: '(manager-facing; all entries)' },
535
+ validated: { ft: 'validated-patterns', label: '(all entries)' },
604
536
  };
605
537
  /** Resolve the files listed in injected context, in delivery order. */
606
538
  function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain) {
@@ -646,7 +578,6 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
646
578
  path: tier.path,
647
579
  displayPath: tier.displayPath,
648
580
  fileType: meta.ft,
649
- gated: meta.gated,
650
581
  label: meta.label,
651
582
  isCoach: key === 'coach',
652
583
  level: tier.level ?? fallbackLevel,
@@ -669,42 +600,14 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
669
600
  const l1 = collect({ global: l1GlobalCats, domain: l1DomainCats }, (ft, dom) => resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, dom ? `${resolvedUserId}-${dom}-${ft}.md` : `${resolvedUserId}-${ft}.md`), 'manager');
670
601
  return { l2, l1, multiTier, coachPresent };
671
602
  }
672
- /**
673
- * Report how many entries have been demoted, without naming the file they live in.
674
- *
675
- * Issue #861 keeps demoted entries out of the injected context, and #1103 keeps it
676
- * that way after trying the alternative. Naming the `.cold.md` path here was
677
- * considered and reverted: the path states a family (`…-mistake-patterns.cold.md`)
678
- * and not a subject, so an instruction to "read it if this job touches its
679
- * subject" is one the agent has no basis to follow. It degrades to reading the
680
- * file every job, which is the token cost #861 removed, or never, which makes the
681
- * line noise.
682
- *
683
- * Recovery for a demoted entry is a synthesis-time concern, not a runtime one.
684
- * `sleep-on-learnings` reads both tiers when deciding whether a signal is a
685
- * recurrence, so an entry that becomes relevant again is found, updated and
686
- * promoted there. See that job's gather-signals and analyze phases.
687
- */
688
- function renderDemotedLine(listed, label) {
689
- const total = listed.reduce((sum, f) => sum + f.dormant, 0);
690
- if (total === 0)
691
- return '';
692
- return total === 1
693
- ? `Demoted: 1 ${label} pattern has gone quiet and is not listed above.\n`
694
- : `Demoted: ${total} ${label} patterns have gone quiet and are not listed above.\n`;
695
- }
696
603
  function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
697
604
  const roots = getLearningRoots(workspaceRoot);
698
605
  const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
699
- const threshold = getScoreThreshold(workspaceRoot);
700
606
  const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain);
701
607
  const multiTier = resolved.multiTier;
702
608
  const toListed = (f) => ({
703
609
  displayPath: f.displayPath,
704
610
  label: f.label,
705
- dormant: f.gated
706
- ? scanMistakePatternFile(f.path, threshold, f.fileType).dormant
707
- : 0,
708
611
  isCoach: f.isCoach,
709
612
  });
710
613
  const l2Files = resolved.l2.map(toListed);
@@ -726,14 +629,12 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
726
629
  section += '### L2 - Org patterns\n';
727
630
  for (const f of l2Files)
728
631
  section += `\`${f.displayPath}\` ${f.label}\n`;
729
- section += renderDemotedLine(l2Files, 'org');
730
632
  section += '\n';
731
633
  }
732
634
  if (hasL1) {
733
635
  section += '### L1 - Your patterns\n';
734
636
  for (const f of l1Files)
735
637
  section += `\`${f.displayPath}\` ${f.label}\n`;
736
- section += renderDemotedLine(l1Files, 'personal');
737
638
  section += '\n';
738
639
  }
739
640
  if (l0CoachingCount > 0 || l0RetroCount > 0) {
@@ -1409,13 +1310,11 @@ function listDomainLearningFiles(dir, prefix, fileType) {
1409
1310
  }
1410
1311
  // Parse the `## [P-…] Title` entries (and their body prose) out of one learning
1411
1312
  // file. Score metadata (Last seen / Recurrences) is captured to compute the
1412
- // effective decayed score and the injected flag; those bookkeeping lines are
1413
- // still excluded from the rendered body.
1313
+ // effective decayed score for display; those bookkeeping lines are excluded from
1314
+ // the rendered body.
1414
1315
  //
1415
- // threshold: entries with score >= threshold (or in a non-gated category) are
1416
- // marked injected:true. Callers that don't need scoring pass Infinity so
1417
- // all entries are injected.
1418
- function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, threshold = exports.DEFAULT_THRESHOLD, usageStats) {
1316
+ // `injected` reflects tier standing (hot = true, cold = false); score is display-only.
1317
+ function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, usageStats) {
1419
1318
  if (!(0, fs_1.existsSync)(filePath))
1420
1319
  return [];
1421
1320
  let content;
@@ -1426,7 +1325,7 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
1426
1325
  return [];
1427
1326
  }
1428
1327
  const fileType = exports.CATEGORY_TO_FILETYPE[category];
1429
- // preferences and manager-coaching are threshold-free — always injected.
1328
+ // mistake-patterns and validated-patterns carry a display score; other categories do not.
1430
1329
  const isScoreGated = fileType === 'mistake-patterns' || fileType === 'validated-patterns';
1431
1330
  const out = [];
1432
1331
  let current = null;
@@ -1443,12 +1342,11 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
1443
1342
  if (curLastSeen !== null) {
1444
1343
  const s = computeEffectiveScore(current.severity, curLastSeen, curRecurrences, fileType, new Date());
1445
1344
  current.score = s;
1446
- current.injected = s >= threshold;
1345
+ current.injected = tier !== 'cold';
1447
1346
  }
1448
1347
  else {
1449
1348
  current.score = null;
1450
- // No date metadata: treat as fresh / always inject.
1451
- current.injected = true;
1349
+ current.injected = tier !== 'cold';
1452
1350
  }
1453
1351
  }
1454
1352
  else {
@@ -1620,7 +1518,6 @@ function readRejectedProposals(workspaceRoot, userId) {
1620
1518
  }
1621
1519
  function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
1622
1520
  const roots = getLearningRoots(workspaceRoot);
1623
- const threshold = getScoreThreshold(workspaceRoot);
1624
1521
  const usageStats = resolveUsageStats();
1625
1522
  // Only the human library reads these observations. An unavailable store must
1626
1523
  // fail this read, so a client can retain known history rather than invent zeroes.
@@ -1657,7 +1554,7 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
1657
1554
  // ResolvedLearningTier uses 'manager' to mean machine-level personal; map it to 'machine'.
1658
1555
  const tierLevel = (t) => t.level === 'manager' ? 'machine' : t.level;
1659
1556
  const parseTiers = (tiers, cat, tierVal, domainVal) => mergeLearningEntriesAcrossTiers(tiers.map((t) => ({
1660
- entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, threshold, usageStats)
1557
+ entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, usageStats)
1661
1558
  .map(entry => decorateAssessment(entry, t.path)),
1662
1559
  })));
1663
1560
  if (scope === 'org') {
@@ -1679,7 +1576,7 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
1679
1576
  seenDomainFile.add(fileName);
1680
1577
  const domainTiers = resolveOrgLearningFileTiers(roots.repoLearningsBase, fileName);
1681
1578
  if (domainTiers.length === 0) {
1682
- out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, threshold, usageStats)
1579
+ out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, usageStats)
1683
1580
  .map(entry => decorateAssessment(entry, (0, path_1.join)(base, fileName))));
1684
1581
  }
1685
1582
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.350",
3
+ "version": "2.0.351",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "author": "Sid Mathur <sid.mathur@gmail.com>",
6
6
  "homepage": "https://github.com/mathursrus/FRAIM#readme",
@@ -246,7 +246,7 @@
246
246
  "electron-updater": "^6.8.9",
247
247
  "express": "^5.2.1",
248
248
  "extract-zip": "^2.0.1",
249
- "fraim": "2.0.350",
249
+ "fraim": "2.0.351",
250
250
  "mongodb": "^7.0.0",
251
251
  "node-cron": "4.2.1",
252
252
  "node-edge-tts": "^1.2.10",