agentxchain 2.46.0 → 2.47.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
@@ -1,4 +1,5 @@
1
1
  import { verifyExportArtifact } from './export-verifier.js';
2
+ import { normalizeRunProvenance, summarizeRunProvenance } from './run-provenance.js';
2
3
 
3
4
  export const GOVERNANCE_REPORT_VERSION = '0.1';
4
5
 
@@ -78,6 +79,10 @@ function extractHistoryTimeline(artifact) {
78
79
  phase: e.phase || null,
79
80
  phase_transition: e.phase_transition_request || null,
80
81
  files_changed_count: Array.isArray(e.files_changed) ? e.files_changed.length : 0,
82
+ concurrent_with: Array.isArray(e.concurrent_with) && e.concurrent_with.length > 0 ? e.concurrent_with : undefined,
83
+ sibling_attributed_files: Array.isArray(e.observed_artifact?.attributed_to_concurrent_siblings) && e.observed_artifact.attributed_to_concurrent_siblings.length > 0
84
+ ? e.observed_artifact.attributed_to_concurrent_siblings
85
+ : undefined,
81
86
  decisions: Array.isArray(e.decisions) ? e.decisions.map((d) => d?.id || d).filter(Boolean) : [],
82
87
  objections: Array.isArray(e.objections) ? e.objections.map((o) => o?.id || o).filter(Boolean) : [],
83
88
  cost_usd: typeof e.cost?.total_usd === 'number' ? e.cost.total_usd : null,
@@ -99,6 +104,61 @@ function extractDecisionDigest(artifact) {
99
104
  }));
100
105
  }
101
106
 
107
+ function extractApprovalPolicyDigest(artifact) {
108
+ const data = extractFileData(artifact, '.agentxchain/decision-ledger.jsonl');
109
+ if (!Array.isArray(data) || data.length === 0) return [];
110
+ return data
111
+ .filter((d) => d?.type === 'approval_policy')
112
+ .map((d) => ({
113
+ gate_type: d.gate_type || null,
114
+ action: d.action || null,
115
+ matched_rule: d.matched_rule || null,
116
+ from_phase: d.from_phase || null,
117
+ to_phase: d.to_phase || null,
118
+ reason: d.reason || '',
119
+ gate_id: d.gate_id || null,
120
+ timestamp: d.timestamp || null,
121
+ }));
122
+ }
123
+
124
+ function extractGateFailureDigest(artifact) {
125
+ const data = extractFileData(artifact, '.agentxchain/decision-ledger.jsonl');
126
+ if (!Array.isArray(data) || data.length === 0) return [];
127
+ return data
128
+ .filter((d) => d?.type === 'gate_failure')
129
+ .map((d) => ({
130
+ gate_type: d.gate_type || null,
131
+ gate_id: d.gate_id || null,
132
+ phase: d.phase || null,
133
+ from_phase: d.from_phase || null,
134
+ to_phase: d.to_phase || null,
135
+ requested_by_turn: d.requested_by_turn || null,
136
+ failed_at: d.failed_at || null,
137
+ queued_request: d.queued_request === true,
138
+ reasons: Array.isArray(d.reasons) ? d.reasons : [],
139
+ missing_files: Array.isArray(d.missing_files) ? d.missing_files : [],
140
+ missing_verification: d.missing_verification === true,
141
+ }));
142
+ }
143
+
144
+ function extractTimeoutEventDigest(artifact, relPath = '.agentxchain/decision-ledger.jsonl') {
145
+ const data = extractFileData(artifact, relPath);
146
+ if (!Array.isArray(data) || data.length === 0) return [];
147
+ return data
148
+ .filter((d) => typeof d?.type === 'string' && d.type.startsWith('timeout'))
149
+ .map((d) => ({
150
+ type: d.type,
151
+ scope: d.scope || null,
152
+ phase: d.phase || null,
153
+ turn_id: d.turn_id || null,
154
+ limit_minutes: typeof d.limit_minutes === 'number' ? d.limit_minutes : null,
155
+ elapsed_minutes: typeof d.elapsed_minutes === 'number' ? d.elapsed_minutes : null,
156
+ exceeded_by_minutes: typeof d.exceeded_by_minutes === 'number' ? d.exceeded_by_minutes : null,
157
+ action: d.action || null,
158
+ timestamp: d.timestamp || null,
159
+ }));
160
+ }
161
+
102
162
  function extractHookSummary(artifact) {
103
163
  const data = extractFileData(artifact, '.agentxchain/hook-audit.jsonl');
104
164
  if (!Array.isArray(data) || data.length === 0) return null;
@@ -408,6 +468,23 @@ function extractCoordinatorDecisionDigest(artifact) {
408
468
  }));
