fraim-hub 2.0.312 → 2.0.314

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.
@@ -19,12 +19,31 @@ const pack_home_1 = require("../cli/utils/pack-home");
19
19
  // Directories scanned for employee jobs at runtime, in lowest-to-highest
20
20
  // precedence order. Later entries win on {categoryId, jobId} collision.
21
21
  //
22
- // FRAIM execution normally reads only the two `fraim/` layers: the synced
23
- // baseline and the personalized override. The Hub can explicitly opt into the
22
+ // FRAIM execution normally reads three layers: the machine-level baseline
23
+ // (shared across every project on this machine), the project-level synced
24
+ // baseline, and the personalized override. The Hub can explicitly opt into the
24
25
  // source `registry/` fallback when running against the FRAIM repo before sync.
25
- // - <project>/fraim/ai-employee/jobs/<category>/ - synced baseline
26
- // - <project>/fraim/personalized-employee/jobs/<category>/ - taught/customized override
26
+ // - ~/.fraim/ai-employee/jobs/<category>/ - machine-level baseline (issue #1644)
27
+ // - <project>/fraim/ai-employee/jobs/<category>/ - synced baseline
28
+ // - <project>/fraim/personalized-employee/jobs/<category>/ - taught/customized override
27
29
  // Only the personalized-employee layer is "personalized" (issue #566).
30
+ //
31
+ // Issue #1644: `fraim sync` Layer 1 (always runs, regardless of whether cwd is
32
+ // a FRAIM project) writes baseline job stubs to ~/.fraim/ai-employee/jobs/ and
33
+ // ~/.fraim/ai-manager/jobs/ so every project on the machine sees the current
34
+ // catalog without its own project-level sync. This is the lowest-precedence
35
+ // layer: a project- or personalized-level stub with the same id always wins.
36
+ //
37
+ // Resolved as a function, not a module-level constant: getUserFraimDirPath()
38
+ // reads FRAIM_USER_DIR at call time, and callers (including tests that
39
+ // isolate a temp machine home) need that resolved fresh on every discovery
40
+ // call, not frozen at module-import time.
41
+ function machineJobLayers(role) {
42
+ const root = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), role, 'jobs');
43
+ // Issue #1002 convention: a machine-level stub displays as `~/.fraim/...`,
44
+ // not a long `../../..` walk out of the project.
45
+ return [{ root, segments: [], personalized: false, displayRoot: root, displayPrefix: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`${role}/jobs`) }];
46
+ }
28
47
  const EMPLOYEE_JOB_LAYERS = [
29
48
  { segments: ['ai-employee', 'jobs'], personalized: false },
30
49
  ];
@@ -90,8 +109,14 @@ function resolveExtendedStubPath(projectPath, extendsValue) {
90
109
  if (!relative || relative.includes('..'))
91
110
  return null;
92
111
  const candidateRoots = [
112
+ // Project-level checked first, matching the precedence used everywhere
113
+ // else in this file (project overrides machine on a name collision).
93
114
  path_1.default.join(projectPath, 'fraim', 'ai-employee', 'jobs'),
94
115
  path_1.default.join(projectPath, 'fraim', 'ai-manager', 'jobs'),
116
+ // Issue #1644: the baseline a stub `extends` may exist only at the
117
+ // machine-level home now that Layer 1 writes it there.
118
+ path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-employee', 'jobs'),
119
+ path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-manager', 'jobs'),
95
120
  path_1.default.join(projectPath, 'registry', 'jobs', 'ai-employee'),
96
121
  path_1.default.join(projectPath, 'registry', 'jobs', 'ai-manager'),
97
122
  ];
@@ -286,8 +311,8 @@ function discoverEmployeeJobs(projectPath, options = {}) {
286
311
  if (!project.exists || (!project.hasFraim && !options.includeRegistry))
287
312
  return [];
288
313
  const layers = discoverLayers(projectPath, options.includeRegistry
289
- ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]
290
- : [...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]);
314
+ ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...machineJobLayers('ai-employee'), ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]
315
+ : [...machineJobLayers('ai-employee'), ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]);
291
316
  // Group by categoryId so all layers contribute to the same labelled category.
292
317
  const jobsByKey = new Map();
293
318
  for (const layer of layers) {
@@ -313,8 +338,8 @@ function discoverManagerTemplates(projectPath, options = {}) {
313
338
  if (!project.exists || (!project.hasFraim && !options.includeRegistry))
314
339
  return [];
315
340
  const layers = discoverLayers(projectPath, options.includeRegistry
316
- ? [...REGISTRY_MANAGER_JOB_LAYERS, ...MANAGER_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'manager-jobs')]
317
- : [...MANAGER_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'manager-jobs')]);
341
+ ? [...REGISTRY_MANAGER_JOB_LAYERS, ...machineJobLayers('ai-manager'), ...MANAGER_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'manager-jobs')]
342
+ : [...machineJobLayers('ai-manager'), ...MANAGER_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'manager-jobs')]);
318
343
  const templatesByKey = new Map();
