agentxchain 2.129.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.
- package/bin/agentxchain.js +10 -0
- package/package.json +1 -1
- package/src/commands/accept-turn.js +14 -0
- package/src/commands/checkpoint-turn.js +35 -0
- package/src/commands/doctor.js +31 -2
- package/src/commands/mission.js +661 -7
- package/src/commands/reject-turn.js +36 -2
- package/src/commands/restart.js +72 -8
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +13 -1
- package/src/lib/continuous-run.js +8 -1
- package/src/lib/coordinator-dispatch.js +25 -0
- package/src/lib/governed-state.js +150 -1
- package/src/lib/intake.js +12 -0
- package/src/lib/mission-plans.js +510 -6
- package/src/lib/missions.js +9 -2
- package/src/lib/repo-observer.js +1 -0
- package/src/lib/run-events.js +1 -0
- package/src/lib/run-loop.js +20 -0
- package/src/lib/turn-checkpoint.js +221 -0
package/src/commands/mission.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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('
|
|
1815
|
+
: (rec.status === 'failed' || rec.status === 'needs_attention') ? chalk.red('needs_attention')
|
|
1181
1816
|
: chalk.cyan('launched');
|
|
1182
|
-
|
|
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
|
-
|
|
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;
|