agentxchain 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/agentxchain.js +1 -0
- package/package.json +3 -1
- package/scripts/release-postflight.sh +21 -5
- package/scripts/sync-homebrew.sh +225 -0
- package/src/commands/init.js +16 -6
- package/src/commands/migrate.js +7 -3
- package/src/commands/run.js +29 -1
- package/src/commands/template-set.js +51 -2
- package/src/lib/adapter-interface.js +31 -0
- package/src/lib/coordinator-config.js +31 -0
- package/src/lib/coordinator-dispatch.js +10 -1
- package/src/lib/coordinator-gates.js +28 -1
- package/src/lib/coordinator-recovery.js +10 -0
- package/src/lib/coordinator-state.js +73 -0
- package/src/lib/governed-templates.js +60 -0
- package/src/lib/report.js +759 -27
- package/src/lib/workflow-gate-semantics.js +209 -0
- package/src/templates/governed/api-service.json +8 -1
- package/src/templates/governed/cli-tool.json +8 -1
- package/src/templates/governed/library.json +8 -1
- package/src/templates/governed/web-app.json +8 -1
package/src/lib/report.js
CHANGED
|
@@ -58,6 +58,385 @@ function formatStatusCounts(statusCounts) {
|
|
|
58
58
|
return entries.map(([status, count]) => `${status}(${count})`).join(', ');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function extractFileData(artifact, relPath) {
|
|
62
|
+
const entry = artifact.files?.[relPath];
|
|
63
|
+
if (!entry) return null;
|
|
64
|
+
return entry.data ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractHistoryTimeline(artifact) {
|
|
68
|
+
const data = extractFileData(artifact, '.agentxchain/history.jsonl');
|
|
69
|
+
if (!Array.isArray(data) || data.length === 0) return [];
|
|
70
|
+
return data
|
|
71
|
+
.filter((e) => typeof e?.turn_id === 'string' && typeof e?.role === 'string')
|
|
72
|
+
.sort((a, b) => (a.accepted_sequence || 0) - (b.accepted_sequence || 0))
|
|
73
|
+
.map((e) => ({
|
|
74
|
+
turn_id: e.turn_id,
|
|
75
|
+
role: e.role,
|
|
76
|
+
status: e.status || 'unknown',
|
|
77
|
+
summary: e.summary || '',
|
|
78
|
+
phase: e.phase || null,
|
|
79
|
+
phase_transition: e.phase_transition_request || null,
|
|
80
|
+
files_changed_count: Array.isArray(e.files_changed) ? e.files_changed.length : 0,
|
|
81
|
+
decisions: Array.isArray(e.decisions) ? e.decisions.map((d) => d?.id || d).filter(Boolean) : [],
|
|
82
|
+
objections: Array.isArray(e.objections) ? e.objections.map((o) => o?.id || o).filter(Boolean) : [],
|
|
83
|
+
cost_usd: typeof e.cost?.total_usd === 'number' ? e.cost.total_usd : null,
|
|
84
|
+
accepted_at: e.accepted_at || null,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractDecisionDigest(artifact) {
|
|
89
|
+
const data = extractFileData(artifact, '.agentxchain/decision-ledger.jsonl');
|
|
90
|
+
if (!Array.isArray(data) || data.length === 0) return [];
|
|
91
|
+
return data
|
|
92
|
+
.filter((d) => typeof d?.id === 'string')
|
|
93
|
+
.map((d) => ({
|
|
94
|
+
id: d.id,
|
|
95
|
+
turn_id: d.turn_id || null,
|
|
96
|
+
role: d.role || null,
|
|
97
|
+
phase: d.phase || null,
|
|
98
|
+
statement: d.statement || '',
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function extractHookSummary(artifact) {
|
|
103
|
+
const data = extractFileData(artifact, '.agentxchain/hook-audit.jsonl');
|
|
104
|
+
if (!Array.isArray(data) || data.length === 0) return null;
|
|
105
|
+
const events = {};
|
|
106
|
+
let blocked = 0;
|
|
107
|
+
for (const entry of data) {
|
|
108
|
+
const event = entry?.event || 'unknown';
|
|
109
|
+
events[event] = (events[event] || 0) + 1;
|
|
110
|
+
if (entry?.blocked || entry?.result === 'blocked') blocked++;
|
|
111
|
+
}
|
|
112
|
+
return { total: data.length, blocked, events };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function computeTiming(artifact, turns) {
|
|
116
|
+
const createdAt = artifact.state?.created_at || null;
|
|
117
|
+
let completedAt = null;
|
|
118
|
+
if (artifact.summary?.status === 'completed' && turns.length > 0) {
|
|
119
|
+
completedAt = turns[turns.length - 1].accepted_at || null;
|
|
120
|
+
}
|
|
121
|
+
let durationSeconds = null;
|
|
122
|
+
if (createdAt && completedAt) {
|
|
123
|
+
const start = new Date(createdAt).getTime();
|
|
124
|
+
const end = new Date(completedAt).getTime();
|
|
125
|
+
if (Number.isFinite(start) && Number.isFinite(end) && end >= start) {
|
|
126
|
+
durationSeconds = Math.round((end - start) / 1000);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { created_at: createdAt, completed_at: completedAt, duration_seconds: durationSeconds };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isValidTimestamp(value) {
|
|
133
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
134
|
+
const parsed = Date.parse(value);
|
|
135
|
+
return Number.isFinite(parsed);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function computeDurationSeconds(createdAt, completedAt) {
|
|
139
|
+
if (!isValidTimestamp(createdAt) || !isValidTimestamp(completedAt)) return null;
|
|
140
|
+
const start = Date.parse(createdAt);
|
|
141
|
+
const end = Date.parse(completedAt);
|
|
142
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
|
|
143
|
+
return Math.round((end - start) / 1000);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractGateSummary(artifact) {
|
|
147
|
+
const phaseGateStatus = artifact.state?.phase_gate_status;
|
|
148
|
+
if (!phaseGateStatus || typeof phaseGateStatus !== 'object' || Array.isArray(phaseGateStatus)) return [];
|
|
149
|
+
return Object.entries(phaseGateStatus)
|
|
150
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
151
|
+
.map(([gate_id, status]) => ({
|
|
152
|
+
gate_id,
|
|
153
|
+
status: typeof status === 'string' && status.length > 0 ? status : 'unknown',
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractIntakeLinks(artifact) {
|
|
158
|
+
const runId = artifact.summary?.run_id;
|
|
159
|
+
if (typeof runId !== 'string' || runId.length === 0) return [];
|
|
160
|
+
return Object.entries(artifact.files || {})
|
|
161
|
+
.filter(([relPath, entry]) => relPath.startsWith('.agentxchain/intake/intents/') && entry?.format === 'json')
|
|
162
|
+
.map(([, entry]) => entry.data)
|
|
163
|
+
.filter((intent) => intent && typeof intent === 'object' && intent.target_run === runId && typeof intent.intent_id === 'string')
|
|
164
|
+
.sort((left, right) => {
|
|
165
|
+
const leftTime = Date.parse(left.updated_at || left.started_at || left.created_at || 0);
|
|
166
|
+
const rightTime = Date.parse(right.updated_at || right.started_at || right.created_at || 0);
|
|
167
|
+
return leftTime - rightTime;
|
|
168
|
+
})
|
|
169
|
+
.map((intent) => ({
|
|
170
|
+
intent_id: intent.intent_id,
|
|
171
|
+
event_id: intent.event_id || null,
|
|
172
|
+
status: intent.status || null,
|
|
173
|
+
priority: intent.priority || null,
|
|
174
|
+
template: intent.template || null,
|
|
175
|
+
target_turn: intent.target_turn || null,
|
|
176
|
+
started_at: intent.started_at || null,
|
|
177
|
+
updated_at: intent.updated_at || null,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractRecoverySummary(artifact) {
|
|
182
|
+
const blockedReason = artifact.state?.blocked_reason;
|
|
183
|
+
if (!blockedReason || typeof blockedReason !== 'object' || Array.isArray(blockedReason)) return null;
|
|
184
|
+
const recovery = blockedReason.recovery;
|
|
185
|
+
if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery)) return null;
|
|
186
|
+
return {
|
|
187
|
+
category: blockedReason.category || null,
|
|
188
|
+
typed_reason: recovery.typed_reason || null,
|
|
189
|
+
owner: recovery.owner || null,
|
|
190
|
+
recovery_action: recovery.recovery_action || null,
|
|
191
|
+
detail: recovery.detail || null,
|
|
192
|
+
turn_retained: typeof recovery.turn_retained === 'boolean' ? recovery.turn_retained : null,
|
|
193
|
+
blocked_at: blockedReason.blocked_at || null,
|
|
194
|
+
turn_id: blockedReason.turn_id || null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function summarizeCoordinatorEvent(entry) {
|
|
199
|
+
const type = entry?.type || 'unknown';
|
|
200
|
+
const ts = entry?.timestamp || '';
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'run_initialized': {
|
|
203
|
+
const repoCount = entry.repo_runs ? Object.keys(entry.repo_runs).length : 0;
|
|
204
|
+
return `Coordinator run initialized with ${repoCount} repo${repoCount !== 1 ? 's' : ''}`;
|
|
205
|
+
}
|
|
206
|
+
case 'turn_dispatched':
|
|
207
|
+
return `Dispatched turn to ${entry.repo_id || 'unknown'} (${entry.role || '?'}) in workstream ${entry.workstream_id || 'unknown'}`;
|
|
208
|
+
case 'acceptance_projection': {
|
|
209
|
+
const turnRef = entry.repo_turn_id ? ` (turn ${entry.repo_turn_id})` : '';
|
|
210
|
+
const summaryText = entry.summary ? ` — ${entry.summary}` : '';
|
|
211
|
+
return `Projected acceptance from ${entry.repo_id || 'unknown'}${turnRef}${summaryText}`;
|
|
212
|
+
}
|
|
213
|
+
case 'context_generated': {
|
|
214
|
+
const upstreamCount = Array.isArray(entry.upstream_repo_ids) ? entry.upstream_repo_ids.length : 0;
|
|
215
|
+
return `Generated cross-repo context for ${entry.target_repo_id || 'unknown'} from ${upstreamCount} upstream repo${upstreamCount !== 1 ? 's' : ''}`;
|
|
216
|
+
}
|
|
217
|
+
case 'phase_transition_requested':
|
|
218
|
+
return `Requested phase transition: ${entry.from || '?'} → ${entry.to || '?'}`;
|
|
219
|
+
case 'phase_transition_approved':
|
|
220
|
+
return `Phase transition approved: ${entry.from || '?'} → ${entry.to || '?'}`;
|
|
221
|
+
case 'run_completion_requested':
|
|
222
|
+
return `Requested run completion (gate: ${entry.gate || 'unknown'})`;
|
|
223
|
+
case 'run_completed':
|
|
224
|
+
return 'Coordinator run completed';
|
|
225
|
+
case 'state_resynced': {
|
|
226
|
+
const resynced = Array.isArray(entry.resynced_repos) ? entry.resynced_repos.length : 0;
|
|
227
|
+
const barrierChanges = Array.isArray(entry.barrier_changes) ? entry.barrier_changes.length : 0;
|
|
228
|
+
return `Resynced state for ${resynced} repo${resynced !== 1 ? 's' : ''}, ${barrierChanges} barrier change${barrierChanges !== 1 ? 's' : ''}`;
|
|
229
|
+
}
|
|
230
|
+
case 'blocked_resolved':
|
|
231
|
+
return `Blocked state resolved: ${entry.from || '?'} → ${entry.to || '?'}`;
|
|
232
|
+
default:
|
|
233
|
+
return `${type} event${ts ? ` at ${ts}` : ''}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function extractCoordinatorTimeline(artifact) {
|
|
238
|
+
const data = extractFileData(artifact, '.agentxchain/multirepo/history.jsonl');
|
|
239
|
+
if (!Array.isArray(data) || data.length === 0) return [];
|
|
240
|
+
return data
|
|
241
|
+
.filter((e) => e && typeof e === 'object' && !Array.isArray(e))
|
|
242
|
+
.map((e) => {
|
|
243
|
+
const details = {};
|
|
244
|
+
if (e.gate) details.gate = e.gate;
|
|
245
|
+
if (e.projection_ref) details.projection_ref = e.projection_ref;
|
|
246
|
+
if (e.context_ref) details.context_ref = e.context_ref;
|
|
247
|
+
if (Array.isArray(e.barrier_changes) && e.barrier_changes.length > 0) details.barrier_changes = e.barrier_changes;
|
|
248
|
+
if (e.blocked_reason) details.blocked_reason = e.blocked_reason;
|
|
249
|
+
return {
|
|
250
|
+
type: e.type || 'unknown',
|
|
251
|
+
timestamp: e.timestamp || null,
|
|
252
|
+
summary: summarizeCoordinatorEvent(e),
|
|
253
|
+
repo_id: e.repo_id || e.target_repo_id || null,
|
|
254
|
+
workstream_id: e.workstream_id || null,
|
|
255
|
+
details: Object.keys(details).length > 0 ? details : null,
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function computeCoordinatorTiming(artifact, coordinatorTimeline) {
|
|
261
|
+
const coordinatorState = extractFileData(artifact, '.agentxchain/multirepo/state.json');
|
|
262
|
+
const createdAtFromHistory = coordinatorTimeline
|
|
263
|
+
.find((entry) => entry.type === 'run_initialized' && isValidTimestamp(entry.timestamp))
|
|
264
|
+
?.timestamp || null;
|
|
265
|
+
const completedAtFromHistory = [...coordinatorTimeline]
|
|
266
|
+
.reverse()
|
|
267
|
+
.find((entry) => entry.type === 'run_completed' && isValidTimestamp(entry.timestamp))
|
|
268
|
+
?.timestamp || null;
|
|
269
|
+
|
|
270
|
+
const createdAt = createdAtFromHistory
|
|
271
|
+
|| (isValidTimestamp(coordinatorState?.created_at) ? coordinatorState.created_at : null);
|
|
272
|
+
|
|
273
|
+
let completedAt = null;
|
|
274
|
+
const completedState = artifact.summary?.status === 'completed' || coordinatorState?.status === 'completed';
|
|
275
|
+
if (completedState) {
|
|
276
|
+
completedAt = completedAtFromHistory
|
|
277
|
+
|| (isValidTimestamp(coordinatorState?.updated_at) ? coordinatorState.updated_at : null);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
created_at: createdAt,
|
|
282
|
+
completed_at: completedAt,
|
|
283
|
+
duration_seconds: computeDurationSeconds(createdAt, completedAt),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeCoordinatorBlockedReason(blockedReason) {
|
|
288
|
+
if (typeof blockedReason === 'string' && blockedReason.trim().length > 0) {
|
|
289
|
+
return blockedReason;
|
|
290
|
+
}
|
|
291
|
+
if (blockedReason && typeof blockedReason === 'object' && !Array.isArray(blockedReason)) {
|
|
292
|
+
if (typeof blockedReason.reason === 'string' && blockedReason.reason.trim().length > 0) {
|
|
293
|
+
return blockedReason.reason;
|
|
294
|
+
}
|
|
295
|
+
if (typeof blockedReason.category === 'string' && blockedReason.category.trim().length > 0) {
|
|
296
|
+
return blockedReason.category;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizePendingGate(pendingGate) {
|
|
303
|
+
if (!pendingGate || typeof pendingGate !== 'object' || Array.isArray(pendingGate)) return null;
|
|
304
|
+
if (typeof pendingGate.gate !== 'string' || pendingGate.gate.length === 0) return null;
|
|
305
|
+
if (typeof pendingGate.gate_type !== 'string' || pendingGate.gate_type.length === 0) return null;
|
|
306
|
+
const normalized = {
|
|
307
|
+
gate: pendingGate.gate,
|
|
308
|
+
gate_type: pendingGate.gate_type,
|
|
309
|
+
};
|
|
310
|
+
if (typeof pendingGate.from === 'string' && pendingGate.from.length > 0) normalized.from = pendingGate.from;
|
|
311
|
+
if (typeof pendingGate.to === 'string' && pendingGate.to.length > 0) normalized.to = pendingGate.to;
|
|
312
|
+
if (Array.isArray(pendingGate.required_repos)) normalized.required_repos = pendingGate.required_repos;
|
|
313
|
+
if (Array.isArray(pendingGate.human_barriers)) normalized.human_barriers = pendingGate.human_barriers;
|
|
314
|
+
if (typeof pendingGate.requested_at === 'string' && pendingGate.requested_at.length > 0) {
|
|
315
|
+
normalized.requested_at = pendingGate.requested_at;
|
|
316
|
+
}
|
|
317
|
+
return normalized;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function deriveCoordinatorNextActions({ status, blockedReason, pendingGate, repos, coordinatorRepoRuns }) {
|
|
321
|
+
const nextActions = [];
|
|
322
|
+
|
|
323
|
+
if (status === 'blocked') {
|
|
324
|
+
nextActions.push({
|
|
325
|
+
command: 'agentxchain multi resume',
|
|
326
|
+
reason: `Coordinator is blocked${blockedReason ? `: ${blockedReason}` : ''}. Resume after fixing the underlying issue.`,
|
|
327
|
+
});
|
|
328
|
+
if (pendingGate) {
|
|
329
|
+
nextActions.push({
|
|
330
|
+
command: 'agentxchain multi approve-gate',
|
|
331
|
+
reason: `After resume, approve pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return nextActions;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const driftedRepos = repos
|
|
338
|
+
.filter((repo) => repo.ok)
|
|
339
|
+
.filter((repo) => {
|
|
340
|
+
const coordinatorStatus = coordinatorRepoRuns?.[repo.repo_id]?.status || null;
|
|
341
|
+
return coordinatorStatus && repo.status && coordinatorStatus !== repo.status;
|
|
342
|
+
})
|
|
343
|
+
.map((repo) => repo.repo_id);
|
|
344
|
+
|
|
345
|
+
if (driftedRepos.length > 0) {
|
|
346
|
+
nextActions.push({
|
|
347
|
+
command: 'agentxchain multi resync',
|
|
348
|
+
reason: `Coordinator state disagrees with child repo status for: ${driftedRepos.join(', ')}.`,
|
|
349
|
+
});
|
|
350
|
+
return nextActions;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (pendingGate) {
|
|
354
|
+
nextActions.push({
|
|
355
|
+
command: 'agentxchain multi approve-gate',
|
|
356
|
+
reason: `Coordinator is waiting on pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
|
|
357
|
+
});
|
|
358
|
+
return nextActions;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (status === 'active' || status === 'paused') {
|
|
362
|
+
nextActions.push({
|
|
363
|
+
command: 'agentxchain multi step',
|
|
364
|
+
reason: 'Coordinator has no blocked state or pending gate and can continue.',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return nextActions;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function extractCoordinatorDecisionDigest(artifact) {
|
|
372
|
+
const data = extractFileData(artifact, '.agentxchain/multirepo/decision-ledger.jsonl');
|
|
373
|
+
if (!Array.isArray(data) || data.length === 0) return [];
|
|
374
|
+
return data
|
|
375
|
+
.filter((d) => typeof d?.id === 'string')
|
|
376
|
+
.map((d) => ({
|
|
377
|
+
id: d.id,
|
|
378
|
+
turn_id: d.turn_id || null,
|
|
379
|
+
role: d.role || null,
|
|
380
|
+
phase: d.phase || null,
|
|
381
|
+
category: d.category || null,
|
|
382
|
+
statement: d.statement || '',
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function extractBarrierSummary(artifact) {
|
|
387
|
+
const data = extractFileData(artifact, '.agentxchain/multirepo/barriers.json');
|
|
388
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return [];
|
|
389
|
+
return Object.entries(data)
|
|
390
|
+
.filter(([, b]) => b && typeof b === 'object' && !Array.isArray(b))
|
|
391
|
+
.sort(([a], [b]) => a.localeCompare(b, 'en'))
|
|
392
|
+
.map(([barrierId, b]) => ({
|
|
393
|
+
barrier_id: barrierId,
|
|
394
|
+
workstream_id: b.workstream_id || null,
|
|
395
|
+
type: b.type || 'unknown',
|
|
396
|
+
status: b.status || 'unknown',
|
|
397
|
+
required_repos: Array.isArray(b.required_repos) ? b.required_repos : [],
|
|
398
|
+
satisfied_repos: Array.isArray(b.satisfied_repos) ? b.satisfied_repos : [],
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function summarizeBarrierTransition(entry) {
|
|
403
|
+
const bid = entry.barrier_id || '?';
|
|
404
|
+
const prev = entry.previous_status || '?';
|
|
405
|
+
const next = entry.new_status || '?';
|
|
406
|
+
const repo = entry.causation?.repo_id || null;
|
|
407
|
+
|
|
408
|
+
if (prev === 'pending' && next === 'partially_satisfied') {
|
|
409
|
+
return `Barrier ${bid}: first repo satisfied${repo ? ` (${repo})` : ''}`;
|
|
410
|
+
}
|
|
411
|
+
if (prev === 'partially_satisfied' && next === 'satisfied') {
|
|
412
|
+
return `Barrier ${bid}: all repos satisfied${repo ? ` (${repo} completed the set)` : ''}`;
|
|
413
|
+
}
|
|
414
|
+
if (prev === 'pending' && next === 'satisfied') {
|
|
415
|
+
return `Barrier ${bid}: satisfied${repo ? ` (single-repo barrier, ${repo})` : ''}`;
|
|
416
|
+
}
|
|
417
|
+
if (next === 'completed') {
|
|
418
|
+
return `Barrier ${bid}: completed`;
|
|
419
|
+
}
|
|
420
|
+
return `Barrier ${bid}: ${prev} → ${next}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function extractBarrierLedgerTimeline(artifact) {
|
|
424
|
+
const data = extractFileData(artifact, '.agentxchain/multirepo/barrier-ledger.jsonl');
|
|
425
|
+
if (!Array.isArray(data) || data.length === 0) return [];
|
|
426
|
+
return data
|
|
427
|
+
.filter((e) => e && typeof e === 'object' && !Array.isArray(e) && e.type === 'barrier_transition')
|
|
428
|
+
.map((e) => ({
|
|
429
|
+
barrier_id: e.barrier_id || 'unknown',
|
|
430
|
+
timestamp: e.timestamp || null,
|
|
431
|
+
previous_status: e.previous_status || 'unknown',
|
|
432
|
+
new_status: e.new_status || 'unknown',
|
|
433
|
+
summary: summarizeBarrierTransition(e),
|
|
434
|
+
workstream_id: e.causation?.workstream_id || null,
|
|
435
|
+
repo_id: e.causation?.repo_id || null,
|
|
436
|
+
trigger: e.causation?.trigger || null,
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
|
|
61
440
|
function deriveRepoStatusCounts(repoStatuses) {
|
|
62
441
|
const counts = {};
|
|
63
442
|
for (const status of Object.values(repoStatuses || {})) {
|
|
@@ -76,6 +455,14 @@ function buildRunSubject(artifact) {
|
|
|
76
455
|
.filter((role) => typeof role === 'string' && role.length > 0),
|
|
77
456
|
)].sort((a, b) => a.localeCompare(b, 'en'));
|
|
78
457
|
|
|
458
|
+
const turns = extractHistoryTimeline(artifact);
|
|
459
|
+
const decisions = extractDecisionDigest(artifact);
|
|
460
|
+
const hookSummary = extractHookSummary(artifact);
|
|
461
|
+
const timing = computeTiming(artifact, turns);
|
|
462
|
+
const gateSummary = extractGateSummary(artifact);
|
|
463
|
+
const intakeLinks = extractIntakeLinks(artifact);
|
|
464
|
+
const recoverySummary = extractRecoverySummary(artifact);
|
|
465
|
+
|
|
79
466
|
return {
|
|
80
467
|
kind: 'governed_run',
|
|
81
468
|
project: {
|
|
@@ -97,6 +484,15 @@ function buildRunSubject(artifact) {
|
|
|
97
484
|
retained_turn_ids: retainedTurns,
|
|
98
485
|
active_roles: activeRoles,
|
|
99
486
|
budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
|
|
487
|
+
created_at: timing.created_at,
|
|
488
|
+
completed_at: timing.completed_at,
|
|
489
|
+
duration_seconds: timing.duration_seconds,
|
|
490
|
+
turns,
|
|
491
|
+
decisions,
|
|
492
|
+
hook_summary: hookSummary,
|
|
493
|
+
gate_summary: gateSummary,
|
|
494
|
+
intake_links: intakeLinks,
|
|
495
|
+
recovery_summary: recoverySummary,
|
|
100
496
|
},
|
|
101
497
|
artifacts: {
|
|
102
498
|
history_entries: artifact.summary?.history_entries || 0,
|
|
@@ -112,23 +508,49 @@ function buildRunSubject(artifact) {
|
|
|
112
508
|
}
|
|
113
509
|
|
|
114
510
|
function buildCoordinatorSubject(artifact) {
|
|
511
|
+
const coordinatorState = extractFileData(artifact, '.agentxchain/multirepo/state.json') || {};
|
|
115
512
|
const repoStatuses = artifact.summary?.repo_run_statuses || {};
|
|
116
513
|
const repoStatusCounts = deriveRepoStatusCounts(repoStatuses);
|
|
117
514
|
const repos = Object.entries(artifact.repos || {})
|
|
118
515
|
.sort(([left], [right]) => left.localeCompare(right, 'en'))
|
|
119
|
-
.map(([repoId, repoEntry]) =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
516
|
+
.map(([repoId, repoEntry]) => {
|
|
517
|
+
const base = {
|
|
518
|
+
repo_id: repoId,
|
|
519
|
+
path: repoEntry?.path || null,
|
|
520
|
+
ok: Boolean(repoEntry?.ok),
|
|
521
|
+
status: repoEntry?.ok ? repoEntry.export?.summary?.status || null : null,
|
|
522
|
+
run_id: repoEntry?.ok ? repoEntry.export?.summary?.run_id || null : null,
|
|
523
|
+
phase: repoEntry?.ok ? repoEntry.export?.summary?.phase || null : null,
|
|
524
|
+
project_id: repoEntry?.ok ? repoEntry.export?.project?.id || null : null,
|
|
525
|
+
project_name: repoEntry?.ok ? repoEntry.export?.project?.name || null : null,
|
|
526
|
+
error: repoEntry?.ok ? null : repoEntry?.error || null,
|
|
527
|
+
};
|
|
528
|
+
if (!repoEntry?.ok || !repoEntry?.export) return base;
|
|
529
|
+
const childExport = repoEntry.export;
|
|
530
|
+
base.turns = extractHistoryTimeline(childExport);
|
|
531
|
+
base.decisions = extractDecisionDigest(childExport);
|
|
532
|
+
base.hook_summary = extractHookSummary(childExport);
|
|
533
|
+
base.gate_summary = extractGateSummary(childExport);
|
|
534
|
+
base.recovery_summary = extractRecoverySummary(childExport);
|
|
535
|
+
base.blocked_on = childExport.state?.blocked_on || null;
|
|
536
|
+
return base;
|
|
537
|
+
});
|
|
130
538
|
|
|
131
539
|
const repoErrorCount = repos.filter((repo) => !repo.ok).length;
|
|
540
|
+
const coordinatorTimeline = extractCoordinatorTimeline(artifact);
|
|
541
|
+
const barrierSummary = extractBarrierSummary(artifact);
|
|
542
|
+
const barrierLedgerTimeline = extractBarrierLedgerTimeline(artifact);
|
|
543
|
+
const decisionDigest = extractCoordinatorDecisionDigest(artifact);
|
|
544
|
+
const timing = computeCoordinatorTiming(artifact, coordinatorTimeline);
|
|
545
|
+
const blockedReason = normalizeCoordinatorBlockedReason(coordinatorState.blocked_reason);
|
|
546
|
+
const pendingGate = normalizePendingGate(coordinatorState.pending_gate);
|
|
547
|
+
const nextActions = deriveCoordinatorNextActions({
|
|
548
|
+
status: artifact.summary?.status || null,
|
|
549
|
+
blockedReason,
|
|
550
|
+
pendingGate,
|
|
551
|
+
repos,
|
|
552
|
+
coordinatorRepoRuns: coordinatorState.repo_runs || {},
|
|
553
|
+
});
|
|
132
554
|
|
|
133
555
|
return {
|
|
134
556
|
kind: 'coordinator_workspace',
|
|
@@ -143,11 +565,21 @@ function buildCoordinatorSubject(artifact) {
|
|
|
143
565
|
super_run_id: artifact.summary?.super_run_id || null,
|
|
144
566
|
status: artifact.summary?.status || null,
|
|
145
567
|
phase: artifact.summary?.phase || null,
|
|
568
|
+
blocked_reason: blockedReason,
|
|
569
|
+
pending_gate: pendingGate,
|
|
570
|
+
next_actions: nextActions,
|
|
571
|
+
created_at: timing.created_at,
|
|
572
|
+
completed_at: timing.completed_at,
|
|
573
|
+
duration_seconds: timing.duration_seconds,
|
|
146
574
|
barrier_count: artifact.summary?.barrier_count || 0,
|
|
147
575
|
repo_status_counts: repoStatusCounts,
|
|
148
576
|
repo_ok_count: repos.length - repoErrorCount,
|
|
149
577
|
repo_error_count: repoErrorCount,
|
|
150
578
|
},
|
|
579
|
+
coordinator_timeline: coordinatorTimeline,
|
|
580
|
+
barrier_summary: barrierSummary,
|
|
581
|
+
barrier_ledger_timeline: barrierLedgerTimeline,
|
|
582
|
+
decision_digest: decisionDigest,
|
|
151
583
|
repos,
|
|
152
584
|
artifacts: {
|
|
153
585
|
history_entries: artifact.summary?.history_entries || 0,
|
|
@@ -250,6 +682,16 @@ export function formatGovernanceReportText(report) {
|
|
|
250
682
|
);
|
|
251
683
|
}
|
|
252
684
|
|
|
685
|
+
if (run.created_at) {
|
|
686
|
+
lines.push(`Started: ${run.created_at}`);
|
|
687
|
+
}
|
|
688
|
+
if (run.completed_at) {
|
|
689
|
+
lines.push(`Completed: ${run.completed_at}`);
|
|
690
|
+
}
|
|
691
|
+
if (run.duration_seconds != null) {
|
|
692
|
+
lines.push(`Duration: ${run.duration_seconds}s`);
|
|
693
|
+
}
|
|
694
|
+
|
|
253
695
|
lines.push(
|
|
254
696
|
`History entries: ${artifacts.history_entries}`,
|
|
255
697
|
`Decision entries: ${artifacts.decision_entries}`,
|
|
@@ -261,11 +703,59 @@ export function formatGovernanceReportText(report) {
|
|
|
261
703
|
`Coordinator artifacts: ${yesNo(artifacts.coordinator_present)}`,
|
|
262
704
|
);
|
|
263
705
|
|
|
706
|
+
if (run.turns && run.turns.length > 0) {
|
|
707
|
+
lines.push('', 'Turn Timeline:');
|
|
708
|
+
for (let i = 0; i < run.turns.length; i++) {
|
|
709
|
+
const t = run.turns[i];
|
|
710
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
711
|
+
const phase = t.phase_transition ? `${t.phase || '?'} -> ${t.phase_transition}` : (t.phase || '?');
|
|
712
|
+
lines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (run.decisions && run.decisions.length > 0) {
|
|
717
|
+
lines.push('', 'Decisions:');
|
|
718
|
+
for (const d of run.decisions) {
|
|
719
|
+
lines.push(` - ${d.id} (${d.role || '?'}, ${d.phase || '?'}): ${d.statement}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (run.gate_summary && run.gate_summary.length > 0) {
|
|
724
|
+
lines.push('', 'Gate Outcomes:');
|
|
725
|
+
for (const gate of run.gate_summary) {
|
|
726
|
+
lines.push(` - ${gate.gate_id}: ${gate.status}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (run.intake_links && run.intake_links.length > 0) {
|
|
731
|
+
lines.push('', 'Intake Linkage:');
|
|
732
|
+
for (const intake of run.intake_links) {
|
|
733
|
+
lines.push(` - ${intake.intent_id} | status: ${intake.status || 'unknown'} | event: ${intake.event_id || 'n/a'} | target turn: ${intake.target_turn || 'n/a'} | started: ${intake.started_at || 'n/a'}`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (run.hook_summary) {
|
|
738
|
+
lines.push('', 'Hook Activity:');
|
|
739
|
+
lines.push(` Total: ${run.hook_summary.total}, Blocked: ${run.hook_summary.blocked}`);
|
|
740
|
+
const eventList = Object.entries(run.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${e}(${c})`).join(', ');
|
|
741
|
+
if (eventList) lines.push(` Events: ${eventList}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (run.recovery_summary) {
|
|
745
|
+
lines.push('', 'Recovery:');
|
|
746
|
+
lines.push(` Category: ${run.recovery_summary.category || 'unknown'}`);
|
|
747
|
+
lines.push(` Typed reason: ${run.recovery_summary.typed_reason || 'unknown'}`);
|
|
748
|
+
lines.push(` Owner: ${run.recovery_summary.owner || 'unknown'}`);
|
|
749
|
+
lines.push(` Action: ${run.recovery_summary.recovery_action || 'n/a'}`);
|
|
750
|
+
lines.push(` Detail: ${run.recovery_summary.detail || 'n/a'}`);
|
|
751
|
+
lines.push(` Turn retained: ${run.recovery_summary.turn_retained == null ? 'n/a' : yesNo(run.recovery_summary.turn_retained)}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
264
754
|
return lines.join('\n');
|
|
265
755
|
}
|
|
266
756
|
|
|
267
|
-
const { coordinator, run, artifacts, repos } = report.subject;
|
|
268
|
-
|
|
757
|
+
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest } = report.subject;
|
|
758
|
+
const lines = [
|
|
269
759
|
'AgentXchain Governance Report',
|
|
270
760
|
`Input: ${report.input}`,
|
|
271
761
|
`Export kind: ${report.export_kind}`,
|
|
@@ -275,17 +765,107 @@ export function formatGovernanceReportText(report) {
|
|
|
275
765
|
`Super run: ${run.super_run_id || 'none'}`,
|
|
276
766
|
`Status: ${run.status || 'unknown'}`,
|
|
277
767
|
`Phase: ${run.phase || 'unknown'}`,
|
|
768
|
+
`Blocked reason: ${run.blocked_reason || 'none'}`,
|
|
769
|
+
`Started: ${run.created_at || 'n/a'}`,
|
|
278
770
|
`Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
|
|
279
771
|
`Workstreams: ${coordinator.workstream_count}`,
|
|
280
772
|
`Barriers: ${run.barrier_count}`,
|
|
281
773
|
`Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
|
|
282
774
|
`History entries: ${artifacts.history_entries}`,
|
|
283
775
|
`Decision entries: ${artifacts.decision_entries}`,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
if (run.completed_at) {
|
|
779
|
+
lines.push(`Completed: ${run.completed_at}`);
|
|
780
|
+
}
|
|
781
|
+
if (run.duration_seconds != null) {
|
|
782
|
+
lines.push(`Duration: ${run.duration_seconds}s`);
|
|
783
|
+
}
|
|
784
|
+
if (run.pending_gate) {
|
|
785
|
+
lines.push(`Pending gate: ${run.pending_gate.gate} (${run.pending_gate.gate_type})`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (run.next_actions && run.next_actions.length > 0) {
|
|
789
|
+
lines.push('', 'Next Actions:');
|
|
790
|
+
for (let i = 0; i < run.next_actions.length; i++) {
|
|
791
|
+
const action = run.next_actions[i];
|
|
792
|
+
lines.push(` ${i + 1}. ${action.command} | ${action.reason}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (coordinator_timeline && coordinator_timeline.length > 0) {
|
|
797
|
+
lines.push('', 'Coordinator Timeline:');
|
|
798
|
+
for (let i = 0; i < coordinator_timeline.length; i++) {
|
|
799
|
+
const ev = coordinator_timeline[i];
|
|
800
|
+
const ts = ev.timestamp ? ` [${ev.timestamp}]` : '';
|
|
801
|
+
lines.push(` ${i + 1}. [${ev.type}]${ts} ${ev.summary}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (barrier_summary && barrier_summary.length > 0) {
|
|
806
|
+
lines.push('', 'Barrier Summary:');
|
|
807
|
+
for (const b of barrier_summary) {
|
|
808
|
+
const satisfied = b.satisfied_repos.length;
|
|
809
|
+
const required = b.required_repos.length;
|
|
810
|
+
lines.push(` - ${b.barrier_id}: ${b.status} (${b.type}, ${satisfied}/${required} repos satisfied, workstream ${b.workstream_id || 'unknown'})`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (barrier_ledger_timeline && barrier_ledger_timeline.length > 0) {
|
|
815
|
+
lines.push('', 'Barrier Transitions:');
|
|
816
|
+
for (let i = 0; i < barrier_ledger_timeline.length; i++) {
|
|
817
|
+
const t = barrier_ledger_timeline[i];
|
|
818
|
+
const ts = t.timestamp ? ` [${t.timestamp}]` : '';
|
|
819
|
+
lines.push(` ${i + 1}.${ts} ${t.summary}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (decision_digest && decision_digest.length > 0) {
|
|
824
|
+
lines.push('', 'Coordinator Decisions:');
|
|
825
|
+
for (const d of decision_digest) {
|
|
826
|
+
lines.push(` - ${d.id} (${d.role || '?'}, ${d.phase || '?'}): ${d.statement}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
lines.push('Repo details:');
|
|
831
|
+
lines.push(...repos.flatMap((repo) => {
|
|
832
|
+
if (!repo.ok) {
|
|
833
|
+
return [`- ${repo.repo_id}: failed export, ${repo.error || 'unknown error'}, path ${repo.path || 'unknown'}`];
|
|
834
|
+
}
|
|
835
|
+
const repoLines = [`- ${repo.repo_id}: ok, status ${repo.status || 'unknown'}, run ${repo.run_id || 'none'}, path ${repo.path || 'unknown'}`];
|
|
836
|
+
if (repo.blocked_on) {
|
|
837
|
+
repoLines.push(` Blocked on: ${summarizeBlockedOn(repo.blocked_on)}`);
|
|
838
|
+
}
|
|
839
|
+
if (repo.turns && repo.turns.length > 0) {
|
|
840
|
+
repoLines.push(' Turn Timeline:');
|
|
841
|
+
for (let i = 0; i < repo.turns.length; i++) {
|
|
842
|
+
const t = repo.turns[i];
|
|
843
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
844
|
+
const phase = t.phase_transition ? `${t.phase || '?'} -> ${t.phase_transition}` : (t.phase || '?');
|
|
845
|
+
repoLines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (repo.decisions && repo.decisions.length > 0) {
|
|
849
|
+
repoLines.push(' Decisions:');
|
|
850
|
+
for (const d of repo.decisions) {
|
|
851
|
+
repoLines.push(` - ${d.id} (${d.role || '?'}, ${d.phase || '?'}): ${d.statement}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (repo.gate_summary && repo.gate_summary.length > 0) {
|
|
855
|
+
repoLines.push(' Gate Outcomes:');
|
|
856
|
+
for (const gate of repo.gate_summary) {
|
|
857
|
+
repoLines.push(` - ${gate.gate_id}: ${gate.status}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (repo.hook_summary) {
|
|
861
|
+
repoLines.push(` Hook Activity: ${repo.hook_summary.total} total, ${repo.hook_summary.blocked} blocked`);
|
|
862
|
+
}
|
|
863
|
+
if (repo.recovery_summary) {
|
|
864
|
+
repoLines.push(` Recovery: ${repo.recovery_summary.category || 'unknown'} — ${repo.recovery_summary.typed_reason || 'unknown'} (owner: ${repo.recovery_summary.owner || 'unknown'})`);
|
|
865
|
+
}
|
|
866
|
+
return repoLines;
|
|
867
|
+
}));
|
|
868
|
+
return lines.join('\n');
|
|
289
869
|
}
|
|
290
870
|
|
|
291
871
|
export function formatGovernanceReportMarkdown(report) {
|
|
@@ -337,6 +917,16 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
337
917
|
lines.push(`- Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`);
|
|
338
918
|
}
|
|
339
919
|
|
|
920
|
+
if (run.created_at) {
|
|
921
|
+
lines.push(`- Started: \`${run.created_at}\``);
|
|
922
|
+
}
|
|
923
|
+
if (run.completed_at) {
|
|
924
|
+
lines.push(`- Completed: \`${run.completed_at}\``);
|
|
925
|
+
}
|
|
926
|
+
if (run.duration_seconds != null) {
|
|
927
|
+
lines.push(`- Duration: \`${run.duration_seconds}s\``);
|
|
928
|
+
}
|
|
929
|
+
|
|
340
930
|
lines.push(
|
|
341
931
|
`- History entries: ${artifacts.history_entries}`,
|
|
342
932
|
`- Decision entries: ${artifacts.decision_entries}`,
|
|
@@ -348,11 +938,61 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
348
938
|
`- Coordinator artifacts: \`${yesNo(artifacts.coordinator_present)}\``,
|
|
349
939
|
);
|
|
350
940
|
|
|
941
|
+
if (run.turns && run.turns.length > 0) {
|
|
942
|
+
lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
|
|
943
|
+
for (let i = 0; i < run.turns.length; i++) {
|
|
944
|
+
const t = run.turns[i];
|
|
945
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
946
|
+
const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
|
|
947
|
+
const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
|
|
948
|
+
lines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count} | ${cost} | ${t.accepted_at || 'n/a'} |`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (run.decisions && run.decisions.length > 0) {
|
|
953
|
+
lines.push('', '## Decisions', '');
|
|
954
|
+
for (const d of run.decisions) {
|
|
955
|
+
lines.push(`- **${d.id}** (${d.role || '?'}, ${d.phase || '?'} phase): ${d.statement}`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (run.gate_summary && run.gate_summary.length > 0) {
|
|
960
|
+
lines.push('', '## Gate Outcomes', '');
|
|
961
|
+
for (const gate of run.gate_summary) {
|
|
962
|
+
lines.push(`- \`${gate.gate_id}\`: \`${gate.status}\``);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (run.intake_links && run.intake_links.length > 0) {
|
|
967
|
+
lines.push('', '## Intake Linkage', '');
|
|
968
|
+
for (const intake of run.intake_links) {
|
|
969
|
+
lines.push(`- \`${intake.intent_id}\` (${intake.status || 'unknown'}) from event \`${intake.event_id || 'n/a'}\`, target turn \`${intake.target_turn || 'n/a'}\`, started \`${intake.started_at || 'n/a'}\``);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (run.hook_summary) {
|
|
974
|
+
lines.push('', '## Hook Activity', '');
|
|
975
|
+
lines.push(`- Total hook executions: ${run.hook_summary.total}`);
|
|
976
|
+
lines.push(`- Blocked: ${run.hook_summary.blocked}`);
|
|
977
|
+
const eventList = Object.entries(run.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${e}(${c})`).join(', ');
|
|
978
|
+
if (eventList) lines.push(`- Events: ${eventList}`);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (run.recovery_summary) {
|
|
982
|
+
lines.push('', '## Recovery', '');
|
|
983
|
+
lines.push(`- Category: \`${run.recovery_summary.category || 'unknown'}\``);
|
|
984
|
+
lines.push(`- Typed reason: \`${run.recovery_summary.typed_reason || 'unknown'}\``);
|
|
985
|
+
lines.push(`- Owner: \`${run.recovery_summary.owner || 'unknown'}\``);
|
|
986
|
+
lines.push(`- Action: \`${run.recovery_summary.recovery_action || 'n/a'}\``);
|
|
987
|
+
lines.push(`- Detail: ${run.recovery_summary.detail || 'n/a'}`);
|
|
988
|
+
lines.push(`- Turn retained: \`${run.recovery_summary.turn_retained == null ? 'n/a' : yesNo(run.recovery_summary.turn_retained)}\``);
|
|
989
|
+
}
|
|
990
|
+
|
|
351
991
|
return lines.join('\n');
|
|
352
992
|
}
|
|
353
993
|
|
|
354
|
-
const { coordinator, run, artifacts, repos } = report.subject;
|
|
355
|
-
|
|
994
|
+
const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest } = report.subject;
|
|
995
|
+
const mdLines = [
|
|
356
996
|
'# AgentXchain Governance Report',
|
|
357
997
|
'',
|
|
358
998
|
`- Input: \`${report.input}\``,
|
|
@@ -363,17 +1003,109 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
363
1003
|
`- Super run: \`${run.super_run_id || 'none'}\``,
|
|
364
1004
|
`- Status: \`${run.status || 'unknown'}\``,
|
|
365
1005
|
`- Phase: \`${run.phase || 'unknown'}\``,
|
|
1006
|
+
`- Blocked reason: \`${run.blocked_reason || 'none'}\``,
|
|
1007
|
+
`- Started: \`${run.created_at || 'n/a'}\``,
|
|
366
1008
|
`- Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
|
|
367
1009
|
`- Workstreams: ${coordinator.workstream_count}`,
|
|
368
1010
|
`- Barriers: ${run.barrier_count}`,
|
|
369
1011
|
`- Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
|
|
370
1012
|
`- History entries: ${artifacts.history_entries}`,
|
|
371
1013
|
`- Decision entries: ${artifacts.decision_entries}`,
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1014
|
+
];
|
|
1015
|
+
|
|
1016
|
+
if (run.completed_at) {
|
|
1017
|
+
mdLines.push(`- Completed: \`${run.completed_at}\``);
|
|
1018
|
+
}
|
|
1019
|
+
if (run.duration_seconds != null) {
|
|
1020
|
+
mdLines.push(`- Duration: \`${run.duration_seconds}s\``);
|
|
1021
|
+
}
|
|
1022
|
+
if (run.pending_gate) {
|
|
1023
|
+
mdLines.push(`- Pending gate: \`${run.pending_gate.gate}\` (\`${run.pending_gate.gate_type}\`)`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (run.next_actions && run.next_actions.length > 0) {
|
|
1027
|
+
mdLines.push('', '## Next Actions', '');
|
|
1028
|
+
for (let i = 0; i < run.next_actions.length; i++) {
|
|
1029
|
+
const action = run.next_actions[i];
|
|
1030
|
+
mdLines.push(`${i + 1}. \`${action.command}\`: ${action.reason}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (coordinator_timeline && coordinator_timeline.length > 0) {
|
|
1035
|
+
mdLines.push('', '## Coordinator Timeline', '', '| # | Type | Time | Summary |', '|---|------|------|---------|');
|
|
1036
|
+
for (let i = 0; i < coordinator_timeline.length; i++) {
|
|
1037
|
+
const ev = coordinator_timeline[i];
|
|
1038
|
+
const ts = ev.timestamp ? `\`${ev.timestamp}\`` : 'n/a';
|
|
1039
|
+
const escapedSummary = ev.summary.replace(/\|/g, '\\|');
|
|
1040
|
+
mdLines.push(`| ${i + 1} | \`${ev.type}\` | ${ts} | ${escapedSummary} |`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (barrier_summary && barrier_summary.length > 0) {
|
|
1045
|
+
mdLines.push('', '## Barrier Summary', '', '| Barrier | Workstream | Type | Status | Satisfied |', '|---------|------------|------|--------|-----------|');
|
|
1046
|
+
for (const b of barrier_summary) {
|
|
1047
|
+
mdLines.push(`| \`${b.barrier_id}\` | \`${b.workstream_id || 'unknown'}\` | \`${b.type}\` | \`${b.status}\` | ${b.satisfied_repos.length}/${b.required_repos.length} repos |`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if (barrier_ledger_timeline && barrier_ledger_timeline.length > 0) {
|
|
1052
|
+
mdLines.push('', '## Barrier Transitions', '', '| # | Time | Barrier | From | To | Summary |', '|---|------|---------|------|----|---------|');
|
|
1053
|
+
for (let i = 0; i < barrier_ledger_timeline.length; i++) {
|
|
1054
|
+
const t = barrier_ledger_timeline[i];
|
|
1055
|
+
const ts = t.timestamp ? `\`${t.timestamp}\`` : 'n/a';
|
|
1056
|
+
const escapedSummary = t.summary.replace(/\|/g, '\\|');
|
|
1057
|
+
mdLines.push(`| ${i + 1} | ${ts} | \`${t.barrier_id}\` | \`${t.previous_status}\` | \`${t.new_status}\` | ${escapedSummary} |`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (decision_digest && decision_digest.length > 0) {
|
|
1062
|
+
mdLines.push('', '## Coordinator Decisions', '');
|
|
1063
|
+
for (const d of decision_digest) {
|
|
1064
|
+
mdLines.push(`- **${d.id}** (${d.role || '?'}, ${d.phase || '?'} phase): ${d.statement}`);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
mdLines.push('', '## Repo Details', '');
|
|
1069
|
+
mdLines.push(...repos.flatMap((repo) => {
|
|
1070
|
+
if (!repo.ok) {
|
|
1071
|
+
return [`- \`${repo.repo_id}\`: failed export, ${repo.error || 'unknown error'}, path \`${repo.path || 'unknown'}\``];
|
|
1072
|
+
}
|
|
1073
|
+
const repoLines = [`### ${repo.repo_id}`, '', `- Status: \`${repo.status || 'unknown'}\``, `- Run: \`${repo.run_id || 'none'}\``, `- Phase: \`${repo.phase || 'unknown'}\``, `- Path: \`${repo.path || 'unknown'}\``];
|
|
1074
|
+
if (repo.blocked_on) {
|
|
1075
|
+
repoLines.push(`- Blocked on: \`${summarizeBlockedOn(repo.blocked_on)}\``);
|
|
1076
|
+
}
|
|
1077
|
+
if (repo.turns && repo.turns.length > 0) {
|
|
1078
|
+
repoLines.push('', '#### Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
|
|
1079
|
+
for (let i = 0; i < repo.turns.length; i++) {
|
|
1080
|
+
const t = repo.turns[i];
|
|
1081
|
+
const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
|
|
1082
|
+
const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
|
|
1083
|
+
const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
|
|
1084
|
+
repoLines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count} | ${cost} | ${t.accepted_at || 'n/a'} |`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
if (repo.decisions && repo.decisions.length > 0) {
|
|
1088
|
+
repoLines.push('', '#### Decisions', '');
|
|
1089
|
+
for (const d of repo.decisions) {
|
|
1090
|
+
repoLines.push(`- **${d.id}** (${d.role || '?'}, ${d.phase || '?'} phase): ${d.statement}`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (repo.gate_summary && repo.gate_summary.length > 0) {
|
|
1094
|
+
repoLines.push('', '#### Gate Outcomes', '');
|
|
1095
|
+
for (const gate of repo.gate_summary) {
|
|
1096
|
+
repoLines.push(`- \`${gate.gate_id}\`: \`${gate.status}\``);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (repo.hook_summary) {
|
|
1100
|
+
repoLines.push('', '#### Hook Activity', '', `- Total: ${repo.hook_summary.total}`, `- Blocked: ${repo.hook_summary.blocked}`);
|
|
1101
|
+
const eventList = Object.entries(repo.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${e}(${c})`).join(', ');
|
|
1102
|
+
if (eventList) repoLines.push(`- Events: ${eventList}`);
|
|
1103
|
+
}
|
|
1104
|
+
if (repo.recovery_summary) {
|
|
1105
|
+
repoLines.push('', '#### Recovery', '', `- Category: \`${repo.recovery_summary.category || 'unknown'}\``, `- Typed reason: \`${repo.recovery_summary.typed_reason || 'unknown'}\``, `- Owner: \`${repo.recovery_summary.owner || 'unknown'}\``, `- Action: \`${repo.recovery_summary.recovery_action || 'n/a'}\``);
|
|
1106
|
+
}
|
|
1107
|
+
repoLines.push('');
|
|
1108
|
+
return repoLines;
|
|
1109
|
+
}));
|
|
1110
|
+
return mdLines.join('\n');
|
|
379
1111
|
}
|