fraim-framework 2.0.171 → 2.0.174

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.
Files changed (32) hide show
  1. package/dist/src/ai-hub/hosts.js +227 -6
  2. package/dist/src/ai-hub/server.js +1014 -35
  3. package/dist/src/cli/commands/add-ide.js +2 -0
  4. package/dist/src/cli/commands/cleanup-artifacts.js +39 -0
  5. package/dist/src/cli/commands/init-project.js +12 -5
  6. package/dist/src/cli/commands/sync.js +74 -7
  7. package/dist/src/cli/fraim.js +2 -0
  8. package/dist/src/cli/setup/ide-detector.js +6 -0
  9. package/dist/src/cli/utils/agent-adapters.js +40 -18
  10. package/dist/src/cli/utils/fraim-gitignore.js +13 -0
  11. package/dist/src/cli/utils/remote-sync.js +129 -53
  12. package/dist/src/cli/utils/user-config.js +12 -0
  13. package/dist/src/config/ai-manager-hiring.js +121 -0
  14. package/dist/src/config/compat.js +16 -0
  15. package/dist/src/config/feature-flags.js +25 -0
  16. package/dist/src/config/persona-capability-bundles.js +273 -0
  17. package/dist/src/config/persona-hiring.js +270 -0
  18. package/dist/src/config/portfolio-slug-overrides.js +17 -0
  19. package/dist/src/config/pricing.js +37 -0
  20. package/dist/src/config/stripe.js +43 -0
  21. package/dist/src/core/fraim-config-schema.generated.js +8 -2
  22. package/dist/src/core/utils/local-registry-resolver.js +26 -0
  23. package/dist/src/core/utils/project-fraim-paths.js +89 -2
  24. package/dist/src/first-run/session-service.js +9 -0
  25. package/dist/src/local-mcp-server/artifact-retention-cleanup.js +298 -0
  26. package/dist/src/local-mcp-server/learning-context-builder.js +41 -81
  27. package/dist/src/local-mcp-server/stdio-server.js +42 -7
  28. package/package.json +5 -1
  29. package/public/ai-hub/index.html +205 -89
  30. package/public/ai-hub/review.css +12 -0
  31. package/public/ai-hub/script.js +1720 -240
  32. package/public/ai-hub/styles.css +473 -6
@@ -15,6 +15,8 @@ const TREE_WIDTH_MIN = 176;
15
15
  const TREE_WIDTH_MAX = 380;
16
16
  const TREE_WIDTH_DEFAULT = 216;
17
17
  const PAGE_SCOPED_JOBS = new Set(['organization-onboarding', 'manager-agreements', 'project-onboarding', 'organizational-learning-synthesis']);
18
+ // Jobs scoped to the Company/Manager area — never shown in the Projects workspace rail.
19
+ const AREA_SCOPED_JOBS = new Set(['organization-onboarding', 'organizational-learning-synthesis', 'manager-agreements']);
18
20
 
