agentxchain 2.45.0 → 2.46.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dashboard/app.js CHANGED
@@ -15,6 +15,8 @@ import { render as renderCrossRepo } from './components/cross-repo.js';
15
15
  import { render as renderBlockers } from './components/blockers.js';
16
16
  import { render as renderArtifacts } from './components/artifacts.js';
17
17
  import { render as renderRunHistory } from './components/run-history.js';
18
+ import { render as renderTimeouts } from './components/timeouts.js';
19
+ import { render as renderCoordinatorTimeouts } from './components/coordinator-timeouts.js';
18
20
 
19
21
  const VIEWS = {
20
22
  timeline: { fetch: ['state', 'continuity', 'history', 'audit', 'annotations', 'connectors'], render: renderTimeline },
@@ -27,6 +29,8 @@ const VIEWS = {
27
29
  blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
28
30
  artifacts: { fetch: ['workflowKitArtifacts'], render: renderArtifacts },
29
31
  'run-history': { fetch: ['runHistory'], render: renderRunHistory },
32
+ timeouts: { fetch: ['timeouts'], render: renderTimeouts },
33
+ 'coordinator-timeouts': { fetch: ['coordinatorTimeouts'], render: renderCoordinatorTimeouts },
30
34
  };
31
35
 
32
36
  const API_MAP = {
@@ -45,6 +49,8 @@ const API_MAP = {
45
49
  workflowKitArtifacts: '/api/workflow-kit-artifacts',
46
50
  connectors: '/api/connectors',
47
51
  runHistory: '/api/run-history',
52
+ timeouts: '/api/timeouts',
53
+ coordinatorTimeouts: '/api/coordinator/timeouts',
48
54
  };
49
55
 
50
56
  const viewState = {
@@ -0,0 +1,220 @@
1
+ function esc(str) {
2
+ if (!str) return '';
3
+ return String(str)
4
+ .replace(/&/g, '&')
5
+ .replace(/</g, '&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.45.0",
3
+ "version": "2.46.2",
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
@@ -23,6 +23,36 @@ export async function acceptTurnCommand(opts = {}) {
23
23
  resolutionMode: opts.resolution || 'standard',
24
24
  });
25
25
  if (!result.ok) {
26
+ if (result.error_code === 'policy_escalation' || result.error_code === 'policy_violation') {
27
+ const recovery = result.state ? deriveRecoveryDescriptor(result.state, config) : null;
28
+ const retainedTurnId = result.state?.blocked_reason?.turn_id || opts.turn || '(unknown)';
29
+ const policyTitle = result.error_code === 'policy_escalation'
30
+ ? 'Turn Acceptance Escalated By Policy'
31
+ : 'Turn Acceptance Blocked By Policy';
32
+
33
+ console.log('');
34
+ console.log(chalk.yellow(` ${policyTitle}`));
35
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
36
+ console.log('');
37
+ console.log(` ${chalk.dim('Turn:')} ${retainedTurnId}`);
38
+ console.log(` ${chalk.dim('Error:')} ${result.error}`);
39
+ const violations = Array.isArray(result.policy_violations) ? result.policy_violations : [];
40
+ for (const violation of violations) {
41
+ console.log(` ${chalk.dim('Policy:')} ${violation.policy_id} (${violation.rule})`);
42
+ console.log(` ${chalk.dim('Detail:')} ${violation.message}`);
43
+ }
44
+ if (recovery) {
45
+ console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
46
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
47
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
48
+ console.log(` ${chalk.dim('Turn:')} ${recovery.turn_retained ? 'retained' : 'cleared'}`);
49
+ } else {
50
+ console.log(` ${chalk.dim('Action:')} Fix the policy condition, then rerun agentxchain accept-turn`);
51
+ }
52
+ console.log('');
53
+ process.exit(1);
54
+ }
55
+
26
56
  if (result.error_code?.startsWith('hook_') || result.error_code === 'hook_blocked') {
27
57
  const recovery = deriveRecoveryDescriptor(result.state);
28
58
  const activeTurn = result.state?.current_turn;
@@ -547,11 +547,14 @@ function buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitC
547
547
  Object.keys(roles).map((roleId) => [roleId, `.agentxchain/prompts/${roleId}.md`])
548
548
  );
549
549
 
550
+ const policies = cloneJsonCompatible(blueprint?.policies || []);
551
+
550
552
  return {
551
553
  roles,
552
554
  runtimes,
553
555
  routing,
554
556
  gates,
557
+ policies,
555
558
  prompts,
556
559
  workflowKitConfig: effectiveWorkflowKitConfig,
557
560
  };
@@ -627,7 +630,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
627
630
  const template = loadGovernedTemplate(templateId);
628
631
  const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
629
632
  const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
630
- const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
633
+ const { roles, runtimes, routing, gates, policies, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
631
634
  const scaffoldWorkflowKitConfig = effectiveWorkflowKitConfig
632
635
  ? normalizeWorkflowKit(effectiveWorkflowKitConfig, Object.keys(routing))
633
636
  : null;
@@ -667,6 +670,9 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
667
670
  max_deadlock_cycles: 2
668
671
  }
669
672
  };
673
+ if (policies && policies.length > 0) {
674
+ config.policies = policies;
675
+ }
670
676
  if (effectiveWorkflowKitConfig) {
671
677
  config.workflow_kit = effectiveWorkflowKitConfig;
672
678
  }
@@ -686,6 +692,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
686
692
  escalation: null,
687
693
  queued_phase_transition: null,
688
694
  queued_run_completion: null,
695
+ last_gate_failure: null,
689
696
  phase_gate_status: phaseGateStatus,
690
697
  budget_reservations: {},
691
698
  budget_status: {
@@ -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',