agentxchain 2.46.0 → 2.47.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.
@@ -262,7 +262,8 @@ program
262
262
  .description('Show cross-run history of governed runs in this project')
263
263
  .option('-j, --json', 'Output as JSON')
264
264
  .option('-l, --limit <n>', 'Number of recent runs to show (default: 20)')
265
- .option('-s, --status <status>', 'Filter by status: completed, blocked, failed')
265
+ .option('-s, --status <status>', 'Filter by status: completed or blocked')
266
+ .option('--lineage <run_id>', 'Show lineage chain for a specific run')
266
267
  .option('-d, --dir <path>', 'Project directory')
267
268
  .action(historyCommand);
268
269
 
@@ -355,6 +356,8 @@ program
355
356
  .option('--verbose', 'Stream adapter subprocess output')
356
357
  .option('--dry-run', 'Print what would be dispatched without executing')
357
358
  .option('--no-report', 'Suppress automatic governance report after run completes')
359
+ .option('--continue-from <run_id>', 'Continue from a prior terminal run (sets trigger=continuation)')
360
+ .option('--recover-from <run_id>', 'Recover from a prior blocked run (sets trigger=recovery)')
358
361
  .action(runCommand);
359
362
 
360
363
  program
package/dashboard/app.js CHANGED
@@ -15,6 +15,8 @@ import { render as renderCrossRepo } from './components/cross-repo.js';
15
15
  import { render as renderBlockers } from './components/blockers.js';
16
16
  import { render as renderArtifacts } from './components/artifacts.js';
17
17
  import { render as renderRunHistory } from './components/run-history.js';
18
+ import { render as renderTimeouts } from './components/timeouts.js';
19
+ import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
18
20
 
