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.
- package/README.md +13 -7
- package/bin/agentxchain.js +16 -8
- package/dashboard/app.js +111 -7
- package/dashboard/components/blocked.js +95 -11
- package/dashboard/components/blockers.js +85 -86
- package/dashboard/components/coordinator-timeouts.js +13 -0
- package/dashboard/components/cross-repo.js +17 -12
- package/dashboard/components/gate.js +31 -11
- package/dashboard/components/initiative.js +173 -78
- package/dashboard/components/ledger.js +28 -0
- package/dashboard/components/live-status.js +39 -0
- package/dashboard/components/run-history.js +76 -1
- package/dashboard/components/timeline.js +5 -1
- package/dashboard/index.html +21 -0
- package/dashboard/live-observer.js +91 -0
- package/package.json +1 -1
- package/scripts/release-bump.sh +26 -3
- package/scripts/release-preflight.sh +82 -38
- package/src/commands/accept-turn.js +3 -3
- package/src/commands/decisions.js +98 -29
- package/src/commands/diff.js +27 -4
- package/src/commands/doctor.js +48 -16
- package/src/commands/generate.js +126 -1
- package/src/commands/history.js +21 -3
- package/src/commands/init.js +15 -97
- package/src/commands/multi.js +223 -54
- package/src/commands/phase.js +11 -13
- package/src/commands/reject-turn.js +1 -1
- package/src/commands/restart.js +28 -11
- package/src/commands/resume.js +6 -6
- package/src/commands/role.js +51 -14
- package/src/commands/run.js +5 -11
- package/src/commands/status.js +145 -13
- package/src/commands/step.js +36 -29
- package/src/lib/admission-control.js +14 -12
- package/src/lib/blocked-state.js +150 -0
- package/src/lib/conflict-actions.js +17 -0
- package/src/lib/context-section-parser.js +2 -0
- package/src/lib/continuity-status.js +1 -1
- package/src/lib/coordinator-blocker-presentation.js +127 -0
- package/src/lib/coordinator-event-narrative.js +43 -0
- package/src/lib/coordinator-gate-approval.js +98 -0
- package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
- package/src/lib/coordinator-next-actions.js +128 -0
- package/src/lib/coordinator-pending-gate-presentation.js +79 -0
- package/src/lib/coordinator-presentation-detail.js +11 -0
- package/src/lib/coordinator-repo-snapshots.js +53 -0
- package/src/lib/coordinator-repo-status-presentation.js +134 -0
- package/src/lib/dashboard/actions.js +105 -29
- package/src/lib/dashboard/bridge-server.js +7 -0
- package/src/lib/dashboard/coordinator-blockers.js +17 -0
- package/src/lib/dashboard/coordinator-repo-status.js +50 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
- package/src/lib/dashboard/state-reader.js +36 -1
- package/src/lib/dispatch-bundle.js +23 -0
- package/src/lib/export-diff.js +70 -38
- package/src/lib/export-verifier.js +3 -0
- package/src/lib/history-diff-summary.js +249 -0
- package/src/lib/manual-qa-fallback.js +18 -0
- package/src/lib/normalized-config.js +27 -22
- package/src/lib/planning-artifacts.js +131 -0
- package/src/lib/recent-event-summary.js +132 -0
- package/src/lib/repo-decisions.js +69 -28
- package/src/lib/report.js +353 -145
- package/src/lib/run-diff.js +4 -0
- 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
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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 (
|
|
62
|
-
html += `<
|
|
104
|
+
if (action.command) {
|
|
105
|
+
html += `<pre class="recovery-command mono" data-copy="${esc(action.command)}">${esc(action.command)}</pre>`;
|
|
63
106
|
}
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
html +=
|
|
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
|
|
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">${
|
|
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
|
|
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(
|
|
183
|
-
<td class="mono">${esc(
|
|
184
|
-
<td>${badge(
|
|
185
|
-
<td>${esc(
|
|
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, '&')
|
|
5
|
+
.replace(/</g, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
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">
|
package/dashboard/index.html
CHANGED
|
@@ -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; }
|