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.
- package/README.md +2 -0
- package/bin/agentxchain.js +38 -4
- package/package.json +1 -1
- package/scripts/verify-post-publish.sh +55 -5
- package/src/commands/accept-turn.js +14 -0
- package/src/commands/checkpoint-turn.js +35 -0
- package/src/commands/connector.js +17 -2
- package/src/commands/doctor.js +151 -1
- package/src/commands/events.js +7 -1
- package/src/commands/init.js +42 -11
- package/src/commands/inject.js +1 -1
- package/src/commands/mission.js +803 -7
- package/src/commands/reissue-turn.js +122 -0
- package/src/commands/reject-turn.js +60 -6
- package/src/commands/restart.js +81 -10
- package/src/commands/resume.js +20 -9
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +58 -4
- package/src/commands/step.js +49 -10
- package/src/commands/validate.js +78 -20
- package/src/lib/cli-version.js +106 -0
- package/src/lib/connector-probe.js +146 -5
- package/src/lib/continuous-run.js +22 -87
- package/src/lib/coordinator-dispatch.js +25 -0
- package/src/lib/dispatch-bundle.js +39 -0
- package/src/lib/governed-state.js +624 -11
- package/src/lib/governed-templates.js +1 -0
- package/src/lib/intake.js +233 -77
- package/src/lib/mission-plans.js +510 -6
- package/src/lib/missions.js +65 -6
- package/src/lib/normalized-config.js +50 -15
- package/src/lib/repo-observer.js +8 -2
- package/src/lib/run-events.js +5 -0
- package/src/lib/run-loop.js +25 -0
- package/src/lib/runner-interface.js +2 -0
- package/src/lib/session-checkpoint.js +18 -2
- package/src/lib/turn-checkpoint.js +221 -0
- package/src/templates/governed/full-local-cli.json +71 -0
package/src/commands/mission.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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('
|
|
1815
|
+
: (rec.status === 'failed' || rec.status === 'needs_attention') ? chalk.red('needs_attention')
|
|
1131
1816
|
: chalk.cyan('launched');
|
|
1132
|
-
|
|
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
|
-
|
|
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 {
|