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/bin/agentxchain.js +4 -1
- package/dashboard/app.js +6 -0
- package/dashboard/components/coordinator-timeouts.js +220 -0
- package/dashboard/components/timeouts.js +201 -0
- package/dashboard/index.html +2 -0
- package/package.json +1 -1
- package/scripts/publish-from-tag.sh +33 -4
- package/src/commands/history.js +41 -1
- package/src/commands/init.js +1 -0
- package/src/commands/migrate.js +1 -0
- package/src/commands/run.js +32 -1
- package/src/commands/status.js +55 -0
- package/src/lib/approval-policy.js +139 -0
- package/src/lib/blocked-state.js +11 -0
- package/src/lib/dashboard/bridge-server.js +14 -0
- package/src/lib/dashboard/coordinator-timeout-status.js +139 -0
- package/src/lib/dashboard/timeout-status.js +201 -0
- package/src/lib/export.js +2 -0
- package/src/lib/governed-state.js +428 -30
- package/src/lib/normalized-config.js +123 -0
- package/src/lib/reference-conformance-adapter.js +1 -0
- package/src/lib/repo-observer.js +132 -1
- package/src/lib/report.js +331 -6
- package/src/lib/run-history.js +111 -0
- package/src/lib/run-loop.js +9 -3
- package/src/lib/run-provenance.js +90 -0
- package/src/lib/schema.js +47 -0
- package/src/lib/timeout-evaluator.js +234 -0
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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(', ');
|
package/src/lib/run-history.js
CHANGED
|
@@ -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
|
*/
|
package/src/lib/run-loop.js
CHANGED
|
@@ -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
|
-
|
|
62
|
-
|
|
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
|
}
|