409
469
  }
410
470
 
471
+ function extractCoordinatorApprovalPolicyDigest(artifact) {
472
+ const data = extractFileData(artifact, '.agentxchain/multirepo/decision-ledger.jsonl');
473
+ if (!Array.isArray(data) || data.length === 0) return [];
474
+ return data
475
+ .filter((d) => d?.type === 'approval_policy')
476
+ .map((d) => ({
477
+ gate_type: d.gate_type || null,
478
+ action: d.action || null,
479
+ matched_rule: d.matched_rule || null,
480
+ from_phase: d.from_phase || null,
481
+ to_phase: d.to_phase || null,
482
+ reason: d.reason || '',
483
+ gate_id: d.gate_id || null,
484
+ timestamp: d.timestamp || null,
485
+ }));
486
+ }
487
+
411
488
  function extractBarrierSummary(artifact) {
412
489
  const data = extractFileData(artifact, '.agentxchain/multirepo/barriers.json');
413
490
  if (!data || typeof data !== 'object' || Array.isArray(data)) return [];
@@ -538,6 +615,9 @@ function buildRunSubject(artifact) {
538
615
 
539
616
  const turns = extractHistoryTimeline(artifact);
540
617
  const decisions = extractDecisionDigest(artifact);
618
+ const approvalPolicyEvents = extractApprovalPolicyDigest(artifact);
619
+ const gateFailures = extractGateFailureDigest(artifact);
620
+ const timeoutEvents = extractTimeoutEventDigest(artifact);
541
621
  const hookSummary = extractHookSummary(artifact);
542
622
  const timing = computeTiming(artifact, turns);
543
623
  const gateSummary = extractGateSummary(artifact);
@@ -560,6 +640,7 @@ function buildRunSubject(artifact) {
560
640
  phase: artifact.summary?.phase || null,
561
641
  blocked_on: artifact.state?.blocked_on || null,
562
642
  blocked_reason: artifact.state?.blocked_reason || null,
643
+ provenance: normalizeRunProvenance(artifact.summary?.provenance || artifact.state?.provenance),
563
644
  active_turn_count: activeTurns.length,
564
645
  retained_turn_count: retainedTurns.length,
565
646
  active_turn_ids: activeTurns,
@@ -571,6 +652,9 @@ function buildRunSubject(artifact) {
571
652
  duration_seconds: timing.duration_seconds,
572
653
  turns,
573
654
  decisions,
655
+ approval_policy_events: approvalPolicyEvents,
656
+ gate_failures: gateFailures,
657
+ timeout_events: timeoutEvents,
574
658
  hook_summary: hookSummary,
575
659
  gate_summary: gateSummary,
576
660
  intake_links: intakeLinks,
@@ -643,6 +727,9 @@ function buildCoordinatorSubject(artifact) {
643
727
  const childExport = repoEntry.export;
644
728
  base.turns = extractHistoryTimeline(childExport);
645
729
  base.decisions = extractDecisionDigest(childExport);
730
+ base.approval_policy_events = extractApprovalPolicyDigest(childExport);
731
+ base.gate_failures = extractGateFailureDigest(childExport);
732
+ base.timeout_events = extractTimeoutEventDigest(childExport);
646
733
  base.hook_summary = extractHookSummary(childExport);
647
734
  base.gate_summary = extractGateSummary(childExport);
648
735
  base.recovery_summary = extractRecoverySummary(childExport);
@@ -656,6 +743,8 @@ function buildCoordinatorSubject(artifact) {
656
743
  const barrierSummary = extractBarrierSummary(artifact);
657
744
  const barrierLedgerTimeline = extractBarrierLedgerTimeline(artifact);
658
745
  const decisionDigest = extractCoordinatorDecisionDigest(artifact);
746
+ const coordinatorApprovalPolicyEvents = extractCoordinatorApprovalPolicyDigest(artifact);
747
+ const coordinatorTimeoutEvents = extractTimeoutEventDigest(artifact, '.agentxchain/multirepo/decision-ledger.jsonl');
659
748
  const timing = computeCoordinatorTiming(artifact, coordinatorTimeline);
660
749
  const blockedReason = normalizeCoordinatorBlockedReason(coordinatorState.blocked_reason);
661
750
  const pendingGate = normalizePendingGate(coordinatorState.pending_gate);
@@ -698,6 +787,8 @@ function buildCoordinatorSubject(artifact) {
698
787
  barrier_summary: barrierSummary,
699
788
  barrier_ledger_timeline: barrierLedgerTimeline,
700
789
  decision_digest: decisionDigest,
790
+ approval_policy_events: coordinatorApprovalPolicyEvents,
791
+ timeout_events: coordinatorTimeoutEvents,
701
792
  recovery_report: extractRecoveryReportSummary(artifact),
702
793
  repos,
703
794
  artifacts: {
@@ -810,6 +901,9 @@ export function formatGovernanceReportText(report) {
810
901
  if (run.duration_seconds != null) {
811
902
  lines.push(`Duration: ${run.duration_seconds}s`);
812
903
  }
904
+ if (summarizeRunProvenance(run.provenance)) {
905
+ lines.push(`Provenance: ${summarizeRunProvenance(run.provenance)}`);
906
+ }
813
907
 
814
908
  lines.push(
815
909
  `History entries: ${artifacts.history_entries}`,
@@ -828,7 +922,8 @@ export function formatGovernanceReportText(report) {
828
922
  const t = run.turns[i];
829
923
  const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
830
924
  const phase = t.phase_transition ? `${t.phase || '?'} -> ${t.phase_transition}` : (t.phase || '?');
831
- lines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
925
+ const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling-attributed)` : '';
926
+ lines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count}${siblingNote} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
832
927
  }
833
928
  }
834
929
 
@@ -846,6 +941,46 @@ export function formatGovernanceReportText(report) {
846
941
  }
847
942
  }
848
943
 
944
+ if (run.gate_failures && run.gate_failures.length > 0) {
945
+ lines.push('', 'Gate Failures:');
946
+ for (const failure of run.gate_failures) {
947
+ const request = failure.gate_type === 'run_completion'
948
+ ? 'run completion'
949
+ : `${failure.from_phase || failure.phase || '?'} -> ${failure.to_phase || '?'}`;
950
+ const source = failure.queued_request ? 'queued drain' : 'direct';
951
+ lines.push(` - ${failure.gate_id || 'unknown'} | ${failure.gate_type || 'unknown'} | request: ${request} | source: ${source} | at: ${failure.failed_at || 'n/a'}`);
952
+ for (const reason of failure.reasons || []) {
953
+ lines.push(` reason: ${reason}`);
954
+ }
955
+ }
956
+ }
957
+
958
+ if (run.approval_policy_events && run.approval_policy_events.length > 0) {
959
+ lines.push('', 'Approval Policy:');
960
+ for (const evt of run.approval_policy_events) {
961
+ const transition = evt.gate_type === 'run_completion'
962
+ ? 'run completion'
963
+ : `${evt.from_phase || '?'} -> ${evt.to_phase || '?'}`;
964
+ const rule = evt.matched_rule ? ` | rule: ${typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule}` : '';
965
+ lines.push(` - ${evt.action || 'unknown'} | ${evt.gate_type || 'unknown'} | ${transition}${rule} | at: ${evt.timestamp || 'n/a'}`);
966
+ if (evt.reason) lines.push(` reason: ${evt.reason}`);
967
+ }
968
+ }
969
+
970
+ if (run.timeout_events && run.timeout_events.length > 0) {
971
+ lines.push('', 'Timeout Events:');
972
+ for (const evt of run.timeout_events) {
973
+ const label = evt.type === 'timeout_warning' ? 'warning'
974
+ : evt.type === 'timeout_skip' ? 'skip'
975
+ : evt.type === 'timeout_skip_failed' ? 'skip failed'
976
+ : 'escalation';
977
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
978
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
979
+ const exceeded = evt.exceeded_by_minutes != null ? `+${evt.exceeded_by_minutes}m` : '';
980
+ lines.push(` - ${label} | ${evt.scope || '?'} scope | ${elapsed}/${limit}${exceeded ? ` (${exceeded})` : ''} | action: ${evt.action || 'n/a'} | phase: ${evt.phase || 'n/a'} | at: ${evt.timestamp || 'n/a'}`);
981
+ }
982
+ }
983
+
849
984
  if (run.intake_links && run.intake_links.length > 0) {
850
985
  lines.push('', 'Intake Linkage:');
851
986
  for (const intake of run.intake_links) {
@@ -896,7 +1031,19 @@ export function formatGovernanceReportText(report) {
896
1031
  return lines.join('\n');
897
1032
  }
898
1033
 
899
- const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, recovery_report } = report.subject;
1034
+ const {
1035
+ coordinator,
1036
+ run,
1037
+ artifacts,
1038
+ repos,
1039
+ coordinator_timeline,
1040
+ barrier_summary,
1041
+ barrier_ledger_timeline,
1042
+ decision_digest,
1043
+ approval_policy_events,
1044
+ timeout_events,
1045
+ recovery_report,
1046
+ } = report.subject;
900
1047
  const lines = [
901
1048
  'AgentXchain Governance Report',
902
1049
  `Input: ${report.input}`,
@@ -979,6 +1126,32 @@ export function formatGovernanceReportText(report) {
979
1126
  }
980
1127
  }
981
1128
 
1129
+ if (approval_policy_events && approval_policy_events.length > 0) {
1130
+ lines.push('', 'Approval Policy:');
1131
+ for (const evt of approval_policy_events) {
1132
+ const transition = evt.gate_type === 'run_completion'
1133
+ ? 'run completion'
1134
+ : `${evt.from_phase || '?'} -> ${evt.to_phase || '?'}`;
1135
+ const rule = evt.matched_rule ? ` | rule: ${typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule}` : '';
1136
+ lines.push(` - ${evt.action || 'unknown'} | ${evt.gate_type || 'unknown'} | ${transition}${rule} | at: ${evt.timestamp || 'n/a'}`);
1137
+ if (evt.reason) lines.push(` reason: ${evt.reason}`);
1138
+ }
1139
+ }
1140
+
1141
+ if (timeout_events && timeout_events.length > 0) {
1142
+ lines.push('', 'Timeout Events:');
1143
+ for (const evt of timeout_events) {
1144
+ const label = evt.type === 'timeout_warning' ? 'warning'
1145
+ : evt.type === 'timeout_skip' ? 'skip'
1146
+ : evt.type === 'timeout_skip_failed' ? 'skip failed'
1147
+ : 'escalation';
1148
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
1149
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
1150
+ const exceeded = evt.exceeded_by_minutes != null ? `+${evt.exceeded_by_minutes}m` : '';
1151
+ lines.push(` - ${label} | ${evt.scope || '?'} scope | ${elapsed}/${limit}${exceeded ? ` (${exceeded})` : ''} | action: ${evt.action || 'n/a'} | phase: ${evt.phase || 'n/a'} | at: ${evt.timestamp || 'n/a'}`);
1152
+ }
1153
+ }
1154
+
982
1155
  if (recovery_report) {
983
1156
  lines.push('', 'Recovery Report:');
984
1157
  lines.push(` Trigger: ${recovery_report.trigger || 'n/a'}`);
@@ -1003,7 +1176,8 @@ export function formatGovernanceReportText(report) {
1003
1176
  const t = repo.turns[i];
1004
1177
  const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
1005
1178
  const phase = t.phase_transition ? `${t.phase || '?'} -> ${t.phase_transition}` : (t.phase || '?');
1006
- repoLines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
1179
+ const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling-attributed)` : '';
1180
+ repoLines.push(` ${i + 1}. [${t.role}] ${t.summary || '(no summary)'} | phase: ${phase} | files: ${t.files_changed_count}${siblingNote} | cost: ${cost} | ${t.accepted_at || 'n/a'}`);
1007
1181
  }
1008
1182
  }
1009
1183
  if (repo.decisions && repo.decisions.length > 0) {
@@ -1018,6 +1192,39 @@ export function formatGovernanceReportText(report) {
1018
1192
  repoLines.push(` - ${gate.gate_id}: ${gate.status}`);
1019
1193
  }
1020
1194
  }
1195
+ if (repo.gate_failures && repo.gate_failures.length > 0) {
1196
+ repoLines.push(' Gate Failures:');
1197
+ for (const failure of repo.gate_failures) {
1198
+ const request = failure.gate_type === 'run_completion'
1199
+ ? 'run completion'
1200
+ : `${failure.from_phase || failure.phase || '?'} -> ${failure.to_phase || '?'}`;
1201
+ repoLines.push(` - ${failure.gate_id || 'unknown'}: ${failure.gate_type || 'unknown'} | ${request} | ${failure.queued_request ? 'queued drain' : 'direct'} | ${failure.failed_at || 'n/a'}`);
1202
+ for (const reason of failure.reasons || []) {
1203
+ repoLines.push(` reason: ${reason}`);
1204
+ }
1205
+ }
1206
+ }
1207
+ if (repo.approval_policy_events && repo.approval_policy_events.length > 0) {
1208
+ repoLines.push(' Approval Policy:');
1209
+ for (const evt of repo.approval_policy_events) {
1210
+ const transition = evt.gate_type === 'run_completion'
1211
+ ? 'run completion'
1212
+ : `${evt.from_phase || '?'} -> ${evt.to_phase || '?'}`;
1213
+ repoLines.push(` - ${evt.action || 'unknown'}: ${evt.gate_type || 'unknown'} | ${transition} | ${evt.timestamp || 'n/a'}`);
1214
+ }
1215
+ }
1216
+ if (repo.timeout_events && repo.timeout_events.length > 0) {
1217
+ repoLines.push(' Timeout Events:');
1218
+ for (const evt of repo.timeout_events) {
1219
+ const label = evt.type === 'timeout_warning' ? 'warning'
1220
+ : evt.type === 'timeout_skip' ? 'skip'
1221
+ : evt.type === 'timeout_skip_failed' ? 'skip failed'
1222
+ : 'escalation';
1223
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
1224
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
1225
+ repoLines.push(` - ${label}: ${evt.scope || '?'} scope | ${elapsed}/${limit} | action: ${evt.action || 'n/a'} | ${evt.timestamp || 'n/a'}`);
1226
+ }
1227
+ }
1021
1228
  if (repo.hook_summary) {
1022
1229
  repoLines.push(` Hook Activity: ${repo.hook_summary.total} total, ${repo.hook_summary.blocked} blocked`);
1023
1230
  }
@@ -1098,6 +1305,9 @@ export function formatGovernanceReportMarkdown(report) {
1098
1305
  if (run.duration_seconds != null) {
1099
1306
  lines.push(`- Duration: \`${run.duration_seconds}s\``);
1100
1307
  }
1308
+ if (summarizeRunProvenance(run.provenance)) {
1309
+ lines.push(`- Provenance: \`${summarizeRunProvenance(run.provenance)}\``);
1310
+ }
1101
1311
 
1102
1312
  lines.push(
1103
1313
  `- History entries: ${artifacts.history_entries}`,
@@ -1117,7 +1327,8 @@ export function formatGovernanceReportMarkdown(report) {
1117
1327
  const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
1118
1328
  const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
1119
1329
  const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
1120
- lines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1330
+ const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
1331
+ lines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1121
1332
  }
1122
1333
  }
1123
1334
 
@@ -1135,6 +1346,45 @@ export function formatGovernanceReportMarkdown(report) {
1135
1346
  }
1136
1347
  }
1137
1348
 
1349
+ if (run.gate_failures && run.gate_failures.length > 0) {
1350
+ lines.push('', '## Gate Failures', '');
1351
+ for (const failure of run.gate_failures) {
1352
+ const request = failure.gate_type === 'run_completion'
1353
+ ? 'run completion'
1354
+ : `${failure.from_phase || failure.phase || '?'} → ${failure.to_phase || '?'}`;
1355
+ lines.push(`- \`${failure.gate_id || 'unknown'}\` (${failure.gate_type || 'unknown'}) at \`${failure.failed_at || 'n/a'}\` via ${failure.queued_request ? 'queued drain' : 'direct'} request: ${request}`);
1356
+ for (const reason of failure.reasons || []) {
1357
+ lines.push(` - ${reason}`);
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ if (run.approval_policy_events && run.approval_policy_events.length > 0) {
1363
+ lines.push('', '## Approval Policy', '');
1364
+ for (const evt of run.approval_policy_events) {
1365
+ const transition = evt.gate_type === 'run_completion'
1366
+ ? 'run completion'
1367
+ : `${evt.from_phase || '?'} → ${evt.to_phase || '?'}`;
1368
+ const rule = evt.matched_rule ? ` — rule: \`${typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule}\`` : '';
1369
+ lines.push(`- **${evt.action || 'unknown'}** (${evt.gate_type || 'unknown'}) ${transition}${rule} at \`${evt.timestamp || 'n/a'}\``);
1370
+ if (evt.reason) lines.push(` - ${evt.reason}`);
1371
+ }
1372
+ }
1373
+
1374
+ if (run.timeout_events && run.timeout_events.length > 0) {
1375
+ lines.push('', '## Timeout Events', '');
1376
+ for (const evt of run.timeout_events) {
1377
+ const label = evt.type === 'timeout_warning' ? 'Warning'
1378
+ : evt.type === 'timeout_skip' ? 'Skip'
1379
+ : evt.type === 'timeout_skip_failed' ? 'Skip Failed'
1380
+ : 'Escalation';
1381
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
1382
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
1383
+ const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
1384
+ lines.push(`- **${label}** (\`${evt.scope || '?'}\` scope) — ${elapsed}/${limit}${exceeded}, action: \`${evt.action || 'n/a'}\`, phase: \`${evt.phase || 'n/a'}\` at \`${evt.timestamp || 'n/a'}\``);
1385
+ }
1386
+ }
1387
+
1138
1388
  if (run.intake_links && run.intake_links.length > 0) {
1139
1389
  lines.push('', '## Intake Linkage', '');
1140
1390
  for (const intake of run.intake_links) {
@@ -1188,7 +1438,19 @@ export function formatGovernanceReportMarkdown(report) {
1188
1438
  return lines.join('\n');
1189
1439
  }
1190
1440
 
1191
- const { coordinator, run, artifacts, repos, coordinator_timeline, barrier_summary, barrier_ledger_timeline, decision_digest, recovery_report: coordRecoveryReport } = report.subject;
1441
+ const {
1442
+ coordinator,
1443
+ run,
1444
+ artifacts,
1445
+ repos,
1446
+ coordinator_timeline,
1447
+ barrier_summary,
1448
+ barrier_ledger_timeline,
1449
+ decision_digest,
1450
+ approval_policy_events,
1451
+ timeout_events,
1452
+ recovery_report: coordRecoveryReport,
1453
+ } = report.subject;
1192
1454
  const mdLines = [
1193
1455
  '# AgentXchain Governance Report',
1194
1456
  '',
@@ -1272,6 +1534,32 @@ export function formatGovernanceReportMarkdown(report) {
1272
1534
  }
1273
1535
  }
1274
1536
 
1537
+ if (approval_policy_events && approval_policy_events.length > 0) {
1538
+ mdLines.push('', '## Approval Policy', '');
1539
+ for (const evt of approval_policy_events) {
1540
+ const transition = evt.gate_type === 'run_completion'
1541
+ ? 'run completion'
1542
+ : `${evt.from_phase || '?'} → ${evt.to_phase || '?'}`;
1543
+ const rule = evt.matched_rule ? ` — rule: \`${typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule}\`` : '';
1544
+ mdLines.push(`- **${evt.action || 'unknown'}** (${evt.gate_type || 'unknown'}) ${transition}${rule} at \`${evt.timestamp || 'n/a'}\``);
1545
+ if (evt.reason) mdLines.push(` - ${evt.reason}`);
1546
+ }
1547
+ }
1548
+
1549
+ if (timeout_events && timeout_events.length > 0) {
1550
+ mdLines.push('', '## Timeout Events', '');
1551
+ for (const evt of timeout_events) {
1552
+ const label = evt.type === 'timeout_warning' ? 'Warning'
1553
+ : evt.type === 'timeout_skip' ? 'Skip'
1554
+ : evt.type === 'timeout_skip_failed' ? 'Skip Failed'
1555
+ : 'Escalation';
1556
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
1557
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
1558
+ const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
1559
+ mdLines.push(`- **${label}** (\`${evt.scope || '?'}\` scope) — ${elapsed}/${limit}${exceeded}, action: \`${evt.action || 'n/a'}\`, phase: \`${evt.phase || 'n/a'}\` at \`${evt.timestamp || 'n/a'}\``);
1560
+ }
1561
+ }
1562
+
1275
1563
  if (coordRecoveryReport) {
1276
1564
  mdLines.push('', '## Recovery Report', '');
1277
1565
  mdLines.push(`- **Trigger:** ${coordRecoveryReport.trigger || 'n/a'}`);
@@ -1297,7 +1585,8 @@ export function formatGovernanceReportMarkdown(report) {
1297
1585
  const cost = t.cost_usd != null ? formatUsd(t.cost_usd) : 'n/a';
1298
1586
  const phase = t.phase_transition ? `${t.phase || '?'} → ${t.phase_transition}` : (t.phase || '?');
1299
1587
  const summary = (t.summary || '(no summary)').replace(/\|/g, '\\|');
1300
- repoLines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1588
+ const siblingNote = Array.isArray(t.sibling_attributed_files) ? ` (${t.sibling_attributed_files.length} sibling)` : '';
1589
+ repoLines.push(`| ${i + 1} | ${t.role} | ${phase} | ${summary} | ${t.files_changed_count}${siblingNote} | ${cost} | ${t.accepted_at || 'n/a'} |`);
1301
1590
  }
1302
1591
  }
1303
1592
  if (repo.decisions && repo.decisions.length > 0) {
@@ -1312,6 +1601,42 @@ export function formatGovernanceReportMarkdown(report) {
1312
1601
  repoLines.push(`- \`${gate.gate_id}\`: \`${gate.status}\``);
1313
1602
  }
1314
1603
  }
1604
+ if (repo.gate_failures && repo.gate_failures.length > 0) {
1605
+ repoLines.push('', '#### Gate Failures', '');
1606
+ for (const failure of repo.gate_failures) {
1607
+ const request = failure.gate_type === 'run_completion'
1608
+ ? 'run completion'
1609
+ : `${failure.from_phase || failure.phase || '?'} → ${failure.to_phase || '?'}`;
1610
+ repoLines.push(`- \`${failure.gate_id || 'unknown'}\` (${failure.gate_type || 'unknown'}) at \`${failure.failed_at || 'n/a'}\` via ${failure.queued_request ? 'queued drain' : 'direct'} request: ${request}`);
1611
+ for (const reason of failure.reasons || []) {
1612
+ repoLines.push(` - ${reason}`);
1613
+ }
1614
+ }
1615
+ }
1616
+ if (repo.approval_policy_events && repo.approval_policy_events.length > 0) {
1617
+ repoLines.push('', '#### Approval Policy', '');
1618
+ for (const evt of repo.approval_policy_events) {
1619
+ const transition = evt.gate_type === 'run_completion'
1620
+ ? 'run completion'
1621
+ : `${evt.from_phase || '?'} → ${evt.to_phase || '?'}`;
1622
+ const rule = evt.matched_rule ? ` — rule: \`${typeof evt.matched_rule === 'object' ? JSON.stringify(evt.matched_rule) : evt.matched_rule}\`` : '';
1623
+ repoLines.push(`- **${evt.action || 'unknown'}** (${evt.gate_type || 'unknown'}) ${transition}${rule} at \`${evt.timestamp || 'n/a'}\``);
1624
+ if (evt.reason) repoLines.push(` - ${evt.reason}`);
1625
+ }
1626
+ }
1627
+ if (repo.timeout_events && repo.timeout_events.length > 0) {
1628
+ repoLines.push('', '#### Timeout Events', '');
1629
+ for (const evt of repo.timeout_events) {
1630
+ const label = evt.type === 'timeout_warning' ? 'Warning'
1631
+ : evt.type === 'timeout_skip' ? 'Skip'
1632
+ : evt.type === 'timeout_skip_failed' ? 'Skip Failed'
1633
+ : 'Escalation';
1634
+ const elapsed = evt.elapsed_minutes != null ? `${evt.elapsed_minutes}m` : '?';
1635
+ const limit = evt.limit_minutes != null ? `${evt.limit_minutes}m` : '?';
1636
+ const exceeded = evt.exceeded_by_minutes != null ? ` (+${evt.exceeded_by_minutes}m)` : '';
1637
+ repoLines.push(`- **${label}** (\`${evt.scope || '?'}\` scope) — ${elapsed}/${limit}${exceeded}, action: \`${evt.action || 'n/a'}\`, phase: \`${evt.phase || 'n/a'}\` at \`${evt.timestamp || 'n/a'}\``);
1638
+ }
1639
+ }
1315
1640
  if (repo.hook_summary) {
1316
1641
  repoLines.push('', '#### Hook Activity', '', `- Total: ${repo.hook_summary.total}`, `- Blocked: ${repo.hook_summary.blocked}`);
1317
1642
  const eventList = Object.entries(repo.hook_summary.events).sort(([a], [b]) => a.localeCompare(b, 'en')).map(([e, c]) => `${e}(${c})`).join(', ');
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
11
11
  import { join, dirname } from 'path';
12
+ import { normalizeRunProvenance } from './run-provenance.js';
12
13
 
13
14
  const RUN_HISTORY_PATH = '.agentxchain/run-history.jsonl';
14
15
  const HISTORY_PATH = '.agentxchain/history.jsonl';
@@ -79,6 +80,7 @@ export function recordRunHistory(root, state, config, status) {
79
80
  gate_results: state?.phase_gate_status || {},
80
81
  connector_used: connectorUsed,
81
82
  model_used: modelUsed,
83
+ provenance: normalizeRunProvenance(state?.provenance),
82
84
  recorded_at: new Date().toISOString(),
83
85
  };
84
86
 
@@ -132,6 +134,115 @@ export function queryRunHistory(root, opts = {}) {
132
134
  return entries;
133
135
  }
134
136
 
137
+ /**
138
+ * Walk run lineage backwards from a given run_id via parent_run_id links.
139
+ *
140
+ * Returns an ordered array (oldest ancestor first) of history entries
141
+ * forming the lineage chain. If a parent_run_id references a run not
142
+ * found in history, the chain terminates with a broken_link sentinel.
143
+ *
144
+ * @param {string} root - project root directory
145
+ * @param {string} runId - the run to trace lineage for
146
+ * @returns {{ ok: boolean, chain?: Array<object>, error?: string }}
147
+ */
148
+ export function queryRunLineage(root, runId) {
149
+ const filePath = join(root, RUN_HISTORY_PATH);
150
+ if (!existsSync(filePath)) {
151
+ return { ok: false, error: 'No run history found. Run at least one governed run first.' };
152
+ }
153
+
154
+ let content;
155
+ try {
156
+ content = readFileSync(filePath, 'utf8').trim();
157
+ } catch {
158
+ return { ok: false, error: 'Failed to read run history file.' };
159
+ }
160
+ if (!content) {
161
+ return { ok: false, error: 'No run history found. Run at least one governed run first.' };
162
+ }
163
+
164
+ const entries = content
165
+ .split('\n')
166
+ .filter(Boolean)
167
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
168
+ .filter(Boolean);
169
+
170
+ // Build lookup by run_id
171
+ const byId = new Map();
172
+ for (const entry of entries) {
173
+ if (entry.run_id) byId.set(entry.run_id, entry);
174
+ }
175
+
176
+ // Find the target entry
177
+ const target = byId.get(runId);
178
+ if (!target) {
179
+ return { ok: false, error: `Run ${runId} not found in run history.` };
180
+ }
181
+
182
+ // Walk backwards collecting ancestors
183
+ const chain = [target];
184
+ let current = target;
185
+ const visited = new Set([runId]);
186
+
187
+ while (current.provenance?.parent_run_id) {
188
+ const parentId = current.provenance.parent_run_id;
189
+ if (visited.has(parentId)) break; // safety: prevent cycles
190
+ visited.add(parentId);
191
+
192
+ const parent = byId.get(parentId);
193
+ if (!parent) {
194
+ chain.unshift({ broken_link: true, missing_run_id: parentId });
195
+ break;
196
+ }
197
+ chain.unshift(parent);
198
+ current = parent;
199
+ }
200
+
201
+ return { ok: true, chain };
202
+ }
203
+
204
+ /**
205
+ * Validate that a run_id exists in history and is in a terminal state.
206
+ * Used by --continue-from and --recover-from flag validation.
207
+ *
208
+ * @param {string} root - project root directory
209
+ * @param {string} runId - the run_id to validate
210
+ * @returns {{ ok: boolean, entry?: object, error?: string }}
211
+ */
212
+ export function validateParentRun(root, runId) {
213
+ const filePath = join(root, RUN_HISTORY_PATH);
214
+ if (!existsSync(filePath)) {
215
+ return { ok: false, error: `Run ${runId} not found in run history` };
216
+ }
217
+
218
+ let content;
219
+ try {
220
+ content = readFileSync(filePath, 'utf8').trim();
221
+ } catch {
222
+ return { ok: false, error: `Run ${runId} not found in run history` };
223
+ }
224
+ if (!content) {
225
+ return { ok: false, error: `Run ${runId} not found in run history` };
226
+ }
227
+
228
+ const entries = content
229
+ .split('\n')
230
+ .filter(Boolean)
231
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
232
+ .filter(Boolean);
233
+
234
+ const entry = entries.find(e => e.run_id === runId);
235
+ if (!entry) {
236
+ return { ok: false, error: `Run ${runId} not found in run history` };
237
+ }
238
+
239
+ if (!WRITABLE_TERMINAL_STATUSES.has(entry.status)) {
240
+ return { ok: false, error: `Run ${runId} is still active (status: ${entry.status}). Cannot chain from a non-terminal run.` };
241
+ }
242
+
243
+ return { ok: true, entry };
244
+ }
245
+
135
246
  /**
136
247
  * Get the path to the run-history file.
137
248
  */
@@ -38,7 +38,7 @@ const DEFAULT_MAX_TURNS = 50;
38
38
  * @param {string} root - project root directory
39
39
  * @param {object} config - normalized governed config
40
40
  * @param {object} callbacks - { selectRole, dispatch, approveGate, onEvent? }
41
- * @param {object} [options] - { maxTurns?: number }
41
+ * @param {object} [options] - { maxTurns?: number, provenance?: object, startNewRunFromCompleted?: boolean, startNewRunFromBlocked?: boolean }
42
42
  * @returns {Promise<RunLoopResult>}
43
43
  */
44
44
  export async function runLoop(root, config, callbacks, options = {}) {
@@ -58,8 +58,14 @@ export async function runLoop(root, config, callbacks, options = {}) {
58
58
 
59
59
  // ── Initialize if idle ──────────────────────────────────────────────────
60
60
  let state = loadState(root, config);
61
- if (!state || state.status === 'idle') {
62
- const initResult = initRun(root, config);
61
+ const shouldRestartCompleted = state?.status === 'completed' && options.startNewRunFromCompleted === true;
62
+ const shouldRestartBlocked = state?.status === 'blocked' && options.startNewRunFromBlocked === true;
63
+ if (!state || state.status === 'idle' || shouldRestartCompleted || shouldRestartBlocked) {
64
+ const initOpts = options.provenance ? { provenance: options.provenance } : {};
65
+ if (shouldRestartCompleted || shouldRestartBlocked) {
66
+ initOpts.allow_terminal_restart = true;
67
+ }
68
+ const initResult = initRun(root, config, initOpts);
63
69
  if (!initResult.ok) {
64
70
  return makeResult(false, 'init_failed', loadState(root, config), turnsExecuted, turnHistory, gatesApproved, [initResult.error]);
65
71
  }