agentxchain 2.129.0 → 2.130.1

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.
@@ -20,6 +20,7 @@ import {
20
20
  createPlanArtifact,
21
21
  getReadyWorkstreams,
22
22
  getWorkstreamStatusSummary,
23
+ launchCoordinatorWorkstream,
23
24
  launchWorkstream,
24
25
  retryWorkstream,
25
26
  markWorkstreamOutcome,
@@ -28,10 +29,13 @@ import {
28
29
  loadPlan,
29
30
  buildPlannerPrompt,
30
31
  parsePlannerResponse,
32
+ synchronizeCoordinatorPlanState,
31
33
  validatePlannerOutput,
32
34
  } from '../lib/mission-plans.js';
33
35
  import { executeChainedRun } from '../lib/run-chain.js';
34
36
  import { executeGovernedRun } from './run.js';
37
+ import { dispatchCoordinatorTurn, selectAssignmentForWorkstream } from '../lib/coordinator-dispatch.js';
38
+ import { loadCoordinatorState } from '../lib/coordinator-state.js';
35
39
 
36
40
  export async function missionStartCommand(opts) {
37
41
  const root = findProjectRoot(opts.dir || process.cwd());
@@ -325,7 +329,7 @@ export async function missionPlanShowCommand(planTarget, opts) {
325
329
  }
326
330
 
327
331
  // Resolve plan target
328
- const plan = planTarget && planTarget !== 'latest'
332
+ let plan = planTarget && planTarget !== 'latest'
329
333
  ? loadPlan(root, mission.mission_id, planTarget)
330
334
  : loadLatestPlan(root, mission.mission_id);
331
335
 
@@ -339,6 +343,15 @@ export async function missionPlanShowCommand(planTarget, opts) {
339
343
  return;
340
344
  }
341
345
 
346
+ if (mission.coordinator && plan.coordinator_scope) {
347
+ const synced = synchronizeCoordinatorPlanState(root, mission, plan);
348
+ if (!synced.ok) {
349
+ console.error(chalk.red(synced.error));
350
+ process.exit(1);
351
+ }
352
+ plan = synced.plan;
353
+ }
354
+
342
355
  if (opts.json) {
343
356
  console.log(JSON.stringify(plan, null, 2));
344
357
  return;
@@ -504,7 +517,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
504
517
  process.exit(1);
505
518
  }
506
519
 
507
- const plan = planTarget && planTarget !== 'latest'
520
+ let plan = planTarget && planTarget !== 'latest'
508
521
  ? loadPlan(root, mission.mission_id, planTarget)
509
522
  : loadLatestPlan(root, mission.mission_id);
510
523
 
@@ -518,6 +531,113 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
518
531
  process.exit(1);
519
532
  }
520
533
 
534
+ if (mission.coordinator && plan.coordinator_scope) {
535
+ const synced = synchronizeCoordinatorPlanState(root, mission, plan);
536
+ if (!synced.ok) {
537
+ console.error(chalk.red(synced.error));
538
+ process.exit(1);
539
+ }
540
+ plan = synced.plan;
541
+ }
542
+
543
+ if (mission.coordinator && plan.coordinator_scope) {
544
+ if (opts.retry) {
545
+ console.error(chalk.red('--retry is not supported for coordinator-bound mission plans yet.'));
546
+ process.exit(1);
547
+ }
548
+
549
+ const coordinatorConfigResult = loadCoordinatorConfig(mission.coordinator.workspace_path);
550
+ if (!coordinatorConfigResult.ok) {
551
+ console.error(chalk.red('Coordinator config validation failed:'));
552
+ for (const err of coordinatorConfigResult.errors || []) {
553
+ console.error(chalk.red(` ${err}`));
554
+ }
555
+ process.exit(1);
556
+ }
557
+
558
+ const coordinatorState = loadCoordinatorState(mission.coordinator.workspace_path);
559
+ if (!coordinatorState) {
560
+ console.error(chalk.red(`Coordinator state not found at ${mission.coordinator.workspace_path}.`));
561
+ process.exit(1);
562
+ }
563
+ if (coordinatorState.status !== 'active') {
564
+ console.error(chalk.red(`Coordinator run ${coordinatorState.super_run_id} is not active (status: "${coordinatorState.status}").`));
565
+ process.exit(1);
566
+ }
567
+
568
+ const assignment = selectAssignmentForWorkstream(
569
+ mission.coordinator.workspace_path,
570
+ coordinatorState,
571
+ coordinatorConfigResult.config,
572
+ opts.workstream,
573
+ );
574
+ if (!assignment.ok) {
575
+ console.error(chalk.red(`Coordinator launch blocked: ${assignment.detail || assignment.reason}`));
576
+ process.exit(1);
577
+ }
578
+
579
+ const dispatch = dispatchCoordinatorTurn(
580
+ mission.coordinator.workspace_path,
581
+ coordinatorState,
582
+ coordinatorConfigResult.config,
583
+ assignment,
584
+ );
585
+ if (!dispatch.ok) {
586
+ console.error(chalk.red(`Coordinator dispatch failed: ${dispatch.error}`));
587
+ process.exit(1);
588
+ }
589
+
590
+ const launch = launchCoordinatorWorkstream(
591
+ root,
592
+ mission,
593
+ plan.plan_id,
594
+ opts.workstream,
595
+ {
596
+ ...dispatch,
597
+ role: assignment.role,
598
+ },
599
+ coordinatorConfigResult.config,
600
+ );
601
+ if (!launch.ok) {
602
+ console.error(chalk.red(launch.error));
603
+ process.exit(1);
604
+ }
605
+
606
+ const syncedWorkstream = launch.plan.workstreams.find((ws) => ws.workstream_id === opts.workstream);
607
+ const jsonPayload = {
608
+ dispatch_mode: 'coordinator',
609
+ mission_id: mission.mission_id,
610
+ plan_id: launch.plan.plan_id,
611
+ workstream_id: opts.workstream,
612
+ super_run_id: mission.coordinator.super_run_id,
613
+ repo_id: dispatch.repo_id,
614
+ repo_turn_id: dispatch.turn_id,
615
+ role: assignment.role,
616
+ bundle_path: dispatch.bundle_path,
617
+ context_ref: dispatch.context_ref || null,
618
+ workstream_status: syncedWorkstream?.launch_status || 'launched',
619
+ launch_record: launch.launchRecord,
620
+ };
621
+
622
+ if (opts.json) {
623
+ console.log(JSON.stringify(jsonPayload, null, 2));
624
+ return;
625
+ }
626
+
627
+ console.log(chalk.green(`Dispatched coordinator workstream ${chalk.bold(opts.workstream)} to ${chalk.bold(dispatch.repo_id)}`));
628
+ console.log('');
629
+ console.log(chalk.dim(` Mission: ${mission.mission_id}`));
630
+ console.log(chalk.dim(` Plan: ${launch.plan.plan_id}`));
631
+ console.log(chalk.dim(` Super Run: ${mission.coordinator.super_run_id}`));
632
+ console.log(chalk.dim(` Repo: ${dispatch.repo_id}`));
633
+ console.log(chalk.dim(` Repo Turn: ${dispatch.turn_id}`));
634
+ console.log(chalk.dim(` Role: ${assignment.role}`));
635
+ console.log(chalk.dim(` Workstream: ${syncedWorkstream?.launch_status || 'launched'}`));
636
+ console.log('');
637
+ renderPlan(launch.plan);
638
+ return;
639
+ }
640
+
521
641
  const launch = opts.retry
522
642
  ? retryWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream)
