agentxchain 2.91.0 → 2.93.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
@@ -5,6 +5,7 @@ import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance
5
5
  export const GOVERNANCE_REPORT_VERSION = '0.1';
6
6
 
7
7
  const VALID_DELEGATION_OUTCOMES = new Set(['completed', 'failed', 'mixed', 'pending']);
8
+ const VALID_DASHBOARD_SESSION_STATUSES = new Set(['running', 'pid_only', 'stale', 'not_running']);
8
9
 
9
10
  function normalizeDelegationSummary(summary) {
10
11
  if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
@@ -58,6 +59,37 @@ function extractDelegationSummary(artifact) {
58
59
  return normalizeDelegationSummary(buildDelegationSummary(artifact.files || {}));
59
60
  }
60
61
 
62
+ function normalizeDashboardSessionSummary(summary) {
63
+ if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return null;
64
+ if (!VALID_DASHBOARD_SESSION_STATUSES.has(summary.status)) return null;
65
+ return {
66
+ status: summary.status,
67
+ pid: Number.isInteger(summary.pid) ? summary.pid : null,
68
+ url: typeof summary.url === 'string' && summary.url.length > 0 ? summary.url : null,
69
+ started_at: typeof summary.started_at === 'string' && summary.started_at.length > 0 ? summary.started_at : null,
70
+ };
71
+ }
72
+
73
+ function extractDashboardSessionSummary(artifact) {
74
+ return normalizeDashboardSessionSummary(artifact.summary?.dashboard_session);
75
+ }
76
+
77
+ function formatDashboardSessionLine(session) {
78
+ if (!session) return null;
79
+ switch (session.status) {
80
+ case 'running':
81
+ return `running at ${session.url || 'unknown url'} (PID: ${session.pid || '?'})`;
82
+ case 'pid_only':
83
+ return `pid_only (PID: ${session.pid || '?'}, session metadata missing)`;
84
+ case 'stale':
85
+ return `stale session files${session.pid ? ` (PID: ${session.pid})` : ''}${session.url ? ` at ${session.url}` : ''}`;
86
+ case 'not_running':
87
+ return 'not_running';
88
+ default:
89
+ return null;
90
+ }
91
+ }
92
+
61
93
  function yesNo(value) {
62
94
  return value ? 'yes' : 'no';
63
95
  }
@@ -916,6 +948,7 @@ function buildRunSubject(artifact) {
916
948
  const continuity = extractContinuityMetadata(artifact);
917
949
  const governanceEvents = extractGovernanceEventDigest(artifact);
918
950
  const delegationSummary = extractDelegationSummary(artifact);
951
+ const dashboardSession = extractDashboardSessionSummary(artifact);
919
952
 
920
953
  return {
921
954
  kind: 'governed_run',
@@ -942,6 +975,7 @@ function buildRunSubject(artifact) {
942
975
  active_roles: activeRoles,
943
976
  budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
944
977
  cost_summary: computeCostSummary(turns),
978
+ dashboard_session: dashboardSession,
945
979
  created_at: timing.created_at,
946
980
  completed_at: timing.completed_at,
947
981
  duration_seconds: timing.duration_seconds,
@@ -958,6 +992,7 @@ function buildRunSubject(artifact) {
958
992
  recovery_summary: recoverySummary,
959
993
  continuity,
960
994
  workflow_kit_artifacts: extractWorkflowKitArtifacts(artifact),
995
+ repo_decisions: artifact.summary?.repo_decisions || null,
961
996
  },
962
997
  artifacts: {
963
998
  history_entries: artifact.summary?.history_entries || 0,
@@ -1209,6 +1244,9 @@ export function formatGovernanceReportText(report) {
1209
1244
  if (run.inherited_context?.parent_run_id) {
1210
1245
  lines.push(`Inherited from: ${run.inherited_context.parent_run_id} (${run.inherited_context.parent_status || 'unknown'})`);
1211
1246
  }
1247
+ if (run.dashboard_session) {
1248
+ lines.push(`Dashboard session: ${formatDashboardSessionLine(run.dashboard_session)}`);
1249
+ }
1212
1250
 
1213
1251
  lines.push(
1214
1252
  `History entries: ${artifacts.history_entries}`,
@@ -1256,6 +1294,14 @@ export function formatGovernanceReportText(report) {
1256
1294
  }
1257
1295
  }
1258
1296
 
1297
+ if (run.repo_decisions?.active?.length > 0) {
1298
+ lines.push('', 'Repo Decisions:');
1299
+ lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
1300
+ for (const d of run.repo_decisions.active) {
1301
+ lines.push(` - ${d.id} (${d.category}): ${d.statement}`);
1302
+ }
1303
+ }
1304
+
1259
1305
  if (run.turns && run.turns.length > 0) {
1260
1306
  lines.push('', 'Turn Timeline:');
1261
1307
  for (let i = 0; i < run.turns.length; i++) {
@@ -1688,6 +1734,9 @@ export function formatGovernanceReportMarkdown(report) {
1688
1734
  if (run.inherited_context?.parent_run_id) {
1689
1735
  lines.push(`- Inherited from: \`${run.inherited_context.parent_run_id}\` (${run.inherited_context.parent_status || 'unknown'})`);
1690
1736
  }
1737
+ if (run.dashboard_session) {
1738
+ lines.push(`- Dashboard session: \`${formatDashboardSessionLine(run.dashboard_session)}\``);
1739
+ }
1691
1740
 
1692
1741
  lines.push(
1693
1742
  `- History entries: ${artifacts.history_entries}`,
@@ -1738,6 +1787,16 @@ export function formatGovernanceReportMarkdown(report) {
1738
1787
  }
1739
1788
  }
1740
1789
 
1790
+ if (run.repo_decisions?.active?.length > 0) {
1791
+ lines.push('', '## Repo Decisions', '');
1792
+ lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
1793
+ lines.push('| ID | Category | Statement | Role | Run |', '|----|----------|-----------|------|-----|');
1794
+ for (const d of run.repo_decisions.active) {
1795
+ const stmt = (d.statement || '').replace(/\|/g, '\\|');
1796
+ lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | \`${(d.run_id || '').slice(0, 12)}\` |`);
1797
+ }
1798
+ }
1799
+
1741
1800
  if (run.turns && run.turns.length > 0) {
1742
1801
  lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
1743
1802
  for (let i = 0; i < run.turns.length; i++) {
@@ -2117,3 +2176,562 @@ export function formatGovernanceReportMarkdown(report) {
2117
2176
  }));
2118
2177
  return mdLines.join('\n');
2119
2178
  }
2179
+
2180
+ // --- HTML governance report formatter ---
2181
+
2182
+ function esc(str) {
2183
+ if (typeof str !== 'string') return '';
2184
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2185
+ }
2186
+
2187
+ function badge(status) {
2188
+ const colors = {
2189
+ pass: '#22c55e', running: '#3b82f6', completed: '#22c55e',
2190
+ failed: '#ef4444', error: '#ef4444', fail: '#ef4444',
2191
+ blocked: '#f59e0b', pending: '#a855f7', mixed: '#f59e0b',
2192
+ paused: '#6b7280', not_running: '#6b7280', stale: '#f59e0b',
2193
+ pid_only: '#f59e0b',
2194
+ };
2195
+ const color = colors[status] || '#6b7280';
2196
+ return `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.85em;font-weight:600;color:#fff;background:${color}">${esc(String(status))}</span>`;
2197
+ }
2198
+
2199
+ function htmlTable(headers, rows) {
2200
+ const lines = ['<table>', '<thead><tr>'];
2201
+ for (const h of headers) lines.push(`<th>${esc(h)}</th>`);
2202
+ lines.push('</tr></thead>', '<tbody>');
2203
+ for (const row of rows) {
2204
+ lines.push('<tr>');
2205
+ for (const cell of row) lines.push(`<td>${cell}</td>`);
2206
+ lines.push('</tr>');
2207
+ }
2208
+ lines.push('</tbody>', '</table>');
2209
+ return lines.join('');
2210
+ }
2211
+
2212
+ function htmlSection(title, content, level = 2) {
2213
+ const tag = `h${level}`;
2214
+ return `<${tag}>${esc(title)}</${tag}>\n${content}`;
2215
+ }
2216
+
2217
+ function htmlDl(pairs) {
2218
+ const lines = ['<dl>'];
2219
+ for (const [label, value] of pairs) {
2220
+ lines.push(`<dt>${esc(label)}</dt><dd>${value}</dd>`);
2221
+ }
2222
+ lines.push('</dl>');
2223
+ return lines.join('');
2224
+ }
2225
+
2226
+ const HTML_STYLES = `
2227
+ *{box-sizing:border-box;margin:0;padding:0}
2228
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;line-height:1.6;color:#1a1a2e;background:#f8fafc;padding:2rem;max-width:1100px;margin:0 auto}
2229
+ h1{font-size:1.6rem;margin-bottom:0.5rem;border-bottom:2px solid #e2e8f0;padding-bottom:0.5rem}
2230
+ h2{font-size:1.2rem;margin-top:2rem;margin-bottom:0.75rem;color:#334155}
2231
+ h3{font-size:1.05rem;margin-top:1.5rem;margin-bottom:0.5rem;color:#475569}
2232
+ h4{font-size:0.95rem;margin-top:1rem;margin-bottom:0.4rem;color:#64748b}
2233
+ dl{display:grid;grid-template-columns:max-content 1fr;gap:0.3rem 1rem;margin-bottom:1rem}
2234
+ dt{font-weight:600;color:#475569;white-space:nowrap}
2235
+ dd{color:#1e293b}
2236
+ table{width:100%;border-collapse:collapse;margin:0.75rem 0 1.5rem;font-size:0.9rem}
2237
+ th{background:#f1f5f9;font-weight:600;text-align:left;padding:0.5rem 0.75rem;border-bottom:2px solid #cbd5e1;color:#334155}
2238
+ td{padding:0.4rem 0.75rem;border-bottom:1px solid #e2e8f0}
2239
+ tr:hover td{background:#f8fafc}
2240
+ code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em;background:#f1f5f9;padding:1px 4px;border-radius:3px}
2241
+ .header{display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem}
2242
+ .header-brand{font-size:0.85rem;color:#64748b}
2243
+ .meta{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
2244
+ .section{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:1.25rem;margin-bottom:1rem}
2245
+ ul{margin:0.5rem 0;padding-left:1.5rem}
2246
+ li{margin-bottom:0.25rem}
2247
+ .warn{color:#d97706;font-weight:600}
2248
+ @media(prefers-color-scheme:dark){
2249
+ body{background:#0f172a;color:#e2e8f0}
2250
+ h1{border-bottom-color:#334155}
2251
+ h2,h3,h4{color:#94a3b8}
2252
+ dt{color:#94a3b8}dd{color:#e2e8f0}
2253
+ th{background:#1e293b;border-bottom-color:#475569;color:#cbd5e1}
2254
+ td{border-bottom-color:#334155}
2255
+ tr:hover td{background:#1e293b}
2256
+ code{background:#1e293b}
2257
+ .meta,.section{background:#1e293b;border-color:#334155}
2258
+ .header-brand{color:#94a3b8}
2259
+ }
2260
+ @media print{
2261
+ body{background:#fff;color:#000;padding:1rem}
2262
+ .meta,.section{border:1px solid #ccc}
2263
+ th{background:#eee}
2264
+ }
2265
+ `;
2266
+
2267
+ function wrapHtml(title, bodyContent) {
2268
+ return `<!DOCTYPE html>
2269
+ <html lang="en">
2270
+ <head>
2271
+ <meta charset="utf-8">
2272
+ <meta name="viewport" content="width=device-width,initial-scale=1">
2273
+ <title>${esc(title)}</title>
2274
+ <style>${HTML_STYLES}</style>
2275
+ </head>
2276
+ <body>
2277
+ <div class="header">
2278
+ <h1>AgentXchain Governance Report</h1>
2279
+ <span class="header-brand">agentxchain.dev</span>
2280
+ </div>
2281
+ ${bodyContent}
2282
+ </body>
2283
+ </html>`;
2284
+ }
2285
+
2286
+ function renderHtmlGovEventDetail(evt) {
2287
+ const parts = [];
2288
+ switch (evt.type) {
2289
+ case 'policy_escalation':
2290
+ for (const v of evt.violations || []) {
2291
+ parts.push(`<li>Violation: <code>${esc(v.policy_id || '?')}</code> / <code>${esc(v.rule || '?')}</code> — ${esc(v.message || 'n/a')}</li>`);
2292
+ }
2293
+ break;
2294
+ case 'conflict_detected':
2295
+ if (evt.conflicting_files?.length > 0) parts.push(`<li>Files: ${evt.conflicting_files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</li>`);
2296
+ if (evt.overlap_ratio != null) parts.push(`<li>Overlap: ${(evt.overlap_ratio * 100).toFixed(0)}%</li>`);
2297
+ break;
2298
+ case 'operator_escalated':
2299
+ if (evt.reason) parts.push(`<li>Reason: ${esc(evt.reason)}</li>`);
2300
+ if (evt.blocked_on) parts.push(`<li>Blocked on: <code>${esc(evt.blocked_on)}</code></li>`);
2301
+ break;
2302
+ case 'escalation_resolved':
2303
+ if (evt.resolved_via) parts.push(`<li>Resolved via: <code>${esc(evt.resolved_via)}</code></li>`);
2304
+ break;
2305
+ }
2306
+ return parts.length ? `<ul>${parts.join('')}</ul>` : '';
2307
+ }
2308
+
2309
+ function renderRunHtml(report) {
2310
+ const { project, run, artifacts } = report.subject;
2311
+ const sections = [];
2312
+
2313
+ // Meta section
2314
+ const metaPairs = [
2315
+ ['Input', `<code>${esc(report.input)}</code>`],
2316
+ ['Export kind', `<code>${esc(report.export_kind)}</code>`],
2317
+ ['Verification', badge('pass')],
2318
+ ['Project', `${esc(project.name || 'unknown')} (<code>${esc(project.id || 'unknown')}</code>)`],
2319
+ ];
2320
+ if (project.goal) metaPairs.push(['Goal', esc(project.goal)]);
2321
+ metaPairs.push(
2322
+ ['Template', `<code>${esc(project.template)}</code>`],
2323
+ ['Protocol', `<code>${esc(project.protocol_mode || 'unknown')}</code> (schema <code>${esc(project.schema_version || 'unknown')}</code>)`],
2324
+ ['Run ID', `<code>${esc(run.run_id || 'none')}</code>`],
2325
+ ['Status', badge(run.status || 'unknown')],
2326
+ ['Phase', `<code>${esc(run.phase || 'unknown')}</code>`],
2327
+ ['Blocked on', `<code>${esc(summarizeBlockedState(run))}</code>`],
2328
+ ['Active turns', `${run.active_turn_count}${run.active_turn_ids.length ? ` (${run.active_turn_ids.map((id) => `<code>${esc(id)}</code>`).join(', ')})` : ''}`],
2329
+ ['Retained turns', `${run.retained_turn_count}${run.retained_turn_ids.length ? ` (${run.retained_turn_ids.map((id) => `<code>${esc(id)}</code>`).join(', ')})` : ''}`],
2330
+ ['Active roles', run.active_roles.length ? run.active_roles.map((r) => `<code>${esc(r)}</code>`).join(', ') : '<code>none</code>'],
2331
+ );
2332
+
2333
+ if (run.budget_status) {
2334
+ const warnTag = run.budget_status.warn_mode ? ' <span class="warn">[OVER BUDGET]</span>' : '';
2335
+ metaPairs.push(['Budget', `spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}${warnTag}`]);
2336
+ }
2337
+ if (run.created_at) metaPairs.push(['Started', `<code>${esc(run.created_at)}</code>`]);
2338
+ if (run.completed_at) metaPairs.push(['Completed', `<code>${esc(run.completed_at)}</code>`]);
2339
+ if (run.duration_seconds != null) metaPairs.push(['Duration', `<code>${run.duration_seconds}s</code>`]);
2340
+ if (summarizeRunProvenance(run.provenance)) metaPairs.push(['Provenance', `<code>${esc(summarizeRunProvenance(run.provenance))}</code>`]);
2341
+ if (run.inherited_context?.parent_run_id) metaPairs.push(['Inherited from', `<code>${esc(run.inherited_context.parent_run_id)}</code> (${esc(run.inherited_context.parent_status || 'unknown')})`]);
2342
+ if (run.dashboard_session) metaPairs.push(['Dashboard', `<code>${esc(formatDashboardSessionLine(run.dashboard_session))}</code>`]);
2343
+
2344
+ metaPairs.push(
2345
+ ['History entries', String(artifacts.history_entries)],
2346
+ ['Decision entries', String(artifacts.decision_entries)],
2347
+ ['Hook audit entries', String(artifacts.hook_audit_entries)],
2348
+ ['Notification entries', String(artifacts.notification_audit_entries)],
2349
+ ['Dispatch files', String(artifacts.dispatch_artifact_files)],
2350
+ ['Staging files', String(artifacts.staging_artifact_files)],
2351
+ ['Intake artifacts', artifacts.intake_present ? 'yes' : 'no'],
2352
+ ['Coordinator artifacts', artifacts.coordinator_present ? 'yes' : 'no'],
2353
+ );
2354
+
2355
+ sections.push(`<div class="meta">${htmlDl(metaPairs)}</div>`);
2356
+
2357
+ // Cost Summary
2358
+ if (run.cost_summary) {
2359
+ const cs = run.cost_summary;
2360
+ let costHtml = `<p><strong>Total:</strong> ${formatUsd(cs.total_usd)} across ${cs.turn_count} turn${cs.turn_count !== 1 ? 's' : ''} (${cs.costed_turn_count} with cost data)</p>`;
2361
+ if (cs.total_input_tokens != null || cs.total_output_tokens != null) {
2362
+ costHtml += `<p><strong>Tokens:</strong> ${formatTokenCount(cs.total_input_tokens)} input / ${formatTokenCount(cs.total_output_tokens)} output</p>`;
2363
+ }
2364
+ if (cs.by_role.length > 0) {
2365
+ costHtml += htmlTable(
2366
+ ['Role', 'Cost', 'Turns', 'Input Tokens', 'Output Tokens'],
2367
+ cs.by_role.map((r) => [esc(r.role), formatUsd(r.usd), String(r.turns), formatTokenCount(r.input_tokens), formatTokenCount(r.output_tokens)]),
2368
+ );
2369
+ }
2370
+ if (cs.by_phase.length > 0) {
2371
+ costHtml += htmlTable(
2372
+ ['Phase', 'Cost', 'Turns'],
2373
+ cs.by_phase.map((p) => [esc(p.phase), formatUsd(p.usd), String(p.turns)]),
2374
+ );
2375
+ }
2376
+ sections.push(`<div class="section">${htmlSection('Cost Summary', costHtml)}</div>`);
2377
+ }
2378
+
2379
+ // Delegation Summary
2380
+ if (run.delegation_summary?.delegation_chains?.length > 0) {
2381
+ const ds = run.delegation_summary;
2382
+ let delHtml = `<p>Total delegations issued: ${ds.total_delegations_issued}</p>`;
2383
+ const rows = [];
2384
+ for (const chain of ds.delegation_chains) {
2385
+ for (let i = 0; i < chain.delegations.length; i++) {
2386
+ const d = chain.delegations[i];
2387
+ rows.push([
2388
+ i === 0 ? esc(chain.parent_role) : '',
2389
+ i === 0 ? `<code>${esc(chain.parent_turn_id)}</code>` : '',
2390
+ i === 0 ? badge(chain.outcome) : '',
2391
+ i === 0 ? `<code>${esc(chain.review_turn_id || 'pending')}</code>` : '',
2392
+ `<code>${esc(d.delegation_id)}</code> &rarr; <code>${esc(d.to_role)}</code>`,
2393
+ `<code>${esc(d.child_turn_id || 'pending')}</code>`,
2394
+ badge(d.status),
2395
+ esc(d.charter),
2396
+ ]);
2397
+ }
2398
+ }
2399
+ delHtml += htmlTable(['Parent Role', 'Parent Turn', 'Outcome', 'Review Turn', 'Delegation', 'Child Turn', 'Status', 'Charter'], rows);
2400
+ sections.push(`<div class="section">${htmlSection('Delegation Summary', delHtml)}</div>`);
2401
+ }
2402
+
2403
+ // Repo Decisions
2404
+ if (run.repo_decisions?.active?.length > 0) {
2405
+ let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
2406
+ rdHtml += htmlTable(
2407
+ ['ID', 'Category', 'Statement', 'Role', 'Run'],
2408
+ run.repo_decisions.active.map((d) => [esc(d.id), esc(d.category), esc(d.statement || ''), esc(d.role || '\u2014'), `<code>${esc((d.run_id || '').slice(0, 12))}</code>`]),
2409
+ );
2410
+ sections.push(`<div class="section">${htmlSection('Repo Decisions', rdHtml)}</div>`);
2411
+ }
2412
+
2413
+ // Turn Timeline
2414
+ if (run.turns && run.turns.length > 0) {
2415
+ const turnRows = run.turns.map((t, i) => {
2416
+ const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
2417
+ const phase = t.phase_transition ? `${esc(t.phase || '?')} &rarr; ${esc(t.phase_transition)}` : esc(t.phase || '?');
2418
+ const sibNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
2419
+ return [String(i + 1), esc(t.role), phase, esc(t.summary || '(no summary)'), `${t.files_changed_count}${sibNote}`, cost, esc(formatTurnTimelineTime(t))];
2420
+ });
2421
+ sections.push(`<div class="section">${htmlSection('Turn Timeline', htmlTable(['#', 'Role', 'Phase', 'Summary', 'Files', 'Cost', 'Time'], turnRows))}</div>`);
2422
+ }
2423
+
2424
+ // Decisions
2425
+ if (run.decisions && run.decisions.length > 0) {
2426
+ const decList = run.decisions.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('');
2427
+ sections.push(`<div class="section">${htmlSection('Decisions', `<ul>${decList}</ul>`)}</div>`);
2428
+ }
2429
+
2430
+ // Gate Outcomes
2431
+ if (run.gate_summary && run.gate_summary.length > 0) {
2432
+ const gateList = run.gate_summary.map((g) => `<li><code>${esc(g.gate_id)}</code>: ${badge(g.status)}</li>`).join('');
2433
+ sections.push(`<div class="section">${htmlSection('Gate Outcomes', `<ul>${gateList}</ul>`)}</div>`);
2434
+ }
2435
+
2436
+ // Gate Failures
2437
+ if (run.gate_failures && run.gate_failures.length > 0) {
2438
+ let gfHtml = '<ul>';
2439
+ for (const failure of run.gate_failures) {
2440
+ const request = failure.gate_type === 'run_completion' ? 'run completion' : `${esc(failure.from_phase || failure.phase || '?')} &rarr; ${esc(failure.to_phase || '?')}`;
2441
+ gfHtml += `<li><code>${esc(failure.gate_id || 'unknown')}</code> (${esc(failure.gate_type || 'unknown')}) at <code>${esc(failure.failed_at || 'n/a')}</code>: ${request}`;
2442
+ if (failure.reasons?.length) {
2443
+ gfHtml += '<ul>' + failure.reasons.map((r) => `<li>${esc(r)}</li>`).join('') + '</ul>';
2444
+ }
2445
+ gfHtml += '</li>';
2446
+ }
2447
+ gfHtml += '</ul>';
2448
+ sections.push(`<div class="section">${htmlSection('Gate Failures', gfHtml)}</div>`);
2449
+ }
2450
+
2451
+ // Approval Policy
2452
+ if (run.approval_policy_events && run.approval_policy_events.length > 0) {
2453
+ let apHtml = '<ul>';
2454
+ for (const evt of run.approval_policy_events) {
2455
+ const transition = evt.gate_type === 'run_completion' ? 'run completion' : `${esc(evt.from_phase || '?')} &rarr; ${esc(evt.to_phase || '?')}`;
2456
+ apHtml += `<li><strong>${esc(evt.action || 'unknown')}</strong> (${esc(evt.gate_type || 'unknown')}) ${transition} at <code>${esc(evt.timestamp || 'n/a')}</code>`;
2457
+ if (evt.reason) apHtml += `<br>${esc(evt.reason)}`;
2458
+ apHtml += '</li>';
2459
+ }
2460
+ apHtml += '</ul>';
2461
+ sections.push(`<div class="section">${htmlSection('Approval Policy', apHtml)}</div>`);
2462
+ }
2463
+
2464
+ // Governance Events
2465
+ if (run.governance_events && run.governance_events.length > 0) {
2466
+ let geHtml = '<ul>';
2467
+ for (const evt of run.governance_events) {
2468
+ geHtml += `<li><strong>${esc(evt.type)}</strong> (<code>${esc(evt.role || '?')}</code>, <code>${esc(evt.phase || '?')}</code> phase) at <code>${esc(evt.timestamp || 'n/a')}</code>${renderHtmlGovEventDetail(evt)}</li>`;
2469
+ }
2470
+ geHtml += '</ul>';
2471
+ sections.push(`<div class="section">${htmlSection('Governance Events', geHtml)}</div>`);
2472
+ }
2473
+
2474
+ // Timeout Events
2475
+ if (run.timeout_events && run.timeout_events.length > 0) {
2476
+ let teHtml = '<ul>';
2477
+ for (const evt of run.timeout_events) {
2478
+ const label = evt.type === 'timeout_warning' ? 'Warning' : evt.type === 'timeout_skip' ? 'Skip' : evt.type === 'timeout_skip_failed' ? 'Skip Failed' : 'Escalation';
2479
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
2480
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
2481
+ const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
2482
+ teHtml += `<li><strong>${label}</strong> (<code>${esc(evt.scope || '?')}</code> scope) \u2014 ${elapsed}/${limit}${exceeded}, action: <code>${esc(evt.action || 'n/a')}</code>, phase: <code>${esc(evt.phase || 'n/a')}</code> at <code>${esc(evt.timestamp || 'n/a')}</code></li>`;
2483
+ }
2484
+ teHtml += '</ul>';
2485
+ sections.push(`<div class="section">${htmlSection('Timeout Events', teHtml)}</div>`);
2486
+ }
2487
+
2488
+ // Intake Linkage
2489
+ if (run.intake_links && run.intake_links.length > 0) {
2490
+ const ilRows = run.intake_links.map((intake) => [
2491
+ `<code>${esc(intake.intent_id)}</code>`,
2492
+ badge(intake.status || 'unknown'),
2493
+ `<code>${esc(intake.event_id || 'n/a')}</code>`,
2494
+ `<code>${esc(intake.target_turn || 'n/a')}</code>`,
2495
+ `<code>${esc(intake.started_at || 'n/a')}</code>`,
2496
+ ]);
2497
+ sections.push(`<div class="section">${htmlSection('Intake Linkage', htmlTable(['Intent', 'Status', 'Event', 'Target Turn', 'Started'], ilRows))}</div>`);
2498
+ }
2499
+
2500
+ // Hook Activity
2501
+ if (run.hook_summary) {
2502
+ const eventList = Object.entries(run.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${esc(e)}(${c})`).join(', ');
2503
+ const hookHtml = htmlDl([
2504
+ ['Total executions', String(run.hook_summary.total)],
2505
+ ['Blocked', String(run.hook_summary.blocked)],
2506
+ ...(eventList ? [['Events', eventList]] : []),
2507
+ ]);
2508
+ sections.push(`<div class="section">${htmlSection('Hook Activity', hookHtml)}</div>`);
2509
+ }
2510
+
2511
+ // Recovery
2512
+ if (run.recovery_summary) {
2513
+ const rs = run.recovery_summary;
2514
+ sections.push(`<div class="section">${htmlSection('Recovery', htmlDl([
2515
+ ['Category', `<code>${esc(rs.category || 'unknown')}</code>`],
2516
+ ['Typed reason', `<code>${esc(rs.typed_reason || 'unknown')}</code>`],
2517
+ ['Owner', `<code>${esc(rs.owner || 'unknown')}</code>`],
2518
+ ['Action', `<code>${esc(rs.recovery_action || 'n/a')}</code>`],
2519
+ ['Detail', esc(rs.detail || 'n/a')],
2520
+ ['Turn retained', rs.turn_retained == null ? 'n/a' : (rs.turn_retained ? 'yes' : 'no')],
2521
+ ]))}</div>`);
2522
+ }
2523
+
2524
+ // Continuity
2525
+ if (run.continuity) {
2526
+ const pairs = [
2527
+ ['Session', `<code>${esc(run.continuity.session_id || 'unknown')}</code>`],
2528
+ ['Checkpoint', `<code>${esc(run.continuity.checkpoint_reason || 'unknown')}</code> at <code>${esc(run.continuity.last_checkpoint_at || 'n/a')}</code>`],
2529
+ ['Last turn', `<code>${esc(run.continuity.last_turn_id || 'none')}</code>`],
2530
+ ['Last role', `<code>${esc(run.continuity.last_role || 'unknown')}</code>`],
2531
+ ['Last phase', `<code>${esc(run.continuity.last_phase || 'unknown')}</code>`],
2532
+ ];
2533
+ if (run.continuity.stale_checkpoint) {
2534
+ pairs.push(['Warning', `<span class="warn">checkpoint tracks run <code>${esc(run.continuity.run_id)}</code>, but export tracks <code>${esc(run.run_id)}</code></span>`]);
2535
+ }
2536
+ sections.push(`<div class="section">${htmlSection('Continuity', htmlDl(pairs))}</div>`);
2537
+ }
2538
+
2539
+ // Workflow Artifacts
2540
+ if (Array.isArray(run.workflow_kit_artifacts) && run.workflow_kit_artifacts.length > 0) {
2541
+ let waHtml = `<p>Phase: <code>${esc(run.phase || 'unknown')}</code></p>`;
2542
+ waHtml += htmlTable(
2543
+ ['Artifact', 'Required', 'Semantics', 'Owner', 'Resolution', 'Status'],
2544
+ run.workflow_kit_artifacts.map((art) => [
2545
+ `<code>${esc(art.path)}</code>`,
2546
+ art.required ? 'yes' : 'no',
2547
+ art.semantics ? `<code>${esc(art.semantics)}</code>` : 'none',
2548
+ art.owned_by ? `<code>${esc(art.owned_by)}</code>` : 'none',
2549
+ esc(art.owner_resolution),
2550
+ art.exists ? 'exists' : '<strong class="warn">missing</strong>',
2551
+ ]),
2552
+ );
2553
+ sections.push(`<div class="section">${htmlSection('Workflow Artifacts', waHtml)}</div>`);
2554
+ }
2555
+
2556
+ return wrapHtml('AgentXchain Governance Report', sections.join('\n'));
2557
+ }
2558
+
2559
+ function renderCoordinatorHtml(report) {
2560
+ const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, approval_policy_events, governance_events, timeout_events, recovery_report: coordRecoveryReport } = report.subject;
2561
+ const sections = [];
2562
+
2563
+ const metaPairs = [
2564
+ ['Input', `<code>${esc(report.input)}</code>`],
2565
+ ['Export kind', `<code>${esc(report.export_kind)}</code>`],
2566
+ ['Verification', badge('pass')],
2567
+ ['Workspace', `${esc(coordinator.project_name || 'unknown')} (<code>${esc(coordinator.project_id || 'unknown')}</code>)`],
2568
+ ['Schema', `<code>${esc(coordinator.schema_version || 'unknown')}</code>`],
2569
+ ['Super run', `<code>${esc(run.super_run_id || 'none')}</code>`],
2570
+ ['Status', badge(run.status || 'unknown')],
2571
+ ['Phase', `<code>${esc(run.phase || 'unknown')}</code>`],
2572
+ ['Blocked reason', `<code>${esc(run.blocked_reason || 'none')}</code>`],
2573
+ ];
2574
+
2575
+ if (run.run_id_mismatches?.length > 0) {
2576
+ metaPairs.push(['Run ID mismatches', `<strong class="warn">${run.run_id_mismatches.length}</strong>`]);
2577
+ }
2578
+
2579
+ metaPairs.push(
2580
+ ['Started', `<code>${esc(run.created_at || 'n/a')}</code>`],
2581
+ ['Repos', `${coordinator.repo_count} total, ${run.repo_ok_count} exported, ${run.repo_error_count} failed`],
2582
+ ['Workstreams', String(coordinator.workstream_count)],
2583
+ ['Barriers', String(run.barrier_count)],
2584
+ ['Repo statuses', formatStatusCounts(run.repo_status_counts)],
2585
+ ['History entries', String(artifacts.history_entries)],
2586
+ ['Decision entries', String(artifacts.decision_entries)],
2587
+ );
2588
+ if (run.completed_at) metaPairs.push(['Completed', `<code>${esc(run.completed_at)}</code>`]);
2589
+ if (run.duration_seconds != null) metaPairs.push(['Duration', `<code>${run.duration_seconds}s</code>`]);
2590
+ if (run.pending_gate) metaPairs.push(['Pending gate', `<code>${esc(run.pending_gate.gate)}</code> (<code>${esc(run.pending_gate.gate_type)}</code>)`]);
2591
+
2592
+ sections.push(`<div class="meta">${htmlDl(metaPairs)}</div>`);
2593
+
2594
+ // Next Actions
2595
+ if (run.next_actions?.length > 0) {
2596
+ const naHtml = '<ol>' + run.next_actions.map((a) => `<li><code>${esc(a.command)}</code>: ${esc(a.reason)}</li>`).join('') + '</ol>';
2597
+ sections.push(`<div class="section">${htmlSection('Next Actions', naHtml)}</div>`);
2598
+ }
2599
+
2600
+ // Coordinator Timeline
2601
+ if (coordinator_timeline?.length > 0) {
2602
+ const tlRows = coordinator_timeline.map((ev, i) => [String(i + 1), `<code>${esc(ev.type)}</code>`, `<code>${esc(ev.timestamp || 'n/a')}</code>`, esc(ev.summary)]);
2603
+ sections.push(`<div class="section">${htmlSection('Coordinator Timeline', htmlTable(['#', 'Type', 'Time', 'Summary'], tlRows))}</div>`);
2604
+ }
2605
+
2606
+ // Barrier Summary
2607
+ if (barrier_summary?.length > 0) {
2608
+ const bRows = barrier_summary.map((b) => [
2609
+ `<code>${esc(b.barrier_id)}</code>`,
2610
+ `<code>${esc(b.workstream_id || 'unknown')}</code>`,
2611
+ `<code>${esc(b.type)}</code>`,
2612
+ badge(b.status),
2613
+ `${b.satisfied_repos.length}/${b.required_repos.length} repos`,
2614
+ ]);
2615
+ sections.push(`<div class="section">${htmlSection('Barrier Summary', htmlTable(['Barrier', 'Workstream', 'Type', 'Status', 'Satisfied'], bRows))}</div>`);
2616
+ }
2617
+
2618
+ // Barrier Transitions
2619
+ if (barrier_ledger_timeline?.length > 0) {
2620
+ const btRows = barrier_ledger_timeline.map((t, i) => [String(i + 1), `<code>${esc(t.timestamp || 'n/a')}</code>`, `<code>${esc(t.barrier_id)}</code>`, `<code>${esc(t.previous_status)}</code>`, `<code>${esc(t.new_status)}</code>`, esc(t.summary)]);
2621
+ sections.push(`<div class="section">${htmlSection('Barrier Transitions', htmlTable(['#', 'Time', 'Barrier', 'From', 'To', 'Summary'], btRows))}</div>`);
2622
+ }
2623
+
2624
+ // Coordinator Decisions
2625
+ if (decision_digest?.length > 0) {
2626
+ const ddList = decision_digest.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('');
2627
+ sections.push(`<div class="section">${htmlSection('Coordinator Decisions', `<ul>${ddList}</ul>`)}</div>`);
2628
+ }
2629
+
2630
+ // Approval Policy
2631
+ if (approval_policy_events?.length > 0) {
2632
+ let apHtml = '<ul>';
2633
+ for (const evt of approval_policy_events) {
2634
+ const transition = evt.gate_type === 'run_completion' ? 'run completion' : `${esc(evt.from_phase || '?')} &rarr; ${esc(evt.to_phase || '?')}`;
2635
+ apHtml += `<li><strong>${esc(evt.action || 'unknown')}</strong> (${esc(evt.gate_type || 'unknown')}) ${transition} at <code>${esc(evt.timestamp || 'n/a')}</code>`;
2636
+ if (evt.reason) apHtml += `<br>${esc(evt.reason)}`;
2637
+ apHtml += '</li>';
2638
+ }
2639
+ apHtml += '</ul>';
2640
+ sections.push(`<div class="section">${htmlSection('Approval Policy', apHtml)}</div>`);
2641
+ }
2642
+
2643
+ // Governance Events
2644
+ if (governance_events?.length > 0) {
2645
+ let geHtml = '<ul>';
2646
+ for (const evt of governance_events) {
2647
+ geHtml += `<li><strong>${esc(evt.type)}</strong> (<code>${esc(evt.role || '?')}</code>, <code>${esc(evt.phase || '?')}</code> phase) at <code>${esc(evt.timestamp || 'n/a')}</code>${renderHtmlGovEventDetail(evt)}</li>`;
2648
+ }
2649
+ geHtml += '</ul>';
2650
+ sections.push(`<div class="section">${htmlSection('Governance Events', geHtml)}</div>`);
2651
+ }
2652
+
2653
+ // Timeout Events
2654
+ if (timeout_events?.length > 0) {
2655
+ let teHtml = '<ul>';
2656
+ for (const evt of timeout_events) {
2657
+ const label = evt.type === 'timeout_warning' ? 'Warning' : evt.type === 'timeout_skip' ? 'Skip' : 'Escalation';
2658
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
2659
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
2660
+ teHtml += `<li><strong>${label}</strong> (<code>${esc(evt.scope || '?')}</code>) \u2014 ${elapsed}/${limit}, action: <code>${esc(evt.action || 'n/a')}</code> at <code>${esc(evt.timestamp || 'n/a')}</code></li>`;
2661
+ }
2662
+ teHtml += '</ul>';
2663
+ sections.push(`<div class="section">${htmlSection('Timeout Events', teHtml)}</div>`);
2664
+ }
2665
+
2666
+ // Recovery Report
2667
+ if (coordRecoveryReport) {
2668
+ sections.push(`<div class="section">${htmlSection('Recovery Report', htmlDl([
2669
+ ['Trigger', esc(coordRecoveryReport.trigger || 'n/a')],
2670
+ ['Impact', esc(coordRecoveryReport.impact || 'n/a')],
2671
+ ['Mitigation', esc(coordRecoveryReport.mitigation || 'n/a')],
2672
+ ['Owner', esc(coordRecoveryReport.owner || 'n/a')],
2673
+ ['Exit Condition', esc(coordRecoveryReport.exit_condition || 'n/a')],
2674
+ ]))}</div>`);
2675
+ }
2676
+
2677
+ // Repo Details
2678
+ if (repos?.length > 0) {
2679
+ let repoHtml = '';
2680
+ for (const repo of repos) {
2681
+ if (!repo.ok) {
2682
+ repoHtml += `<h3>${esc(repo.repo_id)}</h3><p>Failed export: ${esc(repo.error || 'unknown error')}, path <code>${esc(repo.path || 'unknown')}</code></p>`;
2683
+ continue;
2684
+ }
2685
+ const repoPairs = [
2686
+ ['Status', badge(repo.status || 'unknown')],
2687
+ ['Run', `<code>${esc(repo.run_id || 'none')}</code>`],
2688
+ ['Phase', `<code>${esc(repo.phase || 'unknown')}</code>`],
2689
+ ['Path', `<code>${esc(repo.path || 'unknown')}</code>`],
2690
+ ];
2691
+ if (repo.blocked_on) repoPairs.push(['Blocked on', `<code>${esc(summarizeBlockedOn(repo.blocked_on))}</code>`]);
2692
+ repoHtml += `<h3>${esc(repo.repo_id)}</h3>${htmlDl(repoPairs)}`;
2693
+
2694
+ if (repo.turns?.length > 0) {
2695
+ const turnRows = repo.turns.map((t, i) => {
2696
+ const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
2697
+ const phase = t.phase_transition ? `${esc(t.phase || '?')} &rarr; ${esc(t.phase_transition)}` : esc(t.phase || '?');
2698
+ return [String(i + 1), esc(t.role), phase, esc(t.summary || '(no summary)'), String(t.files_changed_count), cost, esc(formatTurnTimelineTime(t))];
2699
+ });
2700
+ repoHtml += htmlSection('Turn Timeline', htmlTable(['#', 'Role', 'Phase', 'Summary', 'Files', 'Cost', 'Time'], turnRows), 4);
2701
+ }
2702
+ if (repo.decisions?.length > 0) {
2703
+ repoHtml += htmlSection('Decisions', '<ul>' + repo.decisions.map((d) => `<li><strong>${esc(d.id)}</strong> (${esc(d.role || '?')}, ${esc(d.phase || '?')} phase): ${esc(d.statement)}</li>`).join('') + '</ul>', 4);
2704
+ }
2705
+ if (repo.gate_summary?.length > 0) {
2706
+ repoHtml += htmlSection('Gate Outcomes', '<ul>' + repo.gate_summary.map((g) => `<li><code>${esc(g.gate_id)}</code>: ${badge(g.status)}</li>`).join('') + '</ul>', 4);
2707
+ }
2708
+ }
2709
+ sections.push(`<div class="section">${htmlSection('Repo Details', repoHtml)}</div>`);
2710
+ }
2711
+
2712
+ return wrapHtml('AgentXchain Governance Report — Coordinator', sections.join('\n'));
2713
+ }
2714
+
2715
+ export function formatGovernanceReportHtml(report) {
2716
+ if (report.overall === 'error') {
2717
+ return wrapHtml('AgentXchain Governance Report — Error', `
2718
+ <div class="meta">
2719
+ ${htmlDl([['Input', `<code>${esc(report.input)}</code>`], ['Status', badge('error')], ['Message', esc(report.message)]])}
2720
+ </div>`);
2721
+ }
2722
+
2723
+ if (report.overall === 'fail') {
2724
+ const errorList = (report.verification?.errors || []).map((e) => `<li>${esc(e)}</li>`).join('');
2725
+ return wrapHtml('AgentXchain Governance Report — Fail', `
2726
+ <div class="meta">
2727
+ ${htmlDl([['Input', `<code>${esc(report.input)}</code>`], ['Verification', badge('fail')], ['Message', esc(report.message)]])}
2728
+ </div>
2729
+ ${errorList ? `<div class="section"><h2>Verification Errors</h2><ul>${errorList}</ul></div>` : ''}`);
2730
+ }
2731
+
2732
+ if (report.subject?.kind === 'governed_run') {
2733
+ return renderRunHtml(report);
2734
+ }
2735
+
2736
+ return renderCoordinatorHtml(report);
2737
+ }