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.
@@ -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' ? '❌ failed' : '✅ succeeded';
124
- const exitStr = action.exit_code != null ? ` (exit ${action.exit_code})` : '';
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, '&amp;')
12
+ .replace(/</g, '&lt;')
13
+ .replace(/>/g, '&gt;')
14
+ .replace(/"/g, '&quot;')
15
+ .replace(/'/g, '&#39;');
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
- const exitStr = a.exit_code != null ? ` (exit ${a.exit_code})` : '';
245
- html += `<li>${outcome} ${esc(aLabel)}${esc(exitStr)}</li>`;
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.109.0",
3
+ "version": "2.111.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- console.log(` ${action.index}. ${action.label || action.run}`);
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:')} ${failure?.exit_code ?? failure?.signal ?? 'unknown'}`);
114
- if (failure?.stderr_tail) {
115
- console.log(` ${chalk.dim('stderr:')} ${failure.stderr_tail}`);
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
- console.log(` ${action.index}. ${action.label || action.run}`);
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:')} ${failure?.exit_code ?? failure?.signal ?? 'unknown'}`);
123
- if (failure?.stderr_tail) {
124
- console.log(` ${chalk.dim('stderr:')} ${failure.stderr_tail}`);
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 ──────────────────────────────────────────────────────────────
@@ -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') {