fraim-hub 2.0.341 → 2.0.342

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.
@@ -27,7 +27,7 @@ const HEADER_FIELDS = [
27
27
  'runId', 'status', 'pauseReason', 'createdAt', 'lastUpdatedAt', 'issueNumber',
28
28
  'sourceTrigger', 'blocked', 'stopped', 'reviewApproved', 'compareMode', 'compareRunId',
29
29
  'managedByRunId', 'managedByPersonaKey', 'managedReviewStatus', 'delegationTaskId',
30
- 'humanCoachingDisabled', 'personaSnapshot', 'reviewHandoff',
30
+ 'humanCoachingDisabled', 'personaSnapshot', 'reviewHandoff', 'proposalSet',
31
31
  ];
32
32
  const HEADER_FIELD_SET = new Set([...HEADER_FIELDS, 'personaSnapshot', 'delegation', 'reviewHandoff']);
33
33
  /**
@@ -571,7 +571,7 @@ class AiHubConversationStore {
571
571
  try {
572
572
  const parsed = JSON.parse(fs_1.default.readFileSync(this.indexPath(bucketDir), 'utf8'));
573
573
  if (Array.isArray(parsed.headers)) {
574
- return { activeId: parsed.activeId ?? null, headers: parsed.headers };
574
+ return { version: parsed.version, activeId: parsed.activeId ?? null, headers: parsed.headers };
575
575
  }
576
576
  }
577
577
  catch {
@@ -583,12 +583,12 @@ class AiHubConversationStore {
583
583
  // source of truth) and persist it best-effort.
584
584
  loadIndex(bucketDir, bucketKey) {
585
585
  const existing = this.readIndex(bucketDir);
586
- if (existing) {
586
+ if (existing && existing.version === 3) {
587
587
  const sanitized = sanitizeBucketIndex(existing);
588
588
  if (sanitized.changed) {
589
589
  try {
590
590
  (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), {
591
- version: 2,
591
+ version: 3,
592
592
  bucketKey,
593
593
  activeId: sanitized.index.activeId,
594
594
  headers: sanitized.index.headers,
@@ -601,10 +601,13 @@ class AiHubConversationStore {
601
601
  return sanitized.index;
602
602
  }
603
603
  const headers = this.readAllConversations(bucketDir, bucketKey).map(toHeader).sort(newestFirst);
604
- const rebuilt = { activeId: null, headers };
604
+ // Older indexes predate proposalSet in headers. Rebuild from canonical shards
605
+ // once so pending reviews are visible immediately after upgrading.
606
+ const activeId = existing?.activeId && headers.some((header) => header.id === existing.activeId) ? existing.activeId : null;
607
+ const rebuilt = { activeId, headers };
605
608
  if (headers.length > 0) {
606
609
  try {
607
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey, activeId: null, headers });
610
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey, activeId, headers });
608
611
  }
609
612
  catch {
610
613
  /* best-effort */
@@ -623,7 +626,7 @@ class AiHubConversationStore {
623
626
  writeIndex(bucketDir, bucketKey, activeId, conversations) {
624
627
  const headers = conversations.map(toHeader).sort(newestFirst);
625
628
  const validActive = activeId && headers.some((h) => h.id === activeId) ? activeId : null;
626
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey, activeId: validActive, headers });
629
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey, activeId: validActive, headers });
627
630
  }
628
631
  // Author a whole bucket to exactly `conversations` (used by migration and replaceProject).
629
632
  writeBucket(bucketKey, state) {
@@ -872,7 +875,7 @@ class AiHubConversationStore {
872
875
  const state = (0, conversation_store_lock_1.withBucketLock)(this.lockPath(bucketDir), () => {
873
876
  const idx = this.loadIndex(bucketDir, key);
874
877
  const valid = activeId && idx.headers.some((h) => h.id === activeId) ? activeId : null;
875
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey: key, activeId: valid, headers: idx.headers });
878
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey: key, activeId: valid, headers: idx.headers });
876
879
  return normalizeProjectState(key, { activeId: valid, conversations: idx.headers });
877
880
  });
878
881
  if (!bucketAlreadyExisted)
