agentxchain 2.103.0 → 2.105.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 (66) hide show
  1. package/README.md +13 -7
  2. package/bin/agentxchain.js +16 -8
  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/scripts/release-preflight.sh +82 -38
  19. package/src/commands/accept-turn.js +3 -3
  20. package/src/commands/decisions.js +98 -29
  21. package/src/commands/diff.js +27 -4
  22. package/src/commands/doctor.js +48 -16
  23. package/src/commands/generate.js +126 -1
  24. package/src/commands/history.js +21 -3
  25. package/src/commands/init.js +15 -97
  26. package/src/commands/multi.js +223 -54
  27. package/src/commands/phase.js +11 -13
  28. package/src/commands/reject-turn.js +1 -1
  29. package/src/commands/restart.js +28 -11
  30. package/src/commands/resume.js +6 -6
  31. package/src/commands/role.js +51 -14
  32. package/src/commands/run.js +5 -11
  33. package/src/commands/status.js +145 -13
  34. package/src/commands/step.js +36 -29
  35. package/src/lib/admission-control.js +14 -12
  36. package/src/lib/blocked-state.js +150 -0
  37. package/src/lib/conflict-actions.js +17 -0
  38. package/src/lib/context-section-parser.js +2 -0
  39. package/src/lib/continuity-status.js +1 -1
  40. package/src/lib/coordinator-blocker-presentation.js +127 -0
  41. package/src/lib/coordinator-event-narrative.js +43 -0
  42. package/src/lib/coordinator-gate-approval.js +98 -0
  43. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  44. package/src/lib/coordinator-next-actions.js +128 -0
  45. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  46. package/src/lib/coordinator-presentation-detail.js +11 -0
  47. package/src/lib/coordinator-repo-snapshots.js +53 -0
  48. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  49. package/src/lib/dashboard/actions.js +105 -29
  50. package/src/lib/dashboard/bridge-server.js +7 -0
  51. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  52. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  53. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  54. package/src/lib/dashboard/state-reader.js +36 -1
  55. package/src/lib/dispatch-bundle.js +23 -0
  56. package/src/lib/export-diff.js +70 -38
  57. package/src/lib/export-verifier.js +3 -0
  58. package/src/lib/history-diff-summary.js +249 -0
  59. package/src/lib/manual-qa-fallback.js +18 -0
  60. package/src/lib/normalized-config.js +27 -22
  61. package/src/lib/planning-artifacts.js +131 -0
  62. package/src/lib/recent-event-summary.js +132 -0
  63. package/src/lib/repo-decisions.js +69 -28
  64. package/src/lib/report.js +353 -145
  65. package/src/lib/run-diff.js +4 -0
  66. package/src/lib/runtime-capabilities.js +222 -0
