@yemi33/minions 0.1.2453 → 0.1.2455

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 (48) hide show
  1. package/bin/install-internal-minions.js +136 -6
  2. package/bin/minions.js +27 -13
  3. package/dashboard/js/agent-identity.js +121 -0
  4. package/dashboard/js/modal.js +4 -0
  5. package/dashboard/js/refresh.js +38 -3
  6. package/dashboard/js/render-agents.js +14 -2
  7. package/dashboard/js/render-dispatch.js +2 -2
  8. package/dashboard/js/render-other.js +147 -13
  9. package/dashboard/js/render-prd.js +6 -5
  10. package/dashboard/js/render-prs.js +44 -25
  11. package/dashboard/js/render-work-items.js +442 -22
  12. package/dashboard/js/settings.js +8 -0
  13. package/dashboard/js/utils.js +40 -0
  14. package/dashboard/pages/tools.html +1 -0
  15. package/dashboard/pages/work.html +13 -0
  16. package/dashboard/shared/pr-author.js +75 -0
  17. package/dashboard/shared/pr-filters.js +28 -5
  18. package/dashboard/styles.css +85 -0
  19. package/dashboard-build.js +3 -3
  20. package/dashboard.js +93 -7
  21. package/docs/README.md +1 -0
  22. package/docs/copilot-cli-schema.md +1 -0
  23. package/docs/engine-restart.md +4 -2
  24. package/docs/internal-install.md +44 -5
  25. package/docs/named-agents.md +48 -0
  26. package/docs/pr-author-identity.md +63 -10
  27. package/docs/runtime-adapters.md +39 -0
  28. package/docs/temporary-agents.md +172 -0
  29. package/engine/ado/comment.js +261 -4
  30. package/engine/agents/llm.js +26 -0
  31. package/engine/agents/playbook.js +2 -1
  32. package/engine/api/settings-validation.js +25 -0
  33. package/engine/core/operator-identity.js +23 -1
  34. package/engine/core/queries.js +53 -2
  35. package/engine/core/shared.js +286 -9
  36. package/engine/db/migrations/032-review-enrolled-pr-context-only.js +95 -0
  37. package/engine/operations/cli.js +102 -1
  38. package/engine/orchestration/lifecycle.js +7 -0
  39. package/engine/orchestration/routing.js +4 -1
  40. package/engine/providers/gh-comment.js +159 -0
  41. package/engine/recovery/stop-stack.js +16 -2
  42. package/engine/runtimes/claude.js +3 -0
  43. package/engine/runtimes/codex.js +4 -0
  44. package/engine/runtimes/copilot.js +23 -2
  45. package/engine.js +2 -2
  46. package/package.json +1 -1
  47. package/playbooks/fix.md +23 -2
  48. package/playbooks/shared-rules.md +24 -2
@@ -702,13 +702,97 @@ function _formatTrackedCost(value, available) {
702
702
  return '$' + (Number(value) || 0).toFixed(2);
703
703
  }
704
704
 
