agentxchain 2.135.0 → 2.136.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.
@@ -123,7 +123,7 @@ import { historyCommand } from '../src/commands/history.js';
123
123
  import { decisionsCommand } from '../src/commands/decisions.js';
124
124
  import { diffCommand } from '../src/commands/diff.js';
125
125
  import { eventsCommand } from '../src/commands/events.js';
126
- import { connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
126
+ import { connectorCapabilitiesCommand, connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
127
127
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
128
128
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
129
129
  import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
@@ -299,6 +299,13 @@ connectorCmd
299
299
  .option('--timeout <ms>', 'Per-probe timeout in milliseconds', '8000')
300
300
  .action(connectorCheckCommand);
301
301
 
302
+ connectorCmd
303
+ .command('capabilities [runtime_id]')
304
+ .description('Show merged capability contract for a runtime or all runtimes (machine-readable handshake)')
305
+ .option('-j, --json', 'Output as JSON')
306
+ .option('--all', 'Show capabilities for all configured runtimes')
307
+ .action(connectorCapabilitiesCommand);
308
+
302
309
  connectorCmd
303
310
  .command('validate <runtime_id>')
304
311
  .description('Dispatch one synthetic governed turn through a runtime and validate the staged turn result')
@@ -530,6 +537,8 @@ missionPlanCmd
530
537
  .option('-m, --mission <mission_id>', 'Explicit mission ID')
531
538
  .option('--max-waves <n>', 'Maximum number of dependency waves (default: 10)')
532
539
  .option('--continue-on-failure', 'Skip failed workstreams and keep launching ready ones')
540
+ .option('--auto-retry', 'Coordinator-only: retry one retryable repo-local failure within the same autopilot session')
541
+ .option('--max-retries <n>', 'Coordinator-only: maximum auto-retries per workstream/repo pair in one autopilot session (default: 1)')
533
542
  .option('--cooldown <seconds>', 'Pause between waves in seconds (default: 5)')
534
543
  .option('--auto-approve', 'Auto-approve run gates during execution')
535
544
  .option('-j, --json', 'Output as JSON')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.135.0",
3
+ "version": "2.136.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  ".": "./bin/agentxchain.js",
11
11
  "./adapter-interface": "./src/lib/adapter-interface.js",
12
12
  "./runner-interface": "./src/lib/runner-interface.js",
13
- "./run-loop": "./src/lib/run-loop.js"
13
+ "./run-loop": "./src/lib/run-loop.js",
14
+ "./schemas/agentxchain-config": "./src/lib/schemas/agentxchain-config.schema.json",
15
+ "./schemas/connector-capabilities-output": "./src/lib/schemas/connector-capabilities-output.schema.json"
14
16
  },
15
17
  "files": [
16
18
  "bin/",
@@ -154,6 +154,8 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
154
154
  test/release-identity-hardening.test.js
155
155
  test/normalized-config.test.js
156
156
  test/conformance.test.js
157
+ test/beta-scenario-emission-guard.test.js
158
+ test/claim-reality-preflight.test.js
157
159
  test/beta-tester-scenarios/*.test.js
158
160
  )
159
161
  GATE_TEST_ARGS=()
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { loadProjectContext } from '../lib/config.js';
4
4
  import { DEFAULT_VALIDATE_TIMEOUT_MS, validateConfiguredConnector } from '../lib/connector-validate.js';
5
5
  import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
6
+ import { getRuntimeCapabilityContract, getRoleRuntimeCapabilityContract, getCapabilityDeclarationWarnings } from '../lib/runtime-capabilities.js';
6
7
 
7
8
  function printJson(result, exitCode) {
8
9
  console.log(JSON.stringify(result, null, 2));
@@ -192,6 +193,141 @@ function printValidateText(result, exitCode) {
192
193
  process.exit(exitCode);
193
194
  }
194
195
 
196
+ function buildCapabilityReport(runtimeId, runtime, roles) {
197
+ const mergedContract = getRuntimeCapabilityContract(runtime);
198
+ const declaredCapabilities = (runtime.capabilities && typeof runtime.capabilities === 'object' && !Array.isArray(runtime.capabilities))
199
+ ? { ...runtime.capabilities }
200
+ : {};
201
+ const declarationWarnings = getCapabilityDeclarationWarnings(runtime);
202
+
203
+ const roleBindings = [];
204
+ for (const [roleId, role] of Object.entries(roles)) {
205
+ const boundRuntime = role.runtime_id || role.runtime;
206
+ if (boundRuntime !== runtimeId) continue;
207
+ const roleContract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
208
+ roleBindings.push({
209
+ role_id: roleContract.role_id,
210
+ role_write_authority: roleContract.role_write_authority,
211
+ effective_write_path: roleContract.effective_write_path,
212
+ workflow_artifact_ownership: roleContract.workflow_artifact_ownership,
213
+ notes: roleContract.notes,
214
+ });
215
+ }
216
+
217
+ return {
218
+ runtime_id: runtimeId,
219
+ runtime_type: runtime.type || 'unknown',
220
+ declared_capabilities: declaredCapabilities,
221
+ merged_contract: mergedContract,
222
+ declaration_warnings: declarationWarnings,
223
+ role_bindings: roleBindings,
224
+ };
225
+ }
226
+
227
+ function printCapabilitiesText(report) {
228
+ console.log('');
229
+ console.log(chalk.bold(` ${report.runtime_id}`) + chalk.dim(` (${report.runtime_type})`));
230
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
231
+
232
+ const c = report.merged_contract;
233
+ console.log(` ${chalk.dim('Transport:')} ${c.transport}`);
234
+ console.log(` ${chalk.dim('Write files:')} ${c.can_write_files}`);
235
+ console.log(` ${chalk.dim('Proposals:')} ${c.proposal_support}`);
236
+ console.log(` ${chalk.dim('Artifact ownership:')} ${c.workflow_artifact_ownership}`);
237
+ console.log(` ${chalk.dim('Local binary:')} ${c.requires_local_binary ? 'yes' : 'no'}`);
238
+
239
+ if (Object.keys(report.declared_capabilities).length > 0) {
240
+ console.log('');
241
+ console.log(` ${chalk.dim('Declared overrides:')}`);
242
+ for (const [k, v] of Object.entries(report.declared_capabilities)) {
243
+ console.log(` ${k}: ${v}`);
244
+ }
245
+ }
246
+
247
+ if (report.declaration_warnings.length > 0) {
248
+ console.log('');
249
+ for (const w of report.declaration_warnings) {
250
+ console.log(` ${chalk.yellow('!')} ${w}`);
251
+ }
252
+ }
253
+
254
+ if (report.role_bindings.length > 0) {
255
+ console.log('');
256
+ console.log(` ${chalk.dim('Role bindings:')}`);
257
+ for (const rb of report.role_bindings) {
258
+ const badge = rb.effective_write_path.startsWith('invalid') ? chalk.red('INVALID') : chalk.green('OK');
259
+ console.log(` ${badge} ${rb.role_id} (${rb.role_write_authority}) -> ${rb.effective_write_path}`);
260
+ for (const note of rb.notes) {
261
+ console.log(` ${chalk.dim(note)}`);
262
+ }
263
+ }
264
+ }
265
+ }
266
+
267
+ export async function connectorCapabilitiesCommand(runtimeId, options = {}) {
268
+ const context = loadProjectContext();
269
+ if (!context) {
270
+ const payload = { error: 'No governed agentxchain.json found.' };
271
+ if (options.json) { printJson(payload, 2); return; }
272
+ console.error(chalk.red('No governed agentxchain.json found. Run this inside a governed project.'));
273
+ process.exit(2);
274
+ }
275
+
276
+ const config = context.config;
277
+ const runtimes = config.runtimes || {};
278
+ const roles = config.roles || {};
279
+
280
+ if (options.all) {
281
+ const reports = [];
282
+ for (const [id, runtime] of Object.entries(runtimes)) {
283
+ reports.push(buildCapabilityReport(id, runtime, roles));
284
+ }
285
+ const payload = { runtimes: reports };
286
+ if (options.json) { printJson(payload, 0); return; }
287
+
288
+ console.log('');
289
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
290
+ console.log(chalk.dim(' ' + '='.repeat(44)));
291
+ if (reports.length === 0) {
292
+ console.log(' No runtimes configured.');
293
+ } else {
294
+ for (const r of reports) { printCapabilitiesText(r); }
295
+ }
296
+ console.log('');
297
+ process.exit(0);
298
+ }
299
+
300
+ if (!runtimeId) {
301
+ const payload = { error: 'Runtime ID required. Use --all to list all runtimes.', available_runtimes: Object.keys(runtimes) };
302
+ if (options.json) { printJson(payload, 2); return; }
303
+ console.error(chalk.red('Runtime ID required. Use --all to list all runtimes.'));
304
+ if (Object.keys(runtimes).length > 0) {
305
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
306
+ }
307
+ process.exit(2);
308
+ }
309
+
310
+ if (!runtimes[runtimeId]) {
311
+ const payload = { error: `Runtime "${runtimeId}" not found.`, available_runtimes: Object.keys(runtimes) };
312
+ if (options.json) { printJson(payload, 2); return; }
313
+ console.error(chalk.red(`Runtime "${runtimeId}" not found.`));
314
+ if (Object.keys(runtimes).length > 0) {
315
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
316
+ }
317
+ process.exit(2);
318
+ }
319
+
320
+ const report = buildCapabilityReport(runtimeId, runtimes[runtimeId], roles);
321
+ if (options.json) { printJson(report, 0); return; }
322
+
323
+ console.log('');
324
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
325
+ console.log(chalk.dim(' ' + '='.repeat(44)));
326
+ printCapabilitiesText(report);
327
+ console.log('');
328
+ process.exit(0);
329
+ }
330
+
195
331
  export async function connectorValidateCommand(runtimeId, options = {}) {
196
332
  const context = loadProjectContext();
197
333
  if (!context) {
@@ -190,6 +190,121 @@ function buildCoordinatorProjectionWarning(message) {
190
190
  };
191
191
  }
192
192
 
193
+ function formatAutoRetryBudgetError(workstreamId, repoId, maxRetries) {
194
+ return `Auto-retry budget exhausted for ${workstreamId}/${repoId} (max ${maxRetries} per autopilot session).`;
195
+ }
196
+
197
+ async function executeCoordinatorRetryRun(root, mission, workstreamId, coordinatorConfig, opts = {}) {
198
+ const retry = retryCoordinatorWorkstream(
199
+ root,
200
+ mission,
201
+ opts.planId,
202
+ workstreamId,
203
+ coordinatorConfig,
204
+ {
205
+ reason: opts.reason || `mission plan retry ${workstreamId}`,
206
+ },
207
+ );
208
+ if (!retry.ok) {
209
+ return { ok: false, error: retry.error, plan: opts.plan || null };
210
+ }
211
+
212
+ const executor = opts._executeGovernedRun || executeGovernedRun;
213
+ const childLog = opts.json ? noop : (opts._log || console.log);
214
+ const repoContext = loadProjectContext(retry.retryResult.repo_path);
215
+ if (!repoContext) {
216
+ return {
217
+ ok: false,
218
+ error: `Cannot load project context for retried repo at ${retry.retryResult.repo_path}.`,
219
+ plan: retry.plan,
220
+ retryResult: retry.retryResult,
221
+ launchRecord: retry.launchRecord,
222
+ };
223
+ }
224
+
225
+ const runOpts = {
226
+ autoApprove: !!opts.autoApprove,
227
+ log: childLog,
228
+ provenance: {
229
+ trigger: opts.trigger || 'manual',
230
+ created_by: 'operator',
231
+ trigger_reason: opts.triggerReasonPrefix
232
+ ? `${opts.triggerReasonPrefix} repo:${retry.retryResult.repo_id}`
233
+ : `mission:${mission.mission_id} workstream:${workstreamId} coordinator-retry:${retry.retryResult.repo_id}`,
234
+ },
235
+ };
236
+
237
+ const retryWarnings = [];
238
+ let execution;
239
+ try {
240
+ execution = await executor(repoContext, runOpts);
241
+ } catch (error) {
242
+ const syncedRetryFailure = synchronizeCoordinatorPlanState(root, mission, retry.plan);
243
+ return {
244
+ ok: false,
245
+ error: `Coordinator retry execution failed: ${error.message}`,
246
+ plan: syncedRetryFailure.ok ? syncedRetryFailure.plan : retry.plan,
247
+ retryResult: retry.retryResult,
248
+ launchRecord: retry.launchRecord,
249
+ warnings: retryWarnings,
250
+ reconciliation_required: false,
251
+ exit_code: 1,
252
+ };
253
+ }
254
+
255
+ if ((execution?.exitCode ?? 0) === 0) {
256
+ const projection = projectAcceptedCoordinatorTurn(
257
+ mission.coordinator.workspace_path,
258
+ coordinatorConfig,
259
+ retry.retryResult.repo_id,
260
+ retry.retryResult.reissued_turn_id,
261
+ workstreamId,
262
+ loadCoordinatorState(mission.coordinator.workspace_path),
263
+ );
264
+ if (!projection.ok) {
265
+ const warning = buildCoordinatorProjectionWarning(projection.error);
266
+ retryWarnings.push(warning);
267
+ console.error(chalk.yellow(`Coordinator retry projection warning: ${warning.message}`));
268
+ emitRunEvent(mission.coordinator.workspace_path, 'coordinator_retry_projection_warning', {
269
+ run_id: mission.coordinator.super_run_id,
270
+ phase: coordinatorConfig?.workstreams?.[workstreamId]?.phase || null,
271
+ status: 'active',
272
+ payload: {
273
+ workstream_id: workstreamId,
274
+ repo_id: retry.retryResult.repo_id,
275
+ reissued_turn_id: retry.retryResult.reissued_turn_id,
276
+ warning_code: warning.code,
277
+ warning_message: warning.message,
278
+ },
279
+ });
280
+ }
281
+ }
282
+
283
+ const syncedRetry = synchronizeCoordinatorPlanState(root, mission, retry.plan);
284
+ const retriedPlan = syncedRetry.ok ? syncedRetry.plan : retry.plan;
285
+ const retriedWorkstream = retriedPlan.workstreams.find((ws) => ws.workstream_id === workstreamId);
286
+ const retriedLaunchRecord = retriedPlan.launch_records.find(
287
+ (record) => record.workstream_id === workstreamId && record.dispatch_mode === 'coordinator',
288
+ ) || retry.launchRecord;
289
+ const retryCount = retriedLaunchRecord?.repo_dispatches?.filter(
290
+ (dispatch) => dispatch.repo_id === retry.retryResult.repo_id && dispatch.is_retry,
291
+ ).length || 0;
292
+
293
+ const success = (execution?.exitCode ?? 0) === 0;
294
+ return {
295
+ ok: success,
296
+ error: success ? null : `Coordinator retry execution ended with exit code ${execution?.exitCode ?? 1}.`,
297
+ plan: retriedPlan,
298
+ workstream: retriedWorkstream,
299
+ launchRecord: retriedLaunchRecord,
300
+ retryResult: retry.retryResult,
301
+ warnings: retryWarnings,
302
+ reconciliation_required: retryWarnings.length > 0,
303
+ exit_code: execution?.exitCode ?? 0,
304
+ retry_count: retryCount,
305
+ };
306
+ }
307
+
193
308
  export async function missionListCommand(opts) {
194
309
  const root = findProjectRoot(opts.dir || process.cwd());
195
310
  if (!root) {
@@ -608,100 +723,28 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
608
723
  }
609
724
 
610
725
  if (opts.retry) {
611
- const retry = retryCoordinatorWorkstream(
726
+ const retryExecution = await executeCoordinatorRetryRun(
612
727
  root,
613
728
  mission,
614
- plan.plan_id,
615
729
  opts.workstream,
616
730
  coordinatorConfigResult.config,
617
731
  {
732
+ ...opts,
733
+ planId: plan.plan_id,
734
+ plan,
618
735
  reason: `mission plan retry ${opts.workstream}`,
619
- },
620
- );
621
- if (!retry.ok) {
622
- console.error(chalk.red(retry.error));
623
- process.exit(1);
624
- }
625
-
626
- const executor = opts._executeGovernedRun || executeGovernedRun;
627
- const childLog = opts.json ? noop : (opts._log || console.log);
628
- const repoContext = loadProjectContext(retry.retryResult.repo_path);
629
- if (!repoContext) {
630
- console.error(chalk.red(`Cannot load project context for retried repo at ${retry.retryResult.repo_path}.`));
631
- process.exit(1);
632
- }
633
-
634
- const runOpts = {
635
- autoApprove: !!opts.autoApprove,
636
- log: childLog,
637
- provenance: {
638
736
  trigger: 'manual',
639
- created_by: 'operator',
640
- trigger_reason: `mission:${mission.mission_id} workstream:${opts.workstream} coordinator-retry:${retry.retryResult.repo_id}`,
737
+ triggerReasonPrefix: `mission:${mission.mission_id} workstream:${opts.workstream} coordinator-retry`,
641
738
  },
642
- };
643
-
644
- let execution;
645
- const retryWarnings = [];
646
- try {
647
- execution = await executor(repoContext, runOpts);
648
- } catch (error) {
649
- const syncedRetryFailure = synchronizeCoordinatorPlanState(root, mission, retry.plan);
650
- console.error(chalk.red(`Coordinator retry execution failed: ${error.message}`));
651
- if (opts.json) {
652
- console.log(JSON.stringify({
653
- dispatch_mode: 'coordinator',
654
- retry: true,
655
- mission_id: mission.mission_id,
656
- plan_id: retry.plan.plan_id,
657
- workstream_id: opts.workstream,
658
- repo_id: retry.retryResult.repo_id,
659
- retried_repo_turn_id: retry.retryResult.failed_turn_id,
660
- repo_turn_id: retry.retryResult.reissued_turn_id,
661
- workstream_status: syncedRetryFailure.ok
662
- ? syncedRetryFailure.plan.workstreams.find((ws) => ws.workstream_id === opts.workstream)?.launch_status || 'needs_attention'
663
- : 'needs_attention',
664
- launch_record: retry.launchRecord,
665
- error: error.message,
666
- }, null, 2));
667
- }
739
+ );
740
+ if (!retryExecution.retryResult) {
741
+ console.error(chalk.red(retryExecution.error));
668
742
  process.exit(1);
669
743
  }
670
744
 
671
- if ((execution?.exitCode ?? 0) === 0) {
672
- const projection = projectAcceptedCoordinatorTurn(
673
- mission.coordinator.workspace_path,
674
- coordinatorConfigResult.config,
675
- retry.retryResult.repo_id,
676
- retry.retryResult.reissued_turn_id,
677
- opts.workstream,
678
- loadCoordinatorState(mission.coordinator.workspace_path),
679
- );
680
- if (!projection.ok) {
681
- const warning = buildCoordinatorProjectionWarning(projection.error);
682
- retryWarnings.push(warning);
683
- console.error(chalk.yellow(`Coordinator retry projection warning: ${warning.message}`));
684
- emitRunEvent(mission.coordinator.workspace_path, 'coordinator_retry_projection_warning', {
685
- run_id: mission.coordinator.super_run_id,
686
- phase: coordinatorConfigResult.config?.workstreams?.[opts.workstream]?.phase || null,
687
- status: 'active',
688
- payload: {
689
- workstream_id: opts.workstream,
690
- repo_id: retry.retryResult.repo_id,
691
- reissued_turn_id: retry.retryResult.reissued_turn_id,
692
- warning_code: warning.code,
693
- warning_message: warning.message,
694
- },
695
- });
696
- }
697
- }
698
-
699
- const syncedRetry = synchronizeCoordinatorPlanState(root, mission, retry.plan);
700
- const retriedPlan = syncedRetry.ok ? syncedRetry.plan : retry.plan;
701
- const retriedWorkstream = retriedPlan.workstreams.find((ws) => ws.workstream_id === opts.workstream);
702
- const retriedLaunchRecord = retriedPlan.launch_records.find(
703
- (record) => record.workstream_id === opts.workstream && record.dispatch_mode === 'coordinator',
704
- ) || retry.launchRecord;
745
+ const retriedPlan = retryExecution.plan;
746
+ const retriedWorkstream = retryExecution.workstream;
747
+ const retriedLaunchRecord = retryExecution.launchRecord;
705
748
 
706
749
  if (opts.json) {
707
750
  console.log(JSON.stringify({
@@ -711,41 +754,41 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
711
754
  plan_id: retriedPlan.plan_id,
712
755
  workstream_id: opts.workstream,
713
756
  super_run_id: mission.coordinator.super_run_id,
714
- repo_id: retry.retryResult.repo_id,
715
- retried_repo_turn_id: retry.retryResult.failed_turn_id,
716
- repo_turn_id: retry.retryResult.reissued_turn_id,
717
- role: retry.retryResult.role,
718
- bundle_path: retry.retryResult.bundle_path,
719
- context_ref: retry.retryResult.context_ref,
757
+ repo_id: retryExecution.retryResult.repo_id,
758
+ retried_repo_turn_id: retryExecution.retryResult.failed_turn_id,
759
+ repo_turn_id: retryExecution.retryResult.reissued_turn_id,
760
+ role: retryExecution.retryResult.role,
761
+ bundle_path: retryExecution.retryResult.bundle_path,
762
+ context_ref: retryExecution.retryResult.context_ref,
720
763
  workstream_status: retriedWorkstream?.launch_status || 'launched',
721
764
  launch_record: retriedLaunchRecord,
722
- exit_code: execution?.exitCode ?? 0,
723
- warnings: retryWarnings,
724
- reconciliation_required: retryWarnings.length > 0,
765
+ exit_code: retryExecution.exit_code ?? 0,
766
+ warnings: retryExecution.warnings || [],
767
+ reconciliation_required: retryExecution.reconciliation_required || false,
725
768
  }, null, 2));
726
- if ((execution?.exitCode ?? 0) !== 0) {
727
- process.exit(execution.exitCode);
769
+ if (!retryExecution.ok) {
770
+ process.exit(retryExecution.exit_code || 1);
728
771
  }
729
772
  return;
730
773
  }
731
774
 
732
- console.log(chalk.green(`Retried coordinator workstream ${chalk.bold(opts.workstream)} in ${chalk.bold(retry.retryResult.repo_id)}`));
775
+ console.log(chalk.green(`Retried coordinator workstream ${chalk.bold(opts.workstream)} in ${chalk.bold(retryExecution.retryResult.repo_id)}`));
733
776
  console.log('');
734
777
  console.log(chalk.dim(` Mission: ${mission.mission_id}`));
735
778
  console.log(chalk.dim(` Plan: ${retriedPlan.plan_id}`));
736
779
  console.log(chalk.dim(` Super Run: ${mission.coordinator.super_run_id}`));
737
- console.log(chalk.dim(` Repo: ${retry.retryResult.repo_id}`));
738
- console.log(chalk.dim(` Old Turn: ${retry.retryResult.failed_turn_id}`));
739
- console.log(chalk.dim(` New Turn: ${retry.retryResult.reissued_turn_id}`));
780
+ console.log(chalk.dim(` Repo: ${retryExecution.retryResult.repo_id}`));
781
+ console.log(chalk.dim(` Old Turn: ${retryExecution.retryResult.failed_turn_id}`));
782
+ console.log(chalk.dim(` New Turn: ${retryExecution.retryResult.reissued_turn_id}`));
740
783
  console.log(chalk.dim(` Workstream: ${retriedWorkstream?.launch_status || 'launched'}`));
741
- if (retryWarnings.length > 0) {
742
- console.log(chalk.yellow(` Warning: ${retryWarnings[0].message}`));
784
+ if ((retryExecution.warnings || []).length > 0) {
785
+ console.log(chalk.yellow(` Warning: ${retryExecution.warnings[0].message}`));
743
786
  }
744
787
  console.log('');
745
788
  renderPlan(retriedPlan);
746
- if ((execution?.exitCode ?? 0) !== 0) {
747
- console.error(chalk.red(`Coordinator retry execution ended with exit code ${execution.exitCode}.`));
748
- process.exit(execution.exitCode);
789
+ if (!retryExecution.ok) {
790
+ console.error(chalk.red(retryExecution.error));
791
+ process.exit(retryExecution.exit_code || 1);
749
792
  }
750
793
  return;
751
794
  }
@@ -1119,6 +1162,11 @@ export async function missionPlanAutopilotCommand(planTarget, opts) {
1119
1162
  process.exit(1);
1120
1163
  }
1121
1164
 
1165
+ if (opts.autoRetry && !mission.coordinator) {
1166
+ console.error(chalk.red('--auto-retry is only supported for coordinator-bound missions.'));
1167
+ process.exit(1);
1168
+ }
1169
+
1122
1170
  if (mission.coordinator) {
1123
1171
  return coordinatorAutopilot(planTarget, opts, context, mission);
1124
1172
  }
@@ -1766,14 +1814,25 @@ async function coordinatorAutopilot(planTarget, opts, context, mission) {
1766
1814
 
1767
1815
  const maxWaves = Math.max(1, parseInt(opts.maxWaves, 10) || 10);
1768
1816
  const cooldownSeconds = Math.max(0, parseInt(opts.cooldown, 10) || 5);
1817
+ const autoRetryEnabled = !!opts.autoRetry;
1818
+ const parsedMaxRetries = Number.parseInt(opts.maxRetries, 10);
1819
+ const maxRetries = autoRetryEnabled
1820
+ ? (Number.isFinite(parsedMaxRetries) ? parsedMaxRetries : 1)
1821
+ : 0;
1822
+ if (autoRetryEnabled && maxRetries < 1) {
1823
+ console.error(chalk.red('--max-retries must be >= 1 when --auto-retry is set.'));
1824
+ process.exit(1);
1825
+ }
1769
1826
  const sleep = opts._sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
1770
1827
 
1771
1828
  const waves = [];
1772
1829
  let totalLaunched = 0;
1773
1830
  let totalCompleted = 0;
1774
1831
  let totalFailed = 0;
1832
+ let totalRetries = 0;
1775
1833
  let terminalReason = null;
1776
1834
  let interrupted = false;
1835
+ const retryCounts = new Map();
1777
1836
 
1778
1837
  const onSigint = () => { interrupted = true; };
1779
1838
  process.on('SIGINT', onSigint);
@@ -1848,6 +1907,92 @@ async function coordinatorAutopilot(planTarget, opts, context, mission) {
1848
1907
  totalLaunched++;
1849
1908
 
1850
1909
  if (result.status === 'needs_attention') {
1910
+ if (autoRetryEnabled && result.repo_id) {
1911
+ const retryKey = `${ws.workstream_id}:${result.repo_id}`;
1912
+ const usedRetries = retryCounts.get(retryKey) || 0;
1913
+
1914
+ if (usedRetries < maxRetries) {
1915
+ retryCounts.set(retryKey, usedRetries + 1);
1916
+
1917
+ const retryExecution = await executeCoordinatorRetryRun(
1918
+ root,
1919
+ mission,
1920
+ ws.workstream_id,
1921
+ coord.config,
1922
+ {
1923
+ ...opts,
1924
+ planId: plan.plan_id,
1925
+ plan,
1926
+ trigger: 'autopilot',
1927
+ triggerReasonPrefix: `mission:${mission.mission_id} workstream:${ws.workstream_id} autopilot:wave-${waveNum} auto-retry`,
1928
+ reason: `mission autopilot auto-retry ${ws.workstream_id}/${result.repo_id}`,
1929
+ },
1930
+ );
1931
+
1932
+ if (retryExecution.plan) {
1933
+ plan = retryExecution.plan;
1934
+ }
1935
+ if (retryExecution.retryResult) {
1936
+ totalRetries++;
1937
+ }
1938
+
1939
+ if (retryExecution.ok) {
1940
+ totalCompleted++;
1941
+ waveResults.push({
1942
+ workstream_id: ws.workstream_id,
1943
+ status: 'dispatched',
1944
+ repo_id: retryExecution.retryResult.repo_id,
1945
+ turn_id: retryExecution.retryResult.reissued_turn_id,
1946
+ retried: true,
1947
+ retry_count: retryExecution.retry_count,
1948
+ retried_repo_turn_id: retryExecution.retryResult.failed_turn_id,
1949
+ warnings: retryExecution.warnings || [],
1950
+ reconciliation_required: retryExecution.reconciliation_required || false,
1951
+ });
1952
+ if (!opts.json) {
1953
+ console.log(chalk.green(`→ ${retryExecution.retryResult.repo_id} retried ✓`));
1954
+ }
1955
+ continue;
1956
+ }
1957
+
1958
+ waveHadFailure = true;
1959
+ totalFailed++;
1960
+ waveResults.push({
1961
+ workstream_id: ws.workstream_id,
1962
+ status: 'needs_attention',
1963
+ repo_id: retryExecution.retryResult?.repo_id || result.repo_id,
1964
+ turn_id: retryExecution.retryResult?.reissued_turn_id || result.turn_id,
1965
+ retried: Boolean(retryExecution.retryResult),
1966
+ retry_count: retryExecution.retry_count || usedRetries + 1,
1967
+ retried_repo_turn_id: retryExecution.retryResult?.failed_turn_id || result.turn_id,
1968
+ error: retryExecution.error,
1969
+ warnings: retryExecution.warnings || [],
1970
+ reconciliation_required: retryExecution.reconciliation_required || false,
1971
+ });
1972
+ if (!opts.json) {
1973
+ console.log(chalk.red(`→ ${result.repo_id} auto-retry failed ✗ (${retryExecution.error})`));
1974
+ }
1975
+ continue;
1976
+ }
1977
+
1978
+ waveHadFailure = true;
1979
+ totalFailed++;
1980
+ const budgetError = formatAutoRetryBudgetError(ws.workstream_id, result.repo_id, maxRetries);
1981
+ waveResults.push({
1982
+ workstream_id: ws.workstream_id,
1983
+ status: 'needs_attention',
1984
+ repo_id: result.repo_id,
1985
+ turn_id: result.turn_id,
1986
+ retry_budget_exhausted: true,
1987
+ retry_count: usedRetries,
1988
+ error: budgetError,
1989
+ });
1990
+ if (!opts.json) {
1991
+ console.log(chalk.red(`→ ${result.repo_id} needs_attention ✗ (${budgetError})`));
1992
+ }
1993
+ continue;
1994
+ }
1995
+
1851
1996
  waveHadFailure = true;
1852
1997
  totalFailed++;
1853
1998
  waveResults.push({ workstream_id: ws.workstream_id, status: 'needs_attention', repo_id: result.repo_id, turn_id: result.turn_id });
@@ -1917,6 +2062,7 @@ async function coordinatorAutopilot(planTarget, opts, context, mission) {
1917
2062
  total_launched: totalLaunched,
1918
2063
  completed: totalCompleted,
1919
2064
  failed: totalFailed,
2065
+ total_retries: totalRetries,
1920
2066
  terminal_reason: terminalReason,
1921
2067
  },
1922
2068
  };
@@ -1930,6 +2076,7 @@ async function coordinatorAutopilot(planTarget, opts, context, mission) {
1930
2076
  console.log(` Launched: ${totalLaunched}`);
1931
2077
  console.log(` Completed: ${totalCompleted}`);
1932
2078
  console.log(` Failed: ${totalFailed}`);
2079
+ console.log(` Retries: ${totalRetries}`);
1933
2080
  console.log(` Outcome: ${formatTerminalReason(terminalReason)}`);
1934
2081
  if (terminalReason === 'plan_completed') {
1935
2082
  console.log(chalk.green('\n Plan completed successfully.'));
@@ -326,6 +326,9 @@ export async function restartCommand(opts) {
326
326
  console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
327
327
  process.exit(1);
328
328
  }
329
+ if (reactivated.migration_notice) {
330
+ console.log(chalk.yellow(reactivated.migration_notice));
331
+ }
329
332
  }
330
333
 
331
334
  // Determine role from option or routing