agentxchain 2.103.0 → 2.105.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.
Files changed (66) hide show
  1. package/README.md +13 -7
  2. package/bin/agentxchain.js +16 -8
  3. package/dashboard/app.js +111 -7
  4. package/dashboard/components/blocked.js +95 -11
  5. package/dashboard/components/blockers.js +85 -86
  6. package/dashboard/components/coordinator-timeouts.js +13 -0
  7. package/dashboard/components/cross-repo.js +17 -12
  8. package/dashboard/components/gate.js +31 -11
  9. package/dashboard/components/initiative.js +173 -78
  10. package/dashboard/components/ledger.js +28 -0
  11. package/dashboard/components/live-status.js +39 -0
  12. package/dashboard/components/run-history.js +76 -1
  13. package/dashboard/components/timeline.js +5 -1
  14. package/dashboard/index.html +21 -0
  15. package/dashboard/live-observer.js +91 -0
  16. package/package.json +1 -1
  17. package/scripts/release-bump.sh +26 -3
  18. package/scripts/release-preflight.sh +82 -38
  19. package/src/commands/accept-turn.js +3 -3
  20. package/src/commands/decisions.js +98 -29
  21. package/src/commands/diff.js +27 -4
  22. package/src/commands/doctor.js +48 -16
  23. package/src/commands/generate.js +126 -1
  24. package/src/commands/history.js +21 -3
  25. package/src/commands/init.js +15 -97
  26. package/src/commands/multi.js +223 -54
  27. package/src/commands/phase.js +11 -13
  28. package/src/commands/reject-turn.js +1 -1
  29. package/src/commands/restart.js +28 -11
  30. package/src/commands/resume.js +6 -6
  31. package/src/commands/role.js +51 -14
  32. package/src/commands/run.js +5 -11
  33. package/src/commands/status.js +145 -13
  34. package/src/commands/step.js +36 -29
  35. package/src/lib/admission-control.js +14 -12
  36. package/src/lib/blocked-state.js +150 -0
  37. package/src/lib/conflict-actions.js +17 -0
  38. package/src/lib/context-section-parser.js +2 -0
  39. package/src/lib/continuity-status.js +1 -1
  40. package/src/lib/coordinator-blocker-presentation.js +127 -0
  41. package/src/lib/coordinator-event-narrative.js +43 -0
  42. package/src/lib/coordinator-gate-approval.js +98 -0
  43. package/src/lib/coordinator-gate-evaluation-presentation.js +57 -0
  44. package/src/lib/coordinator-next-actions.js +128 -0
  45. package/src/lib/coordinator-pending-gate-presentation.js +79 -0
  46. package/src/lib/coordinator-presentation-detail.js +11 -0
  47. package/src/lib/coordinator-repo-snapshots.js +53 -0
  48. package/src/lib/coordinator-repo-status-presentation.js +134 -0
  49. package/src/lib/dashboard/actions.js +105 -29
  50. package/src/lib/dashboard/bridge-server.js +7 -0
  51. package/src/lib/dashboard/coordinator-blockers.js +17 -0
  52. package/src/lib/dashboard/coordinator-repo-status.js +50 -0
  53. package/src/lib/dashboard/coordinator-timeout-status.js +34 -11
  54. package/src/lib/dashboard/state-reader.js +36 -1
  55. package/src/lib/dispatch-bundle.js +23 -0
  56. package/src/lib/export-diff.js +70 -38
  57. package/src/lib/export-verifier.js +3 -0
  58. package/src/lib/history-diff-summary.js +249 -0
  59. package/src/lib/manual-qa-fallback.js +18 -0
  60. package/src/lib/normalized-config.js +27 -22
  61. package/src/lib/planning-artifacts.js +131 -0
  62. package/src/lib/recent-event-summary.js +132 -0
  63. package/src/lib/repo-decisions.js +69 -28
  64. package/src/lib/report.js +353 -145
  65. package/src/lib/run-diff.js +4 -0
  66. package/src/lib/runtime-capabilities.js +222 -0
package/src/lib/report.js CHANGED
@@ -1,6 +1,16 @@
1
1
  import { verifyExportArtifact } from './export-verifier.js';
2
2
  import { buildDelegationSummary } from './export.js';
3
3
  import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance.js';
4
+ import { deriveGovernedRunNextActions, deriveRuntimeBlockedGuidance } from './blocked-state.js';
5
+ import {
6
+ buildRecentEventSummary,
7
+ formatRecentEventSummaryLine,
8
+ } from './recent-event-summary.js';
9
+ import {
10
+ deriveCoordinatorNextActions,
11
+ } from './coordinator-next-actions.js';
12
+ import { buildCoordinatorRepoStatusEntries } from './coordinator-repo-status-presentation.js';
13
+ import { summarizeCoordinatorEvent } from './coordinator-event-narrative.js';
4
14
 
5
15
  export const GOVERNANCE_REPORT_VERSION = '0.1';
6
16
 
@@ -77,6 +87,18 @@ function extractDashboardSessionSummary(artifact) {
77
87
  return normalizeDashboardSessionSummary(artifact.summary?.dashboard_session);
78
88
  }
79
89
 
