agentxchain 2.128.0 → 2.130.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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/bin/agentxchain.js +38 -4
  3. package/package.json +1 -1
  4. package/scripts/verify-post-publish.sh +55 -5
  5. package/src/commands/accept-turn.js +14 -0
  6. package/src/commands/checkpoint-turn.js +35 -0
  7. package/src/commands/connector.js +17 -2
  8. package/src/commands/doctor.js +151 -1
  9. package/src/commands/events.js +7 -1
  10. package/src/commands/init.js +42 -11
  11. package/src/commands/inject.js +1 -1
  12. package/src/commands/mission.js +803 -7
  13. package/src/commands/reissue-turn.js +122 -0
  14. package/src/commands/reject-turn.js +60 -6
  15. package/src/commands/restart.js +81 -10
  16. package/src/commands/resume.js +20 -9
  17. package/src/commands/run.js +13 -0
  18. package/src/commands/status.js +58 -4
  19. package/src/commands/step.js +49 -10
  20. package/src/commands/validate.js +78 -20
  21. package/src/lib/cli-version.js +106 -0
  22. package/src/lib/connector-probe.js +146 -5
  23. package/src/lib/continuous-run.js +22 -87
  24. package/src/lib/coordinator-dispatch.js +25 -0
  25. package/src/lib/dispatch-bundle.js +39 -0
  26. package/src/lib/governed-state.js +624 -11
  27. package/src/lib/governed-templates.js +1 -0
  28. package/src/lib/intake.js +233 -77
  29. package/src/lib/mission-plans.js +510 -6
  30. package/src/lib/missions.js +65 -6
  31. package/src/lib/normalized-config.js +50 -15
  32. package/src/lib/repo-observer.js +8 -2
  33. package/src/lib/run-events.js +5 -0
  34. package/src/lib/run-loop.js +25 -0
  35. package/src/lib/runner-interface.js +2 -0
  36. package/src/lib/session-checkpoint.js +18 -2
  37. package/src/lib/turn-checkpoint.js +221 -0
  38. package/src/templates/governed/full-local-cli.json +71 -0
@@ -4,6 +4,7 @@ import { resolve } from 'path';
4
4
  import { findProjectRoot, loadProjectContext } from '../lib/config.js';
