fraim-hub 2.0.232 → 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.
@@ -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',
@@ -2452,7 +2460,7 @@ class AiHubServer {
2452
2460
  id: employeeJob.id,
2453
2461
  title: employeeJob.title,
2454
2462
  stubPath: employeeJob.stubPath,
2455
- personaKey: employeeJob.requiredPersonaKey ?? getHubPersonaForJob(employeeJob.id),
2463
+ personaKey: employeeJob.requiredPersonaKey ?? getCustomPersonaForJob(projectPath, employeeJob.id) ?? getHubPersonaForJob(employeeJob.id),
2456
2464
  };
2457
2465
  }
2458
2466
  const managerTemplate = (0, catalog_1.discoverManagerTemplates)(projectPath).find((job) => job.id === jobId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.232",
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.232",
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);