523
643
  : launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
@@ -616,6 +736,10 @@ async function missionPlanLaunchAllReady(planTarget, opts, context) {
616
736
  process.exit(1);
617
737
  }
618
738
 
739
+ if (mission.coordinator) {
740
+ return coordinatorLaunchAllReady(planTarget, opts, context, mission);
741
+ }
742
+
619
743
  const plan = planTarget && planTarget !== 'latest'
620
744
  ? loadPlan(root, mission.mission_id, planTarget)
621
745
  : loadLatestPlan(root, mission.mission_id);
@@ -810,6 +934,10 @@ export async function missionPlanAutopilotCommand(planTarget, opts) {
810
934
  process.exit(1);
811
935
  }
812
936
 
937
+ if (mission.coordinator) {
938
+ return coordinatorAutopilot(planTarget, opts, context, mission);
939
+ }
940
+
813
941
  const plan = planTarget && planTarget !== 'latest'
814
942
  ? loadPlan(root, mission.mission_id, planTarget)
815
943
  : loadLatestPlan(root, mission.mission_id);
@@ -1117,6 +1245,509 @@ function deriveAutopilotIdleOutcome(plan, continueOnFailure) {
1117
1245
  return 'no_ready_workstreams';
1118
1246
  }
1119
1247
 
