agentxchain 2.109.0 → 2.111.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 +31 -0
- package/dashboard/app.js +3 -0
- package/dashboard/components/blocked.js +4 -2
- package/dashboard/components/chain.js +200 -0
- package/dashboard/components/gate.js +6 -3
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/src/commands/approve-completion.js +9 -4
- package/src/commands/approve-transition.js +9 -4
- package/src/commands/chain.js +252 -0
- package/src/commands/diff.js +19 -0
- package/src/commands/run.js +110 -97
- package/src/commands/status.js +2 -2
- package/src/lib/chain-reports.js +54 -0
- package/src/lib/dashboard/bridge-server.js +8 -0
- package/src/lib/dashboard/chain-report-reader.js +15 -0
- package/src/lib/dashboard/state-reader.js +6 -1
- package/src/lib/export-diff.js +10 -1
- package/src/lib/gate-actions.js +33 -2
- package/src/lib/governed-state.js +6 -2
- package/src/lib/report.js +6 -3
- package/src/lib/run-chain.js +262 -0
package/bin/agentxchain.js
CHANGED
|
@@ -121,6 +121,7 @@ import { diffCommand } from '../src/commands/diff.js';
|
|
|
121
121
|
import { eventsCommand } from '../src/commands/events.js';
|
|
122
122
|
import { connectorCheckCommand } from '../src/commands/connector.js';
|
|
123
123
|
import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
|
|
124
|
+
import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
|
|
124
125
|
|
|
125
126
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
126
127
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
@@ -392,6 +393,32 @@ program
|
|
|
392
393
|
.option('-d, --dir <path>', 'Project directory')
|
|
393
394
|
.action(eventsCommand);
|
|
394
395
|
|
|
396
|
+
const chainCmd = program
|
|
397
|
+
.command('chain')
|
|
398
|
+
.description('Inspect run-chaining history and reports');
|
|
399
|
+
|
|
400
|
+
chainCmd
|
|
401
|
+
.command('latest')
|
|
402
|
+
.description('Show the most recent chain report')
|
|
403
|
+
.option('-j, --json', 'Output as JSON')
|
|
404
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
405
|
+
.action(chainLatestCommand);
|
|
406
|
+
|
|
407
|
+
chainCmd
|
|
408
|
+
.command('list')
|
|
409
|
+
.description('List all chain reports')
|
|
410
|
+
.option('-j, --json', 'Output as JSON')
|
|
411
|
+
.option('-l, --limit <n>', 'Max chain reports to show (default: 20)')
|
|
412
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
413
|
+
.action(chainListCommand);
|
|
414
|
+
|
|
415
|
+
chainCmd
|
|
416
|
+
.command('show <chain_id>')
|
|
417
|
+
.description('Show a specific chain report by ID')
|
|
418
|
+
.option('-j, --json', 'Output as JSON')
|
|
419
|
+
.option('-d, --dir <path>', 'Project directory')
|
|
420
|
+
.action(chainShowCommand);
|
|
421
|
+
|
|
395
422
|
program
|
|
396
423
|
.command('validate')
|
|
397
424
|
.description('Validate project protocol artifacts')
|
|
@@ -516,6 +543,10 @@ program
|
|
|
516
543
|
.option('--continue-from <run_id>', 'Continue from a prior terminal run (sets trigger=continuation)')
|
|
517
544
|
.option('--recover-from <run_id>', 'Recover from a prior blocked run (sets trigger=recovery)')
|
|
518
545
|
.option('--inherit-context', 'Inherit read-only summary context from the parent run (requires --continue-from or --recover-from)')
|
|
546
|
+
.option('--chain', 'Auto-chain runs: when a run completes, start a continuation automatically')
|
|
547
|
+
.option('--max-chains <n>', 'Maximum continuation runs in chain mode (default: 5)', parseInt)
|
|
548
|
+
.option('--chain-on <statuses>', 'Comma-separated terminal statuses that trigger chaining (default: completed)')
|
|
549
|
+
.option('--chain-cooldown <seconds>', 'Seconds to wait between chained runs (default: 5)', parseInt)
|
|
519
550
|
.action(runCommand);
|
|
520
551
|
|
|
521
552
|
program
|
package/dashboard/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
|
15
15
|
import { render as renderDelegations } from './components/delegations.js';
|
|
16
16
|
import { render as renderBlockers } from './components/blockers.js';
|
|
17
17
|
import { render as renderArtifacts } from './components/artifacts.js';
|
|
18
|
+
import { render as renderChain } from './components/chain.js';
|
|
18
19
|
import { render as renderRunHistory } from './components/run-history.js';
|
|
19
20
|
import { render as renderTimeouts } from './components/timeouts.js';
|
|
20
21
|
import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
|
|
@@ -35,6 +36,7 @@ const VIEWS = {
|
|
|
35
36
|
'cross-repo': { fetch: ['coordinatorState', 'coordinatorHistory'], render: renderCrossRepo },
|
|
36
37
|
blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
|
|
37
38
|
artifacts: { fetch: ['workflowKitArtifacts'], render: renderArtifacts },
|
|
39
|
+
chain: { fetch: ['chainReports'], render: renderChain },
|
|
38
40
|
'run-history': { fetch: ['runHistory'], render: renderRunHistory },
|
|
39
41
|
timeouts: { fetch: ['timeouts'], render: renderTimeouts },
|
|
40
42
|
'coordinator-timeouts': { fetch: ['coordinatorTimeouts'], render: renderCoordinatorTimeouts },
|
|
@@ -58,6 +60,7 @@ const API_MAP = {
|
|
|
58
60
|
coordinatorBlockers: '/api/coordinator/blockers',
|
|
59
61
|
coordinatorRepoStatusRows: '/api/coordinator/repo-status',
|
|
60
62
|
workflowKitArtifacts: '/api/workflow-kit-artifacts',
|
|
63
|
+
chainReports: '/api/chain-reports',
|
|
61
64
|
connectors: '/api/connectors',
|
|
62
65
|
runHistory: '/api/run-history',
|
|
63
66
|
timeouts: '/api/timeouts',
|
|
@@ -120,8 +120,10 @@ function renderGateActionFailure(gateActions, state) {
|
|
|
120
120
|
html += `<div class="annotation-list">`;
|
|
121
121
|
for (const action of actions) {
|
|
122
122
|
const label = action.action_label || action.command || `action ${action.action_index || '?'}`;
|
|
123
|
-
const outcome = action.status === 'failed'
|
|
124
|
-
|
|
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})` : '');
|
|
125
127
|
html += `<div class="annotation-card">
|
|
126
128
|
<span class="mono">${esc(String(action.action_index || '?'))}.</span>
|
|
127
129
|
<span>${esc(label)}</span>
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain view — renders run-chaining visibility from /api/chain-reports.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes snapshot data from the bridge server and returns
|
|
5
|
+
* HTML for latest-chain lineage plus recent chain-session history.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function esc(str) {
|
|
9
|
+
if (str == null) return '';
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
19
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatStatus(status) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'completed':
|
|
25
|
+
return badge('completed', 'var(--green)');
|
|
26
|
+
case 'blocked':
|
|
27
|
+
return badge('blocked', 'var(--yellow)');
|
|
28
|
+
case 'failed':
|
|
29
|
+
return badge('failed', 'var(--red)');
|
|
30
|
+
default:
|
|
31
|
+
return badge(status || 'unknown', 'var(--text-dim)');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatTerminalReason(reason) {
|
|
36
|
+
switch (reason) {
|
|
37
|
+
case 'chain_limit_reached':
|
|
38
|
+
return badge('chain limit reached', '#38bdf8');
|
|
39
|
+
case 'non_chainable_status':
|
|
40
|
+
return badge('non-chainable status', 'var(--yellow)');
|
|
41
|
+
case 'operator_abort':
|
|
42
|
+
return badge('operator abort', 'var(--red)');
|
|
43
|
+
case 'parent_validation_failed':
|
|
44
|
+
return badge('parent validation failed', 'var(--red)');
|
|
45
|
+
default:
|
|
46
|
+
return badge(reason || 'unknown', 'var(--text-dim)');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatDuration(ms) {
|
|
51
|
+
if (ms == null) return '—';
|
|
52
|
+
if (ms < 1000) return `${ms}ms`;
|
|
53
|
+
const seconds = Math.floor(ms / 1000);
|
|
54
|
+
if (seconds < 60) return `${seconds}s`;
|
|
55
|
+
const minutes = Math.floor(seconds / 60);
|
|
56
|
+
const remainingSeconds = seconds % 60;
|
|
57
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
58
|
+
const hours = Math.floor(minutes / 60);
|
|
59
|
+
const remainingMinutes = minutes % 60;
|
|
60
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatDate(iso) {
|
|
64
|
+
if (!iso) return '—';
|
|
65
|
+
try {
|
|
66
|
+
const date = new Date(iso);
|
|
67
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
68
|
+
+ ' ' + date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
69
|
+
} catch {
|
|
70
|
+
return esc(iso);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function truncateId(value, len = 12) {
|
|
75
|
+
if (!value) return '—';
|
|
76
|
+
return value.length > len ? `${value.slice(0, len)}…` : value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatContextSummary(summary) {
|
|
80
|
+
if (!summary) return '—';
|
|
81
|
+
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (Array.isArray(summary.parent_roles_used) && summary.parent_roles_used.length > 0) {
|
|
84
|
+
parts.push(`${summary.parent_roles_used.length} roles`);
|
|
85
|
+
}
|
|
86
|
+
if (summary.parent_phases_completed_count > 0) {
|
|
87
|
+
parts.push(`${summary.parent_phases_completed_count} phases`);
|
|
88
|
+
}
|
|
89
|
+
if (summary.recent_decisions_count > 0) {
|
|
90
|
+
parts.push(`${summary.recent_decisions_count} decisions`);
|
|
91
|
+
}
|
|
92
|
+
if (summary.recent_accepted_turns_count > 0) {
|
|
93
|
+
parts.push(`${summary.recent_accepted_turns_count} turns`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parts.length > 0 ? esc(parts.join(', ')) : '—';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderLatestRunsTable(report) {
|
|
100
|
+
if (!Array.isArray(report?.runs) || report.runs.length === 0) {
|
|
101
|
+
return `<p class="section-subtitle">No runs recorded for this chain.</p>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let html = `<div class="section"><h3>Latest Chain Lineage</h3>
|
|
105
|
+
<table class="data-table">
|
|
106
|
+
<thead>
|
|
107
|
+
<tr>
|
|
108
|
+
<th>#</th>
|
|
109
|
+
<th>Run ID</th>
|
|
110
|
+
<th>Status</th>
|
|
111
|
+
<th>Trigger</th>
|
|
112
|
+
<th>Turns</th>
|
|
113
|
+
<th>Duration</th>
|
|
114
|
+
<th>Parent</th>
|
|
115
|
+
<th>Inherited Context</th>
|
|
116
|
+
</tr>
|
|
117
|
+
</thead>
|
|
118
|
+
<tbody>`;
|
|
119
|
+
|
|
120
|
+
report.runs.forEach((run, index) => {
|
|
121
|
+
html += `<tr>
|
|
122
|
+
<td style="color:var(--text-dim)">${index + 1}</td>
|
|
123
|
+
<td class="mono" title="${esc(run.run_id || '')}">${esc(truncateId(run.run_id))}</td>
|
|
124
|
+
<td>${formatStatus(run.status)}</td>
|
|
125
|
+
<td>${esc(run.provenance_trigger || '—')}</td>
|
|
126
|
+
<td>${run.turns ?? '—'}</td>
|
|
127
|
+
<td>${formatDuration(run.duration_ms)}</td>
|
|
128
|
+
<td class="mono" title="${esc(run.parent_run_id || '')}">${esc(truncateId(run.parent_run_id))}</td>
|
|
129
|
+
<td>${formatContextSummary(run.inherited_context_summary)}</td>
|
|
130
|
+
</tr>`;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
html += '</tbody></table></div>';
|
|
134
|
+
return html;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderRecentChainsTable(reports) {
|
|
138
|
+
let html = `<div class="section"><h3>Recent Chain Sessions</h3>
|
|
139
|
+
<table class="data-table">
|
|
140
|
+
<thead>
|
|
141
|
+
<tr>
|
|
142
|
+
<th>#</th>
|
|
143
|
+
<th>Chain ID</th>
|
|
144
|
+
<th>Runs</th>
|
|
145
|
+
<th>Turns</th>
|
|
146
|
+
<th>Terminal</th>
|
|
147
|
+
<th>Duration</th>
|
|
148
|
+
<th>Started</th>
|
|
149
|
+
</tr>
|
|
150
|
+
</thead>
|
|
151
|
+
<tbody>`;
|
|
152
|
+
|
|
153
|
+
reports.forEach((report, index) => {
|
|
154
|
+
html += `<tr>
|
|
155
|
+
<td style="color:var(--text-dim)">${index + 1}</td>
|
|
156
|
+
<td class="mono" title="${esc(report.chain_id || '')}">${esc(truncateId(report.chain_id, 14))}</td>
|
|
157
|
+
<td>${report.runs?.length || 0}</td>
|
|
158
|
+
<td>${report.total_turns ?? '—'}</td>
|
|
159
|
+
<td>${formatTerminalReason(report.terminal_reason)}</td>
|
|
160
|
+
<td>${formatDuration(report.total_duration_ms)}</td>
|
|
161
|
+
<td>${formatDate(report.started_at)}</td>
|
|
162
|
+
</tr>`;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
html += '</tbody></table></div>';
|
|
166
|
+
return html;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function render({ chainReports }) {
|
|
170
|
+
if (!chainReports || typeof chainReports !== 'object') {
|
|
171
|
+
return `<div class="placeholder"><h2>Chain</h2><p>No chain data available. Run a chained governed session to populate this view.</p></div>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const reports = Array.isArray(chainReports.reports) ? chainReports.reports : [];
|
|
175
|
+
const latest = chainReports.latest || reports[0] || null;
|
|
176
|
+
|
|
177
|
+
if (!latest || reports.length === 0) {
|
|
178
|
+
return `<div class="placeholder"><h2>Chain</h2><p>No chain reports found. Run <code>agentxchain run --chain</code> to record automatic continuation lineage.</p></div>`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let html = `<div class="chain-view"><div class="run-header"><div class="run-meta">`;
|
|
182
|
+
html += `<span class="turn-count">latest chain ${esc(latest.chain_id || '—')}</span>`;
|
|
183
|
+
html += badge(`${latest.runs?.length || 0} runs`, '#38bdf8');
|
|
184
|
+
html += badge(`${latest.total_turns || 0} turns`, 'var(--green)');
|
|
185
|
+
html += formatTerminalReason(latest.terminal_reason);
|
|
186
|
+
html += `</div></div>`;
|
|
187
|
+
|
|
188
|
+
html += `<div class="section"><h3>Latest Chain Summary</h3><dl class="detail-list">`;
|
|
189
|
+
html += `<dt>Started</dt><dd>${esc(latest.started_at || '—')}</dd>`;
|
|
190
|
+
html += `<dt>Completed</dt><dd>${esc(latest.completed_at || '—')}</dd>`;
|
|
191
|
+
html += `<dt>Total Duration</dt><dd>${formatDuration(latest.total_duration_ms)}</dd>`;
|
|
192
|
+
html += `<dt>Total Turns</dt><dd>${latest.total_turns ?? '—'}</dd>`;
|
|
193
|
+
html += `<dt>Terminal Reason</dt><dd>${esc(latest.terminal_reason || '—')}</dd>`;
|
|
194
|
+
html += `</dl></div>`;
|
|
195
|
+
|
|
196
|
+
html += renderLatestRunsTable(latest);
|
|
197
|
+
html += renderRecentChainsTable(reports);
|
|
198
|
+
html += '</div>';
|
|
199
|
+
return html;
|
|
200
|
+
}
|
|
@@ -240,9 +240,12 @@ function renderGateActionsSection(gateActions) {
|
|
|
240
240
|
html += `<ul>`;
|
|
241
241
|
for (const a of attempt.actions) {
|
|
242
242
|
const aLabel = a.action_label || a.command || `action ${a.action_index || '?'}`;
|
|
243
|
-
const outcome = a.status === 'failed'
|
|
244
|
-
|
|
245
|
-
|
|
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>`;
|
|
246
249
|
}
|
|
247
250
|
html += `</ul>`;
|
|
248
251
|
}
|
package/dashboard/index.html
CHANGED
|
@@ -405,6 +405,7 @@
|
|
|
405
405
|
<a href="#gate">Gates</a>
|
|
406
406
|
<a href="#blockers">Blockers</a>
|
|
407
407
|
<a href="#artifacts">Artifacts</a>
|
|
408
|
+
<a href="#chain">Chain</a>
|
|
408
409
|
<a href="#run-history">Run History</a>
|
|
409
410
|
<a href="#timeouts">Timeouts</a>
|
|
410
411
|
<a href="#coordinator-timeouts">Coordinator Timeouts</a>
|
package/package.json
CHANGED
|
@@ -44,7 +44,8 @@ export async function approveCompletionCommand(opts) {
|
|
|
44
44
|
if (result.gate_actions?.length > 0) {
|
|
45
45
|
console.log(` ${chalk.dim('Gate actions:')} ${result.gate_actions.length}`);
|
|
46
46
|
for (const action of result.gate_actions) {
|
|
47
|
-
|
|
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}`);
|
|
48
49
|
if (action.label) {
|
|
49
50
|
console.log(` ${chalk.dim(action.run)}`);
|
|
50
51
|
}
|
|
@@ -103,6 +104,10 @@ function printGateHookFailure(result, gateType, gateInfo) {
|
|
|
103
104
|
|
|
104
105
|
function printGateActionFailure(result, gateInfo) {
|
|
105
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;
|
|
106
111
|
|
|
107
112
|
console.log('');
|
|
108
113
|
console.log(chalk.yellow(' Run Completion Blocked By Gate Action'));
|
|
@@ -110,9 +115,9 @@ function printGateActionFailure(result, gateInfo) {
|
|
|
110
115
|
console.log('');
|
|
111
116
|
console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
|
|
112
117
|
console.log(` ${chalk.dim('Action:')} ${failure?.action_label || failure?.command || '(unknown)'}`);
|
|
113
|
-
console.log(` ${chalk.dim('Exit:')} ${
|
|
114
|
-
if (
|
|
115
|
-
console.log(` ${chalk.dim('stderr:')} ${
|
|
118
|
+
console.log(` ${chalk.dim('Exit:')} ${exitLabel}`);
|
|
119
|
+
if (stderrOrError) {
|
|
120
|
+
console.log(` ${chalk.dim('stderr:')} ${stderrOrError}`);
|
|
116
121
|
}
|
|
117
122
|
console.log(` ${chalk.dim('Retry:')} agentxchain approve-completion`);
|
|
118
123
|
console.log('');
|
|
@@ -43,7 +43,8 @@ export async function approveTransitionCommand(opts) {
|
|
|
43
43
|
if (result.gate_actions?.length > 0) {
|
|
44
44
|
console.log(` ${chalk.dim('Gate actions:')} ${result.gate_actions.length}`);
|
|
45
45
|
for (const action of result.gate_actions) {
|
|
46
|
-
|
|
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}`);
|
|
47
48
|
if (action.label) {
|
|
48
49
|
console.log(` ${chalk.dim(action.run)}`);
|
|
49
50
|
}
|
|
@@ -108,6 +109,10 @@ function printGateHookFailure(result, gateType, gateInfo) {
|
|
|
108
109
|
|
|
109
110
|
function printGateActionFailure(result, gateType, gateInfo) {
|
|
110
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;
|
|
111
116
|
|
|
112
117
|
console.log('');
|
|
113
118
|
console.log(chalk.yellow(` ${gateType === 'phase_transition' ? 'Phase Transition' : 'Run Completion'} Blocked By Gate Action`));
|
|
@@ -119,9 +124,9 @@ function printGateActionFailure(result, gateType, gateInfo) {
|
|
|
119
124
|
}
|
|
120
125
|
console.log(` ${chalk.dim('Gate:')} ${gateInfo.gate}`);
|
|
121
126
|
console.log(` ${chalk.dim('Action:')} ${failure?.action_label || failure?.command || '(unknown)'}`);
|
|
122
|
-
console.log(` ${chalk.dim('Exit:')} ${
|
|
123
|
-
if (
|
|
124
|
-
console.log(` ${chalk.dim('stderr:')} ${
|
|
127
|
+
console.log(` ${chalk.dim('Exit:')} ${exitLabel}`);
|
|
128
|
+
if (stderrOrError) {
|
|
129
|
+
console.log(` ${chalk.dim('stderr:')} ${stderrOrError}`);
|
|
125
130
|
}
|
|
126
131
|
console.log(` ${chalk.dim('Retry:')} ${gateType === 'phase_transition' ? 'agentxchain approve-transition' : 'agentxchain approve-completion'}`);
|
|
127
132
|
console.log('');
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agentxchain chain — operator-facing read surface for chain reports.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces chain report metadata so operators can inspect lights-out
|
|
5
|
+
* run-chaining history without opening raw JSON files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { findProjectRoot } from '../lib/config.js';
|
|
10
|
+
import {
|
|
11
|
+
loadAllChainReports,
|
|
12
|
+
loadChainReport,
|
|
13
|
+
loadLatestChainReport,
|
|
14
|
+
} from '../lib/chain-reports.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* agentxchain chain latest — show the most recent chain report.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} opts - { json?: boolean, dir?: string }
|
|
20
|
+
*/
|
|
21
|
+
export async function chainLatestCommand(opts) {
|
|
22
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
23
|
+
if (!root) {
|
|
24
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const report = loadLatestChainReport(root);
|
|
29
|
+
if (!report) {
|
|
30
|
+
console.log(chalk.dim('No chain reports found.'));
|
|
31
|
+
console.log(chalk.dim(' Run `agentxchain run --chain` to enable auto-chaining.'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (opts.json) {
|
|
36
|
+
console.log(JSON.stringify(report, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
renderChainReport(report);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* agentxchain chain list — list all chain reports.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} opts - { json?: boolean, limit?: number, dir?: string }
|
|
47
|
+
*/
|
|
48
|
+
export async function chainListCommand(opts) {
|
|
49
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
50
|
+
if (!root) {
|
|
51
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const reports = loadAllChainReports(root);
|
|
56
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
|
|
57
|
+
const limited = reports.slice(0, limit);
|
|
58
|
+
|
|
59
|
+
if (opts.json) {
|
|
60
|
+
console.log(JSON.stringify(limited, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (limited.length === 0) {
|
|
65
|
+
console.log(chalk.dim('No chain reports found.'));
|
|
66
|
+
console.log(chalk.dim(' Run `agentxchain run --chain` to enable auto-chaining.'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Table header
|
|
71
|
+
const header = [
|
|
72
|
+
pad('#', 4),
|
|
73
|
+
pad('Chain ID', 16),
|
|
74
|
+
pad('Runs', 6),
|
|
75
|
+
pad('Turns', 7),
|
|
76
|
+
pad('Terminal Reason', 28),
|
|
77
|
+
pad('Duration', 12),
|
|
78
|
+
pad('Started', 22),
|
|
79
|
+
].join(' ');
|
|
80
|
+
|
|
81
|
+
console.log(chalk.bold(header));
|
|
82
|
+
console.log(chalk.dim('─'.repeat(header.length)));
|
|
83
|
+
|
|
84
|
+
limited.forEach((report, i) => {
|
|
85
|
+
const idx = String(i + 1);
|
|
86
|
+
const chainId = report.chain_id || '—';
|
|
87
|
+
const runs = String(report.runs?.length || 0);
|
|
88
|
+
const turns = String(report.total_turns || 0);
|
|
89
|
+
const terminal = formatTerminalReason(report.terminal_reason);
|
|
90
|
+
const duration = report.total_duration_ms != null
|
|
91
|
+
? formatDuration(report.total_duration_ms)
|
|
92
|
+
: '—';
|
|
93
|
+
const started = report.started_at
|
|
94
|
+
? new Date(report.started_at).toLocaleString()
|
|
95
|
+
: '—';
|
|
96
|
+
|
|
97
|
+
console.log([
|
|
98
|
+
pad(idx, 4),
|
|
99
|
+
pad(chainId, 16),
|
|
100
|
+
pad(runs, 6),
|
|
101
|
+
pad(turns, 7),
|
|
102
|
+
pad(terminal, 28),
|
|
103
|
+
pad(duration, 12),
|
|
104
|
+
pad(started, 22),
|
|
105
|
+
].join(' '));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
console.log(chalk.dim(`\n${limited.length} chain(s) shown${reports.length > limit ? ` (${reports.length} total)` : ''}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* agentxchain chain show <chain_id> — show a specific chain report.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} chainId
|
|
115
|
+
* @param {object} opts - { json?: boolean, dir?: string }
|
|
116
|
+
*/
|
|
117
|
+
export async function chainShowCommand(chainId, opts) {
|
|
118
|
+
const root = findProjectRoot(opts.dir || process.cwd());
|
|
119
|
+
if (!root) {
|
|
120
|
+
console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const report = loadChainReport(root, chainId);
|
|
125
|
+
if (!report) {
|
|
126
|
+
console.error(chalk.red(`Chain report not found: ${chainId}`));
|
|
127
|
+
console.log(chalk.dim(' Use `agentxchain chain list` to see available chain reports.'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (opts.json) {
|
|
132
|
+
console.log(JSON.stringify(report, null, 2));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
renderChainReport(report);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── Rendering ─────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function renderChainReport(report) {
|
|
142
|
+
console.log(chalk.bold(`Chain Report: ${report.chain_id}`));
|
|
143
|
+
console.log('');
|
|
144
|
+
console.log(` Total runs: ${report.runs?.length || 0}`);
|
|
145
|
+
console.log(` Total turns: ${report.total_turns || 0}`);
|
|
146
|
+
console.log(` Duration: ${formatDuration(report.total_duration_ms || 0)}`);
|
|
147
|
+
console.log(` Terminal: ${formatTerminalReason(report.terminal_reason)}`);
|
|
148
|
+
console.log(` Started: ${report.started_at || '—'}`);
|
|
149
|
+
console.log(` Completed: ${report.completed_at || '—'}`);
|
|
150
|
+
console.log('');
|
|
151
|
+
|
|
152
|
+
if (!report.runs || report.runs.length === 0) {
|
|
153
|
+
console.log(chalk.dim(' No runs recorded.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Run table header
|
|
158
|
+
const runHeader = [
|
|
159
|
+
pad('#', 4),
|
|
160
|
+
pad('Run ID', 14),
|
|
161
|
+
pad('Status', 12),
|
|
162
|
+
pad('Trigger', 14),
|
|
163
|
+
pad('Turns', 7),
|
|
164
|
+
pad('Duration', 12),
|
|
165
|
+
pad('Parent', 14),
|
|
166
|
+
pad('Ctx', 40),
|
|
167
|
+
].join(' ');
|
|
168
|
+
|
|
169
|
+
console.log(chalk.bold(' Runs:'));
|
|
170
|
+
console.log(` ${chalk.dim(runHeader)}`);
|
|
171
|
+
console.log(` ${chalk.dim('─'.repeat(runHeader.length))}`);
|
|
172
|
+
|
|
173
|
+
report.runs.forEach((run, i) => {
|
|
174
|
+
const idx = String(i + 1);
|
|
175
|
+
const runId = (run.run_id || '—').slice(0, 12);
|
|
176
|
+
const status = formatStatus(run.status);
|
|
177
|
+
const trigger = run.provenance_trigger || '—';
|
|
178
|
+
const turns = String(run.turns || 0);
|
|
179
|
+
const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : '—';
|
|
180
|
+
const parent = run.parent_run_id ? run.parent_run_id.slice(0, 12) : '—';
|
|
181
|
+
const ctx = formatInheritedContextSummary(run.inherited_context_summary);
|
|
182
|
+
|
|
183
|
+
console.log(` ${[
|
|
184
|
+
pad(idx, 4),
|
|
185
|
+
pad(runId, 14),
|
|
186
|
+
pad(status, 12),
|
|
187
|
+
pad(trigger, 14),
|
|
188
|
+
pad(turns, 7),
|
|
189
|
+
pad(duration, 12),
|
|
190
|
+
pad(parent, 14),
|
|
191
|
+
pad(ctx, 40),
|
|
192
|
+
].join(' ')}`);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatInheritedContextSummary(summary) {
|
|
197
|
+
if (!summary) return '—';
|
|
198
|
+
|
|
199
|
+
const parts = [];
|
|
200
|
+
if (summary.parent_roles_used?.length) {
|
|
201
|
+
parts.push(`${summary.parent_roles_used.length} roles`);
|
|
202
|
+
}
|
|
203
|
+
if (summary.parent_phases_completed_count > 0) {
|
|
204
|
+
parts.push(`${summary.parent_phases_completed_count} phases`);
|
|
205
|
+
}
|
|
206
|
+
if (summary.recent_decisions_count > 0) {
|
|
207
|
+
parts.push(`${summary.recent_decisions_count} decisions`);
|
|
208
|
+
}
|
|
209
|
+
if (summary.recent_accepted_turns_count > 0) {
|
|
210
|
+
parts.push(`${summary.recent_accepted_turns_count} turns`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return parts.length > 0 ? parts.join(', ') : '—';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function formatTerminalReason(reason) {
|
|
217
|
+
if (!reason) return '—';
|
|
218
|
+
switch (reason) {
|
|
219
|
+
case 'chain_limit_reached': return chalk.cyan('chain limit reached');
|
|
220
|
+
case 'non_chainable_status': return chalk.yellow('non-chainable status');
|
|
221
|
+
case 'operator_abort': return chalk.red('operator abort');
|
|
222
|
+
case 'parent_validation_failed': return chalk.red('parent validation failed');
|
|
223
|
+
case 'completed': return chalk.green('completed');
|
|
224
|
+
case 'blocked': return chalk.yellow('blocked');
|
|
225
|
+
default: return reason;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatStatus(status) {
|
|
230
|
+
if (status === 'completed') return chalk.green('completed');
|
|
231
|
+
if (status === 'blocked') return chalk.yellow('blocked');
|
|
232
|
+
if (status === 'failed') return chalk.red('failed');
|
|
233
|
+
return status || '—';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatDuration(ms) {
|
|
237
|
+
if (ms < 1000) return `${ms}ms`;
|
|
238
|
+
const seconds = Math.floor(ms / 1000);
|
|
239
|
+
if (seconds < 60) return `${seconds}s`;
|
|
240
|
+
const minutes = Math.floor(seconds / 60);
|
|
241
|
+
const remainingSeconds = seconds % 60;
|
|
242
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
243
|
+
const hours = Math.floor(minutes / 60);
|
|
244
|
+
const remainingMinutes = minutes % 60;
|
|
245
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function pad(str, width) {
|
|
249
|
+
return String(str).padEnd(width);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Data Loading ──────────────────────────────────────────────────────────────
|
package/src/commands/diff.js
CHANGED
|
@@ -215,9 +215,28 @@ function formatValue(value, label = '') {
|
|
|
215
215
|
if (typeof value === 'boolean') return value ? 'yes' : 'no';
|
|
216
216
|
if (label === 'Cost' || label === 'Budget') return `$${value.toFixed(4)}`;
|
|
217
217
|
if (label === 'Duration') return formatDuration(value);
|
|
218
|
+
if (label === 'Blocked reason' && value && typeof value === 'object') {
|
|
219
|
+
return formatBlockedReason(value);
|
|
220
|
+
}
|
|
221
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
218
222
|
return String(value);
|
|
219
223
|
}
|
|
220
224
|
|
|
225
|
+
function formatBlockedReason(reason) {
|
|
226
|
+
const category = reason.category || 'unknown';
|
|
227
|
+
const gateAction = reason.gate_action;
|
|
228
|
+
if (category === 'gate_action_failed' && gateAction) {
|
|
229
|
+
const actionLabel = gateAction.action_label || gateAction.command || 'unknown action';
|
|
230
|
+
if (gateAction.timed_out) {
|
|
231
|
+
return `gate_action_failed: ${actionLabel} timed out after ${gateAction.timeout_ms}ms`;
|
|
232
|
+
}
|
|
233
|
+
const exit = gateAction.exit_code != null ? ` (exit ${gateAction.exit_code})` : '';
|
|
234
|
+
return `gate_action_failed: ${actionLabel} failed${exit}`;
|
|
235
|
+
}
|
|
236
|
+
const detail = reason.detail || reason.recovery?.detail || '';
|
|
237
|
+
return detail ? `${category}: ${detail}` : category;
|
|
238
|
+
}
|
|
239
|
+
|
|
221
240
|
function formatDelta(delta, label) {
|
|
222
241
|
if (delta == null || delta === 0) return '';
|
|
223
242
|
if (label === 'Cost' || label === 'Budget') {
|