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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.100.0",
3
+ "version": "2.101.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ }
@@ -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'),