agentxchain 2.5.0 → 2.7.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,18 +59,20 @@ 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 { verifyProtocolCommand } from '../src/commands/verify.js';
62
+ import { verifyExportCommand, verifyProtocolCommand } from '../src/commands/verify.js';
63
63
  import { kickoffCommand } from '../src/commands/kickoff.js';
64
64
  import { rebindCommand } from '../src/commands/rebind.js';
65
65
  import { branchCommand } from '../src/commands/branch.js';
66
66
  import { migrateCommand } from '../src/commands/migrate.js';
67
67
  import { resumeCommand } from '../src/commands/resume.js';
68
+ import { escalateCommand } from '../src/commands/escalate.js';
68
69
  import { acceptTurnCommand } from '../src/commands/accept-turn.js';
69
70
  import { rejectTurnCommand } from '../src/commands/reject-turn.js';
70
71
  import { stepCommand } from '../src/commands/step.js';
71
72
  import { approveTransitionCommand } from '../src/commands/approve-transition.js';
72
73
  import { approveCompletionCommand } from '../src/commands/approve-completion.js';
73
74
  import { dashboardCommand } from '../src/commands/dashboard.js';
75
+ import { exportCommand } from '../src/commands/export.js';
74
76
  import {
75
77
  pluginInstallCommand,
76
78
  pluginListCommand,
@@ -121,6 +123,13 @@ program
121
123
  .option('-j, --json', 'Output as JSON')
122
124
  .action(statusCommand);
123
125
 
126
+ program
127
+ .command('export')
128
+ .description('Export the governed run audit surface as a single artifact')
129
+ .option('--format <format>', 'Export format (json)', 'json')
130
+ .option('--output <path>', 'Write the export artifact to a file instead of stdout')
131
+ .action(exportCommand);
132
+
124
133
  program
125
134
  .command('start')
126
135
  .description('Launch legacy v3 agents in your IDE')
@@ -231,6 +240,13 @@ verifyCmd
231
240
  .option('--format <format>', 'Output format: text or json', 'text')
232
241
  .action(verifyProtocolCommand);
233
242
 
243
+ verifyCmd
244
+ .command('export')
245
+ .description('Verify an AgentXchain export artifact against its embedded file bytes and summaries')
246
+ .option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
247
+ .option('--format <format>', 'Output format: text or json', 'text')
248
+ .action(verifyExportCommand);
249
+
234
250
  program
235
251
  .command('migrate')
236
252
  .description('Migrate a legacy v3 project to governed format')
@@ -245,6 +261,15 @@ program
245
261
  .option('--turn <id>', 'Target a specific retained turn when multiple exist')
246
262
  .action(resumeCommand);
247
263
 
264
+ program
265
+ .command('escalate')
266
+ .description('Raise an operator escalation and block the governed run intentionally')
267
+ .requiredOption('--reason <reason>', 'Operator escalation summary')
268
+ .option('--detail <detail>', 'Longer escalation detail for status and ledger surfaces')
269
+ .option('--action <action>', 'Override the default recovery action string')
270
+ .option('--turn <id>', 'Target a specific active turn when multiple turns exist')
271
+ .action(escalateCommand);
272
+
248
273
  program
249
274
  .command('accept-turn')
250
275
  .description('Accept the currently staged governed turn result')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.5.0",
3
+ "version": "2.7.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
3
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
4
+ import { raiseOperatorEscalation } from '../lib/governed-state.js';
5
+
6
+ export async function escalateCommand(opts) {
7
+ const context = loadProjectContext();
8
+ if (!context) {
9
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
10
+ process.exit(1);
11
+ }
12
+
13
+ const { root, config } = context;
14
+
15
+ if (config.protocol_mode !== 'governed') {
16
+ console.log(chalk.red('The escalate command is only available for governed projects.'));
17
+ console.log(chalk.dim('Legacy projects use: agentxchain claim / release'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const state = loadProjectState(root, config);
22
+ if (!state) {
23
+ console.log(chalk.red('No governed state.json found. Run `agentxchain init --governed` first.'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const result = raiseOperatorEscalation(root, config, {
28
+ reason: opts.reason,
29
+ detail: opts.detail,
30
+ action: opts.action,
31
+ turnId: opts.turn,
32
+ });
33
+ if (!result.ok) {
34
+ console.log(chalk.red(result.error));
35
+ process.exit(1);
36
+ }
37
+
38
+ const recovery = deriveRecoveryDescriptor(result.state);
39
+
40
+ console.log('');
41
+ console.log(chalk.yellow(' Run Escalated'));
42
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
43
+ console.log('');
44
+ console.log(` ${chalk.dim('Reason:')} ${result.escalation.reason}`);
45
+ if (result.escalation.detail && result.escalation.detail !== result.escalation.reason) {
46
+ console.log(` ${chalk.dim('Detail:')} ${result.escalation.detail}`);
47
+ }
48
+ console.log(` ${chalk.dim('Blocked:')} ${result.state.blocked_on}`);
49
+ console.log(` ${chalk.dim('Source:')} operator`);
50
+ console.log(` ${chalk.dim('Turn:')} ${result.escalation.from_turn_id || 'none retained'}`);
51
+ if (result.escalation.from_role) {
52
+ console.log(` ${chalk.dim('Role:')} ${result.escalation.from_role}`);
53
+ }
54
+ console.log('');
55
+
56
+ if (recovery) {
57
+ console.log(` ${chalk.dim('Typed:')} ${recovery.typed_reason}`);
58
+ console.log(` ${chalk.dim('Owner:')} ${recovery.owner}`);
59
+ console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
60
+ console.log(` ${chalk.dim('Retained:')} ${recovery.turn_retained ? 'yes' : 'no'}`);
61
+ }
62
+ console.log('');
63
+ }
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ import { buildRunExport, buildCoordinatorExport } from '../lib/export.js';
5
+ import { COORDINATOR_CONFIG_FILE } from '../lib/coordinator-config.js';
6
+ import { safeWriteJson } from '../lib/safe-write.js';
7
+
8
+ function detectExportKind(cwd) {
9
+ // Governed project takes priority (agentxchain.json)
10
+ if (existsSync(join(cwd, 'agentxchain.json'))) {
11
+ return 'governed';
12
+ }
13
+ // Coordinator workspace (agentxchain-multi.json)
14
+ if (existsSync(join(cwd, COORDINATOR_CONFIG_FILE))) {
15
+ return 'coordinator';
16
+ }
17
+ return null;
18
+ }
19
+
20
+ export async function exportCommand(options) {
21
+ const format = options.format || 'json';
22
+ if (format !== 'json') {
23
+ console.error(`Unsupported export format "${format}". Only "json" is supported in this slice.`);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+
28
+ const cwd = process.cwd();
29
+ const kind = detectExportKind(cwd);
30
+
31
+ let result;
32
+ try {
33
+ if (kind === 'governed') {
34
+ result = buildRunExport(cwd);
35
+ } else if (kind === 'coordinator') {
36
+ result = buildCoordinatorExport(cwd);
37
+ } else {
38
+ result = {
39
+ ok: false,
40
+ error: 'No governed project or coordinator workspace found. Run this inside an AgentXchain governed project or coordinator workspace.',
41
+ };
42
+ }
43
+ } catch (error) {
44
+ console.error(error.message || String(error));
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+
49
+ if (!result.ok) {
50
+ console.error(result.error);
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+
55
+ if (options.output) {
56
+ const outputPath = resolve(cwd, options.output);
57
+ safeWriteJson(outputPath, result.export);
58
+ console.log(`Exported ${kind === 'coordinator' ? 'coordinator workspace' : 'governed run'} audit to ${options.output}`);
59
+ return;
60
+ }
61
+
62
+ console.log(JSON.stringify(result.export, null, 2));
63
+ }
@@ -24,6 +24,7 @@ import {
24
24
  markRunBlocked,
25
25
  getActiveTurns,
26
26
  getActiveTurnCount,
27
+ reactivateGovernedRun,
27
28
  STATE_PATH,
28
29
  } from '../lib/governed-state.js';
29
30
  import { writeDispatchBundle, getDispatchTurnDir, getTurnStagingResultPath } from '../lib/dispatch-bundle.js';
@@ -154,6 +155,62 @@ export async function resumeCommand(opts) {
154
155
  }
155
156
  }
156
157
 
158
+ if (state.status === 'blocked' && activeCount > 0) {
159
+ let retainedTurn = null;
160
+ if (opts.turn) {
161
+ retainedTurn = activeTurns[opts.turn];
162
+ if (!retainedTurn) {
163
+ console.log(chalk.red(`No active turn found for --turn ${opts.turn}`));
164
+ process.exit(1);
165
+ }
166
+ } else if (activeCount > 1) {
167
+ console.log(chalk.red('Multiple retained turns exist. Use --turn <id> to specify which to re-dispatch.'));
168
+ for (const turn of Object.values(activeTurns)) {
169
+ console.log(` ${chalk.yellow('●')} ${turn.turn_id} — ${chalk.bold(turn.assigned_role)} (${turn.status})`);
170
+ }
171
+ console.log('');
172
+ console.log(chalk.dim('Example: agentxchain resume --turn <turn_id>'));
173
+ process.exit(1);
174
+ } else {
175
+ retainedTurn = Object.values(activeTurns)[0];
176
+ }
177
+
178
+ if (retainedTurn.status === 'conflicted') {
179
+ console.log(chalk.red(`Turn ${retainedTurn.turn_id} is conflicted. Resolve the conflict before resuming.`));
180
+ process.exit(1);
181
+ }
182
+
183
+ console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
184
+ console.log(` Role: ${retainedTurn.assigned_role}`);
185
+ console.log(` Attempt: ${retainedTurn.attempt}`);
186
+ console.log('');
187
+
188
+ const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
189
+ if (!reactivated.ok) {
190
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
191
+ process.exit(1);
192
+ }
193
+ state = reactivated.state;
194
+
195
+ const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
196
+ if (!bundleResult.ok) {
197
+ console.log(chalk.red(`Failed to write dispatch bundle: ${bundleResult.error}`));
198
+ process.exit(1);
199
+ }
200
+ printDispatchBundleWarnings(bundleResult);
201
+
202
+ const hooksConfig = config.hooks || {};
203
+ if (hooksConfig.after_dispatch?.length > 0) {
204
+ const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, retainedTurn, config);
205
+ if (!afterDispatchResult.ok) {
206
+ process.exit(1);
207
+ }
208
+ }
209
+
210
+ printDispatchSummary(state, config, retainedTurn);
211
+ return;
212
+ }
213
+
157
214
  // §47: idle + no run_id → initialize new run
158
215
  if (state.status === 'idle' && !state.run_id) {
159
216
  const initResult = initializeGovernedRun(root, config);
@@ -165,10 +222,22 @@ export async function resumeCommand(opts) {
165
222
  console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
166
223
  }
167
224
 
225
+ // §47: paused + run_id exists → resume same run
226
+ if (state.status === 'blocked' && state.run_id) {
227
+ const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
228
+ if (!reactivated.ok) {
229
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
230
+ process.exit(1);
231
+ }
232
+ state = reactivated.state;
233
+ console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
234
+ }
235
+
168
236
  // §47: paused + run_id exists → resume same run
169
237
  if (state.status === 'paused' && state.run_id) {
170
238
  state.status = 'active';
171
239
  state.blocked_on = null;
240
+ state.blocked_reason = null;
172
241
  state.escalation = null;
173
242
  safeWriteJson(statePath, state);
174
243
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
@@ -203,7 +272,7 @@ export async function resumeCommand(opts) {
203
272
  const hooksConfig = config.hooks || {};
204
273
  const turn = state.current_turn;
205
274
  if (hooksConfig.after_dispatch?.length > 0) {
206
- const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, turn);
275
+ const afterDispatchResult = runAfterDispatchHooks(root, hooksConfig, state, turn, config);
207
276
  if (!afterDispatchResult.ok) {
208
277
  process.exit(1);
209
278
  }
@@ -259,7 +328,7 @@ function resolveTargetRole(opts, state, config) {
259
328
  return null;
260
329
  }
261
330
 
262
- function runAfterDispatchHooks(root, hooksConfig, state, turn) {
331
+ function runAfterDispatchHooks(root, hooksConfig, state, turn, config) {
263
332
  const turnId = turn.turn_id;
264
333
  const roleId = turn.assigned_role;
265
334
 
@@ -300,6 +369,7 @@ function runAfterDispatchHooks(root, hooksConfig, state, turn) {
300
369
  detail,
301
370
  },
302
371
  turnId,
372
+ notificationConfig: config,
303
373
  });
304
374
 
305
375
  printDispatchHookFailure({
@@ -32,6 +32,7 @@ import {
32
32
  markRunBlocked,
33
33
  getActiveTurnCount,
34
34
  getActiveTurns,
35
+ reactivateGovernedRun,
35
36
  STATE_PATH,
36
37
  } from '../lib/governed-state.js';
37
38
  import { getMaxConcurrentTurns } from '../lib/normalized-config.js';
@@ -203,8 +204,12 @@ export async function stepCommand(opts) {
203
204
  }
204
205
 
205
206
  console.log(chalk.yellow(`Re-dispatching blocked turn: ${targetTurn.turn_id}`));
206
- state = clearBlockedState(state);
207
- safeWriteJson(join(root, STATE_PATH), state);
207
+ const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
208
+ if (!reactivated.ok) {
209
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
210
+ process.exit(1);
211
+ }
212
+ state = reactivated.state;
208
213
  skipAssignment = true;
209
214
 
210
215
  const bundleResult = writeDispatchBundle(root, state, config);
@@ -251,8 +256,12 @@ export async function stepCommand(opts) {
251
256
 
252
257
  // paused → resume
253
258
  if (!skipAssignment && state.status === 'blocked' && state.run_id) {
254
- state = clearBlockedState(state);
255
- safeWriteJson(join(root, STATE_PATH), state);
259
+ const reactivated = reactivateGovernedRun(root, state, { via: 'step', notificationConfig: config });
260
+ if (!reactivated.ok) {
261
+ console.log(chalk.red(`Failed to reactivate blocked run: ${reactivated.error}`));
262
+ process.exit(1);
263
+ }
264
+ state = reactivated.state;
256
265
  console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
257
266
  }
258
267
 
@@ -332,6 +341,7 @@ export async function stepCommand(opts) {
332
341
  hookResults: afterDispatchHooks,
333
342
  phase: 'after_dispatch',
334
343
  defaultDetail: `after_dispatch hook blocked dispatch for turn ${turn.turn_id}`,
344
+ config,
335
345
  });
336
346
  printLifecycleHookFailure('Dispatch Blocked By Hook', blocked.result, {
337
347
  turnId: turn.turn_id,
@@ -382,6 +392,7 @@ export async function stepCommand(opts) {
382
392
  },
383
393
  turnId: turn.turn_id,
384
394
  hooksConfig,
395
+ notificationConfig: config,
385
396
  });
386
397
  if (blocked.ok) {
387
398
  state = blocked.state;
@@ -462,6 +473,7 @@ export async function stepCommand(opts) {
462
473
  },
463
474
  turnId: turn.turn_id,
464
475
  hooksConfig,
476
+ notificationConfig: config,
465
477
  });
466
478
  if (blocked.ok) {
467
479
  state = blocked.state;
@@ -530,6 +542,7 @@ export async function stepCommand(opts) {
530
542
  },
531
543
  turnId: turn.turn_id,
532
544
  hooksConfig,
545
+ notificationConfig: config,
533
546
  });
534
547
  if (blocked.ok) {
535
548
  state = blocked.state;
@@ -556,6 +569,7 @@ export async function stepCommand(opts) {
556
569
  },
557
570
  turnId: turn.turn_id,
558
571
  hooksConfig,
572
+ notificationConfig: config,
559
573
  });
560
574
  if (blocked.ok) {
561
575
  state = blocked.state;
@@ -643,6 +657,7 @@ export async function stepCommand(opts) {
643
657
  hookResults: beforeValidationHooks,
644
658
  phase: 'before_validation',
645
659
  defaultDetail: `before_validation hook blocked validation for turn ${turn.turn_id}`,
660
+ config,
646
661
  });
647
662
  printLifecycleHookFailure('Validation Blocked By Hook', blocked.result, {
648
663
  turnId: turn.turn_id,
@@ -674,6 +689,7 @@ export async function stepCommand(opts) {
674
689
  hookResults: afterValidationHooks,
675
690
  phase: 'after_validation',
676
691
  defaultDetail: `after_validation hook blocked acceptance for turn ${turn.turn_id}`,
692
+ config,
677
693
  });
678
694
  printLifecycleHookFailure('Validation Blocked By Hook', blocked.result, {
679
695
  turnId: turn.turn_id,
@@ -758,7 +774,7 @@ function loadHookStagedTurn(root, stagingRel) {
758
774
  }
759
775
  }
760
776
 
761
- function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail }) {
777
+ function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail, config }) {
762
778
  const hookName = hookResults.blocker?.hook_name
763
779
  || hookResults.results?.find((entry) => entry.hook_name)?.hook_name
764
780
  || 'unknown';
@@ -777,6 +793,7 @@ function blockStepForHookIssue(root, turn, { hookResults, phase, defaultDetail }
777
793
  detail,
778
794
  },
779
795
  turnId: turn.turn_id,
796
+ notificationConfig: config,
780
797
  });
781
798
 
782
799
  return {
@@ -860,16 +877,6 @@ function resolveTargetRole(opts, state, config) {
860
877
  return null;
861
878
  }
862
879
 
863
- function clearBlockedState(state) {
864
- return {
865
- ...state,
866
- status: 'active',
867
- blocked_on: null,
868
- blocked_reason: null,
869
- escalation: null,
870
- };
871
- }
872
-
873
880
  function printRecoverySummary(state, heading) {
874
881
  const recovery = deriveRecoveryDescriptor(state);
875
882
  console.log(chalk.yellow(heading));
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { resolve } from 'node:path';
3
+ import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
3
4
  import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
4
5
 
5
6
  export async function verifyProtocolCommand(opts) {
@@ -35,6 +36,38 @@ export async function verifyProtocolCommand(opts) {
35
36
  process.exit(result.exitCode);
36
37
  }
37
38
 
39
+ export async function verifyExportCommand(opts) {
40
+ const format = opts.format || 'text';
41
+ const loaded = loadExportArtifact(opts.input || '-', process.cwd());
42
+
43
+ if (!loaded.ok) {
44
+ if (format === 'json') {
45
+ console.log(JSON.stringify({
46
+ overall: 'error',
47
+ input: loaded.input,
48
+ message: loaded.error,
49
+ }, null, 2));
50
+ } else {
51
+ console.log(chalk.red(`Export verification failed: ${loaded.error}`));
52
+ }
53
+ process.exit(2);
54
+ }
55
+
56
+ const result = verifyExportArtifact(loaded.artifact);
57
+ const report = {
58
+ ...result.report,
59
+ input: loaded.input,
60
+ };
61
+
62
+ if (format === 'json') {
63
+ console.log(JSON.stringify(report, null, 2));
64
+ } else {
65
+ printExportReport(report);
66
+ }
67
+
68
+ process.exit(result.ok ? 0 : 1);
69
+ }
70
+
38
71
  function printProtocolReport(report) {
39
72
  console.log('');
40
73
  console.log(chalk.bold(' AgentXchain Protocol Conformance'));
@@ -74,3 +107,30 @@ function printProtocolReport(report) {
74
107
 
75
108
  console.log('');
76
109
  }
110
+
111
+ function printExportReport(report) {
112
+ console.log('');
113
+ console.log(chalk.bold(' AgentXchain Export Verification'));
114
+ console.log(chalk.dim(' ' + '─'.repeat(43)));
115
+ console.log(chalk.dim(` Input: ${report.input}`));
116
+ console.log(chalk.dim(` Export kind: ${report.export_kind || 'unknown'}`));
117
+ console.log(chalk.dim(` Schema: ${report.schema_version || 'unknown'}`));
118
+ console.log('');
119
+
120
+ const overallLabel = report.overall === 'pass'
121
+ ? chalk.green('PASS')
122
+ : report.overall === 'fail'
123
+ ? chalk.red('FAIL')
124
+ : chalk.red('ERROR');
125
+ console.log(` Overall: ${overallLabel}`);
126
+ console.log(chalk.dim(` Files verified: ${report.file_count}`));
127
+ if (report.repo_count) {
128
+ console.log(chalk.dim(` Embedded repos: ${report.repo_count}`));
129
+ }
130
+
131
+ for (const error of report.errors || []) {
132
+ console.log(chalk.red(` ✗ ${error}`));
133
+ }
134
+
135
+ console.log('');
136
+ }
@@ -65,12 +65,16 @@ export function deriveRecoveryDescriptor(state) {
65
65
  }
66
66
 
67
67
  if (state.blocked_on.startsWith('escalation:')) {
68
+ const isOperatorEscalation = state.blocked_on.startsWith('escalation:operator:') || state.escalation?.source === 'operator';
69
+ const recoveryAction = turnRetained
70
+ ? 'Resolve the escalation, then run agentxchain step --resume'
71
+ : 'Resolve the escalation, then run agentxchain step';
68
72
  return {
69
- typed_reason: 'retries_exhausted',
73
+ typed_reason: isOperatorEscalation ? 'operator_escalation' : 'retries_exhausted',
70
74
  owner: 'human',
71
- recovery_action: 'Resolve the escalation, then run agentxchain step --resume',
75
+ recovery_action: recoveryAction,
72
76
  turn_retained: turnRetained,
73
- detail: state.blocked_on,
77
+ detail: state.escalation?.detail || state.escalation?.reason || state.blocked_on,
74
78
  };
75
79
  }
76
80