19
21
  const VIEWS = {
20
22
  timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors'], render: renderTimeline },
@@ -27,6 +29,8 @@ const VIEWS = {
27
29
  blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
28
30
  artifacts: { fetch: ['workflowKitArtifacts'], render: renderArtifacts },
29
31
  'run-history': { fetch: ['runHistory'], render: renderRunHistory },
32
+ timeouts: { fetch: ['timeouts'], render: renderTimeouts },
33
+ 'coordinator-timeouts': { fetch: ['coordinatorTimeouts'], render: renderCoordinatorTimeouts },
30
34
  };
31
35
 
32
36
  const API_MAP = {
@@ -45,6 +49,8 @@ const API_MAP = {
45
49
  workflowKitArtifacts: '/api/workflow-kit-artifacts',
46
50
  connectors: '/api/connectors',
47
51
  runHistory: '/api/run-history',
52
+ timeouts: '/api/timeouts',
53
+ coordinatorTimeouts: '/api/coordinator/timeouts',
48
54
  };
49
55
 
50
56
  const viewState = {
@@ -0,0 +1,220 @@
1
+ function esc(str) {
2
+ if (!str) return '';
3
+ return String(str)
4
+ .replace(/&/g, '&amp;')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
10
+
11
+ function badge(label, color = 'var(--text-dim)') {
12
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
13
+ }
14
+
15
+ function scopeLabel(scope) {
16
+ if (scope === 'turn') return 'Per-Turn';
17
+ if (scope === 'phase') return 'Per-Phase';
18
+ if (scope === 'run') return 'Per-Run';
19
+ return esc(scope || '—');
20
+ }
21
+
22
+ function actionBadge(action) {
23
+ if (action === 'escalate') return badge('escalate', 'var(--red)');
24
+ if (action === 'warn') return badge('warn', 'var(--yellow)');
25
+ if (action === 'skip_phase') return badge('skip_phase', 'var(--accent)');
26
+ return badge(action || '—');
27
+ }
28
+
29
+ function typeLabel(type) {
30
+ if (type === 'timeout') return badge('exceeded', 'var(--red)');
31
+ if (type === 'timeout_warning') return badge('warning', 'var(--yellow)');
32
+ if (type === 'timeout_skip') return badge('skipped', 'var(--accent)');
33
+ if (type === 'timeout_skip_failed') return badge('skip failed', 'var(--red)');
34
+ return badge(type || '—');
35
+ }
36
+
37
+ function statusColor(status) {
38
+ const colors = {
39
+ active: 'var(--green)',
40
+ blocked: 'var(--red)',
41
+ paused: 'var(--yellow)',
42
+ completed: 'var(--accent)',
43
+ idle: 'var(--text-dim)',
44
+ initialized: 'var(--accent)',
45
+ linked: 'var(--green)',
46
+ };
47
+ return colors[status] || 'var(--text-dim)';
48
+ }
49
+
50
+ function renderSummary(summary) {
51
+ return `<div class="gate-card"><h3>Summary</h3>
52
+ <dl class="detail-list">
53
+ <dt>Repos</dt><dd>${summary.repo_count}</dd>
54
+ <dt>Timeout Configured</dt><dd>${summary.configured_repo_count}</dd>
55
+ <dt>Live Exceeded</dt><dd>${summary.repos_with_live_exceeded}</dd>
56
+ <dt>Live Warnings</dt><dd>${summary.repos_with_live_warnings}</dd>
57
+ <dt>Repo Events</dt><dd>${summary.repo_event_count}</dd>
58
+ <dt>Coordinator Events</dt><dd>${summary.coordinator_event_count}</dd>
59
+ </dl>
60
+ </div>`;
61
+ }
62
+
63
+ function renderConfigTable(config) {
64
+ if (!config) {
65
+ return `<p style="color:var(--text-dim)">No <code>timeouts</code> configured in this repo.</p>`;
66
+ }
67
+
68
+ let html = `<table class="data-table">
69
+ <thead><tr><th>Scope</th><th>Limit</th><th>Action</th></tr></thead>
70
+ <tbody>`;
71
+ if (config.per_turn_minutes) {
72
+ html += `<tr><td>Per-Turn</td><td>${config.per_turn_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
73
+ }
74
+ if (config.per_phase_minutes) {
75
+ html += `<tr><td>Per-Phase</td><td>${config.per_phase_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
76
+ }
77
+ if (config.per_run_minutes) {
78
+ html += `<tr><td>Per-Run</td><td>${config.per_run_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
79
+ }
80
+ for (const override of (config.phase_overrides || [])) {
81
+ html += `<tr><td>Phase: <strong>${esc(override.phase)}</strong></td><td>${override.limit_minutes ? `${override.limit_minutes}m` : 'inherit'}</td><td>${override.action ? actionBadge(override.action) : 'inherit'}</td></tr>`;
82
+ }
83
+ html += `</tbody></table>`;
84
+ return html;
85
+ }
86
+
87
+ function renderLiveTable(live) {
88
+ const exceeded = live?.exceeded || [];
89
+ const warnings = live?.warnings || [];
90
+ if (exceeded.length === 0 && warnings.length === 0) {
91
+ return `<p style="color:var(--green)">No live timeout pressure.</p>`;
92
+ }
93
+
94
+ let html = `<table class="data-table">
95
+ <thead><tr><th>Status</th><th>Scope</th><th>Turn</th><th>Phase</th><th>Elapsed</th><th>Limit</th><th>Exceeded By</th><th>Action</th></tr></thead>
96
+ <tbody>`;
97
+ for (const item of exceeded) {
98
+ const turnLabel = item.turn_id
99
+ ? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
100
+ : '—';
101
+ html += `<tr style="border-left:3px solid var(--red)">
102
+ <td>${badge('EXCEEDED', 'var(--red)')}</td>
103
+ <td>${scopeLabel(item.scope)}</td>
104
+ <td>${turnLabel}</td>
105
+ <td>${item.phase ? esc(item.phase) : '—'}</td>
106
+ <td>${item.elapsed_minutes}m</td>
107
+ <td>${item.limit_minutes}m</td>
108
+ <td style="color:var(--red)">${item.exceeded_by_minutes}m</td>
109
+ <td>${actionBadge(item.action)}</td>
110
+ </tr>`;
111
+ }
112
+ for (const item of warnings) {
113
+ const turnLabel = item.turn_id
114
+ ? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
115
+ : '—';
116
+ html += `<tr style="border-left:3px solid var(--yellow)">
117
+ <td>${badge('WARNING', 'var(--yellow)')}</td>
118
+ <td>${scopeLabel(item.scope)}</td>
119
+ <td>${turnLabel}</td>
120
+ <td>${item.phase ? esc(item.phase) : '—'}</td>
121
+ <td>${item.elapsed_minutes}m</td>
122
+ <td>${item.limit_minutes}m</td>
123
+ <td>${item.exceeded_by_minutes}m</td>
124
+ <td>${actionBadge(item.action)}</td>
125
+ </tr>`;
126
+ }
127
+ html += `</tbody></table>`;
128
+ return html;
129
+ }
130
+
131
+ function renderEventTable(events, title) {
132
+ if (!Array.isArray(events) || events.length === 0) {
133
+ return `<div class="section"><h3>${title}</h3><p style="color:var(--text-dim)">No timeout events recorded.</p></div>`;
134
+ }
135
+
136
+ let html = `<div class="section"><h3>${title}</h3>
137
+ <table class="data-table">
138
+ <thead><tr><th>Type</th><th>Scope</th><th>Phase</th><th>Turn</th><th>Elapsed</th><th>Limit</th><th>Action</th></tr></thead>
139
+ <tbody>`;
140
+ for (const event of events) {
141
+ html += `<tr>
142
+ <td>${typeLabel(event.type)}</td>
143
+ <td>${scopeLabel(event.scope)}</td>
144
+ <td>${event.phase ? esc(event.phase) : '—'}</td>
145
+ <td class="mono">${event.turn_id ? esc(event.turn_id) : '—'}</td>
146
+ <td>${event.elapsed_minutes != null ? `${event.elapsed_minutes}m` : '—'}</td>
147
+ <td>${event.limit_minutes != null ? `${event.limit_minutes}m` : '—'}</td>
148
+ <td>${actionBadge(event.action)}</td>
149
+ </tr>`;
150
+ }
151
+ html += `</tbody></table></div>`;
152
+ return html;
153
+ }
154
+
155
+ function renderRepoCard(repo) {
156
+ let html = `<div class="turn-card">
157
+ <div class="turn-header">
158
+ <span class="mono">${esc(repo.repo_id)}</span>
159
+ ${repo.status ? badge(repo.status, statusColor(repo.status)) : ''}
160
+ ${repo.configured ? badge('timeouts configured', 'var(--green)') : badge('no timeouts', 'var(--text-dim)')}
161
+ </div>`;
162
+
163
+ html += `<dl class="detail-list">
164
+ <dt>Path</dt><dd class="mono">${esc(repo.path)}</dd>`;
165
+ if (repo.run_id) {
166
+ html += `<dt>Run</dt><dd class="mono">${esc(repo.run_id)}</dd>`;
167
+ }
168
+ if (repo.phase) {
169
+ html += `<dt>Phase</dt><dd>${esc(repo.phase)}</dd>`;
170
+ }
171
+ html += `</dl>`;
172
+
173
+ if (repo.error) {
174
+ html += `<p style="color:var(--red)">${esc(repo.error.error)}</p></div>`;
175
+ return html;
176
+ }
177
+
178
+ html += `<div class="section"><h3>Configuration</h3>${renderConfigTable(repo.config)}</div>`;
179
+ if (repo.live) {
180
+ html += `<div class="section"><h3>Live Pressure</h3>${renderLiveTable(repo.live)}</div>`;
181
+ }
182
+ html += renderEventTable(repo.events, 'Repo Events');
183
+ html += `</div>`;
184
+ return html;
185
+ }
186
+
187
+ export function render({ coordinatorTimeouts }) {
188
+ if (!coordinatorTimeouts) {
189
+ return `<div class="placeholder"><h2>Coordinator Timeouts</h2><p>No coordinator timeout data available. Ensure a coordinator run is active.</p></div>`;
190
+ }
191
+
192
+ if (coordinatorTimeouts.ok === false) {
193
+ return `<div class="placeholder"><h2>Coordinator Timeouts</h2><p>${esc(coordinatorTimeouts.error || 'Failed to load coordinator timeout data.')}</p></div>`;
194
+ }
195
+
196
+ let html = `<div class="timeouts-view"><div class="run-header"><div class="run-meta">`;
197
+ if (coordinatorTimeouts.super_run_id) {
198
+ html += `<span class="mono run-id">${esc(coordinatorTimeouts.super_run_id)}</span>`;
199
+ }
200
+ if (coordinatorTimeouts.status) {
201
+ html += badge(coordinatorTimeouts.status, statusColor(coordinatorTimeouts.status));
202
+ }
203
+ if (coordinatorTimeouts.phase) {
204
+ html += `<span class="phase-label">Phase: <strong>${esc(coordinatorTimeouts.phase)}</strong></span>`;
205
+ }
206
+ html += `${badge('coordinator timeout view', 'var(--accent)')}</div></div>`;
207
+
208
+ if (coordinatorTimeouts.blocked_reason) {
209
+ html += `<div class="blocked-banner"><div class="blocked-icon">BLOCKED</div><div class="blocked-reason">${esc(typeof coordinatorTimeouts.blocked_reason === 'string' ? coordinatorTimeouts.blocked_reason : JSON.stringify(coordinatorTimeouts.blocked_reason))}</div></div>`;
210
+ }
211
+
212
+ html += renderSummary(coordinatorTimeouts.summary);
213
+ html += renderEventTable(coordinatorTimeouts.coordinator_events, 'Coordinator Events');
214
+ html += `<div class="section"><h3>Repo Timeout Status</h3><div class="turn-list">`;
215
+ for (const repo of (coordinatorTimeouts.repos || [])) {
216
+ html += renderRepoCard(repo);
217
+ }
218
+ html += `</div></div></div>`;
219
+ return html;
220
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Timeouts view — renders live timeout pressure and persisted timeout events.
3
+ *
4
+ * Pure render function: takes data from /api/timeouts, returns HTML.
5
+ * All evaluation is server-side. This view renders the snapshot.
6
+ *
7
+ * See: TIMEOUT_DASHBOARD_SURFACE_SPEC.md
8
+ */
9
+
10
+ function esc(str) {
11
+ if (!str) return '';
12
+ return String(str)
13
+ .replace(/&/g, '&amp;')
14
+ .replace(/</g, '&lt;')
15
+ .replace(/>/g, '&gt;')
16
+ .replace(/"/g, '&quot;')
17
+ .replace(/'/g, '&#39;');
18
+ }
19
+
20
+ function badge(label, color = 'var(--text-dim)') {
21
+ return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
22
+ }
23
+
24
+ function scopeLabel(scope) {
25
+ if (scope === 'turn') return 'Per-Turn';
26
+ if (scope === 'phase') return 'Per-Phase';
27
+ if (scope === 'run') return 'Per-Run';
28
+ return esc(scope || '—');
29
+ }
30
+
31
+ function actionBadge(action) {
32
+ if (action === 'escalate') return badge('escalate', 'var(--red)');
33
+ if (action === 'warn') return badge('warn', 'var(--yellow)');
34
+ if (action === 'skip_phase') return badge('skip_phase', 'var(--accent)');
35
+ return badge(action || '—', 'var(--text-dim)');
36
+ }
37
+
38
+ function typeLabel(type) {
39
+ if (type === 'timeout') return badge('exceeded', 'var(--red)');
40
+ if (type === 'timeout_warning') return badge('warning', 'var(--yellow)');
41
+ if (type === 'timeout_skip') return badge('skipped', 'var(--accent)');
42
+ if (type === 'timeout_skip_failed') return badge('skip failed', 'var(--red)');
43
+ return badge(type || '—');
44
+ }
45
+
46
+ function formatTime(ts) {
47
+ if (!ts) return '—';
48
+ try {
49
+ const d = new Date(ts);
50
+ return d.toLocaleString();
51
+ } catch {
52
+ return esc(ts);
53
+ }
54
+ }
55
+
56
+ function renderConfigTable(config) {
57
+ let html = `<div class="section"><h3>Timeout Configuration</h3>
58
+ <table class="data-table">
59
+ <thead><tr><th>Scope</th><th>Limit</th><th>Action</th></tr></thead>
60
+ <tbody>`;
61
+
62
+ if (config.per_turn_minutes) {
63
+ html += `<tr><td>Per-Turn</td><td>${config.per_turn_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
64
+ }
65
+ if (config.per_phase_minutes) {
66
+ html += `<tr><td>Per-Phase (global)</td><td>${config.per_phase_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
67
+ }
68
+ if (config.per_run_minutes) {
69
+ html += `<tr><td>Per-Run</td><td>${config.per_run_minutes}m</td><td>${actionBadge(config.action)}</td></tr>`;
70
+ }
71
+
72
+ if (Array.isArray(config.phase_overrides)) {
73
+ for (const override of config.phase_overrides) {
74
+ const limitStr = override.limit_minutes ? `${override.limit_minutes}m` : '<span style="color:var(--text-dim)">inherit</span>';
75
+ const actionStr = override.action ? actionBadge(override.action) : '<span style="color:var(--text-dim)">inherit</span>';
76
+ html += `<tr><td>Phase: <strong>${esc(override.phase)}</strong></td><td>${limitStr}</td><td>${actionStr}</td></tr>`;
77
+ }
78
+ }
79
+
80
+ html += `</tbody></table></div>`;
81
+ return html;
82
+ }
83
+
84
+ function renderLivePressure(live) {
85
+ const hasExceeded = live.exceeded && live.exceeded.length > 0;
86
+ const hasWarnings = live.warnings && live.warnings.length > 0;
87
+
88
+ if (!hasExceeded && !hasWarnings) {
89
+ return `<div class="section"><h3>Live Pressure</h3><p style="color:var(--green)">No timeouts exceeded or approaching limits.</p></div>`;
90
+ }
91
+
92
+ let html = `<div class="section"><h3>Live Pressure</h3>
93
+ <table class="data-table">
94
+ <thead><tr><th>Status</th><th>Scope</th><th>Turn</th><th>Phase</th><th>Elapsed</th><th>Limit</th><th>Exceeded By</th><th>Action</th></tr></thead>
95
+ <tbody>`;
96
+
97
+ for (const item of (live.exceeded || [])) {
98
+ const turnLabel = item.turn_id
99
+ ? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
100
+ : '—';
101
+ html += `<tr style="border-left:3px solid var(--red)">
102
+ <td>${badge('EXCEEDED', 'var(--red)')}</td>
103
+ <td>${scopeLabel(item.scope)}</td>
104
+ <td>${turnLabel}</td>
105
+ <td>${item.phase ? esc(item.phase) : '—'}</td>
106
+ <td>${item.elapsed_minutes}m</td>
107
+ <td>${item.limit_minutes}m</td>
108
+ <td style="color:var(--red)">${item.exceeded_by_minutes}m</td>
109
+ <td>${actionBadge(item.action)}</td>
110
+ </tr>`;
111
+ }
112
+
113
+ for (const item of (live.warnings || [])) {
114
+ const turnLabel = item.turn_id
115
+ ? `<span class="mono">${esc(item.turn_id)}</span>${item.role_id ? ` <span style="color:var(--text-dim)">(${esc(item.role_id)})</span>` : ''}`
116
+ : '—';
117
+ html += `<tr style="border-left:3px solid var(--yellow)">
118
+ <td>${badge('WARNING', 'var(--yellow)')}</td>
119
+ <td>${scopeLabel(item.scope)}</td>
120
+ <td>${turnLabel}</td>
121
+ <td>${item.phase ? esc(item.phase) : '—'}</td>
122
+ <td>${item.elapsed_minutes}m</td>
123
+ <td>${item.limit_minutes}m</td>
124
+ <td>${item.exceeded_by_minutes}m</td>
125
+ <td>${actionBadge(item.action)}</td>
126
+ </tr>`;
127
+ }
128
+
129
+ html += `</tbody></table></div>`;
130
+ return html;
131
+ }
132
+
133
+ function renderEvents(events) {
134
+ if (!Array.isArray(events) || events.length === 0) {
135
+ return `<div class="section"><h3>Timeout Events</h3><p style="color:var(--text-dim)">No timeout events recorded in the decision ledger.</p></div>`;
136
+ }
137
+
138
+ let html = `<div class="section"><h3>Timeout Events</h3>
139
+ <table class="data-table">
140
+ <thead><tr><th>Type</th><th>Scope</th><th>Phase</th><th>Turn</th><th>Elapsed</th><th>Limit</th><th>Action</th><th>Timestamp</th></tr></thead>
141
+ <tbody>`;
142
+
143
+ for (const event of events) {
144
+ html += `<tr>
145
+ <td>${typeLabel(event.type)}</td>
146
+ <td>${scopeLabel(event.scope)}</td>
147
+ <td>${event.phase ? esc(event.phase) : '—'}</td>
148
+ <td class="mono">${event.turn_id ? esc(event.turn_id) : '—'}</td>
149
+ <td>${event.elapsed_minutes != null ? `${event.elapsed_minutes}m` : '—'}</td>
150
+ <td>${event.limit_minutes != null ? `${event.limit_minutes}m` : '—'}</td>
151
+ <td>${actionBadge(event.action)}</td>
152
+ <td>${formatTime(event.timestamp)}</td>
153
+ </tr>`;
154
+ }
155
+
156
+ html += `</tbody></table></div>`;
157
+ return html;
158
+ }
159
+
160
+ export function render({ timeouts }) {
161
+ if (!timeouts) {
162
+ return `<div class="placeholder"><h2>Timeouts</h2><p>No timeout data available. Ensure a governed run is active.</p></div>`;
163
+ }
164
+
165
+ if (timeouts.ok === false) {
166
+ const hint = timeouts.code === 'config_missing' || timeouts.code === 'state_missing'
167
+ ? ' Run <code>agentxchain init --governed</code> to get started.'
168
+ : '';
169
+ return `<div class="placeholder"><h2>Timeouts</h2><p>${esc(timeouts.error || 'Failed to load timeout data.')}${hint}</p></div>`;
170
+ }
171
+
172
+ if (!timeouts.configured) {
173
+ return `<div class="placeholder"><h2>Timeouts</h2><p>No <code>timeouts</code> configured in <code>agentxchain.json</code>. Add a <code>timeouts</code> section to enable time-limit enforcement.</p></div>`;
174
+ }
175
+
176
+ let html = `<div class="timeouts-view">`;
177
+
178
+ // Header
179
+ html += `<div class="run-header"><div class="run-meta">`;
180
+ html += `<span class="phase-label"><strong>Timeouts</strong></span>`;
181
+ html += badge('configured', 'var(--green)');
182
+ const eventCount = Array.isArray(timeouts.events) ? timeouts.events.length : 0;
183
+ if (eventCount > 0) {
184
+ html += `<span class="turn-count">${eventCount} event${eventCount !== 1 ? 's' : ''} recorded</span>`;
185
+ }
186
+ html += `</div></div>`;
187
+
188
+ // Config summary
189
+ html += renderConfigTable(timeouts.config);
190
+
191
+ // Live pressure
192
+ if (timeouts.live) {
193
+ html += renderLivePressure(timeouts.live);
194
+ }
195
+
196
+ // Persisted events
197
+ html += renderEvents(timeouts.events);
198
+
199
+ html += `</div>`;
200
+ return html;
201
+ }
@@ -384,6 +384,8 @@
384
384
  <a href="#blockers">Blockers</a>
385
385
  <a href="#artifacts">Artifacts</a>
386
386
  <a href="#run-history">Run History</a>
387
+ <a href="#timeouts">Timeouts</a>
388
+ <a href="#coordinator-timeouts">Coordinator Timeouts</a>
387
389
  </nav>
388
390
  <main id="view-container">
389
391
  <div class="placeholder">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.46.0",
3
+ "version": "2.47.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,10 +16,35 @@ cleanup() {
16
16
  trap cleanup EXIT
17
17
 
18
18
  usage() {
19
- echo "Usage: bash scripts/publish-from-tag.sh <vX.Y.Z>" >&2
19
+ echo "Usage: bash scripts/publish-from-tag.sh [--skip-preflight] <vX.Y.Z>" >&2
20
20
  }
21
21
 
22
- TAG="${1:-}"
22
+ SKIP_PREFLIGHT=0
23
+ TAG=""
24
+
25
+ while [[ $# -gt 0 ]]; do
26
+ case "$1" in
27
+ --skip-preflight)
28
+ SKIP_PREFLIGHT=1
29
+ shift
30
+ ;;
31
+ -*)
32
+ echo "Error: unknown option '$1'" >&2
33
+ usage
34
+ exit 1
35
+ ;;
36
+ *)
37
+ if [[ -n "$TAG" ]]; then
38
+ echo "Error: release tag must be provided exactly once" >&2
39
+ usage
40
+ exit 1
41
+ fi
42
+ TAG="$1"
43
+ shift
44
+ ;;
45
+ esac
46
+ done
47
+
23
48
  if [[ -z "$TAG" ]]; then
24
49
  echo "Error: release tag is required" >&2
25
50
  usage
@@ -55,8 +80,12 @@ if ! [[ "$RETRY_DELAY_SECONDS" =~ ^[0-9]+$ ]]; then
55
80
  fi
56
81
 
57
82
  echo "Publishing ${PACKAGE_NAME}@${RELEASE_VERSION} from ${TAG}"
58
- echo "Running strict release preflight..."
59
- bash scripts/release-preflight.sh --strict --target-version "${RELEASE_VERSION}"
83
+ if [[ "$SKIP_PREFLIGHT" -eq 1 ]]; then
84
+ echo "Skipping strict release preflight because the caller already owns tagged-state verification."
85
+ else
86
+ echo "Running strict release preflight..."
87
+ bash scripts/release-preflight.sh --strict --target-version "${RELEASE_VERSION}"
88
+ fi
60
89
 
61
90
  EXISTING_VERSION="$(npm view "${PACKAGE_NAME}@${RELEASE_VERSION}" version 2>/dev/null || true)"
62
91
  if [[ "$EXISTING_VERSION" == "$RELEASE_VERSION" ]]; then
@@ -7,7 +7,8 @@
7
7
  import { resolve } from 'path';
8
8
  import { existsSync, readFileSync } from 'fs';
9
9
  import chalk from 'chalk';
10
- import { queryRunHistory } from '../lib/run-history.js';
10
+ import { queryRunHistory, queryRunLineage } from '../lib/run-history.js';
11
+ import { getRunTriggerLabel, summarizeRunProvenance } from '../lib/run-provenance.js';
11
12
 
12
13
  /**
13
14
  * @param {object} opts - { json?: boolean, limit?: number, status?: string, dir?: string }
@@ -19,6 +20,42 @@ export async function historyCommand(opts) {
19
20
  process.exit(1);
20
21
  }
21
22
 
23
+ // ── Lineage mode ─────────────────────────────────────────────────────────
24
+ if (opts.lineage) {
25
+ const result = queryRunLineage(root, opts.lineage);
26
+ if (!result.ok) {
27
+ console.error(chalk.red(result.error));
28
+ process.exit(1);
29
+ }
30
+
31
+ if (opts.json) {
32
+ console.log(JSON.stringify(result.chain, null, 2));
33
+ return;
34
+ }
35
+
36
+ console.log(chalk.bold(`Run Lineage for ${opts.lineage}:`));
37
+ result.chain.forEach((entry, i) => {
38
+ if (entry.broken_link) {
39
+ const prefix = i === 0 ? ' ' : ' └─ ';
40
+ console.log(chalk.red(`${prefix}[broken link: ${entry.missing_run_id}]`));
41
+ return;
42
+ }
43
+ const runId = (entry.run_id || '—').slice(0, 12);
44
+ const status = formatStatus(entry.status);
45
+ const phases = (entry.phases_completed || []).join(',') || '—';
46
+ const turns = `${entry.total_turns || 0} turns`;
47
+ const cost = entry.total_cost_usd != null ? `$${entry.total_cost_usd.toFixed(2)}` : '';
48
+ const trigger = getRunTriggerLabel(entry.provenance);
49
+ const parentNote = entry.provenance?.parent_run_id
50
+ ? ` from ${entry.provenance.parent_run_id.slice(0, 12)}`
51
+ : '';
52
+ const prefix = i === 0 ? ' ' : ' └─ ';
53
+ console.log(`${prefix}${runId} ${status} ${pad(phases, 20)} ${pad(turns, 10)} ${pad(cost, 8)} (${trigger}${parentNote})`);
54
+ });
55
+ return;
56
+ }
57
+
58
+ // ── Standard history view ────────────────────────────────────────────────
22
59
  const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
23
60
  const entries = queryRunHistory(root, {
24
61
  limit,
@@ -43,6 +80,7 @@ export async function historyCommand(opts) {
43
80
  pad('#', 4),
44
81
  pad('Run ID', 14),
45
82
  pad('Status', 11),
83
+ pad('Trigger', 14),
46
84
  pad('Phases', 8),
47
85
  pad('Turns', 6),
48
86
  pad('Cost', 10),
@@ -57,6 +95,7 @@ export async function historyCommand(opts) {
57
95
  const idx = String(i + 1);
58
96
  const runId = (entry.run_id || '—').slice(0, 12);
59
97
  const status = formatStatus(entry.status);
98
+ const trigger = getRunTriggerLabel(entry.provenance);
60
99
  const phases = String(entry.phases_completed?.length || 0);
61
100
  const turns = String(entry.total_turns || 0);
62
101
  const cost = entry.total_cost_usd != null
@@ -73,6 +112,7 @@ export async function historyCommand(opts) {
73
112
  pad(idx, 4),
74
113
  pad(runId, 14),
75
114
  pad(status, 11),
115
+ pad(trigger, 14),
76
116
  pad(phases, 8),
77
117
  pad(turns, 6),
78
118
  pad(cost, 10),
@@ -692,6 +692,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
692
692
  escalation: null,
693
693
  queued_phase_transition: null,
694
694
  queued_run_completion: null,
695
+ last_gate_failure: null,
695
696
  phase_gate_status: phaseGateStatus,
696
697
  budget_reservations: {},
697
698
  budget_status: {
@@ -218,6 +218,7 @@ export async function migrateCommand(opts) {
218
218
  escalation: null,
219
219
  queued_phase_transition: null,
220
220
  queued_run_completion: null,
221
+ last_gate_failure: null,
221
222
  phase_gate_status: {
222
223
  planning_signoff: inferredPhase === 'planning' ? 'pending' : 'passed',
223
224
  implementation_complete: inferredPhase === 'qa' ? 'passed' : 'pending',