319
344
  for (const layer of layers) {
320
345
  const groupLabel = humanizeName(layer.categoryId);
@@ -401,6 +426,8 @@ function findJobStubPath(projectPath, jobId) {
401
426
  // inspect any FRAIM job run, including manager-side jobs such as
402
427
  // sleep-on-learnings.
403
428
  const layers = discoverLayers(projectPath, [
429
+ ...machineJobLayers('ai-employee'),
430
+ ...machineJobLayers('ai-manager'),
404
431
  ...EMPLOYEE_JOB_LAYERS,
405
432
  ...MANAGER_JOB_LAYERS,
406
433
  ...personalizedLayerSegments(projectPath, 'jobs'),
@@ -518,8 +545,8 @@ function getAiHubCategories(projectPath, options = {}) {
518
545
  return [];
519
546
  // A category is any directory found at any layer; deduplicate by id.
520
547
  const layers = discoverLayers(projectPath, options.includeRegistry
521
- ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]
522
- : [...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]);
548
+ ? [...REGISTRY_EMPLOYEE_JOB_LAYERS, ...machineJobLayers('ai-employee'), ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]
549
+ : [...machineJobLayers('ai-employee'), ...EMPLOYEE_JOB_LAYERS, ...personalizedLayerSegments(projectPath, 'jobs')]);
523
550
  const seen = new Map();
524
551
  for (const layer of layers) {
525
552
  if (!seen.has(layer.categoryId)) {
@@ -41,6 +41,7 @@ exports.selfHealBrokenManagedAgents = selfHealBrokenManagedAgents;
41
41
  exports.configureFraimForHubAgent = configureFraimForHubAgent;
42
42
  exports.installAgentAndRefreshDetection = installAgentAndRefreshDetection;
43
43
  exports.hubCommandVersion = hubCommandVersion;
44
+ exports.__setStartupSyncSpawnForTests = __setStartupSyncSpawnForTests;
44
45
  exports.__setAgentInstallTimeoutMsForTests = __setAgentInstallTimeoutMsForTests;
45
46
  exports.hubRunProcess = hubRunProcess;
46
47
  exports.buildOpenFileInvocation = buildOpenFileInvocation;
@@ -2147,6 +2148,13 @@ function hubRunSync(command, args, env) {
2147
2148
  // (typically seconds) while still bounding the failure the way AC-C1 requires.
2148
2149
  const DEFAULT_AGENT_INSTALL_TIMEOUT_MS = 90_000;
2149
2150
  let agentInstallTimeoutMs = DEFAULT_AGENT_INSTALL_TIMEOUT_MS;
2151
+ // Test seam for the startup machine-level sync (Issue #1645). Allows tests to
2152
+ // intercept the spawn call without actually invoking npx.
2153
+ let startupSyncSpawnFn = child_process_1.spawn;
2154
+ /** Test seam only. Pass null to restore the production spawn. */
2155
+ function __setStartupSyncSpawnForTests(fn) {
2156
+ startupSyncSpawnFn = fn ?? child_process_1.spawn;
2157
+ }
2150
2158
  /** Test seam only. Mirrors __setEmployeeDetectionTtlForTests in hosts.ts — proves the bound
2151
2159
  * actually fires without a real 90s hang. Pass null to restore the production value. */
2152
2160
  function __setAgentInstallTimeoutMsForTests(ms) {
@@ -3265,6 +3273,17 @@ class AiHubServer {
3265
3273
  console.warn('[ai-hub] self-heal pass failed:', error?.message || error);
3266
3274
  });
3267
3275
  }
3276
+ // Issue #1645: fire-and-forget machine-level sync so the delegate job list
3277
+ // is current within the session. Skipped in NODE_ENV=test to avoid spawning
3278
+ // real processes in the test suite's many short-lived Hub instances.
3279
+ if (process.env.NODE_ENV !== 'test') {
3280
+ const syncChild = startupSyncSpawnFn('npx', ['fraim', 'sync', '--global'], {
3281
+ detached: true,
3282
+ stdio: 'ignore',
3283
+ });
3284
+ syncChild.on('error', () => { });
3285
+ syncChild.unref();
3286
+ }
3268
3287
  // Update-badge freshness: refresh the npm-latest cache on our own clock, not just when
3269
3288
  // a page load or relaunch decision happens to ask for it. Without this, a Hub that's
3270
3289
  // resident in the tray for days with no window reload and no tray re-click never learns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.312",
3
+ "version": "2.0.314",
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",
@@ -211,7 +211,7 @@
211
211
  "electron-updater": "^6.8.9",
212
212
  "express": "^5.2.1",
213
213
  "extract-zip": "^2.0.1",
214
- "fraim": "2.0.312",
214
+ "fraim": "2.0.314",
215
215
  "mongodb": "^7.0.0",
216
216
  "node-cron": "4.2.1",
217
217
  "node-edge-tts": "^1.2.10",
@@ -5669,7 +5669,6 @@ function scrollThreadAfterViewportSync(conv, shouldScrollForUpdate, forceBottom)
5669
5669
  if (latest.status === 'running') {
5670
5670
  ensurePendingAnchorScrollListener(host);
5671
5671
  if (!forceBottom && pendingAnchorUserScrolled(host)) {
5672
- host.scrollTop = 0;
5673
5672
  return;
5674
5673
  }
5675
5674
  const pendingRow = host.querySelector('.message[data-pending="true"]');