@@ -937,7 +940,7 @@ class AiHubConversationStore {
937
940
  if (!conversationExists) {
938
941
  (0, conversation_search_projection_1.deleteConversationSearchEntry)(bucketDir, conversationId);
939
942
  if (headers.length !== index.headers.length || activeId !== index.activeId) {
940
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey: key, activeId, headers });
943
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey: key, activeId, headers });
941
944
  }
942
945
  return { removed: false, activeId };
943
946
  }
@@ -947,7 +950,7 @@ class AiHubConversationStore {
947
950
  (0, conversation_search_projection_1.deleteConversationSearchEntry)(bucketDir, conversationId);
948
951
  fs_1.default.rmSync(this.indexPath(bucketDir), { force: true });
949
952
  fs_1.default.rmSync(conversationPath, { force: true });
950
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey: key, activeId, headers });
953
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey: key, activeId, headers });
951
954
  return { removed: true, activeId };
952
955
  });
953
956
  }
@@ -960,7 +963,7 @@ class AiHubConversationStore {
960
963
  let nextActive = activeId === undefined ? idx.activeId : activeId;
961
964
  if (nextActive && !headers.some((h) => h.id === nextActive))
962
965
  nextActive = null;
963
- (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 2, bucketKey, activeId: nextActive, headers });
966
+ (0, atomic_json_file_1.writeJsonAtomic)(this.indexPath(bucketDir), { version: 3, bucketKey, activeId: nextActive, headers });
964
967
  return normalizeProjectState(bucketKey, { activeId: nextActive, conversations: headers });
965
968
  }