1248
+ function getCoordinatorWaveWorkstreams(plan) {
1249
+ if (!plan || !Array.isArray(plan.workstreams)) {
1250
+ return [];
1251
+ }
1252
+
1253
+ return plan.workstreams.filter((ws) => {
1254
+ if (ws.launch_status === 'ready') {
1255
+ return true;
1256
+ }
1257
+
1258
+ if (ws.launch_status !== 'launched') {
1259
+ return false;
1260
+ }
1261
+
1262
+ const progress = ws.coordinator_progress;
1263
+ if (!progress || !Array.isArray(progress.pending_repo_ids) || progress.pending_repo_ids.length === 0) {
1264
+ return false;
1265
+ }
1266
+
1267
+ if ((progress.repo_failure_count || 0) > 0) {
1268
+ return false;
1269
+ }
1270
+
1271
+ return true;
1272
+ });
1273
+ }
1274
+
1275
+ // ── Coordinator wave execution ──────────────────────────────────────────────
1276
+
1277
+ /**
1278
+ * Dispatch a single coordinator workstream, execute the repo-local turn,
1279
+ * and synchronize plan state. Returns { ok, status, repo_id, turn_id, error }.
1280
+ */
1281
+ async function dispatchAndExecuteCoordinatorWorkstream(
1282
+ root, mission, plan, workstreamId, coordinatorConfig, coordinatorState, opts,
1283
+ ) {
1284
+ const executor = opts._executeGovernedRun || executeGovernedRun;
1285
+ const logger = opts._log || console.log;
1286
+
1287
+ // 1. Select assignment
1288
+ const assignment = selectAssignmentForWorkstream(
1289
+ mission.coordinator.workspace_path,
1290
+ coordinatorState,
1291
+ coordinatorConfig,
1292
+ workstreamId,
1293
+ );
1294
+ if (!assignment.ok) {
1295
+ return { ok: false, status: 'assignment_blocked', error: assignment.detail || assignment.reason };
1296
+ }
1297
+
1298
+ // 2. Dispatch coordinator turn (writes bundle, does not execute)
1299
+ const dispatch = dispatchCoordinatorTurn(
1300
+ mission.coordinator.workspace_path,
1301
+ coordinatorState,
1302
+ coordinatorConfig,
1303
+ assignment,
1304
+ );
1305
+ if (!dispatch.ok) {
1306
+ return { ok: false, status: 'dispatch_error', error: dispatch.error };
1307
+ }
1308
+
1309
+ // 3. Record launch in plan
1310
+ const launch = launchCoordinatorWorkstream(
1311
+ root,
1312
+ mission,
1313
+ plan.plan_id,
1314
+ workstreamId,
1315
+ { ...dispatch, role: assignment.role },
1316
+ coordinatorConfig,
1317
+ );
1318
+ if (!launch.ok) {
1319
+ return { ok: false, status: 'launch_record_error', error: launch.error };
1320
+ }
1321
+
1322
+ // 4. Execute the repo-local turn in the target repo
1323
+ const repoPath = coordinatorConfig.repos?.[dispatch.repo_id]?.resolved_path;
1324
+ if (!repoPath) {
1325
+ return { ok: false, status: 'repo_path_error', error: `No resolved_path for repo "${dispatch.repo_id}"` };
1326
+ }
1327
+
1328
+ const repoContext = loadProjectContext(repoPath);
1329
+ if (!repoContext) {
1330
+ return { ok: false, status: 'repo_context_error', error: `Cannot load project context for repo "${dispatch.repo_id}" at "${repoPath}"` };
1331
+ }
1332
+
1333
+ const runOpts = {
1334
+ autoApprove: !!opts.autoApprove,
1335
+ log: logger,
1336
+ provenance: {
1337
+ trigger: opts.trigger || 'manual',
1338
+ created_by: 'operator',
1339
+ trigger_reason: `mission:${mission.mission_id} workstream:${workstreamId} coordinator:${mission.coordinator.super_run_id} repo:${dispatch.repo_id}`,
1340
+ },
1341
+ };
1342
+
1343
+ let execution;
1344
+ try {
1345
+ execution = await executor(repoContext, runOpts);
1346
+ } catch (error) {
1347
+ return {
1348
+ ok: false,
1349
+ status: 'needs_attention',
1350
+ repo_id: dispatch.repo_id,
1351
+ turn_id: dispatch.turn_id,
1352
+ error: error.message,
1353
+ };
1354
+ }
1355
+
1356
+ // 5. Sync plan state from coordinator (updates barriers, accepted repos, failures)
1357
+ const synced = synchronizeCoordinatorPlanState(root, mission, launch.plan);
1358
+
1359
+ const exitCode = execution?.exitCode || 0;
1360
+ const wsStatus = exitCode === 0 ? 'dispatched' : 'needs_attention';
1361
+
1362
+ return {
1363
+ ok: true,
1364
+ status: wsStatus,
1365
+ repo_id: dispatch.repo_id,
1366
+ turn_id: dispatch.turn_id,
1367
+ exit_code: exitCode,
1368
+ plan: synced.ok ? synced.plan : launch.plan,
1369
+ };
1370
+ }
1371
+
1372
+ /**
1373
+ * Load coordinator config and state for a mission. Returns { ok, config, state, error }.
1374
+ */
1375
+ function loadCoordinatorForMission(mission) {
1376
+ const coordinatorConfigResult = loadCoordinatorConfig(mission.coordinator.workspace_path);
1377
+ if (!coordinatorConfigResult.ok) {
1378
+ return { ok: false, error: `Coordinator config validation failed: ${(coordinatorConfigResult.errors || []).join(', ')}` };
1379
+ }
1380
+
1381
+ const coordinatorState = loadCoordinatorState(mission.coordinator.workspace_path);
1382
+ if (!coordinatorState) {
1383
+ return { ok: false, error: `Coordinator state not found at ${mission.coordinator.workspace_path}` };
1384
+ }
1385
+ if (coordinatorState.status !== 'active') {
1386
+ return { ok: false, error: `Coordinator run ${coordinatorState.super_run_id} is not active (status: "${coordinatorState.status}")` };
1387
+ }
1388
+
1389
+ return { ok: true, config: coordinatorConfigResult.config, state: coordinatorState };
1390
+ }
1391
+
1392
+ /**
1393
+ * Coordinator-aware --all-ready: dispatches all ready workstreams sequentially,
1394
+ * executing each repo-local turn and syncing barrier state between dispatches.
1395
+ */
1396
+ async function coordinatorLaunchAllReady(planTarget, opts, context, mission) {
1397
+ const { root } = context;
1398
+
1399
+ let plan = planTarget && planTarget !== 'latest'
1400
+ ? loadPlan(root, mission.mission_id, planTarget)
1401
+ : loadLatestPlan(root, mission.mission_id);
1402
+
1403
+ if (!plan) {
1404
+ console.error(chalk.red('No plan found.'));
1405
+ process.exit(1);
1406
+ }
1407
+
1408
+ // Sync plan state first
1409
+ const synced = synchronizeCoordinatorPlanState(root, mission, plan);
1410
+ if (synced.ok) plan = synced.plan;
1411
+
1412
+ if (plan.status !== 'approved') {
1413
+ console.error(chalk.red(`Plan ${plan.plan_id} is not approved (status: "${plan.status}").`));
1414
+ process.exit(1);
1415
+ }
1416
+
1417
+ const coord = loadCoordinatorForMission(mission);
1418
+ if (!coord.ok) {
1419
+ console.error(chalk.red(coord.error));
1420
+ process.exit(1);
1421
+ }
1422
+
1423
+ const readyWorkstreams = getReadyWorkstreams(plan);
1424
+ if (readyWorkstreams.length === 0) {
1425
+ const summary = getWorkstreamStatusSummary(plan);
1426
+ const parts = Object.entries(summary).map(([status, count]) => `${count} ${status}`);
1427
+ console.error(chalk.red(`No ready workstreams. Distribution: ${parts.join(', ')}.`));
1428
+ process.exit(1);
1429
+ }
1430
+
1431
+ if (!opts.json) {
1432
+ console.log(chalk.bold(`Coordinator --all-ready: launching ${readyWorkstreams.length} workstream(s) from plan ${plan.plan_id}...\n`));
1433
+ }
1434
+
1435
+ const results = [];
1436
+ let hadFailure = false;
1437
+
1438
+ for (let i = 0; i < readyWorkstreams.length; i++) {
1439
+ const ws = readyWorkstreams[i];
1440
+ const prefix = `[${i + 1}/${readyWorkstreams.length}]`;
1441
+
1442
+ if (hadFailure) {
1443
+ results.push({ workstream_id: ws.workstream_id, status: 'skipped', skip_reason: 'prior workstream failed' });
1444
+ if (!opts.json) {
1445
+ console.log(`${prefix} ${chalk.dim(ws.workstream_id)} — ${chalk.dim('skipped (prior workstream failed)')}`);
1446
+ }
1447
+ continue;
1448
+ }
1449
+
1450
+ if (!opts.json) {
1451
+ process.stdout.write(`${prefix} ${chalk.cyan(ws.workstream_id)} ... `);
1452
+ }
1453
+
1454
+ const result = await dispatchAndExecuteCoordinatorWorkstream(
1455
+ root, mission, plan, ws.workstream_id, coord.config, coord.state, { ...opts, trigger: 'manual' },
1456
+ );
1457
+
1458
+ if (!result.ok) {
1459
+ hadFailure = true;
1460
+ results.push({ workstream_id: ws.workstream_id, status: 'needs_attention', error: result.error });
1461
+ if (!opts.json) {
1462
+ console.log(chalk.red(`needs_attention ✗ (${result.error})`));
1463
+ }
1464
+ continue;
1465
+ }
1466
+
1467
+ // Update plan reference for subsequent dispatches
1468
+ if (result.plan) plan = result.plan;
1469
+
1470
+ const wsStatus = result.status === 'needs_attention' ? 'needs_attention' : 'dispatched';
1471
+ if (wsStatus === 'needs_attention') hadFailure = true;
1472
+
1473
+ results.push({
1474
+ workstream_id: ws.workstream_id,
1475
+ status: wsStatus,
1476
+ repo_id: result.repo_id,
1477
+ turn_id: result.turn_id,
1478
+ exit_code: result.exit_code,
1479
+ });
1480
+
1481
+ if (!opts.json) {
1482
+ if (wsStatus === 'needs_attention') {
1483
+ console.log(chalk.red('needs_attention ✗'));
1484
+ } else {
1485
+ console.log(chalk.green(`→ ${result.repo_id} ✓`));
1486
+ }
1487
+ }
1488
+ }
1489
+
1490
+ // Summary
1491
+ const dispatched = results.filter((r) => r.status === 'dispatched').length;
1492
+ const failed = results.filter((r) => r.status === 'needs_attention').length;
1493
+ const skipped = results.filter((r) => r.status === 'skipped').length;
1494
+
1495
+ if (opts.json) {
1496
+ console.log(JSON.stringify({
1497
+ plan_id: plan.plan_id,
1498
+ mission_id: mission.mission_id,
1499
+ dispatch_mode: 'coordinator',
1500
+ results,
1501
+ summary: { total: results.length, dispatched, failed, skipped },
1502
+ }, null, 2));
1503
+ } else {
1504
+ console.log('');
1505
+ console.log(chalk.bold(`Summary: ${dispatched} dispatched, ${failed} failed, ${skipped} skipped`));
1506
+ if (hadFailure) {
1507
+ console.log(chalk.dim(' Inspect plan state with `agentxchain mission plan show latest`'));
1508
+ }
1509
+ }
1510
+
1511
+ if (hadFailure) process.exit(1);
1512
+ }
1513
+
1514
+ /**
1515
+ * Coordinator-aware autopilot: wave-based unattended execution for coordinator-bound missions.
1516
+ * Each wave dispatches all ready workstreams, executes repo-local turns, and syncs barrier state.
1517
+ */
1518
+ async function coordinatorAutopilot(planTarget, opts, context, mission) {
1519
+ const { root } = context;
1520
+
1521
+ let plan = planTarget && planTarget !== 'latest'
1522
+ ? loadPlan(root, mission.mission_id, planTarget)
1523
+ : loadLatestPlan(root, mission.mission_id);
1524
+
1525
+ if (!plan) {
1526
+ console.error(chalk.red('No plan found.'));
1527
+ process.exit(1);
1528
+ }
1529
+
1530
+ const continueOnFailure = !!opts.continueOnFailure;
1531
+
1532
+ // Sync plan state
1533
+ const initialSync = synchronizeCoordinatorPlanState(root, mission, plan);
1534
+ if (initialSync.ok) plan = initialSync.plan;
1535
+
1536
+ if (plan.status === 'completed') {
1537
+ if (opts.json) {
1538
+ console.log(JSON.stringify({
1539
+ plan_id: plan.plan_id,
1540
+ mission_id: mission.mission_id,
1541
+ dispatch_mode: 'coordinator',
1542
+ waves: [],
1543
+ summary: { total_waves: 0, total_launched: 0, completed: 0, failed: 0, terminal_reason: 'plan_completed' },
1544
+ }, null, 2));
1545
+ } else {
1546
+ console.log(chalk.green(`Plan ${plan.plan_id} is already completed. Nothing to do.`));
1547
+ }
1548
+ return;
1549
+ }
1550
+
1551
+ if (plan.status !== 'approved' && !(plan.status === 'needs_attention' && continueOnFailure)) {
1552
+ console.error(chalk.red(`Plan ${plan.plan_id} status is "${plan.status}". Autopilot requires an approved plan${continueOnFailure ? ' (or needs_attention with --continue-on-failure)' : ''}.`));
1553
+ process.exit(1);
1554
+ }
1555
+
1556
+ const coord = loadCoordinatorForMission(mission);
1557
+ if (!coord.ok) {
1558
+ console.error(chalk.red(coord.error));
1559
+ process.exit(1);
1560
+ }
1561
+
1562
+ const maxWaves = Math.max(1, parseInt(opts.maxWaves, 10) || 10);
1563
+ const cooldownSeconds = Math.max(0, parseInt(opts.cooldown, 10) || 5);
1564
+ const sleep = opts._sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
1565
+
1566
+ const waves = [];
1567
+ let totalLaunched = 0;
1568
+ let totalCompleted = 0;
1569
+ let totalFailed = 0;
1570
+ let terminalReason = null;
1571
+ let interrupted = false;
1572
+
1573
+ const onSigint = () => { interrupted = true; };
1574
+ process.on('SIGINT', onSigint);
1575
+
1576
+ try {
1577
+ for (let waveNum = 1; waveNum <= maxWaves; waveNum++) {
1578
+ if (interrupted) {
1579
+ terminalReason = 'interrupted';
1580
+ break;
1581
+ }
1582
+
1583
+ // Re-sync plan from disk + coordinator state
1584
+ const currentPlan = loadPlan(root, mission.mission_id, plan.plan_id);
1585
+ if (!currentPlan) {
1586
+ terminalReason = 'plan_read_error';
1587
+ break;
1588
+ }
1589
+ const coordSync = synchronizeCoordinatorPlanState(root, mission, currentPlan);
1590
+ plan = coordSync.ok ? coordSync.plan : currentPlan;
1591
+
1592
+ if (plan.status === 'completed') {
1593
+ terminalReason = 'plan_completed';
1594
+ break;
1595
+ }
1596
+
1597
+ const readyWorkstreams = getCoordinatorWaveWorkstreams(plan);
1598
+ if (readyWorkstreams.length === 0) {
1599
+ terminalReason = deriveAutopilotIdleOutcome(plan, continueOnFailure);
1600
+ break;
1601
+ }
1602
+
1603
+ if (!opts.json) {
1604
+ console.log(chalk.bold(`\n━━━ Wave ${waveNum} — coordinator: ${readyWorkstreams.length} workstream(s) ━━━\n`));
1605
+ }
1606
+
1607
+ const waveResults = [];
1608
+ let waveHadFailure = false;
1609
+
1610
+ for (let i = 0; i < readyWorkstreams.length; i++) {
1611
+ if (interrupted) break;
1612
+
1613
+ const ws = readyWorkstreams[i];
1614
+ const prefix = `[${i + 1}/${readyWorkstreams.length}]`;
1615
+
1616
+ if (waveHadFailure && !continueOnFailure) {
1617
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'skipped', skip_reason: 'prior workstream failed' });
1618
+ if (!opts.json) {
1619
+ console.log(`${prefix} ${chalk.dim(ws.workstream_id)} — ${chalk.dim('skipped (prior workstream failed)')}`);
1620
+ }
1621
+ continue;
1622
+ }
1623
+
1624
+ if (!opts.json) {
1625
+ process.stdout.write(`${prefix} ${chalk.cyan(ws.workstream_id)} ... `);
1626
+ }
1627
+
1628
+ const result = await dispatchAndExecuteCoordinatorWorkstream(
1629
+ root, mission, plan, ws.workstream_id, coord.config, coord.state, { ...opts, trigger: 'autopilot' },
1630
+ );
1631
+
1632
+ if (!result.ok) {
1633
+ waveHadFailure = true;
1634
+ totalFailed++;
1635
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'needs_attention', error: result.error });
1636
+ if (!opts.json) {
1637
+ console.log(chalk.red(`needs_attention ✗ (${result.error})`));
1638
+ }
1639
+ continue;
1640
+ }
1641
+
1642
+ if (result.plan) plan = result.plan;
1643
+ totalLaunched++;
1644
+
1645
+ if (result.status === 'needs_attention') {
1646
+ waveHadFailure = true;
1647
+ totalFailed++;
1648
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'needs_attention', repo_id: result.repo_id, turn_id: result.turn_id });
1649
+ if (!opts.json) {
1650
+ console.log(chalk.red(`→ ${result.repo_id} needs_attention ✗`));
1651
+ }
1652
+ } else {
1653
+ totalCompleted++;
1654
+ waveResults.push({ workstream_id: ws.workstream_id, status: 'dispatched', repo_id: result.repo_id, turn_id: result.turn_id });
1655
+ if (!opts.json) {
1656
+ console.log(chalk.green(`→ ${result.repo_id} ✓`));
1657
+ }
1658
+ }
1659
+ }
1660
+
1661
+ waves.push({ wave: waveNum, results: waveResults });
1662
+
1663
+ if (interrupted) {
1664
+ terminalReason = 'interrupted';
1665
+ break;
1666
+ }
1667
+
1668
+ // Re-sync and check plan completion
1669
+ const afterSync = synchronizeCoordinatorPlanState(root, mission, plan);
1670
+ if (afterSync.ok) plan = afterSync.plan;
1671
+
1672
+ if (plan.status === 'completed') {
1673
+ terminalReason = 'plan_completed';
1674
+ break;
1675
+ }
1676
+
1677
+ if (waveHadFailure && !continueOnFailure) {
1678
+ terminalReason = 'failure_stopped';
1679
+ break;
1680
+ }
1681
+
1682
+ if (waveNum === maxWaves) {
1683
+ terminalReason = 'wave_limit_reached';
1684
+ break;
1685
+ }
1686
+
1687
+ // Cooldown between waves
1688
+ if (cooldownSeconds > 0 && !interrupted) {
1689
+ if (!opts.json) {
1690
+ console.log(chalk.dim(`\nCooldown: ${cooldownSeconds}s before next wave...\n`));
1691
+ }
1692
+ await sleep(cooldownSeconds * 1000);
1693
+ }
1694
+ }
1695
+ } finally {
1696
+ process.removeListener('SIGINT', onSigint);
1697
+ }
1698
+
1699
+ if (!terminalReason) {
1700
+ terminalReason = totalFailed > 0
1701
+ ? (continueOnFailure ? 'plan_incomplete' : 'failure_stopped')
1702
+ : 'plan_completed';
1703
+ }
1704
+
1705
+ const jsonOutput = {
1706
+ plan_id: plan.plan_id,
1707
+ mission_id: mission.mission_id,
1708
+ dispatch_mode: 'coordinator',
1709
+ waves,
1710
+ summary: {
1711
+ total_waves: waves.length,
1712
+ total_launched: totalLaunched,
1713
+ completed: totalCompleted,
1714
+ failed: totalFailed,
1715
+ terminal_reason: terminalReason,
1716
+ },
1717
+ };
1718
+
1719
+ if (opts.json) {
1720
+ console.log(JSON.stringify(jsonOutput, null, 2));
1721
+ } else {
1722
+ console.log('');
1723
+ console.log(chalk.bold('━━━ Coordinator Autopilot Summary ━━━'));
1724
+ console.log(` Waves: ${waves.length}`);
1725
+ console.log(` Launched: ${totalLaunched}`);
1726
+ console.log(` Completed: ${totalCompleted}`);
1727
+ console.log(` Failed: ${totalFailed}`);
1728
+ console.log(` Outcome: ${formatTerminalReason(terminalReason)}`);
1729
+ if (terminalReason === 'plan_completed') {
1730
+ console.log(chalk.green('\n Plan completed successfully.'));
1731
+ } else if (terminalReason === 'deadlock') {
1732
+ console.log(chalk.red('\n Deadlock: remaining workstreams are blocked with unsatisfiable dependencies.'));
1733
+ console.log(chalk.dim(' Inspect with `agentxchain mission plan show latest`.'));
1734
+ } else if (terminalReason === 'wave_limit_reached') {
1735
+ console.log(chalk.yellow(`\n Wave limit reached. Run autopilot again to continue.`));
1736
+ } else if (terminalReason === 'failure_stopped') {
1737
+ console.log(chalk.red('\n Stopped due to workstream failure.'));
1738
+ console.log(chalk.dim(' Use --continue-on-failure to skip failures, or resolve the issue and rerun autopilot.'));
1739
+ } else if (terminalReason === 'plan_incomplete') {
1740
+ console.log(chalk.yellow('\n Autopilot exhausted all launchable work, but failed workstreams still need attention.'));
1741
+ } else if (terminalReason === 'interrupted') {
1742
+ console.log(chalk.yellow('\n Interrupted by operator.'));
1743
+ }
1744
+ }
1745
+
1746
+ if (terminalReason !== 'plan_completed') {
1747
+ process.exit(1);
1748
+ }
1749
+ }
1750
+
1120
1751
  // ── Plan rendering ───────────────────────────────────────────────────────────