@@ -1,3 +1,9 @@
1
+ import {
2
+ buildCoordinatorAttentionSnapshotPresentation,
3
+ summarizeCoordinatorAttention,
4
+ } from '../../src/lib/coordinator-blocker-presentation.js';
5
+ import { buildCoordinatorRepoStatusRows } from '../../src/lib/coordinator-repo-status-presentation.js';
6
+
1
7
  function esc(str) {
2
8
  if (!str) return '';
3
9
  return String(str)
@@ -37,73 +43,126 @@ function summarizeBarriers(barriers) {
37
43
  return counts;
38
44
  }
39
45
 
40
- function renderCoordinatorAttentionSnapshot(coordinatorBlockers) {
41
- if (!coordinatorBlockers || coordinatorBlockers.ok === false) {
42
- return '';
46
+ function summarizeDecisionConstraints(barriers) {
47
+ const entries = Object.entries(barriers || {});
48
+ const pendingRequirementSets = [];
49
+ let barrierCount = 0;
50
+ let repoRequirementCount = 0;
51
+ let requiredDecisionCount = 0;
52
+ let satisfiedRequirementCount = 0;
53
+
54
+ for (const [barrierId, barrier] of entries) {
55
+ const decisionIdsByRepo = barrier?.required_decision_ids_by_repo || barrier?.alignment_decision_ids || null;
56
+ if (!decisionIdsByRepo || typeof decisionIdsByRepo !== 'object' || Array.isArray(decisionIdsByRepo)) {
57
+ continue;
58
+ }
59
+
60
+ const repoEntries = Object.entries(decisionIdsByRepo)
61
+ .filter(([, ids]) => Array.isArray(ids) && ids.length > 0);
62
+ if (repoEntries.length === 0) {
63
+ continue;
64
+ }
65
+
66
+ barrierCount += 1;
67
+ const satisfiedRepos = new Set(Array.isArray(barrier?.satisfied_repos) ? barrier.satisfied_repos : []);
68
+ for (const [repoId, ids] of repoEntries) {
69
+ repoRequirementCount += 1;
70
+ requiredDecisionCount += ids.length;
71
+ if (satisfiedRepos.has(repoId)) {
72
+ satisfiedRequirementCount += 1;
73
+ } else {
74
+ pendingRequirementSets.push({ barrierId, repoId, decisionIds: ids });
75
+ }
76
+ }
43
77
  }
44
78
 
45
- const active = coordinatorBlockers.active || {};
46
- const blockers = Array.isArray(active.blockers)
47
- ? active.blockers.filter((blocker) => blocker?.code !== 'no_next_phase')
48
- : [];
49
- const hasBlockers = blockers.length > 0;
50
- const title = coordinatorBlockers.mode === 'pending_gate' ? 'Approval Snapshot' : 'Blocker Snapshot';
79
+ if (barrierCount === 0) {
80
+ return null;
81
+ }
51
82
 
52
- let html = `<div class="gate-card">
53
- <h3>${title}</h3>
54
- <dl class="detail-list">`;
55
- if (coordinatorBlockers.mode) {
56
- html += `<dt>Mode</dt><dd>${esc(coordinatorBlockers.mode)}</dd>`;
83
+ return {
84
+ barrier_count: barrierCount,
85
+ repo_requirement_count: repoRequirementCount,
86
+ required_decision_count: requiredDecisionCount,
87
+ satisfied_requirement_count: satisfiedRequirementCount,
88
+ pending_requirement_count: pendingRequirementSets.length,
89
+ first_pending_requirement: pendingRequirementSets[0] || null,
90
+ additional_pending_requirement_count: Math.max(0, pendingRequirementSets.length - 1),
91
+ };
92
+ }
93
+
94
+ function renderPrimaryAction(action) {
95
+ if (!action || typeof action !== 'object') {
96
+ return '';
57
97
  }
58
- if (active.gate_type) {
59
- html += `<dt>Type</dt><dd>${esc(active.gate_type)}</dd>`;
98
+
99
+ let html = `<div class="turn-card">
100
+ <div class="turn-header"><span>Primary Action</span></div>`;
101
+ if (action.reason) {
102
+ html += `<div class="turn-summary">${esc(action.reason)}</div>`;
60
103
  }
61
- if (active.gate_id) {
62
- html += `<dt>Gate</dt><dd class="mono">${esc(active.gate_id)}</dd>`;
104
+ if (action.command) {
105
+ html += `<pre class="recovery-command mono" data-copy="${esc(action.command)}">${esc(action.command)}</pre>`;
63
106
  }
64
- if (active.current_phase) {
65
- html += `<dt>Current</dt><dd>${esc(active.current_phase)}</dd>`;
107
+ html += `</div>`;
108
+ return html;
109
+ }
110
+
111
+ function renderDetailRows(details) {
112
+ if (!Array.isArray(details) || details.length === 0) {
113
+ return '';
66
114
  }
67
- if (active.target_phase) {
68
- html += `<dt>Target</dt><dd>${esc(active.target_phase)}</dd>`;
115
+
116
+ let html = '';
117
+ for (const detail of details) {
118
+ html += `<dt>${esc(detail.label)}</dt><dd${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</dd>`;
69
119
  }
70
- if (hasBlockers) {
71
- html += `<dt>Blockers</dt><dd>${blockers.length}</dd>`;
120
+ return html;
121
+ }
122
+
123
+ function renderCoordinatorAttentionSnapshot(coordinatorBlockers) {
124
+ const presentation = buildCoordinatorAttentionSnapshotPresentation(coordinatorBlockers);
125
+ if (!presentation) {
126
+ return '';
72
127
  }
73
- html += `</dl>`;
74
128
 
75
- if (coordinatorBlockers.mode === 'pending_gate') {
76
- html += `<p class="turn-summary">All coordinator prerequisites are satisfied. Human approval is the remaining action.</p>`;
77
- } else if (hasBlockers) {
78
- html += `<div class="turn-list">`;
79
- for (const blocker of blockers) {
80
- html += `<div class="turn-card">
81
- <div class="turn-header"><span class="mono">${esc(blocker.code || 'unknown')}</span></div>`;
82
- if (blocker.message) {
83
- html += `<div class="turn-summary">${esc(blocker.message)}</div>`;
84
- }
85
- if (blocker.repo_id || blocker.expected_run_id || blocker.actual_run_id) {
86
- html += `<dl class="detail-list">`;
87
- if (blocker.repo_id) html += `<dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
88
- if (blocker.expected_run_id) html += `<dt>Expected</dt><dd class="mono">${esc(blocker.expected_run_id)}</dd>`;
89
- if (blocker.actual_run_id) html += `<dt>Actual</dt><dd class="mono">${esc(blocker.actual_run_id)}</dd>`;
90
- if (blocker.current_phase) html += `<dt>Current Phase</dt><dd>${esc(blocker.current_phase)}</dd>`;
91
- if (blocker.required_phase) html += `<dt>Required Phase</dt><dd>${esc(blocker.required_phase)}</dd>`;
92
- html += `</dl>`;
93
- }
94
- html += `</div>`;
129
+ const summary = summarizeCoordinatorAttention(coordinatorBlockers);
130
+ const { primaryBlocker, primaryAction } = presentation;
131
+ const { blockers } = summary;
132
+ const hasBlockers = blockers.length > 0;
133
+
134
+ let html = `<div class="gate-card">
135
+ <h3>${presentation.title}</h3>
136
+ <p class="section-subtitle">${presentation.subtitle}</p>
137
+ <dl class="detail-list">${renderDetailRows(presentation.details)}</dl>`;
138
+
139
+ if (presentation.summaryMessage) {
140
+ html += `<p class="turn-summary">${esc(presentation.summaryMessage)}</p>`;
141
+ } else if (primaryBlocker) {
142
+ html += `<div class="turn-card">
143
+ <div class="turn-header"><span class="mono">${esc(primaryBlocker.code || 'unknown')}</span></div>`;
144
+ if (primaryBlocker.message) {
145
+ html += `<div class="turn-summary">${esc(primaryBlocker.message)}</div>`;
146
+ }
147
+ if (presentation.primaryBlockerDetails.length > 0) {
148
+ html += `<dl class="detail-list">${renderDetailRows(presentation.primaryBlockerDetails)}</dl>`;
149
+ }
150
+ html += `</div>`;
151
+ if (presentation.additionalBlockerCount > 0) {
152
+ html += `<p class="turn-detail">${presentation.additionalBlockerCount} additional blocker${presentation.additionalBlockerCount !== 1 ? 's are' : ' is'} summarized in <a href="#blockers">Blockers</a>.</p>`;
153
+ }
154
+ }
155
+
156
+ if (primaryAction) {
157
+ html += `<div class="section" style="margin-top:12px">${renderPrimaryAction(primaryAction)}`;
158
+ if (presentation.additionalActionCount > 0) {
159
+ html += `<p class="turn-detail">${presentation.additionalActionCount} additional action${presentation.additionalActionCount !== 1 ? 's remain' : ' remains'} in <a href="#blockers">Blockers</a>.</p>`;
95
160
  }
96
161
  html += `</div>`;
97
- } else if (coordinatorBlockers.blocked_reason) {
98
- html += `<p class="turn-summary">${esc(
99
- typeof coordinatorBlockers.blocked_reason === 'string'
100
- ? coordinatorBlockers.blocked_reason
101
- : JSON.stringify(coordinatorBlockers.blocked_reason)
102
- )}</p>`;
103
162
  }
104
163
 
105
164
  html += `<div class="gate-action">
106
- <p>Inspect full diagnostics:</p>
165
+ <p>Inspect full blocker diagnostics and ordered recovery steps:</p>
107
166
  <p><a href="#blockers">Open Blockers view</a></p>
108
167
  </div>
109
168
  </div>`;
@@ -111,20 +170,43 @@ function renderCoordinatorAttentionSnapshot(coordinatorBlockers) {
111
170
  return html;
112
171
  }
113
172
 
173
+ function getCoordinatorRepoRows(coordinatorState, coordinatorRepoStatusRows) {
174
+ if (Array.isArray(coordinatorRepoStatusRows) && coordinatorRepoStatusRows.length > 0) {
175
+ return coordinatorRepoStatusRows;
176
+ }
177
+
178
+ return buildCoordinatorRepoStatusRows({
179
+ config: null,
180
+ coordinatorRepoRuns: coordinatorState?.repo_runs || {},
181
+ });
182
+ }
183
+
184
+ function renderRepoRowDetails(details) {
185
+ if (!Array.isArray(details) || details.length === 0) {
186
+ return '-';
187
+ }
188
+
189
+ return details.map((detail) => (
190
+ `<div><span class="detail-label">${esc(detail.label)}:</span> <span${detail.mono ? ' class="mono"' : ''}>${esc(detail.value)}</span></div>`
191
+ )).join('');
192
+ }
193
+
114
194
  export function render({
115
195
  coordinatorState,
116
196
  coordinatorBarriers = {},
117
197
  barrierLedger = [],
118
198
  coordinatorBlockers = null,
199
+ coordinatorRepoStatusRows = null,
119
200
  }) {
120
201
  if (!coordinatorState) {
121
202
  return `<div class="placeholder"><h2>No Initiative</h2><p>No coordinator run found. Start one with <code class="mono">agentxchain multi init</code></p></div>`;
122
203
  }
123
204
 
124
- const repoRuns = Object.entries(coordinatorState.repo_runs || {});
205
+ const repoRows = getCoordinatorRepoRows(coordinatorState, coordinatorRepoStatusRows);
125
206
  const barriers = Object.entries(coordinatorBarriers || {});
126
207
  const pendingGate = coordinatorState.pending_gate || null;
127
208
  const barrierCounts = summarizeBarriers(coordinatorBarriers);
209
+ const decisionConstraintSummary = summarizeDecisionConstraints(coordinatorBarriers);
128
210
  const recentBarrierTransitions = Array.isArray(barrierLedger)
129
211
  ? barrierLedger.slice(-5).reverse()
130
212
  : [];
@@ -135,30 +217,12 @@ export function render({
135
217
  <span class="mono run-id">${esc(coordinatorState.super_run_id)}</span>
136
218
  ${badge(coordinatorState.status || 'unknown', statusColor(coordinatorState.status))}
137
219
  <span class="phase-label">Phase: <strong>${esc(coordinatorState.phase || 'unknown')}</strong></span>
138
- <span class="turn-count">${repoRuns.length} repo${repoRuns.length !== 1 ? 's' : ''}</span>
220
+ <span class="turn-count">${repoRows.length} repo${repoRows.length !== 1 ? 's' : ''}</span>
139
221
  </div>
140
222
  </div>`;
141
223
 
142
224
  if (pendingGate || coordinatorState.blocked_reason) {
143
225
  html += `<div class="section"><h3>Coordinator Attention</h3><div class="initiative-grid">`;
144
- if (pendingGate) {
145
- html += `<div class="gate-card">
146
- <h3>Pending Gate</h3>
147
- <dl class="detail-list">
148
- <dt>Type</dt><dd>${esc(pendingGate.gate_type)}</dd>
149
- <dt>Gate</dt><dd class="mono">${esc(pendingGate.gate)}</dd>`;
150
- if (pendingGate.from) html += `<dt>From</dt><dd>${esc(pendingGate.from)}</dd>`;
151
- if (pendingGate.to) html += `<dt>To</dt><dd>${esc(pendingGate.to)}</dd>`;
152
- if (Array.isArray(pendingGate.required_repos) && pendingGate.required_repos.length > 0) {
153
- html += `<dt>Repos</dt><dd>${esc(pendingGate.required_repos.join(', '))}</dd>`;
154
- }
155
- html += `</dl>
156
- <div class="gate-action">
157
- <p>Approve with:</p>
158
- <pre class="recovery-command mono" data-copy="agentxchain multi approve-gate">agentxchain multi approve-gate</pre>
159
- </div>
160
- </div>`;
161
- }
162
226
  const blockerSnapshot = renderCoordinatorAttentionSnapshot(coordinatorBlockers);
163
227
  if (blockerSnapshot) {
164
228
  html += blockerSnapshot;
@@ -175,14 +239,45 @@ export function render({
175
239
  html += `</div></div>`;
176
240
  }
177
241
 
242
+ if (decisionConstraintSummary) {
243
+ html += `<div class="section"><h3>Cross-Run Constraints</h3><div class="initiative-grid">`;
244
+ html += `<div class="gate-card">
245
+ <h3>Decision Constraints</h3>
246
+ <p class="section-subtitle">First-glance coordinator carryover only. Full per-barrier decision requirements stay in Barrier Snapshot.</p>
247
+ <dl class="detail-list">
248
+ <dt>Barriers</dt><dd>${decisionConstraintSummary.barrier_count}</dd>
249
+ <dt>Repo Requirements</dt><dd>${decisionConstraintSummary.repo_requirement_count}</dd>
250
+ <dt>Required IDs</dt><dd>${decisionConstraintSummary.required_decision_count}</dd>
251
+ <dt>Pending</dt><dd>${decisionConstraintSummary.pending_requirement_count}</dd>
252
+ </dl>`;
253
+
254
+ if (decisionConstraintSummary.first_pending_requirement) {
255
+ const pending = decisionConstraintSummary.first_pending_requirement;
256
+ html += `<div class="turn-card">
257
+ <div class="turn-header"><span>Next Pending Requirement</span></div>
258
+ <div class="turn-detail"><span class="detail-label">Barrier:</span> <span class="mono">${esc(pending.barrierId)}</span></div>
259
+ <div class="turn-detail"><span class="detail-label">Repo:</span> <span class="mono">${esc(pending.repoId)}</span></div>
260
+ <div class="turn-detail"><span class="detail-label">Decision IDs:</span> <span class="mono">${esc(pending.decisionIds.join(', '))}</span></div>
261
+ </div>`;
262
+ if (decisionConstraintSummary.additional_pending_requirement_count > 0) {
263
+ html += `<p class="turn-detail">${decisionConstraintSummary.additional_pending_requirement_count} additional pending requirement${decisionConstraintSummary.additional_pending_requirement_count !== 1 ? 's remain' : ' remains'} in Barrier Snapshot.</p>`;
264
+ }
265
+ } else {
266
+ html += `<p class="turn-summary">All declared coordinator decision requirements are currently satisfied.</p>`;
267
+ }
268
+
269
+ html += `</div></div></div>`;
270
+ }
271
+
178
272
  html += `<div class="section"><h3>Repo Runs</h3><table class="data-table">
179
- <thead><tr><th>Repo</th><th>Run</th><th>Status</th><th>Phase</th></tr></thead><tbody>`;
180
- for (const [repoId, repoRun] of repoRuns) {
273
+ <thead><tr><th>Repo</th><th>Run</th><th>Status</th><th>Phase</th><th>Details</th></tr></thead><tbody>`;
274
+ for (const row of repoRows) {
181
275
  html += `<tr>
182
- <td class="mono">${esc(repoId)}</td>
183
- <td class="mono">${esc(repoRun.run_id || '-')}</td>
184
- <td>${badge(repoRun.status || 'unknown', statusColor(repoRun.status))}</td>
185
- <td>${esc(repoRun.phase || '-')}</td>
276
+ <td class="mono">${esc(row.repo_id || '-')}</td>
277
+ <td class="mono">${esc(row.run_id || '-')}</td>
278
+ <td>${badge(row.status || 'unknown', statusColor(row.status))}</td>
279
+ <td>${esc(row.phase || '-')}</td>
280
+ <td>${renderRepoRowDetails(row.details)}</td>
186
281
  </tr>`;
187
282
  }
188
283
  html += `</tbody></table></div>`;
@@ -160,6 +160,31 @@ function renderLedgerTable(entries, filter) {
160
160
  return { html, filteredCount: filtered.length };
161
161
  }
162
162
 
163
+ function renderRepoDecisionSummary(summary) {
164
+ if (!summary) return '';
165
+
166
+ const operatorSummary = summary.operator_summary || {};
167
+ const categories = Array.isArray(operatorSummary.active_categories) && operatorSummary.active_categories.length > 0
168
+ ? operatorSummary.active_categories.join(', ')
169
+ : 'none active';
170
+ const highestAuthority = typeof operatorSummary.highest_active_authority_level === 'number'
171
+ ? `${operatorSummary.highest_active_authority_level} (${operatorSummary.highest_active_authority_role || 'unknown'})`
172
+ : '—';
173
+ const lineage = `${operatorSummary.superseding_active_count || 0} active superseding earlier decision${operatorSummary.superseding_active_count === 1 ? '' : 's'} · ${operatorSummary.overridden_with_successor_count || 0} overridden with recorded successor${operatorSummary.overridden_with_successor_count === 1 ? '' : 's'}`;
174
+
175
+ return `<div class="section">
176
+ <h3>Repo Decision Carryover</h3>
177
+ <p class="section-subtitle">Cross-run repo-level decisions that remain binding outside the turn ledger</p>
178
+ <div class="run-meta">
179
+ <span class="turn-count">${summary.active_count || 0} active</span>
180
+ <span class="badge">${summary.overridden_count || 0} overridden</span>
181
+ <span class="badge">categories: ${esc(categories)}</span>
182
+ <span class="badge">highest authority: ${esc(highestAuthority)}</span>
183
+ <span class="badge">${esc(lineage)}</span>
184
+ </div>
185
+ </div>`;
186
+ }
187
+
163
188
  function buildSections({ ledger, coordinatorLedger, state, coordinatorState }) {
164
189
  const sections = [];
165
190
  const hasRepoContext = Boolean(state) || (Array.isArray(ledger) && ledger.length > 0);
@@ -187,6 +212,7 @@ function buildSections({ ledger, coordinatorLedger, state, coordinatorState }) {
187
212
  export function render({
188
213
  ledger,
189
214
  coordinatorLedger = null,
215
+ repoDecisionsSummary = null,
190
216
  state = null,
191
217
  coordinatorState = null,
192
218
  filter = {},
@@ -203,6 +229,8 @@ export function render({
203
229
  ${renderFilterBar(combinedLedger, filter)}
204
230
  </div>`;
205
231
 
232
+ html += renderRepoDecisionSummary(repoDecisionsSummary);
233
+
206
234
  for (const section of sections) {
207
235
  const { html: tableHtml, filteredCount } = renderLedgerTable(section.entries, filter);
208
236
  html += `<div class="section"><h3>${section.title}</h3>
@@ -0,0 +1,39 @@
1
+ function esc(str) {
2
+ if (!str) return '';
3
+ return String(str)
4
+ .replace(/&/g, '&amp;')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
10
+
11
+ function badgeTone(state) {
12
+ switch (state) {
13
+ case 'live':
14
+ return 'var(--green)';
15
+ case 'stale':
16
+ return 'var(--yellow)';
17
+ case 'disconnected':
18
+ return 'var(--red)';
19
+ default:
20
+ return 'var(--text-dim)';
21
+ }
22
+ }
23
+
24
+ export function renderLiveStatus(liveMeta) {
25
+ if (!liveMeta) return '';
26
+
27
+ const tone = badgeTone(liveMeta.freshness_state);
28
+ return `<div class="live-status-banner live-status-${esc(liveMeta.freshness_state)}">
29
+ <div class="turn-header">
30
+ <strong>${esc(liveMeta.title || 'Live Feed')}</strong>
31
+ <span class="badge" style="color:${tone};border-color:${tone}">${esc(liveMeta.freshness_label || 'Unknown')}</span>
32
+ </div>
33
+ <div class="live-status-grid">
34
+ <span><strong>Freshness:</strong> ${esc(liveMeta.refresh_detail || 'unknown')}</span>
35
+ <span><strong>Connection:</strong> ${esc(liveMeta.connection_detail || 'unknown')}</span>
36
+ <span><strong>Event:</strong> ${esc(liveMeta.event_detail || 'unknown')}</span>
37
+ </div>
38
+ </div>`;
39
+ }
@@ -34,6 +34,21 @@ function statusBadge(status) {
34
34
  }
35
35
  }
36
36
 
37
+ function outcomeBadge(outcome) {
38
+ switch (outcome?.label) {
39
+ case 'clean':
40
+ return badge('clean', 'var(--green)');
41
+ case 'follow-on':
42
+ return badge('follow-on', '#38bdf8');
43
+ case 'operator':
44
+ return badge('operator', 'var(--yellow)');
45
+ case 'blocked':
46
+ return badge('blocked', 'var(--yellow)');
47
+ default:
48
+ return badge(outcome?.label || 'unknown', 'var(--text-dim)');
49
+ }
50
+ }
51
+
37
52
  function formatDuration(ms) {
38
53
  if (ms == null) return '—';
39
54
  if (ms < 1000) return `${ms}ms`;
@@ -75,6 +90,49 @@ function truncateHeadline(headline, len = 40) {
75
90
  return normalized.slice(0, len - 1) + '…';
76
91
  }
77
92
 
93
+ function truncateLine(value, len = 68) {
94
+ if (!value) return '—';
95
+ const normalized = String(value).replace(/\s+/g, ' ').trim();
96
+ if (normalized.length <= len) return normalized;
97
+ return normalized.slice(0, len - 1) + '…';
98
+ }
99
+
100
+ function normalizeSingleLine(value) {
101
+ if (typeof value !== 'string') return null;
102
+ const normalized = value.replace(/\s+/g, ' ').trim();
103
+ return normalized.length > 0 ? normalized : null;
104
+ }
105
+
106
+ function buildOutcomeSummary(entry) {
107
+ const status = typeof entry?.status === 'string' ? entry.status : 'unknown';
108
+ const nextAction = normalizeSingleLine(
109
+ entry?.retrospective?.next_operator_action
110
+ || entry?.retrospective?.follow_on_hint
111
+ || null
112
+ );
113
+
114
+ if (status === 'blocked' && nextAction) {
115
+ return { label: 'operator', next_action: nextAction };
116
+ }
117
+ if (status === 'blocked') {
118
+ return { label: 'blocked', next_action: null };
119
+ }
120
+ if (status === 'completed' && nextAction) {
121
+ return { label: 'follow-on', next_action: nextAction };
122
+ }
123
+ if (status === 'completed') {
124
+ return { label: 'clean', next_action: null };
125
+ }
126
+ return { label: 'unknown', next_action: nextAction };
127
+ }
128
+
129
+ function getTriggerLabel(provenance) {
130
+ const trigger = typeof provenance?.trigger === 'string' && provenance.trigger.trim()
131
+ ? provenance.trigger.trim()
132
+ : null;
133
+ return trigger || 'legacy';
134
+ }
135
+
78
136
  function isInheritable(entry) {
79
137
  const snap = entry?.inheritance_snapshot;
80
138
  if (!snap) return false;
@@ -84,6 +142,7 @@ function isInheritable(entry) {
84
142
  }
85
143
 
86
144
  function renderRow(entry, index) {
145
+ const outcome = buildOutcomeSummary(entry);
87
146
  const rowClass = entry.status === 'blocked'
88
147
  ? ' style="border-left:3px solid var(--yellow)"'
89
148
  : entry.status === 'failed'
@@ -102,17 +161,23 @@ function renderRow(entry, index) {
102
161
  ? `<span title="Has inheritance snapshot — usable by child runs" style="color:var(--green)">✓</span>`
103
162
  : `<span style="color:var(--text-dim)">—</span>`;
104
163
 
164
+ const nextAction = outcome.next_action
165
+ ? `<div class="next-hint" style="font-size:0.82em;color:var(--text-dim);margin-top:4px">next: ${esc(truncateLine(outcome.next_action))}</div>`
166
+ : '';
167
+
105
168
  return `<tr${rowClass}>
106
169
  <td style="color:var(--text-dim)">${index + 1}</td>
107
170
  <td class="mono" title="${esc(entry.run_id)}">${esc(truncateId(entry.run_id))}</td>
108
171
  <td>${statusBadge(entry.status)}${blockedInfo}</td>
172
+ <td>${outcomeBadge(outcome)}</td>
173
+ <td>${esc(getTriggerLabel(entry.provenance))}</td>
109
174
  <td>${ctxIndicator}</td>
110
175
  <td>${phases}</td>
111
176
  <td>${entry.total_turns ?? '—'}</td>
112
177
  <td>${formatCost(entry.total_cost_usd)}</td>
113
178
  <td>${formatDuration(entry.duration_ms)}</td>
114
179
  <td>${formatDate(entry.recorded_at || entry.completed_at)}</td>
115
- <td title="${esc(entry.retrospective?.headline || '')}">${esc(truncateHeadline(entry.retrospective?.headline))}</td>
180
+ <td title="${esc(entry.retrospective?.headline || '')}">${esc(truncateHeadline(entry.retrospective?.headline))}${nextAction}</td>
116
181
  </tr>`;
117
182
  }
118
183
 
@@ -128,6 +193,11 @@ export function render({ runHistory }) {
128
193
  const total = runHistory.length;
129
194
  const completed = runHistory.filter(e => e.status === 'completed').length;
130
195
  const blocked = runHistory.filter(e => e.status === 'blocked').length;
196
+ const outcomeCounts = runHistory.reduce((counts, entry) => {
197
+ const outcome = buildOutcomeSummary(entry).label;
198
+ counts[outcome] = (counts[outcome] || 0) + 1;
199
+ return counts;
200
+ }, {});
131
201
 
132
202
  let html = `<div class="run-history-view">`;
133
203
 
@@ -136,6 +206,9 @@ export function render({ runHistory }) {
136
206
  html += `<span class="turn-count">${total} run${total !== 1 ? 's' : ''} recorded</span>`;
137
207
  if (completed > 0) html += badge(`${completed} completed`, 'var(--green)');
138
208
  if (blocked > 0) html += badge(`${blocked} blocked`, 'var(--yellow)');
209
+ if (outcomeCounts.clean > 0) html += badge(`${outcomeCounts.clean} clean`, 'var(--green)');
210
+ if (outcomeCounts['follow-on'] > 0) html += badge(`${outcomeCounts['follow-on']} follow-on`, '#38bdf8');
211
+ if (outcomeCounts.operator > 0) html += badge(`${outcomeCounts.operator} operator`, 'var(--yellow)');
139
212
  html += `</div></div>`;
140
213
 
141
214
  // Table
@@ -146,6 +219,8 @@ export function render({ runHistory }) {
146
219
  <th>#</th>
147
220
  <th>Run ID</th>
148
221
  <th>Status</th>
222
+ <th>Outcome</th>
223
+ <th>Trigger</th>
149
224
  <th>Ctx</th>
150
225
  <th>Phases</th>
151
226
  <th>Turns</th>
@@ -4,6 +4,8 @@
4
4
  * Pure render function: takes data, returns HTML string. Testable in Node.js.
5
5
  */
6
6
 
7
+ import { renderLiveStatus } from './live-status.js';
8
+
7
9
  function esc(str) {
8
10
  if (!str) return '';
9
11
  return String(str)
@@ -304,7 +306,7 @@ function renderConnectorHealthPanel(connectorsPayload) {
304
306
 
305
307
  export { formatDuration, computeElapsed, formatTimestamp };
306
308
 
307
- export function render({ state, continuity, history, annotations, audit, connectors, coordinatorAudit = null, coordinatorAnnotations = null }) {
309
+ export function render({ state, continuity, history, annotations, audit, connectors, coordinatorAudit = null, coordinatorAnnotations = null, liveMeta = null }) {
308
310
  if (!state) {
309
311
  return `<div class="placeholder"><h2>No Run</h2><p>No governed run found. Start one with <code class="mono">agentxchain init --governed</code></p></div>`;
310
312
  }
@@ -314,6 +316,8 @@ export function render({ state, continuity, history, annotations, audit, connect
314
316
 
315
317
  let html = `<div class="timeline-view">`;
316
318
 
319
+ html += renderLiveStatus(liveMeta);
320
+
317
321
  // Run header
318
322
  html += `<div class="run-header">
319
323
  <div class="run-meta">
@@ -143,6 +143,27 @@
143
143
  .section { margin-bottom: 24px; }
144
144
  .section h3 { font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--text); }
145
145
  .section-subtitle { font-size: 12px; color: var(--text-dim); margin-bottom: 12px; }
146
+ .live-status-banner {
147
+ margin-bottom: 16px;
148
+ padding: 12px 14px;
149
+ border-radius: 6px;
150
+ border: 1px solid var(--border);
151
+ background: rgba(99, 102, 241, 0.08);
152
+ }
153
+ .live-status-live { border-color: rgba(34, 197, 94, 0.35); }
154
+ .live-status-stale { border-color: rgba(234, 179, 8, 0.35); }
155
+ .live-status-disconnected { border-color: rgba(239, 68, 68, 0.35); }
156
+ .live-status-connecting { border-color: rgba(136, 136, 160, 0.35); }
157
+ .live-status-grid {
158
+ display: flex;
159
+ flex-wrap: wrap;
160
+ gap: 10px 14px;
161
+ margin-top: 8px;
162
+ font-size: 12px;
163
+ color: var(--text-dim);
164
+ line-height: 1.45;
165
+ }
166
+ .live-status-grid strong { color: var(--text); }
146
167
 
147
168
  /* Turn cards */
148
169
  .turn-list { display: flex; flex-direction: column; gap: 8px; }