705
+ // Registry of NON-USD runtime-reported billable units (W-msdcgitc). Generic so a
706
+ // future adapter can surface its own unit without touching call sites; the
707
+ // display layer keys off typed `m.billableUnit` metadata, never a runtime name.
708
+ var BILLABLE_UNIT_META = {
709
+ premiumRequests: { abbrev: 'req', label: 'premium requests', tileLabel: 'Premium Requests' },
710
+ };
711
+
712
+ function _billableBucket(m, unit) {
713
+ return (m && m.billableUnits && m.billableUnits[unit]) || null;
714
+ }
715
+
716
+ function _formatBillableUnitValue(unit, value) {
717
+ var meta = BILLABLE_UNIT_META[unit];
718
+ var abbr = meta ? meta.abbrev : unit;
719
+ var n = Number(value) || 0;
720
+ var num = Number.isInteger(n) ? String(n) : n.toFixed(1);
721
+ return num + ' ' + abbr;
722
+ }
723
+
724
+ function _hasReportedBillableUnits(m) {
725
+ if (!m || !m.billableUnits) return false;
726
+ for (var u in m.billableUnits) {
727
+ var b = m.billableUnits[u];
728
+ if (b && (b.reported || 0) > 0) return true;
729
+ }
730
+ return false;
731
+ }
732
+
733
+ function _firstReportedBillableUnit(m) {
734
+ if (!m || !m.billableUnits) return null;
735
+ for (var u2 in m.billableUnits) {
736
+ var b2 = m.billableUnits[u2];
737
+ if (b2 && (b2.reported || 0) > 0) return u2;
738
+ }
739
+ return null;
740
+ }
741
+
742
+ // Primary consumption cell for an agent row — the SAME UI position where a
743
+ // USD-billed runtime shows dollar cost. For 'usd' runtimes this is unchanged
744
+ // dollar rendering; for a native-unit runtime (e.g. Copilot premium requests)
745
+ // it shows the authoritative reported count, and 'N/A' when the CLI never
746
+ // reported the unit (distinct from a genuine reported 0). Switches on typed
747
+ // metadata (m.billableUnit), never the runtime name.
748
+ function _formatPrimaryUsage(m) {
749
+ var unit = m.billableUnit || 'usd';
750
+ if (unit === 'usd') {
751
+ return _formatTrackedCost(m.totalCostUsd, m.costTracking !== false);
752
+ }
753
+ var bucket = _billableBucket(m, unit);
754
+ if (!bucket || (bucket.reported || 0) === 0) return 'N/A';
755
+ return _formatBillableUnitValue(unit, bucket.total || 0);
756
+ }
757
+
758
+ function _formatPrimaryPerTask(m, tasks) {
759
+ var unit = m.billableUnit || 'usd';
760
+ if (unit === 'usd') {
761
+ var costAvailable = m.costTracking !== false;
762
+ return tasks > 0 ? _formatTrackedCost((m.totalCostUsd || 0) / tasks, costAvailable) : '-';
763
+ }
764
+ var bucket = _billableBucket(m, unit);
765
+ if (!bucket || (bucket.reported || 0) === 0) return 'N/A';
766
+ return tasks > 0 ? _formatBillableUnitValue(unit, (bucket.total || 0) / tasks) : '-';
767
+ }
768
+
769
+ // Primary consumption cell for an _engine category row (CC/doc-chat/…). USD wins
770
+ // when present; otherwise a reported native billable unit (e.g. Copilot premium
771
+ // requests for a Copilot-driven Command Center) renders in the SAME position,
772
+ // and 'N/A' when neither is reported. Typed off the data, never a runtime name.
773
+ function _formatEnginePrimaryUsage(e) {
774
+ if ((e.costUsd || 0) > 0) return _formatTrackedCost(e.costUsd, true);
775
+ var unit = _firstReportedBillableUnit(e);
776
+ if (unit) return _formatBillableUnitValue(unit, e.billableUnits[unit].total || 0);
777
+ return 'N/A';
778
+ }
779
+
780
+ function _formatEnginePrimaryPerCall(e, calls) {
781
+ if (calls <= 0) return '-';
782
+ if ((e.costUsd || 0) > 0) return _formatTrackedCost((e.costUsd || 0) / calls, true);
783
+ var unit = _firstReportedBillableUnit(e);
784
+ if (unit) return _formatBillableUnitValue(unit, (e.billableUnits[unit].total || 0) / calls);
785
+ return 'N/A';
786
+ }
787
+
705
788
  function _usageExists(m) {
706
789
  return !!((m.totalCostUsd || 0)
707
790
  || (m.totalInputTokens || 0)
708
791
  || (m.totalOutputTokens || 0)
709
792
  || (m.totalCacheRead || 0)
710
793
  || (m.tasksCompleted || 0)
711
- || (m.tasksErrored || 0));
794
+ || (m.tasksErrored || 0)
795
+ || _hasReportedBillableUnits(m));
712
796
  }
713
797
 
