agentxchain 2.104.0 → 2.106.0

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 (62) hide show
  1. package/README.md +12 -6
  2. package/bin/agentxchain.js +5 -5
  3. package/dashboard/app.js +111 -7
  4. package/dashboard/components/blocked.js +95 -11
  5. package/dashboard/components/blockers.js +85 -86
  6. package/dashboard/components/coordinator-timeouts.js +13 -0
  7. package/dashboard/components/cross-repo.js +17 -12
  8. package/dashboard/components/gate.js +31 -11
  9. package/dashboard/components/initiative.js +173 -78
  10. package/dashboard/components/ledger.js +28 -0
  11. package/dashboard/components/live-status.js +39 -0
  12. package/dashboard/components/run-history.js +76 -1
  13. package/dashboard/components/timeline.js +5 -1
  14. package/dashboard/index.html +21 -0
  15. package/dashboard/live-observer.js +91 -0
  16. package/package.json +1 -1
  17. package/scripts/release-bump.sh +26 -3
  18. package/src/commands/accept-turn.js +3 -3
  19. package/src/commands/decisions.js +98 -29
  20. package/src/commands/diff.js +27 -4
  21. package/src/commands/doctor.js +48 -16
  22. package/src/commands/history.js +21 -3
  23. package/src/commands/multi.js +223 -54
  24. package/src/commands/phase.js +11 -13
  25. package/src/commands/reject-turn.js +1 -1
  26. package/src/commands/restart.js +28 -11
  27. package/src/commands/resume.js +6 -6
  28. package/src/commands/role.js +51 -14
  29. package/src/commands/run.js +5 -11
  30. package/src/commands/status.js +145 -13
  31. package/src/commands/step.js +36 -29
  32. package/src/lib/admission-control.js +14 -12
  33. package/src/lib/blocked-state.js +150 -0
  34. package/src/lib/conflict-actions.js +17 -0
  35. package/src/lib/context-section-parser.js +2 -0
  36. package/src/lib/continuity-status.js +1 -1
  37. package/src/lib/coordinator-blocker-presentation.js +127 -0
  38. package/src/lib/coordinator-event-narrative.js +43 -0
  39. package/src/lib/coordinator-gate-approval.js +98 -0
  40. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  41. package/src/lib/coordinator-next-actions.js +128 -0
  42. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  43. package/src/lib/coordinator-presentation-detail.js +11 -0
  44. package/src/lib/coordinator-repo-snapshots.js +53 -0
  45. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  46. package/src/lib/dashboard/actions.js +105 -29
  47. package/src/lib/dashboard/bridge-server.js +7 -0
  48. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  49. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  50. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  51. package/src/lib/dashboard/state-reader.js +36 -1
  52. package/src/lib/dispatch-bundle.js +23 -0
  53. package/src/lib/export-diff.js +70 -38
  54. package/src/lib/export-verifier.js +3 -0
  55. package/src/lib/history-diff-summary.js +249 -0
  56. package/src/lib/manual-qa-fallback.js +18 -0
  57. package/src/lib/normalized-config.js +27 -22
  58. package/src/lib/recent-event-summary.js +132 -0
  59. package/src/lib/repo-decisions.js +69 -28
  60. package/src/lib/report.js +353 -145
  61. package/src/lib/run-diff.js +4 -0
  62. package/src/lib/runtime-capabilities.js +222 -0
@@ -9,6 +9,13 @@
9
9
  * server-side gate evaluators used by `multi step` and `multi approve-gate`.
10
10
  */
11
11
 
