fraim-hub 2.0.231 → 2.0.233

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.
@@ -58,10 +58,8 @@ function parseSeekMentoringSignal(line) {
58
58
  typeof obj.item === 'object' && obj.item !== null) {
59
59
  const item = obj.item;
60
60
  const itemType = item.type;
61
- const tool = typeof item.tool === 'string' ? item.tool : null;
62
- if (itemType === 'mcp_tool_call' &&
63
- (tool === 'seekMentoring' || tool === 'mcp__fraim__seekMentoring')) {
64
- const args = item.arguments;
61
+ if (itemType === 'mcp_tool_call' && isFraimTool(item.tool, 'seekMentoring')) {
62
+ const args = normalizeToolArgs(item.arguments) || undefined;
65
63
  const sig = extractSignalFromArgs(args);
66
64
  if (sig)
67
65
  return sig;
@@ -82,13 +80,9 @@ function parseSeekMentoringSignal(line) {
82
80
  continue;
83
81
  const c = candidate;
84
82
  const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
85
- const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
86
- const isSeekMentoring = nameField === 'seekMentoring' ||
87
- nameField === 'mcp__fraim__seekMentoring' ||
88
- nameField.endsWith('seekMentoring');
89
- if (!isToolUse || !isSeekMentoring)
83
+ if (!isToolUse || !isFraimTool(readToolName(c), 'seekMentoring'))
90
84
  continue;
91
- const input = (c.input || c.arguments || c.parameters);
85
+ const input = normalizeToolArgs(c.input || c.arguments || c.parameters) || undefined;
92
86
  const sig = extractSignalFromArgs(input);
93
87
  if (sig)
94
88
  return sig;
@@ -116,8 +110,7 @@ function parseFraimJobLoadSignal(line) {
116
110
  if ((obj.type === 'item.started' || obj.type === 'item.completed') &&
117
111
  typeof obj.item === 'object' && obj.item !== null) {
118
112
  const item = obj.item;
119
- const tool = typeof item.tool === 'string' ? item.tool : '';
120
- if (item.type === 'mcp_tool_call' && isGetFraimJobTool(tool)) {
113
+ if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'get_fraim_job')) {
121
114
  const sig = readFraimJobFromArgs(item.arguments);
122
115
  if (sig)
123
116
  return sig;
@@ -138,8 +131,7 @@ function parseFraimJobLoadSignal(line) {
138
131
  continue;
139
132
  const c = candidate;
140
133
  const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
141
- const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
142
- if (!isToolUse || !isGetFraimJobTool(nameField))
134
+ if (!isToolUse || !isFraimTool(readToolName(c), 'get_fraim_job'))
143
135
  continue;
144
136
  const sig = readFraimJobFromArgs(c.input || c.arguments || c.parameters);
145
137
  if (sig)
@@ -276,8 +268,8 @@ function parseAgentIdentitySignal(line) {
276
268
  // Codex shape.
277
269
  if ((obj.type === 'item.started' || obj.type === 'item.completed') && typeof obj.item === 'object' && obj.item !== null) {
278
270
  const item = obj.item;
279
- if (item.type === 'mcp_tool_call' && item.tool === 'fraim_connect') {
280
- return readAgentFromArgs(item.arguments);
271
+ if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'fraim_connect')) {
272
+ return readAgentFromArgs(normalizeToolArgs(item.arguments) || undefined);
281
273
  }
282
274
  }
283
275
  // Claude Code shape.
@@ -293,10 +285,9 @@ function parseAgentIdentitySignal(line) {
293
285
  const c = candidate;
294
286
  if (c.type !== 'tool_use' && c.type !== 'function_call')
295
287
  continue;
296
- const name = typeof c.name === 'string' ? c.name : '';
297
- if (!name.endsWith('fraim_connect'))
288
+ if (!isFraimTool(readToolName(c), 'fraim_connect'))
298
289
  continue;
299
- const sig = readAgentFromArgs((c.input || c.arguments));
290
+ const sig = readAgentFromArgs(normalizeToolArgs(c.input || c.arguments || c.parameters) || undefined);
300
291
  if (sig)
301
292
  return sig;
302
293
  }
@@ -312,10 +303,31 @@ function readAgentFromArgs(args) {
312
303
  return null;
313
304
  return { agentName, agentModel };
314
305
  }
315
- function isGetFraimJobTool(toolName) {
316
- return toolName === 'get_fraim_job' ||
317
- toolName === 'mcp__fraim__get_fraim_job' ||
318
- toolName.endsWith('get_fraim_job');
306
+ function readToolName(candidate) {
307
+ if (typeof candidate.name === 'string')
308
+ return candidate.name;
309
+ if (typeof candidate.tool_name === 'string')
310
+ return candidate.tool_name;
311
+ if (typeof candidate.tool === 'string')
312
+ return candidate.tool;
313
+ if (typeof candidate.function === 'object' && candidate.function !== null) {
314
+ const fn = candidate.function;
315
+ if (typeof fn.name === 'string')
316
+ return fn.name;
317
+ }
318
+ return null;
319
+ }
320
+ function canonicalToolName(rawName) {
321
+ if (typeof rawName !== 'string')
322
+ return null;
323
+ const trimmed = rawName.trim();
324
+ if (!trimmed)
325
+ return null;
326
+ const byDoubleUnderscore = trimmed.split('__').filter(Boolean).pop() || trimmed;
327
+ return byDoubleUnderscore.split(/[./:]/).filter(Boolean).pop() || null;
328
+ }
329
+ function isFraimTool(rawName, canonicalName) {
330
+ return canonicalToolName(rawName) === canonicalName;
319
331
  }
320
332
  function readFraimJobFromArgs(rawArgs) {
321
333
  const args = normalizeToolArgs(rawArgs);
@@ -154,6 +154,14 @@ function getHubPersonaForJob(jobName) {
154
154
  return null;
155
155
  return getProtectedPersonaForHubJob(jobName) ?? DEFAULT_UNASSIGNED_PERSONA_KEY;
156
156
  }
157
+ // Issue #991: consult custom employee records before falling back to the catalog.
158
+ // Returns the key of the first custom employee whose jobIds array includes jobId,
159
+ // or null if no custom employee owns it.
160
+ function getCustomPersonaForJob(projectPath, jobId) {
161
+ const employees = (0, custom_employees_1.readCustomEmployees)(projectPath);
162
+ const match = employees.find((e) => Array.isArray(e.jobIds) && e.jobIds.includes(jobId));
163
+ return match ? match.key : null;
164
+ }
157
165
  const FRAIM_INTERNAL_JOB_IDS = new Set([
158
166
  'contribute-to-fraim',
159
167
  'create-registry-asset',
@@ -1237,6 +1245,9 @@ async function configureFraimForHubAgent(hubId) {
1237
1245
  }
1238
1246
  }
1239
1247
  function hubCommandVersion(command, extraBinDirs, basePath) {
1248
+ if (process.env.NODE_ENV === 'test' && process.env.FRAIM_TEST_HUB_COMMAND_VERSION_EMPTY === '1') {
1249
+ return null;
1250
+ }
1240
1251
  const executable = process.platform === 'win32' ? 'cmd.exe' : command;
1241
1252
  const args = process.platform === 'win32'
1242
1253
  ? ['/d', '/s', '/c', `${command} --version`]
@@ -1252,6 +1263,9 @@ function hubCommandVersion(command, extraBinDirs, basePath) {
1252
1263
  return raw || null;
1253
1264
  }
1254
1265
  function hubRunProcess(command, args, env) {
1266
+ if (process.env.NODE_ENV === 'test' && command === 'npm' && process.env.FRAIM_TEST_HUB_NPM_ERROR) {
1267
+ return Promise.reject(new Error(process.env.FRAIM_TEST_HUB_NPM_ERROR));
1268
+ }
1255
1269
  return new Promise((resolve, reject) => {
1256
1270
  const [realCmd, realArgs] = process.platform === 'win32'
1257
1271
  ? ['cmd.exe', ['/d', '/s', '/c', command, ...args]]
@@ -2446,7 +2460,7 @@ class AiHubServer {
2446
2460
  id: employeeJob.id,
2447
2461
  title: employeeJob.title,
2448
2462
  stubPath: employeeJob.stubPath,
2449
- personaKey: employeeJob.requiredPersonaKey ?? getHubPersonaForJob(employeeJob.id),
2463
+ personaKey: employeeJob.requiredPersonaKey ?? getCustomPersonaForJob(projectPath, employeeJob.id) ?? getHubPersonaForJob(employeeJob.id),
2450
2464
  };
2451
2465
  }
2452
2466
  const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
@@ -76,6 +76,8 @@ const guiAppDetect = (configSurfaceCheck, appName, options = {}) => {
76
76
  };
77
77
  };
78
78
  const availableByVersionProbe = (command) => {
79
+ if (process.env.FRAIM_DETECT_DISABLE_CLI_PROBES === '1')
80
+ return false;
79
81
  const result = process.platform === 'win32'
80
82
  ? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
81
83
  : (0, child_process_1.spawnSync)(command, ['--version'], { encoding: 'utf8', timeout: 1500 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.231",
3
+ "version": "2.0.233",
4
4
  "description": "FRAIM Hub local companion package.",
5
5
  "bin": {
6
6
  "fraim-hub": "bin/fraim-hub.js"
@@ -158,7 +158,7 @@
158
158
  "electron": "^41.2.2",
159
159
  "electron-updater": "^6.8.9",
160
160
  "express": "^5.2.1",
161
- "fraim": "2.0.231",
161
+ "fraim": "2.0.233",
162
162
  "mongodb": "^7.0.0",
163
163
  "node-cron": "4.2.1",
164
164
  "node-edge-tts": "^1.2.10",
@@ -73,6 +73,8 @@ const state = {
73
73
  // the active employee; the user never sees the raw invocation syntax.
74
74
  pendingCoachingJobId: null,
75
75
  pendingCoachingLabel: null,
76
+ // Issue #989 R1-R6: area preserved when a next-job chip opens step-2 pre-filled.
77
+ pendingNextJobArea: null,
76
78
  conversationPersistTimer: null,
77
79
  conversationRefreshHandle: null,
78
80
  conversationDiskAvailable: true,
@@ -300,7 +302,11 @@ function tfRefreshPersonaDependentSurfaces() {
300
302
  else if (tf.area === 'brain') tfRenderBrain();
301
303
  if (tf.area === 'projects') {
302
304
  if (tf.projectView === 'overview') tfRenderOverview();
303
- else tfRenderTree();
305
+ else {
306
+ tfRenderTree();
307
+ // tfRenderTree rebuilds tree chrome only; renderRail rebuilds per-employee accordion groups.
308
+ if (typeof renderRail === 'function') renderRail();
309
+ }
304
310
  }
305
311
  }
306
312
 
@@ -1664,7 +1670,7 @@ function renderRail() {
1664
1670
  titleSpan.className = 'conv-title';
1665
1671
  titleSpan.textContent = conv.title || '';
1666
1672
  bodyDiv.appendChild(titleSpan);
1667
- appendHubTimestamp(bodyDiv, conv.lastUpdatedAt, 'conv-time', 'Run last updated');
1673
+ appendHubTimestamp(bodyDiv, conversationStartTimestamp(conv), 'conv-time', 'Job started');
1668
1674
  btn.appendChild(bodyDiv);
1669
1675
  // Issue #566 R7: mark runs of a personalized (taught/customized) job.
1670
1676
  if (isTaughtJob(conv.jobId)) {
@@ -4851,7 +4857,14 @@ function renderNextJobRecommendations(conv) {
4851
4857
  : conv.scope === 'company' ? 'company'
4852
4858
  : 'projects';
4853
4859
  const employeeId = conversationAgentName(conv) || (state.selectedEmployeeId) || 'claude';
4854
- startRun(job, instructions, employeeId, undefined, area);
4860
+ // Issue #989 R1-R6: open step-2 with job pre-selected and instructions pre-filled
4861
+ // so the manager can review/edit before starting, instead of calling startRun directly.
4862
+ state.selectedJob = job;
4863
+ state.selectedEmployeeId = employeeId;
4864
+ state.pendingNextJobArea = area;
4865
+ showStep2(job);
4866
+ els['instructions'].value = instructions;
4867
+ els['start'].disabled = instructions.trim().length === 0;
4855
4868
  });
4856
4869
  }
4857
4870
  chips.appendChild(btn);
@@ -6018,6 +6031,7 @@ function closeModal() {
6018
6031
  els['modal'].classList.remove('open');
6019
6032
  els['modal'].hidden = true;
6020
6033
  state.activeFilter = null;
6034
+ state.pendingNextJobArea = null;
6021
6035
  }
6022
6036
 
6023
6037
  // Build the unified picker list. Employee jobs are organized by their
@@ -6707,7 +6721,15 @@ function foldRunIntoConversation(conv, run) {
6707
6721
  if (run.configuredAgentLabel) conv.configuredAgentLabel = run.configuredAgentLabel;
6708
6722
  if (run.baseHostId) conv.baseHostId = run.baseHostId;
6709
6723
  if (run.personaKey !== undefined) {
6710
- conv.personaKey = run.personaKey;
6724
+ // Issue #991: do not let a server-side catalog/default key overwrite a custom:*
6725
+ // key the client already holds. The client stamps custom:* at kickoff from the
6726
+ // user's explicit action (cpPersonaOverride); the server resolves it via
6727
+ // getCustomPersonaForJob, but the client guard is defense-in-depth.
6728
+ const isClientCustom = conv.personaKey && conv.personaKey.startsWith('custom:');
6729
+ const isServerCustom = run.personaKey && run.personaKey.startsWith('custom:');
6730
+ if (!isClientCustom || isServerCustom) {
6731
+ conv.personaKey = run.personaKey;
6732
+ }
6711
6733
  }
6712
6734
  // Issue #347: keep the latest server snapshot under conv.run so the
6713
6735
  // tracker / totals renderers can read it without re-doing work. Stages,
@@ -7561,6 +7583,9 @@ function wireEvents() {
7561
7583
  if (!job || !text) return;
7562
7584
  const employeeId = els['employee-select'].value || state.selectedEmployeeId;
7563
7585
  state.selectedEmployeeId = employeeId;
7586
+ // Issue #989 R4: consume pendingNextJobArea so the run lands in the right tab.
7587
+ const invokedArea = state.pendingNextJobArea || undefined;
7588
+ state.pendingNextJobArea = null;
7564
7589
  closeModal();
7565
7590
  // R2: if no project path is set, show the inline project picker instead.
7566
7591
  if (!state.projectPath) {
@@ -7574,7 +7599,7 @@ function wireEvents() {
7574
7599
  showHireStrip(job, pendingConvId, text, employeeId);
7575
7600
  return;
7576
7601
  }
7577
- await startRun(job, text, employeeId);
7602
+ await startRun(job, text, employeeId, undefined, invokedArea);
7578
7603
  });
7579
7604
  els['job-search'].addEventListener('input', () => renderJobCatalog(els['job-search'].value));
7580
7605
  els['modal'].addEventListener('click', (e) => {
@@ -10486,7 +10511,7 @@ function tfBuildManagerRunItem(conv) {
10486
10511
  const body = document.createElement('span'); body.className = 'conv-body';
10487
10512
  const title = document.createElement('span'); title.className = 'conv-title';
10488
10513
  title.textContent = conv.title || conv.jobTitle || conv.jobId || 'Run';
10489
- body.appendChild(title); appendHubTimestamp(body, conv.lastUpdatedAt, 'conv-time', 'Run last updated'); btn.appendChild(body);
10514
+ body.appendChild(title); appendHubTimestamp(body, conversationStartTimestamp(conv), 'conv-time', 'Job started'); btn.appendChild(body);
10490
10515
  const dotClass = conversationStateDotClass(conv);
10491
10516
  const dot = document.createElement('span'); dot.className = 'state-dot conv-state-dot dot-' + dotClass;
10492
10517
  if (typeof tfDotTitle === 'function') dot.title = tfDotTitle(dotClass);