agentxchain 2.100.0 → 2.101.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 +7 -1
- package/package.json +1 -1
- package/src/commands/verify.js +136 -0
- package/src/lib/export-diff.js +80 -0
- package/src/lib/export.js +7 -0
package/bin/agentxchain.js
CHANGED
|
@@ -59,7 +59,7 @@ import { generateCommand } from '../src/commands/generate.js';
|
|
|
59
59
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
62
|
-
import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
62
|
+
import { verifyDiffCommand, verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
63
63
|
import { replayTurnCommand } from '../src/commands/replay.js';
|
|
64
64
|
import { replayExportCommand } from '../src/commands/replay-export.js';
|
|
65
65
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
@@ -401,6 +401,12 @@ verifyCmd
|
|
|
401
401
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
402
402
|
.action(verifyExportCommand);
|
|
403
403
|
|
|
404
|
+
verifyCmd
|
|
405
|
+
.command('diff <left_export> <right_export>')
|
|
406
|
+
.description('Verify two export artifacts, then detect governance regressions between them')
|
|
407
|
+
.option('--format <format>', 'Output format: text or json', 'text')
|
|
408
|
+
.action(verifyDiffCommand);
|
|
409
|
+
|
|
404
410
|
const replayCmd = program
|
|
405
411
|
.command('replay')
|
|
406
412
|
.description('Replay accepted governed evidence against the current workspace');
|
package/package.json
CHANGED
package/src/commands/verify.js
CHANGED
|
@@ -6,6 +6,7 @@ import { validateStagedTurnResult } from '../lib/turn-result-validator.js';
|
|
|
6
6
|
import { getTurnStagingResultPath } from '../lib/turn-paths.js';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
8
|
import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
|
|
9
|
+
import { buildExportDiff, resolveExportArtifact } from '../lib/export-diff.js';
|
|
9
10
|
import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
@@ -72,6 +73,18 @@ function emitProtocolVerifyError(format, message) {
|
|
|
72
73
|
console.log(chalk.red(`Protocol verification failed: ${message}`));
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
function emitVerifyDiffError(format, message) {
|
|
77
|
+
if (format === 'json') {
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
overall: 'error',
|
|
80
|
+
message,
|
|
81
|
+
}, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(chalk.red(`Diff verification failed: ${message}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export async function verifyExportCommand(opts) {
|
|
76
89
|
const format = opts.format || 'text';
|
|
77
90
|
const loaded = loadExportArtifact(opts.input || '-', process.cwd());
|
|
@@ -104,6 +117,75 @@ export async function verifyExportCommand(opts) {
|
|
|
104
117
|
process.exit(result.ok ? 0 : 1);
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
export async function verifyDiffCommand(leftRef, rightRef, opts = {}) {
|
|
121
|
+
const format = opts.format || 'text';
|
|
122
|
+
const leftLoaded = resolveExportArtifact(leftRef);
|
|
123
|
+
if (!leftLoaded.ok) {
|
|
124
|
+
emitVerifyDiffError(format, leftLoaded.error);
|
|
125
|
+
process.exit(2);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rightLoaded = resolveExportArtifact(rightRef);
|
|
129
|
+
if (!rightLoaded.ok) {
|
|
130
|
+
emitVerifyDiffError(format, rightLoaded.error);
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (leftLoaded.artifact.export_kind !== rightLoaded.artifact.export_kind) {
|
|
135
|
+
emitVerifyDiffError(
|
|
136
|
+
format,
|
|
137
|
+
`Export kinds do not match: ${leftLoaded.artifact.export_kind} vs ${rightLoaded.artifact.export_kind}`,
|
|
138
|
+
);
|
|
139
|
+
process.exit(2);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const leftVerification = verifyExportArtifact(leftLoaded.artifact);
|
|
143
|
+
const rightVerification = verifyExportArtifact(rightLoaded.artifact);
|
|
144
|
+
const leftReport = { ...leftVerification.report, input: leftLoaded.resolved_ref };
|
|
145
|
+
const rightReport = { ...rightVerification.report, input: rightLoaded.resolved_ref };
|
|
146
|
+
|
|
147
|
+
let diff = null;
|
|
148
|
+
let overall = 'pass';
|
|
149
|
+
let exitCode = 0;
|
|
150
|
+
let message = null;
|
|
151
|
+
|
|
152
|
+
if (!leftVerification.ok || !rightVerification.ok) {
|
|
153
|
+
overall = 'fail';
|
|
154
|
+
exitCode = 1;
|
|
155
|
+
message = 'Diff skipped because one or both exports failed verification.';
|
|
156
|
+
} else {
|
|
157
|
+
const diffResult = buildExportDiff(leftLoaded.artifact, rightLoaded.artifact, {
|
|
158
|
+
left_ref: leftLoaded.resolved_ref,
|
|
159
|
+
right_ref: rightLoaded.resolved_ref,
|
|
160
|
+
});
|
|
161
|
+
if (!diffResult.ok) {
|
|
162
|
+
emitVerifyDiffError(format, diffResult.error);
|
|
163
|
+
process.exit(2);
|
|
164
|
+
}
|
|
165
|
+
diff = diffResult.diff;
|
|
166
|
+
if (diff.has_regressions) {
|
|
167
|
+
overall = 'fail';
|
|
168
|
+
exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const report = {
|
|
173
|
+
overall,
|
|
174
|
+
left: leftReport,
|
|
175
|
+
right: rightReport,
|
|
176
|
+
diff,
|
|
177
|
+
message,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (format === 'json') {
|
|
181
|
+
console.log(JSON.stringify(report, null, 2));
|
|
182
|
+
} else {
|
|
183
|
+
printVerifyDiffReport(report);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
process.exit(exitCode);
|
|
187
|
+
}
|
|
188
|
+
|
|
107
189
|
export async function verifyTurnCommand(turnId, opts = {}) {
|
|
108
190
|
const context = loadProjectContext();
|
|
109
191
|
if (!context) {
|
|
@@ -237,6 +319,54 @@ function printExportReport(report) {
|
|
|
237
319
|
console.log('');
|
|
238
320
|
}
|
|
239
321
|
|
|
322
|
+
function printVerifyDiffReport(report) {
|
|
323
|
+
console.log('');
|
|
324
|
+
console.log(chalk.bold(' AgentXchain Diff Verification'));
|
|
325
|
+
console.log(chalk.dim(' ' + '─'.repeat(41)));
|
|
326
|
+
console.log(chalk.dim(` Left: ${report.left.input}`));
|
|
327
|
+
console.log(chalk.dim(` Right: ${report.right.input}`));
|
|
328
|
+
console.log('');
|
|
329
|
+
|
|
330
|
+
const overallLabel = report.overall === 'pass'
|
|
331
|
+
? chalk.green('PASS')
|
|
332
|
+
: report.overall === 'fail'
|
|
333
|
+
? chalk.red('FAIL')
|
|
334
|
+
: chalk.red('ERROR');
|
|
335
|
+
console.log(` Overall: ${overallLabel}`);
|
|
336
|
+
console.log(` Left export: ${formatVerifyStatus(report.left.overall)}`);
|
|
337
|
+
console.log(` Right export: ${formatVerifyStatus(report.right.overall)}`);
|
|
338
|
+
|
|
339
|
+
if (report.diff) {
|
|
340
|
+
console.log(chalk.dim(` Diff subject: ${report.diff.subject_kind}`));
|
|
341
|
+
console.log(chalk.dim(` Changed: ${report.diff.changed ? 'yes' : 'no'}`));
|
|
342
|
+
console.log(chalk.dim(` Governance regressions: ${report.diff.regression_count}`));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (report.message) {
|
|
346
|
+
console.log(chalk.yellow(` ${report.message}`));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const error of report.left.errors || []) {
|
|
350
|
+
console.log(chalk.red(` left ✗ ${error}`));
|
|
351
|
+
}
|
|
352
|
+
for (const error of report.right.errors || []) {
|
|
353
|
+
console.log(chalk.red(` right ✗ ${error}`));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (report.diff?.regressions?.length > 0) {
|
|
357
|
+
console.log('');
|
|
358
|
+
console.log(chalk.bold.red(' Governance Regressions'));
|
|
359
|
+
for (const regression of report.diff.regressions) {
|
|
360
|
+
const severity = regression.severity === 'error'
|
|
361
|
+
? chalk.red(`[${regression.severity}]`)
|
|
362
|
+
: chalk.yellow(`[${regression.severity}]`);
|
|
363
|
+
console.log(` ${severity} ${regression.id}: ${regression.message}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log('');
|
|
368
|
+
}
|
|
369
|
+
|
|
240
370
|
function resolveTargetTurnId(requestedTurnId, activeTurns) {
|
|
241
371
|
const turnIds = Object.keys(activeTurns);
|
|
242
372
|
|
|
@@ -351,3 +481,9 @@ function formatOutcome(outcome) {
|
|
|
351
481
|
if (outcome === 'mismatch') return chalk.red('mismatch');
|
|
352
482
|
return chalk.yellow('not_reproducible');
|
|
353
483
|
}
|
|
484
|
+
|
|
485
|
+
function formatVerifyStatus(status) {
|
|
486
|
+
if (status === 'pass') return chalk.green('PASS');
|
|
487
|
+
if (status === 'fail') return chalk.red('FAIL');
|
|
488
|
+
return chalk.red(String(status || 'error').toUpperCase());
|
|
489
|
+
}
|
package/src/lib/export-diff.js
CHANGED
|
@@ -193,6 +193,7 @@ function normalizeRunExport(artifact) {
|
|
|
193
193
|
run_id: summary.run_id || null,
|
|
194
194
|
status: summary.status || null,
|
|
195
195
|
phase: summary.phase || null,
|
|
196
|
+
workflow_phase_order: Array.isArray(summary.workflow_phase_order) ? summary.workflow_phase_order : null,
|
|
196
197
|
project_name: artifact.project?.name || null,
|
|
197
198
|
project_goal: summary.project_goal || artifact.project?.goal || null,
|
|
198
199
|
provenance_trigger: summary.provenance?.trigger || null,
|
|
@@ -213,6 +214,7 @@ function normalizeRunExport(artifact) {
|
|
|
213
214
|
budget_warn_mode: budgetStatus.warn_mode === true,
|
|
214
215
|
budget_exhausted: budgetStatus.exhausted === true,
|
|
215
216
|
phase_gate_status: normalizeGateStatusMap(phaseGateStatus),
|
|
217
|
+
delegation_missing_decisions: normalizeDelegationMissingMap(summary.delegation_summary),
|
|
216
218
|
};
|
|
217
219
|
}
|
|
218
220
|
|
|
@@ -227,6 +229,7 @@ function normalizeCoordinatorExport(artifact) {
|
|
|
227
229
|
super_run_id: summary.super_run_id || null,
|
|
228
230
|
status: summary.status || null,
|
|
229
231
|
phase: summary.phase || null,
|
|
232
|
+
workflow_phase_order: Array.isArray(summary.workflow_phase_order) ? summary.workflow_phase_order : null,
|
|
230
233
|
project_name: artifact.coordinator?.project_name || null,
|
|
231
234
|
barrier_count: toNumber(summary.barrier_count),
|
|
232
235
|
history_entries: toNumber(summary.history_entries),
|
|
@@ -354,6 +357,27 @@ function normalizeNumericMap(value) {
|
|
|
354
357
|
);
|
|
355
358
|
}
|
|
356
359
|
|
|
360
|
+
function normalizeDelegationMissingMap(summary) {
|
|
361
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) return {};
|
|
362
|
+
const chains = Array.isArray(summary.delegation_chains) ? summary.delegation_chains : [];
|
|
363
|
+
const entries = [];
|
|
364
|
+
for (const chain of chains) {
|
|
365
|
+
const delegations = Array.isArray(chain?.delegations) ? chain.delegations : [];
|
|
366
|
+
for (const delegation of delegations) {
|
|
367
|
+
if (!delegation || typeof delegation !== 'object' || Array.isArray(delegation)) continue;
|
|
368
|
+
const delegationId = typeof delegation.delegation_id === 'string' && delegation.delegation_id.trim()
|
|
369
|
+
? delegation.delegation_id.trim()
|
|
370
|
+
: null;
|
|
371
|
+
if (!delegationId) continue;
|
|
372
|
+
entries.push([
|
|
373
|
+
delegationId,
|
|
374
|
+
normalizeStringArray(delegation.missing_decision_ids),
|
|
375
|
+
]);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return Object.fromEntries(entries.sort(([left], [right]) => left.localeCompare(right, 'en')));
|
|
379
|
+
}
|
|
380
|
+
|
|
357
381
|
function toNumber(value) {
|
|
358
382
|
return typeof value === 'number' ? value : null;
|
|
359
383
|
}
|
|
@@ -391,6 +415,39 @@ function detectRunRegressions(left, right) {
|
|
|
391
415
|
});
|
|
392
416
|
}
|
|
393
417
|
|
|
418
|
+
// Phase regression: backward movement in workflow phase order
|
|
419
|
+
if (left.phase && right.phase === null) {
|
|
420
|
+
// Phase disappeared — information loss
|
|
421
|
+
regressions.push({
|
|
422
|
+
id: `REG-PHASE-${String(++counter).padStart(3, '0')}`,
|
|
423
|
+
category: 'phase',
|
|
424
|
+
severity: 'warning',
|
|
425
|
+
message: `Phase regressed from "${left.phase}" to null (phase information lost)`,
|
|
426
|
+
field: 'phase',
|
|
427
|
+
left: left.phase,
|
|
428
|
+
right: null,
|
|
429
|
+
});
|
|
430
|
+
} else if (left.phase && right.phase && left.phase !== right.phase) {
|
|
431
|
+
// Use right export's phase order as canonical (or left if right doesn't have one)
|
|
432
|
+
const phaseOrder = right.workflow_phase_order || left.workflow_phase_order;
|
|
433
|
+
if (Array.isArray(phaseOrder) && phaseOrder.length > 0) {
|
|
434
|
+
const leftIndex = phaseOrder.indexOf(left.phase);
|
|
435
|
+
const rightIndex = phaseOrder.indexOf(right.phase);
|
|
436
|
+
// Only flag when both phases are known and right is earlier than left
|
|
437
|
+
if (leftIndex !== -1 && rightIndex !== -1 && rightIndex < leftIndex) {
|
|
438
|
+
regressions.push({
|
|
439
|
+
id: `REG-PHASE-${String(++counter).padStart(3, '0')}`,
|
|
440
|
+
category: 'phase',
|
|
441
|
+
severity: 'warning',
|
|
442
|
+
message: `Phase moved backward from "${left.phase}" (position ${leftIndex}) to "${right.phase}" (position ${rightIndex})`,
|
|
443
|
+
field: 'phase',
|
|
444
|
+
left: left.phase,
|
|
445
|
+
right: right.phase,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
394
451
|
// Budget warn_mode regression
|
|
395
452
|
if (left.budget_warn_mode === false && right.budget_warn_mode === true) {
|
|
396
453
|
regressions.push({
|
|
@@ -432,6 +489,29 @@ function detectRunRegressions(left, right) {
|
|
|
432
489
|
});
|
|
433
490
|
}
|
|
434
491
|
|
|
492
|
+
// Delegation contract regressions: newly missing required decisions.
|
|
493
|
+
const allDelegationIds = new Set([
|
|
494
|
+
...Object.keys(left.delegation_missing_decisions || {}),
|
|
495
|
+
...Object.keys(right.delegation_missing_decisions || {}),
|
|
496
|
+
]);
|
|
497
|
+
for (const delegationId of allDelegationIds) {
|
|
498
|
+
const leftMissing = normalizeStringArray((left.delegation_missing_decisions || {})[delegationId]);
|
|
499
|
+
const rightMissing = normalizeStringArray((right.delegation_missing_decisions || {})[delegationId]);
|
|
500
|
+
const leftSet = new Set(leftMissing);
|
|
501
|
+
const newlyMissing = rightMissing.filter((decisionId) => !leftSet.has(decisionId));
|
|
502
|
+
if (newlyMissing.length > 0) {
|
|
503
|
+
regressions.push({
|
|
504
|
+
id: `REG-DELEGATION-MISSING-${String(++counter).padStart(3, '0')}`,
|
|
505
|
+
category: 'delegation',
|
|
506
|
+
severity: 'error',
|
|
507
|
+
message: `Delegation "${delegationId}" is now missing required decisions: ${newlyMissing.join(', ')}`,
|
|
508
|
+
field: `delegation_summary.${delegationId}.missing_decision_ids`,
|
|
509
|
+
left: leftMissing,
|
|
510
|
+
right: rightMissing,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
435
515
|
// Gate regressions: passed/approved -> failed/blocked
|
|
436
516
|
const allGateIds = new Set([...Object.keys(left.phase_gate_status || {}), ...Object.keys(right.phase_gate_status || {})]);
|
|
437
517
|
for (const gateId of allGateIds) {
|
package/src/lib/export.js
CHANGED
|
@@ -454,6 +454,9 @@ export function buildRunExport(startDir = process.cwd()) {
|
|
|
454
454
|
run_id: state?.run_id || null,
|
|
455
455
|
status: state?.status || null,
|
|
456
456
|
phase: state?.phase || null,
|
|
457
|
+
workflow_phase_order: config.routing && Object.keys(config.routing).length > 0
|
|
458
|
+
? Object.keys(config.routing)
|
|
459
|
+
: null,
|
|
457
460
|
provenance: normalizeRunProvenance(state?.provenance),
|
|
458
461
|
inherited_context: state?.inherited_context || null,
|
|
459
462
|
active_turn_ids: activeTurns,
|
|
@@ -640,6 +643,10 @@ export function buildCoordinatorExport(startDir = process.cwd()) {
|
|
|
640
643
|
super_run_id: coordState?.super_run_id || null,
|
|
641
644
|
status: coordState?.status || null,
|
|
642
645
|
phase: coordState?.phase || null,
|
|
646
|
+
workflow_phase_order: rawConfig.routing && typeof rawConfig.routing === 'object'
|
|
647
|
+
&& Object.keys(rawConfig.routing).length > 0
|
|
648
|
+
? Object.keys(rawConfig.routing)
|
|
649
|
+
: null,
|
|
643
650
|
repo_run_statuses: repoRunStatuses,
|
|
644
651
|
barrier_count: barrierCount,
|
|
645
652
|
history_entries: countJsonl(files, '.agentxchain/multirepo/history.jsonl'),
|