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/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
- repo_id: repoId,
121
- path: repoEntry?.path || null,
122
- ok: Boolean(repoEntry?.ok),
123
- status: repoEntry?.ok ? repoEntry.export?.summary?.status || null : null,
124
- run_id: repoEntry?.ok ? repoEntry.export?.summary?.run_id || null : null,
125
- phase: repoEntry?.ok ? repoEntry.export?.summary?.phase || null : null,
126
- project_id: repoEntry?.ok ? repoEntry.export?.project?.id || null : null,
127
- project_name: repoEntry?.ok ? repoEntry.export?.project?.name || null : null,
128
- error: repoEntry?.ok ? null : repoEntry?.error || null,
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
- return [
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
- 'Repo details:',
285
- ...repos.map((repo) => repo.ok
286
- ? `- ${repo.repo_id}: ok, status ${repo.status || 'unknown'}, run ${repo.run_id || 'none'}, path ${repo.path || 'unknown'}`
287
- : `- ${repo.repo_id}: failed export, ${repo.error || 'unknown error'}, path ${repo.path || 'unknown'}`),
288
- ].join('\n');
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
- return [
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
- '## Repo Details',
374
- '',
375
- ...repos.map((repo) => repo.ok
376
- ? `- \`${repo.repo_id}\`: ok, status \`${repo.status || 'unknown'}\`, run \`${repo.run_id || 'none'}\`, path \`${repo.path || 'unknown'}\``
377
- : `- \`${repo.repo_id}\`: failed export, ${repo.error || 'unknown error'}, path \`${repo.path || 'unknown'}\``),
378
- ].join('\n');
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
  }