agentxchain 2.108.0 → 2.110.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 +2 -0
- package/dashboard/app.js +44 -4
- package/dashboard/components/blocked.js +57 -0
- package/dashboard/components/gate.js +52 -0
- package/dashboard/components/timeouts.js +21 -3
- package/package.json +1 -1
- package/src/commands/approve-completion.js +45 -1
- package/src/commands/approve-transition.js +49 -1
- package/src/commands/status.js +50 -4
- package/src/commands/step.js +2 -0
- package/src/lib/dashboard/bridge-server.js +49 -0
- package/src/lib/dashboard/gate-action-reader.js +58 -0
- package/src/lib/dashboard/timeout-status.js +47 -18
- package/src/lib/gate-actions.js +263 -0
- package/src/lib/governed-state.js +161 -3
- package/src/lib/normalized-config.js +6 -1
- package/src/lib/notification-runner.js +162 -6
- package/src/lib/report.js +50 -0
- package/src/lib/run-loop.js +3 -0
package/bin/agentxchain.js
CHANGED
|
@@ -521,11 +521,13 @@ program
|
|
|
521
521
|
program
|
|
522
522
|
.command('approve-transition')
|
|
523
523
|
.description('Approve a pending phase transition that requires human sign-off')
|
|
524
|
+
.option('--dry-run', 'Show configured gate actions without executing approval')
|
|
524
525
|
.action(approveTransitionCommand);
|
|
525
526
|
|
|
526
527
|
program
|
|
527
528
|
.command('approve-completion')
|
|
528
529
|
.description('Approve a pending run completion that requires human sign-off')
|
|
530
|
+
.option('--dry-run', 'Show configured gate actions without executing approval')
|
|
529
531
|
.action(approveCompletionCommand);
|
|
530
532
|
|
|
531
533
|
program
|
package/dashboard/app.js
CHANGED
|
@@ -29,8 +29,8 @@ const VIEWS = {
|
|
|
29
29
|
delegations: { fetch: ['state', 'history'], render: renderDelegations },
|
|
30
30
|
ledger: { fetch: ['state', 'ledger', 'coordinatorState', 'coordinatorLedger', 'repoDecisionsSummary'], render: renderLedger },
|
|
31
31
|
hooks: { fetch: ['audit', 'annotations', 'coordinatorAudit', 'coordinatorAnnotations'], render: renderHooks },
|
|
32
|
-
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit', 'coordinatorBlockers', 'coordinatorRepoStatusRows'], render: renderBlocked },
|
|
33
|
-
gate: { fetch: ['state', 'history', 'coordinatorState', 'coordinatorHistory', 'coordinatorBarriers'], render: renderGate },
|
|
32
|
+
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit', 'coordinatorBlockers', 'coordinatorRepoStatusRows', 'gateActions'], render: renderBlocked },
|
|
33
|
+
gate: { fetch: ['state', 'history', 'coordinatorState', 'coordinatorHistory', 'coordinatorBarriers', 'gateActions'], render: renderGate },
|
|
34
34
|
initiative: { fetch: ['coordinatorState', 'coordinatorBarriers', 'barrierLedger', 'coordinatorBlockers', 'coordinatorRepoStatusRows'], render: renderInitiative },
|
|
35
35
|
'cross-repo': { fetch: ['coordinatorState', 'coordinatorHistory'], render: renderCrossRepo },
|
|
36
36
|
blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
|
|
@@ -62,6 +62,7 @@ const API_MAP = {
|
|
|
62
62
|
runHistory: '/api/run-history',
|
|
63
63
|
timeouts: '/api/timeouts',
|
|
64
64
|
coordinatorTimeouts: '/api/coordinator/timeouts',
|
|
65
|
+
gateActions: '/api/gate-actions',
|
|
65
66
|
events: '/api/events?type=turn_conflicted&limit=10',
|
|
66
67
|
};
|
|
67
68
|
|
|
@@ -79,11 +80,14 @@ const viewState = {
|
|
|
79
80
|
hookName: 'all',
|
|
80
81
|
},
|
|
81
82
|
};
|
|
83
|
+
const DASHBOARD_POLL_INTERVAL_MS = 60 * 1000;
|
|
82
84
|
|
|
83
85
|
let activeViewName = null;
|
|
84
86
|
let activeViewData = null;
|
|
85
87
|
let dashboardSession = null;
|
|
86
88
|
let actionInFlight = false;
|
|
89
|
+
let pollInFlight = false;
|
|
90
|
+
let pollTimer = null;
|
|
87
91
|
const liveObserverState = {
|
|
88
92
|
connected: false,
|
|
89
93
|
lastRefreshAt: null,
|
|
@@ -146,7 +150,7 @@ async function pickInitialView() {
|
|
|
146
150
|
}
|
|
147
151
|
|
|
148
152
|
function buildRenderData(viewName, data) {
|
|
149
|
-
const liveMeta = viewName === 'timeline'
|
|
153
|
+
const liveMeta = (viewName === 'timeline' || viewName === 'timeouts')
|
|
150
154
|
? buildLiveMeta({
|
|
151
155
|
connected: liveObserverState.connected,
|
|
152
156
|
lastRefreshAt: liveObserverState.lastRefreshAt,
|
|
@@ -274,6 +278,39 @@ function rerenderActiveView() {
|
|
|
274
278
|
renderView(activeViewName, activeViewData);
|
|
275
279
|
}
|
|
276
280
|
|
|
281
|
+
async function pollDashboard({ refreshView = false } = {}) {
|
|
282
|
+
if (pollInFlight) return;
|
|
283
|
+
pollInFlight = true;
|
|
284
|
+
try {
|
|
285
|
+
await fetch('/api/poll', { cache: 'no-store' });
|
|
286
|
+
if (refreshView) {
|
|
287
|
+
await loadView(currentView());
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// Best-effort heartbeat only
|
|
291
|
+
} finally {
|
|
292
|
+
pollInFlight = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function startDashboardPolling() {
|
|
297
|
+
if (pollTimer) return;
|
|
298
|
+
|
|
299
|
+
const tick = () => {
|
|
300
|
+
if (document.visibilityState === 'hidden') return;
|
|
301
|
+
if (actionInFlight) return;
|
|
302
|
+
void pollDashboard({ refreshView: true });
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
pollTimer = setInterval(tick, DASHBOARD_POLL_INTERVAL_MS);
|
|
306
|
+
|
|
307
|
+
document.addEventListener('visibilitychange', () => {
|
|
308
|
+
if (document.visibilityState === 'visible') {
|
|
309
|
+
void pollDashboard({ refreshView: true });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
277
314
|
// ── WebSocket connection ──────────────────────────────────────────────────
|
|
278
315
|
|
|
279
316
|
let ws = null;
|
|
@@ -291,7 +328,9 @@ function connect() {
|
|
|
291
328
|
statusLabel.textContent = 'Connected';
|
|
292
329
|
reconnectDelay = 1000;
|
|
293
330
|
liveObserverState.connected = true;
|
|
294
|
-
|
|
331
|
+
void pollDashboard().finally(() => {
|
|
332
|
+
loadView(currentView());
|
|
333
|
+
});
|
|
295
334
|
};
|
|
296
335
|
|
|
297
336
|
ws.onmessage = (event) => {
|
|
@@ -501,5 +540,6 @@ function fallbackSelect(el) {
|
|
|
501
540
|
|
|
502
541
|
Promise.all([pickInitialView(), loadSession()]).finally(() => {
|
|
503
542
|
updateNav();
|
|
543
|
+
startDashboardPolling();
|
|
504
544
|
connect();
|
|
505
545
|
});
|
|
@@ -92,6 +92,56 @@ function formatCoordinatorRepoCardMeta(row) {
|
|
|
92
92
|
return parts.join(' | ') || '-';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function getGateActionDryRunCommand(gateActions, state) {
|
|
96
|
+
const gateType = gateActions?.latest_attempt?.gate_type
|
|
97
|
+
|| (state?.pending_run_completion ? 'run_completion' : null)
|
|
98
|
+
|| (state?.pending_phase_transition ? 'phase_transition' : null);
|
|
99
|
+
|
|
100
|
+
return gateType === 'run_completion'
|
|
101
|
+
? 'agentxchain approve-completion --dry-run'
|
|
102
|
+
: 'agentxchain approve-transition --dry-run';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderGateActionFailure(gateActions, state) {
|
|
106
|
+
if (!gateActions?.latest_attempt || gateActions.latest_attempt.status !== 'failed') return '';
|
|
107
|
+
|
|
108
|
+
const attempt = gateActions.latest_attempt;
|
|
109
|
+
const actions = Array.isArray(attempt.actions) ? attempt.actions : [];
|
|
110
|
+
const dryRunCommand = getGateActionDryRunCommand(gateActions, state);
|
|
111
|
+
|
|
112
|
+
let html = `<div class="section"><h3>Gate Action Failure</h3>`;
|
|
113
|
+
html += `<dl class="detail-list">`;
|
|
114
|
+
html += `<dt>Attempt</dt><dd class="mono">${esc(attempt.attempt_id || '-')}</dd>`;
|
|
115
|
+
html += `<dt>Gate</dt><dd class="mono">${esc(attempt.gate_id || '-')}</dd>`;
|
|
116
|
+
html += `<dt>Attempted At</dt><dd class="mono">${esc(attempt.attempted_at || '-')}</dd>`;
|
|
117
|
+
html += `</dl>`;
|
|
118
|
+
|
|
119
|
+
if (actions.length > 0) {
|
|
120
|
+
html += `<div class="annotation-list">`;
|
|
121
|
+
for (const action of actions) {
|
|
122
|
+
const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
|
|
123
|
+
const outcome = action.status === 'failed'
|
|
124
|
+
? (action.timed_out ? `⏱ timed out after ${action.timeout_ms}ms` : '❌ failed')
|
|
125
|
+
: '✅ succeeded';
|
|
126
|
+
const exitStr = action.timed_out ? '' : (action.exit_code != null ? ` (exit ${action.exit_code})` : '');
|
|
127
|
+
html += `<div class="annotation-card">
|
|
128
|
+
<span class="mono">${esc(String(action.action_index || '?'))}.</span>
|
|
129
|
+
<span>${esc(label)}</span>
|
|
130
|
+
<span>${esc(outcome)}${esc(exitStr)}</span>
|
|
131
|
+
</div>`;
|
|
132
|
+
if (action.status === 'failed' && action.stderr_tail) {
|
|
133
|
+
html += `<pre class="recovery-command mono">${esc(action.stderr_tail)}</pre>`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
html += `</div>`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
html += `<p class="recovery-hint">Re-run with dry-run first:</p>`;
|
|
140
|
+
html += `<pre class="recovery-command mono" data-copy="${esc(dryRunCommand)}">${esc(dryRunCommand)}</pre>`;
|
|
141
|
+
html += `</div>`;
|
|
142
|
+
return html;
|
|
143
|
+
}
|
|
144
|
+
|
|
95
145
|
export function render({
|
|
96
146
|
state,
|
|
97
147
|
audit = [],
|
|
@@ -99,6 +149,7 @@ export function render({
|
|
|
99
149
|
coordinatorAudit = [],
|
|
100
150
|
coordinatorBlockers = null,
|
|
101
151
|
coordinatorRepoStatusRows = null,
|
|
152
|
+
gateActions = null,
|
|
102
153
|
}) {
|
|
103
154
|
const activeState = state?.status === 'blocked' ? state : coordinatorState;
|
|
104
155
|
const activeAudit = activeState === state ? audit : coordinatorAudit;
|
|
@@ -168,6 +219,12 @@ export function render({
|
|
|
168
219
|
</div>`;
|
|
169
220
|
}
|
|
170
221
|
|
|
222
|
+
// Gate-action failure detail (only for gate_action_failed blocks)
|
|
223
|
+
const category = String(reason).toLowerCase();
|
|
224
|
+
if (category.includes('gate_action_failed') && !isCoordinator) {
|
|
225
|
+
html += renderGateActionFailure(gateActions, activeState);
|
|
226
|
+
}
|
|
227
|
+
|
|
171
228
|
if (runtimeGuidance.length > 0) {
|
|
172
229
|
html += `<div class="section"><h3>Runtime Guidance</h3><div class="annotation-list">`;
|
|
173
230
|
for (const entry of runtimeGuidance) {
|
|
@@ -210,12 +210,58 @@ function aggregateCoordinatorEvidence(entries) {
|
|
|
210
210
|
return { summaries, decisions, objections: [], risks: [], files };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function renderGateActionsSection(gateActions) {
|
|
214
|
+
if (!gateActions) return '';
|
|
215
|
+
|
|
216
|
+
const configured = Array.isArray(gateActions.configured) ? gateActions.configured : [];
|
|
217
|
+
const attempt = gateActions.latest_attempt || null;
|
|
218
|
+
|
|
219
|
+
if (configured.length === 0 && !attempt) return '';
|
|
220
|
+
|
|
221
|
+
let html = `<div class="gate-support"><p><strong>Gate Actions:</strong></p>`;
|
|
222
|
+
|
|
223
|
+
if (configured.length > 0) {
|
|
224
|
+
html += `<ul>`;
|
|
225
|
+
for (const action of configured) {
|
|
226
|
+
const label = action.label || action.run || `action ${action.index || '?'}`;
|
|
227
|
+
html += `<li><div><span class="mono">${esc(String(action.index || '?'))}.</span> ${esc(label)}</div>`;
|
|
228
|
+
if (action.run) {
|
|
229
|
+
html += `<div class="mono">${esc(action.run)}</div>`;
|
|
230
|
+
}
|
|
231
|
+
html += `</li>`;
|
|
232
|
+
}
|
|
233
|
+
html += `</ul>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (attempt) {
|
|
237
|
+
const statusLabel = attempt.status === 'failed' ? '❌ Failed' : '✅ Succeeded';
|
|
238
|
+
html += `<p><strong>Last Attempt:</strong> ${esc(statusLabel)} at ${esc(attempt.attempted_at || 'unknown')}</p>`;
|
|
239
|
+
if (Array.isArray(attempt.actions) && attempt.actions.length > 0) {
|
|
240
|
+
html += `<ul>`;
|
|
241
|
+
for (const a of attempt.actions) {
|
|
242
|
+
const aLabel = a.action_label || a.command || `action ${a.action_index || '?'}`;
|
|
243
|
+
const outcome = a.status === 'failed'
|
|
244
|
+
? (a.timed_out ? '⏱' : '❌')
|
|
245
|
+
: '✅';
|
|
246
|
+
const timeoutStr = a.timed_out ? ` timed out after ${a.timeout_ms}ms` : '';
|
|
247
|
+
const exitStr = a.timed_out ? '' : (a.exit_code != null ? ` (exit ${a.exit_code})` : '');
|
|
248
|
+
html += `<li>${outcome} ${esc(aLabel)}${esc(exitStr)}${esc(timeoutStr)}</li>`;
|
|
249
|
+
}
|
|
250
|
+
html += `</ul>`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
html += `</div>`;
|
|
255
|
+
return html;
|
|
256
|
+
}
|
|
257
|
+
|
|
213
258
|
export function render({
|
|
214
259
|
state,
|
|
215
260
|
history = [],
|
|
216
261
|
coordinatorState = null,
|
|
217
262
|
coordinatorHistory = [],
|
|
218
263
|
coordinatorBarriers = {},
|
|
264
|
+
gateActions = null,
|
|
219
265
|
}) {
|
|
220
266
|
const repoPendingTransition = state?.pending_phase_transition || null;
|
|
221
267
|
const repoPendingCompletion = state?.pending_run_completion || null;
|
|
@@ -284,6 +330,9 @@ export function render({
|
|
|
284
330
|
)).join('')}</ul></div>`;
|
|
285
331
|
}
|
|
286
332
|
}
|
|
333
|
+
if (!isCoordinator) {
|
|
334
|
+
html += renderGateActionsSection(gateActions);
|
|
335
|
+
}
|
|
287
336
|
html += renderApproveControls({
|
|
288
337
|
buttonLabel: isCoordinator ? 'Approve Coordinator Gate' : 'Approve Transition',
|
|
289
338
|
cliCommand: isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-transition',
|
|
@@ -329,6 +378,9 @@ export function render({
|
|
|
329
378
|
if (evidence.files.length > 0) {
|
|
330
379
|
html += `<div class="gate-support"><p><strong>Files Changed:</strong></p><ul>${evidence.files.map(f => `<li class="mono">${esc(f)}</li>`).join('')}</ul></div>`;
|
|
331
380
|
}
|
|
381
|
+
if (!isCoordinator) {
|
|
382
|
+
html += renderGateActionsSection(gateActions);
|
|
383
|
+
}
|
|
332
384
|
html += renderApproveControls({
|
|
333
385
|
buttonLabel: isCoordinator ? 'Approve Coordinator Gate' : 'Approve Completion',
|
|
334
386
|
cliCommand: isCoordinator ? 'agentxchain multi approve-gate' : 'agentxchain approve-completion',
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* Pure render function: takes data from /api/timeouts, returns HTML.
|
|
5
5
|
* All evaluation is server-side. This view renders the snapshot.
|
|
6
6
|
*
|
|
7
|
+
* Receives liveMeta from the live-observer for websocket freshness display.
|
|
8
|
+
*
|
|
7
9
|
* See: TIMEOUT_DASHBOARD_SURFACE_SPEC.md
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
import { renderLiveStatus } from './live-status.js';
|
|
13
|
+
|
|
10
14
|
function esc(str) {
|
|
11
15
|
if (!str) return '';
|
|
12
16
|
return String(str)
|
|
@@ -82,14 +86,22 @@ function renderConfigTable(config) {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
function renderLivePressure(live) {
|
|
89
|
+
const liveContext = live?.context || null;
|
|
85
90
|
const hasExceeded = live.exceeded && live.exceeded.length > 0;
|
|
86
91
|
const hasWarnings = live.warnings && live.warnings.length > 0;
|
|
92
|
+
const approvalNote = liveContext?.awaiting_approval
|
|
93
|
+
? `<p style="color:var(--yellow)">Approval wait does not mutate timeout state, but phase/run clocks continue until the next accepted turn.${liveContext.requested_at ? ` Requested: <code>${esc(liveContext.requested_at)}</code>.` : ''}</p>`
|
|
94
|
+
: '';
|
|
87
95
|
|
|
88
96
|
if (!hasExceeded && !hasWarnings) {
|
|
89
|
-
|
|
97
|
+
const message = liveContext?.awaiting_approval
|
|
98
|
+
? `<p style="color:var(--text-dim)">No current phase/run timeout pressure during this approval wait.</p>`
|
|
99
|
+
: `<p style="color:var(--green)">No timeouts exceeded or approaching limits.</p>`;
|
|
100
|
+
return `<div class="section"><h3>Live Pressure</h3>${approvalNote}${message}</div>`;
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
let html = `<div class="section"><h3>Live Pressure</h3>
|
|
104
|
+
${approvalNote}
|
|
93
105
|
<table class="data-table">
|
|
94
106
|
<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
107
|
<tbody>`;
|
|
@@ -157,7 +169,7 @@ function renderEvents(events) {
|
|
|
157
169
|
return html;
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
export function render({ timeouts }) {
|
|
172
|
+
export function render({ timeouts, liveMeta }) {
|
|
161
173
|
if (!timeouts) {
|
|
162
174
|
return `<div class="placeholder"><h2>Timeouts</h2><p>No timeout data available. Ensure a governed run is active.</p></div>`;
|
|
163
175
|
}
|
|
@@ -175,6 +187,9 @@ export function render({ timeouts }) {
|
|
|
175
187
|
|
|
176
188
|
let html = `<div class="timeouts-view">`;
|
|
177
189
|
|
|
190
|
+
// Freshness banner — timeout data is time-sensitive so operators need visibility
|
|
191
|
+
html += renderLiveStatus(liveMeta);
|
|
192
|
+
|
|
178
193
|
// Header
|
|
179
194
|
html += `<div class="run-header"><div class="run-meta">`;
|
|
180
195
|
html += `<span class="phase-label"><strong>Timeouts</strong></span>`;
|
|
@@ -190,7 +205,10 @@ export function render({ timeouts }) {
|
|
|
190
205
|
|
|
191
206
|
// Live pressure
|
|
192
207
|
if (timeouts.live) {
|
|
193
|
-
html += renderLivePressure(
|
|
208
|
+
html += renderLivePressure({
|
|
209
|
+
...timeouts.live,
|
|
210
|
+
context: timeouts.live_context || null,
|
|
211
|
+
});
|
|
194
212
|
}
|
|
195
213
|
|
|
196
214
|
// Persisted events
|
package/package.json
CHANGED
|
@@ -37,11 +37,31 @@ export async function approveCompletionCommand(opts) {
|
|
|
37
37
|
console.log(` ${chalk.dim('Turn:')} ${pc.requested_by_turn}`);
|
|
38
38
|
console.log('');
|
|
39
39
|
|
|
40
|
-
const result = approveRunCompletion(root, config);
|
|
40
|
+
const result = approveRunCompletion(root, config, { dryRun: opts.dryRun });
|
|
41
|
+
|
|
42
|
+
if (result.dry_run) {
|
|
43
|
+
console.log(chalk.cyan(' Dry Run: gate approval preview only'));
|
|
44
|
+
if (result.gate_actions?.length > 0) {
|
|
45
|
+
console.log(` ${chalk.dim('Gate actions:')} ${result.gate_actions.length}`);
|
|
46
|
+
for (const action of result.gate_actions) {
|
|
47
|
+
const timeoutHint = action.timeout_ms && action.timeout_ms !== 900_000 ? chalk.dim(` [timeout: ${action.timeout_ms}ms]`) : '';
|
|
48
|
+
console.log(` ${action.index}. ${action.label || action.run}${timeoutHint}`);
|
|
49
|
+
if (action.label) {
|
|
50
|
+
console.log(` ${chalk.dim(action.run)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(` ${chalk.dim('Gate actions:')} none configured`);
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
41
59
|
|
|
42
60
|
if (!result.ok) {
|
|
43
61
|
if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
|
|
44
62
|
printGateHookFailure(result, 'run_completion', pc);
|
|
63
|
+
} else if (result.error_code === 'gate_action_failed') {
|
|
64
|
+
printGateActionFailure(result, pc);
|
|
45
65
|
} else {
|
|
46
66
|
console.log(chalk.red(` Failed: ${result.error}`));
|
|
47
67
|
}
|
|
@@ -49,6 +69,9 @@ export async function approveCompletionCommand(opts) {
|
|
|
49
69
|
}
|
|
50
70
|
|
|
51
71
|
console.log(chalk.green(' \u2713 Run completed'));
|
|
72
|
+
if (result.gateActionRun?.actions?.length > 0) {
|
|
73
|
+
console.log(` ${chalk.dim('Gate actions:')} ${result.gateActionRun.actions.length} completed`);
|
|
74
|
+
}
|
|
52
75
|
console.log(chalk.dim(` Completed at: ${result.state.completed_at}`));
|
|
53
76
|
console.log('');
|
|
54
77
|
}
|
|
@@ -78,3 +101,24 @@ function printGateHookFailure(result, gateType, gateInfo) {
|
|
|
78
101
|
}
|
|
79
102
|
console.log('');
|
|
80
103
|
}
|
|
104
|
+
|
|
105
|
+
function printGateActionFailure(result, gateInfo) {
|
|
106
|
+
const failure = result.gateActionRun?.failed_action;
|
|
107
|
+
const exitLabel = failure?.timed_out
|
|
108
|
+
? `timeout after ${failure.timeout_ms}ms`
|
|
109
|
+
: failure?.exit_code ?? failure?.signal ?? 'unknown';
|
|
110
|
+
const stderrOrError = failure?.stderr_tail || failure?.spawn_error || null;
|
|
111
|
+
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log(chalk.yellow(' Run Completion Blocked By Gate Action'));
|
|
114
|
+
console.log(chalk.dim(' ' + '-'.repeat(44)));
|
|
115
|
+
console.log('');
|
|
116
|
+
console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
|
|
117
|
+
console.log(` ${chalk.dim('Action:')} ${failure?.action_label || failure?.command || '(unknown)'}`);
|
|
118
|
+
console.log(` ${chalk.dim('Exit:')} ${exitLabel}`);
|
|
119
|
+
if (stderrOrError) {
|
|
120
|
+
console.log(` ${chalk.dim('stderr:')} ${stderrOrError}`);
|
|
121
|
+
}
|
|
122
|
+
console.log(` ${chalk.dim('Retry:')} agentxchain approve-completion`);
|
|
123
|
+
console.log('');
|
|
124
|
+
}
|
|
@@ -36,11 +36,31 @@ export async function approveTransitionCommand(opts) {
|
|
|
36
36
|
console.log(` ${chalk.dim('Turn:')} ${pt.requested_by_turn}`);
|
|
37
37
|
console.log('');
|
|
38
38
|
|
|
39
|
-
const result = approvePhaseTransition(root, config);
|
|
39
|
+
const result = approvePhaseTransition(root, config, { dryRun: opts.dryRun });
|
|
40
|
+
|
|
41
|
+
if (result.dry_run) {
|
|
42
|
+
console.log(chalk.cyan(' Dry Run: gate approval preview only'));
|
|
43
|
+
if (result.gate_actions?.length > 0) {
|
|
44
|
+
console.log(` ${chalk.dim('Gate actions:')} ${result.gate_actions.length}`);
|
|
45
|
+
for (const action of result.gate_actions) {
|
|
46
|
+
const timeoutHint = action.timeout_ms && action.timeout_ms !== 900_000 ? chalk.dim(` [timeout: ${action.timeout_ms}ms]`) : '';
|
|
47
|
+
console.log(` ${action.index}. ${action.label || action.run}${timeoutHint}`);
|
|
48
|
+
if (action.label) {
|
|
49
|
+
console.log(` ${chalk.dim(action.run)}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` ${chalk.dim('Gate actions:')} none configured`);
|
|
54
|
+
}
|
|
55
|
+
console.log('');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
if (!result.ok) {
|
|
42
60
|
if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
|
|
43
61
|
printGateHookFailure(result, 'phase_transition', pt);
|
|
62
|
+
} else if (result.error_code === 'gate_action_failed') {
|
|
63
|
+
printGateActionFailure(result, 'phase_transition', pt);
|
|
44
64
|
} else {
|
|
45
65
|
console.log(chalk.red(` Failed: ${result.error}`));
|
|
46
66
|
}
|
|
@@ -48,6 +68,9 @@ export async function approveTransitionCommand(opts) {
|
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
console.log(chalk.green(` ✓ Phase advanced: ${pt.from} → ${pt.to}`));
|
|
71
|
+
if (result.gateActionRun?.actions?.length > 0) {
|
|
72
|
+
console.log(` ${chalk.dim('Gate actions:')} ${result.gateActionRun.actions.length} completed`);
|
|
73
|
+
}
|
|
51
74
|
console.log(chalk.dim(` Run status: ${result.state.status}`));
|
|
52
75
|
console.log('');
|
|
53
76
|
console.log(chalk.dim(` Next: agentxchain step (to run the first turn in ${pt.to} phase)`));
|
|
@@ -83,3 +106,28 @@ function printGateHookFailure(result, gateType, gateInfo) {
|
|
|
83
106
|
}
|
|
84
107
|
console.log('');
|
|
85
108
|
}
|
|
109
|
+
|
|
110
|
+
function printGateActionFailure(result, gateType, gateInfo) {
|
|
111
|
+
const failure = result.gateActionRun?.failed_action;
|
|
112
|
+
const exitLabel = failure?.timed_out
|
|
113
|
+
? `timeout after ${failure.timeout_ms}ms`
|
|
114
|
+
: failure?.exit_code ?? failure?.signal ?? 'unknown';
|
|
115
|
+
const stderrOrError = failure?.stderr_tail || failure?.spawn_error || null;
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(chalk.yellow(` ${gateType === 'phase_transition' ? 'Phase Transition' : 'Run Completion'} Blocked By Gate Action`));
|
|
119
|
+
console.log(chalk.dim(' ' + '-'.repeat(44)));
|
|
120
|
+
console.log('');
|
|
121
|
+
if (gateType === 'phase_transition') {
|
|
122
|
+
console.log(` ${chalk.dim('From:')} ${gateInfo.from}`);
|
|
123
|
+
console.log(` ${chalk.dim('To:')} ${gateInfo.to}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
|
|
126
|
+
console.log(` ${chalk.dim('Action:')} ${failure?.action_label || failure?.command || '(unknown)'}`);
|
|
127
|
+
console.log(` ${chalk.dim('Exit:')} ${exitLabel}`);
|
|
128
|
+
if (stderrOrError) {
|
|
129
|
+
console.log(` ${chalk.dim('stderr:')} ${stderrOrError}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(` ${chalk.dim('Retry:')} ${gateType === 'phase_transition' ? 'agentxchain approve-transition' : 'agentxchain approve-completion'}`);
|
|
132
|
+
console.log('');
|
|
133
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -14,9 +14,11 @@ import { getConnectorHealth } from '../lib/connector-health.js';
|
|
|
14
14
|
import { readRepoDecisions, summarizeRepoDecisions } from '../lib/repo-decisions.js';
|
|
15
15
|
import { deriveWorkflowKitArtifacts } from '../lib/workflow-kit-artifacts.js';
|
|
16
16
|
import { evaluateTimeouts } from '../lib/timeout-evaluator.js';
|
|
17
|
+
import { evaluateApprovalSlaReminders } from '../lib/notification-runner.js';
|
|
17
18
|
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
18
19
|
import { readRecentRunEventSummary } from '../lib/recent-event-summary.js';
|
|
19
20
|
import { deriveConflictedTurnResolutionActions } from '../lib/conflict-actions.js';
|
|
21
|
+
import { summarizeLatestGateActionAttempt } from '../lib/gate-actions.js';
|
|
20
22
|
import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
21
23
|
|
|
22
24
|
export async function statusCommand(opts) {
|
|
@@ -125,6 +127,14 @@ function renderGovernedStatus(context, opts) {
|
|
|
125
127
|
const repoDecisionSummary = summarizeRepoDecisions(readRepoDecisions(root), config);
|
|
126
128
|
|
|
127
129
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
130
|
+
const gateActionAttempt = state?.pending_phase_transition
|
|
131
|
+
? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
|
|
132
|
+
: state?.pending_run_completion
|
|
133
|
+
? summarizeLatestGateActionAttempt(root, 'run_completion', state.pending_run_completion.gate)
|
|
134
|
+
: null;
|
|
135
|
+
|
|
136
|
+
// Fire approval SLA reminders as a side effect (webhook-only, no CLI output)
|
|
137
|
+
evaluateApprovalSlaReminders(root, config, state);
|
|
128
138
|
|
|
129
139
|
if (opts.json) {
|
|
130
140
|
const dashPid = getDashboardPid(root);
|
|
@@ -152,6 +162,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
152
162
|
next_actions: nextActions,
|
|
153
163
|
connector_health: connectorHealth,
|
|
154
164
|
recent_event_summary: recentEventSummary,
|
|
165
|
+
gate_action_attempt: gateActionAttempt,
|
|
155
166
|
workflow_kit_artifacts: workflowKitArtifacts,
|
|
156
167
|
dashboard_session: dashboardSessionObj,
|
|
157
168
|
}, null, 2));
|
|
@@ -197,6 +208,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
197
208
|
const activeTurnCount = getActiveTurnCount(state);
|
|
198
209
|
const activeTurns = getActiveTurns(state);
|
|
199
210
|
const singleActiveTurn = getActiveTurn(state);
|
|
211
|
+
const approvalPending = Boolean(state?.pending_phase_transition || state?.pending_run_completion);
|
|
200
212
|
if (activeTurnCount > 1) {
|
|
201
213
|
console.log(` ${chalk.dim('Turns:')} ${activeTurnCount} active`);
|
|
202
214
|
for (const turn of Object.values(activeTurns)) {
|
|
@@ -249,8 +261,14 @@ function renderGovernedStatus(context, opts) {
|
|
|
249
261
|
if (singleActiveTurn.status === 'conflicted' && singleActiveTurn.conflict_state) {
|
|
250
262
|
const cs = singleActiveTurn.conflict_state;
|
|
251
263
|
const files = cs.conflict_error?.conflicting_files || [];
|
|
264
|
+
const count = cs.detection_count || 1;
|
|
252
265
|
const [reassignAction, mergeAction] = deriveConflictedTurnResolutionActions(singleActiveTurn.turn_id);
|
|
253
|
-
console.log(` ${chalk.dim('Conflict:')} ${chalk.red(`${files.length} file(s) conflicting`)} — detection #${
|
|
266
|
+
console.log(` ${chalk.dim('Conflict:')} ${chalk.red(`${files.length} file(s) conflicting`)} — detection #${count}`);
|
|
267
|
+
if (cs.conflict_error?.overlap_ratio != null) {
|
|
268
|
+
console.log(` ${chalk.dim('Overlap:')} ${(cs.conflict_error.overlap_ratio * 100).toFixed(0)}%`);
|
|
269
|
+
}
|
|
270
|
+
const suggestion = cs.conflict_error?.suggested_resolution || 'reject_and_reassign';
|
|
271
|
+
console.log(` ${chalk.dim('Suggest:')} ${suggestion}`);
|
|
254
272
|
console.log(` ${chalk.dim('Resolve:')} ${chalk.cyan(reassignAction.command)}`);
|
|
255
273
|
console.log(` ${chalk.dim(' or:')} ${chalk.cyan(mergeAction.command)}`);
|
|
256
274
|
}
|
|
@@ -321,12 +339,33 @@ function renderGovernedStatus(context, opts) {
|
|
|
321
339
|
const pt = state.pending_phase_transition;
|
|
322
340
|
console.log(` ${chalk.dim('Pending:')} ${formatGovernedPhase(pt.from)} → ${formatGovernedPhase(pt.to)}`);
|
|
323
341
|
console.log(` ${chalk.dim('Gate:')} ${pt.gate} (requires human approval)`);
|
|
342
|
+
if (pt.requested_at) {
|
|
343
|
+
console.log(` ${chalk.dim('Requested:')} ${pt.requested_at} (${timeSince(pt.requested_at)} ago)`);
|
|
344
|
+
}
|
|
324
345
|
}
|
|
325
346
|
|
|
326
347
|
if (state?.pending_run_completion) {
|
|
327
348
|
const pc = state.pending_run_completion;
|
|
328
349
|
console.log(` ${chalk.dim('Pending:')} ${chalk.bold('Run Completion')}`);
|
|
329
350
|
console.log(` ${chalk.dim('Gate:')} ${pc.gate} (requires human approval)`);
|
|
351
|
+
if (pc.requested_at) {
|
|
352
|
+
console.log(` ${chalk.dim('Requested:')} ${pc.requested_at} (${timeSince(pc.requested_at)} ago)`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (gateActionAttempt) {
|
|
357
|
+
console.log(` ${chalk.dim('Gate actions:')} ${gateActionAttempt.status} at ${gateActionAttempt.attempted_at || 'unknown time'}`);
|
|
358
|
+
for (const action of gateActionAttempt.actions) {
|
|
359
|
+
const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
|
|
360
|
+
const outcome = action.status === 'failed'
|
|
361
|
+
? (action.timed_out ? chalk.red(`timed out after ${action.timeout_ms}ms`) : chalk.red('failed'))
|
|
362
|
+
: chalk.green('succeeded');
|
|
363
|
+
const exit = action.timed_out ? '' : (action.exit_code == null ? '' : ` (exit ${action.exit_code})`);
|
|
364
|
+
console.log(` ${action.action_index || '?'}. ${label} — ${outcome}${exit}`);
|
|
365
|
+
if (action.status === 'failed' && action.stderr_tail) {
|
|
366
|
+
console.log(` ${chalk.dim(action.stderr_tail)}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
330
369
|
}
|
|
331
370
|
|
|
332
371
|
if (state?.status === 'completed') {
|
|
@@ -368,14 +407,20 @@ function renderGovernedStatus(context, opts) {
|
|
|
368
407
|
|
|
369
408
|
renderWorkflowKitArtifactsSection(workflowKitArtifacts);
|
|
370
409
|
|
|
371
|
-
if (config.timeouts && state?.status === 'active') {
|
|
372
|
-
const activeTurn = getActiveTurn(state);
|
|
410
|
+
if (config.timeouts && (state?.status === 'active' || approvalPending)) {
|
|
411
|
+
const activeTurn = state?.status === 'active' ? getActiveTurn(state) : null;
|
|
373
412
|
const turnResult = activeTurn ? { role: activeTurn.assigned_role } : undefined;
|
|
374
413
|
const timeoutEval = evaluateTimeouts({ config, state, turn: activeTurn, turnResult, now: new Date().toISOString() });
|
|
375
414
|
const allItems = [...timeoutEval.exceeded, ...timeoutEval.warnings];
|
|
376
|
-
if (allItems.length > 0) {
|
|
415
|
+
if (allItems.length > 0 || approvalPending) {
|
|
377
416
|
console.log('');
|
|
378
417
|
console.log(` ${chalk.dim('Timeouts:')}`);
|
|
418
|
+
if (approvalPending) {
|
|
419
|
+
console.log(` ${chalk.yellow('◷')} approval wait does not mutate timeout state; phase/run clocks keep ticking until the next accepted turn`);
|
|
420
|
+
}
|
|
421
|
+
if (approvalPending && allItems.length === 0) {
|
|
422
|
+
console.log(` ${chalk.dim('No current phase/run timeout pressure.')}`);
|
|
423
|
+
}
|
|
379
424
|
for (const item of allItems) {
|
|
380
425
|
const isExceeded = timeoutEval.exceeded.includes(item);
|
|
381
426
|
const elapsed = item.elapsed_minutes != null ? `${item.elapsed_minutes}m` : '?';
|
|
@@ -637,6 +682,7 @@ function formatRunStatus(status) {
|
|
|
637
682
|
|
|
638
683
|
function timeSince(iso) {
|
|
639
684
|
const ms = Date.now() - new Date(iso).getTime();
|
|
685
|
+
if (!Number.isFinite(ms) || ms < 0) return '0s';
|
|
640
686
|
const sec = Math.floor(ms / 1000);
|
|
641
687
|
if (sec < 60) return `${sec}s`;
|
|
642
688
|
const min = Math.floor(sec / 60);
|
package/src/commands/step.js
CHANGED
|
@@ -67,6 +67,7 @@ import { runHooks } from '../lib/hook-runner.js';
|
|
|
67
67
|
import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
|
|
68
68
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
69
69
|
import { shouldSuggestManualQaFallback } from '../lib/manual-qa-fallback.js';
|
|
70
|
+
import { evaluateApprovalSlaReminders } from '../lib/notification-runner.js';
|
|
70
71
|
|
|
71
72
|
export async function stepCommand(opts) {
|
|
72
73
|
const context = loadProjectContext();
|
|
@@ -169,6 +170,7 @@ export async function stepCommand(opts) {
|
|
|
169
170
|
|
|
170
171
|
if (!skipAssignment) {
|
|
171
172
|
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
173
|
+
evaluateApprovalSlaReminders(root, config, state);
|
|
172
174
|
printRecoverySummary(state, 'This run is awaiting approval.', config);
|
|
173
175
|
process.exit(1);
|
|
174
176
|
}
|