5
5
  import {
6
6
  attachChainToMission,
7
+ bindCoordinatorToMission,
7
8
  buildMissionListSummary,
8
9
  buildMissionSnapshot,
9
10
  createMission,
@@ -12,11 +13,14 @@ import {
12
13
  loadMissionArtifact,
13
14
  loadMissionSnapshot,
14
15
  } from '../lib/missions.js';
16
+ import { loadCoordinatorConfig } from '../lib/coordinator-config.js';
17
+ import { initializeCoordinatorRun } from '../lib/coordinator-state.js';
15
18
  import {
16
19
  approvePlanArtifact,
17
20
  createPlanArtifact,
18
21
  getReadyWorkstreams,
19
22
  getWorkstreamStatusSummary,
23
+ launchCoordinatorWorkstream,
20
24
  launchWorkstream,
21
25
  retryWorkstream,
22
26
  markWorkstreamOutcome,
@@ -25,10 +29,13 @@ import {
25
29
  loadPlan,
26
30
  buildPlannerPrompt,
27
31
  parsePlannerResponse,
32
+ synchronizeCoordinatorPlanState,
28
33
  validatePlannerOutput,
29
34
  } from '../lib/mission-plans.js';
30
35
  import { executeChainedRun } from '../lib/run-chain.js';
31
36
  import { executeGovernedRun } from './run.js';
37
+ import { dispatchCoordinatorTurn, selectAssignmentForWorkstream } from '../lib/coordinator-dispatch.js';
38
+ import { loadCoordinatorState } from '../lib/coordinator-state.js';
32
39
 
33
40
  export async function missionStartCommand(opts) {
34
41
  const root = findProjectRoot(opts.dir || process.cwd());
@@ -48,6 +55,22 @@ export async function missionStartCommand(opts) {
48
55
  process.exit(1);
49
56
  }
50
57
 
58
+ // Multi-repo: validate coordinator config before creating mission (fail-fast)
59
+ let coordinatorConfig = null;
60
+ let coordinatorWorkspacePath = null;
61
+ if (opts.multi) {
62
+ coordinatorWorkspacePath = resolve(opts.coordinatorWorkspace || opts.coordinatorConfig || root);
63
+ coordinatorConfig = loadCoordinatorConfig(coordinatorWorkspacePath);
64
+ if (!coordinatorConfig.ok) {
65
+ console.error(chalk.red('Coordinator config validation failed:'));
66
+ console.error(chalk.dim(` Expected agentxchain-multi.json at: ${coordinatorWorkspacePath}`));
67
+ for (const err of coordinatorConfig.errors || []) {
68
+ console.error(chalk.red(` ${err}`));
69
+ }
70
+ process.exit(1);
71
+ }
72
+ }
73
+
51
74
  const result = createMission(root, {
52
75
  missionId: opts.id,
53
76
  title,
@@ -58,6 +81,37 @@ export async function missionStartCommand(opts) {
58
81
  process.exit(1);
59
82
  }
60
83
 
84
+ // Multi-repo: initialize coordinator and bind to mission
85
+ if (opts.multi && coordinatorConfig) {
86
+ const initResult = initializeCoordinatorRun(coordinatorWorkspacePath, coordinatorConfig.config);
87
+ if (!initResult.ok) {
88
+ // Atomic rollback: delete the mission artifact
89
+ const { getMissionsDir } = await import('../lib/missions.js');
90
+ const { unlinkSync, existsSync: fileExists } = await import('fs');
91
+ const { join: joinPath } = await import('path');
92
+ const missionFile = joinPath(getMissionsDir(root), `${result.mission.mission_id}.json`);
93
+ if (fileExists(missionFile)) {
94
+ try { unlinkSync(missionFile); } catch { /* best effort */ }
95
+ }
96
+ console.error(chalk.red('Coordinator initialization failed:'));
97
+ for (const err of initResult.errors || []) {
98
+ console.error(chalk.red(` ${err}`));
99
+ }
100
+ process.exit(1);
101
+ }
102
+
103
+ const bindResult = bindCoordinatorToMission(root, result.mission.mission_id, {
104
+ super_run_id: initResult.super_run_id,
105
+ config_path: opts.coordinatorConfig || null,
106
+ workspace_path: coordinatorWorkspacePath,
107
+ });
108
+ if (!bindResult.ok) {
109
+ console.error(chalk.yellow(`Mission created but coordinator binding failed: ${bindResult.error}`));
110
+ } else {
111
+ result.mission = bindResult.mission;
112
+ }
113
+ }
114
+
61
115
  const snapshot = buildMissionSnapshot(root, result.mission);
62
116
  if (opts.plan) {
63
117
  try {
@@ -275,7 +329,7 @@ export async function missionPlanShowCommand(planTarget, opts) {
275
329
  }
276
330
 
277
331
  // Resolve plan target
278
- const plan = planTarget && planTarget !== 'latest'
332
+ let plan = planTarget && planTarget !== 'latest'
279
333
  ? loadPlan(root, mission.mission_id, planTarget)
280
334
  : loadLatestPlan(root, mission.mission_id);
281
335
 
@@ -289,6 +343,15 @@ export async function missionPlanShowCommand(planTarget, opts) {
289
343
  return;
290
344
  }
291
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
+
292
355
  if (opts.json) {
293
356
  console.log(JSON.stringify(plan, null, 2));
294
357
  return;
@@ -454,7 +517,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
454
517
  process.exit(1);
455
518
  }
456
519
 
457
- const plan = planTarget && planTarget !== 'latest'
520
+ let plan = planTarget && planTarget !== 'latest'
458
521
  ? loadPlan(root, mission.mission_id, planTarget)
459
522
  : loadLatestPlan(root, mission.mission_id);
460
523
 
@@ -468,6 +531,113 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
468
531
  process.exit(1);
469
532
  }
470
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
+
471
641
  const launch = opts.retry
472
642
  ? retryWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream)
473
643
  : launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
@@ -566,6 +736,10 @@ async function missionPlanLaunchAllReady(planTarget, opts, context) {
566
736
  process.exit(1);
567
737
  }
568
738
 
739
+ if (mission.coordinator) {
740
+ return coordinatorLaunchAllReady(planTarget, opts, context, mission);
741
+ }
742
+
569
743
  const plan = planTarget && planTarget !== 'latest'
570
744
  ? loadPlan(root, mission.mission_id, planTarget)
571
745
  : loadLatestPlan(root, mission.mission_id);
@@ -760,6 +934,10 @@ export async function missionPlanAutopilotCommand(planTarget, opts) {
760
934
  process.exit(1);
761
935
  }
762
936
 
937
+ if (mission.coordinator) {
938
+ return coordinatorAutopilot(planTarget, opts, context, mission);
939
+ }
940
+
763
941
  const plan = planTarget && planTarget !== 'latest'
764
942
  ? loadPlan(root, mission.mission_id, planTarget)
765
943
  : loadLatestPlan(root, mission.mission_id);
@@ -1067,6 +1245,509 @@ function deriveAutopilotIdleOutcome(plan, continueOnFailure) {
1067
1245
  return 'no_ready_workstreams';
1068
1246
  }
1069
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
+
1070
1751
  // ── Plan rendering ───────────────────────────────────────────────────────────
1071
1752
 
1072
1753
  function renderPlan(plan) {
@@ -1085,6 +1766,10 @@ function renderPlan(plan) {
1085
1766
  console.log(` Approved: ${plan.approved_at}`);
1086
1767
  }
1087
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
+ }
1088
1773
  console.log('');
1089
1774
 
1090
1775
  if (!plan.workstreams || plan.workstreams.length === 0) {
@@ -1127,9 +1812,17 @@ function renderPlan(plan) {
1127
1812
  console.log(chalk.bold(' Launch records:'));
1128
1813
  for (const rec of plan.launch_records) {
1129
1814
  const statusTag = rec.status === 'completed' ? chalk.green('completed')
1130
- : rec.status === 'failed' ? chalk.red('failed')
1815
+ : (rec.status === 'failed' || rec.status === 'needs_attention') ? chalk.red('needs_attention')
1131
1816
  : chalk.cyan('launched');
1132
- 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
+ }
1133
1826
  }
1134
1827
  }
1135
1828
 
@@ -1212,11 +1905,22 @@ async function callPlannerLLM(config, systemPrompt, userPrompt) {
1212
1905
 
1213
1906
  async function createMissionPlan(root, mission, opts = {}) {
1214
1907
  const { constraints, roleHints } = normalizePlannerOptions(opts);
1215
- 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);
1216
1919
  const result = createPlanArtifact(root, mission, {
1217
1920
  constraints,
1218
1921
  roleHints,
1219
1922
  plannerOutput,
1923
+ coordinatorConfig,
1220
1924
  });
1221
1925
 
1222
1926
  if (!result.ok) {
@@ -1235,8 +1939,8 @@ function normalizePlannerOptions(opts = {}) {
1235
1939
  };
1236
1940
  }
1237
1941
 
1238
- async function resolvePlannerOutput(root, mission, constraints, roleHints, opts = {}) {
1239
- 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);
1240
1944
 
1241
1945
  if (opts._plannerOutput) {
1242
1946
  return opts._plannerOutput;
@@ -1288,6 +1992,54 @@ function renderMissionPlanError(error) {
1288
1992
  }
1289
1993
  }
1290
1994
 
1995
+ // ── Mission Bind-Coordinator Command ───────────────────────────────────────
1996
+
1997
+ export async function missionBindCoordinatorCommand(missionId, opts) {
1998
+ const root = findProjectRoot(opts.dir || process.cwd());
1999
+ if (!root) {
2000
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
2001
+ process.exit(1);
2002
+ }
2003
+
2004
+ const superRunId = String(opts.superRunId || '').trim();
2005
+ const configPath = String(opts.coordinatorConfig || '').trim();
2006
+ if (!superRunId) {
2007
+ console.error(chalk.red('--super-run-id is required.'));
2008
+ process.exit(1);
2009
+ }
2010
+
2011
+ const mission = missionId
2012
+ ? loadMissionArtifact(root, missionId)
2013
+ : loadLatestMissionArtifact(root);
2014
+ if (!mission) {
2015
+ console.error(chalk.red(missionId ? `Mission not found: ${missionId}` : 'No mission found.'));
2016
+ process.exit(1);
2017
+ }
2018
+
2019
+ const workspacePath = resolve(opts.coordinatorWorkspace || root);
2020
+ const result = bindCoordinatorToMission(root, mission.mission_id, {
2021
+ super_run_id: superRunId,
2022
+ config_path: configPath || null,
2023
+ workspace_path: workspacePath,
2024
+ });
2025
+
2026
+ if (!result.ok) {
2027
+ console.error(chalk.red(result.error));
2028
+ process.exit(1);
2029
+ }
2030
+
2031
+ const snapshot = buildMissionSnapshot(root, result.mission);
2032
+ if (opts.json) {
2033
+ console.log(JSON.stringify(snapshot, null, 2));
2034
+ return;
2035
+ }
2036
+
2037
+ console.log(chalk.green(`Bound coordinator ${superRunId} to ${snapshot.mission_id}`));
2038
+ renderMissionSnapshot(snapshot);
2039
+ }
2040
+
2041
+ // ── Rendering ──────────────────────────────────────────────────────────────
2042
+
1291
2043
  function renderMissionSnapshot(snapshot) {
1292
2044
  const latestPlan = snapshot.latest_plan || null;
1293
2045
 
@@ -1309,6 +2061,36 @@ function renderMissionSnapshot(snapshot) {
1309
2061
  console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
1310
2062
  }
1311
2063
 
2064
+ // Coordinator (multi-repo) section
2065
+ if (snapshot.coordinator_status) {
2066
+ const cs = snapshot.coordinator_status;
2067
+ console.log('');
2068
+ console.log(chalk.bold(' Coordinator (multi-repo):'));
2069
+ if (cs.unreachable) {
2070
+ console.log(` Super Run: ${cs.super_run_id || '—'}`);
2071
+ console.log(` Status: ${chalk.red('unreachable')}`);
2072
+ } else {
2073
+ console.log(` Super Run: ${cs.super_run_id || '—'}`);
2074
+ console.log(` Status: ${formatCoordinatorStatus(cs.status)}`);
2075
+ console.log(` Phase: ${cs.phase || '—'}`);
2076
+ if (cs.repo_runs && Object.keys(cs.repo_runs).length > 0) {
2077
+ console.log(' Repos:');
2078
+ for (const [repoId, repo] of Object.entries(cs.repo_runs)) {
2079
+ console.log(` ${pad(repoId, 16)} ${pad(repo.status || '—', 14)} ${pad(repo.phase || '—', 18)} ${repo.run_id || '—'}`);
2080
+ }
2081
+ }
2082
+ if (cs.pending_barriers && cs.pending_barriers.length > 0) {
2083
+ console.log(' Barriers:');
2084
+ for (const barrier of cs.pending_barriers) {
2085
+ console.log(` ${pad(barrier.id, 28)} ${pad(barrier.type || '—', 24)} ${barrier.status || '—'}`);
2086
+ }
2087
+ }
2088
+ if (cs.blocked_reason) {
2089
+ console.log(` Blocked: ${chalk.yellow(cs.blocked_reason)}`);
2090
+ }
2091
+ }
2092
+ }
2093
+
1312
2094
  if (latestPlan) {
1313
2095
  console.log('');
1314
2096
  console.log(chalk.bold(' Latest plan:'));
@@ -1374,6 +2156,20 @@ function formatMissionStatus(status) {
1374
2156
  }
1375
2157
  }
1376
2158
 
2159
+ function formatCoordinatorStatus(status) {
2160
+ if (!status) return '—';
2161
+ switch (status) {
2162
+ case 'active':
2163
+ return chalk.green('active');
2164
+ case 'blocked':
2165
+ return chalk.red('blocked');
2166
+ case 'completed':
2167
+ return chalk.cyan('completed');
2168
+ default:
2169
+ return status;
2170
+ }
2171
+ }
2172
+
1377
2173
  function formatTimestamp(value) {
1378
2174
  if (!value) return '—';
1379
2175
  try {