agentxchain 2.46.0 → 2.47.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/bin/agentxchain.js +4 -1
- package/dashboard/app.js +6 -0
- package/dashboard/components/coordinator-timeouts.js +220 -0
- package/dashboard/components/timeouts.js +201 -0
- package/dashboard/index.html +2 -0
- package/package.json +1 -1
- package/scripts/publish-from-tag.sh +33 -4
- package/src/commands/history.js +41 -1
- package/src/commands/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/run.js +32 -1
- package/src/commands/status.js +55 -0
- package/src/lib/approval-policy.js +139 -0
- package/src/lib/blocked-state.js +11 -0
- package/src/lib/dashboard/bridge-server.js +14 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +139 -0
- package/src/lib/dashboard/timeout-status.js +201 -0
- package/src/lib/export.js +2 -0
- package/src/lib/governed-state.js +428 -30
- package/src/lib/normalized-config.js +123 -0
- package/src/lib/reference-conformance-adapter.js +1 -0
- package/src/lib/repo-observer.js +132 -1
- package/src/lib/report.js +331 -6
- package/src/lib/run-history.js +111 -0
- package/src/lib/run-loop.js +9 -3
- package/src/lib/run-provenance.js +90 -0
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
package/bin/agentxchain.js
CHANGED
|
@@ -262,7 +262,8 @@ program
|
|
|
262
262
|
.description('Show cross-run history of governed runs in this project')
|
|
263
263
|
.option('-j, --json', 'Output as JSON')
|
|
264
264
|
.option('-l, --limit <n>', 'Number of recent runs to show (default: 20)')
|
|
265
|
-
.option('-s, --status <status>', 'Filter by status: completed
|
|
265
|
+
.option('-s, --status <status>', 'Filter by status: completed or blocked')
|
|
266
|
+
.option('--lineage <run_id>', 'Show lineage chain for a specific run')
|
|
266
267
|
.option('-d, --dir <path>', 'Project directory')
|
|
267
268
|
.action(historyCommand);
|
|
268
269
|
|
|
@@ -355,6 +356,8 @@ program
|
|
|
355
356
|
.option('--verbose', 'Stream adapter subprocess output')
|
|
356
357
|
.option('--dry-run', 'Print what would be dispatched without executing')
|
|
357
358
|
.option('--no-report', 'Suppress automatic governance report after run completes')
|
|
359
|
+
.option('--continue-from <run_id>', 'Continue from a prior terminal run (sets trigger=continuation)')
|
|
360
|
+
.option('--recover-from <run_id>', 'Recover from a prior blocked run (sets trigger=recovery)')
|
|
358
361
|
.action(runCommand);
|
|
359
362
|
|
|
360
363
|
program
|
package/dashboard/app.js
CHANGED
|
@@ -15,6 +15,8 @@ import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
|
15
15
|
import { render as renderBlockers } from './components/blockers.js';
|
|
16
16
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
17
17
|
import { render as renderRunHistory } from './components/run-history.js';
|
|
18
|
+
import { render as renderTimeouts } from './components/timeouts.js';
|
|
19
|
+
import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
|
|
18
20
|
|
|
19
21
|
const VIEWS = {
|
|
20
22
|
timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors'], render: renderTimeline },
|
|
@@ -27,6 +29,8 @@ const VIEWS = {
|
|
|
27
29
|
blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
|
|
28
30
|
artifacts: { fetch: ['workflowKitArtifacts'], render: renderArtifacts },
|
|
29
31
|
'run-history': { fetch: ['runHistory'], render: renderRunHistory },
|
|
32
|
+
timeouts: { fetch: ['timeouts'], render: renderTimeouts },
|
|
33
|
+
'coordinator-timeouts': { fetch: ['coordinatorTimeouts'], render: renderCoordinatorTimeouts },
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
const API_MAP = {
|
|
@@ -45,6 +49,8 @@ const API_MAP = {
|
|
|
45
49
|
workflowKitArtifacts: '/api/workflow-kit-artifacts',
|
|
46
50
|
connectors: '/api/connectors',
|
|
47
51
|
runHistory: '/api/run-history',
|
|
52
|
+
timeouts: '/api/timeouts',
|
|
53
|
+
coordinatorTimeouts: '/api/coordinator/timeouts',
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
const viewState = {
|
|
@@ -0,0 +1,220 @@
|
|
|
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 badge(label, color = 'var(--text-dim)') {
|
|
12
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function scopeLabel(scope) {
|
|
16
|
+
if (scope === 'turn') return 'Per-Turn';
|
|
17
|
+
if (scope === 'phase') return 'Per-Phase';
|
|
18
|
+
if (scope === 'run') return 'Per-Run';
|
|
19
|
+
return esc(scope || '—');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function actionBadge(action) {
|
|
23
|
+
if (action === 'escalate') return badge('escalate', 'var(--red)');
|
|
24
|
+
if (action === 'warn') return badge('warn', 'var(--yellow)');
|
|
25
|
+
if (action === 'skip_phase') return badge('skip_phase', 'var(--accent)');
|
|
26
|
+
return badge(action || '—');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function typeLabel(type) {
|
|
30
|
+
if (type === 'timeout') return badge('exceeded', 'var(--red)');
|
|
31
|
+
if (type === 'timeout_warning') return badge('warning', 'var(--yellow)');
|
|
32
|
+
if (type === 'timeout_skip') return badge('skipped', 'var(--accent)');
|
|
33
|
+
if (type === 'timeout_skip_failed') return badge('skip failed', 'var(--red)');
|
|
34
|
+
return badge(type || '—');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function statusColor(status) {
|
|
38
|
+
const colors = {
|
|
39
|
+
active: 'var(--green)',
|
|
40
|
+
blocked: 'var(--red)',
|
|
41
|
+
paused: 'var(--yellow)',
|
|
42
|
+
completed: 'var(--accent)',
|
|
43
|
+
idle: 'var(--text-dim)',
|
|
44
|
+
initialized: 'var(--accent)',
|
|
45
|
+
linked: 'var(--green)',
|
|
46
|
+
};
|
|
47
|
+
return colors[status] || 'var(--text-dim)';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function renderSummary(summary) {
|
|
51
|
+
return `<div class="gate-card"><h3>Summary</h3>
|
|
52
|
+
<dl class="detail-list">
|
|
53
|
+
<dt>Repos</dt><dd>${summary.repo_count}</dd>
|
|
54
|
+
<dt>Timeout Configured</dt><dd>${summary.configured_repo_count}</dd>
|
|
55
|
+
<dt>Live Exceeded</dt><dd>${summary.repos_with_live_exceeded}</dd>
|
|
56
|
+
<dt>Live Warnings</dt><dd>${summary.repos_with_live_warnings}</dd>
|
|
57
|
+
<dt>Repo Events</dt><dd>${summary.repo_event_count}</dd>
|
|
58
|
+
<dt>Coordinator Events</dt><dd>${summary.coordinator_event_count}</dd>
|
|
59
|
+
</dl>
|
|
60
|
+
</div>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderConfigTable(config) {
|
|
64
|
+
if (!config) {
|
|
65
|
+
return `<p style="color:var(--text-dim)">No <code>timeouts</code> configured in this repo.</p>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let html = `<table class="data-table">
|
|
69
|
+
<thead><tr><th>Scope</th><th>Limit</th><th>Action</th></tr></thead>
|
|
70
|
+
<tbody>`;
|
|
71
|
+
if (config.per_turn_minutes) {
|
|
72
|
+
html += `<tr><td>Per-Turn</td><td>${config.per_turn_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
73
|
+
}
|
|
74
|
+
if (config.per_phase_minutes) {
|
|
75
|
+
html += `<tr><td>Per-Phase</td><td>${config.per_phase_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
76
|
+
}
|
|
77
|
+
if (config.per_run_minutes) {
|
|
78
|
+
html += `<tr><td>Per-Run</td><td>${config.per_run_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
79
|
+
}
|
|
80
|
+
for (const override of (config.phase_overrides || [])) {
|
|
81
|
+
html += `<tr><td>Phase: <strong>${esc(override.phase)}</strong></td><td>${override.limit_minutes ? `${override.limit_minutes}m` : 'inherit'}</td><td>${override.action ? actionBadge(override.action) : 'inherit'}</td></tr>`;
|
|
82
|
+
}
|
|
83
|
+
html += `</tbody></table>`;
|
|
84
|
+
return html;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderLiveTable(live) {
|
|
88
|
+
const exceeded = live?.exceeded || [];
|
|
89
|
+
const warnings = live?.warnings || [];
|
|
90
|
+
if (exceeded.length === 0 && warnings.length === 0) {
|
|
91
|
+
return `<p style="color:var(--green)">No live timeout pressure.</p>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let html = `<table class="data-table">
|
|
95
|
+
<thead><tr><th>Status</th><th>Scope</th><th>Turn</th><th>Phase</th><th>Elapsed</th><th>Limit</th><th>Exceeded By</th><th>Action</th></tr></thead>
|
|
96
|
+
<tbody>`;
|
|
97
|
+
for (const item of exceeded) {
|
|
98
|
+
const turnLabel = item.turn_id
|
|
99
|
+
? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
|
|
100
|
+
: '—';
|
|
101
|
+
html += `<tr style="border-left:3px solid var(--red)">
|
|
102
|
+
<td>${badge('EXCEEDED', 'var(--red)')}</td>
|
|
103
|
+
<td>${scopeLabel(item.scope)}</td>
|
|
104
|
+
<td>${turnLabel}</td>
|
|
105
|
+
<td>${item.phase ? esc(item.phase) : '—'}</td>
|
|
106
|
+
<td>${item.elapsed_minutes}m</td>
|
|
107
|
+
<td>${item.limit_minutes}m</td>
|
|
108
|
+
<td style="color:var(--red)">${item.exceeded_by_minutes}m</td>
|
|
109
|
+
<td>${actionBadge(item.action)}</td>
|
|
110
|
+
</tr>`;
|
|
111
|
+
}
|
|
112
|
+
for (const item of warnings) {
|
|
113
|
+
const turnLabel = item.turn_id
|
|
114
|
+
? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
|
|
115
|
+
: '—';
|
|
116
|
+
html += `<tr style="border-left:3px solid var(--yellow)">
|
|
117
|
+
<td>${badge('WARNING', 'var(--yellow)')}</td>
|
|
118
|
+
<td>${scopeLabel(item.scope)}</td>
|
|
119
|
+
<td>${turnLabel}</td>
|
|
120
|
+
<td>${item.phase ? esc(item.phase) : '—'}</td>
|
|
121
|
+
<td>${item.elapsed_minutes}m</td>
|
|
122
|
+
<td>${item.limit_minutes}m</td>
|
|
123
|
+
<td>${item.exceeded_by_minutes}m</td>
|
|
124
|
+
<td>${actionBadge(item.action)}</td>
|
|
125
|
+
</tr>`;
|
|
126
|
+
}
|
|
127
|
+
html += `</tbody></table>`;
|
|
128
|
+
return html;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderEventTable(events, title) {
|
|
132
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
133
|
+
return `<div class="section"><h3>${title}</h3><p style="color:var(--text-dim)">No timeout events recorded.</p></div>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let html = `<div class="section"><h3>${title}</h3>
|
|
137
|
+
<table class="data-table">
|
|
138
|
+
<thead><tr><th>Type</th><th>Scope</th><th>Phase</th><th>Turn</th><th>Elapsed</th><th>Limit</th><th>Action</th></tr></thead>
|
|
139
|
+
<tbody>`;
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
html += `<tr>
|
|
142
|
+
<td>${typeLabel(event.type)}</td>
|
|
143
|
+
<td>${scopeLabel(event.scope)}</td>
|
|
144
|
+
<td>${event.phase ? esc(event.phase) : '—'}</td>
|
|
145
|
+
<td class="mono">${event.turn_id ? esc(event.turn_id) : '—'}</td>
|
|
146
|
+
<td>${event.elapsed_minutes != null ? `${event.elapsed_minutes}m` : '—'}</td>
|
|
147
|
+
<td>${event.limit_minutes != null ? `${event.limit_minutes}m` : '—'}</td>
|
|
148
|
+
<td>${actionBadge(event.action)}</td>
|
|
149
|
+
</tr>`;
|
|
150
|
+
}
|
|
151
|
+
html += `</tbody></table></div>`;
|
|
152
|
+
return html;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderRepoCard(repo) {
|
|
156
|
+
let html = `<div class="turn-card">
|
|
157
|
+
<div class="turn-header">
|
|
158
|
+
<span class="mono">${esc(repo.repo_id)}</span>
|
|
159
|
+
${repo.status ? badge(repo.status, statusColor(repo.status)) : ''}
|
|
160
|
+
${repo.configured ? badge('timeouts configured', 'var(--green)') : badge('no timeouts', 'var(--text-dim)')}
|
|
161
|
+
</div>`;
|
|
162
|
+
|
|
163
|
+
html += `<dl class="detail-list">
|
|
164
|
+
<dt>Path</dt><dd class="mono">${esc(repo.path)}</dd>`;
|
|
165
|
+
if (repo.run_id) {
|
|
166
|
+
html += `<dt>Run</dt><dd class="mono">${esc(repo.run_id)}</dd>`;
|
|
167
|
+
}
|
|
168
|
+
if (repo.phase) {
|
|
169
|
+
html += `<dt>Phase</dt><dd>${esc(repo.phase)}</dd>`;
|
|
170
|
+
}
|
|
171
|
+
html += `</dl>`;
|
|
172
|
+
|
|
173
|
+
if (repo.error) {
|
|
174
|
+
html += `<p style="color:var(--red)">${esc(repo.error.error)}</p></div>`;
|
|
175
|
+
return html;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
html += `<div class="section"><h3>Configuration</h3>${renderConfigTable(repo.config)}</div>`;
|
|
179
|
+
if (repo.live) {
|
|
180
|
+
html += `<div class="section"><h3>Live Pressure</h3>${renderLiveTable(repo.live)}</div>`;
|
|
181
|
+
}
|
|
182
|
+
html += renderEventTable(repo.events, 'Repo Events');
|
|
183
|
+
html += `</div>`;
|
|
184
|
+
return html;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function render({ coordinatorTimeouts }) {
|
|
188
|
+
if (!coordinatorTimeouts) {
|
|
189
|
+
return `<div class="placeholder"><h2>Coordinator Timeouts</h2><p>No coordinator timeout data available. Ensure a coordinator run is active.</p></div>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (coordinatorTimeouts.ok === false) {
|
|
193
|
+
return `<div class="placeholder"><h2>Coordinator Timeouts</h2><p>${esc(coordinatorTimeouts.error || 'Failed to load coordinator timeout data.')}</p></div>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let html = `<div class="timeouts-view"><div class="run-header"><div class="run-meta">`;
|
|
197
|
+
if (coordinatorTimeouts.super_run_id) {
|
|
198
|
+
html += `<span class="mono run-id">${esc(coordinatorTimeouts.super_run_id)}</span>`;
|
|
199
|
+
}
|
|
200
|
+
if (coordinatorTimeouts.status) {
|
|
201
|
+
html += badge(coordinatorTimeouts.status, statusColor(coordinatorTimeouts.status));
|
|
202
|
+
}
|
|
203
|
+
if (coordinatorTimeouts.phase) {
|
|
204
|
+
html += `<span class="phase-label">Phase: <strong>${esc(coordinatorTimeouts.phase)}</strong></span>`;
|
|
205
|
+
}
|
|
206
|
+
html += `${badge('coordinator timeout view', 'var(--accent)')}</div></div>`;
|
|
207
|
+
|
|
208
|
+
if (coordinatorTimeouts.blocked_reason) {
|
|
209
|
+
html += `<div class="blocked-banner"><div class="blocked-icon">BLOCKED</div><div class="blocked-reason">${esc(typeof coordinatorTimeouts.blocked_reason === 'string' ? coordinatorTimeouts.blocked_reason : JSON.stringify(coordinatorTimeouts.blocked_reason))}</div></div>`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
html += renderSummary(coordinatorTimeouts.summary);
|
|
213
|
+
html += renderEventTable(coordinatorTimeouts.coordinator_events, 'Coordinator Events');
|
|
214
|
+
html += `<div class="section"><h3>Repo Timeout Status</h3><div class="turn-list">`;
|
|
215
|
+
for (const repo of (coordinatorTimeouts.repos || [])) {
|
|
216
|
+
html += renderRepoCard(repo);
|
|
217
|
+
}
|
|
218
|
+
html += `</div></div></div>`;
|
|
219
|
+
return html;
|
|
220
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeouts view — renders live timeout pressure and persisted timeout events.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data from /api/timeouts, returns HTML.
|
|
5
|
+
* All evaluation is server-side. This view renders the snapshot.
|
|
6
|
+
*
|
|
7
|
+
* See: TIMEOUT_DASHBOARD_SURFACE_SPEC.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function esc(str) {
|
|
11
|
+
if (!str) return '';
|
|
12
|
+
return String(str)
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
21
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scopeLabel(scope) {
|
|
25
|
+
if (scope === 'turn') return 'Per-Turn';
|
|
26
|
+
if (scope === 'phase') return 'Per-Phase';
|
|
27
|
+
if (scope === 'run') return 'Per-Run';
|
|
28
|
+
return esc(scope || '—');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function actionBadge(action) {
|
|
32
|
+
if (action === 'escalate') return badge('escalate', 'var(--red)');
|
|
33
|
+
if (action === 'warn') return badge('warn', 'var(--yellow)');
|
|
34
|
+
if (action === 'skip_phase') return badge('skip_phase', 'var(--accent)');
|
|
35
|
+
return badge(action || '—', 'var(--text-dim)');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function typeLabel(type) {
|
|
39
|
+
if (type === 'timeout') return badge('exceeded', 'var(--red)');
|
|
40
|
+
if (type === 'timeout_warning') return badge('warning', 'var(--yellow)');
|
|
41
|
+
if (type === 'timeout_skip') return badge('skipped', 'var(--accent)');
|
|
42
|
+
if (type === 'timeout_skip_failed') return badge('skip failed', 'var(--red)');
|
|
43
|
+
return badge(type || '—');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTime(ts) {
|
|
47
|
+
if (!ts) return '—';
|
|
48
|
+
try {
|
|
49
|
+
const d = new Date(ts);
|
|
50
|
+
return d.toLocaleString();
|
|
51
|
+
} catch {
|
|
52
|
+
return esc(ts);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderConfigTable(config) {
|
|
57
|
+
let html = `<div class="section"><h3>Timeout Configuration</h3>
|
|
58
|
+
<table class="data-table">
|
|
59
|
+
<thead><tr><th>Scope</th><th>Limit</th><th>Action</th></tr></thead>
|
|
60
|
+
<tbody>`;
|
|
61
|
+
|
|
62
|
+
if (config.per_turn_minutes) {
|
|
63
|
+
html += `<tr><td>Per-Turn</td><td>${config.per_turn_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
64
|
+
}
|
|
65
|
+
if (config.per_phase_minutes) {
|
|
66
|
+
html += `<tr><td>Per-Phase (global)</td><td>${config.per_phase_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
67
|
+
}
|
|
68
|
+
if (config.per_run_minutes) {
|
|
69
|
+
html += `<tr><td>Per-Run</td><td>${config.per_run_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(config.phase_overrides)) {
|
|
73
|
+
for (const override of config.phase_overrides) {
|
|
74
|
+
const limitStr = override.limit_minutes ? `${override.limit_minutes}m` : '<span style="color:var(--text-dim)">inherit</span>';
|
|
75
|
+
const actionStr = override.action ? actionBadge(override.action) : '<span style="color:var(--text-dim)">inherit</span>';
|
|
76
|
+
html += `<tr><td>Phase: <strong>${esc(override.phase)}</strong></td><td>${limitStr}</td><td>${actionStr}</td></tr>`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
html += `</tbody></table></div>`;
|
|
81
|
+
return html;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderLivePressure(live) {
|
|
85
|
+
const hasExceeded = live.exceeded && live.exceeded.length > 0;
|
|
86
|
+
const hasWarnings = live.warnings && live.warnings.length > 0;
|
|
87
|
+
|
|
88
|
+
if (!hasExceeded && !hasWarnings) {
|
|
89
|
+
return `<div class="section"><h3>Live Pressure</h3><p style="color:var(--green)">No timeouts exceeded or approaching limits.</p></div>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let html = `<div class="section"><h3>Live Pressure</h3>
|
|
93
|
+
<table class="data-table">
|
|
94
|
+
<thead><tr><th>Status</th><th>Scope</th><th>Turn</th><th>Phase</th><th>Elapsed</th><th>Limit</th><th>Exceeded By</th><th>Action</th></tr></thead>
|
|
95
|
+
<tbody>`;
|
|
96
|
+
|
|
97
|
+
for (const item of (live.exceeded || [])) {
|
|
98
|
+
const turnLabel = item.turn_id
|
|
99
|
+
? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
|
|
100
|
+
: '—';
|
|
101
|
+
html += `<tr style="border-left:3px solid var(--red)">
|
|
102
|
+
<td>${badge('EXCEEDED', 'var(--red)')}</td>
|
|
103
|
+
<td>${scopeLabel(item.scope)}</td>
|
|
104
|
+
<td>${turnLabel}</td>
|
|
105
|
+
<td>${item.phase ? esc(item.phase) : '—'}</td>
|
|
106
|
+
<td>${item.elapsed_minutes}m</td>
|
|
107
|
+
<td>${item.limit_minutes}m</td>
|
|
108
|
+
<td style="color:var(--red)">${item.exceeded_by_minutes}m</td>
|
|
109
|
+
<td>${actionBadge(item.action)}</td>
|
|
110
|
+
</tr>`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const item of (live.warnings || [])) {
|
|
114
|
+
const turnLabel = item.turn_id
|
|
115
|
+
? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
|
|
116
|
+
: '—';
|
|
117
|
+
html += `<tr style="border-left:3px solid var(--yellow)">
|
|
118
|
+
<td>${badge('WARNING', 'var(--yellow)')}</td>
|
|
119
|
+
<td>${scopeLabel(item.scope)}</td>
|
|
120
|
+
<td>${turnLabel}</td>
|
|
121
|
+
<td>${item.phase ? esc(item.phase) : '—'}</td>
|
|
122
|
+
<td>${item.elapsed_minutes}m</td>
|
|
123
|
+
<td>${item.limit_minutes}m</td>
|
|
124
|
+
<td>${item.exceeded_by_minutes}m</td>
|
|
125
|
+
<td>${actionBadge(item.action)}</td>
|
|
126
|
+
</tr>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
html += `</tbody></table></div>`;
|
|
130
|
+
return html;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderEvents(events) {
|
|
134
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
135
|
+
return `<div class="section"><h3>Timeout Events</h3><p style="color:var(--text-dim)">No timeout events recorded in the decision ledger.</p></div>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let html = `<div class="section"><h3>Timeout Events</h3>
|
|
139
|
+
<table class="data-table">
|
|
140
|
+
<thead><tr><th>Type</th><th>Scope</th><th>Phase</th><th>Turn</th><th>Elapsed</th><th>Limit</th><th>Action</th><th>Timestamp</th></tr></thead>
|
|
141
|
+
<tbody>`;
|
|
142
|
+
|
|
143
|
+
for (const event of events) {
|
|
144
|
+
html += `<tr>
|
|
145
|
+
<td>${typeLabel(event.type)}</td>
|
|
146
|
+
<td>${scopeLabel(event.scope)}</td>
|
|
147
|
+
<td>${event.phase ? esc(event.phase) : '—'}</td>
|
|
148
|
+
<td class="mono">${event.turn_id ? esc(event.turn_id) : '—'}</td>
|
|
149
|
+
<td>${event.elapsed_minutes != null ? `${event.elapsed_minutes}m` : '—'}</td>
|
|
150
|
+
<td>${event.limit_minutes != null ? `${event.limit_minutes}m` : '—'}</td>
|
|
151
|
+
<td>${actionBadge(event.action)}</td>
|
|
152
|
+
<td>${formatTime(event.timestamp)}</td>
|
|
153
|
+
</tr>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
html += `</tbody></table></div>`;
|
|
157
|
+
return html;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function render({ timeouts }) {
|
|
161
|
+
if (!timeouts) {
|
|
162
|
+
return `<div class="placeholder"><h2>Timeouts</h2><p>No timeout data available. Ensure a governed run is active.</p></div>`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (timeouts.ok === false) {
|
|
166
|
+
const hint = timeouts.code === 'config_missing' || timeouts.code === 'state_missing'
|
|
167
|
+
? ' Run <code>agentxchain init --governed</code> to get started.'
|
|
168
|
+
: '';
|
|
169
|
+
return `<div class="placeholder"><h2>Timeouts</h2><p>${esc(timeouts.error || 'Failed to load timeout data.')}${hint}</p></div>`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!timeouts.configured) {
|
|
173
|
+
return `<div class="placeholder"><h2>Timeouts</h2><p>No <code>timeouts</code> configured in <code>agentxchain.json</code>. Add a <code>timeouts</code> section to enable time-limit enforcement.</p></div>`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let html = `<div class="timeouts-view">`;
|
|
177
|
+
|
|
178
|
+
// Header
|
|
179
|
+
html += `<div class="run-header"><div class="run-meta">`;
|
|
180
|
+
html += `<span class="phase-label"><strong>Timeouts</strong></span>`;
|
|
181
|
+
html += badge('configured', 'var(--green)');
|
|
182
|
+
const eventCount = Array.isArray(timeouts.events) ? timeouts.events.length : 0;
|
|
183
|
+
if (eventCount > 0) {
|
|
184
|
+
html += `<span class="turn-count">${eventCount} event${eventCount !== 1 ? 's' : ''} recorded</span>`;
|
|
185
|
+
}
|
|
186
|
+
html += `</div></div>`;
|
|
187
|
+
|
|
188
|
+
// Config summary
|
|
189
|
+
html += renderConfigTable(timeouts.config);
|
|
190
|
+
|
|
191
|
+
// Live pressure
|
|
192
|
+
if (timeouts.live) {
|
|
193
|
+
html += renderLivePressure(timeouts.live);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Persisted events
|
|
197
|
+
html += renderEvents(timeouts.events);
|
|
198
|
+
|
|
199
|
+
html += `</div>`;
|
|
200
|
+
return html;
|
|
201
|
+
}
|
package/dashboard/index.html
CHANGED
|
@@ -384,6 +384,8 @@
|
|
|
384
384
|
<a href="#blockers">Blockers</a>
|
|
385
385
|
<a href="#artifacts">Artifacts</a>
|
|
386
386
|
<a href="#run-history">Run History</a>
|
|
387
|
+
<a href="#timeouts">Timeouts</a>
|
|
388
|
+
<a href="#coordinator-timeouts">Coordinator Timeouts</a>
|
|
387
389
|
</nav>
|
|
388
390
|
<main id="view-container">
|
|
389
391
|
<div class="placeholder">
|
package/package.json
CHANGED
|
@@ -16,10 +16,35 @@ cleanup() {
|
|
|
16
16
|
trap cleanup EXIT
|
|
17
17
|
|
|
18
18
|
usage() {
|
|
19
|
-
echo "Usage: bash scripts/publish-from-tag.sh <vX.Y.Z>" >&2
|
|
19
|
+
echo "Usage: bash scripts/publish-from-tag.sh [--skip-preflight] <vX.Y.Z>" >&2
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
SKIP_PREFLIGHT=0
|
|
23
|
+
TAG=""
|
|
24
|
+
|
|
25
|
+
while [[ $# -gt 0 ]]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
--skip-preflight)
|
|
28
|
+
SKIP_PREFLIGHT=1
|
|
29
|
+
shift
|
|
30
|
+
;;
|
|
31
|
+
-*)
|
|
32
|
+
echo "Error: unknown option '$1'" >&2
|
|
33
|
+
usage
|
|
34
|
+
exit 1
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
if [[ -n "$TAG" ]]; then
|
|
38
|
+
echo "Error: release tag must be provided exactly once" >&2
|
|
39
|
+
usage
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
TAG="$1"
|
|
43
|
+
shift
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
23
48
|
if [[ -z "$TAG" ]]; then
|
|
24
49
|
echo "Error: release tag is required" >&2
|
|
25
50
|
usage
|
|
@@ -55,8 +80,12 @@ if ! [[ "$RETRY_DELAY_SECONDS" =~ ^[0-9]+$ ]]; then
|
|
|
55
80
|
fi
|
|
56
81
|
|
|
57
82
|
echo "Publishing ${PACKAGE_NAME}@${RELEASE_VERSION} from ${TAG}"
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
|
|
84
|
+
echo "Skipping strict release preflight because the caller already owns tagged-state verification."
|
|
85
|
+
else
|
|
86
|
+
echo "Running strict release preflight..."
|
|
87
|
+
bash scripts/release-preflight.sh --strict --target-version "${RELEASE_VERSION}"
|
|
88
|
+
fi
|
|
60
89
|
|
|
61
90
|
EXISTING_VERSION="$(npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version 2>/dev/null || true)"
|
|
62
91
|
if [[ "$EXISTING_VERSION" == "$RELEASE_VERSION" ]]; then
|
package/src/commands/history.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { resolve } from 'path';
|
|
8
8
|
import { existsSync, readFileSync } from 'fs';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
-
import { queryRunHistory } from '../lib/run-history.js';
|
|
10
|
+
import { queryRunHistory, queryRunLineage } from '../lib/run-history.js';
|
|
11
|
+
import { getRunTriggerLabel, summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @param {object} opts - { json?: boolean, limit?: number, status?: string, dir?: string }
|
|
@@ -19,6 +20,42 @@ export async function historyCommand(opts) {
|
|
|
19
20
|
process.exit(1);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// ── Lineage mode ─────────────────────────────────────────────────────────
|
|
24
|
+
if (opts.lineage) {
|
|
25
|
+
const result = queryRunLineage(root, opts.lineage);
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
console.error(chalk.red(result.error));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.log(JSON.stringify(result.chain, null, 2));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(chalk.bold(`Run Lineage for ${opts.lineage}:`));
|
|
37
|
+
result.chain.forEach((entry, i) => {
|
|
38
|
+
if (entry.broken_link) {
|
|
39
|
+
const prefix = i === 0 ? ' ' : ' └─ ';
|
|
40
|
+
console.log(chalk.red(`${prefix}[broken link: ${entry.missing_run_id}]`));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const runId = (entry.run_id || '—').slice(0, 12);
|
|
44
|
+
const status = formatStatus(entry.status);
|
|
45
|
+
const phases = (entry.phases_completed || []).join(',') || '—';
|
|
46
|
+
const turns = `${entry.total_turns || 0} turns`;
|
|
47
|
+
const cost = entry.total_cost_usd != null ? `$${entry.total_cost_usd.toFixed(2)}` : '';
|
|
48
|
+
const trigger = getRunTriggerLabel(entry.provenance);
|
|
49
|
+
const parentNote = entry.provenance?.parent_run_id
|
|
50
|
+
? ` from ${entry.provenance.parent_run_id.slice(0, 12)}`
|
|
51
|
+
: '';
|
|
52
|
+
const prefix = i === 0 ? ' ' : ' └─ ';
|
|
53
|
+
console.log(`${prefix}${runId} ${status} ${pad(phases, 20)} ${pad(turns, 10)} ${pad(cost, 8)} (${trigger}${parentNote})`);
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Standard history view ────────────────────────────────────────────────
|
|
22
59
|
const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
|
|
23
60
|
const entries = queryRunHistory(root, {
|
|
24
61
|
limit,
|
|
@@ -43,6 +80,7 @@ export async function historyCommand(opts) {
|
|
|
43
80
|
pad('#', 4),
|
|
44
81
|
pad('Run ID', 14),
|
|
45
82
|
pad('Status', 11),
|
|
83
|
+
pad('Trigger', 14),
|
|
46
84
|
pad('Phases', 8),
|
|
47
85
|
pad('Turns', 6),
|
|
48
86
|
pad('Cost', 10),
|
|
@@ -57,6 +95,7 @@ export async function historyCommand(opts) {
|
|
|
57
95
|
const idx = String(i + 1);
|
|
58
96
|
const runId = (entry.run_id || '—').slice(0, 12);
|
|
59
97
|
const status = formatStatus(entry.status);
|
|
98
|
+
const trigger = getRunTriggerLabel(entry.provenance);
|
|
60
99
|
const phases = String(entry.phases_completed?.length || 0);
|
|
61
100
|
const turns = String(entry.total_turns || 0);
|
|
62
101
|
const cost = entry.total_cost_usd != null
|
|
@@ -73,6 +112,7 @@ export async function historyCommand(opts) {
|
|
|
73
112
|
pad(idx, 4),
|
|
74
113
|
pad(runId, 14),
|
|
75
114
|
pad(status, 11),
|
|
115
|
+
pad(trigger, 14),
|
|
76
116
|
pad(phases, 8),
|
|
77
117
|
pad(turns, 6),
|
|
78
118
|
pad(cost, 10),
|
package/src/commands/init.js
CHANGED
|
@@ -692,6 +692,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
692
692
|
escalation: null,
|
|
693
693
|
queued_phase_transition: null,
|
|
694
694
|
queued_run_completion: null,
|
|
695
|
+
last_gate_failure: null,
|
|
695
696
|
phase_gate_status: phaseGateStatus,
|
|
696
697
|
budget_reservations: {},
|
|
697
698
|
budget_status: {
|
package/src/commands/migrate.js
CHANGED
|
@@ -218,6 +218,7 @@ export async function migrateCommand(opts) {
|
|
|
218
218
|
escalation: null,
|
|
219
219
|
queued_phase_transition: null,
|
|
220
220
|
queued_run_completion: null,
|
|
221
|
+
last_gate_failure: null,
|
|
221
222
|
phase_gate_status: {
|
|
222
223
|
planning_signoff: inferredPhase === 'planning' ? 'pending' : 'passed',
|
|
223
224
|
implementation_complete: inferredPhase === 'qa' ? 'passed' : 'pending',
|