966
969
  stateFromIndex(bucketDir, bucketKey) {
@@ -69,7 +69,7 @@ ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
69
69
  Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
70
70
 
71
71
  2. **If the user did not specify a FRAIM job or topic after activation**:
72
- If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect \`fraim/personalized-employee/jobs/\` for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
72
+ If local FRAIM job stubs are present in the workspace, read that jobs directory's \`INDEX.md\` first (it lists every job's name, one-line intent, and category) and match the request against it; open an individual stub only to confirm a shortlisted candidate, or fall back to scanning the stubs directly if no \`INDEX.md\` is present. Also check \`fraim/personalized-employee/jobs/\` (and its own \`INDEX.md\`, if present) for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
73
73
 
74
74
  3. **Find the match**:
75
75
  If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists, call \`get_fraim_job({ job: "adhoc-prompt" })\` directly and execute it with the user's instructions as the task input — no confirmation question, and do not pick the nearest catalog job.
@@ -520,33 +520,60 @@ class LocalRegistryResolver {
520
520
  // publish or sync and resolves at all under `single-machine`).
521
521
  const managerLocalContent = this.readManagerLocalOverlayFile(path);
522
522
  if (managerLocalContent !== null) {
523
+ // Issue #1867: apply inheritance so an extends-based override merges with its parent.
524
+ let resolvedManagerLocal;
525
+ try {
526
+ resolvedManagerLocal = await this.resolveInheritance(managerLocalContent, path);
527
+ }
528
+ catch {
529
+ resolvedManagerLocal = { content: managerLocalContent, imports: [] };
530
+ }
523
531
  return {
524
- content: managerLocalContent,
532
+ content: resolvedManagerLocal.content,
525
533
  source: 'local',
526
534
  personalized: true,
527
- inherited: false,
535
+ inherited: resolvedManagerLocal.imports.length > 0,
536
+ imports: resolvedManagerLocal.imports.length > 0 ? resolvedManagerLocal.imports : undefined,
528
537
  scope: 'manager'
529
538
  };
530
539
  }
531
540
  // Issue #869: manager overlay (precedence 3 — the synced cross-machine copy)
532
541
  const managerOverlayContent = this.readManagerOverlayFile(path);
533
542
  if (managerOverlayContent !== null) {
543
+ // Issue #1867: apply inheritance so an extends-based override merges with its parent.
544
+ let resolvedManagerOverlay;
545
+ try {
546
+ resolvedManagerOverlay = await this.resolveInheritance(managerOverlayContent, path);
547
+ }
548
+ catch {
549
+ resolvedManagerOverlay = { content: managerOverlayContent, imports: [] };
550
+ }
534
551
  return {
535
- content: managerOverlayContent,
552
+ content: resolvedManagerOverlay.content,
536
553
  source: 'local',
537
554
  personalized: true,
538
- inherited: false,
555
+ inherited: resolvedManagerOverlay.imports.length > 0,
556
+ imports: resolvedManagerOverlay.imports.length > 0 ? resolvedManagerOverlay.imports : undefined,
539
557
  scope: 'manager'
540
558
  };
541
559
  }
542
560
  // Issue #869 Phase 2: org overlay (precedence 3 — after manager, before synced baseline)
543
561
  const orgOverlayContent = this.readOrgOverlayFile(path);
544
562
  if (orgOverlayContent !== null) {
563
+ // Issue #1867: apply inheritance so an extends-based override merges with its parent.
564
+ let resolvedOrgOverlay;
565
+ try {
566
+ resolvedOrgOverlay = await this.resolveInheritance(orgOverlayContent, path);
567
+ }
568
+ catch {
569
+ resolvedOrgOverlay = { content: orgOverlayContent, imports: [] };
570
+ }
545
571
  return {
546
- content: orgOverlayContent,
572
+ content: resolvedOrgOverlay.content,
547
573
  source: 'local',
548
574
  personalized: true,
549
- inherited: false,
575
+ inherited: resolvedOrgOverlay.imports.length > 0,
576
+ imports: resolvedOrgOverlay.imports.length > 0 ? resolvedOrgOverlay.imports : undefined,
550
577
  scope: 'org'
551
578
  };
552
579
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim-hub",
3
- "version": "2.0.341",
3
+ "version": "2.0.342",
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",
@@ -231,7 +231,7 @@
231
231
  "electron-updater": "^6.8.9",
232
232
  "express": "^5.2.1",
233
233
  "extract-zip": "^2.0.1",
234
- "fraim": "2.0.341",
234
+ "fraim": "2.0.342",
235
235
  "mongodb": "^7.0.0",
236
236
  "node-cron": "4.2.1",
237
237
  "node-edge-tts": "^1.2.10",
@@ -362,6 +362,8 @@
362
362
  .learn-prop .lp-skip.on {
363
363
  background: var(--danger, #a24747); color: #fff; border-color: transparent;
364
364
  }
365
+ :root[data-theme="dark"] .learn-prop .lp-promote.on,
366
+ :root[data-theme="dark"] .learn-prop .lp-skip.on { color: var(--bg); }
365
367
  .learn-prop .lp-reason { font-size: 11.5px; color: var(--muted); line-height: 1.5; }
366
368
  .learn-prop .lp-reason b { color: var(--text); font-weight: 600; }
367
369
  .learn-prop .lp-why { display: flex; flex-direction: column; gap: 5px; margin-top: 2px; }
@@ -387,9 +389,8 @@
387
389
  gets its own slightly darker text rather than restyling every managed-disabled
388
390
  control on surfaces this change did not validate. */
389
391
  .review-actions.learn-batch .rb-approve.managed-disabled { color: #5f5f5f; }
390
- /* A skipped row's level choice is moot: no level reads as chosen, and the whole
391
- picker recedes so the row's one live state is the skip. */
392
- .learn-prop .lp-level-pick.lp-moot { opacity: 0.55; }
392
+ /* Choosing a level restores a skipped proposal, so these controls stay readable. */
393
+ .learn-prop .lp-level-pick.lp-moot { opacity: 1; }
393
394
  :root[data-theme="dark"] .review-actions.learn-batch .rb-approve.managed-disabled { color: #cfcfcf; }
394
395
 
395
396
  /* ── R10: Reverse mentoring items (Got it / Dismiss) ── */
@@ -9366,6 +9366,7 @@ function foldRunIntoConversation(conv, run) {
9366
9366
  conv.hostSessions = { ...(conv.hostSessions || {}), ...run.hostSessions };
9367
9367
  }
9368
9368
  const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
9369
+ conv.proposalSet = run.proposalSet || null;
9369
9370
  if (runHandoff) {
9370
9371
  conv.reviewHandoff = runHandoff;
9371
9372
  } else if (run.reviewHandoff) {