19
21
  const state = {
20
22
  bootstrap: null,
@@ -36,6 +38,7 @@ const state = {
36
38
  pendingCoachingJobId: null,
37
39
  pendingCoachingLabel: null,
38
40
  conversationPersistTimer: null,
41
+ conversationRefreshHandle: null,
39
42
  conversationDiskAvailable: true,
40
43
  // Issue #539: command palette state
41
44
  cpSelectedJob: null, // {id, title, intent, requiredPersonaKey}
@@ -424,16 +427,92 @@ async function hydrateConversationsFromServer() {
424
427
  for (const conv of conversations) normalizeGeminiConversationMessages(conv);
425
428
  state.conversations[state.projectPath] = conversations;
426
429
  const activeCandidates = [state.activeId, payload.activeId].filter(Boolean);
427
- state.activeId = activeCandidates.find((id) => conversations.some((conv) => conv.id === id)) || (conversations[0] ? conversations[0].id : null);
430
+ // Area-scoped jobs must not become the Projects workspace active conversation
431
+ // they are shown in their own Company/Manager panels via tfActiveOrgConv/tfActiveMgrConv.
432
+ const projectConv = (id) => conversations.find((c) => c.id === id && !AREA_SCOPED_JOBS.has(c.jobId));
433
+ const firstProjectConv = conversations.find((c) => !AREA_SCOPED_JOBS.has(c.jobId));
434
+ const bestActive = activeCandidates.map(projectConv).find(Boolean) || firstProjectConv || null;
435
+ state.activeId = bestActive ? bestActive.id : null;
428
436
  state.conversationDiskAvailable = true;
429
437
  renderRail();
430
438
  renderActive();
439
+ syncConversationRefreshPolling();
431
440
  } catch (error) {
432
441
  state.conversationDiskAvailable = false;
433
442
  console.warn('Could not hydrate conversations from server:', error);
434
443
  }
435
444
  }
436
445
 
446
+ // Background poll: pick up conversations created by scheduled/webhook runs
447
+ // without requiring the user to reload. Merges new/updated conversations into
448
+ // the existing list, preserving the active selection.
449
+ async function bgRefreshConversations() {
450
+ if (!state.projectPath) return;
451
+ try {
452
+ const payload = await requestJson(`/api/ai-hub/conversations?projectPath=${encodeURIComponent(state.projectPath)}`);
453
+ const incoming = Array.isArray(payload.conversations) ? payload.conversations : [];
454
+ if (!incoming.length) return;
455
+ const existing = state.conversations[state.projectPath] || [];
456
+ const existingIds = new Set(existing.map((c) => c.id));
457
+ let changed = false;
458
+ for (const conv of incoming) {
459
+ normalizeGeminiConversationMessages(conv);
460
+ if (!existingIds.has(conv.id)) {
461
+ existing.push(conv);
462
+ changed = true;
463
+ } else {
464
+ // Update status/messages on known conversations (e.g. running → completed)
465
+ const idx = existing.findIndex((c) => c.id === conv.id);
466
+ if (idx !== -1 && existing[idx].status !== conv.status) {
467
+ existing[idx] = conv;
468
+ changed = true;
469
+ }
470
+ }
471
+ }
472
+ if (changed) {
473
+ state.conversations[state.projectPath] = existing;
474
+ renderRail();
475
+ }
476
+ } catch (_) { /* silent — background poll, not critical */ }
477
+ }
478
+
479
+ let _bgConvPollHandle = null;
480
+ function startBgConvPoll() {
481
+ if (_bgConvPollHandle) return;
482
+ _bgConvPollHandle = window.setInterval(bgRefreshConversations, 30000);
483
+ }
484
+ function stopBgConvPoll() {
485
+ if (_bgConvPollHandle) { window.clearInterval(_bgConvPollHandle); _bgConvPollHandle = null; }
486
+ }
487
+
488
+ function needsConversationRefresh() {
489
+ const active = activeConversation();
490
+ if (!active) return false;
491
+ if (isManagedDelegationChild(active) && active.status === 'running') return true;
492
+ const ledger = normalizeDelegationLedger(active.delegation);
493
+ if (!ledger) return false;
494
+ return ledger.tasks.some((task) => {
495
+ const child = delegationTaskConversation(task, ledger);
496
+ if (!child) return true;
497
+ if (child.status === 'running') return true;
498
+ if (child.status === 'completed' && child.managedReviewStatus !== 'reviewed') return true;
499
+ return false;
500
+ });
501
+ }
502
+
503
+ function syncConversationRefreshPolling() {
504
+ const shouldPoll = needsConversationRefresh();
505
+ if (shouldPoll && !state.conversationRefreshHandle) {
506
+ state.conversationRefreshHandle = window.setInterval(() => {
507
+ hydrateConversationsFromServer().catch((error) =>
508
+ console.warn('Could not refresh delegated conversations:', error));
509
+ }, 1500);
510
+ } else if (!shouldPoll && state.conversationRefreshHandle) {
511
+ window.clearInterval(state.conversationRefreshHandle);
512
+ state.conversationRefreshHandle = null;
513
+ }
514
+ }
515
+
437
516
  function conversationTimestamp(conv) {
438
517
  const value = conv && conv.lastUpdatedAt;
439
518
  if (typeof value === 'number' && Number.isFinite(value)) return value;
@@ -476,6 +555,7 @@ function panelStateFor(convId) {
476
555
  }
477
556
 
478
557
  function defaultCoachOpen(conv) {
558
+ if (isManagerOversightConversation(conv)) return false;
479
559
  return true;
480
560
  }
481
561
 
@@ -535,6 +615,7 @@ function syncQuickCoachButtons(conv) {
535
615
  function stripReviewHandoffBlocks(text) {
536
616
  return String(text || '')
537
617
  .replace(/<review_handoff>\s*[\s\S]*?\s*<\/review_handoff>/gi, '')
618
+ .replace(/\[Thought:\s*true\]\s*/gi, '')
538
619
  .replace(/\n{3,}/g, '\n\n')
539
620
  .trim();
540
621
  }
@@ -707,11 +788,22 @@ function renderRail() {
707
788
  // #521: in the project workspace, project-lifecycle jobs (onboarding +
708
789
  // sleep-on-learnings) have their own "Project Updates" home, so keep them out
709
790
  // of the general Runs list to avoid showing the same run twice.
710
- const inWorkspace = !!document.querySelector('.workspace-conv');
791
+ const inWorkspace = !!document.querySelector('#proj-workspace .workspace-conv');
711
792
  const projectUpdateJobs = new Set(['project-onboarding', 'sleep-on-learnings']);
712
- const list = projectConversations().filter((conv) =>
793
+ const allProjectConversations = projectConversations();
794
+ const managedChildren = allProjectConversations.filter(isManagedDelegationChild);
795
+ const childrenByManager = new Map();
796
+ for (const child of managedChildren) {
797
+ const parentId = child.managedByRunId;
798
+ if (!parentId) continue;
799
+ if (!childrenByManager.has(parentId)) childrenByManager.set(parentId, []);
800
+ childrenByManager.get(parentId).push(child);
801
+ }
802
+ const list = allProjectConversations.filter((conv) =>
713
803
  (!state.selectedPersonaKey || conv.personaKey === state.selectedPersonaKey) &&
714
- !(inWorkspace && projectUpdateJobs.has(conv.jobId))
804
+ !isManagedDelegationChild(conv) &&
805
+ !(inWorkspace && projectUpdateJobs.has(conv.jobId)) &&
806
+ !AREA_SCOPED_JOBS.has(conv.jobId)
715
807
  );
716
808
 
717
809
  // Issue #550: Two-path routing for ad-hoc (freeform) conversations.
@@ -740,50 +832,80 @@ function renderRail() {
740
832
  groups.get(key).conversations.push(conv);
741
833
  }
742
834
 
835
+ function buildRunButton(conv, options = {}) {
836
+ const btn = document.createElement('button');
837
+ btn.type = 'button';
838
+ btn.className = (options.child ? 'conv-item conv-item--delegated-child' : 'conv-item') + (state.activeId === conv.id ? ' active' : '');
839
+ btn.dataset.conv = conv.id;
840
+ const bodyDiv = document.createElement('span');
841
+ bodyDiv.className = 'conv-body';
842
+ const titleSpan = document.createElement('span');
843
+ titleSpan.className = 'conv-title';
844
+ titleSpan.textContent = conv.title || '';
845
+ bodyDiv.appendChild(titleSpan);
846
+ btn.appendChild(bodyDiv);
847
+ // Issue #566 R7: mark runs of a personalized (taught/customized) job.
848
+ if (isTaughtJob(conv.jobId)) {
849
+ const taught = document.createElement('span');
850
+ taught.className = 'taught-badge';
851
+ taught.textContent = 'Personalized';
852
+ taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
853
+ btn.appendChild(taught);
854
+ }
855
+ const dotClass = conversationStateDotClass(conv);
856
+ const statusDot = document.createElement('span');
857
+ statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
858
+ statusDot.title = tfDotTitle(dotClass);
859
+ btn.appendChild(statusDot);
860
+ const statusSpan = document.createElement('span');
861
+ statusSpan.className = 'conv-status';
862
+ statusSpan.textContent = conversationStateLabel(conv);
863
+ statusSpan.classList.add(conversationUiState(conv));
864
+ btn.appendChild(statusSpan);
865
+ // Issue #578: sourceTrigger chip - show scheduled/webhook badge on automated runs.
866
+ if (conv.sourceTrigger && conv.sourceTrigger !== 'manager') {
867
+ const trigBadge = document.createElement('span');
868
+ trigBadge.className = 'trigger-badge trigger-badge--' + conv.sourceTrigger;
869
+ trigBadge.textContent = conv.sourceTrigger === 'scheduled' ? 'Scheduled' : 'Webhook';
870
+ trigBadge.title = conv.sourceTrigger === 'scheduled'
871
+ ? 'This run was started by the built-in cron scheduler'
872
+ : 'This run was triggered by an inbound webhook';
873
+ btn.appendChild(trigBadge);
874
+ }
875
+ if (conv.compareMode === 'ab') {
876
+ const badge = document.createElement('span');
877
+ badge.className = 'ab-badge';
878
+ badge.textContent = 'A/B';
879
+ btn.appendChild(badge);
880
+ }
881
+ if (!options.child) tfAttachRunDelete(btn, conv, { tree: false }); // #533 R6: delete a run
882
+ btn.addEventListener('click', () => switchToConversation(conv.id));
883
+ return btn;
884
+ }
885
+
743
886
  // Helper: build the run-item buttons shared by employee groups and Watercooler.
744
887
  function buildGroupList(conversations) {
745
888
  const groupList = document.createElement('div');
746
889
  groupList.className = 'conv-employee-list';
747
890
  for (const conv of conversations) {
748
- const btn = document.createElement('button');
749
- btn.type = 'button';
750
- btn.className = 'conv-item' + (state.activeId === conv.id ? ' active' : '');
751
- btn.dataset.conv = conv.id;
752
- const bodyDiv = document.createElement('span');
753
- bodyDiv.className = 'conv-body';
754
- const titleSpan = document.createElement('span');
755
- titleSpan.className = 'conv-title';
756
- titleSpan.textContent = conv.title || '';
757
- bodyDiv.appendChild(titleSpan);
758
- btn.appendChild(bodyDiv);
759
- // Issue #566 R7: mark runs of a personalized (taught/customized) job.
760
- if (isTaughtJob(conv.jobId)) {
761
- const taught = document.createElement('span');
762
- taught.className = 'taught-badge';
763
- taught.textContent = 'Personalized';
764
- taught.title = 'Personalized for this project (taught or customized in your fraim/personalized-employee layer)';
765
- btn.appendChild(taught);
766
- }
767
- const dotClass = conversationStateDotClass(conv);
768
- const statusDot = document.createElement('span');
769
- statusDot.className = 'state-dot conv-state-dot dot-' + dotClass;
770
- statusDot.title = tfDotTitle(dotClass);
771
- btn.appendChild(statusDot);
772
- const statusSpan = document.createElement('span');
773
- statusSpan.className = 'conv-status';
774
- statusSpan.textContent = conversationStateLabel(conv);
775
- statusSpan.classList.add(conversationUiState(conv));
776
- btn.appendChild(statusSpan);
777
- // Issue #442: A/B badge on rail entry.
778
- if (conv.compareMode === 'ab') {
779
- const badge = document.createElement('span');
780
- badge.className = 'ab-badge';
781
- badge.textContent = 'A/B';
782
- btn.appendChild(badge);
783
- }
784
- tfAttachRunDelete(btn, conv, { tree: false }); // #533 R6: delete a run
785
- btn.addEventListener('click', () => switchToConversation(conv.id));
891
+ const btn = buildRunButton(conv);
786
892
  groupList.appendChild(btn);
893
+ const children = [
894
+ ...(childrenByManager.get(conv.id) || []),
895
+ ...(conv.runId && conv.runId !== conv.id ? (childrenByManager.get(conv.runId) || []) : []),
896
+ ];
897
+ if (children.length) {
898
+ const delegatedList = document.createElement('div');
899
+ delegatedList.className = 'conv-delegated-list';
900
+ const label = document.createElement('div');
901
+ label.className = 'conv-delegated-label';
902
+ label.textContent = 'Mandy-managed workstreams';
903
+ delegatedList.appendChild(label);
904
+ for (const child of children) {
905
+ delegatedList.appendChild(buildRunButton(child, { child: true }));
906
+ }
907
+ groupList.appendChild(delegatedList);
908
+ }
787
909
  }
788
910
  return groupList;
789
911
  }
@@ -887,8 +1009,18 @@ function conversationUiState(conv) {
887
1009
  // not stay stuck as stopped even if the stopped flag was not cleared.
888
1010
  if (conv.stopped && conv.status === 'failed') return 'stopped';
889
1011
  if (conv.status === 'failed') return 'waiting';
1012
+ if (isManagedDelegationChild(conv) && conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'complete';
890
1013
  if (conv.status === 'completed' && conv.reviewApproved) return 'complete';
891
- if (conv.status === 'completed') return 'waiting';
1014
+ if (conv.status === 'completed') {
1015
+ // For fully-delegate runs: if child workstreams are still active, the manager
1016
+ // is waiting for them — show 'working' not 'waiting' so the user isn't prompted
1017
+ // to act when the Hub will auto-route child deliverables back to Mandy.
1018
+ if (isManagerOversightConversation(conv)) {
1019
+ const hasActiveChildren = projectConversations().some(c => c.managedByRunId === conv.runId && c.status === 'running');
1020
+ if (hasActiveChildren) return 'working';
1021
+ }
1022
+ return 'waiting';
1023
+ }
892
1024
  return 'idle';
893
1025
  }
894
1026
 
@@ -903,6 +1035,12 @@ function conversationStateDotClass(conv) {
903
1035
  }
904
1036
 
905
1037
  function conversationStateLabel(conv) {
1038
+ if (isManagedDelegationChild(conv)) {
1039
+ if (conv.status === 'running') return 'Working for Mandy';
1040
+ if (conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'Reviewed by Mandy';
1041
+ if (conv.status === 'completed') return 'Submitted to Mandy';
1042
+ if (conv.status === 'failed') return 'Needs Mandy';
1043
+ }
906
1044
  const uiState = conversationUiState(conv);
907
1045
  if (uiState === 'working') return 'Working';
908
1046
  if (uiState === 'waiting') return 'Waiting on you';
@@ -912,6 +1050,17 @@ function conversationStateLabel(conv) {
912
1050
  return 'Idle';
913
1051
  }
914
1052
 
1053
+ function isManagedDelegationChild(conv) {
1054
+ return !!(conv && (conv.managedByRunId || conv.delegationTaskId || conv.humanCoachingDisabled));
1055
+ }
1056
+
1057
+ function managedByPersonaLabel(conv) {
1058
+ const key = conv && (conv.managedByPersonaKey || 'mandy');
1059
+ if (key === 'mandy') return 'Mandy';
1060
+ const persona = key ? personaMap().get(key) : null;
1061
+ return persona ? persona.displayName : 'Mandy';
1062
+ }
1063
+
915
1064
  function personaMap() {
916
1065
  const map = new Map();
917
1066
  for (const persona of state.bootstrap?.personas || []) {
@@ -1183,7 +1332,7 @@ function showHireStrip(job, conversationId, instructions, employeeId) {
1183
1332
  els['hire-strip'].hidden = false;
1184
1333
  // Switch workspace to conv mode so the hire strip (inside .page) is visible
1185
1334
  // even when there is no active conversation (which normally keeps it in mode-brief).
1186
- const wc = document.querySelector('.workspace-conv');
1335
+ const wc = document.querySelector('#proj-workspace .workspace-conv');
1187
1336
  if (wc) { wc.classList.add('mode-conv'); wc.classList.remove('mode-brief'); }
1188
1337
  }
1189
1338
 
@@ -1289,7 +1438,7 @@ let threadMessageViewportObserver = null;
1289
1438
  // coach, and micro-manage all have room. Toggle the mode class the CSS keys off,
1290
1439
  // and reflect the state in the sidebar's Brief nav entry.
1291
1440
  function tfApplyWorkspaceMode() {
1292
- const wc = document.querySelector('.workspace-conv');
1441
+ const wc = document.querySelector('#proj-workspace .workspace-conv');
1293
1442
  if (!wc) return;
1294
1443
  const hasConv = typeof activeConversation === 'function' && !!activeConversation();
1295
1444
  wc.classList.toggle('mode-conv', hasConv);
@@ -1317,12 +1466,15 @@ function renderActive() {
1317
1466
  renderedEventCount = 0;
1318
1467
  renderedStatus = null;
1319
1468
  renderedDirectEventCount = 0;
1469
+ syncConversationRefreshPolling();
1320
1470
  return;
1321
1471
  }
1322
1472
  els['empty'].hidden = true;
1323
1473
  els['active-conv'].hidden = false;
1324
1474
  els['active-conv'].dataset.runId = conv.runId || '';
1325
1475
  els['active-title'].textContent = conversationTitle(conv);
1476
+ els['active-conv'].classList.toggle('has-delegation-board', !!normalizeDelegationLedger(conv.delegation));
1477
+ els['active-conv'].classList.toggle('has-manager-oversight', isManagerOversightConversation(conv));
1326
1478
  renderConversationIdentity(conv);
1327
1479
  renderRunStatePill(conv);
1328
1480
  syncCoachEmployeeLabel(conv);
@@ -1348,6 +1500,7 @@ function renderActive() {
1348
1500
  syncThreadUiState(conv);
1349
1501
  syncConversationPanels(conv, switchedConv);
1350
1502
  syncQuickCoachButtons(conv);
1503
+ syncManagedDelegationAccess(conv);
1351
1504
  ensureThreadMessageViewportObserver();
1352
1505
 
1353
1506
  // Messages — append only the rows that aren't already in the DOM, so
@@ -1431,6 +1584,7 @@ function renderActive() {
1431
1584
  // Issue #566 R4 — after the manager confirms success, offer to teach this as a
1432
1585
  // job (ad-hoc) or remember the coached steps for the job (structured).
1433
1586
  renderGrowthOffer(conv);
1587
+ syncManagedDelegationAccess(conv);
1434
1588
  syncThreadMessageViewport();
1435
1589
  const m = els['messages'];
1436
1590
  const runningShouldStickToBottom = !!m && conv.status === 'running'
@@ -1456,6 +1610,9 @@ function renderActive() {
1456
1610
  // simply hide the surfaces.
1457
1611
  renderTracker(conv);
1458
1612
  renderTotals(conv);
1613
+ renderManagerOversightBoard(conv);
1614
+ renderDelegationLedger(conv);
1615
+ syncConversationRefreshPolling();
1459
1616
  syncTemplatePickerVisibility();
1460
1617
  // Issue #442: Direct panel status pill, simple progress indicator, and totals.
1461
1618
  if (isABConv) {
@@ -1903,6 +2060,371 @@ function renderTotals(conv) {
1903
2060
  pushTotalsSpan(totals, costLabel, '', "cost: derived from token totals and the agent's published per-million rate");
1904
2061
  }
1905
2062
 
2063
+ function ensureDelegationHost() {
2064
+ const active = els['active-conv'];
2065
+ if (!active) return null;
2066
+ let host = document.getElementById('delegation-ledger');
2067
+ if (host) return host;
2068
+ host = document.createElement('details');
2069
+ host.id = 'delegation-ledger';
2070
+ host.className = 'delegation-ledger panel-details';
2071
+ host.addEventListener('toggle', () => {
2072
+ const conv = activeConversation();
2073
+ if (!conv) return;
2074
+ panelStateFor(conv.id).delegation = host.open;
2075
+ });
2076
+ const status = active.querySelector('.conversation-status');
2077
+ const oversight = document.getElementById('manager-oversight-board');
2078
+ if (oversight && oversight.parentNode) {
2079
+ oversight.parentNode.insertBefore(host, oversight.nextSibling);
2080
+ } else if (status && status.parentNode) {
2081
+ status.parentNode.insertBefore(host, status.nextSibling);
2082
+ } else {
2083
+ active.insertBefore(host, active.firstChild);
2084
+ }
2085
+ return host;
2086
+ }
2087
+
2088
+ const MANAGER_OVERSIGHT_JOB_IDS = new Set([
2089
+ 'fully-delegate',
2090
+ 'delivery-governance-review',
2091
+ 'project-plan-creation',
2092
+ 'stakeholder-status-reporting',
2093
+ 'experiment-tracking',
2094
+ 'cross-functional-dependency-management',
2095
+ ]);
2096
+
2097
+ function isManagerOversightConversation(conv) {
2098
+ return !!conv && (conv.personaKey === 'mandy' || MANAGER_OVERSIGHT_JOB_IDS.has(conv.jobId) || !!delegationLedgerForConversation(conv));
2099
+ }
2100
+
2101
+ function ensureManagerOversightHost() {
2102
+ const active = els['active-conv'];
2103
+ if (!active) return null;
2104
+ let host = document.getElementById('manager-oversight-board');
2105
+ if (host) return host;
2106
+ host = document.createElement('details');
2107
+ host.id = 'manager-oversight-board';
2108
+ host.className = 'manager-oversight-board panel-details';
2109
+ host.addEventListener('toggle', () => {
2110
+ const conv = activeConversation();
2111
+ if (!conv) return;
2112
+ panelStateFor(conv.id).managerOversight = host.open;
2113
+ });
2114
+ const status = active.querySelector('.conversation-status');
2115
+ if (status && status.parentNode) {
2116
+ status.parentNode.insertBefore(host, status.nextSibling);
2117
+ } else {
2118
+ active.insertBefore(host, active.firstChild);
2119
+ }
2120
+ return host;
2121
+ }
2122
+
2123
+ function renderManagerOversightBoard(conv) {
2124
+ const host = ensureManagerOversightHost();
2125
+ if (!host) return;
2126
+ if (!isManagerOversightConversation(conv)) {
2127
+ host.hidden = true;
2128
+ host.innerHTML = '';
2129
+ return;
2130
+ }
2131
+ const ledger = delegationLedgerForConversation(conv);
2132
+ const panelState = panelStateFor(conv.id);
2133
+ const shouldOpen = panelState.managerOversight ?? true;
2134
+ if (host.open !== shouldOpen) host.open = shouldOpen;
2135
+ const childCount = ledger?.tasks?.length || 0;
2136
+ const completedChildren = ledger?.tasks?.filter((task) => {
2137
+ const child = delegationTaskConversation(task, ledger);
2138
+ return child?.status === 'completed' || task.status === 'submitted' || task.status === 'reviewed' || task.status === 'completed';
2139
+ }).length || 0;
2140
+ const reviewedChildren = ledger?.tasks?.filter((task) => {
2141
+ const child = delegationTaskConversation(task, ledger);
2142
+ return child?.managedReviewStatus === 'reviewed' || task.status === 'reviewed';
2143
+ }).length || 0;
2144
+ const artifactCount = Array.isArray(conv.artifacts) ? conv.artifacts.length : 0;
2145
+ const reviewReady = !!reviewHandoffForConversation(conv) || artifactCount > 0;
2146
+ const planRows = ledger?.tasks?.map((task, index) => managerPlanRow(task, ledger, index)) || [];
2147
+
2148
+ host.hidden = false;
2149
+ host.innerHTML = '';
2150
+ const summary = document.createElement('summary');
2151
+ const header = document.createElement('div');
2152
+ header.className = 'manager-oversight-header';
2153
+ const copy = document.createElement('div');
2154
+ copy.className = 'manager-oversight-copy';
2155
+ const kicker = document.createElement('span');
2156
+ kicker.className = 'manager-oversight-kicker';
2157
+ kicker.textContent = 'Execution plan';
2158
+ const title = document.createElement('strong');
2159
+ title.textContent = childCount
2160
+ ? `${childCount} delegated job${childCount === 1 ? '' : 's'} with Mandy review gates`
2161
+ : conv.status === 'running' ? 'Mandy is planning the delegation' : `${conv.jobTitle || conv.title || 'Manager job'} — no delegation emitted`;
2162
+ copy.appendChild(kicker);
2163
+ copy.appendChild(title);
2164
+ const meta = document.createElement('span');
2165
+ meta.className = 'manager-oversight-meta';
2166
+ meta.textContent = childCount
2167
+ ? `${completedChildren}/${childCount} submitted, ${reviewedChildren}/${childCount} reviewed`
2168
+ : conv.status === 'running' ? 'setting up team…' : reviewReady ? `${artifactCount} artifact${artifactCount === 1 ? '' : 's'} surfaced, no delegation graph` : 'delegation graph not returned';
2169
+ header.appendChild(copy);
2170
+ header.appendChild(meta);
2171
+ summary.appendChild(header);
2172
+ host.appendChild(summary);
2173
+
2174
+ const list = document.createElement('div');
2175
+ list.className = 'manager-oversight-list';
2176
+ if (planRows.length) {
2177
+ for (const item of planRows) {
2178
+ const row = document.createElement('div');
2179
+ row.className = `manager-plan-row status-${item.status}`;
2180
+ const number = document.createElement('span');
2181
+ number.className = 'manager-plan-step';
2182
+ number.textContent = String(item.index + 1);
2183
+ const body = document.createElement('div');
2184
+ body.className = 'manager-plan-body';
2185
+ const top = document.createElement('div');
2186
+ top.className = 'manager-plan-top';
2187
+ const titleEl = document.createElement('strong');
2188
+ titleEl.textContent = item.title;
2189
+ const status = document.createElement('span');
2190
+ status.className = `manager-plan-status status-${item.status}`;
2191
+ status.textContent = item.statusLabel;
2192
+ top.appendChild(titleEl);
2193
+ top.appendChild(status);
2194
+ const metaLine = document.createElement('div');
2195
+ metaLine.className = 'manager-plan-meta';
2196
+ metaLine.textContent = `${item.employee} • ${item.dependencyText}`;
2197
+ const review = document.createElement('div');
2198
+ review.className = 'manager-plan-review';
2199
+ review.textContent = item.reviewText;
2200
+ body.appendChild(top);
2201
+ body.appendChild(metaLine);
2202
+ body.appendChild(review);
2203
+ if (item.instructions) {
2204
+ const instructions = document.createElement('div');
2205
+ instructions.className = 'manager-plan-instructions';
2206
+ instructions.textContent = item.instructions;
2207
+ body.appendChild(instructions);
2208
+ }
2209
+ row.appendChild(number);
2210
+ row.appendChild(body);
2211
+ list.appendChild(row);
2212
+ }
2213
+ } else {
2214
+ const empty = document.createElement('div');
2215
+ empty.className = 'manager-plan-empty';
2216
+ const isRunning = conv.status === 'running';
2217
+ const titleEl = document.createElement('strong');
2218
+ titleEl.textContent = isRunning ? 'Delegation plan incoming…' : 'No delegation sequence returned';
2219
+ const detail = document.createElement('span');
2220
+ detail.textContent = isRunning
2221
+ ? 'Mandy is working through the task and will lay out the employee assignments and review gates here once the plan is ready.'
2222
+ : reviewReady
2223
+ ? `This run completed with ${artifactCount} surfaced artifact${artifactCount === 1 ? '' : 's'} but did not emit a delegation sequence. Start a new run to retry.`
2224
+ : 'This run finished without returning a delegation sequence. Start a new run to retry.';
2225
+ empty.appendChild(titleEl);
2226
+ empty.appendChild(detail);
2227
+ list.appendChild(empty);
2228
+ }
2229
+ host.appendChild(list);
2230
+ }
2231
+
2232
+ function managerPlanRow(task, ledger, index) {
2233
+ const child = delegationTaskConversation(task, ledger);
2234
+ const status = delegatedTaskStatus(task, child);
2235
+ const dependsOn = Array.isArray(task.dependsOn) ? task.dependsOn.filter(Boolean) : [];
2236
+ const dependencyText = dependsOn.length
2237
+ ? `starts after ${dependsOn.join(', ')}`
2238
+ : 'can start immediately';
2239
+ const reviewJob = task.reviewJobId ? ` via ${task.reviewJobId}` : '';
2240
+ const reviewText = task.reviewType || task.reviewHandoff?.reviewRequired || child?.reviewHandoff?.reviewRequired
2241
+ ? `Mandy validates this output${reviewJob} before synthesis`
2242
+ : 'Mandy validates this output before synthesis';
2243
+ return {
2244
+ index,
2245
+ title: task.title || task.jobId || task.taskId || `Delegated job ${index + 1}`,
2246
+ employee: delegationPersonaLabel(task.personaKey),
2247
+ dependencyText,
2248
+ reviewText,
2249
+ instructions: task.instructions || task.latestSummary || '',
2250
+ status,
2251
+ statusLabel: statusLabel(status),
2252
+ };
2253
+ }
2254
+
2255
+ function delegationPersonaLabel(personaKey) {
2256
+ if (!personaKey) return 'Unassigned';
2257
+ const persona = personaMap().get(personaKey);
2258
+ return persona ? persona.displayName : personaKey;
2259
+ }
2260
+
2261
+ function delegationTaskConversation(task, ledger) {
2262
+ if (!task || !ledger) return null;
2263
+ const conversations = projectConversations();
2264
+ const ids = new Set([task.conversationId, task.runId].filter(Boolean));
2265
+ const managerIds = new Set([ledger.managerRunId, ledger.rootRunId].filter(Boolean));
2266
+ return conversations.find((conv) =>
2267
+ ids.has(conv.id) ||
2268
+ ids.has(conv.runId) ||
2269
+ (conv.delegationTaskId === task.taskId && (!conv.managedByRunId || managerIds.has(conv.managedByRunId)))
2270
+ ) || null;
2271
+ }
2272
+
2273
+ function delegatedTaskStatus(task, childConv) {
2274
+ if (!childConv) return task.status;
2275
+ if (childConv.status === 'running') return 'running';
2276
+ if (childConv.status === 'failed') return 'blocked';
2277
+ if (childConv.status === 'completed' && childConv.managedReviewStatus === 'reviewed') return 'reviewed';
2278
+ if (childConv.status === 'completed') return 'submitted';
2279
+ return task.status || 'planned';
2280
+ }
2281
+
2282
+ function delegatedTaskSummary(task, childConv) {
2283
+ if (childConv) {
2284
+ const latest = latestEmployeeSurfaceText(childConv);
2285
+ if (latest) return latest;
2286
+ }
2287
+ return task.latestSummary || task.instructions || 'Waiting for an update.';
2288
+ }
2289
+
2290
+ function delegatedTaskArtifacts(task, childConv) {
2291
+ if (childConv?.reviewHandoff?.artifacts?.length) return childConv.reviewHandoff.artifacts;
2292
+ if (childConv?.artifacts?.length) return childConv.artifacts;
2293
+ if (task.reviewHandoff?.artifacts?.length) return task.reviewHandoff.artifacts;
2294
+ return task.artifacts || [];
2295
+ }
2296
+
2297
+ function renderDelegationLedger(conv) {
2298
+ const host = ensureDelegationHost();
2299
+ if (!host) return;
2300
+ const ledger = delegationLedgerForConversation(conv);
2301
+ if (!ledger) {
2302
+ host.hidden = true;
2303
+ host.innerHTML = '';
2304
+ return;
2305
+ }
2306
+
2307
+ host.hidden = false;
2308
+ const panelState = panelStateFor(conv.id);
2309
+ const shouldOpen = panelState.delegation ?? !isManagerOversightConversation(conv);
2310
+ if (host.open !== shouldOpen) host.open = shouldOpen;
2311
+ host.innerHTML = '';
2312
+ const summaryRow = document.createElement('summary');
2313
+ const header = document.createElement('div');
2314
+ header.className = 'delegation-ledger-header';
2315
+ const copy = document.createElement('div');
2316
+ copy.className = 'delegation-ledger-copy';
2317
+ const kicker = document.createElement('span');
2318
+ kicker.className = 'delegation-ledger-kicker';
2319
+ kicker.textContent = 'Delegation board';
2320
+ const title = document.createElement('strong');
2321
+ title.textContent = ledger.objective;
2322
+ copy.appendChild(kicker);
2323
+ copy.appendChild(title);
2324
+ const meta = document.createElement('span');
2325
+ meta.className = 'delegation-ledger-meta';
2326
+ const orchestrator = delegationPersonaLabel(ledger.orchestratorPersonaKey || conv.personaKey);
2327
+ meta.textContent = `${orchestrator} coordinating ${ledger.tasks.length} workstream${ledger.tasks.length === 1 ? '' : 's'}`;
2328
+ header.appendChild(copy);
2329
+ header.appendChild(meta);
2330
+ summaryRow.appendChild(header);
2331
+ host.appendChild(summaryRow);
2332
+
2333
+ const body = document.createElement('div');
2334
+ body.className = 'delegation-ledger-body';
2335
+
2336
+ if (ledger.latestSummary) {
2337
+ const summary = document.createElement('p');
2338
+ summary.className = 'delegation-ledger-summary';
2339
+ summary.textContent = ledger.latestSummary;
2340
+ body.appendChild(summary);
2341
+ }
2342
+
2343
+ const list = document.createElement('div');
2344
+ list.className = 'delegation-task-list';
2345
+ for (const task of ledger.tasks) {
2346
+ const childConv = delegationTaskConversation(task, ledger);
2347
+ const taskStatus = delegatedTaskStatus(task, childConv);
2348
+ const row = document.createElement('article');
2349
+ row.className = `delegation-task delegation-task--${taskStatus}`;
2350
+ const top = document.createElement('div');
2351
+ top.className = 'delegation-task-top';
2352
+ const assignee = document.createElement('span');
2353
+ assignee.className = 'delegation-task-assignee';
2354
+ assignee.textContent = delegationPersonaLabel(task.personaKey);
2355
+ const status = document.createElement('span');
2356
+ status.className = `delegation-task-status status-${taskStatus}`;
2357
+ status.textContent = childConv ? conversationStateLabel(childConv) : taskStatus;
2358
+ top.appendChild(assignee);
2359
+ top.appendChild(status);
2360
+
2361
+ const taskTitle = document.createElement('strong');
2362
+ taskTitle.className = 'delegation-task-title';
2363
+ taskTitle.textContent = task.title;
2364
+ const detail = document.createElement('div');
2365
+ detail.className = 'delegation-task-detail';
2366
+ detail.textContent = [
2367
+ childConv?.jobId || task.jobId,
2368
+ childConv?.sessionId || task.hostSessionId ? `session ${childConv?.sessionId || task.hostSessionId}` : '',
2369
+ task.hostThreadId ? `thread ${task.hostThreadId}` : '',
2370
+ ].filter(Boolean).join(' · ');
2371
+ const summary = document.createElement('p');
2372
+ summary.className = 'delegation-task-summary';
2373
+ summary.textContent = delegatedTaskSummary(task, childConv);
2374
+
2375
+ row.appendChild(top);
2376
+ row.appendChild(taskTitle);
2377
+ if (detail.textContent) row.appendChild(detail);
2378
+ row.appendChild(summary);
2379
+
2380
+ const artifacts = delegatedTaskArtifacts(task, childConv);
2381
+ if (artifacts.length) {
2382
+ const artifactStrip = document.createElement('div');
2383
+ artifactStrip.className = 'delegation-artifacts';
2384
+ for (const artifact of artifacts) {
2385
+ const chip = document.createElement('button');
2386
+ chip.type = 'button';
2387
+ chip.className = 'delegation-artifact';
2388
+ chip.textContent = artifactLabel(artifact);
2389
+ chip.title = artifactLocalPath(artifact) || artifactExternalUrl(artifact) || artifactLabel(artifact);
2390
+ chip.addEventListener('click', async () => {
2391
+ try {
2392
+ await openReviewArtifact(artifact);
2393
+ } catch (err) {
2394
+ showStatus(err instanceof Error ? err.message : 'Could not open artifact.', true);
2395
+ }
2396
+ });
2397
+ artifactStrip.appendChild(chip);
2398
+ }
2399
+ row.appendChild(artifactStrip);
2400
+ } else if (childConv?.status === 'completed' || taskStatus === 'submitted' || taskStatus === 'reviewed') {
2401
+ const inline = document.createElement('div');
2402
+ inline.className = 'delegation-review-note';
2403
+ inline.textContent = 'Inline deliverable; no file artifact reported';
2404
+ row.appendChild(inline);
2405
+ }
2406
+
2407
+ if (childConv) {
2408
+ const view = document.createElement('button');
2409
+ view.type = 'button';
2410
+ view.className = 'delegation-view-work';
2411
+ view.textContent = 'View work';
2412
+ view.addEventListener('click', () => switchToConversation(childConv.id));
2413
+ row.appendChild(view);
2414
+ }
2415
+
2416
+ if (task.reviewHandoff?.reviewRequired || childConv?.reviewHandoff?.reviewRequired) {
2417
+ const review = document.createElement('div');
2418
+ review.className = 'delegation-review-note';
2419
+ review.textContent = 'Ready for Mandy review';
2420
+ row.appendChild(review);
2421
+ }
2422
+ list.appendChild(row);
2423
+ }
2424
+ body.appendChild(list);
2425
+ host.appendChild(body);
2426
+ }
2427
+
1906
2428
  // Issue #442: render Direct totals row using the same pushTotalsSpan helper as FRAIM.
1907
2429
  function renderDirectTotals(conv) {
1908
2430
  const dtotals = els['ab-direct-totals'];
@@ -2098,6 +2620,7 @@ function humanizeSlug(slug) {
2098
2620
  function stripStubReference(text) {
2099
2621
  return String(text || '')
2100
2622
  .replace(/\n?\[Job stub:[^\]]+\]/gi, '')
2623
+ .replace(/\[Thought:\s*true\]\s*/gi, '')
2101
2624
  .replace(/\s+/g, ' ')
2102
2625
  .trim();
2103
2626
  }
@@ -2278,6 +2801,10 @@ function scrollThreadForReview(conv) {
2278
2801
  function syncSendButton() {
2279
2802
  const conv = activeConversation();
2280
2803
  const hasText = els['coach-text'].value.trim().length > 0;
2804
+ if (isManagedDelegationChild(conv)) {
2805
+ els['send'].disabled = true;
2806
+ return;
2807
+ }
2281
2808
  // Send is enabled as soon as the host session exists. We deliberately
2282
2809
  // do NOT gate on status='running' — the manager should be able to
2283
2810
  // coach mid-run (which is exactly what /api/ai-hub/runs/:id/messages
@@ -2291,6 +2818,9 @@ function syncSendButton() {
2291
2818
  }
2292
2819
 
2293
2820
  function defaultCoachNote(conv) {
2821
+ if (isManagedDelegationChild(conv)) {
2822
+ return `${managedByPersonaLabel(conv)} manages coaching and review for this delegated workstream. You can inspect the work, but coach the manager job instead.`;
2823
+ }
2294
2824
  return conv && conv.status === 'running'
2295
2825
  ? 'The employee is still working. Add coaching here to tighten the next step without losing context.'
2296
2826
  : 'The employee is waiting on you. Send the next instruction to continue this run.';
@@ -2308,6 +2838,34 @@ function syncCoachNote(conv = activeConversation()) {
2308
2838
  els['coach-note'].textContent = defaultCoachNote(conv);
2309
2839
  }
2310
2840
 
2841
+ function syncManagedDelegationAccess(conv = activeConversation()) {
2842
+ const managed = isManagedDelegationChild(conv);
2843
+ const textarea = els['coach-text'];
2844
+ if (textarea) {
2845
+ textarea.disabled = managed;
2846
+ textarea.placeholder = managed
2847
+ ? `${managedByPersonaLabel(conv)} owns coaching for this delegated workstream`
2848
+ : 'Tell the employee what to do next...';
2849
+ }
2850
+ if (els['send']) els['send'].disabled = managed || els['send'].disabled;
2851
+ if (els['coach-panel']) els['coach-panel'].classList.toggle('coach-panel--managed-readonly', managed);
2852
+ for (const selector of ['#quick-coach-btns button', '#mark-complete-btn', '#other-manager-jobs-btn', '#review-actions button']) {
2853
+ for (const button of document.querySelectorAll(selector)) {
2854
+ if (managed) {
2855
+ if (button.dataset.managedPrevDisabled === undefined) {
2856
+ button.dataset.managedPrevDisabled = button.disabled ? '1' : '0';
2857
+ }
2858
+ button.disabled = true;
2859
+ } else if (button.dataset.managedPrevDisabled !== undefined) {
2860
+ button.disabled = button.dataset.managedPrevDisabled === '1';
2861
+ delete button.dataset.managedPrevDisabled;
2862
+ }
2863
+ button.classList.toggle('managed-disabled', managed);
2864
+ }
2865
+ }
2866
+ if (managed) closeTemplatePopover();
2867
+ }
2868
+
2311
2869
  function appendEditableCoachingPrompt(promptText) {
2312
2870
  const textarea = els['coach-text'];
2313
2871
  if (!textarea) return;
@@ -2366,6 +2924,11 @@ function convAwaitingReview(conv) {
2366
2924
  const handoff = reviewHandoffForConversation(conv);
2367
2925
  if (handoff) return handoff.reviewRequired === true;
2368
2926
  if (reviewHandoffIssueForConversation(conv)) return true;
2927
+ // Delegation orchestration runs (fully-delegate with an active ledger) only enter review
2928
+ // via an explicit review_handoff — Mandy's delegation artifacts are orchestration working
2929
+ // output, not deliverables submitted to the human manager. Other manager jobs (e.g.,
2930
+ // stakeholder-status-reporting) may legitimately surface artifacts for human review.
2931
+ if (delegationLedgerForConversation(conv)) return false;
2369
2932
  if (conv.status === 'completed' && Array.isArray(conv.artifacts) && conv.artifacts.length > 0) return true;
2370
2933
  // #521: a plain completed turn is NOT awaiting review — the approve/reject card
2371
2934
  // only belongs when the employee actually submits for review (a review_handoff,
@@ -2475,6 +3038,98 @@ function extractReviewHandoffFromText(text) {
2475
3038
  return null;
2476
3039
  }
2477
3040
 
3041
+ const DELEGATION_TASK_STATUSES = new Set(['planned', 'running', 'submitted', 'reviewed', 'blocked', 'completed', 'failed']);
3042
+
3043
+ function cleanString(value) {
3044
+ return typeof value === 'string' ? value.trim() : '';
3045
+ }
3046
+
3047
+ function cleanNullableString(value) {
3048
+ const cleaned = cleanString(value);
3049
+ return cleaned ? cleaned : null;
3050
+ }
3051
+
3052
+ function normalizeDelegationLedger(raw) {
3053
+ if (!raw || typeof raw !== 'object' || raw.delegationRequired !== true) return null;
3054
+ const objective = cleanString(raw.objective);
3055
+ if (!objective) return null;
3056
+ const tasks = Array.isArray(raw.tasks)
3057
+ ? raw.tasks.map((task, index) => {
3058
+ if (!task || typeof task !== 'object') return null;
3059
+ const title = cleanString(task.title);
3060
+ if (!title) return null;
3061
+ const fallbackTaskId = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || `task-${index + 1}`;
3062
+ const status = DELEGATION_TASK_STATUSES.has(cleanString(task.status)) ? cleanString(task.status) : 'planned';
3063
+ const artifacts = Array.isArray(task.artifacts)
3064
+ ? task.artifacts.map(normalizeReviewArtifact).filter(Boolean)
3065
+ : [];
3066
+ return {
3067
+ taskId: cleanString(task.taskId) || fallbackTaskId,
3068
+ title,
3069
+ status,
3070
+ personaKey: cleanNullableString(task.personaKey),
3071
+ jobId: cleanNullableString(task.jobId),
3072
+ instructions: cleanString(task.instructions),
3073
+ latestSummary: cleanString(task.latestSummary),
3074
+ hostThreadId: cleanNullableString(task.hostThreadId),
3075
+ hostSessionId: cleanNullableString(task.hostSessionId),
3076
+ runId: cleanNullableString(task.runId),
3077
+ conversationId: cleanNullableString(task.conversationId),
3078
+ dependsOn: Array.isArray(task.dependsOn) ? task.dependsOn.map(cleanString).filter(Boolean) : [],
3079
+ artifacts,
3080
+ reviewHandoff: normalizeReviewHandoff(task.reviewHandoff),
3081
+ };
3082
+ }).filter(Boolean)
3083
+ : [];
3084
+ if (!tasks.length) return null;
3085
+ return {
3086
+ delegationRequired: true,
3087
+ objective,
3088
+ orchestratorPersonaKey: cleanNullableString(raw.orchestratorPersonaKey),
3089
+ rootRunId: cleanNullableString(raw.rootRunId),
3090
+ managerRunId: cleanNullableString(raw.managerRunId),
3091
+ latestSummary: cleanString(raw.latestSummary),
3092
+ tasks,
3093
+ };
3094
+ }
3095
+
3096
+ function extractDelegationLedgerFromText(text) {
3097
+ if (!text || !/delegationRequired|delegation_ledger|delegationLedger/i.test(text)) return null;
3098
+ const candidates = [];
3099
+ const fenced = String(text).matchAll(/```(?:json)?\s*([\s\S]*?)```/gi);
3100
+ for (const match of fenced) candidates.push(match[1]);
3101
+ const tagged = String(text).match(/<delegation_ledger>\s*([\s\S]*?)\s*<\/delegation_ledger>/i);
3102
+ if (tagged) candidates.push(tagged[1]);
3103
+ const inline = String(text).match(/(\{\s*"delegationRequired"[\s\S]*\})/i);
3104
+ if (inline) candidates.push(inline[1]);
3105
+
3106
+ for (const candidate of candidates) {
3107
+ try {
3108
+ const ledger = normalizeDelegationLedger(JSON.parse(candidate.trim()));
3109
+ if (ledger) return ledger;
3110
+ } catch {
3111
+ // Ignore malformed snippets; the raw transcript is still available.
3112
+ }
3113
+ }
3114
+ return null;
3115
+ }
3116
+
3117
+ function delegationLedgerForConversation(conv) {
3118
+ if (!conv) return null;
3119
+ const existing = normalizeDelegationLedger(conv.delegation);
3120
+ if (existing) return existing;
3121
+ const messages = conv.messages || [];
3122
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
3123
+ if (messages[i].role !== 'employee') continue;
3124
+ const parsed = extractDelegationLedgerFromText(messages[i].text);
3125
+ if (parsed) {
3126
+ conv.delegation = parsed;
3127
+ return parsed;
3128
+ }
3129
+ }
3130
+ return null;
3131
+ }
3132
+
2478
3133
  function hasReviewHandoffSignal(text) {
2479
3134
  return !!(text && /reviewRequired|reviewTarget|review_handoff/i.test(text));
2480
3135
  }
@@ -2622,8 +3277,22 @@ function deriveDeliverableFormat(conv) {
2622
3277
 
2623
3278
  const hay = ((conv && (conv.jobId || '')) + ' ' + (conv && (conv.jobTitle || ''))).toLowerCase();
2624
3279
  // Prefer an explicit artifact path captured from the run when present.
2625
- const artifact = (conv && conv.artifacts && conv.artifacts[0]) || null;
2626
- const artifactName = artifact ? artifact.name : '';
3280
+ const artifacts = Array.isArray(conv && conv.artifacts) ? conv.artifacts : [];
3281
+ if (artifacts.length > 1) {
3282
+ return {
3283
+ key: 'artifact_set',
3284
+ label: `${artifacts.length} artifacts`,
3285
+ actions: artifacts.map((artifact, index) => ({
3286
+ id: `open-artifact-${index}`,
3287
+ label: artifactLabel(artifact),
3288
+ primary: index === 0,
3289
+ kind: artifactActionKind(artifact),
3290
+ artifact,
3291
+ })),
3292
+ };
3293
+ }
3294
+ const artifact = artifacts[0] || null;
3295
+ const artifactName = artifact ? (artifact.name || artifact.path || artifact.url || artifact.label || '') : '';
2627
3296
  const ext = artifactName.includes('.') ? artifactName.split('.').pop().toLowerCase() : '';
2628
3297
 
2629
3298
  const isCode = /implementation|fix|refactor|\bpr\b|pull-request|code|bug|engineering/.test(hay)
@@ -3059,7 +3728,7 @@ function tfShowDoneReviewing(conv, docxPath) {
3059
3728
  wrap.className = 'rc-done-reviewing';
3060
3729
  const label = document.createElement('div');
3061
3730
  label.className = 'rdr-label';
3062
- label.textContent = 'Open the .docx in Word, add your comments, then save. When ready:';
3731
+ label.textContent = 'Open the doc in your preferred editor (Word, Google Docs, etc.), add comments, then save. When ready:';
3063
3732
  const btn = document.createElement('button');
3064
3733
  btn.type = 'button';
3065
3734
  btn.className = 'rdr-done-btn';
@@ -3073,15 +3742,15 @@ function tfShowDoneReviewing(conv, docxPath) {
3073
3742
  ? docxPath.replace((state.projectPath || '').replace(/\\/g, '/').replace(/\/$/, '') + '/', '')
3074
3743
  : '';
3075
3744
  const message = rel
3076
- ? `I've reviewed the document. My feedback is in the Word comments and tracked changes inside \`${rel}\`. Please read every one and address them all in a revision.`
3077
- : `I've finished reviewing the .docx. My feedback is in the Word comments and tracked changes — please read them all and address them in a revision.`;
3745
+ ? `I've reviewed the document. My feedback is in the doc comments and tracked changes inside \`${rel}\`. Please read every one and address them all in a revision.`
3746
+ : `I've finished reviewing the doc. My feedback is in the doc comments and tracked changes — please read them all and address them in a revision.`;
3078
3747
  conv.pendingDocxReview = docxPath || null;
3079
3748
  conv.reviewApproved = false;
3080
3749
  upsertConversation(conv);
3081
3750
  try {
3082
3751
  await continueRun(message);
3083
3752
  wrap.remove();
3084
- showStatus('Comments sent — the employee will read your Word annotations and come back with a revision.', false);
3753
+ showStatus('Comments sent — the employee will read your doc annotations and come back with a revision.', false);
3085
3754
  } catch (err) {
3086
3755
  btn.disabled = false;
3087
3756
  btn.textContent = '✓ Done reviewing — send my comments to the employee';
@@ -3105,7 +3774,7 @@ function tfShowDocxUpload(conv) {
3105
3774
  // is exactly why "Mark complete" left the Project Onboarding dot stale.
3106
3775
  function refreshStatusSurfaces() {
3107
3776
  if (typeof renderRail === 'function') renderRail();
3108
- if (document.querySelector('.workspace-conv')) {
3777
+ if (document.querySelector('#proj-workspace .workspace-conv')) {
3109
3778
  if (typeof tfRenderTree === 'function') tfRenderTree();
3110
3779
  if (typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
3111
3780
  }
@@ -3225,8 +3894,17 @@ function wirePopovers() {
3225
3894
 
3226
3895
  if (e.key === 'Escape') {
3227
3896
  const cpModal = document.getElementById('cp-modal');
3228
- if (cpModal && !cpModal.hidden) {
3897
+ const schedModal = document.getElementById('dep-schedule-modal');
3898
+ const webhookModal = document.getElementById('dep-webhook-modal');
3899
+ const aomModal = document.getElementById('area-onboard-modal');
3900
+ if (aomModal && !aomModal.hidden) {
3901
+ tfCloseAreaOnboardModal();
3902
+ } else if (cpModal && !cpModal.hidden) {
3229
3903
  closePalette();
3904
+ } else if (schedModal && !schedModal.hidden) {
3905
+ schedModal.hidden = true;
3906
+ } else if (webhookModal && !webhookModal.hidden) {
3907
+ webhookModal.hidden = true;
3230
3908
  } else if (els['modal'].classList.contains('open')) {
3231
3909
  closeModal();
3232
3910
  } else {
@@ -4175,6 +4853,9 @@ function foldRunIntoConversation(conv, run) {
4175
4853
  const runHandoff = normalizeReviewHandoff(run.reviewHandoff);
4176
4854
  if (runHandoff) conv.reviewHandoff = runHandoff;
4177
4855
  else reviewHandoffForConversation(conv);
4856
+ const runDelegation = normalizeDelegationLedger(run.delegation);
4857
+ if (runDelegation) conv.delegation = runDelegation;
4858
+ else delegationLedgerForConversation(conv);
4178
4859
  if (Array.isArray(run.artifacts)) {
4179
4860
  conv.artifacts = run.artifacts;
4180
4861
  }
@@ -4186,9 +4867,10 @@ function foldRunIntoConversation(conv, run) {
4186
4867
  // The browser only parses output text when that structured field is absent.
4187
4868
  if (!Array.isArray(run.artifacts)) {
4188
4869
  for (const e of conv.events) {
4189
- const found = extractArtifact(e.text);
4190
- if (found && !conv.artifacts.some((a) => a.name === found.name && a.where === found.where)) {
4191
- conv.artifacts.push(found);
4870
+ for (const found of extractArtifacts(e.text)) {
4871
+ if (!conv.artifacts.some((a) => a.name === found.name && a.where === found.where)) {
4872
+ conv.artifacts.push(found);
4873
+ }
4192
4874
  }
4193
4875
  }
4194
4876
  }
@@ -4210,27 +4892,40 @@ function foldCompareRunIntoConversation(conv, compareRun) {
4210
4892
  };
4211
4893
  }
4212
4894
 
4213
- const ARTIFACT_PATH_RE = /([A-Za-z0-9_\-\.\/]*?(?:docs|public|src|tests)\/[A-Za-z0-9_\-\.\/]+\.[A-Za-z0-9]+)/;
4214
4895
  // Paths under these directories are FRAIM lifecycle bookkeeping (RCAs,
4215
4896
  // raw learnings, evidence dumps, mock files), not deliverables the
4216
4897
  // manager should be drawn to. Excluding them keeps the artifact callout
4217
4898
  // meaningful — it should mean "the employee produced this file for you".
4218
4899
  const ARTIFACT_EXCLUDE_RE = /(^|\/)(retrospectives|evidence|learnings|mocks|raw|archive)\//i;
4219
4900
  function extractArtifact(text) {
4220
- if (!text) return null;
4221
- const match = text.match(ARTIFACT_PATH_RE);
4222
- if (!match) return null;
4223
- const fullPath = match[1];
4224
- if (ARTIFACT_EXCLUDE_RE.test(fullPath)) return null;
4225
- const segments = fullPath.split('/');
4226
- const name = segments[segments.length - 1];
4227
- const where = segments.slice(0, -1).join('/') + '/';
4228
- // Store the absolute path so the agent can re-read the artifact (and any
4229
- // .docx export written alongside it) without guessing the project root.
4230
- const absPath = state.projectPath
4231
- ? state.projectPath.replace(/\\/g, '/').replace(/\/$/, '') + '/' + fullPath
4232
- : null;
4233
- return { name, where, path: absPath };
4901
+ return extractArtifacts(text)[0] || null;
4902
+ }
4903
+
4904
+ function extractArtifacts(text) {
4905
+ if (!text) return [];
4906
+ const artifacts = [];
4907
+ const seen = new Set();
4908
+ const matches = String(text)
4909
+ .split(/[\s`"'()<>{}\[\],;:]+/)
4910
+ .filter((token) => /^(docs|public|src|tests)\//.test(token));
4911
+ for (const candidate of matches) {
4912
+ const fullPath = candidate.replace(/[.)]+$/g, '');
4913
+ if (!/\.[A-Za-z0-9]+$/.test(fullPath)) continue;
4914
+ if (ARTIFACT_EXCLUDE_RE.test(fullPath)) continue;
4915
+ const segments = fullPath.split('/');
4916
+ const name = segments[segments.length - 1];
4917
+ const where = segments.slice(0, -1).join('/') + '/';
4918
+ // Store the absolute path so the agent can re-read the artifact (and any
4919
+ // .docx export written alongside it) without guessing the project root.
4920
+ const absPath = state.projectPath
4921
+ ? state.projectPath.replace(/\\/g, '/').replace(/\/$/, '') + '/' + fullPath
4922
+ : null;
4923
+ const key = absPath || fullPath;
4924
+ if (seen.has(key)) continue;
4925
+ seen.add(key);
4926
+ artifacts.push({ name, where, path: absPath });
4927
+ }
4928
+ return artifacts;
4234
4929
  }
4235
4930
 
4236
4931
  function startPolling() {
@@ -4248,7 +4943,18 @@ function startPolling() {
4248
4943
  }
4249
4944
  try {
4250
4945
  if (!fraimDone) {
4251
- const run = await requestJson(`/api/ai-hub/runs/${conv.runId}`);
4946
+ const runResp = await fetch(`/api/ai-hub/runs/${conv.runId}`, { headers: { 'x-api-key': 'local-dev' } });
4947
+ if (runResp.status === 404) {
4948
+ // Run no longer in server registry (e.g. after restart). Stop polling.
4949
+ window.clearInterval(state.pollHandle);
4950
+ state.pollHandle = null;
4951
+ conv.status = 'done';
4952
+ upsertConversation(conv);
4953
+ renderRail();
4954
+ renderActive();
4955
+ return;
4956
+ }
4957
+ const run = await runResp.json();
4252
4958
  foldRunIntoConversation(conv, run);
4253
4959
  }
4254
4960
  // Issue #442: also poll the compare run when present and not yet terminal.
@@ -4259,6 +4965,9 @@ function startPolling() {
4259
4965
  upsertConversation(conv);
4260
4966
  renderRail();
4261
4967
  renderActive();
4968
+ // #594 R2: keep the Company/Manager area conv panel in sync while polling.
4969
+ if (tf.area === 'company') tfRenderCompany();
4970
+ else if (tf.area === 'manager') tfRenderManager();
4262
4971
  // #521: when a project-onboarding run posts its understanding (reaches the
4263
4972
  // review gate) or completes, refresh the Brief once so the team's captured
4264
4973
  // context appears there for the manager to review/edit and then approve.
@@ -4311,7 +5020,7 @@ async function tryWordWriteBack(conv) {
4311
5020
  const action = conv.wordStartedWithSelection ? 'insert-after' : 'append-to-doc';
4312
5021
  await requestWordContext(action, { text: outputText });
4313
5022
  await requestWordContext('track-changes-off');
4314
- showStatus('Result written to document — review tracked changes in Word.');
5023
+ showStatus('Result written to document — review tracked changes.');
4315
5024
  } catch (e) {
4316
5025
  console.warn('Word write-back failed:', e);
4317
5026
  }
@@ -4327,6 +5036,15 @@ function convNeedsPolling(conv) {
4327
5036
  function switchToConversation(id) {
4328
5037
  state.activeId = id;
4329
5038
  persistConversations();
5039
+ // For Company/Manager: move the shared .page into this area's conv-host and
5040
+ // switch from the info view to the full coaching panel.
5041
+ if (tf.area === 'company' || tf.area === 'manager') {
5042
+ tfEnsurePageInArea(tf.area);
5043
+ const host = document.getElementById(tf.area + '-conv-host');
5044
+ const info = document.getElementById(tf.area + '-info-view');
5045
+ if (host) host.hidden = false;
5046
+ if (info) info.hidden = true;
5047
+ }
4330
5048
  renderRail();
4331
5049
  renderActive();
4332
5050
  const conv = activeConversation();
@@ -4467,9 +5185,22 @@ function renderManagerTeamPool() {
4467
5185
  const personas = (state.bootstrap?.personas || []).filter((p) => (p.seatCount || 0) > 0);
4468
5186
  const managerTeamKeys = new Set((state.bootstrap?.managerTeam || []).map((e) => e.personaKey));
4469
5187
  container.innerHTML = '';
5188
+ if (personas.length === 0) {
5189
+ const msg = document.createElement('p');
5190
+ msg.className = 'manager-no-team-msg';
5191
+ msg.textContent = "You haven't hired any specialists yet. Go to Company to see the full roster and hire the team you need.";
5192
+ container.appendChild(msg);
5193
+ const btn = document.createElement('button');
5194
+ btn.className = 'manager-go-company-btn';
5195
+ btn.type = 'button';
5196
+ btn.textContent = 'Go to Company to hire';
5197
+ btn.addEventListener('click', () => tfShowArea('company'));
5198
+ container.appendChild(btn);
5199
+ return;
5200
+ }
4470
5201
  for (const persona of personas) {
4471
5202
  const row = document.createElement('div');
4472
- row.className = 'team-row';
5203
+ row.className = 'team-row emp-tile';
4473
5204
 
4474
5205
  const dot = document.createElement('span');
4475
5206
  const oos = (persona.seatsInUse || 0) >= (persona.seatCount || 0);
@@ -4922,6 +5653,7 @@ function wireEvents() {
4922
5653
  try {
4923
5654
  await loadBootstrap(null, docUrl);
4924
5655
  await hydrateConversationsFromServer();
5656
+ startBgConvPoll();
4925
5657
  } catch (error) {
4926
5658
  showStatus(error.message, true);
4927
5659
  return;
@@ -5202,7 +5934,6 @@ function renderFirstRunLanding(mode) {
5202
5934
 
5203
5935
  const STORAGE_KEY_PROJECTS_512 = 'fraim.aiHub.projects.v1';
5204
5936
  const STORAGE_KEY_ASSIGNMENTS_512 = 'fraim.aiHub.assignments.v1';
5205
- const STORAGE_KEY_RAIL_512 = 'fraim.aiHub.railCollapsed.v1';
5206
5937
 
5207
5938
  const tf = {
5208
5939
  enabled: false,
@@ -5215,8 +5946,6 @@ const tf = {
5215
5946
  npSelectedEmployees: [],
5216
5947
  };
5217
5948
 
5218
- const TF_STEP_LABELS = { install: 'Install', company: 'Set up company', hire: 'Hire your team', project: 'Start a project' };
5219
- const TF_STEP_ORDER = ['install', 'company', 'hire', 'project'];
5220
5949
  const TF_AVATAR_COLORS = ['#4a7fd4', '#7c6f61', '#5a9e6e', '#b06a4f', '#6b4c92', '#b07a2e', '#2e8b78', '#a24747'];
5221
5950
  function tfAvatarFor(key, index) {
5222
5951
  const color = TF_AVATAR_COLORS[(index || 0) % TF_AVATAR_COLORS.length];
@@ -5299,7 +6028,25 @@ function tfEmployeeDot(projectId, employeeKey) {
5299
6028
  // Area + sub-tab routing
5300
6029
  // ---------------------------------------------------------------------------
5301
6030
  function tfShowArea(area) {
6031
+ if (!state.areaActiveId) state.areaActiveId = {};
6032
+ // Save Projects' active conv when leaving it, so we can restore on return.
6033
+ if (tf.area === 'projects') state.areaActiveId.projects = state.activeId;
6034
+
5302
6035
  tf.area = area;
6036
+
6037
+ if (area === 'projects') {
6038
+ // Restore the saved Projects conv; discard it if it turned out to be area-scoped.
6039
+ const savedId = state.areaActiveId.projects || null;
6040
+ const savedConv = savedId && Object.values(state.conversations || {}).flat().find((c) => c.id === savedId);
6041
+ state.activeId = (savedConv && !AREA_SCOPED_JOBS.has(savedConv.jobId)) ? savedId : null;
6042
+ // Return the shared .page to the Projects workspace and reset Company/Manager hosts.
6043
+ tfEnsurePageInArea('projects');
6044
+ for (const el of document.querySelectorAll('.area-conv-host')) el.hidden = true;
6045
+ for (const el of document.querySelectorAll('.area-info-view')) el.hidden = false;
6046
+ if (typeof renderRail === 'function') renderRail();
6047
+ if (typeof renderActive === 'function') renderActive();
6048
+ }
6049
+
5303
6050
  for (const el of document.querySelectorAll('.hub-area')) {
5304
6051
  const match = el.id === 'area-' + area;
5305
6052
  el.classList.toggle('on', match);
@@ -5354,6 +6101,8 @@ function tfSelectProjectView(view, projectId) {
5354
6101
  tfRenderTree();
5355
6102
  tfRenderProjectContextTop();
5356
6103
  tfApplyWorkspaceMode();
6104
+ // Issue #578: reload deployment roster when entering workspace.
6105
+ loadDeployments();
5357
6106
  }
5358
6107
  }
5359
6108
 
@@ -5835,6 +6584,393 @@ function tfShowProjectBrief() { const a = document.getElementById('proj-brief-ac
5835
6584
  function tfShowProjectLearnings() { const a = document.getElementById('proj-learnings-acc'); if (a) a.open = true; }
5836
6585
  function tfHideProjectLearnings() { /* panels removed in #521; brief/learnings are inline top sections */ }
5837
6586
 
6587
+ // ---------------------------------------------------------------------------
6588
+ // Issue #578: Deployment roster (scheduled + webhook triggers)
6589
+ // ---------------------------------------------------------------------------
6590
+
6591
+ async function loadDeployments() {
6592
+ const list = document.getElementById('proj-deployments-list');
6593
+ if (!list) return;
6594
+ try {
6595
+ const [schResp, whResp] = await Promise.all([
6596
+ fetch('/api/ai-hub/schedules'),
6597
+ fetch('/api/ai-hub/webhooks'),
6598
+ ]);
6599
+ const schedules = schResp.ok ? await schResp.json() : [];
6600
+ const webhooks = whResp.ok ? await whResp.json() : [];
6601
+ renderDeploymentList(list, [...schedules, ...webhooks]);
6602
+ } catch {
6603
+ list.textContent = 'Could not load deployments.';
6604
+ }
6605
+ }
6606
+
6607
+ let _editingDepId = null;
6608
+ let _activeSchPreset = null;
6609
+
6610
+ function detectPreset(expr) {
6611
+ if (!expr) return null;
6612
+ const parts = expr.trim().split(/\s+/);
6613
+ if (parts.length !== 5) return 'custom';
6614
+ const [min, hour, dom, month, dow] = parts;
6615
+ if (dom !== '*' || month !== '*') return 'custom';
6616
+ if (/^\d+$/.test(min) && /^\d+$/.test(hour)) {
6617
+ if (dow === '*') return 'daily';
6618
+ if (dow === '1-5') return 'weekdays';
6619
+ if (/^\d+$/.test(dow)) return 'weekly';
6620
+ }
6621
+ return 'custom';
6622
+ }
6623
+
6624
+ function updateSchCronFromPreset(preset, timeValue, dayValue) {
6625
+ const [hStr, mStr] = (timeValue || '09:00').split(':');
6626
+ const h = parseInt(hStr, 10) || 9;
6627
+ const m = parseInt(mStr, 10) || 0;
6628
+ if (preset === 'daily') return `${m} ${h} * * *`;
6629
+ if (preset === 'weekdays') return `${m} ${h} * * 1-5`;
6630
+ if (preset === 'weekly') return `${m} ${h} * * ${dayValue || 1}`;
6631
+ return '';
6632
+ }
6633
+
6634
+ function applySchPreset(preset) {
6635
+ _activeSchPreset = preset;
6636
+ document.querySelectorAll('#dep-sch-presets .dep-preset-chip').forEach(c => {
6637
+ c.classList.toggle('active', c.dataset.preset === preset);
6638
+ });
6639
+ const timeRow = document.getElementById('dep-sch-time-row');
6640
+ const dayField = document.getElementById('dep-sch-day-field');
6641
+ const customRow = document.getElementById('dep-sch-custom-row');
6642
+ const timeInput = document.getElementById('dep-sch-time');
6643
+ const dayInput = document.getElementById('dep-sch-day');
6644
+ const cronPresetInput = document.getElementById('dep-sch-cron-preset');
6645
+ const preview = document.getElementById('dep-sch-cron-preview');
6646
+ const isCustom = preset === 'custom';
6647
+ const hasTime = preset !== null && !isCustom;
6648
+ if (timeRow) timeRow.hidden = !hasTime;
6649
+ if (dayField) dayField.hidden = preset !== 'weekly';
6650
+ if (customRow) customRow.hidden = !isCustom;
6651
+ if (hasTime && cronPresetInput && timeInput) {
6652
+ const cron = updateSchCronFromPreset(preset, timeInput.value, dayInput ? dayInput.value : '1');
6653
+ cronPresetInput.value = cron;
6654
+ if (preview) preview.textContent = cronToHuman(cron);
6655
+ }
6656
+ }
6657
+
6658
+ function cronToHuman(expr) {
6659
+ if (!expr) return '';
6660
+ const parts = expr.trim().split(/\s+/);
6661
+ if (parts.length < 5) return expr;
6662
+ const [min, hour, dom, month, dow] = parts;
6663
+ if (min.startsWith('*/') && hour === '*' && dom === '*' && month === '*' && dow === '*') {
6664
+ const n = parseInt(min.slice(2), 10);
6665
+ return 'Every ' + n + ' minute' + (n === 1 ? '' : 's');
6666
+ }
6667
+ if (!min.includes('/') && !min.includes(',') && hour === '*' && dom === '*' && month === '*' && dow === '*') {
6668
+ return 'Every hour at :' + min.padStart(2, '0');
6669
+ }
6670
+ const minuteN = parseInt(min, 10);
6671
+ const hourN = parseInt(hour, 10);
6672
+ const hasSpecificTime = !isNaN(minuteN) && !isNaN(hourN) && !min.includes('/') && !min.includes(',') && !hour.includes('/') && !hour.includes(',');
6673
+ if (!hasSpecificTime) return expr;
6674
+ const ampm = hourN >= 12 ? 'PM' : 'AM';
6675
+ const h12 = hourN % 12 === 0 ? 12 : hourN % 12;
6676
+ const timeStr = h12 + ':' + String(minuteN).padStart(2, '0') + ' ' + ampm;
6677
+ const dayNames = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
6678
+ if (dom === '*' && month === '*') {
6679
+ if (dow === '*') return 'Daily at ' + timeStr;
6680
+ if (dow === '1-5' || dow === 'MON-FRI' || dow === 'mon-fri') return 'Weekdays at ' + timeStr;
6681
+ if (dow === '0,6' || dow === '6,0' || dow === 'SAT,SUN' || dow === 'SUN,SAT') return 'Weekends at ' + timeStr;
6682
+ const dowN = parseInt(dow, 10);
6683
+ if (!isNaN(dowN) && dowN >= 0 && dowN <= 6) return dayNames[dowN] + ' at ' + timeStr;
6684
+ return 'On ' + dow + ' at ' + timeStr;
6685
+ }
6686
+ return expr;
6687
+ }
6688
+
6689
+ function renderDeploymentList(container, deployments) {
6690
+ container.innerHTML = '';
6691
+ if (!deployments.length) {
6692
+ const empty = document.createElement('p');
6693
+ empty.className = 'dep-empty';
6694
+ empty.textContent = 'No assignments yet. Use + Schedule or + Trigger above to set up scheduled and triggered assignments.';
6695
+ container.appendChild(empty);
6696
+ return;
6697
+ }
6698
+ for (const dep of deployments) {
6699
+ const card = document.createElement('div');
6700
+ card.className = 'dep-card';
6701
+ const top = document.createElement('div');
6702
+ top.className = 'dep-card-top';
6703
+ const icon = document.createElement('span');
6704
+ icon.className = 'dep-type-icon';
6705
+ icon.textContent = dep.type === 'scheduled' ? '⏱' : '🔗';
6706
+ const label = document.createElement('span');
6707
+ label.className = 'dep-label';
6708
+ label.textContent = dep.label;
6709
+ const typeBadge = document.createElement('span');
6710
+ typeBadge.className = 'dep-type-badge dep-type-badge--' + dep.type;
6711
+ typeBadge.textContent = dep.type === 'scheduled' ? 'Scheduled' : 'Triggered';
6712
+ top.appendChild(icon);
6713
+ top.appendChild(label);
6714
+ top.appendChild(typeBadge);
6715
+ card.appendChild(top);
6716
+
6717
+ if (dep.cronExpr) {
6718
+ const cron = document.createElement('div');
6719
+ cron.className = 'dep-detail';
6720
+ const human = cronToHuman(dep.cronExpr);
6721
+ cron.textContent = human !== dep.cronExpr ? human : dep.cronExpr;
6722
+ if (human !== dep.cronExpr) cron.title = dep.cronExpr;
6723
+ card.appendChild(cron);
6724
+ }
6725
+
6726
+ const employee = (state.bootstrap?.employees || []).find((e) => e.id === dep.hostId);
6727
+ if (employee) {
6728
+ const empRow = document.createElement('div');
6729
+ empRow.className = 'dep-detail dep-detail--emp';
6730
+ empRow.textContent = employee.label;
6731
+ card.appendChild(empRow);
6732
+ }
6733
+
6734
+ if (dep.inboundUrl) {
6735
+ const urlRow = document.createElement('div');
6736
+ urlRow.className = 'dep-detail dep-detail--url';
6737
+ const urlCode = document.createElement('code');
6738
+ urlCode.textContent = dep.inboundUrl;
6739
+ urlCode.className = 'dep-inbound-url dep-inbound-url--inline';
6740
+ const copyBtn = document.createElement('button');
6741
+ copyBtn.type = 'button';
6742
+ copyBtn.className = 'hm-copy-btn';
6743
+ copyBtn.textContent = 'Copy';
6744
+ copyBtn.addEventListener('click', () => { navigator.clipboard.writeText(dep.inboundUrl).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1500); }).catch(() => {}); });
6745
+ urlRow.appendChild(urlCode);
6746
+ urlRow.appendChild(copyBtn);
6747
+ card.appendChild(urlRow);
6748
+ }
6749
+
6750
+ const cardActions = document.createElement('div');
6751
+ cardActions.className = 'dep-card-actions';
6752
+
6753
+ const editBtn = document.createElement('button');
6754
+ editBtn.type = 'button';
6755
+ editBtn.className = 'dep-edit-btn';
6756
+ editBtn.title = 'Edit this assignment';
6757
+ editBtn.textContent = 'Edit';
6758
+ editBtn.addEventListener('click', () => { openDeploymentModal(dep.type === 'scheduled' ? 'schedule' : 'webhook', dep); });
6759
+ cardActions.appendChild(editBtn);
6760
+
6761
+ const delBtn = document.createElement('button');
6762
+ delBtn.type = 'button';
6763
+ delBtn.className = 'dep-del-btn';
6764
+ delBtn.title = 'Remove this assignment';
6765
+ delBtn.textContent = 'Remove';
6766
+ delBtn.addEventListener('click', async () => {
6767
+ const endpoint = dep.type === 'scheduled' ? 'schedules' : 'webhooks';
6768
+ await fetch('/api/ai-hub/' + endpoint + '/' + dep.id, { method: 'DELETE' });
6769
+ await loadDeployments();
6770
+ });
6771
+ cardActions.appendChild(delBtn);
6772
+ card.appendChild(cardActions);
6773
+ container.appendChild(card);
6774
+ }
6775
+ }
6776
+
6777
+ function initDeploymentButtons() {
6778
+ const addSchBtn = document.getElementById('dep-add-schedule-btn');
6779
+ const addWhBtn = document.getElementById('dep-add-webhook-btn');
6780
+ if (addSchBtn) addSchBtn.addEventListener('click', () => openDeploymentModal('schedule'));
6781
+ if (addWhBtn) addWhBtn.addEventListener('click', () => openDeploymentModal('webhook'));
6782
+
6783
+ // Close buttons
6784
+ const schClose = document.getElementById('dep-schedule-close');
6785
+ const whClose = document.getElementById('dep-webhook-close');
6786
+ if (schClose) schClose.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
6787
+ if (whClose) whClose.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
6788
+
6789
+ // Cancel buttons
6790
+ const schCancel = document.getElementById('dep-sch-cancel-btn');
6791
+ const whCancel = document.getElementById('dep-wh-cancel-btn');
6792
+ if (schCancel) schCancel.addEventListener('click', () => { document.getElementById('dep-schedule-modal').hidden = true; });
6793
+ if (whCancel) whCancel.addEventListener('click', () => { document.getElementById('dep-webhook-modal').hidden = true; });
6794
+
6795
+ // Save buttons
6796
+ const schSave = document.getElementById('dep-sch-save-btn');
6797
+ if (schSave) schSave.addEventListener('click', saveScheduleDeployment);
6798
+ const whSave = document.getElementById('dep-wh-save-btn');
6799
+ if (whSave) whSave.addEventListener('click', saveWebhookDeployment);
6800
+
6801
+ // Preset chips
6802
+ document.querySelectorAll('#dep-sch-presets .dep-preset-chip').forEach(chip => {
6803
+ chip.addEventListener('click', () => applySchPreset(chip.dataset.preset));
6804
+ });
6805
+ const schTimeInput = document.getElementById('dep-sch-time');
6806
+ const schDayInput = document.getElementById('dep-sch-day');
6807
+ const cronPresetInput = document.getElementById('dep-sch-cron-preset');
6808
+ if (schTimeInput) {
6809
+ schTimeInput.addEventListener('change', () => {
6810
+ if (_activeSchPreset && _activeSchPreset !== 'custom') {
6811
+ const cron = updateSchCronFromPreset(_activeSchPreset, schTimeInput.value, schDayInput ? schDayInput.value : '1');
6812
+ if (cronPresetInput) cronPresetInput.value = cron;
6813
+ const prev = document.getElementById('dep-sch-cron-preview');
6814
+ if (prev) prev.textContent = cronToHuman(cron);
6815
+ }
6816
+ });
6817
+ }
6818
+ if (schDayInput) {
6819
+ schDayInput.addEventListener('change', () => {
6820
+ if (_activeSchPreset === 'weekly') {
6821
+ const cron = updateSchCronFromPreset('weekly', schTimeInput ? schTimeInput.value : '09:00', schDayInput.value);
6822
+ if (cronPresetInput) cronPresetInput.value = cron;
6823
+ const prev = document.getElementById('dep-sch-cron-preview');
6824
+ if (prev) prev.textContent = cronToHuman(cron);
6825
+ }
6826
+ });
6827
+ }
6828
+
6829
+ // Live cron preview (custom mode)
6830
+ const cronInput = document.getElementById('dep-sch-cron');
6831
+ const cronPreview = document.getElementById('dep-sch-cron-preview');
6832
+ if (cronInput && cronPreview) {
6833
+ cronInput.addEventListener('input', () => {
6834
+ const human = cronToHuman(cronInput.value.trim());
6835
+ cronPreview.textContent = human && human !== cronInput.value.trim() ? human : '';
6836
+ });
6837
+ }
6838
+
6839
+ // Copy inbound URL button
6840
+ const copyBtn = document.getElementById('dep-wh-copy-btn');
6841
+ if (copyBtn) copyBtn.addEventListener('click', () => {
6842
+ const url = document.getElementById('dep-wh-inbound-url').textContent;
6843
+ navigator.clipboard.writeText(url).catch(() => {});
6844
+ });
6845
+ }
6846
+
6847
+ function openDeploymentModal(type, dep) {
6848
+ const jobs = state.bootstrap?.jobs ?? [];
6849
+ const employees = state.bootstrap?.employees ?? [];
6850
+ const editing = dep != null;
6851
+ _editingDepId = editing ? dep.id : null;
6852
+
6853
+ function populateJobs(sel, currentJobId) {
6854
+ sel.innerHTML = '';
6855
+ for (const j of jobs) {
6856
+ const opt = document.createElement('option');
6857
+ opt.value = j.id;
6858
+ opt.textContent = j.title;
6859
+ sel.appendChild(opt);
6860
+ }
6861
+ if (currentJobId) sel.value = currentJobId;
6862
+ }
6863
+
6864
+ function populateEmployees(sel, currentHostId) {
6865
+ sel.innerHTML = '';
6866
+ const fallback = [{ id: 'claude', label: 'Claude' }, { id: 'codex', label: 'Codex' }, { id: 'gemini', label: 'Gemini' }, { id: 'copilot', label: 'Copilot' }];
6867
+ const list = employees.length ? employees : fallback;
6868
+ for (const e of list) {
6869
+ const opt = document.createElement('option');
6870
+ opt.value = e.id;
6871
+ opt.textContent = e.label;
6872
+ sel.appendChild(opt);
6873
+ }
6874
+ if (currentHostId) sel.value = currentHostId;
6875
+ }
6876
+
6877
+ if (type === 'schedule') {
6878
+ const modal = document.getElementById('dep-schedule-modal');
6879
+ document.getElementById('dep-sch-modal-title').textContent = editing ? 'Edit Scheduled Assignment' : 'New Scheduled Assignment';
6880
+ document.getElementById('dep-sch-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
6881
+ populateJobs(document.getElementById('dep-sch-job'), dep?.jobId);
6882
+ populateEmployees(document.getElementById('dep-sch-host'), dep?.hostId || 'claude');
6883
+ document.getElementById('dep-sch-error').hidden = true;
6884
+ document.getElementById('dep-sch-label').value = dep?.label ?? '';
6885
+ document.getElementById('dep-sch-instructions').value = dep?.instructions ?? '';
6886
+ // Detect preset and pre-fill time/day
6887
+ const preset = dep?.cronExpr ? detectPreset(dep.cronExpr) : null;
6888
+ if (preset && preset !== 'custom' && dep?.cronExpr) {
6889
+ const parts = dep.cronExpr.trim().split(/\s+/);
6890
+ const m = parseInt(parts[0], 10) || 0;
6891
+ const h = parseInt(parts[1], 10) || 9;
6892
+ document.getElementById('dep-sch-time').value = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0');
6893
+ if (preset === 'weekly') document.getElementById('dep-sch-day').value = parts[4];
6894
+ document.getElementById('dep-sch-cron-preset').value = dep.cronExpr;
6895
+ } else if (preset === 'custom' && dep?.cronExpr) {
6896
+ document.getElementById('dep-sch-cron').value = dep.cronExpr;
6897
+ const cronPreview = document.getElementById('dep-sch-cron-preview');
6898
+ if (cronPreview) cronPreview.textContent = cronToHuman(dep.cronExpr);
6899
+ } else {
6900
+ document.getElementById('dep-sch-cron').value = '';
6901
+ document.getElementById('dep-sch-time').value = '09:00';
6902
+ }
6903
+ applySchPreset(preset || 'daily');
6904
+ modal.hidden = false;
6905
+ } else {
6906
+ const modal = document.getElementById('dep-webhook-modal');
6907
+ document.getElementById('dep-wh-modal-title').textContent = editing ? 'Edit Triggered Assignment' : 'New Triggered Assignment';
6908
+ document.getElementById('dep-wh-save-btn').textContent = editing ? 'Save changes' : 'Add assignment';
6909
+ populateJobs(document.getElementById('dep-wh-job'), dep?.jobId);
6910
+ populateEmployees(document.getElementById('dep-wh-host'), dep?.hostId || 'claude');
6911
+ document.getElementById('dep-wh-error').hidden = true;
6912
+ document.getElementById('dep-wh-inbound-row').hidden = true;
6913
+ document.getElementById('dep-wh-label').value = dep?.label ?? '';
6914
+ document.getElementById('dep-wh-instructions').value = dep?.instructions ?? '';
6915
+ modal.hidden = false;
6916
+ }
6917
+ }
6918
+
6919
+ async function saveScheduleDeployment() {
6920
+ const label = document.getElementById('dep-sch-label').value.trim();
6921
+ const jobId = document.getElementById('dep-sch-job').value;
6922
+ const hostId = document.getElementById('dep-sch-host').value;
6923
+ const isCustom = _activeSchPreset === 'custom';
6924
+ const cronExpr = isCustom
6925
+ ? document.getElementById('dep-sch-cron').value.trim()
6926
+ : (document.getElementById('dep-sch-cron-preset').value.trim() || '');
6927
+ const instructions = document.getElementById('dep-sch-instructions').value.trim();
6928
+ const errEl = document.getElementById('dep-sch-error');
6929
+ if (!label || !cronExpr) { errEl.textContent = 'Label and schedule are required.'; errEl.hidden = false; return; }
6930
+ const isEdit = _editingDepId !== null;
6931
+ const url = isEdit ? '/api/ai-hub/schedules/' + _editingDepId : '/api/ai-hub/schedules';
6932
+ const method = isEdit ? 'PUT' : 'POST';
6933
+ try {
6934
+ const resp = await fetch(url, {
6935
+ method,
6936
+ headers: { 'Content-Type': 'application/json' },
6937
+ body: JSON.stringify({ label, jobId, hostId, cronExpr, instructions: instructions || undefined }),
6938
+ });
6939
+ if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || (isEdit ? 'Failed to update assignment.' : 'Failed to create assignment.'); errEl.hidden = false; return; }
6940
+ _editingDepId = null;
6941
+ document.getElementById('dep-schedule-modal').hidden = true;
6942
+ await loadDeployments();
6943
+ } catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
6944
+ }
6945
+
6946
+ async function saveWebhookDeployment() {
6947
+ const label = document.getElementById('dep-wh-label').value.trim();
6948
+ const jobId = document.getElementById('dep-wh-job').value;
6949
+ const hostId = document.getElementById('dep-wh-host').value;
6950
+ const instructions = document.getElementById('dep-wh-instructions').value.trim();
6951
+ const errEl = document.getElementById('dep-wh-error');
6952
+ if (!label) { errEl.textContent = 'Label is required.'; errEl.hidden = false; return; }
6953
+ const isEdit = _editingDepId !== null;
6954
+ const url = isEdit ? '/api/ai-hub/webhooks/' + _editingDepId : '/api/ai-hub/webhooks';
6955
+ const method = isEdit ? 'PUT' : 'POST';
6956
+ try {
6957
+ const resp = await fetch(url, {
6958
+ method,
6959
+ headers: { 'Content-Type': 'application/json' },
6960
+ body: JSON.stringify({ label, jobId, hostId, instructions: instructions || undefined }),
6961
+ });
6962
+ if (!resp.ok) { const e = await resp.json().catch(() => ({})); errEl.textContent = e.error || (isEdit ? 'Failed to update assignment.' : 'Failed to create assignment.'); errEl.hidden = false; return; }
6963
+ if (!isEdit) {
6964
+ const dep = await resp.json();
6965
+ document.getElementById('dep-wh-inbound-url').textContent = dep.inboundUrl;
6966
+ document.getElementById('dep-wh-inbound-row').hidden = false;
6967
+ }
6968
+ _editingDepId = null;
6969
+ if (isEdit) document.getElementById('dep-webhook-modal').hidden = true;
6970
+ await loadDeployments();
6971
+ } catch { errEl.textContent = 'Network error — is the Hub running?'; errEl.hidden = false; }
6972
+ }
6973
+
5838
6974
  // ---------------------------------------------------------------------------
5839
6975
  // Company / Manager / Brain content
5840
6976
  // ---------------------------------------------------------------------------
@@ -5903,6 +7039,54 @@ async function tfSaveContext(key, content) {
5903
7039
  return data;
5904
7040
  }
5905
7041
 
7042
+ // #594 R3/R4: GitHub push helpers.
7043
+ function tfIsGithubConfigured() {
7044
+ return !!(state.bootstrap && state.bootstrap.repository && state.bootstrap.repository.provider === 'github');
7045
+ }
7046
+
7047
+ // #594 R4: Render a dismissable inline push bar into `container` when GitHub is
7048
+ // configured. Called after a successful inline context save so the manager can
7049
+ // push the change to the team repo with one click.
7050
+ function tfMaybeShowPushPrompt(key, container) {
7051
+ if (!tfIsGithubConfigured()) return;
7052
+ const existing = container && container.querySelector('.ctx-push-bar');
7053
+ if (existing) existing.remove();
7054
+ const bar = document.createElement('div');
7055
+ bar.className = 'ctx-push-bar';
7056
+ const msg = document.createElement('span');
7057
+ msg.className = 'ctx-push-msg';
7058
+ msg.textContent = 'Change saved locally. Push to share with your team?';
7059
+ const btn = document.createElement('button');
7060
+ btn.className = 'ctx-push-btn';
7061
+ btn.type = 'button';
7062
+ btn.textContent = 'Push to team';
7063
+ btn.addEventListener('click', async () => {
7064
+ btn.disabled = true;
7065
+ btn.textContent = 'Pushing...';
7066
+ try {
7067
+ await requestJson('/api/ai-hub/context/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }) });
7068
+ bar.remove();
7069
+ } catch {
7070
+ btn.disabled = false;
7071
+ btn.textContent = 'Push to team';
7072
+ }
7073
+ });
7074
+ const dismiss = document.createElement('button');
7075
+ dismiss.className = 'ctx-push-dismiss';
7076
+ dismiss.type = 'button';
7077
+ dismiss.textContent = 'Skip';
7078
+ dismiss.addEventListener('click', () => bar.remove());
7079
+ const autoDismiss = document.createElement('span');
7080
+ autoDismiss.className = 'ctx-push-auto';
7081
+ autoDismiss.textContent = 'Auto-dismisses in 30s';
7082
+ bar.appendChild(msg);
7083
+ bar.appendChild(btn);
7084
+ bar.appendChild(dismiss);
7085
+ bar.appendChild(autoDismiss);
7086
+ if (container) container.appendChild(bar);
7087
+ setTimeout(() => { if (bar.isConnected) bar.remove(); }, 30000);
7088
+ }
7089
+
5906
7090
  function tfContextKind(key) {
5907
7091
  return (key === 'orgRules' || key === 'managerRules' || key === 'projectRules') ? 'rules' : 'context';
5908
7092
  }
@@ -6278,6 +7462,7 @@ function tfContextRow(key, label, placeholder, rerender) {
6278
7462
  await tfSaveContext(key, tfContextContentForSave(key, label, readValue()));
6279
7463
  renderView();
6280
7464
  if (typeof rerender === 'function') rerender();
7465
+ tfMaybeShowPushPrompt(key, row);
6281
7466
  } catch (e) {
6282
7467
  save.disabled = false;
6283
7468
  save.textContent = 'Save';
@@ -6425,6 +7610,125 @@ function tfProjectUpdatesSection() {
6425
7610
  return details;
6426
7611
  }
6427
7612
 
7613
+ // #594 R2: render a conversation panel (topline + messages + coach input) into an
7614
+ // area-level container so org/manager jobs are viewable inside Company/Manager tabs.
7615
+ function tfRenderAreaConvPanel(el, conv) {
7616
+ const prevTaVal = (el.querySelector('.area-conv-coach-ta') || {}).value || '';
7617
+ el.innerHTML = '';
7618
+ const topline = document.createElement('div');
7619
+ topline.className = 'conv-topline area-conv-topline';
7620
+ const titleEl = document.createElement('span');
7621
+ titleEl.className = 'conv-job-title';
7622
+ titleEl.textContent = conv.title || conv.jobTitle || '';
7623
+ const dotCls = conversationStateDotClass(conv);
7624
+ const pill = document.createElement('span');
7625
+ pill.className = 'run-state-pill ' + dotCls;
7626
+ pill.textContent = conversationStateLabel(conv);
7627
+ topline.appendChild(titleEl);
7628
+ topline.appendChild(pill);
7629
+ el.appendChild(topline);
7630
+ const thread = document.createElement('div');
7631
+ thread.className = 'area-conv-thread';
7632
+ const msgs = (conv.messages || []).filter((m) => m.role !== 'system');
7633
+ if (msgs.length === 0) {
7634
+ const empty = document.createElement('p');
7635
+ empty.className = 'area-conv-empty';
7636
+ empty.textContent = conv.status === 'running' ? 'Working…' : 'No messages yet.';
7637
+ thread.appendChild(empty);
7638
+ } else {
7639
+ for (const msg of msgs) {
7640
+ const row = document.createElement('div');
7641
+ row.className = 'area-conv-msg area-conv-msg--' + (msg.role || 'employee');
7642
+ const txt = document.createElement('p');
7643
+ txt.className = 'area-conv-msg-text';
7644
+ txt.textContent = msg.text || '';
7645
+ row.appendChild(txt);
7646
+ thread.appendChild(row);
7647
+ }
7648
+ }
7649
+ el.appendChild(thread);
7650
+ // Show the coach input while the job is active — covers 'running' (working)
7651
+ // and 'waiting' uiState (review gate: employee posted and needs manager reply).
7652
+ const uiSt = conversationUiState(conv);
7653
+ const canCoach = uiSt === 'working' || uiSt === 'waiting';
7654
+ if (canCoach) {
7655
+ const coach = document.createElement('div');
7656
+ coach.className = 'area-conv-coach';
7657
+ const ta = document.createElement('textarea');
7658
+ ta.className = 'area-conv-coach-ta';
7659
+ ta.placeholder = 'Coach the team…';
7660
+ ta.rows = 2;
7661
+ if (prevTaVal) ta.value = prevTaVal;
7662
+ const sendBtn = document.createElement('button');
7663
+ sendBtn.type = 'button';
7664
+ sendBtn.className = 'area-conv-coach-send';
7665
+ sendBtn.textContent = 'Send';
7666
+ sendBtn.addEventListener('click', () => {
7667
+ const val = ta.value.trim();
7668
+ if (!val) return;
7669
+ state.activeId = conv.id;
7670
+ continueRun(val);
7671
+ ta.value = '';
7672
+ });
7673
+ coach.appendChild(ta);
7674
+ coach.appendChild(sendBtn);
7675
+ el.appendChild(coach);
7676
+ }
7677
+ }
7678
+
7679
+ // #594 R2: return the most-relevant org-scoped conversation to show in the Company area.
7680
+ // state.conversations is { [projectPath]: ConversationSummary[] } — flatten all.
7681
+ function tfActiveOrgConv() {
7682
+ const orgJobs = new Set(['organization-onboarding', 'organizational-learning-synthesis']);
7683
+ const convs = Object.values(state.conversations || {}).flat().filter((c) => orgJobs.has(c.jobId));
7684
+ // Prefer running > waiting (needs coaching) > most recently updated completed.
7685
+ return (
7686
+ convs.find((c) => c.status === 'running') ||
7687
+ convs.find((c) => c.status === 'waiting') ||
7688
+ convs.slice().sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0))[0] ||
7689
+ null
7690
+ );
7691
+ }
7692
+
7693
+ // #594 R2: return the org-scoped manager-agreements conversation to show in Manager area.
7694
+ function tfActiveMgrConv() {
7695
+ const mgrJobs = new Set(['manager-agreements']);
7696
+ const convs = Object.values(state.conversations || {}).flat().filter((c) => mgrJobs.has(c.jobId));
7697
+ return (
7698
+ convs.find((c) => c.status === 'running') ||
7699
+ convs.find((c) => c.status === 'waiting') ||
7700
+ convs.slice().sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0))[0] ||
7701
+ null
7702
+ );
7703
+ }
7704
+
7705
+ // Move the shared .page conversation panel into the specified area's workspace-conv host.
7706
+ // All .workspace-conv CSS rules (header/rail hidden, conv fills space) apply automatically
7707
+ // because the area-conv-host elements carry the workspace-conv class.
7708
+ function tfEnsurePageInArea(area) {
7709
+ const page = document.getElementById('hub-conv-page');
7710
+ if (!page) return;
7711
+ const target = area === 'projects'
7712
+ ? document.querySelector('#proj-workspace .workspace-conv')
7713
+ : document.getElementById(area + '-conv-host');
7714
+ if (target && page.parentElement !== target) {
7715
+ target.appendChild(page);
7716
+ }
7717
+ }
7718
+
7719
+ // Toggle a Company/Manager area between its info view (accordions) and conversation panel.
7720
+ function tfToggleAreaView(area, view) {
7721
+ const host = document.getElementById(area + '-conv-host');
7722
+ const info = document.getElementById(area + '-info-view');
7723
+ if (view === 'info') {
7724
+ if (host) host.hidden = true;
7725
+ if (info) info.hidden = false;
7726
+ tfEnsurePageInArea('projects');
7727
+ state.activeId = null;
7728
+ renderActive();
7729
+ }
7730
+ }
7731
+
6428
7732
  function tfRenderCompany() {
6429
7733
  // #521 R4/R6: company-level jobs live ONLY in this left rail (organization-onboarding
6430
7734
  // + organizational-learning-synthesis) — not in any project's job list, not as
@@ -6432,6 +7736,13 @@ function tfRenderCompany() {
6432
7736
  const rail = document.getElementById('company-rail');
6433
7737
  if (rail) {
6434
7738
  rail.innerHTML = '';
7739
+ const orgConvActive = !!tfActiveOrgConv();
7740
+ const infoBtn = document.createElement('button');
7741
+ infoBtn.type = 'button';
7742
+ infoBtn.className = 'area-rail-info-btn' + (orgConvActive ? '' : ' info-active');
7743
+ infoBtn.textContent = '📋 Company Info';
7744
+ infoBtn.addEventListener('click', () => tfToggleAreaView('company', 'info'));
7745
+ rail.appendChild(infoBtn);
6435
7746
  const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Company jobs';
6436
7747
  rail.appendChild(head);
6437
7748
  rail.appendChild(tfAreaRailJob('🏢', 'organization-onboarding', 'Organization onboarding', 'Set up / update org context & rules', () => tfStartOrgOnboarding()));
@@ -6444,32 +7755,145 @@ function tfRenderCompany() {
6444
7755
  const learn = document.getElementById('company-learnings');
6445
7756
  if (profile) {
6446
7757
  profile.innerHTML = '';
6447
- // Real org_context.md + org_rules.md content, edited inline.
6448
- profile.appendChild(tfContextRow(
6449
- 'org',
6450
- 'What you do',
6451
- 'Not set up yet — run onboarding to teach your team about your organization, or click Edit to write it directly.',
6452
- tfRenderCompany
6453
- ));
6454
- profile.appendChild(tfContextRow(
6455
- 'orgRules',
6456
- 'Guardrails',
6457
- 'Org-wide rules every employee respects before starting any job. Click Edit to add them.',
6458
- tfRenderCompany
6459
- ));
6460
- // Re-run onboarding moved to the Company jobs rail (#521 R4).
7758
+ const orgCache = tfCtxCache()['org'];
7759
+ if (orgCache && !orgCache.present) {
7760
+ // Context is confirmed absent: show CTA and open the accordion so it is
7761
+ // immediately visible without requiring a manual expand.
7762
+ const ctxAcc = document.getElementById('company-ctx-acc');
7763
+ if (ctxAcc) ctxAcc.open = true;
7764
+ const cta = document.createElement('div');
7765
+ cta.className = 'ctx-cta';
7766
+ const ico = document.createElement('div');
7767
+ ico.className = 'ctx-cta-ico';
7768
+ ico.textContent = '🏢';
7769
+ const title = document.createElement('p');
7770
+ title.className = 'ctx-cta-title';
7771
+ title.textContent = "Your team hasn't been onboarded yet";
7772
+ const msg = document.createElement('p');
7773
+ msg.className = 'ctx-cta-msg';
7774
+ msg.textContent = "Run organization onboarding to teach your employees what you do, how you operate, and the rules they must follow on every job.";
7775
+ const btn = document.createElement('button');
7776
+ btn.className = 'ctx-cta-btn';
7777
+ btn.type = 'button';
7778
+ btn.textContent = 'Run organization onboarding →';
7779
+ btn.addEventListener('click', () => tfStartOrgOnboarding());
7780
+ cta.appendChild(ico);
7781
+ cta.appendChild(title);
7782
+ cta.appendChild(msg);
7783
+ cta.appendChild(btn);
7784
+ profile.appendChild(cta);
7785
+ } else {
7786
+ // Context is present or not yet fetched: render rows (they load themselves).
7787
+ // If not yet fetched, start a background fetch and re-render to possibly switch to CTA.
7788
+ profile.appendChild(tfContextRow(
7789
+ 'org',
7790
+ 'What you do',
7791
+ 'Not set up yet — run onboarding to teach your team about your organization, or click Edit to write it directly.',
7792
+ tfRenderCompany
7793
+ ));
7794
+ profile.appendChild(tfContextRow(
7795
+ 'orgRules',
7796
+ 'Guardrails',
7797
+ 'Org-wide rules every employee respects before starting any job. Click Edit to add them.',
7798
+ tfRenderCompany
7799
+ ));
7800
+ if (!orgCache) {
7801
+ tfFetchContext('org').then(() => tfRenderCompany()).catch(() => {});
7802
+ }
7803
+ }
6461
7804
  }
6462
7805
  if (learn) {
6463
7806
  tfRenderLearningsList(learn, 'org', 'org', 'Nothing here yet — org-wide learnings that apply on every project. Run organizational-learning-synthesis (in Company jobs, left), or add one with + Add learning.');
6464
7807
  }
6465
- // Issue #540 R1-R3: render persona grid on every company-tab activation.
6466
- renderPersonaGrid();
7808
+ tfMaybeRenderOrgPushBanner();
7809
+ // Show the full coaching conversation panel when there is an active org conv;
7810
+ // otherwise show the info view (accordions). This uses the shared .page element
7811
+ // moved via tfEnsurePageInArea() so renderActive() renders the full panel.
7812
+ const orgConv = tfActiveOrgConv();
7813
+ const host = document.getElementById('company-conv-host');
7814
+ const info = document.getElementById('company-info-view');
7815
+ if (orgConv && tf.area === 'company') {
7816
+ state.activeId = orgConv.id;
7817
+ tfEnsurePageInArea('company');
7818
+ if (host) host.hidden = false;
7819
+ if (info) info.hidden = true;
7820
+ renderActive();
7821
+ } else {
7822
+ if (host) host.hidden = true;
7823
+ if (info) info.hidden = false;
7824
+ if (tf.area === 'company') tfEnsurePageInArea('projects');
7825
+ }
6467
7826
  }
7827
+
7828
+ // #594 R3: show push banner in Company area after an org onboarding run completes
7829
+ // and GitHub is configured, so the manager can push the new context to the team repo.
7830
+ function tfMaybeRenderOrgPushBanner() {
7831
+ const host = document.getElementById('company-push-banner');
7832
+ if (!host) return;
7833
+ if (!tfIsGithubConfigured()) { host.innerHTML = ''; return; }
7834
+ const orgJobs = ['organization-onboarding', 'organizational-learning-synthesis'];
7835
+ const hasCompleted = Object.values(state.conversations || {}).flat().some(
7836
+ (c) => orgJobs.includes(c.jobId) && c.status === 'completed'
7837
+ );
7838
+ if (!hasCompleted) { host.innerHTML = ''; return; }
7839
+ const dismissed = window.localStorage.getItem('fraim.aiHub.orgPushDismissed');
7840
+ if (dismissed === '1') { host.innerHTML = ''; return; }
7841
+ if (host.querySelector('.push-to-team-banner')) return;
7842
+ host.innerHTML = '';
7843
+ const banner = document.createElement('div');
7844
+ banner.className = 'push-to-team-banner';
7845
+ banner.id = 'company-push-banner-inner';
7846
+ const ico = document.createElement('span');
7847
+ ico.className = 'ptb-ico';
7848
+ ico.textContent = '📤';
7849
+ const copy = document.createElement('div');
7850
+ copy.className = 'ptb-copy';
7851
+ const repo = state.bootstrap && state.bootstrap.repository;
7852
+ const repoLabel = (repo && repo.owner && repo.name) ? (repo.owner + '/' + repo.name) : 'your team repo';
7853
+ copy.innerHTML = '<strong>Push to team?</strong> Your org context was updated locally. Push to <code>' + repoLabel + '</code> so teammates pick it up on their next pull.';
7854
+ const pushBtn = document.createElement('button');
7855
+ pushBtn.className = 'ptb-push-btn';
7856
+ pushBtn.type = 'button';
7857
+ pushBtn.textContent = 'Push to team';
7858
+ pushBtn.addEventListener('click', async () => {
7859
+ pushBtn.disabled = true;
7860
+ pushBtn.textContent = 'Pushing...';
7861
+ try {
7862
+ await requestJson('/api/ai-hub/context/push', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scope: 'org' }) });
7863
+ window.localStorage.setItem('fraim.aiHub.orgPushDismissed', '1');
7864
+ host.innerHTML = '';
7865
+ } catch {
7866
+ pushBtn.disabled = false;
7867
+ pushBtn.textContent = 'Push to team';
7868
+ }
7869
+ });
7870
+ const dismissBtn = document.createElement('button');
7871
+ dismissBtn.className = 'ptb-dismiss';
7872
+ dismissBtn.type = 'button';
7873
+ dismissBtn.textContent = 'Keep local';
7874
+ dismissBtn.addEventListener('click', () => {
7875
+ window.localStorage.setItem('fraim.aiHub.orgPushDismissed', '1');
7876
+ host.innerHTML = '';
7877
+ });
7878
+ banner.appendChild(ico);
7879
+ banner.appendChild(copy);
7880
+ banner.appendChild(pushBtn);
7881
+ banner.appendChild(dismissBtn);
7882
+ host.appendChild(banner);
7883
+ }
7884
+
6468
7885
  function tfRenderManager() {
6469
7886
  // #521 R6: manager-level job (manager-agreements) lives ONLY in this left rail.
6470
7887
  const rail = document.getElementById('manager-rail');
6471
7888
  if (rail) {
6472
7889
  rail.innerHTML = '';
7890
+ const mgrConvActive = !!tfActiveMgrConv();
7891
+ const infoBtn = document.createElement('button');
7892
+ infoBtn.type = 'button';
7893
+ infoBtn.className = 'area-rail-info-btn' + (mgrConvActive ? '' : ' info-active');
7894
+ infoBtn.textContent = '📋 Manager Info';
7895
+ infoBtn.addEventListener('click', () => tfToggleAreaView('manager', 'info'));
7896
+ rail.appendChild(infoBtn);
6473
7897
  const head = document.createElement('div'); head.className = 'area-rail-head'; head.textContent = 'Manager jobs';
6474
7898
  rail.appendChild(head);
6475
7899
  rail.appendChild(tfAreaRailJob('🤝', 'manager-agreements', 'manager-agreements', 'Set up / update how you work', () => tfStartManagerOnboarding()));
@@ -6482,21 +7906,51 @@ function tfRenderManager() {
6482
7906
  const reverse = document.getElementById('reverse-mentoring');
6483
7907
  if (profile) {
6484
7908
  profile.innerHTML = '';
6485
- // manager_context.md holds feedback style, priorities, and presenting prefs.
6486
- // We edit it as a whole (honest, single-file) rather than parsing facets.
6487
- profile.appendChild(tfContextRow(
6488
- 'manager',
6489
- 'How you work',
6490
- 'Not set up yet — run onboarding to teach your team how you like direction, feedback, priorities, and hand-off. Click Edit to write it directly.',
6491
- tfRenderManager
6492
- ));
6493
- profile.appendChild(tfContextRow(
6494
- 'managerRules',
6495
- 'Rules',
6496
- 'Standing rules for how employees should work with you. Click Edit to add them.',
6497
- tfRenderManager
6498
- ));
6499
- // Re-run onboarding moved to the Manager jobs rail (#521 R3).
7909
+ const mgrCache = tfCtxCache()['manager'];
7910
+ if (mgrCache && !mgrCache.present) {
7911
+ // Context is confirmed absent: show CTA and open the accordion.
7912
+ const mgrCtxAcc = document.getElementById('manager-ctx-acc');
7913
+ if (mgrCtxAcc) mgrCtxAcc.open = true;
7914
+ const cta = document.createElement('div');
7915
+ cta.className = 'ctx-cta';
7916
+ const ico = document.createElement('div');
7917
+ ico.className = 'ctx-cta-ico';
7918
+ ico.textContent = '🤝';
7919
+ const title = document.createElement('p');
7920
+ title.className = 'ctx-cta-title';
7921
+ title.textContent = 'Set up your manager profile';
7922
+ const msg = document.createElement('p');
7923
+ msg.className = 'ctx-cta-msg';
7924
+ msg.textContent = "Run manager onboarding so your team knows how you like to work, give feedback, and delegate. Your preferences become standing rules every employee respects.";
7925
+ const btn = document.createElement('button');
7926
+ btn.className = 'ctx-cta-btn';
7927
+ btn.type = 'button';
7928
+ btn.textContent = 'Run manager onboarding →';
7929
+ btn.addEventListener('click', () => tfStartManagerOnboarding());
7930
+ cta.appendChild(ico);
7931
+ cta.appendChild(title);
7932
+ cta.appendChild(msg);
7933
+ cta.appendChild(btn);
7934
+ profile.appendChild(cta);
7935
+ } else {
7936
+ // Context is present or not yet fetched: render rows (they load themselves).
7937
+ // If not yet fetched, start a background fetch and re-render to possibly switch to CTA.
7938
+ profile.appendChild(tfContextRow(
7939
+ 'manager',
7940
+ 'How you work',
7941
+ 'Not set up yet — run onboarding to teach your team how you like direction, feedback, priorities, and hand-off. Click Edit to write it directly.',
7942
+ tfRenderManager
7943
+ ));
7944
+ profile.appendChild(tfContextRow(
7945
+ 'managerRules',
7946
+ 'Rules',
7947
+ 'Standing rules for how employees should work with you. Click Edit to add them.',
7948
+ tfRenderManager
7949
+ ));
7950
+ if (!mgrCache) {
7951
+ tfFetchContext('manager').then(() => tfRenderManager()).catch(() => {});
7952
+ }
7953
+ }
6500
7954
  }
6501
7955
  if (learn) {
6502
7956
  // #533: Manager learnings = "how your team adapts to you" = your work patterns
@@ -6508,42 +7962,25 @@ function tfRenderManager() {
6508
7962
  // the manager-coaching learnings the team has captured for you.
6509
7963
  tfRenderLearningsList(reverse, 'reverse', 'machine', 'Nothing here yet — as your team works with you, coaching on what you can improve on as a manager appears here. Add one with + Add learning.');
6510
7964
  }
6511
- tfRenderManagerTeam();
6512
7965
  // Issue #540 R4-R7: render manager team pool on every manager-tab activation.
6513
7966
  renderManagerTeamPool();
6514
- }
6515
- function tfRenderManagerTeam() {
6516
- const section = document.getElementById('manager-team-section');
6517
- if (!section) return;
6518
- section.innerHTML = '';
6519
- const hired = tfHiredPersonas();
6520
- if (hired.length === 0) {
6521
- const msg = document.createElement('p');
6522
- msg.className = 'manager-no-team-msg';
6523
- msg.textContent = "You haven't hired any specialists yet. Go to Company to see the full roster and hire the team you need.";
6524
- section.appendChild(msg);
6525
- const btn = document.createElement('button');
6526
- btn.className = 'manager-go-company-btn';
6527
- btn.type = 'button';
6528
- btn.textContent = 'Go to Company to hire';
6529
- btn.addEventListener('click', () => tfShowArea('company'));
6530
- section.appendChild(btn);
6531
- return;
7967
+ // Show the full coaching conversation panel when there is an active manager conv;
7968
+ // otherwise show the info view (accordions). Uses the shared .page element.
7969
+ const mgrConv = tfActiveMgrConv();
7970
+ const mgrHost = document.getElementById('manager-conv-host');
7971
+ const mgrInfo = document.getElementById('manager-info-view');
7972
+ if (mgrConv && tf.area === 'manager') {
7973
+ state.activeId = mgrConv.id;
7974
+ tfEnsurePageInArea('manager');
7975
+ if (mgrHost) mgrHost.hidden = false;
7976
+ if (mgrInfo) mgrInfo.hidden = true;
7977
+ renderActive();
7978
+ } else {
7979
+ if (mgrHost) mgrHost.hidden = true;
7980
+ if (mgrInfo) mgrInfo.hidden = false;
7981
+ if (tf.area === 'manager') tfEnsurePageInArea('projects');
6532
7982
  }
6533
- const grid = document.createElement('div');
6534
- grid.className = 'emp-grid';
6535
- hired.forEach((p, i) => {
6536
- const av = tfAvatarFor(p.displayName, i);
6537
- const tile = document.createElement('div');
6538
- tile.className = 'emp-tile';
6539
- tile.innerHTML = '<div class="et-av" style="background:' + av.color + ';">' + av.badge + '</div>' +
6540
- '<div class="et-name">' + tfEscape(p.displayName) + '</div>' +
6541
- '<div class="et-role">' + tfEscape(p.role || '') + '</div>';
6542
- grid.appendChild(tile);
6543
- });
6544
- section.appendChild(grid);
6545
7983
  }
6546
-
6547
7984
  // #533: Reverse mentoring now renders the real manager-coaching learnings via
6548
7985
  // tfRenderLearningsList(scope='reverse') — consistent with the other learning
6549
7986
  // sections — replacing the earlier representative-seed feed (tfReverseItems /
@@ -6675,7 +8112,7 @@ async function tfFetchPreservedLearnings(scope, level) {
6675
8112
  // cross-surface render churn that left the project tree unstable. #533)
6676
8113
  function tfReRenderScope(scope, level) {
6677
8114
  if (level === 'project') {
6678
- if (document.querySelector('.workspace-conv') && typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
8115
+ if (document.querySelector('#proj-workspace .workspace-conv') && typeof tfRenderProjectContextTop === 'function') tfRenderProjectContextTop();
6679
8116
  return;
6680
8117
  }
6681
8118
  if (scope === 'org') { if (typeof tfRenderCompany === 'function') tfRenderCompany(); return; }
@@ -7033,76 +8470,84 @@ function tfCloseAccountMenu() {
7033
8470
  if (menu) menu.classList.remove('open');
7034
8471
  }
7035
8472
 
7036
- // ---------------------------------------------------------------------------
7037
- // Get-started rail (driven by bootstrap.firstRun {install,company,hire,project})
7038
- // ---------------------------------------------------------------------------
7039
- function tfFirstRunActiveStep(fr) {
7040
- for (const id of TF_STEP_ORDER) { if (!fr[id]) return id; }
7041
- return null;
7042
- }
7043
- function tfRenderRail() {
7044
- const rail = document.getElementById('gs-rail');
7045
- const pill = document.getElementById('gs-pill');
7046
- const steps = document.getElementById('gs-steps');
7047
- if (!rail || !pill || !steps) return;
7048
- const fr = state.bootstrap && state.bootstrap.firstRun;
7049
- const allComplete = fr && TF_STEP_ORDER.every((id) => fr[id]);
7050
- if (!fr || allComplete) { rail.hidden = true; pill.hidden = true; return; }
7051
- const activeStep = tfFirstRunActiveStep(fr);
7052
- const collapsed = window.localStorage.getItem(STORAGE_KEY_RAIL_512) === '1';
7053
- steps.innerHTML = '';
7054
- TF_STEP_ORDER.forEach((id, i) => {
7055
- const btn = document.createElement('button');
7056
- btn.type = 'button';
7057
- btn.className = 'gs-step' + (fr[id] ? ' done' : (id === activeStep ? ' current' : ''));
7058
- btn.textContent = TF_STEP_LABELS[id];
7059
- btn.addEventListener('click', () => tfRailGo(id));
7060
- steps.appendChild(btn);
7061
- if (i < TF_STEP_ORDER.length - 1) {
7062
- const sep = document.createElement('span');
7063
- sep.className = 'gs-sep';
7064
- sep.textContent = '›';
7065
- steps.appendChild(sep);
7066
- }
7067
- });
7068
- const remaining = TF_STEP_ORDER.filter((id) => !fr[id]).length;
7069
- pill.textContent = '🚀 Finish setup · ' + remaining + ' step' + (remaining === 1 ? '' : 's') + ' left';
7070
- rail.hidden = collapsed;
7071
- pill.hidden = !collapsed;
8473
+ // Per-job config for the area onboarding pre-flight modal.
8474
+ const AREA_ONBOARD_CONFIG = {
8475
+ 'organization-onboarding': {
8476
+ title: 'Organization onboarding',
8477
+ desc: 'FRAIM will learn what your company does and write org_context.md and org_rules.md. Add any specific focus areas or context below.',
8478
+ },
8479
+ 'organizational-learning-synthesis': {
8480
+ title: 'Learning synthesis',
8481
+ desc: 'FRAIM will consolidate recurring patterns across projects into org-wide learnings. Add any specific focus areas below.',
8482
+ },
8483
+ 'manager-agreements': {
8484
+ title: 'Manager agreements',
8485
+ desc: 'FRAIM will learn how you work and write manager_context.md and manager_rules.md. Add any specific direction below.',
8486
+ },
8487
+ };
8488
+
8489
+ function tfOpenAreaOnboardModal(jobId, baseMessage, area) {
8490
+ tf.aomJob = { jobId, baseMessage, area };
8491
+ const cfg = AREA_ONBOARD_CONFIG[jobId] || { title: jobId, desc: 'Add any specific context for this run.' };
8492
+ const titleEl = document.getElementById('aom-title');
8493
+ const descEl = document.getElementById('aom-desc');
8494
+ const ctx = document.getElementById('aom-context');
8495
+ if (titleEl) titleEl.textContent = cfg.title;
8496
+ if (descEl) descEl.textContent = cfg.desc;
8497
+ if (ctx) ctx.value = '';
8498
+ const modal = document.getElementById('area-onboard-modal');
8499
+ if (modal) modal.hidden = false;
8500
+ if (ctx) ctx.focus();
8501
+ }
8502
+
8503
+ function tfCloseAreaOnboardModal() {
8504
+ const modal = document.getElementById('area-onboard-modal');
8505
+ if (modal) modal.hidden = true;
8506
+ tf.aomJob = null;
7072
8507
  }
7073
- function tfRailGo(step) {
7074
- if (step === 'install') { window.location.href = '/account'; return; }
7075
- if (step === 'company' || step === 'hire') { tfStartOrgOnboarding(); return; }
7076
- if (step === 'project') { tfShowArea('projects'); tfOpenNewProject(); }
8508
+
8509
+ async function tfSubmitAreaOnboardModal() {
8510
+ const { jobId, baseMessage, area } = tf.aomJob || {};
8511
+ if (!jobId) return;
8512
+ const ctx = document.getElementById('aom-context');
8513
+ const userContext = (ctx && ctx.value.trim()) || '';
8514
+ tfCloseAreaOnboardModal();
8515
+ await tfStartOnboardingJob(jobId, baseMessage, area, userContext);
7077
8516
  }
7078
8517
 
7079
- async function tfStartOnboardingJob(jobId, message, targetArea) {
8518
+ async function tfStartOnboardingJob(jobId, message, targetArea, userContext) {
7080
8519
  const employeeId = state.selectedEmployeeId || 'claude';
7081
8520
  const allJobs = [
7082
8521
  ...((state.bootstrap && state.bootstrap.jobs) || []),
7083
8522
  ...((state.bootstrap && state.bootstrap.managerTemplates) || []),
7084
8523
  ];
7085
8524
  const job = allJobs.find((j) => j.id === jobId) || { id: jobId, title: jobId };
8525
+ // If the user typed a direction in the pre-flight modal, send only that — the
8526
+ // job already knows what to do from its FRAIM instructions. Fall back to the
8527
+ // job's registry intent (or the base message string) when no context is given.
8528
+ const jobMessage = (job && job.intent && job.intent.trim()) ? job.intent : message;
8529
+ const finalMessage = (userContext && userContext.trim()) ? userContext.trim() : jobMessage;
7086
8530
  if (job && typeof startRun === 'function') {
7087
8531
  if (targetArea) tfShowArea(targetArea);
7088
- await startRun(job, message, employeeId);
7089
- tfShowArea('projects');
7090
- tfSelectProjectView('workspace', tf.activeProjectId);
8532
+ await startRun(job, finalMessage, employeeId);
8533
+ // After run creation refresh the area panel so the conversation becomes visible.
8534
+ if (targetArea === 'company') tfRenderCompany();
8535
+ else if (targetArea === 'manager') tfRenderManager();
7091
8536
  } else if (typeof showStatus === 'function') {
7092
8537
  showStatus('Onboarding job not found in this project yet. Pick a project folder first.', true);
7093
8538
  }
7094
8539
  }
7095
8540
 
7096
- async function tfStartOrgOnboarding() {
7097
- await tfStartOnboardingJob(
8541
+ function tfStartOrgOnboarding() {
8542
+ tfOpenAreaOnboardModal(
7098
8543
  'organization-onboarding',
7099
8544
  'Onboard my organization: learn what we do and the guardrails every employee should follow, then write org_context.md and org_rules.md.',
7100
8545
  'company'
7101
8546
  );
7102
8547
  }
7103
8548
 
7104
- async function tfStartManagerOnboarding() {
7105
- await tfStartOnboardingJob(
8549
+ function tfStartManagerOnboarding() {
8550
+ tfOpenAreaOnboardModal(
7106
8551
  'manager-agreements',
7107
8552
  'Set up my manager agreements: learn how I like to work, how I give feedback, and the standing rules employees should follow with me, then write manager_context.md and manager_rules.md.',
7108
8553
  'manager'
@@ -7111,8 +8556,8 @@ async function tfStartManagerOnboarding() {
7111
8556
 
7112
8557
  // #521 R4: company-level learning synthesis — wired to the real
7113
8558
  // organizational-learning-synthesis job, launched only from the Company rail.
7114
- async function tfStartOrgLearningSynthesis() {
7115
- await tfStartOnboardingJob(
8559
+ function tfStartOrgLearningSynthesis() {
8560
+ tfOpenAreaOnboardModal(
7116
8561
  'organizational-learning-synthesis',
7117
8562
  'Synthesize our company learnings: consolidate the recurring patterns across projects into preserved org-wide learnings, then update org memory.',
7118
8563
  'company'
@@ -7138,36 +8583,59 @@ async function tfStartProjectOnboarding(userInput) {
7138
8583
  await tfStartOnboardingJob('project-onboarding', parts.join('\n\n'), 'projects');
7139
8584
  }
7140
8585
 
7141
- // #521: collect the manager's direction before a (re)processing run so they can
7142
- // steer it. mode 'run' = first onboarding (no context yet); 'reprocess' = refine
7143
- // existing context/rules. The input is optional Start runs with or without it.
7144
- function tfOpenOnboardInput(mode) {
7145
- const modal = document.getElementById('onboard-input-modal');
7146
- const title = document.getElementById('obi-title');
7147
- const sub = document.getElementById('obi-sub');
7148
- const input = document.getElementById('obi-input');
7149
- if (!modal) { tfStartProjectOnboarding(); return; }
7150
- const reprocess = mode === 'reprocess';
7151
- if (title) title.textContent = reprocess ? 'Reprocess project onboarding' : 'Run project onboarding';
7152
- if (sub) {
7153
- sub.textContent = reprocess
7154
- ? "Your team re-reads the current context & rules (including your edits) and refines them. Add anything you want them to focus on or incorporate — optional."
7155
- : "Your team explores the project and captures its context and rules. Tell them what this project is or what to focus on — optional.";
7156
- }
7157
- if (input) input.value = '';
7158
- modal.hidden = false;
7159
- if (input) input.focus();
8586
+ // #594 R7: #onboard-input-modal retired. tfOpenOnboardInput now opens #np-modal
8587
+ // with the folder pre-filled and locked so the manager can only supply a direction
8588
+ // message (the intent field) before starting the re-run.
8589
+ function tfOpenOnboardInput() {
8590
+ tfOpenNewProjectRerun();
7160
8591
  }
7161
8592
  function tfCloseOnboardInput() {
7162
- const m = document.getElementById('onboard-input-modal');
7163
- if (m) m.hidden = true;
8593
+ tfCloseNewProject();
7164
8594
  }
7165
8595
  function tfSubmitOnboardInput() {
7166
- const input = document.getElementById('obi-input');
7167
- const val = input ? input.value : '';
7168
- tfCloseOnboardInput();
8596
+ const intent = document.getElementById('np-intent');
8597
+ const val = intent ? intent.value : '';
8598
+ tfCloseNewProject();
7169
8599
  tfStartProjectOnboarding(val);
7170
8600
  }
8601
+ function tfOpenNewProjectRerun() {
8602
+ const modal = document.getElementById('np-modal');
8603
+ if (!modal) { tfStartProjectOnboarding(); return; }
8604
+ const folder = document.getElementById('np-folder');
8605
+ const np1Next = document.getElementById('np1-next');
8606
+ const intent = document.getElementById('np-intent');
8607
+ const browseBtn = document.getElementById('np-browse');
8608
+ if (folder) {
8609
+ folder.value = state.projectPath || '';
8610
+ folder.readOnly = true;
8611
+ }
8612
+ if (browseBtn) browseBtn.hidden = true;
8613
+ if (intent) {
8614
+ intent.value = '';
8615
+ const label = intent.closest('.np-field') && intent.closest('.np-field').querySelector('label');
8616
+ if (label) label.dataset.originalText = label.innerHTML;
8617
+ if (label) label.innerHTML = 'Anything new to focus on this time? <span class="np-optional">(optional)</span>';
8618
+ }
8619
+ if (np1Next) { np1Next.disabled = false; np1Next.removeAttribute('disabled'); }
8620
+ // Update Step 1 heading and description for re-run context.
8621
+ const np1Hdr = document.querySelector('#np1 .np-hdr');
8622
+ if (np1Hdr) {
8623
+ const h2 = np1Hdr.querySelector('h2');
8624
+ const desc = np1Hdr.querySelector('p');
8625
+ const stepLabel = np1Hdr.querySelector('.step-label');
8626
+ if (h2) { h2.dataset.originalText = h2.textContent; h2.textContent = 'Update project onboarding'; }
8627
+ if (desc) { desc.dataset.originalText = desc.textContent; desc.textContent = 'Your project folder is already set. Tell the team anything new to focus on this time.'; }
8628
+ if (stepLabel) { stepLabel.dataset.originalText = stepLabel.textContent; stepLabel.textContent = 'Step 1 of 4 — Re-run'; }
8629
+ }
8630
+ const np1Hint = document.querySelector('#np1 .np-field-hint');
8631
+ if (np1Hint) { np1Hint.dataset.originalText = np1Hint.textContent; np1Hint.textContent = 'This project folder is already set — only the direction below will change.'; }
8632
+ modal.dataset.rerun = '1';
8633
+ // Pre-select all hired personas so step 2 starts with a sensible default.
8634
+ tf.npSelectedEmployees = tfHiredPersonas().map((p) => p.key);
8635
+ tfRenderNpEmployees();
8636
+ tfNpGo(1);
8637
+ modal.hidden = false;
8638
+ }
7171
8639
 
7172
8640
  // ---------------------------------------------------------------------------
7173
8641
  // New-project modal (4-step)
@@ -7188,7 +8656,26 @@ function tfOpenNewProject() {
7188
8656
  }
7189
8657
  function tfCloseNewProject() {
7190
8658
  const m = document.getElementById('np-modal');
7191
- if (m) m.hidden = true;
8659
+ if (m) { m.hidden = true; delete m.dataset.rerun; }
8660
+ const folder = document.getElementById('np-folder');
8661
+ if (folder) folder.readOnly = false;
8662
+ const browseBtn = document.getElementById('np-browse');
8663
+ if (browseBtn) browseBtn.hidden = false;
8664
+ const intent = document.getElementById('np-intent');
8665
+ if (intent) {
8666
+ const label = intent.closest('.np-field') && intent.closest('.np-field').querySelector('label');
8667
+ if (label && label.dataset.originalText) { label.innerHTML = label.dataset.originalText; delete label.dataset.originalText; }
8668
+ }
8669
+ // Restore Step 1 heading, description, step-label, and hint to new-project defaults.
8670
+ const np1Hdr = document.querySelector('#np1 .np-hdr');
8671
+ if (np1Hdr) {
8672
+ for (const sel of ['h2', 'p', '.step-label']) {
8673
+ const el = np1Hdr.querySelector(sel);
8674
+ if (el && el.dataset.originalText) { el.textContent = el.dataset.originalText; delete el.dataset.originalText; }
8675
+ }
8676
+ }
8677
+ const np1Hint = document.querySelector('#np1 .np-field-hint');
8678
+ if (np1Hint && np1Hint.dataset.originalText) { np1Hint.textContent = np1Hint.dataset.originalText; delete np1Hint.dataset.originalText; }
7192
8679
  }
7193
8680
  function tfNpGo(n) {
7194
8681
  tf.npStep = n;
@@ -7643,16 +9130,6 @@ function tfWireShell() {
7643
9130
  if (overviewTab) overviewTab.addEventListener('click', () => tfSelectProjectView('overview'));
7644
9131
  const addTab = document.getElementById('ptab-add');
7645
9132
  if (addTab) addTab.addEventListener('click', tfOpenNewProject);
7646
- const cta = document.getElementById('gs-cta');
7647
- if (cta) cta.addEventListener('click', () => {
7648
- const fr = state.bootstrap && state.bootstrap.firstRun;
7649
- const active = fr && tfFirstRunActiveStep(fr);
7650
- if (active) tfRailGo(active);
7651
- });
7652
- const hide = document.getElementById('gs-hide');
7653
- if (hide) hide.addEventListener('click', () => { window.localStorage.setItem(STORAGE_KEY_RAIL_512, '1'); tfRenderRail(); });
7654
- const pill = document.getElementById('gs-pill');
7655
- if (pill) pill.addEventListener('click', () => { window.localStorage.removeItem(STORAGE_KEY_RAIL_512); tfRenderRail(); });
7656
9133
  const npClose = document.getElementById('np-close');
7657
9134
  if (npClose) npClose.addEventListener('click', tfCloseNewProject);
7658
9135
 
@@ -7690,7 +9167,16 @@ function tfWireShell() {
7690
9167
  }
7691
9168
  const npModal = document.getElementById('np-modal');
7692
9169
  if (npModal) npModal.addEventListener('click', (e) => { if (e.target === npModal) tfCloseNewProject(); });
7693
- const closers = [['aj-close', tfCloseAssignJob], ['ae-close', tfCloseAddEmp], ['pr-close', tfClosePricing], ['obi-close', tfCloseOnboardInput], ['obi-cancel', tfCloseOnboardInput], ['hm-close', tfCloseHireManager]];
9170
+ // #594: Area onboarding pre-flight modal wiring.
9171
+ const aomClose = document.getElementById('aom-close');
9172
+ if (aomClose) aomClose.addEventListener('click', tfCloseAreaOnboardModal);
9173
+ const aomCancel = document.getElementById('aom-cancel');
9174
+ if (aomCancel) aomCancel.addEventListener('click', tfCloseAreaOnboardModal);
9175
+ const aomStart = document.getElementById('aom-start');
9176
+ if (aomStart) aomStart.addEventListener('click', tfSubmitAreaOnboardModal);
9177
+ const aomModal = document.getElementById('area-onboard-modal');
9178
+ if (aomModal) aomModal.addEventListener('click', (e) => { if (e.target === aomModal) tfCloseAreaOnboardModal(); });
9179
+ const closers = [['aj-close', tfCloseAssignJob], ['ae-close', tfCloseAddEmp], ['pr-close', tfClosePricing], ['hm-close', tfCloseHireManager]];
7694
9180
  for (const [id, fn] of closers) {
7695
9181
  const el = document.getElementById(id);
7696
9182
  if (el) el.addEventListener('click', fn);
@@ -7716,16 +9202,7 @@ function tfWireShell() {
7716
9202
  const q = document.getElementById('hm-query');
7717
9203
  if (q && navigator.clipboard) navigator.clipboard.writeText(q.textContent || '');
7718
9204
  });
7719
- const hmBtn = document.getElementById('manager-hire-btn');
7720
- if (hmBtn) hmBtn.addEventListener('click', () => {
7721
- const mh = state.bootstrap && state.bootstrap.managerHiring;
7722
- const hired = ((state.bootstrap && state.bootstrap.personas) || []).filter((p) => p.status === 'hired' && mh && mh.roles[p.key]);
7723
- const first = hired[0] ? hired[0].key : (mh ? Object.keys(mh.roles)[0] : null);
7724
- openHireManagerModal(first);
7725
- });
7726
- const obiStart = document.getElementById('obi-start');
7727
- if (obiStart) obiStart.addEventListener('click', tfSubmitOnboardInput);
7728
- const backdrops = [['assign-job-modal', tfCloseAssignJob], ['add-emp-modal', tfCloseAddEmp], ['pricing-modal', tfClosePricing], ['onboard-input-modal', tfCloseOnboardInput], ['hm-modal', tfCloseHireManager]];
9205
+ const backdrops = [['assign-job-modal', tfCloseAssignJob], ['add-emp-modal', tfCloseAddEmp], ['pricing-modal', tfClosePricing], ['hm-modal', tfCloseHireManager]];
7729
9206
  for (const [id, fn] of backdrops) {
7730
9207
  const el = document.getElementById(id);
7731
9208
  if (el) el.addEventListener('click', (e) => { if (e.target === el) fn(); });
@@ -7736,7 +9213,6 @@ function tfWireShell() {
7736
9213
  // dismisses whichever team-flow modal is open (top-most first).
7737
9214
  const escClosers = [
7738
9215
  ['hm-modal', tfCloseHireManager],
7739
- ['onboard-input-modal', tfCloseOnboardInput],
7740
9216
  ['pricing-modal', tfClosePricing],
7741
9217
  ['add-emp-modal', tfCloseAddEmp],
7742
9218
  ['assign-job-modal', tfCloseAssignJob],
@@ -7805,9 +9281,10 @@ function tfInitShell() {
7805
9281
  } else if (tf.projects.length && !tf.activeProjectId) {
7806
9282
  tf.activeProjectId = tf.projects[0].id;
7807
9283
  }
7808
- tfRenderRail();
7809
9284
  tfRenderProjectTabs();
7810
9285
  tfShowArea('projects');
9286
+ // Issue #578: wire deployment modal buttons.
9287
+ initDeploymentButtons();
7811
9288
  // Open the workspace by default so the conversation surface (#conversation,
7812
9289
  // #new-conv-btn, #empty, the welcome line, the project button) is visible on
7813
9290
  // load — preserving the contract the shipped #339/#347/#429/#442 Hub UI
@@ -7817,12 +9294,15 @@ function tfInitShell() {
7817
9294
  } else {
7818
9295
  tfSelectProjectView('overview');
7819
9296
  }
9297
+ // Server-hydrated completed runs can arrive before the shell finishes wiring
9298
+ // the project workspace. Re-render once the workspace is selected so manager
9299
+ // delegation boards are visible on first load, not only after a refresh tick.
9300
+ renderActive();
7820
9301
  }
7821
9302
 
7822
9303
  // Refresh shell-derived UI after a bootstrap reload (e.g. project picked).
7823
9304
  function tfRefreshAfterBootstrap() {
7824
9305
  if (!tf.enabled) return;
7825
- tfRenderRail();
7826
9306
  tfRenderProjectTabs();
7827
9307
  if (tf.area === 'company') tfRenderCompany();
7828
9308
  else if (tf.area === 'manager') tfRenderManager();