1121
1752
 
1122
1753
  function renderPlan(plan) {
@@ -1135,6 +1766,10 @@ function renderPlan(plan) {
1135
1766
  console.log(` Approved: ${plan.approved_at}`);
1136
1767
  }
1137
1768
  console.log(` Created: ${plan.created_at || '—'}`);
1769
+ if (plan.coordinator_scope) {
1770
+ const cs = plan.coordinator_scope;
1771
+ console.log(` Coordinator: ${chalk.cyan('bound')} (${(cs.repo_ids || []).length} repos, phases: ${(cs.phases || []).join(', ')})`);
1772
+ }
1138
1773
  console.log('');
1139
1774
 
1140
1775
  if (!plan.workstreams || plan.workstreams.length === 0) {
@@ -1177,9 +1812,17 @@ function renderPlan(plan) {
1177
1812
  console.log(chalk.bold(' Launch records:'));
1178
1813
  for (const rec of plan.launch_records) {
1179
1814
  const statusTag = rec.status === 'completed' ? chalk.green('completed')
1180
- : rec.status === 'failed' ? chalk.red('failed')
1815
+ : (rec.status === 'failed' || rec.status === 'needs_attention') ? chalk.red('needs_attention')
1181
1816
  : chalk.cyan('launched');
1182
- console.log(` ${chalk.cyan(rec.workstream_id)} ${rec.chain_id} [${statusTag}]`);
1817
+ if (rec.dispatch_mode === 'coordinator') {
1818
+ const dispatchCount = rec.repo_dispatches?.length || 0;
1819
+ const progress = rec.coordinator_progress;
1820
+ const accepted = progress ? `${progress.accepted_repo_count}/${progress.repo_count}` : '—';
1821
+ const failures = rec.repo_failures?.length || progress?.repo_failure_count || 0;
1822
+ console.log(` ${chalk.cyan(rec.workstream_id)} → coordinator ${rec.super_run_id || '—'} [${statusTag}] dispatches=${dispatchCount} accepted=${accepted} failed=${failures}`);
1823
+ } else {
1824
+ console.log(` ${chalk.cyan(rec.workstream_id)} → ${rec.chain_id} [${statusTag}]`);
1825
+ }
1183
1826
  }
1184
1827
  }
1185
1828
 
@@ -1262,11 +1905,22 @@ async function callPlannerLLM(config, systemPrompt, userPrompt) {
1262
1905
 
1263
1906
  async function createMissionPlan(root, mission, opts = {}) {
1264
1907
  const { constraints, roleHints } = normalizePlannerOptions(opts);
1265
- const plannerOutput = await resolvePlannerOutput(root, mission, constraints, roleHints, opts);
1908
+
1909
+ // Load coordinator config when mission is coordinator-bound
1910
+ let coordinatorConfig = null;
1911
+ if (mission.coordinator && mission.coordinator.workspace_path) {
1912
+ const coordResult = loadCoordinatorConfig(mission.coordinator.workspace_path);
1913
+ if (coordResult.ok) {
1914
+ coordinatorConfig = coordResult.config;
1915
+ }
1916
+ }
1917
+
1918
+ const plannerOutput = await resolvePlannerOutput(root, mission, constraints, roleHints, opts, coordinatorConfig);
1266
1919
  const result = createPlanArtifact(root, mission, {
1267
1920
  constraints,
1268
1921
  roleHints,
1269
1922
  plannerOutput,
1923
+ coordinatorConfig,
1270
1924
  });
1271
1925
 
1272
1926
  if (!result.ok) {
@@ -1285,8 +1939,8 @@ function normalizePlannerOptions(opts = {}) {
1285
1939
  };
1286
1940
  }
1287
1941
 
1288
- async function resolvePlannerOutput(root, mission, constraints, roleHints, opts = {}) {
1289
- const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints);
1942
+ async function resolvePlannerOutput(root, mission, constraints, roleHints, opts = {}, coordinatorConfig = null) {
1943
+ const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints, coordinatorConfig);
1290
1944
 
1291
1945
  if (opts._plannerOutput) {
1292
1946
  return opts._plannerOutput;