agentxchain 2.45.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/accept-turn.js +30 -0
- package/src/commands/init.js +8 -1
- 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 +35 -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 +530 -25
- package/src/lib/governed-templates.js +2 -0
- package/src/lib/normalized-config.js +132 -0
- package/src/lib/policy-evaluator.js +330 -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/src/templates/governed/enterprise-app.json +20 -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
|
|
@@ -23,6 +23,36 @@ export async function acceptTurnCommand(opts = {}) {
|
|
|
23
23
|
resolutionMode: opts.resolution || 'standard',
|
|
24
24
|
});
|
|
25
25
|
if (!result.ok) {
|
|
26
|
+
if (result.error_code === 'policy_escalation' || result.error_code === 'policy_violation') {
|
|
27
|
+
const recovery = result.state ? deriveRecoveryDescriptor(result.state, config) : null;
|
|
28
|
+
const retainedTurnId = result.state?.blocked_reason?.turn_id || opts.turn || '(unknown)';
|
|
29
|
+
const policyTitle = result.error_code === 'policy_escalation'
|
|
30
|
+
? 'Turn Acceptance Escalated By Policy'
|
|
31
|
+
: 'Turn Acceptance Blocked By Policy';
|
|
32
|
+
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.yellow(` ${policyTitle}`));
|
|
35
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(` ${chalk.dim('Turn:')} ${retainedTurnId}`);
|
|
38
|
+
console.log(` ${chalk.dim('Error:')} ${result.error}`);
|
|
39
|
+
const violations = Array.isArray(result.policy_violations) ? result.policy_violations : [];
|
|
40
|
+
for (const violation of violations) {
|
|
41
|
+
console.log(` ${chalk.dim('Policy:')} ${violation.policy_id} (${violation.rule})`);
|
|
42
|
+
console.log(` ${chalk.dim('Detail:')} ${violation.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (recovery) {
|
|
45
|
+
console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
|
|
46
|
+
console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
|
|
47
|
+
console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
|
|
48
|
+
console.log(` ${chalk.dim('Turn:')} ${recovery.turn_retained ? 'retained' : 'cleared'}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(` ${chalk.dim('Action:')} Fix the policy condition, then rerun agentxchain accept-turn`);
|
|
51
|
+
}
|
|
52
|
+
console.log('');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
|
|
27
57
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
28
58
|
const activeTurn = result.state?.current_turn;
|
package/src/commands/init.js
CHANGED
|
@@ -547,11 +547,14 @@ function buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitC
|
|
|
547
547
|
Object.keys(roles).map((roleId) => [roleId, `.agentxchain/prompts/${roleId}.md`])
|
|
548
548
|
);
|
|
549
549
|
|
|
550
|
+
const policies = cloneJsonCompatible(blueprint?.policies || []);
|
|
551
|
+
|
|
550
552
|
return {
|
|
551
553
|
roles,
|
|
552
554
|
runtimes,
|
|
553
555
|
routing,
|
|
554
556
|
gates,
|
|
557
|
+
policies,
|
|
555
558
|
prompts,
|
|
556
559
|
workflowKitConfig: effectiveWorkflowKitConfig,
|
|
557
560
|
};
|
|
@@ -627,7 +630,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
627
630
|
const template = loadGovernedTemplate(templateId);
|
|
628
631
|
const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
|
|
629
632
|
const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
|
|
630
|
-
const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
|
|
633
|
+
const { roles, runtimes, routing, gates, policies, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
|
|
631
634
|
const scaffoldWorkflowKitConfig = effectiveWorkflowKitConfig
|
|
632
635
|
? normalizeWorkflowKit(effectiveWorkflowKitConfig, Object.keys(routing))
|
|
633
636
|
: null;
|
|
@@ -667,6 +670,9 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
667
670
|
max_deadlock_cycles: 2
|
|
668
671
|
}
|
|
669
672
|
};
|
|
673
|
+
if (policies && policies.length > 0) {
|
|
674
|
+
config.policies = policies;
|
|
675
|
+
}
|
|
670
676
|
if (effectiveWorkflowKitConfig) {
|
|
671
677
|
config.workflow_kit = effectiveWorkflowKitConfig;
|
|
672
678
|
}
|
|
@@ -686,6 +692,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
686
692
|
escalation: null,
|
|
687
693
|
queued_phase_transition: null,
|
|
688
694
|
queued_run_completion: null,
|
|
695
|
+
last_gate_failure: null,
|
|
689
696
|
phase_gate_status: phaseGateStatus,
|
|
690
697
|
budget_reservations: {},
|
|
691
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',
|