12
+ import {
13
+ getCoordinatorAttentionStatusCard,
14
+ getCoordinatorBlockerDetails,
15
+ } from '../../src/lib/coordinator-blocker-presentation.js';
16
+ import { buildCoordinatorGateEvaluationPresentation } from '../../src/lib/coordinator-gate-evaluation-presentation.js';
17
+ import { getCoordinatorPendingGateDetails } from '../../src/lib/coordinator-pending-gate-presentation.js';
18
+
12
19
  function esc(str) {
13
20
  if (!str) return '';
14
21
  return String(str)
@@ -41,6 +48,7 @@ function blockerColor(code) {
41
48
  function renderBlockerRow(blocker) {
42
49
  const code = blocker.code || 'unknown';
43
50
  const color = blockerColor(code);
51
+ const details = getCoordinatorBlockerDetails(blocker);
44
52
  let html = `<div class="turn-card" style="border-left: 3px solid ${color}">
45
53
  <div class="turn-header">
46
54
  ${badge(code, color)}
@@ -50,62 +58,55 @@ function renderBlockerRow(blocker) {
50
58
  html += `<div class="turn-summary">${esc(blocker.message)}</div>`;
51
59
  }
52
60
 
53
- if (code === 'repo_run_id_mismatch' && blocker.repo_id) {
54
- html += `<dl class="detail-list">
55
- <dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
56
- if (blocker.expected_run_id) {
57
- html += `<dt>Expected</dt><dd class="mono">${esc(blocker.expected_run_id)}</dd>`;
58
- }
59
- if (blocker.actual_run_id) {
60
- html += `<dt>Actual</dt><dd class="mono">${esc(blocker.actual_run_id)}</dd>`;
61
+ if (details.length > 0) {
62
+ html += `<dl class="detail-list">`;
63
+ for (const detail of details) {
64
+ html += `<dt>${esc(detail.label)}</dt><dd${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</dd>`;
61
65
  }
62
66
  html += `</dl>`;
63
67
  }
64
68
 
65
- if (code === 'repo_not_ready' && blocker.repo_id) {
66
- html += `<dl class="detail-list">
67
- <dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
68
- if (blocker.current_phase) {
69
- html += `<dt>Current Phase</dt><dd>${esc(blocker.current_phase)}</dd>`;
70
- }
71
- if (blocker.required_phase) {
72
- html += `<dt>Required Phase</dt><dd>${esc(blocker.required_phase)}</dd>`;
73
- }
74
- html += `</dl>`;
69
+ html += `</div>`;
70
+ return html;
71
+ }
72
+
73
+ function renderDetailRows(details) {
74
+ if (!Array.isArray(details) || details.length === 0) {
75
+ return '';
75
76
  }
76
77
 
77
- html += `</div>`;
78
+ let html = '';
79
+ for (const detail of details) {
80
+ html += `<dt>${esc(detail.label)}</dt><dd${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</dd>`;
81
+ }
78
82
  return html;
79
83
  }
80
84
 
81
- function renderActiveGate(active) {
85
+ function renderActiveGate(active, coordinatorBlockers = null) {
82
86
  if (!active) return '';
83
87
 
88
+ const pendingGateDetails = active.pending === true
89
+ ? getCoordinatorPendingGateDetails({
90
+ pendingGate: coordinatorBlockers?.pending_gate,
91
+ active,
92
+ })
93
+ : [];
94
+ const evaluationPresentation = active.pending === true
95
+ ? null
96
+ : buildCoordinatorGateEvaluationPresentation({
97
+ gateType: active.gate_type,
98
+ evaluation: active,
99
+ includeReady: true,
100
+ includeBlockerCount: false,
101
+ });
84
102
  let html = `<div class="gate-card">
85
103
  <h3>Active Gate</h3>
86
- <dl class="detail-list">
87
- <dt>Type</dt><dd>${esc(active.gate_type)}</dd>`;
88
-
89
- if (active.gate_id) {
90
- html += `<dt>Gate</dt><dd class="mono">${esc(active.gate_id)}</dd>`;
91
- }
92
- if (active.current_phase) {
93
- html += `<dt>Current Phase</dt><dd>${esc(active.current_phase)}</dd>`;
94
- }
95
- if (active.target_phase) {
96
- html += `<dt>Target Phase</dt><dd>${esc(active.target_phase)}</dd>`;
97
- }
98
- if (typeof active.ready === 'boolean') {
99
- html += `<dt>Ready</dt><dd>${active.ready ? 'Yes' : 'No'}</dd>`;
100
- }
101
- if (active.pending === true) {
102
- html += `<dt>Pending Approval</dt><dd>Yes</dd>`;
103
- }
104
- if (Array.isArray(active.required_repos) && active.required_repos.length > 0) {
105
- html += `<dt>Required Repos</dt><dd>${esc(active.required_repos.join(', '))}</dd>`;
106
- }
107
- if (Array.isArray(active.human_barriers) && active.human_barriers.length > 0) {
108
- html += `<dt>Human Barriers</dt><dd>${esc(active.human_barriers.join(', '))}</dd>`;
104
+ <dl class="detail-list">`;
105
+ if (pendingGateDetails.length > 0) {
106
+ html += renderDetailRows(pendingGateDetails);
107
+ } else {
108
+ html += `<dt>Type</dt><dd>${esc(active.gate_type)}</dd>`;
109
+ html += renderDetailRows(evaluationPresentation?.details || []);
109
110
  }
110
111
 
111
112
  html += `</dl>`;
@@ -124,24 +125,27 @@ function renderActiveGate(active) {
124
125
  }
125
126
 
126
127
  function renderRecoveryCommand(data) {
127
- if (data.mode === 'pending_gate') {
128
- return `<div class="section"><h3>Recovery</h3>
129
- <p class="recovery-hint">Approve the pending gate:</p>
130
- <pre class="recovery-command mono" data-copy="agentxchain multi approve-gate">agentxchain multi approve-gate</pre>
131
- </div>`;
128
+ const nextActions = Array.isArray(data.next_actions) ? data.next_actions : [];
129
+ if (nextActions.length === 0) {
130
+ return '';
132
131
  }
133
132
 
134
- const hasRunIdMismatch = Array.isArray(data.active?.blockers)
135
- && data.active.blockers.some(b => b.code === 'repo_run_id_mismatch');
136
-
137
- if (hasRunIdMismatch) {
138
- return `<div class="section"><h3>Recovery</h3>
139
- <p class="recovery-hint">Run identity drift detected. Investigate child repos before resuming:</p>
140
- <pre class="recovery-command mono" data-copy="agentxchain multi resume">agentxchain multi resume</pre>
141
- </div>`;
133
+ let html = `<div class="section"><h3>Next Actions</h3><ol class="action-list">`;
134
+ for (const action of nextActions) {
135
+ html += `<li class="turn-card">
136
+ <div class="turn-header">
137
+ <span class="mono">${esc(action.command || 'n/a')}</span>
138
+ </div>`;
139
+ if (action.reason) {
140
+ html += `<div class="turn-summary">${esc(action.reason)}</div>`;
141
+ }
142
+ if (action.command) {
143
+ html += `<pre class="recovery-command mono" data-copy="${esc(action.command)}">${esc(action.command)}</pre>`;
144
+ }
145
+ html += `</li>`;
142
146
  }
143
-
144
- return '';
147
+ html += `</ol></div>`;
148
+ return html;
145
149
  }
146
150
 
147
151
  export function render({ coordinatorBlockers }) {
@@ -156,6 +160,7 @@ export function render({ coordinatorBlockers }) {
156
160
  const data = coordinatorBlockers;
157
161
  const blockers = data.active?.blockers || [];
158
162
  const hasBlockers = blockers.length > 0 && !blockers.every(b => b.code === 'no_next_phase');
163
+ const statusCard = getCoordinatorAttentionStatusCard(data);
159
164
 
160
165
  let html = `<div class="blockers-view">`;
161
166
 
@@ -188,16 +193,13 @@ export function render({ coordinatorBlockers }) {
188
193
  }
189
194
 
190
195
  // Status summary
191
- if (!hasBlockers && data.mode === 'pending_gate') {
192
- html += `<div class="gate-card"><h3>Awaiting Approval</h3>
193
- <p class="turn-summary">All prerequisites are satisfied. The coordinator is waiting for human gate approval.</p></div>`;
194
- } else if (!hasBlockers) {
195
- html += `<div class="gate-card"><h3>No Blockers</h3>
196
- <p class="turn-summary">The coordinator gate has no outstanding blockers.</p></div>`;
196
+ if (!hasBlockers && statusCard) {
197
+ html += `<div class="gate-card"><h3>${esc(statusCard.title)}</h3>
198
+ <p class="turn-summary">${esc(statusCard.message)}</p></div>`;
197
199
  }
198
200
 
199
201
  // Active gate detail
200
- html += renderActiveGate(data.active);
202
+ html += renderActiveGate(data.active, data);
201
203
 
202
204
  // Recovery
203
205
  html += renderRecoveryCommand(data);
@@ -208,21 +210,20 @@ export function render({ coordinatorBlockers }) {
208
210
  const { phase_transition, run_completion } = data.evaluations;
209
211
 
210
212
  if (phase_transition) {
213
+ const phasePresentation = buildCoordinatorGateEvaluationPresentation({
214
+ gateType: 'phase_transition',
215
+ evaluation: phase_transition,
216
+ });
211
217
  html += `<div class="turn-card" data-turn-expand>
212
218
  <div class="turn-header">
213
- <span>Phase Transition</span>
214
- ${badge(phase_transition.ready ? 'ready' : 'not ready', phase_transition.ready ? 'var(--green)' : 'var(--yellow)')}
219
+ <span>${esc(phasePresentation.title)}</span>
220
+ ${badge(phasePresentation.statusLabel, phase_transition.ready ? 'var(--green)' : 'var(--yellow)')}
215
221
  </div>
216
222
  <div class="turn-detail-panel">
217
- <dl class="detail-list">`;
218
- if (phase_transition.current_phase) html += `<dt>Current</dt><dd>${esc(phase_transition.current_phase)}</dd>`;
219
- if (phase_transition.target_phase) html += `<dt>Target</dt><dd>${esc(phase_transition.target_phase)}</dd>`;
220
- if (phase_transition.gate_id) html += `<dt>Gate</dt><dd class="mono">${esc(phase_transition.gate_id)}</dd>`;
221
- html += `<dt>Blockers</dt><dd>${phase_transition.blockers?.length || 0}</dd>`;
222
- html += `</dl>`;
223
- if (phase_transition.blockers?.length > 0) {
223
+ <dl class="detail-list">${renderDetailRows(phasePresentation.details)}</dl>`;
224
+ if (phasePresentation.blockers.length > 0) {
224
225
  html += `<div class="annotation-list" style="margin-top:8px">`;
225
- for (const b of phase_transition.blockers) {
226
+ for (const b of phasePresentation.blockers) {
226
227
  html += `<div class="annotation-card">
227
228
  <span class="mono">${esc(b.code || 'unknown')}</span>
228
229
  <span>${esc(b.message || '')}</span>
@@ -234,22 +235,20 @@ export function render({ coordinatorBlockers }) {
234
235
  }
235
236
 
236
237
  if (run_completion) {
238
+ const completionPresentation = buildCoordinatorGateEvaluationPresentation({
239
+ gateType: 'run_completion',
240
+ evaluation: run_completion,
241
+ });
237
242
  html += `<div class="turn-card" data-turn-expand>
238
243
  <div class="turn-header">
239
- <span>Run Completion</span>
240
- ${badge(run_completion.ready ? 'ready' : 'not ready', run_completion.ready ? 'var(--green)' : 'var(--yellow)')}
244
+ <span>${esc(completionPresentation.title)}</span>
245
+ ${badge(completionPresentation.statusLabel, run_completion.ready ? 'var(--green)' : 'var(--yellow)')}
241
246
  </div>
242
247
  <div class="turn-detail-panel">
243
- <dl class="detail-list">`;
244
- if (run_completion.gate_id) html += `<dt>Gate</dt><dd class="mono">${esc(run_completion.gate_id)}</dd>`;
245
- html += `<dt>Blockers</dt><dd>${run_completion.blockers?.length || 0}</dd>`;
246
- if (typeof run_completion.requires_human_approval === 'boolean') {
247
- html += `<dt>Human Approval</dt><dd>${run_completion.requires_human_approval ? 'Required' : 'Not required'}</dd>`;
248
- }
249
- html += `</dl>`;
250
- if (run_completion.blockers?.length > 0) {
248
+ <dl class="detail-list">${renderDetailRows(completionPresentation.details)}</dl>`;
249
+ if (completionPresentation.blockers.length > 0) {
251
250
  html += `<div class="annotation-list" style="margin-top:8px">`;
252
- for (const b of run_completion.blockers) {
251
+ for (const b of completionPresentation.blockers) {
253
252
  html += `<div class="annotation-card">
254
253
  <span class="mono">${esc(b.code || 'unknown')}</span>
255
254
  <span>${esc(b.message || '')}</span>
@@ -152,6 +152,18 @@ function renderEventTable(events, title) {
152
152
  return html;
153
153
  }
154
154
 
155
+ function renderDetailRows(details) {
156
+ if (!Array.isArray(details) || details.length === 0) {
157
+ return '';
158
+ }
159
+
160
+ let html = '';
161
+ for (const detail of details) {
162
+ html += `<dt>${esc(detail.label)}</dt><dd${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</dd>`;
163
+ }
164
+ return html;
165
+ }
166
+
155
167
  function renderRepoCard(repo) {
156
168
  let html = `<div class="turn-card">
157
169
  <div class="turn-header">
@@ -168,6 +180,7 @@ function renderRepoCard(repo) {
168
180
  if (repo.phase) {
169
181
  html += `<dt>Phase</dt><dd>${esc(repo.phase)}</dd>`;
170
182
  }
183
+ html += renderDetailRows(repo.details);
171
184
  html += `</dl>`;
172
185
 
173
186
  if (repo.error) {
@@ -1,3 +1,6 @@
1
+ import { renderLiveStatus } from './live-status.js';
2
+ import { summarizeCoordinatorEvent } from '../../src/lib/coordinator-event-narrative.js';
3
+
1
4
  function esc(str) {
2
5
  if (!str) return '';
3
6
  return String(str)
@@ -26,57 +29,57 @@ function describeEvent(entry) {
26
29
  case 'run_initialized':
27
30
  return {
28
31
  title: 'Coordinator Initialized',
29
- detail: `${Object.keys(entry.repo_runs || {}).length} repo runs linked or initialized`,
32
+ detail: summarizeCoordinatorEvent(entry),
30
33
  };
31
34
  case 'turn_dispatched':
32
35
  return {
33
36
  title: 'Turn Dispatched',
34
- detail: `${entry.role || 'agent'} dispatched to ${entry.repo_id} for ${entry.workstream_id}`,
37
+ detail: summarizeCoordinatorEvent(entry),
35
38
  };
36
39
  case 'acceptance_projection':
37
40
  return {
38
41
  title: 'Acceptance Projected',
39
- detail: entry.summary || `${entry.repo_id} accepted ${entry.repo_turn_id || 'a turn'}`,
42
+ detail: summarizeCoordinatorEvent(entry),
40
43
  };
41
44
  case 'context_generated':
42
45
  return {
43
46
  title: 'Context Generated',
44
- detail: `${entry.target_repo_id} received cross-repo context from ${(entry.upstream_repo_ids || []).join(', ') || 'no upstream repos'}`,
47
+ detail: summarizeCoordinatorEvent(entry),
45
48
  };
46
49
  case 'phase_transition_requested':
47
50
  return {
48
51
  title: 'Phase Gate Requested',
49
- detail: `${entry.from || 'unknown'} -> ${entry.to || 'unknown'} (${entry.gate || 'gate'})`,
52
+ detail: summarizeCoordinatorEvent(entry),
50
53
  };
51
54
  case 'phase_transition_approved':
52
55
  return {
53
56
  title: 'Phase Gate Approved',
54
- detail: `${entry.from || 'unknown'} -> ${entry.to || 'unknown'}`,
57
+ detail: summarizeCoordinatorEvent(entry),
55
58
  };
56
59
  case 'run_completion_requested':
57
60
  return {
58
61
  title: 'Completion Gate Requested',
59
- detail: entry.gate || 'initiative_ship',
62
+ detail: summarizeCoordinatorEvent(entry),
60
63
  };
61
64
  case 'run_completed':
62
65
  return {
63
66
  title: 'Initiative Completed',
64
- detail: entry.gate || 'completion approved',
67
+ detail: summarizeCoordinatorEvent(entry),
65
68
  };
66
69
  case 'state_resynced':
67
70
  return {
68
71
  title: 'Coordinator Resynced',
69
- detail: `${(entry.resynced_repos || []).length} repos updated`,
72
+ detail: summarizeCoordinatorEvent(entry),
70
73
  };
71
74
  default:
72
75
  return {
73
76
  title: entry?.type || 'Unknown Event',
74
- detail: entry?.repo_id || entry?.workstream_id || 'Coordinator history event',
77
+ detail: summarizeCoordinatorEvent(entry),
75
78
  };
76
79
  }
77
80
  }
78
81
 
79
- export function render({ coordinatorState, coordinatorHistory = [] }) {
82
+ export function render({ coordinatorState, coordinatorHistory = [], liveMeta = null }) {
80
83
  if (!coordinatorState) {
81
84
  return `<div class="placeholder"><h2>No Cross-Repo Timeline</h2><p>No coordinator run found. Start one with <code class="mono">agentxchain multi init</code></p></div>`;
82
85
  }
@@ -86,7 +89,9 @@ export function render({ coordinatorState, coordinatorHistory = [] }) {
86
89
  return `<div class="placeholder"><h2>Cross-Repo Timeline</h2><p>No coordinator history recorded yet.</p></div>`;
87
90
  }
88
91
 
89
- let html = `<div class="timeline-view"><div class="section"><h3>Cross-Repo Timeline</h3><div class="turn-list">`;
92
+ let html = `<div class="timeline-view">`;
93
+ html += renderLiveStatus(liveMeta);
94
+ html += `<div class="section"><h3>Cross-Repo Timeline</h3><div class="turn-list">`;
90
95
  for (const entry of events) {
91
96
  const event = describeEvent(entry);
92
97
  const repoId = entry.repo_id || entry.target_repo_id || null;
@@ -5,6 +5,8 @@
5
5
  * Shows a narrow local approve action plus the exact CLI fallback command.
6
6
  */
7
7
 
8
+ import { getCoordinatorPendingGateDetails } from '../../src/lib/coordinator-pending-gate-presentation.js';
9
+
8
10
  function esc(str) {
9
11
  if (!str) return '';
10
12
  return String(str)
@@ -126,6 +128,18 @@ function renderApproveControls({ buttonLabel, cliCommand }) {
126
128
  </div>`;
127
129
  }
128
130
 
131
+ function renderDetailRows(details) {
132
+ if (!Array.isArray(details) || details.length === 0) {
133
+ return '';
134
+ }
135
+
136
+ let html = '';
137
+ for (const detail of details) {
138
+ html += `<dt>${esc(detail.label)}</dt><dd${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</dd>`;
139
+ }
140
+ return html;
141
+ }
142
+
129
143
  export { findPostGateTurns, aggregateEvidence };
130
144
 
131
145
  function findCoordinatorGateRequest(history, pendingGate) {
@@ -227,13 +241,20 @@ export function render({
227
241
  const evidence = isCoordinator
228
242
  ? aggregateCoordinatorEvidence(postGateTurns)
229
243
  : aggregateEvidence(postGateTurns);
244
+ const coordinatorDetails = isCoordinator
245
+ ? getCoordinatorPendingGateDetails({ pendingGate: pendingTransition, includeHumanBarriers: false })
246
+ : [];
230
247
  html += `<div class="gate-card">
231
248
  <h3>Phase Transition Gate</h3>
232
- <dl class="detail-list">
233
- <dt>From</dt><dd>${esc(pendingTransition.from || state?.phase || coordinatorState?.phase)}</dd>
249
+ <dl class="detail-list">`;
250
+ if (isCoordinator) {
251
+ html += renderDetailRows(coordinatorDetails);
252
+ } else {
253
+ html += `<dt>From</dt><dd>${esc(pendingTransition.from || state?.phase || coordinatorState?.phase)}</dd>
234
254
  <dt>To</dt><dd>${esc(pendingTransition.to)}</dd>`;
235
- if (pendingTransition.gate) {
236
- html += `<dt>Gate</dt><dd class="mono">${esc(pendingTransition.gate)}</dd>`;
255
+ if (pendingTransition.gate) {
256
+ html += `<dt>Gate</dt><dd class="mono">${esc(pendingTransition.gate)}</dd>`;
257
+ }
237
258
  }
238
259
  if (pendingTransition.requested_by_turn) {
239
260
  html += `<dt>Requested By</dt><dd class="mono">${esc(pendingTransition.requested_by_turn)}</dd>`;
@@ -241,9 +262,6 @@ export function render({
241
262
  if (postGateTurns.length > 0) {
242
263
  html += `<dt>Evidence Turns</dt><dd>${postGateTurns.length} turn${postGateTurns.length !== 1 ? 's' : ''}</dd>`;
243
264
  }
244
- if (isCoordinator && Array.isArray(pendingTransition.required_repos) && pendingTransition.required_repos.length > 0) {
245
- html += `<dt>Required Repos</dt><dd>${esc(pendingTransition.required_repos.join(', '))}</dd>`;
246
- }
247
265
  html += `</dl>`;
248
266
  if (evidence.summaries.length > 0) {
249
267
  html += `<div class="gate-evidence"><h4>Agent Summaries</h4><ul>`;
@@ -280,10 +298,15 @@ export function render({
280
298
  const evidence = isCoordinator
281
299
  ? aggregateCoordinatorEvidence(postGateTurns)
282
300
  : aggregateEvidence(postGateTurns);
301
+ const coordinatorDetails = isCoordinator
302
+ ? getCoordinatorPendingGateDetails({ pendingGate: pendingCompletion, includeHumanBarriers: false })
303
+ : [];
283
304
  html += `<div class="gate-card">
284
305
  <h3>Run Completion Gate</h3>
285
306
  <dl class="detail-list">`;
286
- if (pendingCompletion.gate) {
307
+ if (isCoordinator) {
308
+ html += renderDetailRows(coordinatorDetails);
309
+ } else if (pendingCompletion.gate) {
287
310
  html += `<dt>Gate</dt><dd class="mono">${esc(pendingCompletion.gate)}</dd>`;
288
311
  }
289
312
  if (pendingCompletion.requested_by_turn) {
@@ -292,9 +315,6 @@ export function render({
292
315
  if (postGateTurns.length > 0) {
293
316
  html += `<dt>Evidence Turns</dt><dd>${postGateTurns.length} turn${postGateTurns.length !== 1 ? 's' : ''}</dd>`;
294
317
  }
295
- if (isCoordinator && Array.isArray(pendingCompletion.required_repos) && pendingCompletion.required_repos.length > 0) {
296
- html += `<dt>Required Repos</dt><dd>${esc(pendingCompletion.required_repos.join(', '))}</dd>`;
297
- }
298
318
  html += `</dl>`;
299
319
  if (evidence.summaries.length > 0) {
300
320
  html += `<div class="gate-evidence"><h4>Agent Summaries</h4><ul>`;