714
798
  function renderTokenUsage(metrics) {
@@ -737,7 +821,15 @@ function renderTokenUsage(metrics) {
737
821
  const totalOutput = agentOutput + engineOutput;
738
822
  const totalCache = agentCache + engineCache;
739
823
 
740
- if (totalCost === 0 && Object.keys(daily).length === 0) {
824
+ // Show the empty state only when there is genuinely no usage of ANY kind.
825
+ // Keying solely off USD cost + daily (the original guard) would wrongly hide a
826
+ // zero-USD fleet that still reported token usage or a native billable unit such
827
+ // as Copilot premium requests (W-msdcgitc). `_usageExists` already folds tokens,
828
+ // tasks, cost, and reported billable units.
829
+ const _anyAgentUsage = agents.some(([, m]) => _usageExists(m));
830
+ const _anyEngineUsage = engineCalls > 0 || engineCost > 0
831
+ || Object.values(engine).some(e => _hasReportedBillableUnits(e));
832
+ if (!_anyAgentUsage && !_anyEngineUsage && Object.keys(daily).length === 0) {
741
833
  el.innerHTML = '<p class="empty">No usage data yet. Token tracking starts on next agent completion.</p>';
742
834
  return;
743
835
  }
@@ -753,6 +845,37 @@ function renderTokenUsage(metrics) {
753
845
  html += '<div class="token-tile"><div class="token-tile-label">Output Tokens</div><div class="token-tile-value">' + fmtTokens(totalOutput) + '</div></div>';
754
846
  html += '<div class="token-tile"><div class="token-tile-label">Cache Reads</div><div class="token-tile-value">' + fmtTokens(totalCache) + '</div></div>';
755
847
 
848
+ // Non-USD runtime-reported billable units (e.g. Copilot premium requests),
849
+ // aggregated across agents + engine categories (excluding the agent-dispatch
850
+ // mirror so agent premium requests aren't double-counted). Driven by typed
851
+ // usage metadata (W-msdcgitc). Unreported (CLI never exposed the unit) renders
852
+ // 'N/A', distinct from a genuine reported 0.
853
+ var billableAgg = {};
854
+ var _foldBillable = function(src) {
855
+ if (!src || !src.billableUnits) return;
856
+ for (var fu in src.billableUnits) {
857
+ var fb = src.billableUnits[fu]; if (!fb) continue;
858
+ var t = billableAgg[fu] || { total: 0, reported: 0, unavailable: 0 };
859
+ t.total += fb.total || 0; t.reported += fb.reported || 0; t.unavailable += fb.unavailable || 0;
860
+ billableAgg[fu] = t;
861
+ }
862
+ };
863
+ for (const [, bm] of agents) _foldBillable(bm);
864
+ for (const ek in engine) {
865
+ if (ek === 'agent-dispatch' || ek.startsWith('test-') || ek.startsWith('_test')) continue;
866
+ _foldBillable(engine[ek]);
867
+ }
868
+ for (var bu in billableAgg) {
869
+ var bb = billableAgg[bu];
870
+ var bmeta = BILLABLE_UNIT_META[bu] || { tileLabel: bu, label: bu, abbrev: bu };
871
+ var breported = (bb.reported || 0) > 0;
872
+ var bval = breported ? _formatBillableUnitValue(bu, bb.total || 0) : 'N/A';
873
+ var bsub = breported
874
+ ? (bb.unavailable > 0 ? bb.unavailable + ' run(s) without data' : 'authoritative ' + bmeta.label)
875
+ : 'not reported by CLI';
876
+ html += '<div class="token-tile"><div class="token-tile-label">' + escHtml(bmeta.tileLabel) + '</div><div class="token-tile-value">' + escHtml(bval) + '</div><div class="token-tile-sub">' + escHtml(bsub) + '</div></div>';
877
+ }
878
+
756
879
  // Today's cost
757
880
  const today = new Date().toISOString().slice(0, 10);
758
881
  const todayData = daily[today];
@@ -787,7 +910,7 @@ function renderTokenUsage(metrics) {
787
910
  var tempAgents = agents.filter(function(a) { return a[0].startsWith('temp-'); });
788
911
  var tokenRows = permAgents.slice();
789
912
  if (tempAgents.length > 0) {
790
- var tempMerged = { totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0, tasksCompleted: 0, tasksErrored: 0, model: '', costTracking: true };
913
+ var tempMerged = { totalCostUsd: 0, totalInputTokens: 0, totalOutputTokens: 0, totalCacheRead: 0, tasksCompleted: 0, tasksErrored: 0, model: '', costTracking: true, billableUnit: 'usd', billableUnits: {} };
791
914
  for (var ti = 0; ti < tempAgents.length; ti++) {
792
915
  var tm = tempAgents[ti][1];
793
916
  tempMerged.totalCostUsd += tm.totalCostUsd || 0;
@@ -798,26 +921,37 @@ function renderTokenUsage(metrics) {
798
921
  tempMerged.tasksErrored += tm.tasksErrored || 0;
799
922
  if (tm.costTracking === false) tempMerged.costTracking = false;
800
923
  if (!tempMerged.model && tm.model) tempMerged.model = tm.model;
924
+ // Carry the native billable unit forward (temp agents share the fleet
925
+ // default runtime) and merge reported billable buckets so a Copilot temp
926
+ // fleet still surfaces premium requests.
927
+ if (tm.billableUnit && tm.billableUnit !== 'usd') tempMerged.billableUnit = tm.billableUnit;
928
+ if (tm.billableUnits) {
929
+ for (var bku in tm.billableUnits) {
930
+ var sbk = tm.billableUnits[bku]; if (!sbk) continue;
931
+ var dbk = tempMerged.billableUnits[bku] || { total: 0, reported: 0, unavailable: 0 };
932
+ dbk.total += sbk.total || 0; dbk.reported += sbk.reported || 0; dbk.unavailable += sbk.unavailable || 0;
933
+ tempMerged.billableUnits[bku] = dbk;
934
+ }
935
+ }
801
936
  }
802
937
  tokenRows.push(['Temp Agents (' + tempAgents.length + ')', tempMerged]);
803
938
  }
804
939
  var agentsWithUsage = tokenRows.filter(function(a) { return _usageExists(a[1]); });
805
940
  if (agentsWithUsage.length > 0) {
806
941
  html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Agent Usage</div>';
807
- html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Agent</th><th style="width:10%">Model</th><th style="width:14%">Cost</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">$/task</th></tr></thead><tbody>';
942
+ html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Agent</th><th style="width:10%">Model</th><th style="width:14%">Usage</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">Per Task</th></tr></thead><tbody>';
808
943
  for (const [id, m] of agentsWithUsage.sort((a, b) => (b[1].totalCostUsd || 0) - (a[1].totalCostUsd || 0))) {
809
944
  const tasks = (m.tasksCompleted || 0) + (m.tasksErrored || 0);
810
- const costAvailable = m.costTracking !== false;
811
- const perTask = tasks > 0 ? _formatTrackedCost((m.totalCostUsd || 0) / tasks, costAvailable) : '-';
945
+ const perTask = _formatPrimaryPerTask(m, tasks);
812
946
  const modelLabel = (m.model || '').replace(/^claude-/, '').replace(/\[.*\]$/, '') || '-';
813
947
  html += '<tr>' +
814
948
  '<td style="font-weight:600">' + escHtml(id) + '</td>' +
815
949
  '<td style="color:var(--muted);font-size:var(--text-sm)">' + escHtml(modelLabel) + '</td>' +
816
- '<td>' + _formatTrackedCost(m.totalCostUsd, costAvailable) + '</td>' +
950
+ '<td>' + escHtml(_formatPrimaryUsage(m)) + '</td>' +
817
951
  '<td>' + fmtTokens(m.totalInputTokens || 0) + '</td>' +
818
952
  '<td>' + fmtTokens(m.totalOutputTokens || 0) + '</td>' +
819
953
  '<td>' + fmtTokens(m.totalCacheRead || 0) + '</td>' +
820
- '<td style="color:var(--muted)">' + perTask + '</td>' +
954
+ '<td style="color:var(--muted)">' + escHtml(perTask) + '</td>' +
821
955
  '</tr>';
822
956
  }
823
957
  html += '</tbody></table>';
@@ -828,14 +962,14 @@ function renderTokenUsage(metrics) {
828
962
  if (engineEntries.length > 0) {
829
963
  const labels = { 'consolidation': 'Consolidation', 'command-center': 'Command Center', 'doc-chat': 'Doc Chat', 'kb-sweep': 'KB Sweep', 'schedule-parse': 'Schedule Parse' };
830
964
  html += '<div style="font-size:var(--text-sm);color:var(--muted);margin:12px 0 4px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px">Engine Usage</div>';
831
- html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Operation</th><th style="width:10%">Calls</th><th style="width:14%">Cost</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">$/call</th></tr></thead><tbody>';
965
+ html += '<table class="token-agent-table"><thead><tr><th style="width:20%">Operation</th><th style="width:10%">Calls</th><th style="width:14%">Usage</th><th style="width:14%">Input</th><th style="width:14%">Output</th><th style="width:14%">Cache</th><th style="width:14%">Per Call</th></tr></thead><tbody>';
832
966
  for (const [cat, e] of engineEntries.sort((a, b) => (b[1].costUsd || 0) - (a[1].costUsd || 0))) {
833
- const costAvailable = (e.costUsd || 0) > 0;
834
- const perCall = (e.calls || 0) > 0 ? _formatTrackedCost((e.costUsd || 0) / e.calls, costAvailable) : '-';
967
+ const calls = e.calls || 0;
968
+ const perCall = _formatEnginePrimaryPerCall(e, calls);
835
969
  html += '<tr>' +
836
970
  '<td style="font-weight:600">' + escHtml(labels[cat] || cat) + '</td>' +
837
971
  '<td>' + (e.calls || 0) + '</td>' +
838
- '<td>' + _formatTrackedCost(e.costUsd, costAvailable) + '</td>' +
972
+ '<td>' + escHtml(_formatEnginePrimaryUsage(e)) + '</td>' +
839
973
  '<td>' + fmtTokens(e.inputTokens || 0) + '</td>' +
840
974
  '<td>' + fmtTokens(e.outputTokens || 0) + '</td>' +
841
975
  '<td>' + fmtTokens(e.cacheRead || 0) + '</td>' +
@@ -1129,7 +1263,7 @@ function renderHarnessDiagnostics(diag) {
1129
1263
  }
1130
1264
  }
1131
1265
 
1132
- window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderCommands, renderHarnessDiagnostics, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, _formatTrackedCost, openScanProjectsModal };
1266
+ window.MinionsOther = { renderProjects, optimisticallyAddProject, projectChipRemove, renderMcpServers, renderCommands, renderHarnessDiagnostics, renderMetrics, renderLlmPerf, renderTokenUsage, _aggregateEngineUsageForTokenTile, _formatTrackedCost, _formatPrimaryUsage, _formatPrimaryPerTask, _formatBillableUnitValue, openScanProjectsModal };
1133
1267
 
1134
1268
  // ─── keep_processes panel (W-mp68q6ke0010de68) ─────────────────────────────
1135
1269
  // Polls /api/keep-processes every refresh (engine page only) and renders a
@@ -317,8 +317,9 @@ function renderPrdProgress(prog) {
317
317
  const wi = wiById[workItemId] || wiById[i.id];
318
318
  const wiAgent = wi?.dispatched_to ? (agentData.find(a => a.id === wi.dispatched_to) || {}) : null;
319
319
  const wiLabel = _renderPrdWorkItemIdBadge(wi ? wi.id : workItemId, i.id);
320
- const agentLabel = wiAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)" title="' + escHtml(wiAgent.name || wi.dispatched_to) + '">' +
321
- (wiAgent.emoji || '') + ' ' + escHtml(wiAgent.name || wi.dispatched_to) + '</span>' : '';
320
+ const wiAgentIdentity = { id: wi?.dispatched_to, name: wiAgent && wiAgent.name };
321
+ const agentLabel = wiAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)" title="' + escHtml(MinionsAgentIdentity.displayName(wiAgentIdentity)) + '">' +
322
+ (wiAgent.emoji || '') + ' ' + MinionsAgentIdentity.nameWithBadgeHtml(wiAgentIdentity) + '</span>' : '';
322
323
 
323
324
  // Branch label — show the target branch for this item
324
325
  const wiBranch = wi ? (wi.branch || wi.featureBranch || (wi._artifacts && wi._artifacts.branch) || '') : '';
@@ -382,7 +383,7 @@ function renderPrdProgress(prog) {
382
383
  statusBadge(c.status) +
383
384
  '<span style="color:var(--muted);font-size:var(--text-sm)">' + escHtml(c.id) + '</span>' +
384
385
  '<span style="flex:1">' + escHtml((c.title || '').replace('Implement: ', '').slice(0, 60)) + '</span>' +
385
- (childAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + (childAgent.emoji || '') + ' ' + escHtml(childAgent.name || c.dispatched_to) + '</span>' : '') +
386
+ (childAgent ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + (childAgent.emoji || '') + ' ' + MinionsAgentIdentity.nameWithBadgeHtml({ id: c.dispatched_to, name: childAgent.name }) + '</span>' : '') +
386
387
  '</div>';
387
388
  }).join('') +
388
389
  '</div>';
@@ -589,7 +590,7 @@ function renderPrdProgress(prog) {
589
590
  const wiForCard = wi[workItemId] || wi[i.id];
590
591
  const agentId = wiForCard?.dispatched_to || '';
591
592
  const agentInfo = agentId ? (agentData.find(a => a.id === agentId) || {}) : null;
592
- const agentDisplay = agentInfo ? (agentInfo.emoji || '') + ' ' + escHtml(agentInfo.name || agentId) : (agentId ? escHtml(agentId) : '');
593
+ const agentDisplay = agentInfo ? (agentInfo.emoji || '') + ' ' + MinionsAgentIdentity.nameWithBadgeHtml({ id: agentId, name: agentInfo.name }) : (agentId ? MinionsAgentIdentity.nameWithBadgeHtml({ id: agentId }) : '');
593
594
  const deps = (i.depends_on || []).join(', ');
594
595
  const wipAnim = i.status === 'in-progress' ? 'animation:prdWipPulse 2s infinite;' : '';
595
596
  const wiIdBadge = _renderPrdWorkItemIdBadge(wiForCard ? wiForCard.id : workItemId, i.id, '8px', '1px 4px');
@@ -951,7 +952,7 @@ async function prdItemEdit(source, itemId) {
951
952
  '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">' +
952
953
  (wi ? '<a href="#" onclick="event.preventDefault();event.stopPropagation();openWorkItemDetail(\'' + escHtml(wi.id) + '\',\'' + escHtml(wi._source || '') + '\')" title="Open ' + escHtml(wi.id) + ' detail" style="font-size:var(--text-base);font-weight:700;color:' + statusColor + ';text-decoration:none;cursor:pointer" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + statusLabel + ' &rarr;</a>' : '<span style="font-size:var(--text-base);font-weight:700;color:' + statusColor + '">' + statusLabel + '</span>') +
953
954
  typePillHtml +
954
- (agent ? '<span style="font-size:var(--text-base);color:var(--muted)">by ' + escHtml(agent) + '</span>' : '') +
955
+ (agent ? '<span style="font-size:var(--text-base);color:var(--muted)">by ' + MinionsAgentIdentity.nameWithBadgeHtml({ id: agent }) + '</span>' : '') +
955
956
  (completedAt ? '<span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(completedAt.slice(0, 16).replace('T', ' ')) + '</span>' : '') +
956
957
  '</div>' +
957
958
  (prLinks ? '<div style="font-size:var(--text-base);margin-bottom:6px">PR: ' + prLinks + '</div>' : '') +
@@ -150,31 +150,50 @@ function prAutomationToggle(idMatch, automation, label, enabled) {
150
150
  // renders "Unknown" rather than borrowing the agent name. Links to the provider
151
151
  // profile only when a validated http(s) URL is present.
152
152
  function _prAuthorCellHtml(pr) {
153
- var a = pr ? pr.author : null;
154
- var login = '';
155
- var displayName = '';
156
- var url = '';
157
- if (a && typeof a === 'object') {
158
- login = a.login || '';
159
- displayName = a.displayName || '';
160
- url = a.url || '';
161
- } else if (typeof a === 'string') {
162
- login = a;
163
- }
164
- var primary = displayName || login;
153
+ var info = (window.MinionsPrAuthor && window.MinionsPrAuthor.display)
154
+ ? window.MinionsPrAuthor.display(pr ? pr.author : null)
155
+ : { name: '', handle: '', url: '' };
156
+ var primary = info.name;
165
157
  if (!primary) {
166
158
  return '<span style="color:var(--muted);font-size:var(--text-base)" title="Author unavailable">Unknown</span>';
167
159
  }
168
- var secondary = (displayName && login && login !== displayName) ? login : '';
160
+ var secondary = info.handle;
169
161
  var inner = escapeHtml(primary)
170
162
  + (secondary ? ' <span style="color:var(--muted);font-size:var(--text-base)">@' + escapeHtml(secondary) + '</span>' : '');
171
163
  var titleText = secondary ? primary + ' (' + secondary + ')' : primary;
172
- if (url && /^https?:\/\//i.test(url)) {
173
- return '<a class="pr-agent" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + inner + '</a>';
164
+ if (info.url) {
165
+ return '<a class="pr-agent" title="' + escapeHtml(titleText) + '" href="' + escapeHtml(safeUrl(info.url)) + '" target="_blank" rel="noopener">' + inner + '</a>';
174
166
  }
175
167
  return '<span class="pr-agent" title="' + escapeHtml(titleText) + '">' + inner + '</span>';
176
168
  }
177
169
 
170
+ // W-msd8pr2300ev95e1 — Foreign-author guardrail for the AutoFix column. When
171
+ // auto-fix is ENABLED and the PR was NOT authored by the current authenticated
172
+ // identity for that provider, render a prominent warning so the operator sees
173
+ // that Minions may push commits to someone else's PR. Ownership tri-state is
174
+ // stamped engine-side (pr._authorOwnership) from STABLE provider identities and
175
+ // fails CLOSED: 'unknown' shows an "author ownership unknown" warning rather
176
+ // than silently treating the PR as self-authored. Never warns when auto-fix is
177
+ // off (no automatic pushes happen) or when ownership is 'self'.
178
+ function _prAutoFixForeignAuthorWarning(pr, autoFixEnabled) {
179
+ if (!autoFixEnabled) return '';
180
+ var ownership = pr && pr._authorOwnership;
181
+ if (ownership === 'self') return '';
182
+ var isForeign = ownership === 'foreign';
183
+ // Author name is presentation-only (never an email) via the shared formatter.
184
+ var authorName = (window.MinionsPrAuthor && window.MinionsPrAuthor.name)
185
+ ? window.MinionsPrAuthor.name(pr ? pr.author : null) : '';
186
+ var label = isForeign
187
+ ? (authorName
188
+ ? 'Auto-fix enabled for another author\'s PR (author: ' + authorName + ')'
189
+ : 'Auto-fix enabled for another author\'s PR')
190
+ : 'Auto-fix enabled but author ownership unknown';
191
+ var cls = 'pr-autofix-warning' + (isForeign ? ' pr-autofix-warning--foreign' : ' pr-autofix-warning--unknown');
192
+ return '<span class="' + cls + '" role="img" '
193
+ + 'title="' + escapeHtml(label) + '" '
194
+ + 'aria-label="' + escapeHtml(label) + '">\u26A0</span>';
195
+ }
196
+
178
197
  function prRow(pr) {
179
198
  // Minions review (agent) state — separate from ADO human review
180
199
  const sq = pr.minionsReview || {};
@@ -325,6 +344,7 @@ function prRow(pr) {
325
344
  : pr.contextOnly !== true;
326
345
  var autoFixBtn = prAutomationToggle(idMatch, 'fix', 'Auto Fix', autoFixEnabled);
327
346
  var autoReviewBtn = prAutomationToggle(idMatch, 'review', 'Auto Review', autoReviewEnabled);
347
+ var autoFixWarning = _prAutoFixForeignAuthorWarning(pr, autoFixEnabled);
328
348
  // Title attrs live on the inner element (link/span/badge) so hovering the
329
349
  // ellipsis-truncated content reveals the full text. Cell tags stay bare so
330
350
  // the header-to-cell count assertion in test/unit.test.js continues to
@@ -348,7 +368,7 @@ function prRow(pr) {
348
368
  '<td><span class="' + branchClass + '" title="' + escapeHtml(branchError || branchLabel) + '">' + escapeHtml(branchLabel) + '</span>' + pendingReasonHtml + '</td>' +
349
369
  '<td>' + authorCellHtml + '</td>' +
350
370
  '<td>' + reviewerCell + '</td>' +
351
- '<td>' + (autoFixBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
371
+ '<td>' + (autoFixBtn ? autoFixBtn + autoFixWarning : '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
352
372
  '<td>' + (autoReviewBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
353
373
  '<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
354
374
  '<td><button class="btn-destructive" style="padding:1px 5px" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
@@ -622,22 +642,21 @@ function _renderPrDetail(pr) {
622
642
  // so operators can answer both "who is this for?" and "which agent handled it?".
623
643
  let author = '';
624
644
  {
625
- const a = pr.author;
626
- let login = '', displayName = '', url = '';
627
- if (a && typeof a === 'object') { login = a.login || ''; displayName = a.displayName || ''; url = a.url || ''; }
628
- else if (typeof a === 'string') { login = a; }
629
- const primary = displayName || login;
645
+ const info = (window.MinionsPrAuthor && window.MinionsPrAuthor.display)
646
+ ? window.MinionsPrAuthor.display(pr.author)
647
+ : { name: '', handle: '', url: '' };
648
+ const primary = info.name;
630
649
  if (primary) {
631
- const secondary = (displayName && login && login !== displayName) ? ' <span style="color:var(--muted)">@' + escapeHtml(login) + '</span>' : '';
632
- const valHtml = (url && /^https?:\/\//i.test(url))
633
- ? '<a href="' + escapeHtml(safeUrl(url)) + '" target="_blank" rel="noopener">' + escapeHtml(primary) + '</a>' + secondary
650
+ const secondary = info.handle ? ' <span style="color:var(--muted)">@' + escapeHtml(info.handle) + '</span>' : '';
651
+ const valHtml = info.url
652
+ ? '<a href="' + escapeHtml(safeUrl(info.url)) + '" target="_blank" rel="noopener">' + escapeHtml(primary) + '</a>' + secondary
634
653
  : escapeHtml(primary) + secondary;
635
654
  author = '<div><strong style="color:var(--muted);font-size:var(--text-base)">Author:</strong> ' + valHtml + '</div>';
636
655
  } else {
637
656
  author = '<div><strong style="color:var(--muted);font-size:var(--text-base)">Author:</strong> <span style="color:var(--muted)">Unknown</span></div>';
638
657
  }
639
658
  }
640
- const agent = pr.agent ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Minions agent:</strong> ' + escapeHtml(String(pr.agent)) + '</div>' : '';
659
+ const agent = pr.agent ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Minions agent:</strong> ' + MinionsAgentIdentity.nameWithBadgeHtml({ id: String(pr.agent) }) + '</div>' : '';
641
660
  const branch = pr.branch ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Branch:</strong> <code>' + escapeHtml(String(pr.branch)) + '</code></div>' : '';
642
661
  const created = pr.created ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Created:</strong> ' + escapeHtml(String(pr.created)) + '</div>' : '';
643
662
  const project = pr._project ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Project:</strong> ' + escapeHtml(String(pr._project)) + '</div>' : '';