90
+ function extractRunEventTimeline(artifact) {
91
+ const data = extractFileData(artifact, '.agentxchain/events.jsonl');
92
+ if (!Array.isArray(data)) return [];
93
+ return data.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry));
94
+ }
95
+
96
+ function formatRecentEventDetail(summary) {
97
+ if (!summary?.latest_event) return 'No event recorded';
98
+ const latest = summary.latest_event;
99
+ return `${latest.summary || latest.event_type || 'unknown_event'} at ${latest.timestamp || 'unknown'}`;
100
+ }
101
+
80
102
  function formatDashboardSessionLine(session) {
81
103
  if (!session) return null;
82
104
  switch (session.status) {
@@ -244,6 +266,35 @@ function formatTokenCount(value) {
244
266
  return value.toLocaleString('en-US');
245
267
  }
246
268
 
269
+ function formatRepoDecisionAuthority(level, role, source) {
270
+ if (typeof level !== 'number') return null;
271
+ const roleLabel = role || 'unknown';
272
+ if (source === 'human_default') return `${level} (${roleLabel}, human default)`;
273
+ if (source === 'unknown_role') return `${level} (${roleLabel}, role missing from config)`;
274
+ return `${level} (${roleLabel})`;
275
+ }
276
+
277
+ function buildRepoDecisionSummaryLines(summary) {
278
+ if (!summary) return [];
279
+ const operatorSummary = summary.operator_summary || {};
280
+ const categories = Array.isArray(operatorSummary.active_categories) && operatorSummary.active_categories.length > 0
281
+ ? operatorSummary.active_categories.join(', ')
282
+ : 'none active';
283
+ const authority = formatRepoDecisionAuthority(
284
+ operatorSummary.highest_active_authority_level,
285
+ operatorSummary.highest_active_authority_role,
286
+ operatorSummary.highest_active_authority_source,
287
+ ) || '—';
288
+ const superseding = operatorSummary.superseding_active_count || 0;
289
+ const overridden = operatorSummary.overridden_with_successor_count || 0;
290
+
291
+ return [
292
+ `Categories: ${categories}`,
293
+ `Highest authority: ${authority}`,
294
+ `Lineage: ${superseding} active superseding earlier decision${superseding === 1 ? '' : 's'} | ${overridden} overridden with recorded successor${overridden === 1 ? '' : 's'}`,
295
+ ];
296
+ }
297
+
247
298
  export function computeCostSummary(turns) {
248
299
  if (!Array.isArray(turns) || turns.length === 0) return null;
249
300
 
@@ -554,6 +605,7 @@ function extractRecoverySummary(artifact) {
554
605
  if (!blockedReason || typeof blockedReason !== 'object' || Array.isArray(blockedReason)) return null;
555
606
  const recovery = blockedReason.recovery;
556
607
  if (!recovery || typeof recovery !== 'object' || Array.isArray(recovery)) return null;
608
+ const runtimeGuidance = deriveRuntimeBlockedGuidance(artifact.state, artifact.config);
557
609
  return {
558
610
  category: blockedReason.category || null,
559
611
  typed_reason: recovery.typed_reason || null,
@@ -563,48 +615,10 @@ function extractRecoverySummary(artifact) {
563
615
  turn_retained: typeof recovery.turn_retained === 'boolean' ? recovery.turn_retained : null,
564
616
  blocked_at: blockedReason.blocked_at || null,
565
617
  turn_id: blockedReason.turn_id || null,
618
+ runtime_guidance: runtimeGuidance,
566
619
  };
567
620
  }
568
621
 
569
- function summarizeCoordinatorEvent(entry) {
570
- const type = entry?.type || 'unknown';
571
- const ts = entry?.timestamp || '';
572
- switch (type) {
573
- case 'run_initialized': {
574
- const repoCount = entry.repo_runs ? Object.keys(entry.repo_runs).length : 0;
575
- return `Coordinator run initialized with ${repoCount} repo${repoCount !== 1 ? 's' : ''}`;
576
- }
577
- case 'turn_dispatched':
578
- return `Dispatched turn to ${entry.repo_id || 'unknown'} (${entry.role || '?'}) in workstream ${entry.workstream_id || 'unknown'}`;
579
- case 'acceptance_projection': {
580
- const turnRef = entry.repo_turn_id ? ` (turn ${entry.repo_turn_id})` : '';
581
- const summaryText = entry.summary ? ` — ${entry.summary}` : '';
582
- return `Projected acceptance from ${entry.repo_id || 'unknown'}${turnRef}${summaryText}`;
583
- }
584
- case 'context_generated': {
585
- const upstreamCount = Array.isArray(entry.upstream_repo_ids) ? entry.upstream_repo_ids.length : 0;
586
- return `Generated cross-repo context for ${entry.target_repo_id || 'unknown'} from ${upstreamCount} upstream repo${upstreamCount !== 1 ? 's' : ''}`;
587
- }
588
- case 'phase_transition_requested':
589
- return `Requested phase transition: ${entry.from || '?'} → ${entry.to || '?'}`;
590
- case 'phase_transition_approved':
591
- return `Phase transition approved: ${entry.from || '?'} → ${entry.to || '?'}`;
592
- case 'run_completion_requested':
593
- return `Requested run completion (gate: ${entry.gate || 'unknown'})`;
594
- case 'run_completed':
595
- return 'Coordinator run completed';
596
- case 'state_resynced': {
597
- const resynced = Array.isArray(entry.resynced_repos) ? entry.resynced_repos.length : 0;
598
- const barrierChanges = Array.isArray(entry.barrier_changes) ? entry.barrier_changes.length : 0;
599
- return `Resynced state for ${resynced} repo${resynced !== 1 ? 's' : ''}, ${barrierChanges} barrier change${barrierChanges !== 1 ? 's' : ''}`;
600
- }
601
- case 'blocked_resolved':
602
- return `Blocked state resolved: ${entry.from || '?'} → ${entry.to || '?'}`;
603
- default:
604
- return `${type} event${ts ? ` at ${ts}` : ''}`;
605
- }
606
- }
607
-
608
622
  function extractCoordinatorTimeline(artifact) {
609
623
  const data = extractFileData(artifact, '.agentxchain/multirepo/history.jsonl');
610
624
  if (!Array.isArray(data) || data.length === 0) return [];
@@ -683,23 +697,6 @@ function normalizeCoordinatorBlockedReason(blockedReason) {
683
697
  return null;
684
698
  }
685
699
 
686
- function detectRunIdMismatches(repos, coordinatorRepoRuns) {
687
- const mismatches = [];
688
- for (const repo of repos) {
689
- if (!repo.ok || !repo.run_id) continue;
690
- const expected = coordinatorRepoRuns[repo.repo_id]?.run_id;
691
- if (!expected) continue;
692
- if (expected !== repo.run_id) {
693
- mismatches.push({
694
- repo_id: repo.repo_id,
695
- expected_run_id: expected,
696
- actual_run_id: repo.run_id,
697
- });
698
- }
699
- }
700
- return mismatches;
701
- }
702
-
703
700
  function normalizePendingGate(pendingGate) {
704
701
  if (!pendingGate || typeof pendingGate !== 'object' || Array.isArray(pendingGate)) return null;
705
702
  if (typeof pendingGate.gate !== 'string' || pendingGate.gate.length === 0) return null;
@@ -718,65 +715,6 @@ function normalizePendingGate(pendingGate) {
718
715
  return normalized;
719
716
  }
720
717
 
721
- function deriveCoordinatorNextActions({ status, blockedReason, pendingGate, repos, coordinatorRepoRuns, runIdMismatches }) {
722
- const nextActions = [];
723
-
724
- if (status === 'blocked') {
725
- nextActions.push({
726
- command: 'agentxchain multi resume',
727
- reason: `Coordinator is blocked${blockedReason ? `: ${blockedReason}` : ''}. Resume after fixing the underlying issue.`,
728
- });
729
- if (runIdMismatches && runIdMismatches.length > 0) {
730
- for (const m of runIdMismatches) {
731
- nextActions.push({
732
- command: `# repo_run_id_mismatch: ${m.repo_id}`,
733
- reason: `Repo "${m.repo_id}" run identity drifted: coordinator expects "${m.expected_run_id}" but repo has "${m.actual_run_id}". Re-initialize the child repo with the correct run or use multi resume after investigation.`,
734
- });
735
- }
736
- }
737
- if (pendingGate) {
738
- nextActions.push({
739
- command: 'agentxchain multi approve-gate',
740
- reason: `After resume, approve pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
741
- });
742
- }
743
- return nextActions;
744
- }
745
-
746
- const driftedRepos = repos
747
- .filter((repo) => repo.ok)
748
- .filter((repo) => {
749
- const coordinatorStatus = coordinatorRepoRuns?.[repo.repo_id]?.status || null;
750
- return coordinatorStatus && repo.status && coordinatorStatus !== repo.status;
751
- })
752
- .map((repo) => repo.repo_id);
753
-
754
- if (driftedRepos.length > 0) {
755
- nextActions.push({
756
- command: 'agentxchain multi resync',
757
- reason: `Coordinator state disagrees with child repo status for: ${driftedRepos.join(', ')}.`,
758
- });
759
- return nextActions;
760
- }
761
-
762
- if (pendingGate) {
763
- nextActions.push({
764
- command: 'agentxchain multi approve-gate',
765
- reason: `Coordinator is waiting on pending gate "${pendingGate.gate}" (${pendingGate.gate_type}).`,
766
- });
767
- return nextActions;
768
- }
769
-
770
- if (status === 'active' || status === 'paused') {
771
- nextActions.push({
772
- command: 'agentxchain multi step',
773
- reason: 'Coordinator has no blocked state or pending gate and can continue.',
774
- });
775
- }
776
-
777
- return nextActions;
778
- }
779
-
780
718
  function extractCoordinatorDecisionDigest(artifact) {
781
719
  const data = extractFileData(artifact, '.agentxchain/multirepo/decision-ledger.jsonl');
782
720
  if (!Array.isArray(data) || data.length === 0) return [];
@@ -879,13 +817,37 @@ function extractBarrierLedgerTimeline(artifact) {
879
817
 
880
818
  function deriveRepoStatusCounts(repoStatuses) {
881
819
  const counts = {};
882
- for (const status of Object.values(repoStatuses || {})) {
820
+ const statuses = Array.isArray(repoStatuses)
821
+ ? repoStatuses
822
+ : Object.values(repoStatuses || {});
823
+ for (const status of statuses) {
883
824
  const key = status || 'unknown';
884
825
  counts[key] = (counts[key] || 0) + 1;
885
826
  }
886
827
  return counts;
887
828
  }
888
829
 
830
+ function deriveCoordinatorStatusDrifts(repoStatusEntries) {
831
+ return repoStatusEntries
832
+ .filter((entry) => entry?.status_drift)
833
+ .map((entry) => entry.status_drift);
834
+ }
835
+
836
+ function deriveCoordinatorTerminalObservabilityNote(status, runIdMismatches, repoStatusDrifts) {
837
+ if (status !== 'completed') return null;
838
+ const driftKinds = [];
839
+ if (Array.isArray(runIdMismatches) && runIdMismatches.length > 0) {
840
+ driftKinds.push('run-id drift');
841
+ }
842
+ if (Array.isArray(repoStatusDrifts) && repoStatusDrifts.length > 0) {
843
+ driftKinds.push('status drift');
844
+ }
845
+ if (driftKinds.length === 0) return null;
846
+
847
+ const verb = driftKinds.length > 1 ? 'remain' : 'remains';
848
+ return `Child repo ${driftKinds.join(' and ')} ${verb} visible for audit, but this coordinator is already completed, so no recovery command is emitted.`;
849
+ }
850
+
889
851
  export function extractWorkflowKitArtifacts(artifact) {
890
852
  const config = artifact.config;
891
853
  if (!config || typeof config !== 'object' || !config.workflow_kit) return null;
@@ -961,10 +923,12 @@ function buildRunSubject(artifact) {
961
923
  const gateSummary = extractGateSummary(artifact);
962
924
  const intakeLinks = extractIntakeLinks(artifact);
963
925
  const recoverySummary = extractRecoverySummary(artifact);
926
+ const nextActions = deriveGovernedRunNextActions(artifact.state, artifact.config);
964
927
  const continuity = extractContinuityMetadata(artifact);
965
928
  const governanceEvents = extractGovernanceEventDigest(artifact);
966
929
  const delegationSummary = extractDelegationSummary(artifact);
967
930
  const dashboardSession = extractDashboardSessionSummary(artifact);
931
+ const recentEventSummary = buildRecentEventSummary(extractRunEventTimeline(artifact));
968
932
 
969
933
  return {
970
934
  kind: 'governed_run',
@@ -992,6 +956,7 @@ function buildRunSubject(artifact) {
992
956
  budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
993
957
  cost_summary: computeCostSummary(turns),
994
958
  dashboard_session: dashboardSession,
959
+ recent_event_summary: recentEventSummary,
995
960
  created_at: timing.created_at,
996
961
  completed_at: timing.completed_at,
997
962
  duration_seconds: timing.duration_seconds,
@@ -1006,6 +971,7 @@ function buildRunSubject(artifact) {
1006
971
  gate_summary: gateSummary,
1007
972
  intake_links: intakeLinks,
1008
973
  recovery_summary: recoverySummary,
974
+ next_actions: nextActions,
1009
975
  continuity,
1010
976
  workflow_kit_artifacts: extractWorkflowKitArtifacts(artifact),
1011
977
  repo_decisions: artifact.summary?.repo_decisions || null,
@@ -1055,8 +1021,8 @@ function extractRecoveryReportSummary(artifact) {
1055
1021
 
1056
1022
  function buildCoordinatorSubject(artifact) {
1057
1023
  const coordinatorState = extractFileData(artifact, '.agentxchain/multirepo/state.json') || {};
1058
- const repoStatuses = artifact.summary?.repo_run_statuses || {};
1059
- const repoStatusCounts = deriveRepoStatusCounts(repoStatuses);
1024
+ const coordinatorStatus = coordinatorState?.status || artifact.summary?.status || null;
1025
+ const coordinatorPhase = coordinatorState?.phase || artifact.summary?.phase || null;
1060
1026
  const repos = Object.entries(artifact.repos || {})
1061
1027
  .sort(([left], [right]) => left.localeCompare(right, 'en'))
1062
1028
  .map(([repoId, repoEntry]) => {
@@ -1098,15 +1064,31 @@ function buildCoordinatorSubject(artifact) {
1098
1064
  const timing = computeCoordinatorTiming(artifact, coordinatorTimeline);
1099
1065
  const blockedReason = normalizeCoordinatorBlockedReason(coordinatorState.blocked_reason);
1100
1066
  const pendingGate = normalizePendingGate(coordinatorState.pending_gate);
1101
- const runIdMismatches = detectRunIdMismatches(repos, coordinatorState.repo_runs || {});
1067
+ const repoStatusEntries = buildCoordinatorRepoStatusEntries({
1068
+ config: artifact.config,
1069
+ coordinatorRepoRuns: coordinatorState.repo_runs || {},
1070
+ repoSnapshots: repos,
1071
+ });
1072
+ const repoStatusCounts = deriveRepoStatusCounts(repoStatusEntries.map((entry) => entry.status));
1073
+ const runIdMismatches = repoStatusEntries
1074
+ .filter((entry) => entry?.run_id_mismatch)
1075
+ .map((entry) => entry.run_id_mismatch);
1076
+ const repoStatusDrifts = deriveCoordinatorStatusDrifts(repoStatusEntries);
1077
+ const recentCoordinatorEvents = buildRecentEventSummary(coordinatorTimeline);
1078
+ const recentChildRepoEvents = buildRecentEventSummary(extractAggregatedEventTimeline(artifact));
1102
1079
  const nextActions = deriveCoordinatorNextActions({
1103
- status: artifact.summary?.status || null,
1080
+ status: coordinatorStatus,
1104
1081
  blockedReason,
1105
1082
  pendingGate,
1106
1083
  repos,
1107
1084
  coordinatorRepoRuns: coordinatorState.repo_runs || {},
1108
1085
  runIdMismatches,
1109
1086
  });
1087
+ const terminalObservabilityNote = deriveCoordinatorTerminalObservabilityNote(
1088
+ coordinatorStatus,
1089
+ runIdMismatches,
1090
+ repoStatusDrifts,
1091
+ );
1110
1092
 
1111
1093
  return {
1112
1094
  kind: 'coordinator_workspace',
@@ -1118,12 +1100,16 @@ function buildCoordinatorSubject(artifact) {
1118
1100
  workstream_count: artifact.coordinator?.workstream_count || 0,
1119
1101
  },
1120
1102
  run: {
1121
- super_run_id: artifact.summary?.super_run_id || null,
1122
- status: artifact.summary?.status || null,
1123
- phase: artifact.summary?.phase || null,
1103
+ super_run_id: coordinatorState?.super_run_id || artifact.summary?.super_run_id || null,
1104
+ status: coordinatorStatus,
1105
+ phase: coordinatorPhase,
1124
1106
  blocked_reason: blockedReason,
1125
1107
  pending_gate: pendingGate,
1126
1108
  run_id_mismatches: runIdMismatches,
1109
+ repo_status_drifts: repoStatusDrifts,
1110
+ terminal_observability_note: terminalObservabilityNote,
1111
+ recent_coordinator_events: recentCoordinatorEvents,
1112
+ recent_child_repo_events: recentChildRepoEvents,
1127
1113
  next_actions: nextActions,
1128
1114
  created_at: timing.created_at,
1129
1115
  completed_at: timing.completed_at,
@@ -1264,6 +1250,10 @@ export function formatGovernanceReportText(report) {
1264
1250
  if (run.dashboard_session) {
1265
1251
  lines.push(`Dashboard session: ${formatDashboardSessionLine(run.dashboard_session)}`);
1266
1252
  }
1253
+ if (run.recent_event_summary) {
1254
+ lines.push(`Recent events: ${formatRecentEventSummaryLine(run.recent_event_summary)}`);
1255
+ lines.push(`Latest event: ${formatRecentEventDetail(run.recent_event_summary)}`);
1256
+ }
1267
1257
 
1268
1258
  lines.push(
1269
1259
  `History entries: ${artifacts.history_entries}`,
@@ -1316,9 +1306,12 @@ export function formatGovernanceReportText(report) {
1316
1306
  }
1317
1307
  }
1318
1308
 
1319
- if (run.repo_decisions?.active?.length > 0) {
1309
+ if (run.repo_decisions) {
1320
1310
  lines.push('', 'Repo Decisions:');
1321
1311
  lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
1312
+ for (const summaryLine of buildRepoDecisionSummaryLines(run.repo_decisions)) {
1313
+ lines.push(` ${summaryLine}`);
1314
+ }
1322
1315
  for (const d of run.repo_decisions.active) {
1323
1316
  const supersedes = d.overrides ? ` | supersedes ${d.overrides}` : '';
1324
1317
  const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
@@ -1425,6 +1418,20 @@ export function formatGovernanceReportText(report) {
1425
1418
  lines.push(` Action: ${run.recovery_summary.recovery_action || 'n/a'}`);
1426
1419
  lines.push(` Detail: ${run.recovery_summary.detail || 'n/a'}`);
1427
1420
  lines.push(` Turn retained: ${run.recovery_summary.turn_retained == null ? 'n/a' : yesNo(run.recovery_summary.turn_retained)}`);
1421
+ if (Array.isArray(run.recovery_summary.runtime_guidance) && run.recovery_summary.runtime_guidance.length > 0) {
1422
+ lines.push(' Runtime guidance:');
1423
+ for (const entry of run.recovery_summary.runtime_guidance) {
1424
+ lines.push(` - ${entry.code} | ${entry.command} | ${entry.reason}`);
1425
+ }
1426
+ }
1427
+ }
1428
+
1429
+ if (run.next_actions && run.next_actions.length > 0) {
1430
+ lines.push('', 'Next Actions:');
1431
+ for (let i = 0; i < run.next_actions.length; i++) {
1432
+ const action = run.next_actions[i];
1433
+ lines.push(` ${i + 1}. ${action.command} | ${action.reason}`);
1434
+ }
1428
1435
  }
1429
1436
 
1430
1437
  if (run.continuity) {
@@ -1486,6 +1493,12 @@ export function formatGovernanceReportText(report) {
1486
1493
  lines.push(` - ${m.repo_id}: expected ${m.expected_run_id}, actual ${m.actual_run_id}`);
1487
1494
  }
1488
1495
  }
1496
+ if (run.repo_status_drifts && run.repo_status_drifts.length > 0) {
1497
+ lines.push(`Repo status drift: ${run.repo_status_drifts.length}`);
1498
+ for (const drift of run.repo_status_drifts) {
1499
+ lines.push(` - ${drift.repo_id}: coordinator ${drift.coordinator_status || 'unknown'}, repo ${drift.repo_status || 'unknown'}`);
1500
+ }
1501
+ }
1489
1502
 
1490
1503
  lines.push(
1491
1504
  `Started: ${run.created_at || 'n/a'}`,
@@ -1506,6 +1519,17 @@ export function formatGovernanceReportText(report) {
1506
1519
  if (run.pending_gate) {
1507
1520
  lines.push(`Pending gate: ${run.pending_gate.gate} (${run.pending_gate.gate_type})`);
1508
1521
  }
1522
+ if (run.recent_coordinator_events) {
1523
+ lines.push(`Recent coordinator events: ${formatRecentEventSummaryLine(run.recent_coordinator_events)}`);
1524
+ lines.push(`Latest coordinator event: ${formatRecentEventDetail(run.recent_coordinator_events)}`);
1525
+ }
1526
+ if (run.recent_child_repo_events) {
1527
+ lines.push(`Recent child repo events: ${formatRecentEventSummaryLine(run.recent_child_repo_events)}`);
1528
+ lines.push(`Latest child repo event: ${formatRecentEventDetail(run.recent_child_repo_events)}`);
1529
+ }
1530
+ if (run.terminal_observability_note) {
1531
+ lines.push(`Terminal drift note: ${run.terminal_observability_note}`);
1532
+ }
1509
1533
 
1510
1534
  if (run.next_actions && run.next_actions.length > 0) {
1511
1535
  lines.push('', 'Next Actions:');
@@ -1776,6 +1800,10 @@ export function formatGovernanceReportMarkdown(report) {
1776
1800
  if (run.dashboard_session) {
1777
1801
  lines.push(`- Dashboard session: \`${formatDashboardSessionLine(run.dashboard_session)}\``);
1778
1802
  }
1803
+ if (run.recent_event_summary) {
1804
+ lines.push(`- Recent events: \`${formatRecentEventSummaryLine(run.recent_event_summary)}\``);
1805
+ lines.push(`- Latest event: ${formatRecentEventDetail(run.recent_event_summary)}`);
1806
+ }
1779
1807
 
1780
1808
  lines.push(
1781
1809
  `- History entries: ${artifacts.history_entries}`,
@@ -1828,14 +1856,19 @@ export function formatGovernanceReportMarkdown(report) {
1828
1856
  }
1829
1857
  }
1830
1858
 
1831
- if (run.repo_decisions?.active?.length > 0) {
1859
+ if (run.repo_decisions) {
1832
1860
  lines.push('', '## Repo Decisions', '');
1833
1861
  lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
1834
- lines.push('| ID | Category | Statement | Role | Authority | Run | Supersedes |', '|----|----------|-----------|------|-----------|-----|------------|');
1835
- for (const d of run.repo_decisions.active) {
1836
- const stmt = (d.statement || '').replace(/\|/g, '\\|');
1837
- const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1838
- lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | ${authority} | \`${(d.run_id || '').slice(0, 12)}\` | ${d.overrides || ''} |`);
1862
+ for (const summaryLine of buildRepoDecisionSummaryLines(run.repo_decisions)) {
1863
+ lines.push(`${summaryLine}`, '');
1864
+ }
1865
+ if (run.repo_decisions.active.length > 0) {
1866
+ lines.push('| ID | Category | Statement | Role | Authority | Run | Supersedes |', '|----|----------|-----------|------|-----------|-----|------------|');
1867
+ for (const d of run.repo_decisions.active) {
1868
+ const stmt = (d.statement || '').replace(/\|/g, '\\|');
1869
+ const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
1870
+ lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | ${authority} | \`${(d.run_id || '').slice(0, 12)}\` | ${d.overrides || '—'} |`);
1871
+ }
1839
1872
  }
1840
1873
  if (run.repo_decisions.overridden?.length > 0) {
1841
1874
  lines.push('', 'Overridden decisions:', '');
@@ -1944,6 +1977,20 @@ export function formatGovernanceReportMarkdown(report) {
1944
1977
  lines.push(`- Action: \`${run.recovery_summary.recovery_action || 'n/a'}\``);
1945
1978
  lines.push(`- Detail: ${run.recovery_summary.detail || 'n/a'}`);
1946
1979
  lines.push(`- Turn retained: \`${run.recovery_summary.turn_retained == null ? 'n/a' : yesNo(run.recovery_summary.turn_retained)}\``);
1980
+ if (Array.isArray(run.recovery_summary.runtime_guidance) && run.recovery_summary.runtime_guidance.length > 0) {
1981
+ lines.push('- Runtime guidance:');
1982
+ for (const entry of run.recovery_summary.runtime_guidance) {
1983
+ lines.push(` - \`${entry.code}\` — \`${entry.command}\`: ${entry.reason}`);
1984
+ }
1985
+ }
1986
+ }
1987
+
1988
+ if (run.next_actions && run.next_actions.length > 0) {
1989
+ lines.push('', '## Next Actions', '');
1990
+ for (let i = 0; i < run.next_actions.length; i++) {
1991
+ const action = run.next_actions[i];
1992
+ lines.push(`${i + 1}. \`${action.command}\`: ${action.reason}`);
1993
+ }
1947
1994
  }
1948
1995
 
1949
1996
  if (run.continuity) {
@@ -2008,6 +2055,12 @@ export function formatGovernanceReportMarkdown(report) {
2008
2055
  mdLines.push(` - \`${m.repo_id}\`: expected \`${m.expected_run_id}\`, actual \`${m.actual_run_id}\``);
2009
2056
  }
2010
2057
  }
2058
+ if (run.repo_status_drifts && run.repo_status_drifts.length > 0) {
2059
+ mdLines.push(`- **Repo status drift: ${run.repo_status_drifts.length}**`);
2060
+ for (const drift of run.repo_status_drifts) {
2061
+ mdLines.push(` - \`${drift.repo_id}\`: coordinator \`${drift.coordinator_status || 'unknown'}\`, repo \`${drift.repo_status || 'unknown'}\``);
2062
+ }
2063
+ }
2011
2064
 
2012
2065
  mdLines.push(
2013
2066
  `- Started: \`${run.created_at || 'n/a'}\``,
@@ -2028,6 +2081,17 @@ export function formatGovernanceReportMarkdown(report) {
2028
2081
  if (run.pending_gate) {
2029
2082
  mdLines.push(`- Pending gate: \`${run.pending_gate.gate}\` (\`${run.pending_gate.gate_type}\`)`);
2030
2083
  }
2084
+ if (run.recent_coordinator_events) {
2085
+ mdLines.push(`- Recent coordinator events: \`${formatRecentEventSummaryLine(run.recent_coordinator_events)}\``);
2086
+ mdLines.push(`- Latest coordinator event: ${formatRecentEventDetail(run.recent_coordinator_events)}`);
2087
+ }
2088
+ if (run.recent_child_repo_events) {
2089
+ mdLines.push(`- Recent child repo events: \`${formatRecentEventSummaryLine(run.recent_child_repo_events)}\``);
2090
+ mdLines.push(`- Latest child repo event: ${formatRecentEventDetail(run.recent_child_repo_events)}`);
2091
+ }
2092
+ if (run.terminal_observability_note) {
2093
+ mdLines.push(`- Terminal drift note: ${run.terminal_observability_note}`);
2094
+ }
2031
2095
 
2032
2096
  if (run.next_actions && run.next_actions.length > 0) {
2033
2097
  mdLines.push('', '## Next Actions', '');
@@ -2403,6 +2467,10 @@ function renderRunHtml(report) {
2403
2467
  if (summarizeRunProvenance(run.provenance)) metaPairs.push(['Provenance', `<code>${esc(summarizeRunProvenance(run.provenance))}</code>`]);
2404
2468
  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')})`]);
2405
2469
  if (run.dashboard_session) metaPairs.push(['Dashboard', `<code>${esc(formatDashboardSessionLine(run.dashboard_session))}</code>`]);
2470
+ if (run.recent_event_summary) {
2471
+ metaPairs.push(['Recent events', esc(formatRecentEventSummaryLine(run.recent_event_summary))]);
2472
+ metaPairs.push(['Latest event', esc(formatRecentEventDetail(run.recent_event_summary))]);
2473
+ }
2406
2474
 
2407
2475
  metaPairs.push(
2408
2476
  ['History entries', String(artifacts.history_entries)],
@@ -2466,20 +2534,25 @@ function renderRunHtml(report) {
2466
2534
  }
2467
2535
 
2468
2536
  // Repo Decisions
2469
- if (run.repo_decisions?.active?.length > 0) {
2537
+ if (run.repo_decisions) {
2470
2538
  let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
2471
- rdHtml += htmlTable(
2472
- ['ID', 'Category', 'Statement', 'Role', 'Authority', 'Run', 'Supersedes'],
2473
- run.repo_decisions.active.map((d) => [
2474
- esc(d.id),
2475
- esc(d.category),
2476
- esc(d.statement || ''),
2477
- esc(d.role || '\u2014'),
2478
- esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
2479
- `<code>${esc((d.run_id || '').slice(0, 12))}</code>`,
2480
- esc(d.overrides || '\u2014'),
2481
- ]),
2482
- );
2539
+ rdHtml += buildRepoDecisionSummaryLines(run.repo_decisions)
2540
+ .map((line) => `<p>${esc(line)}</p>`)
2541
+ .join('');
2542
+ if (run.repo_decisions.active.length > 0) {
2543
+ rdHtml += htmlTable(
2544
+ ['ID', 'Category', 'Statement', 'Role', 'Authority', 'Run', 'Supersedes'],
2545
+ run.repo_decisions.active.map((d) => [
2546
+ esc(d.id),
2547
+ esc(d.category),
2548
+ esc(d.statement || ''),
2549
+ esc(d.role || '\u2014'),
2550
+ esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
2551
+ `<code>${esc((d.run_id || '').slice(0, 12))}</code>`,
2552
+ esc(d.overrides || '\u2014'),
2553
+ ]),
2554
+ );
2555
+ }
2483
2556
  if (run.repo_decisions.overridden?.length > 0) {
2484
2557
  rdHtml += htmlTable(
2485
2558
  ['ID', 'Statement', 'Authority', 'Overridden By'],
@@ -2595,14 +2668,28 @@ function renderRunHtml(report) {
2595
2668
  // Recovery
2596
2669
  if (run.recovery_summary) {
2597
2670
  const rs = run.recovery_summary;
2598
- sections.push(`<div class="section">${htmlSection('Recovery', htmlDl([
2671
+ let recoveryHtml = htmlDl([
2599
2672
  ['Category', `<code>${esc(rs.category || 'unknown')}</code>`],
2600
2673
  ['Typed reason', `<code>${esc(rs.typed_reason || 'unknown')}</code>`],
2601
2674
  ['Owner', `<code>${esc(rs.owner || 'unknown')}</code>`],
2602
2675
  ['Action', `<code>${esc(rs.recovery_action || 'n/a')}</code>`],
2603
2676
  ['Detail', esc(rs.detail || 'n/a')],
2604
2677
  ['Turn retained', rs.turn_retained == null ? 'n/a' : (rs.turn_retained ? 'yes' : 'no')],
2605
- ]))}</div>`);
2678
+ ]);
2679
+ if (Array.isArray(rs.runtime_guidance) && rs.runtime_guidance.length > 0) {
2680
+ const items = '<ul>' + rs.runtime_guidance.map((entry) =>
2681
+ `<li><code>${esc(entry.code)}</code> — <code>${esc(entry.command)}</code>: ${esc(entry.reason)}</li>`
2682
+ ).join('') + '</ul>';
2683
+ recoveryHtml += htmlSection('Runtime Guidance', items);
2684
+ }
2685
+ sections.push(`<div class="section">${htmlSection('Recovery', recoveryHtml)}</div>`);
2686
+ }
2687
+
2688
+ if (run.next_actions?.length > 0) {
2689
+ const nextHtml = '<ol>' + run.next_actions.map((action) =>
2690
+ `<li><code>${esc(action.command)}</code>: ${esc(action.reason)}</li>`
2691
+ ).join('') + '</ol>';
2692
+ sections.push(`<div class="section">${htmlSection('Next Actions', nextHtml)}</div>`);
2606
2693
  }
2607
2694
 
2608
2695
  // Continuity
@@ -2659,6 +2746,9 @@ function renderCoordinatorHtml(report) {
2659
2746
  if (run.run_id_mismatches?.length > 0) {
2660
2747
  metaPairs.push(['Run ID mismatches', `<strong class="warn">${run.run_id_mismatches.length}</strong>`]);
2661
2748
  }
2749
+ if (run.repo_status_drifts?.length > 0) {
2750
+ metaPairs.push(['Repo status drift', `<strong class="warn">${run.repo_status_drifts.length}</strong>`]);
2751
+ }
2662
2752
 
2663
2753
  metaPairs.push(
2664
2754
  ['Started', `<code>${esc(run.created_at || 'n/a')}</code>`],
@@ -2672,6 +2762,17 @@ function renderCoordinatorHtml(report) {
2672
2762
  if (run.completed_at) metaPairs.push(['Completed', `<code>${esc(run.completed_at)}</code>`]);
2673
2763
  if (run.duration_seconds != null) metaPairs.push(['Duration', `<code>${run.duration_seconds}s</code>`]);
2674
2764
  if (run.pending_gate) metaPairs.push(['Pending gate', `<code>${esc(run.pending_gate.gate)}</code> (<code>${esc(run.pending_gate.gate_type)}</code>)`]);
2765
+ if (run.recent_coordinator_events) {
2766
+ metaPairs.push(['Recent coordinator events', esc(formatRecentEventSummaryLine(run.recent_coordinator_events))]);
2767
+ metaPairs.push(['Latest coordinator event', esc(formatRecentEventDetail(run.recent_coordinator_events))]);
2768
+ }
2769
+ if (run.recent_child_repo_events) {
2770
+ metaPairs.push(['Recent child repo events', esc(formatRecentEventSummaryLine(run.recent_child_repo_events))]);
2771
+ metaPairs.push(['Latest child repo event', esc(formatRecentEventDetail(run.recent_child_repo_events))]);
2772
+ }
2773
+ if (run.terminal_observability_note) {
2774
+ metaPairs.push(['Terminal drift note', esc(run.terminal_observability_note)]);
2775
+ }
2675
2776
 
2676
2777
  sections.push(`<div class="meta">${htmlDl(metaPairs)}</div>`);
2677
2778
 
@@ -2681,6 +2782,17 @@ function renderCoordinatorHtml(report) {
2681
2782
  sections.push(`<div class="section">${htmlSection('Next Actions', naHtml)}</div>`);
2682
2783
  }
2683
2784
 
2785
+ if (run.repo_status_drifts?.length > 0) {
2786
+ const driftRows = run.repo_status_drifts.map((drift) => [
2787
+ `<code>${esc(drift.repo_id)}</code>`,
2788
+ `<code>${esc(drift.coordinator_status || 'unknown')}</code>`,
2789
+ `<code>${esc(drift.repo_status || 'unknown')}</code>`,
2790
+ ]);
2791
+ sections.push(
2792
+ `<div class="section">${htmlSection('Repo Status Drift', htmlTable(['Repo', 'Coordinator', 'Repo Authority'], driftRows))}</div>`,
2793
+ );
2794
+ }
2795
+
2684
2796
  // Coordinator Timeline
2685
2797
  if (coordinator_timeline?.length > 0) {
2686
2798
  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)]);
@@ -2805,6 +2917,102 @@ function renderCoordinatorHtml(report) {
2805
2917
  if (repo.gate_summary?.length > 0) {
2806
2918
  repoHtml += htmlSection('Gate Outcomes', '<ul>' + repo.gate_summary.map((g) => `<li><code>${esc(g.gate_id)}</code>: ${badge(g.status)}</li>`).join('') + '</ul>', 4);
2807
2919
  }
2920
+ if (repo.gate_failures?.length > 0) {
2921
+ let gateFailureHtml = '<ul>';
2922
+ for (const failure of repo.gate_failures) {
2923
+ const request = failure.gate_type === 'run_completion'
2924
+ ? 'run completion'
2925
+ : `${esc(failure.from_phase || '?')} &rarr; ${esc(failure.to_phase || '?')}`;
2926
+ gateFailureHtml += `<li><code>${esc(failure.gate_id || 'unknown')}</code> (<code>${esc(failure.gate_type || 'unknown')}</code>) at <code>${esc(failure.failed_at || 'n/a')}</code> via ${failure.queued_request ? 'queued drain' : 'direct'} request: ${request}`;
2927
+ if (failure.reasons?.length > 0) {
2928
+ gateFailureHtml += '<ul>' + failure.reasons.map((reason) => `<li>${esc(reason)}</li>`).join('') + '</ul>';
2929
+ }
2930
+ gateFailureHtml += '</li>';
2931
+ }
2932
+ gateFailureHtml += '</ul>';
2933
+ repoHtml += htmlSection('Gate Failures', gateFailureHtml, 4);
2934
+ }
2935
+ if (repo.approval_policy_events?.length > 0) {
2936
+ let approvalHtml = '<ul>';
2937
+ for (const evt of repo.approval_policy_events) {
2938
+ const transition = evt.gate_type === 'run_completion'
2939
+ ? 'run completion'
2940
+ : `${esc(evt.from_phase || '?')} &rarr; ${esc(evt.to_phase || '?')}`;
2941
+ const rule = evt.matched_rule
2942
+ ? ` — rule: <code>${esc(typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule)}</code>`
2943
+ : '';
2944
+ approvalHtml += `<li><strong>${esc(evt.action || 'unknown')}</strong> (${esc(evt.gate_type || 'unknown')}) ${transition}${rule} at <code>${esc(evt.timestamp || 'n/a')}</code>`;
2945
+ if (evt.reason) approvalHtml += `<br>${esc(evt.reason)}`;
2946
+ approvalHtml += '</li>';
2947
+ }
2948
+ approvalHtml += '</ul>';
2949
+ repoHtml += htmlSection('Approval Policy', approvalHtml, 4);
2950
+ }
2951
+ if (repo.governance_events?.length > 0) {
2952
+ let governanceHtml = '<ul>';
2953
+ for (const evt of repo.governance_events) {
2954
+ governanceHtml += `<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>`;
2955
+ }
2956
+ governanceHtml += '</ul>';
2957
+ repoHtml += htmlSection('Governance Events', governanceHtml, 4);
2958
+ }
2959
+ if (repo.timeout_events?.length > 0) {
2960
+ let timeoutHtml = '<ul>';
2961
+ for (const evt of repo.timeout_events) {
2962
+ const label = evt.type === 'timeout_warning' ? 'Warning'
2963
+ : evt.type === 'timeout_skip' ? 'Skip'
2964
+ : evt.type === 'timeout_skip_failed' ? 'Skip Failed'
2965
+ : 'Escalation';
2966
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
2967
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
2968
+ const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
2969
+ timeoutHtml += `<li><strong>${label}</strong> (<code>${esc(evt.scope || '?')}</code> scope) — ${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>`;
2970
+ }
2971
+ timeoutHtml += '</ul>';
2972
+ repoHtml += htmlSection('Timeout Events', timeoutHtml, 4);
2973
+ }
2974
+ if (repo.hook_summary) {
2975
+ const eventList = Object.entries(repo.hook_summary.events)
2976
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))
2977
+ .map(([event, count]) => `${esc(event)}(${count})`)
2978
+ .join(', ');
2979
+ const hookHtml = htmlDl([
2980
+ ['Total executions', String(repo.hook_summary.total)],
2981
+ ['Blocked', String(repo.hook_summary.blocked)],
2982
+ ...(eventList ? [['Events', eventList]] : []),
2983
+ ]);
2984
+ repoHtml += htmlSection('Hook Activity', hookHtml, 4);
2985
+ }
2986
+ if (repo.recovery_summary) {
2987
+ const recovery = repo.recovery_summary;
2988
+ let recoveryHtml = htmlDl([
2989
+ ['Category', `<code>${esc(recovery.category || 'unknown')}</code>`],
2990
+ ['Typed reason', `<code>${esc(recovery.typed_reason || 'unknown')}</code>`],
2991
+ ['Owner', `<code>${esc(recovery.owner || 'unknown')}</code>`],
2992
+ ['Action', `<code>${esc(recovery.recovery_action || 'n/a')}</code>`],
2993
+ ['Detail', esc(recovery.detail || 'n/a')],
2994
+ ['Turn retained', recovery.turn_retained == null ? 'n/a' : (recovery.turn_retained ? 'yes' : 'no')],
2995
+ ]);
2996
+ if (Array.isArray(recovery.runtime_guidance) && recovery.runtime_guidance.length > 0) {
2997
+ recoveryHtml += htmlSection('Runtime Guidance', '<ul>' + recovery.runtime_guidance.map((entry) =>
2998
+ `<li><code>${esc(entry.code)}</code> — <code>${esc(entry.command)}</code>: ${esc(entry.reason)}</li>`
2999
+ ).join('') + '</ul>', 5);
3000
+ }
3001
+ repoHtml += htmlSection('Recovery', recoveryHtml, 4);
3002
+ }
3003
+ if (repo.continuity) {
3004
+ const continuityPairs = [
3005
+ ['Session', `<code>${esc(repo.continuity.session_id || 'unknown')}</code>`],
3006
+ ['Checkpoint', `<code>${esc(repo.continuity.checkpoint_reason || 'unknown')}</code> at <code>${esc(repo.continuity.last_checkpoint_at || 'n/a')}</code>`],
3007
+ ['Last turn', `<code>${esc(repo.continuity.last_turn_id || 'none')}</code>`],
3008
+ ['Last role', `<code>${esc(repo.continuity.last_role || 'unknown')}</code>`],
3009
+ ['Last phase', `<code>${esc(repo.continuity.last_phase || 'unknown')}</code>`],
3010
+ ];
3011
+ if (repo.continuity.stale_checkpoint) {
3012
+ continuityPairs.push(['Warning', `<span class="warn">checkpoint tracks run <code>${esc(repo.continuity.run_id)}</code>, but repo export tracks <code>${esc(repo.run_id)}</code></span>`]);
3013
+ }
3014
+ repoHtml += htmlSection('Continuity', htmlDl(continuityPairs), 4);
3015
+ }
2808
3016
  }
2809
3017
  sections.push(`<div class="section">${htmlSection('Repo Details', repoHtml)}</div>`);
2810
3018
  }