agentxchain 2.24.1 → 2.24.3
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 +4 -1
- package/dashboard/components/blockers.js +268 -0
- package/dashboard/components/initiative.js +84 -2
- package/dashboard/index.html +1 -0
- package/package.json +1 -1
- package/scripts/release-postflight.sh +32 -8
- package/src/commands/demo.js +1 -1
- package/src/commands/init.js +26 -0
- package/src/commands/multi.js +6 -1
- package/src/commands/run.js +34 -1
- package/src/commands/step.js +13 -1
- package/src/lib/adapters/manual-adapter.js +77 -8
- package/src/lib/coordinator-gates.js +34 -2
- package/src/lib/coordinator-recovery.js +18 -7
- package/src/lib/dashboard/bridge-server.js +8 -0
- package/src/lib/dashboard/coordinator-blockers.js +168 -0
- package/src/lib/report.js +51 -3
package/dashboard/app.js
CHANGED
|
@@ -12,6 +12,7 @@ import { render as renderBlocked } from './components/blocked.js';
|
|
|
12
12
|
import { render as renderGate } from './components/gate.js';
|
|
13
13
|
import { render as renderInitiative } from './components/initiative.js';
|
|
14
14
|
import { render as renderCrossRepo } from './components/cross-repo.js';
|
|
15
|
+
import { render as renderBlockers } from './components/blockers.js';
|
|
15
16
|
|
|
16
17
|
const VIEWS = {
|
|
17
18
|
timeline: { fetch: ['state', 'history', 'audit', 'annotations'], render: renderTimeline },
|
|
@@ -19,8 +20,9 @@ const VIEWS = {
|
|
|
19
20
|
hooks: { fetch: ['audit', 'annotations'], render: renderHooks },
|
|
20
21
|
blocked: { fetch: ['state', 'audit', 'coordinatorState', 'coordinatorAudit'], render: renderBlocked },
|
|
21
22
|
gate: { fetch: ['state', 'history', 'coordinatorState', 'coordinatorHistory', 'coordinatorBarriers'], render: renderGate },
|
|
22
|
-
initiative: { fetch: ['coordinatorState', 'coordinatorBarriers', 'barrierLedger'], render: renderInitiative },
|
|
23
|
+
initiative: { fetch: ['coordinatorState', 'coordinatorBarriers', 'barrierLedger', 'coordinatorBlockers'], render: renderInitiative },
|
|
23
24
|
'cross-repo': { fetch: ['coordinatorState', 'coordinatorHistory'], render: renderCrossRepo },
|
|
25
|
+
blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
|
|
24
26
|
};
|
|
25
27
|
|
|
26
28
|
const API_MAP = {
|
|
@@ -34,6 +36,7 @@ const API_MAP = {
|
|
|
34
36
|
coordinatorBarriers: '/api/coordinator/barriers',
|
|
35
37
|
barrierLedger: '/api/coordinator/barrier-ledger',
|
|
36
38
|
coordinatorAudit: '/api/coordinator/hooks/audit',
|
|
39
|
+
coordinatorBlockers: '/api/coordinator/blockers',
|
|
37
40
|
};
|
|
38
41
|
|
|
39
42
|
const viewState = {
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinator Blockers view — renders computed coordinator gate blockers.
|
|
3
|
+
*
|
|
4
|
+
* Pure render function: takes data from /api/coordinator/blockers, returns HTML.
|
|
5
|
+
* All blocker semantics are server-side (coordinator-blockers.js). This view
|
|
6
|
+
* renders the snapshot without reimplementing gate logic.
|
|
7
|
+
*
|
|
8
|
+
* Per DEC-DASH-COORD-BLOCKERS-002: blocker evaluation must come from the same
|
|
9
|
+
* server-side gate evaluators used by `multi step` and `multi approve-gate`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function esc(str) {
|
|
13
|
+
if (!str) return '';
|
|
14
|
+
return String(str)
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function badge(label, color = 'var(--text-dim)') {
|
|
23
|
+
return `<span class="badge" style="color:${color};border-color:${color}">${esc(label)}</span>`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function modeColor(mode) {
|
|
27
|
+
const colors = {
|
|
28
|
+
pending_gate: 'var(--yellow)',
|
|
29
|
+
phase_transition: 'var(--accent)',
|
|
30
|
+
run_completion: 'var(--green)',
|
|
31
|
+
};
|
|
32
|
+
return colors[mode] || 'var(--text-dim)';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function blockerColor(code) {
|
|
36
|
+
if (code === 'repo_run_id_mismatch') return 'var(--red)';
|
|
37
|
+
if (code === 'no_next_phase') return 'var(--text-dim)';
|
|
38
|
+
return 'var(--yellow)';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function renderBlockerRow(blocker) {
|
|
42
|
+
const code = blocker.code || 'unknown';
|
|
43
|
+
const color = blockerColor(code);
|
|
44
|
+
let html = `<div class="turn-card" style="border-left: 3px solid ${color}">
|
|
45
|
+
<div class="turn-header">
|
|
46
|
+
${badge(code, color)}
|
|
47
|
+
</div>`;
|
|
48
|
+
|
|
49
|
+
if (blocker.message) {
|
|
50
|
+
html += `<div class="turn-summary">${esc(blocker.message)}</div>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (code === 'repo_run_id_mismatch' && blocker.repo_id) {
|
|
54
|
+
html += `<dl class="detail-list">
|
|
55
|
+
<dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
|
|
56
|
+
if (blocker.expected_run_id) {
|
|
57
|
+
html += `<dt>Expected</dt><dd class="mono">${esc(blocker.expected_run_id)}</dd>`;
|
|
58
|
+
}
|
|
59
|
+
if (blocker.actual_run_id) {
|
|
60
|
+
html += `<dt>Actual</dt><dd class="mono">${esc(blocker.actual_run_id)}</dd>`;
|
|
61
|
+
}
|
|
62
|
+
html += `</dl>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (code === 'repo_not_ready' && blocker.repo_id) {
|
|
66
|
+
html += `<dl class="detail-list">
|
|
67
|
+
<dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
|
|
68
|
+
if (blocker.current_phase) {
|
|
69
|
+
html += `<dt>Current Phase</dt><dd>${esc(blocker.current_phase)}</dd>`;
|
|
70
|
+
}
|
|
71
|
+
if (blocker.required_phase) {
|
|
72
|
+
html += `<dt>Required Phase</dt><dd>${esc(blocker.required_phase)}</dd>`;
|
|
73
|
+
}
|
|
74
|
+
html += `</dl>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
html += `</div>`;
|
|
78
|
+
return html;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderActiveGate(active) {
|
|
82
|
+
if (!active) return '';
|
|
83
|
+
|
|
84
|
+
let html = `<div class="gate-card">
|
|
85
|
+
<h3>Active Gate</h3>
|
|
86
|
+
<dl class="detail-list">
|
|
87
|
+
<dt>Type</dt><dd>${esc(active.gate_type)}</dd>`;
|
|
88
|
+
|
|
89
|
+
if (active.gate_id) {
|
|
90
|
+
html += `<dt>Gate</dt><dd class="mono">${esc(active.gate_id)}</dd>`;
|
|
91
|
+
}
|
|
92
|
+
if (active.current_phase) {
|
|
93
|
+
html += `<dt>Current Phase</dt><dd>${esc(active.current_phase)}</dd>`;
|
|
94
|
+
}
|
|
95
|
+
if (active.target_phase) {
|
|
96
|
+
html += `<dt>Target Phase</dt><dd>${esc(active.target_phase)}</dd>`;
|
|
97
|
+
}
|
|
98
|
+
if (typeof active.ready === 'boolean') {
|
|
99
|
+
html += `<dt>Ready</dt><dd>${active.ready ? 'Yes' : 'No'}</dd>`;
|
|
100
|
+
}
|
|
101
|
+
if (active.pending === true) {
|
|
102
|
+
html += `<dt>Pending Approval</dt><dd>Yes</dd>`;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(active.required_repos) && active.required_repos.length > 0) {
|
|
105
|
+
html += `<dt>Required Repos</dt><dd>${esc(active.required_repos.join(', '))}</dd>`;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(active.human_barriers) && active.human_barriers.length > 0) {
|
|
108
|
+
html += `<dt>Human Barriers</dt><dd>${esc(active.human_barriers.join(', '))}</dd>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
html += `</dl>`;
|
|
112
|
+
|
|
113
|
+
if (Array.isArray(active.blockers) && active.blockers.length > 0) {
|
|
114
|
+
html += `<div class="section" style="margin-top:12px"><h3>Blockers (${active.blockers.length})</h3>
|
|
115
|
+
<div class="turn-list">`;
|
|
116
|
+
for (const blocker of active.blockers) {
|
|
117
|
+
html += renderBlockerRow(blocker);
|
|
118
|
+
}
|
|
119
|
+
html += `</div></div>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
html += `</div>`;
|
|
123
|
+
return html;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderRecoveryCommand(data) {
|
|
127
|
+
if (data.mode === 'pending_gate') {
|
|
128
|
+
return `<div class="section"><h3>Recovery</h3>
|
|
129
|
+
<p class="recovery-hint">Approve the pending gate:</p>
|
|
130
|
+
<pre class="recovery-command mono" data-copy="agentxchain multi approve-gate">agentxchain multi approve-gate</pre>
|
|
131
|
+
</div>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hasRunIdMismatch = Array.isArray(data.active?.blockers)
|
|
135
|
+
&& data.active.blockers.some(b => b.code === 'repo_run_id_mismatch');
|
|
136
|
+
|
|
137
|
+
if (hasRunIdMismatch) {
|
|
138
|
+
return `<div class="section"><h3>Recovery</h3>
|
|
139
|
+
<p class="recovery-hint">Run identity drift detected. Investigate child repos before resuming:</p>
|
|
140
|
+
<pre class="recovery-command mono" data-copy="agentxchain multi resume">agentxchain multi resume</pre>
|
|
141
|
+
</div>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function render({ coordinatorBlockers }) {
|
|
148
|
+
if (!coordinatorBlockers) {
|
|
149
|
+
return `<div class="placeholder"><h2>Coordinator Blockers</h2><p>No coordinator blocker data available. Ensure a coordinator run is active.</p></div>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (coordinatorBlockers.ok === false) {
|
|
153
|
+
return `<div class="placeholder"><h2>Coordinator Blockers</h2><p>${esc(coordinatorBlockers.error || 'Failed to load coordinator blockers.')}</p></div>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const data = coordinatorBlockers;
|
|
157
|
+
const blockers = data.active?.blockers || [];
|
|
158
|
+
const hasBlockers = blockers.length > 0 && !blockers.every(b => b.code === 'no_next_phase');
|
|
159
|
+
|
|
160
|
+
let html = `<div class="blockers-view">`;
|
|
161
|
+
|
|
162
|
+
// Header
|
|
163
|
+
html += `<div class="run-header">
|
|
164
|
+
<div class="run-meta">`;
|
|
165
|
+
if (data.super_run_id) {
|
|
166
|
+
html += `<span class="mono run-id">${esc(data.super_run_id)}</span>`;
|
|
167
|
+
}
|
|
168
|
+
if (data.status) {
|
|
169
|
+
const statusColors = { active: 'var(--green)', blocked: 'var(--red)', completed: 'var(--accent)', paused: 'var(--yellow)' };
|
|
170
|
+
html += badge(data.status, statusColors[data.status] || 'var(--text-dim)');
|
|
171
|
+
}
|
|
172
|
+
html += `${badge(data.mode, modeColor(data.mode))}`;
|
|
173
|
+
if (data.phase) {
|
|
174
|
+
html += `<span class="phase-label">Phase: <strong>${esc(data.phase)}</strong></span>`;
|
|
175
|
+
}
|
|
176
|
+
html += `</div></div>`;
|
|
177
|
+
|
|
178
|
+
// Blocked reason
|
|
179
|
+
if (data.blocked_reason) {
|
|
180
|
+
html += `<div class="blocked-banner">
|
|
181
|
+
<div class="blocked-icon">BLOCKED</div>
|
|
182
|
+
<div class="blocked-reason">${esc(
|
|
183
|
+
typeof data.blocked_reason === 'string'
|
|
184
|
+
? data.blocked_reason
|
|
185
|
+
: JSON.stringify(data.blocked_reason)
|
|
186
|
+
)}</div>
|
|
187
|
+
</div>`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Status summary
|
|
191
|
+
if (!hasBlockers && data.mode === 'pending_gate') {
|
|
192
|
+
html += `<div class="gate-card"><h3>Awaiting Approval</h3>
|
|
193
|
+
<p class="turn-summary">All prerequisites are satisfied. The coordinator is waiting for human gate approval.</p></div>`;
|
|
194
|
+
} else if (!hasBlockers) {
|
|
195
|
+
html += `<div class="gate-card"><h3>No Blockers</h3>
|
|
196
|
+
<p class="turn-summary">The coordinator gate has no outstanding blockers.</p></div>`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Active gate detail
|
|
200
|
+
html += renderActiveGate(data.active);
|
|
201
|
+
|
|
202
|
+
// Recovery
|
|
203
|
+
html += renderRecoveryCommand(data);
|
|
204
|
+
|
|
205
|
+
// Evaluations summary (collapsed detail for deeper inspection)
|
|
206
|
+
if (data.evaluations) {
|
|
207
|
+
html += `<div class="section"><h3>Gate Evaluations</h3>`;
|
|
208
|
+
const { phase_transition, run_completion } = data.evaluations;
|
|
209
|
+
|
|
210
|
+
if (phase_transition) {
|
|
211
|
+
html += `<div class="turn-card" data-turn-expand>
|
|
212
|
+
<div class="turn-header">
|
|
213
|
+
<span>Phase Transition</span>
|
|
214
|
+
${badge(phase_transition.ready ? 'ready' : 'not ready', phase_transition.ready ? 'var(--green)' : 'var(--yellow)')}
|
|
215
|
+
</div>
|
|
216
|
+
<div class="turn-detail-panel">
|
|
217
|
+
<dl class="detail-list">`;
|
|
218
|
+
if (phase_transition.current_phase) html += `<dt>Current</dt><dd>${esc(phase_transition.current_phase)}</dd>`;
|
|
219
|
+
if (phase_transition.target_phase) html += `<dt>Target</dt><dd>${esc(phase_transition.target_phase)}</dd>`;
|
|
220
|
+
if (phase_transition.gate_id) html += `<dt>Gate</dt><dd class="mono">${esc(phase_transition.gate_id)}</dd>`;
|
|
221
|
+
html += `<dt>Blockers</dt><dd>${phase_transition.blockers?.length || 0}</dd>`;
|
|
222
|
+
html += `</dl>`;
|
|
223
|
+
if (phase_transition.blockers?.length > 0) {
|
|
224
|
+
html += `<div class="annotation-list" style="margin-top:8px">`;
|
|
225
|
+
for (const b of phase_transition.blockers) {
|
|
226
|
+
html += `<div class="annotation-card">
|
|
227
|
+
<span class="mono">${esc(b.code || 'unknown')}</span>
|
|
228
|
+
<span>${esc(b.message || '')}</span>
|
|
229
|
+
</div>`;
|
|
230
|
+
}
|
|
231
|
+
html += `</div>`;
|
|
232
|
+
}
|
|
233
|
+
html += `</div></div>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (run_completion) {
|
|
237
|
+
html += `<div class="turn-card" data-turn-expand>
|
|
238
|
+
<div class="turn-header">
|
|
239
|
+
<span>Run Completion</span>
|
|
240
|
+
${badge(run_completion.ready ? 'ready' : 'not ready', run_completion.ready ? 'var(--green)' : 'var(--yellow)')}
|
|
241
|
+
</div>
|
|
242
|
+
<div class="turn-detail-panel">
|
|
243
|
+
<dl class="detail-list">`;
|
|
244
|
+
if (run_completion.gate_id) html += `<dt>Gate</dt><dd class="mono">${esc(run_completion.gate_id)}</dd>`;
|
|
245
|
+
html += `<dt>Blockers</dt><dd>${run_completion.blockers?.length || 0}</dd>`;
|
|
246
|
+
if (typeof run_completion.requires_human_approval === 'boolean') {
|
|
247
|
+
html += `<dt>Human Approval</dt><dd>${run_completion.requires_human_approval ? 'Required' : 'Not required'}</dd>`;
|
|
248
|
+
}
|
|
249
|
+
html += `</dl>`;
|
|
250
|
+
if (run_completion.blockers?.length > 0) {
|
|
251
|
+
html += `<div class="annotation-list" style="margin-top:8px">`;
|
|
252
|
+
for (const b of run_completion.blockers) {
|
|
253
|
+
html += `<div class="annotation-card">
|
|
254
|
+
<span class="mono">${esc(b.code || 'unknown')}</span>
|
|
255
|
+
<span>${esc(b.message || '')}</span>
|
|
256
|
+
</div>`;
|
|
257
|
+
}
|
|
258
|
+
html += `</div>`;
|
|
259
|
+
}
|
|
260
|
+
html += `</div></div>`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
html += `</div>`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
html += `</div>`;
|
|
267
|
+
return html;
|
|
268
|
+
}
|
|
@@ -37,7 +37,86 @@ function summarizeBarriers(barriers) {
|
|
|
37
37
|
return counts;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
function renderCoordinatorAttentionSnapshot(coordinatorBlockers) {
|
|
41
|
+
if (!coordinatorBlockers || coordinatorBlockers.ok === false) {
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const active = coordinatorBlockers.active || {};
|
|
46
|
+
const blockers = Array.isArray(active.blockers)
|
|
47
|
+
? active.blockers.filter((blocker) => blocker?.code !== 'no_next_phase')
|
|
48
|
+
: [];
|
|
49
|
+
const hasBlockers = blockers.length > 0;
|
|
50
|
+
const title = coordinatorBlockers.mode === 'pending_gate' ? 'Approval Snapshot' : 'Blocker Snapshot';
|
|
51
|
+
|
|
52
|
+
let html = `<div class="gate-card">
|
|
53
|
+
<h3>${title}</h3>
|
|
54
|
+
<dl class="detail-list">`;
|
|
55
|
+
if (coordinatorBlockers.mode) {
|
|
56
|
+
html += `<dt>Mode</dt><dd>${esc(coordinatorBlockers.mode)}</dd>`;
|
|
57
|
+
}
|
|
58
|
+
if (active.gate_type) {
|
|
59
|
+
html += `<dt>Type</dt><dd>${esc(active.gate_type)}</dd>`;
|
|
60
|
+
}
|
|
61
|
+
if (active.gate_id) {
|
|
62
|
+
html += `<dt>Gate</dt><dd class="mono">${esc(active.gate_id)}</dd>`;
|
|
63
|
+
}
|
|
64
|
+
if (active.current_phase) {
|
|
65
|
+
html += `<dt>Current</dt><dd>${esc(active.current_phase)}</dd>`;
|
|
66
|
+
}
|
|
67
|
+
if (active.target_phase) {
|
|
68
|
+
html += `<dt>Target</dt><dd>${esc(active.target_phase)}</dd>`;
|
|
69
|
+
}
|
|
70
|
+
if (hasBlockers) {
|
|
71
|
+
html += `<dt>Blockers</dt><dd>${blockers.length}</dd>`;
|
|
72
|
+
}
|
|
73
|
+
html += `</dl>`;
|
|
74
|
+
|
|
75
|
+
if (coordinatorBlockers.mode === 'pending_gate') {
|
|
76
|
+
html += `<p class="turn-summary">All coordinator prerequisites are satisfied. Human approval is the remaining action.</p>`;
|
|
77
|
+
} else if (hasBlockers) {
|
|
78
|
+
html += `<div class="turn-list">`;
|
|
79
|
+
for (const blocker of blockers) {
|
|
80
|
+
html += `<div class="turn-card">
|
|
81
|
+
<div class="turn-header"><span class="mono">${esc(blocker.code || 'unknown')}</span></div>`;
|
|
82
|
+
if (blocker.message) {
|
|
83
|
+
html += `<div class="turn-summary">${esc(blocker.message)}</div>`;
|
|
84
|
+
}
|
|
85
|
+
if (blocker.repo_id || blocker.expected_run_id || blocker.actual_run_id) {
|
|
86
|
+
html += `<dl class="detail-list">`;
|
|
87
|
+
if (blocker.repo_id) html += `<dt>Repo</dt><dd class="mono">${esc(blocker.repo_id)}</dd>`;
|
|
88
|
+
if (blocker.expected_run_id) html += `<dt>Expected</dt><dd class="mono">${esc(blocker.expected_run_id)}</dd>`;
|
|
89
|
+
if (blocker.actual_run_id) html += `<dt>Actual</dt><dd class="mono">${esc(blocker.actual_run_id)}</dd>`;
|
|
90
|
+
if (blocker.current_phase) html += `<dt>Current Phase</dt><dd>${esc(blocker.current_phase)}</dd>`;
|
|
91
|
+
if (blocker.required_phase) html += `<dt>Required Phase</dt><dd>${esc(blocker.required_phase)}</dd>`;
|
|
92
|
+
html += `</dl>`;
|
|
93
|
+
}
|
|
94
|
+
html += `</div>`;
|
|
95
|
+
}
|
|
96
|
+
html += `</div>`;
|
|
97
|
+
} else if (coordinatorBlockers.blocked_reason) {
|
|
98
|
+
html += `<p class="turn-summary">${esc(
|
|
99
|
+
typeof coordinatorBlockers.blocked_reason === 'string'
|
|
100
|
+
? coordinatorBlockers.blocked_reason
|
|
101
|
+
: JSON.stringify(coordinatorBlockers.blocked_reason)
|
|
102
|
+
)}</p>`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
html += `<div class="gate-action">
|
|
106
|
+
<p>Inspect full diagnostics:</p>
|
|
107
|
+
<p><a href="#blockers">Open Blockers view</a></p>
|
|
108
|
+
</div>
|
|
109
|
+
</div>`;
|
|
110
|
+
|
|
111
|
+
return html;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function render({
|
|
115
|
+
coordinatorState,
|
|
116
|
+
coordinatorBarriers = {},
|
|
117
|
+
barrierLedger = [],
|
|
118
|
+
coordinatorBlockers = null,
|
|
119
|
+
}) {
|
|
41
120
|
if (!coordinatorState) {
|
|
42
121
|
return `<div class="placeholder"><h2>No Initiative</h2><p>No coordinator run found. Start one with <code class="mono">agentxchain multi init</code></p></div>`;
|
|
43
122
|
}
|
|
@@ -80,7 +159,10 @@ export function render({ coordinatorState, coordinatorBarriers = {}, barrierLedg
|
|
|
80
159
|
</div>
|
|
81
160
|
</div>`;
|
|
82
161
|
}
|
|
83
|
-
|
|
162
|
+
const blockerSnapshot = renderCoordinatorAttentionSnapshot(coordinatorBlockers);
|
|
163
|
+
if (blockerSnapshot) {
|
|
164
|
+
html += blockerSnapshot;
|
|
165
|
+
} else if (coordinatorState.blocked_reason) {
|
|
84
166
|
html += `<div class="gate-card">
|
|
85
167
|
<h3>Blocked State</h3>
|
|
86
168
|
<p class="turn-summary">${esc(
|
package/dashboard/index.html
CHANGED
package/package.json
CHANGED
|
@@ -101,6 +101,27 @@ trim_last_line() {
|
|
|
101
101
|
printf '%s\n' "$1" | awk 'NF { line=$0 } END { gsub(/^[[:space:]]+|[[:space:]]+$/, "", line); print line }'
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
extract_matching_line() {
|
|
105
|
+
local output="$1"
|
|
106
|
+
local expected="$2"
|
|
107
|
+
printf '%s\n' "$output" | awk -v expected="$expected" '
|
|
108
|
+
{
|
|
109
|
+
line=$0
|
|
110
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
|
111
|
+
if (line == expected) {
|
|
112
|
+
print line
|
|
113
|
+
found=1
|
|
114
|
+
exit
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
END {
|
|
118
|
+
if (!found) {
|
|
119
|
+
exit 1
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
'
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
run_install_smoke() {
|
|
105
126
|
if [[ -z "$TARBALL_URL" ]]; then
|
|
106
127
|
echo "registry tarball metadata unavailable for install smoke" >&2
|
|
@@ -147,18 +168,21 @@ run_npx_smoke() {
|
|
|
147
168
|
local npx_status
|
|
148
169
|
|
|
149
170
|
smoke_root="$(mktemp -d "${TMPDIR:-/tmp}/agentxchain-npx-postflight.XXXXXX")"
|
|
150
|
-
mkdir -p "${smoke_root}/home" "${smoke_root}/cache" "${smoke_root}/npm-cache"
|
|
171
|
+
mkdir -p "${smoke_root}/home" "${smoke_root}/cache" "${smoke_root}/npm-cache" "${smoke_root}/workspace"
|
|
151
172
|
|
|
152
173
|
smoke_npmrc="${smoke_root}/.npmrc"
|
|
153
174
|
echo "registry=https://registry.npmjs.org/" > "$smoke_npmrc"
|
|
154
175
|
|
|
155
176
|
npx_output="$(
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
177
|
+
(
|
|
178
|
+
cd "${smoke_root}/workspace" || exit 1
|
|
179
|
+
env -u NODE_AUTH_TOKEN \
|
|
180
|
+
HOME="${smoke_root}/home" \
|
|
181
|
+
XDG_CACHE_HOME="${smoke_root}/cache" \
|
|
182
|
+
NPM_CONFIG_CACHE="${smoke_root}/npm-cache" \
|
|
183
|
+
NPM_CONFIG_USERCONFIG="$smoke_npmrc" \
|
|
184
|
+
npx --yes -p "${PACKAGE_NAME}@${TARGET_VERSION}" -c "${PACKAGE_BIN_NAME} --version" 2>&1
|
|
185
|
+
)
|
|
162
186
|
)"
|
|
163
187
|
npx_status=$?
|
|
164
188
|
|
|
@@ -342,7 +366,7 @@ fi
|
|
|
342
366
|
|
|
343
367
|
echo "[5/7] npx smoke"
|
|
344
368
|
if run_with_retry NPX_OUTPUT "npx smoke" nonempty "" run_npx_smoke; then
|
|
345
|
-
NPX_VERSION="$(trim_last_line "$NPX_OUTPUT")"
|
|
369
|
+
NPX_VERSION="$(extract_matching_line "$NPX_OUTPUT" "$TARGET_VERSION" 2>/dev/null || trim_last_line "$NPX_OUTPUT")"
|
|
346
370
|
if [[ "$NPX_VERSION" == "$TARGET_VERSION" ]]; then
|
|
347
371
|
pass "published npx command resolves and reports ${TARGET_VERSION}"
|
|
348
372
|
else
|
package/src/commands/demo.js
CHANGED
|
@@ -605,7 +605,7 @@ All acceptance criteria met. OBJ-002 (clock skew) noted for follow-up. OBJ-003 (
|
|
|
605
605
|
console.log(chalk.dim(' ─'.repeat(26)));
|
|
606
606
|
console.log('');
|
|
607
607
|
console.log(` ${chalk.bold('Try it for real:')} agentxchain init --governed`);
|
|
608
|
-
console.log(` ${chalk.bold('Step by step:')} https://agentxchain.dev/docs/
|
|
608
|
+
console.log(` ${chalk.bold('Step by step:')} https://agentxchain.dev/docs/getting-started`);
|
|
609
609
|
console.log(` ${chalk.bold('Read more:')} https://agentxchain.dev/docs/quickstart`);
|
|
610
610
|
console.log('');
|
|
611
611
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -105,6 +105,7 @@ const GOVERNED_RUNTIMES = {
|
|
|
105
105
|
'manual-pm': { type: 'manual' },
|
|
106
106
|
'local-dev': DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME,
|
|
107
107
|
'api-qa': { type: 'api_proxy', provider: 'anthropic', model: 'claude-sonnet-4-6', auth_env: 'ANTHROPIC_API_KEY' },
|
|
108
|
+
'manual-qa': { type: 'manual' },
|
|
108
109
|
'manual-director': { type: 'manual' }
|
|
109
110
|
};
|
|
110
111
|
|
|
@@ -684,6 +685,29 @@ async function initGoverned(opts) {
|
|
|
684
685
|
console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
|
|
685
686
|
console.log(` ${chalk.dim('Protocol:')} governed convergence`);
|
|
686
687
|
console.log('');
|
|
688
|
+
|
|
689
|
+
// Readiness hint: tell user which roles work immediately vs which need API keys
|
|
690
|
+
const allRuntimes = { ...GOVERNED_RUNTIMES, 'local-dev': localDevRuntime };
|
|
691
|
+
const needsKey = Object.entries(allRuntimes)
|
|
692
|
+
.filter(([, rt]) => rt.auth_env)
|
|
693
|
+
.map(([id, rt]) => ({ id, env: rt.auth_env }));
|
|
694
|
+
if (needsKey.length > 0) {
|
|
695
|
+
const envVars = [...new Set(needsKey.map(r => r.env))];
|
|
696
|
+
const roleNames = needsKey.map(r => r.id);
|
|
697
|
+
const hasKeys = envVars.every(v => process.env[v]);
|
|
698
|
+
if (hasKeys) {
|
|
699
|
+
console.log(` ${chalk.green('Ready:')} all runtimes configured (${envVars.join(', ')} detected)`);
|
|
700
|
+
} else {
|
|
701
|
+
console.log(` ${chalk.yellow('Mixed-mode:')} pm and eng_director work immediately (manual).`);
|
|
702
|
+
console.log(` ${chalk.yellow(' ')}${roleNames.join(', ')} need ${chalk.bold(envVars.join(', '))} to dispatch automatically.`);
|
|
703
|
+
console.log(` ${chalk.yellow(' ')}Without it, those turns fall back to manual input.`);
|
|
704
|
+
if (allRuntimes['manual-qa']) {
|
|
705
|
+
console.log(` ${chalk.yellow(' ')}No-key QA path: change ${chalk.bold('roles.qa.runtime')} from ${chalk.bold('"api-qa"')} to ${chalk.bold('"manual-qa"')} in ${chalk.bold('agentxchain.json')}.`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
console.log('');
|
|
709
|
+
}
|
|
710
|
+
|
|
687
711
|
console.log(` ${chalk.cyan('Next:')}`);
|
|
688
712
|
if (dir !== process.cwd()) {
|
|
689
713
|
console.log(` ${chalk.bold(`cd ${targetLabel}`)}`);
|
|
@@ -691,6 +715,8 @@ async function initGoverned(opts) {
|
|
|
691
715
|
console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);
|
|
692
716
|
console.log(` ${chalk.bold('agentxchain status')} ${chalk.dim('# inspect phase, gate, and turn state')}`);
|
|
693
717
|
console.log('');
|
|
718
|
+
console.log(` ${chalk.dim('Guide:')} https://agentxchain.dev/docs/getting-started`);
|
|
719
|
+
console.log('');
|
|
694
720
|
}
|
|
695
721
|
|
|
696
722
|
export async function initCommand(opts) {
|
package/src/commands/multi.js
CHANGED
|
@@ -257,7 +257,12 @@ export async function multiStepCommand(options) {
|
|
|
257
257
|
if (gate.blockers.length > 0) {
|
|
258
258
|
console.error(`Coordinator ${gate.type === 'phase_transition' ? 'phase' : 'completion'} gate is not ready:`);
|
|
259
259
|
for (const blocker of gate.blockers) {
|
|
260
|
-
|
|
260
|
+
const codeTag = blocker.code ? `[${blocker.code}] ` : '';
|
|
261
|
+
console.error(` - ${codeTag}${blocker.message}`);
|
|
262
|
+
if (blocker.code === 'repo_run_id_mismatch') {
|
|
263
|
+
console.error(` expected: ${blocker.expected_run_id}`);
|
|
264
|
+
console.error(` actual: ${blocker.actual_run_id}`);
|
|
265
|
+
}
|
|
261
266
|
}
|
|
262
267
|
}
|
|
263
268
|
process.exitCode = 1;
|
package/src/commands/run.js
CHANGED
|
@@ -47,7 +47,7 @@ export async function runCommand(opts) {
|
|
|
47
47
|
process.exit(1);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const { root, config } = context;
|
|
50
|
+
const { root, config, rawConfig } = context;
|
|
51
51
|
|
|
52
52
|
if (config.protocol_mode !== 'governed') {
|
|
53
53
|
console.log(chalk.red('The run command is only available for governed projects.'));
|
|
@@ -112,6 +112,7 @@ export async function runCommand(opts) {
|
|
|
112
112
|
|
|
113
113
|
// ── Track first-call for --role override ────────────────────────────────
|
|
114
114
|
let firstSelectRole = true;
|
|
115
|
+
let qaMissingCredentialsFallback = null;
|
|
115
116
|
|
|
116
117
|
// ── Callbacks ───────────────────────────────────────────────────────────
|
|
117
118
|
const callbacks = {
|
|
@@ -134,6 +135,7 @@ export async function runCommand(opts) {
|
|
|
134
135
|
const runtime = cfg.runtimes?.[runtimeId];
|
|
135
136
|
const runtimeType = runtime?.type || role?.runtime_class || 'manual';
|
|
136
137
|
const hooksConfig = cfg.hooks || {};
|
|
138
|
+
qaMissingCredentialsFallback = null;
|
|
137
139
|
|
|
138
140
|
// Manual adapter is not supported in run mode
|
|
139
141
|
if (runtimeType === 'manual') {
|
|
@@ -219,6 +221,18 @@ export async function runCommand(opts) {
|
|
|
219
221
|
|
|
220
222
|
// Adapter failure
|
|
221
223
|
if (!adapterResult.ok) {
|
|
224
|
+
if (shouldPrintManualQaFallback({
|
|
225
|
+
roleId,
|
|
226
|
+
runtimeId,
|
|
227
|
+
classified: adapterResult.classified,
|
|
228
|
+
rawConfig,
|
|
229
|
+
})) {
|
|
230
|
+
qaMissingCredentialsFallback = {
|
|
231
|
+
roleId,
|
|
232
|
+
runtimeId,
|
|
233
|
+
errorClass: adapterResult.classified.error_class,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
222
236
|
const errorDetail = adapterResult.classified
|
|
223
237
|
? `${adapterResult.classified.error_class}: ${adapterResult.classified.recovery}`
|
|
224
238
|
: adapterResult.error;
|
|
@@ -317,6 +331,10 @@ export async function runCommand(opts) {
|
|
|
317
331
|
}
|
|
318
332
|
}
|
|
319
333
|
|
|
334
|
+
if (qaMissingCredentialsFallback) {
|
|
335
|
+
printManualQaFallback();
|
|
336
|
+
}
|
|
337
|
+
|
|
320
338
|
// Recovery guidance for blocked/rejected states
|
|
321
339
|
if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
|
|
322
340
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
@@ -391,3 +409,18 @@ function promptUser(question) {
|
|
|
391
409
|
rl.on('close', () => resolve(''));
|
|
392
410
|
});
|
|
393
411
|
}
|
|
412
|
+
|
|
413
|
+
function shouldPrintManualQaFallback({ roleId, runtimeId, classified, rawConfig }) {
|
|
414
|
+
return classified?.error_class === 'missing_credentials'
|
|
415
|
+
&& roleId === 'qa'
|
|
416
|
+
&& runtimeId === 'api-qa'
|
|
417
|
+
&& rawConfig?.runtimes?.['manual-qa']?.type === 'manual';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function printManualQaFallback() {
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log(chalk.dim(' No-key QA fallback:'));
|
|
423
|
+
console.log(chalk.dim(' - Edit agentxchain.json and change roles.qa.runtime from "api-qa" to "manual-qa"'));
|
|
424
|
+
console.log(chalk.dim(' - Then recover the retained QA turn with: agentxchain step --resume'));
|
|
425
|
+
console.log(chalk.dim(' - Guide: https://agentxchain.dev/docs/getting-started'));
|
|
426
|
+
}
|
package/src/commands/step.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function stepCommand(opts) {
|
|
|
71
71
|
process.exit(1);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
const { root, config } = context;
|
|
74
|
+
const { root, config, rawConfig } = context;
|
|
75
75
|
|
|
76
76
|
if (config.protocol_mode !== 'governed') {
|
|
77
77
|
console.log(chalk.red('The step command is only available for governed projects.'));
|
|
@@ -422,6 +422,18 @@ export async function stepCommand(opts) {
|
|
|
422
422
|
console.log(chalk.dim(` Retry trace: ${apiResult.retry_trace_path}`));
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
if (
|
|
426
|
+
apiResult.classified?.error_class === 'missing_credentials'
|
|
427
|
+
&& roleId === 'qa'
|
|
428
|
+
&& config.roles?.qa?.runtime_id === 'api-qa'
|
|
429
|
+
&& rawConfig?.runtimes?.['manual-qa']?.type === 'manual'
|
|
430
|
+
) {
|
|
431
|
+
console.log(chalk.dim(' No-key QA fallback:'));
|
|
432
|
+
console.log(chalk.dim(' - Edit agentxchain.json and change roles.qa.runtime from "api-qa" to "manual-qa"'));
|
|
433
|
+
console.log(chalk.dim(' - Then rerun: agentxchain step --resume'));
|
|
434
|
+
console.log(chalk.dim(' - Guide: https://agentxchain.dev/docs/getting-started'));
|
|
435
|
+
}
|
|
436
|
+
|
|
425
437
|
console.log(chalk.dim('The turn remains assigned. You can:'));
|
|
426
438
|
console.log(chalk.dim(' - Fix the issue and retry: agentxchain step --resume'));
|
|
427
439
|
console.log(chalk.dim(' - Complete manually: edit .agentxchain/staging/turn-result.json'));
|
|
@@ -30,31 +30,100 @@ export function printManualDispatchInstructions(state, config, options = {}) {
|
|
|
30
30
|
const role = config.roles?.[turn.assigned_role];
|
|
31
31
|
const promptPath = getDispatchPromptPath(turn.turn_id);
|
|
32
32
|
const stagingPath = getTurnStagingResultPath(turn.turn_id);
|
|
33
|
+
const phase = state.phase || 'planning';
|
|
34
|
+
const roleId = turn.assigned_role;
|
|
33
35
|
|
|
34
36
|
const lines = [];
|
|
35
37
|
lines.push('');
|
|
36
38
|
lines.push(' +---------------------------------------------------------+');
|
|
37
39
|
lines.push(' | MANUAL TURN REQUIRED |');
|
|
38
40
|
lines.push(' | |');
|
|
39
|
-
lines.push(` | Role: ${pad(
|
|
41
|
+
lines.push(` | Role: ${pad(roleId, 46)}|`);
|
|
40
42
|
lines.push(` | Turn: ${pad(turn.turn_id, 46)}|`);
|
|
41
|
-
lines.push(` | Phase: ${pad(
|
|
43
|
+
lines.push(` | Phase: ${pad(phase, 46)}|`);
|
|
42
44
|
lines.push(` | Attempt: ${pad(String(turn.attempt), 46)}|`);
|
|
43
45
|
lines.push(' | |');
|
|
44
46
|
lines.push(` | Prompt: ${pad(promptPath, 46)}|`);
|
|
45
47
|
lines.push(` | Result: ${pad(stagingPath, 46)}|`);
|
|
46
|
-
lines.push(' | |');
|
|
47
|
-
lines.push(' | 1. Read the prompt at the path above |');
|
|
48
|
-
lines.push(' | 2. Complete the work described in the prompt |');
|
|
49
|
-
lines.push(' | 3. Write your turn result JSON to the result path |');
|
|
50
|
-
lines.push(' | |');
|
|
51
|
-
lines.push(' | The step command will detect the file and proceed. |');
|
|
52
48
|
lines.push(' +---------------------------------------------------------+');
|
|
53
49
|
lines.push('');
|
|
50
|
+
lines.push(' Steps:');
|
|
51
|
+
lines.push(` 1. Read the prompt: cat ${promptPath}`);
|
|
52
|
+
lines.push(' 2. Do the work described in the prompt');
|
|
53
|
+
lines.push(` 3. Write turn-result.json to: ${stagingPath}`);
|
|
54
|
+
lines.push(' 4. The step command will detect the file and proceed');
|
|
55
|
+
lines.push('');
|
|
56
|
+
|
|
57
|
+
// Phase-aware guidance
|
|
58
|
+
const gateHints = getPhaseGateHints(phase, roleId, config);
|
|
59
|
+
if (gateHints.length > 0) {
|
|
60
|
+
lines.push(' Gate files to update this phase:');
|
|
61
|
+
for (const hint of gateHints) {
|
|
62
|
+
lines.push(` - ${hint}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Minimal turn-result example
|
|
68
|
+
lines.push(' Minimal turn-result.json:');
|
|
69
|
+
lines.push(' {');
|
|
70
|
+
lines.push(' "schema_version": "1.0",');
|
|
71
|
+
lines.push(` "run_id": "${state.run_id || 'run_...'}",`);
|
|
72
|
+
lines.push(` "turn_id": "${turn.turn_id}",`);
|
|
73
|
+
lines.push(` "role": "${roleId}",`);
|
|
74
|
+
lines.push(` "runtime_id": "${role?.runtime || 'manual'}",`);
|
|
75
|
+
lines.push(' "status": "completed",');
|
|
76
|
+
lines.push(' "summary": "...",');
|
|
77
|
+
lines.push(' "decisions": [{"id":"DEC-001","category":"scope","statement":"...","rationale":"..."}],');
|
|
78
|
+
lines.push(' "objections": [{"id":"OBJ-001","severity":"medium","statement":"...","status":"raised"}],');
|
|
79
|
+
lines.push(' "files_changed": [],');
|
|
80
|
+
lines.push(' "verification": {"status":"skipped","commands":[],"evidence_summary":"..."},');
|
|
81
|
+
lines.push(' "artifact": {"type":"review","ref":null},');
|
|
82
|
+
lines.push(` "proposed_next_role": "${getDefaultNextRole(roleId, config)}",`);
|
|
83
|
+
lines.push(' "phase_transition_request": null,');
|
|
84
|
+
lines.push(' "run_completion_request": null');
|
|
85
|
+
lines.push(' }');
|
|
86
|
+
lines.push('');
|
|
87
|
+
lines.push(' Docs: https://agentxchain.dev/docs/getting-started');
|
|
88
|
+
lines.push('');
|
|
54
89
|
|
|
55
90
|
return lines.join('\n');
|
|
56
91
|
}
|
|
57
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Return gate-file hints relevant to the current phase and role.
|
|
95
|
+
*/
|
|
96
|
+
function getPhaseGateHints(phase, roleId, config) {
|
|
97
|
+
const hints = [];
|
|
98
|
+
const gates = config.gates || {};
|
|
99
|
+
|
|
100
|
+
if (phase === 'planning' && (roleId === 'pm' || roleId === 'human')) {
|
|
101
|
+
hints.push('.planning/PM_SIGNOFF.md — change "Approved: NO" → "Approved: YES" when ready');
|
|
102
|
+
hints.push('.planning/ROADMAP.md — define phases and acceptance criteria');
|
|
103
|
+
hints.push('.planning/SYSTEM_SPEC.md — define ## Purpose, ## Interface, ## Acceptance Tests');
|
|
104
|
+
} else if (phase === 'implementation' && (roleId === 'dev' || roleId === 'human')) {
|
|
105
|
+
hints.push('.planning/IMPLEMENTATION_NOTES.md — record what you built and how to verify');
|
|
106
|
+
} else if (phase === 'qa' && (roleId === 'qa' || roleId === 'human')) {
|
|
107
|
+
hints.push('.planning/acceptance-matrix.md — mark each requirement PASS/FAIL');
|
|
108
|
+
hints.push('.planning/ship-verdict.md — change "## Verdict: PENDING" → "## Verdict: SHIP"');
|
|
109
|
+
hints.push('.planning/RELEASE_NOTES.md — user impact, verification summary, upgrade notes');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return hints;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Suggest a reasonable next role based on current role.
|
|
117
|
+
*/
|
|
118
|
+
function getDefaultNextRole(roleId, config) {
|
|
119
|
+
const routing = config.routing || {};
|
|
120
|
+
if (routing[roleId]?.default_next) return routing[roleId].default_next;
|
|
121
|
+
if (roleId === 'pm') return 'dev';
|
|
122
|
+
if (roleId === 'dev') return 'qa';
|
|
123
|
+
if (roleId === 'qa') return 'human';
|
|
124
|
+
return 'human';
|
|
125
|
+
}
|
|
126
|
+
|
|
58
127
|
/**
|
|
59
128
|
* Wait for the staged turn result file to appear.
|
|
60
129
|
*
|
|
@@ -21,6 +21,26 @@ function loadRepoState(repoPath) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function buildRunIdMismatchBlocker(state, repoId, repoState) {
|
|
25
|
+
const expectedRunId = state?.repo_runs?.[repoId]?.run_id ?? null;
|
|
26
|
+
if (!expectedRunId) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const actualRunId = repoState?.run_id ?? null;
|
|
31
|
+
if (actualRunId === expectedRunId) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
code: 'repo_run_id_mismatch',
|
|
37
|
+
repo_id: repoId,
|
|
38
|
+
expected_run_id: expectedRunId,
|
|
39
|
+
actual_run_id: actualRunId,
|
|
40
|
+
message: `Repo "${repoId}" run identity drifted: coordinator expects "${expectedRunId}" but repo has "${actualRunId ?? 'null'}"`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
function getActiveTurnCount(repoState) {
|
|
25
45
|
if (!repoState?.active_turns || typeof repoState.active_turns !== 'object' || Array.isArray(repoState.active_turns)) {
|
|
26
46
|
return 0;
|
|
@@ -68,7 +88,7 @@ function getRequiredReposForPhase(state, config) {
|
|
|
68
88
|
return [...required];
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
function buildRepoBlockers(config, repoIds) {
|
|
91
|
+
function buildRepoBlockers(state, config, repoIds) {
|
|
72
92
|
const blockers = [];
|
|
73
93
|
|
|
74
94
|
for (const repoId of repoIds) {
|
|
@@ -84,6 +104,12 @@ function buildRepoBlockers(config, repoIds) {
|
|
|
84
104
|
continue;
|
|
85
105
|
}
|
|
86
106
|
|
|
107
|
+
const runIdMismatch = buildRunIdMismatchBlocker(state, repoId, repoState);
|
|
108
|
+
if (runIdMismatch) {
|
|
109
|
+
blockers.push(runIdMismatch);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
if (repoState.status === 'blocked') {
|
|
88
114
|
blockers.push({
|
|
89
115
|
code: 'repo_blocked',
|
|
@@ -234,7 +260,7 @@ export function evaluatePhaseGate(workspacePath, state, config, targetPhase) {
|
|
|
234
260
|
}
|
|
235
261
|
|
|
236
262
|
const repoIds = getRequiredReposForPhase(state, config);
|
|
237
|
-
blockers.push(...buildRepoBlockers(config, repoIds));
|
|
263
|
+
blockers.push(...buildRepoBlockers(state, config, repoIds));
|
|
238
264
|
|
|
239
265
|
const barriers = readBarriers(workspacePath);
|
|
240
266
|
const barrierState = buildPhaseBarrierState(state, config, barriers);
|
|
@@ -391,6 +417,12 @@ export function evaluateCompletionGate(workspacePath, state, config) {
|
|
|
391
417
|
continue;
|
|
392
418
|
}
|
|
393
419
|
|
|
420
|
+
const runIdMismatch = buildRunIdMismatchBlocker(state, repoId, repoState);
|
|
421
|
+
if (runIdMismatch) {
|
|
422
|
+
blockers.push(runIdMismatch);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
394
426
|
if (repoState.status === 'blocked') {
|
|
395
427
|
blockers.push({
|
|
396
428
|
code: 'repo_blocked',
|
|
@@ -83,6 +83,10 @@ function isAcceptedRepoHistoryEntry(entry) {
|
|
|
83
83
|
return Boolean(entry.accepted_at) || entry.status === 'accepted';
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function buildRunIdMismatchReason(repoId, expectedRunId, actualRunId) {
|
|
87
|
+
return `Repo "${repoId}" run identity drifted: coordinator expects "${expectedRunId}" but repo has "${actualRunId ?? 'null'}"`;
|
|
88
|
+
}
|
|
89
|
+
|
|
86
90
|
// ── Divergence Detection ────────────────────────────────────────────────────
|
|
87
91
|
|
|
88
92
|
/**
|
|
@@ -124,13 +128,13 @@ export function detectDivergence(workspacePath, state, config) {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// Run ID mismatch
|
|
127
|
-
if (repoRun.run_id &&
|
|
131
|
+
if (repoRun.run_id && repoRun.run_id !== (repoState.run_id ?? null)) {
|
|
128
132
|
mismatches.push({
|
|
129
133
|
type: 'run_id_mismatch',
|
|
130
134
|
repo_id: repoId,
|
|
131
135
|
coordinator_run_id: repoRun.run_id,
|
|
132
|
-
repo_run_id: repoState.run_id,
|
|
133
|
-
detail:
|
|
136
|
+
repo_run_id: repoState.run_id ?? null,
|
|
137
|
+
detail: buildRunIdMismatchReason(repoId, repoRun.run_id, repoState.run_id ?? null),
|
|
134
138
|
});
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -238,6 +242,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
238
242
|
|
|
239
243
|
// Step 1: Refresh repo_runs from repo-local authority
|
|
240
244
|
const updatedRepoRuns = { ...state.repo_runs };
|
|
245
|
+
const runIdMismatches = [];
|
|
241
246
|
|
|
242
247
|
for (const [repoId, repoRun] of Object.entries(state.repo_runs || {})) {
|
|
243
248
|
const repo = config.repos?.[repoId];
|
|
@@ -254,9 +259,11 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
254
259
|
|
|
255
260
|
const changes = {};
|
|
256
261
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
262
|
+
if (repoRun.run_id && repoRun.run_id !== (repoState.run_id ?? null)) {
|
|
263
|
+
const reason = buildRunIdMismatchReason(repoId, repoRun.run_id, repoState.run_id ?? null);
|
|
264
|
+
runIdMismatches.push(reason);
|
|
265
|
+
errors.push(reason);
|
|
266
|
+
continue;
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
// Update status from repo-local authority
|
|
@@ -395,7 +402,11 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
395
402
|
let blockedReason = null;
|
|
396
403
|
const pendingGate = state.pending_gate;
|
|
397
404
|
|
|
398
|
-
if (
|
|
405
|
+
if (runIdMismatches.length > 0) {
|
|
406
|
+
blockedReason = runIdMismatches.join('; ');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!blockedReason && pendingGate) {
|
|
399
410
|
const gateCoherent = validatePendingGateCoherence(pendingGate, updatedRepoRuns, config);
|
|
400
411
|
if (!gateCoherent.ok) {
|
|
401
412
|
blockedReason = gateCoherent.reason;
|
|
@@ -17,6 +17,7 @@ import { join, extname, resolve, sep } from 'path';
|
|
|
17
17
|
import { readResource } from './state-reader.js';
|
|
18
18
|
import { FileWatcher } from './file-watcher.js';
|
|
19
19
|
import { approvePendingDashboardGate } from './actions.js';
|
|
20
|
+
import { readCoordinatorBlockerSnapshot } from './coordinator-blockers.js';
|
|
20
21
|
|
|
21
22
|
const MIME_TYPES = {
|
|
22
23
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -208,6 +209,7 @@ function resolveDashboardAssetPath(dashboardDir, pathname) {
|
|
|
208
209
|
// ── Bridge Server ───────────────────────────────────────────────────────────
|
|
209
210
|
|
|
210
211
|
export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847 }) {
|
|
212
|
+
const workspacePath = resolve(agentxchainDir, '..');
|
|
211
213
|
const wsClients = new Set();
|
|
212
214
|
const watcher = new FileWatcher(agentxchainDir);
|
|
213
215
|
const mutationToken = randomBytes(24).toString('hex');
|
|
@@ -271,6 +273,12 @@ export function createBridgeServer({ agentxchainDir, dashboardDir, port = 3847 }
|
|
|
271
273
|
return;
|
|
272
274
|
}
|
|
273
275
|
|
|
276
|
+
if (pathname === '/api/coordinator/blockers') {
|
|
277
|
+
const result = readCoordinatorBlockerSnapshot(workspacePath);
|
|
278
|
+
writeJson(res, result.status, result.body);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
274
282
|
// API routes
|
|
275
283
|
if (pathname.startsWith('/api/')) {
|
|
276
284
|
const result = readResource(agentxchainDir, pathname);
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { evaluateCompletionGate, evaluatePhaseGate } from '../coordinator-gates.js';
|
|
2
|
+
import { loadCoordinatorConfig } from '../coordinator-config.js';
|
|
3
|
+
import { loadCoordinatorState } from '../coordinator-state.js';
|
|
4
|
+
|
|
5
|
+
function normalizePendingGate(pendingGate) {
|
|
6
|
+
if (!pendingGate || typeof pendingGate !== 'object' || Array.isArray(pendingGate)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof pendingGate.gate !== 'string' || pendingGate.gate.length === 0) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof pendingGate.gate_type !== 'string' || pendingGate.gate_type.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalized = {
|
|
19
|
+
gate: pendingGate.gate,
|
|
20
|
+
gate_type: pendingGate.gate_type,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (typeof pendingGate.from === 'string' && pendingGate.from.length > 0) {
|
|
24
|
+
normalized.from = pendingGate.from;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof pendingGate.to === 'string' && pendingGate.to.length > 0) {
|
|
28
|
+
normalized.to = pendingGate.to;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(pendingGate.required_repos)) {
|
|
32
|
+
normalized.required_repos = pendingGate.required_repos;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(pendingGate.human_barriers)) {
|
|
36
|
+
normalized.human_barriers = pendingGate.human_barriers;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof pendingGate.requested_at === 'string' && pendingGate.requested_at.length > 0) {
|
|
40
|
+
normalized.requested_at = pendingGate.requested_at;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizePhaseEvaluation(evaluation) {
|
|
47
|
+
return {
|
|
48
|
+
ready: Boolean(evaluation?.ready),
|
|
49
|
+
gate_id: evaluation?.gate_id ?? null,
|
|
50
|
+
current_phase: evaluation?.current_phase ?? null,
|
|
51
|
+
target_phase: evaluation?.target_phase ?? null,
|
|
52
|
+
required_repos: Array.isArray(evaluation?.required_repos) ? evaluation.required_repos : [],
|
|
53
|
+
workstreams: Array.isArray(evaluation?.workstreams) ? evaluation.workstreams : [],
|
|
54
|
+
human_barriers: Array.isArray(evaluation?.human_barriers) ? evaluation.human_barriers : [],
|
|
55
|
+
blockers: Array.isArray(evaluation?.blockers) ? evaluation.blockers : [],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeCompletionEvaluation(evaluation) {
|
|
60
|
+
return {
|
|
61
|
+
ready: Boolean(evaluation?.ready),
|
|
62
|
+
gate_id: evaluation?.gate_id ?? null,
|
|
63
|
+
required_repos: Array.isArray(evaluation?.required_repos) ? evaluation.required_repos : [],
|
|
64
|
+
human_barriers: Array.isArray(evaluation?.human_barriers) ? evaluation.human_barriers : [],
|
|
65
|
+
requires_human_approval: evaluation?.requires_human_approval !== false,
|
|
66
|
+
blockers: Array.isArray(evaluation?.blockers) ? evaluation.blockers : [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildPendingGateSnapshot(pendingGate) {
|
|
71
|
+
return {
|
|
72
|
+
gate_type: pendingGate.gate_type,
|
|
73
|
+
gate_id: pendingGate.gate,
|
|
74
|
+
ready: true,
|
|
75
|
+
current_phase: pendingGate.from ?? null,
|
|
76
|
+
target_phase: pendingGate.to ?? null,
|
|
77
|
+
required_repos: Array.isArray(pendingGate.required_repos) ? pendingGate.required_repos : [],
|
|
78
|
+
human_barriers: Array.isArray(pendingGate.human_barriers) ? pendingGate.human_barriers : [],
|
|
79
|
+
blockers: [],
|
|
80
|
+
pending: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function phaseIsFinal(phaseEvaluation) {
|
|
85
|
+
return phaseEvaluation.blockers.length === 1
|
|
86
|
+
&& phaseEvaluation.blockers[0]?.code === 'no_next_phase';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getConfigErrorResponse(errors) {
|
|
90
|
+
const issueList = Array.isArray(errors) ? errors : [];
|
|
91
|
+
const missing = issueList.some((error) => typeof error === 'string' && error.startsWith('config_missing:'));
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
status: missing ? 404 : 422,
|
|
96
|
+
body: {
|
|
97
|
+
ok: false,
|
|
98
|
+
code: missing ? 'coordinator_config_missing' : 'coordinator_config_invalid',
|
|
99
|
+
error: missing
|
|
100
|
+
? 'Coordinator config not found. Run `agentxchain multi init` first.'
|
|
101
|
+
: 'Coordinator config is invalid.',
|
|
102
|
+
errors: issueList,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function readCoordinatorBlockerSnapshot(workspacePath) {
|
|
108
|
+
const configResult = loadCoordinatorConfig(workspacePath);
|
|
109
|
+
if (!configResult.ok) {
|
|
110
|
+
return getConfigErrorResponse(configResult.errors);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const state = loadCoordinatorState(workspacePath);
|
|
114
|
+
if (!state) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
status: 404,
|
|
118
|
+
body: {
|
|
119
|
+
ok: false,
|
|
120
|
+
code: 'coordinator_state_missing',
|
|
121
|
+
error: 'Coordinator state not found. Run `agentxchain multi init` first.',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const phaseEvaluation = normalizePhaseEvaluation(
|
|
127
|
+
evaluatePhaseGate(workspacePath, state, configResult.config),
|
|
128
|
+
);
|
|
129
|
+
const completionEvaluation = normalizeCompletionEvaluation(
|
|
130
|
+
evaluateCompletionGate(workspacePath, state, configResult.config),
|
|
131
|
+
);
|
|
132
|
+
const pendingGate = normalizePendingGate(state.pending_gate);
|
|
133
|
+
|
|
134
|
+
let mode = 'phase_transition';
|
|
135
|
+
let active = {
|
|
136
|
+
gate_type: 'phase_transition',
|
|
137
|
+
...phaseEvaluation,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (pendingGate) {
|
|
141
|
+
mode = 'pending_gate';
|
|
142
|
+
active = buildPendingGateSnapshot(pendingGate);
|
|
143
|
+
} else if (phaseIsFinal(phaseEvaluation)) {
|
|
144
|
+
mode = 'run_completion';
|
|
145
|
+
active = {
|
|
146
|
+
gate_type: 'run_completion',
|
|
147
|
+
...completionEvaluation,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
status: 200,
|
|
154
|
+
body: {
|
|
155
|
+
mode,
|
|
156
|
+
super_run_id: state.super_run_id ?? null,
|
|
157
|
+
status: state.status ?? null,
|
|
158
|
+
phase: state.phase ?? null,
|
|
159
|
+
blocked_reason: state.blocked_reason ?? null,
|
|
160
|
+
pending_gate: pendingGate,
|
|
161
|
+
active,
|
|
162
|
+
evaluations: {
|
|
163
|
+
phase_transition: phaseEvaluation,
|
|
164
|
+
run_completion: completionEvaluation,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
package/src/lib/report.js
CHANGED
|
@@ -299,6 +299,23 @@ function normalizeCoordinatorBlockedReason(blockedReason) {
|
|
|
299
299
|
return null;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
function detectRunIdMismatches(repos, coordinatorRepoRuns) {
|
|
303
|
+
const mismatches = [];
|
|
304
|
+
for (const repo of repos) {
|
|
305
|
+
if (!repo.ok || !repo.run_id) continue;
|
|
306
|
+
const expected = coordinatorRepoRuns[repo.repo_id]?.run_id;
|
|
307
|
+
if (!expected) continue;
|
|
308
|
+
if (expected !== repo.run_id) {
|
|
309
|
+
mismatches.push({
|
|
310
|
+
repo_id: repo.repo_id,
|
|
311
|
+
expected_run_id: expected,
|
|
312
|
+
actual_run_id: repo.run_id,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return mismatches;
|
|
317
|
+
}
|
|
318
|
+
|
|
302
319
|
function normalizePendingGate(pendingGate) {
|
|
303
320
|
if (!pendingGate || typeof pendingGate !== 'object' || Array.isArray(pendingGate)) return null;
|
|
304
321
|
if (typeof pendingGate.gate !== 'string' || pendingGate.gate.length === 0) return null;
|
|
@@ -317,7 +334,7 @@ function normalizePendingGate(pendingGate) {
|
|
|
317
334
|
return normalized;
|
|
318
335
|
}
|
|
319
336
|
|
|
320
|
-
function deriveCoordinatorNextActions({ status, blockedReason, pendingGate, repos, coordinatorRepoRuns }) {
|
|
337
|
+
function deriveCoordinatorNextActions({ status, blockedReason, pendingGate, repos, coordinatorRepoRuns, runIdMismatches }) {
|
|
321
338
|
const nextActions = [];
|
|
322
339
|
|
|
323
340
|
if (status === 'blocked') {
|
|
@@ -325,6 +342,14 @@ function deriveCoordinatorNextActions({ status, blockedReason, pendingGate, repo
|
|
|
325
342
|
command: 'agentxchain multi resume',
|
|
326
343
|
reason: `Coordinator is blocked${blockedReason ? `: ${blockedReason}` : ''}. Resume after fixing the underlying issue.`,
|
|
327
344
|
});
|
|
345
|
+
if (runIdMismatches && runIdMismatches.length > 0) {
|
|
346
|
+
for (const m of runIdMismatches) {
|
|
347
|
+
nextActions.push({
|
|
348
|
+
command: `# repo_run_id_mismatch: ${m.repo_id}`,
|
|
349
|
+
reason: `Repo "${m.repo_id}" run identity drifted: coordinator expects "${m.expected_run_id}" but repo has "${m.actual_run_id}". Re-initialize the child repo with the correct run or use multi resume after investigation.`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
328
353
|
if (pendingGate) {
|
|
329
354
|
nextActions.push({
|
|
330
355
|
command: 'agentxchain multi approve-gate',
|
|
@@ -574,12 +599,14 @@ function buildCoordinatorSubject(artifact) {
|
|
|
574
599
|
const timing = computeCoordinatorTiming(artifact, coordinatorTimeline);
|
|
575
600
|
const blockedReason = normalizeCoordinatorBlockedReason(coordinatorState.blocked_reason);
|
|
576
601
|
const pendingGate = normalizePendingGate(coordinatorState.pending_gate);
|
|
602
|
+
const runIdMismatches = detectRunIdMismatches(repos, coordinatorState.repo_runs || {});
|
|
577
603
|
const nextActions = deriveCoordinatorNextActions({
|
|
578
604
|
status: artifact.summary?.status || null,
|
|
579
605
|
blockedReason,
|
|
580
606
|
pendingGate,
|
|
581
607
|
repos,
|
|
582
608
|
coordinatorRepoRuns: coordinatorState.repo_runs || {},
|
|
609
|
+
runIdMismatches,
|
|
583
610
|
});
|
|
584
611
|
|
|
585
612
|
return {
|
|
@@ -597,6 +624,7 @@ function buildCoordinatorSubject(artifact) {
|
|
|
597
624
|
phase: artifact.summary?.phase || null,
|
|
598
625
|
blocked_reason: blockedReason,
|
|
599
626
|
pending_gate: pendingGate,
|
|
627
|
+
run_id_mismatches: runIdMismatches,
|
|
600
628
|
next_actions: nextActions,
|
|
601
629
|
created_at: timing.created_at,
|
|
602
630
|
completed_at: timing.completed_at,
|
|
@@ -797,6 +825,16 @@ export function formatGovernanceReportText(report) {
|
|
|
797
825
|
`Status: ${run.status || 'unknown'}`,
|
|
798
826
|
`Phase: ${run.phase || 'unknown'}`,
|
|
799
827
|
`Blocked reason: ${run.blocked_reason || 'none'}`,
|
|
828
|
+
];
|
|
829
|
+
|
|
830
|
+
if (run.run_id_mismatches && run.run_id_mismatches.length > 0) {
|
|
831
|
+
lines.push(`Run ID mismatches: ${run.run_id_mismatches.length}`);
|
|
832
|
+
for (const m of run.run_id_mismatches) {
|
|
833
|
+
lines.push(` - ${m.repo_id}: expected ${m.expected_run_id}, actual ${m.actual_run_id}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
lines.push(
|
|
800
838
|
`Started: ${run.created_at || 'n/a'}`,
|
|
801
839
|
`Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
|
|
802
840
|
`Workstreams: ${coordinator.workstream_count}`,
|
|
@@ -804,7 +842,7 @@ export function formatGovernanceReportText(report) {
|
|
|
804
842
|
`Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
|
|
805
843
|
`History entries: ${artifacts.history_entries}`,
|
|
806
844
|
`Decision entries: ${artifacts.decision_entries}`,
|
|
807
|
-
|
|
845
|
+
);
|
|
808
846
|
|
|
809
847
|
if (run.completed_at) {
|
|
810
848
|
lines.push(`Completed: ${run.completed_at}`);
|
|
@@ -1044,6 +1082,16 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1044
1082
|
`- Status: \`${run.status || 'unknown'}\``,
|
|
1045
1083
|
`- Phase: \`${run.phase || 'unknown'}\``,
|
|
1046
1084
|
`- Blocked reason: \`${run.blocked_reason || 'none'}\``,
|
|
1085
|
+
];
|
|
1086
|
+
|
|
1087
|
+
if (run.run_id_mismatches && run.run_id_mismatches.length > 0) {
|
|
1088
|
+
mdLines.push(`- **Run ID mismatches: ${run.run_id_mismatches.length}**`);
|
|
1089
|
+
for (const m of run.run_id_mismatches) {
|
|
1090
|
+
mdLines.push(` - \`${m.repo_id}\`: expected \`${m.expected_run_id}\`, actual \`${m.actual_run_id}\``);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
mdLines.push(
|
|
1047
1095
|
`- Started: \`${run.created_at || 'n/a'}\``,
|
|
1048
1096
|
`- Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
|
|
1049
1097
|
`- Workstreams: ${coordinator.workstream_count}`,
|
|
@@ -1051,7 +1099,7 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1051
1099
|
`- Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
|
|
1052
1100
|
`- History entries: ${artifacts.history_entries}`,
|
|
1053
1101
|
`- Decision entries: ${artifacts.decision_entries}`,
|
|
1054
|
-
|
|
1102
|
+
);
|
|
1055
1103
|
|
|
1056
1104
|
if (run.completed_at) {
|
|
1057
1105
|
mdLines.push(`- Completed: \`${run.completed_at}\``);
|