agentxchain 2.46.0 → 2.46.2
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/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/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/status.js +49 -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/governed-state.js +373 -25
- 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 +323 -6
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
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/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',
|
package/src/commands/status.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/govern
|
|
|
5
5
|
import { getContinuityStatus } from '../lib/continuity-status.js';
|
|
6
6
|
import { getConnectorHealth } from '../lib/connector-health.js';
|
|
7
7
|
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
8
|
+
import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
|
|
8
9
|
|
|
9
10
|
export async function statusCommand(opts) {
|
|
10
11
|
const context = loadProjectContext();
|
|
@@ -192,6 +193,10 @@ function renderGovernedStatus(context, opts) {
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
196
|
+
if (state?.last_gate_failure) {
|
|
197
|
+
renderLastGateFailure(state.last_gate_failure, config);
|
|
198
|
+
}
|
|
199
|
+
|
|
195
200
|
const recovery = deriveRecoveryDescriptor(state, config);
|
|
196
201
|
if (recovery) {
|
|
197
202
|
console.log('');
|
|
@@ -237,6 +242,25 @@ function renderGovernedStatus(context, opts) {
|
|
|
237
242
|
|
|
238
243
|
renderWorkflowKitArtifactsSection(workflowKitArtifacts);
|
|
239
244
|
|
|
245
|
+
if (config.timeouts && state?.status === 'active') {
|
|
246
|
+
const activeTurn = getActiveTurn(state);
|
|
247
|
+
const turnResult = activeTurn ? { role: activeTurn.assigned_role } : undefined;
|
|
248
|
+
const timeoutEval = evaluateTimeouts({ config, state, turn: activeTurn, turnResult, now: new Date().toISOString() });
|
|
249
|
+
const allItems = [...timeoutEval.exceeded, ...timeoutEval.warnings];
|
|
250
|
+
if (allItems.length > 0) {
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(` ${chalk.dim('Timeouts:')}`);
|
|
253
|
+
for (const item of allItems) {
|
|
254
|
+
const isExceeded = timeoutEval.exceeded.includes(item);
|
|
255
|
+
const elapsed = item.elapsed_minutes != null ? `${item.elapsed_minutes}m` : '?';
|
|
256
|
+
const limit = item.limit_minutes != null ? `${item.limit_minutes}m` : '?';
|
|
257
|
+
const icon = isExceeded ? chalk.red('⚠') : chalk.yellow('◷');
|
|
258
|
+
const label = isExceeded ? chalk.red(`EXCEEDED ${item.scope}`) : chalk.yellow(`${item.scope}`);
|
|
259
|
+
console.log(` ${icon} ${label}: ${elapsed}/${limit} (action: ${item.action || 'n/a'})`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
240
264
|
if (state?.budget_status) {
|
|
241
265
|
console.log('');
|
|
242
266
|
console.log(` ${chalk.dim('Budget:')} spent $${formatUsd(state.budget_status.spent_usd)} / remaining $${formatUsd(state.budget_status.remaining_usd)}`);
|
|
@@ -363,6 +387,31 @@ function renderWorkflowKitArtifactsSection(wkData) {
|
|
|
363
387
|
}
|
|
364
388
|
}
|
|
365
389
|
|
|
390
|
+
function renderLastGateFailure(failure, config) {
|
|
391
|
+
const entryRole = config?.routing?.[failure.phase]?.entry_role || null;
|
|
392
|
+
const suggestedCommand = entryRole ? `agentxchain assign ${entryRole}` : 'agentxchain assign <role>';
|
|
393
|
+
const requestLabel = failure.gate_type === 'run_completion'
|
|
394
|
+
? 'Run completion'
|
|
395
|
+
: `${failure.from_phase || failure.phase} -> ${failure.to_phase || 'unknown'}`;
|
|
396
|
+
|
|
397
|
+
console.log('');
|
|
398
|
+
console.log(` ${chalk.dim('Gate fail:')} ${chalk.red.bold(failure.gate_type === 'run_completion' ? 'RUN COMPLETION' : 'PHASE TRANSITION')}`);
|
|
399
|
+
console.log(` ${chalk.dim('Gate:')} ${failure.gate_id || 'unknown'}`);
|
|
400
|
+
console.log(` ${chalk.dim('Request:')} ${requestLabel}`);
|
|
401
|
+
console.log(` ${chalk.dim('Source:')} ${failure.queued_request ? 'queued drain request' : 'direct request'}`);
|
|
402
|
+
console.log(` ${chalk.dim('When:')} ${failure.failed_at || 'unknown'}`);
|
|
403
|
+
if (failure.requested_by_turn) {
|
|
404
|
+
console.log(` ${chalk.dim('Turn:')} ${failure.requested_by_turn}`);
|
|
405
|
+
}
|
|
406
|
+
if (Array.isArray(failure.reasons) && failure.reasons.length > 0) {
|
|
407
|
+
console.log(` ${chalk.dim('Reasons:')}`);
|
|
408
|
+
for (const reason of failure.reasons) {
|
|
409
|
+
console.log(` ${chalk.red('•')} ${reason}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
console.log(` ${chalk.dim('Action:')} ${chalk.cyan(suggestedCommand)} to keep working in ${failure.phase}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
366
415
|
function formatPhase(phase) {
|
|
367
416
|
const colors = { discovery: chalk.blue, build: chalk.green, qa: chalk.yellow, deploy: chalk.magenta, blocked: chalk.red };
|
|
368
417
|
return (colors[phase] || chalk.white)(phase);
|