agentxchain 2.113.0 → 2.115.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 +9 -2
- package/package.json +1 -1
- package/src/commands/mission.js +338 -57
- package/src/lib/dashboard/plan-reader.js +3 -21
- package/src/lib/mission-plans.js +117 -0
- package/src/lib/missions.js +3 -0
package/bin/agentxchain.js
CHANGED
|
@@ -430,6 +430,10 @@ missionCmd
|
|
|
430
430
|
.requiredOption('--title <text>', 'Mission title')
|
|
431
431
|
.requiredOption('--goal <text>', 'Mission goal')
|
|
432
432
|
.option('--id <mission_id>', 'Override the derived mission ID')
|
|
433
|
+
.option('--plan', 'Generate a proposed mission plan immediately after mission creation')
|
|
434
|
+
.option('--constraint <text>', 'Add a constraint to the planner when using --plan (repeatable)', collectOption, [])
|
|
435
|
+
.option('--role-hint <role>', 'Hint available roles to the planner when using --plan (repeatable)', collectOption, [])
|
|
436
|
+
.option('--planner-output-file <path>', 'Read planner JSON output from a file instead of calling the configured planner')
|
|
433
437
|
.option('-j, --json', 'Output as JSON')
|
|
434
438
|
.option('-d, --dir <path>', 'Project directory')
|
|
435
439
|
.action(missionStartCommand);
|
|
@@ -462,6 +466,7 @@ const missionPlanCmd = missionCmd
|
|
|
462
466
|
.description('Generate a decomposition plan for a mission (default: latest mission)')
|
|
463
467
|
.option('--constraint <text>', 'Add a constraint to the planner (repeatable)', collectOption, [])
|
|
464
468
|
.option('--role-hint <role>', 'Hint available roles to the planner (repeatable)', collectOption, [])
|
|
469
|
+
.option('--planner-output-file <path>', 'Read planner JSON output from a file instead of calling the configured planner')
|
|
465
470
|
.option('-j, --json', 'Output as JSON')
|
|
466
471
|
.option('-d, --dir <path>', 'Project directory')
|
|
467
472
|
.action(missionPlanCommand);
|
|
@@ -483,8 +488,10 @@ missionPlanCmd
|
|
|
483
488
|
|
|
484
489
|
missionPlanCmd
|
|
485
490
|
.command('launch [plan_id]')
|
|
486
|
-
.description('Launch
|
|
487
|
-
.
|
|
491
|
+
.description('Launch workstream(s) from an approved plan (default: latest plan)')
|
|
492
|
+
.option('-w, --workstream <id>', 'Workstream ID to launch (mutually exclusive with --all-ready)')
|
|
493
|
+
.option('--all-ready', 'Launch all ready workstreams sequentially (mutually exclusive with --workstream)')
|
|
494
|
+
.option('--retry', 'Retry a failed workstream (requires --workstream, only for needs_attention status)')
|
|
488
495
|
.option('-m, --mission <mission_id>', 'Explicit mission ID')
|
|
489
496
|
.option('--auto-approve', 'Auto-approve run gates while executing the launched workstream')
|
|
490
497
|
.option('-j, --json', 'Output as JSON')
|
package/package.json
CHANGED
package/src/commands/mission.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
2
4
|
import { findProjectRoot, loadProjectContext } from '../lib/config.js';
|
|
3
5
|
import {
|
|
4
6
|
attachChainToMission,
|
|
@@ -13,7 +15,10 @@ import {
|
|
|
13
15
|
import {
|
|
14
16
|
approvePlanArtifact,
|
|
15
17
|
createPlanArtifact,
|
|
18
|
+
getReadyWorkstreams,
|
|
19
|
+
getWorkstreamStatusSummary,
|
|
16
20
|
launchWorkstream,
|
|
21
|
+
retryWorkstream,
|
|
17
22
|
markWorkstreamOutcome,
|
|
18
23
|
loadAllPlans,
|
|
19
24
|
loadLatestPlan,
|
|
@@ -54,6 +59,26 @@ export async function missionStartCommand(opts) {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const snapshot = buildMissionSnapshot(root, result.mission);
|
|
62
|
+
if (opts.plan) {
|
|
63
|
+
try {
|
|
64
|
+
const plan = await createMissionPlan(root, result.mission, opts);
|
|
65
|
+
if (opts.json) {
|
|
66
|
+
console.log(JSON.stringify({ mission: snapshot, plan }, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(chalk.green(`Created mission ${snapshot.mission_id}`));
|
|
71
|
+
console.log(chalk.dim(` Goal: ${snapshot.goal}`));
|
|
72
|
+
console.log(chalk.green(`Created plan ${plan.plan_id} for mission ${snapshot.mission_id}`));
|
|
73
|
+
renderPlan(plan);
|
|
74
|
+
return;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(chalk.yellow(`Mission ${snapshot.mission_id} was created, but automatic plan generation failed.`));
|
|
77
|
+
renderMissionPlanError(error);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
if (opts.json) {
|
|
58
83
|
console.log(JSON.stringify(snapshot, null, 2));
|
|
59
84
|
return;
|
|
@@ -211,66 +236,21 @@ export async function missionPlanCommand(missionTarget, opts) {
|
|
|
211
236
|
console.error(chalk.red(`Mission "${mission.mission_id}" has no goal text. The planner cannot operate on missing mission intent.`));
|
|
212
237
|
process.exit(1);
|
|
213
238
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let plannerOutput;
|
|
220
|
-
const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints);
|
|
221
|
-
|
|
222
|
-
if (opts._plannerOutput) {
|
|
223
|
-
// Injected planner output for testing — skip LLM call
|
|
224
|
-
plannerOutput = opts._plannerOutput;
|
|
225
|
-
} else {
|
|
226
|
-
// Attempt to use the configured api_proxy adapter for decomposition
|
|
227
|
-
try {
|
|
228
|
-
const { loadConfig } = await import('../lib/config.js');
|
|
229
|
-
const config = loadConfig(root);
|
|
230
|
-
const plannerConfig = config?.mission_planner || config?.api_proxy;
|
|
231
|
-
|
|
232
|
-
if (plannerConfig && plannerConfig.base_url && plannerConfig.model) {
|
|
233
|
-
const response = await callPlannerLLM(plannerConfig, systemPrompt, userPrompt);
|
|
234
|
-
const parsed = parsePlannerResponse(response);
|
|
235
|
-
if (!parsed.ok) {
|
|
236
|
-
console.error(chalk.red(`Planner response parse error: ${parsed.error}`));
|
|
237
|
-
process.exit(1);
|
|
238
|
-
}
|
|
239
|
-
plannerOutput = parsed.data;
|
|
240
|
-
} else {
|
|
241
|
-
console.error(chalk.red('No mission planner or api_proxy configured.'));
|
|
242
|
-
console.error(chalk.dim(' Add "mission_planner" or "api_proxy" to agentxchain.json with base_url and model.'));
|
|
243
|
-
console.error(chalk.dim(' Or pass planner output via --planner-output-file <path> for offline use.'));
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
} catch (err) {
|
|
247
|
-
console.error(chalk.red(`Planner call failed: ${err.message}`));
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Validate and create plan artifact
|
|
253
|
-
const result = createPlanArtifact(root, mission, {
|
|
254
|
-
constraints,
|
|
255
|
-
roleHints,
|
|
256
|
-
plannerOutput,
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
if (!result.ok) {
|
|
260
|
-
console.error(chalk.red('Plan validation failed:'));
|
|
261
|
-
for (const err of result.errors) {
|
|
262
|
-
console.error(chalk.red(` • ${err}`));
|
|
263
|
-
}
|
|
239
|
+
let plan;
|
|
240
|
+
try {
|
|
241
|
+
plan = await createMissionPlan(root, mission, opts);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
renderMissionPlanError(error);
|
|
264
244
|
process.exit(1);
|
|
265
245
|
}
|
|
266
246
|
|
|
267
247
|
if (opts.json) {
|
|
268
|
-
console.log(JSON.stringify(
|
|
248
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
269
249
|
return;
|
|
270
250
|
}
|
|
271
251
|
|
|
272
|
-
console.log(chalk.green(`Created plan ${
|
|
273
|
-
renderPlan(
|
|
252
|
+
console.log(chalk.green(`Created plan ${plan.plan_id} for mission ${mission.mission_id}`));
|
|
253
|
+
renderPlan(plan);
|
|
274
254
|
}
|
|
275
255
|
|
|
276
256
|
/**
|
|
@@ -438,11 +418,32 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
438
418
|
}
|
|
439
419
|
const { root } = context;
|
|
440
420
|
|
|
441
|
-
|
|
442
|
-
|
|
421
|
+
// Mutual exclusivity guard
|
|
422
|
+
if (opts.allReady && opts.workstream) {
|
|
423
|
+
console.error(chalk.red('--all-ready and --workstream are mutually exclusive. Use one or the other.'));
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (opts.retry && !opts.workstream) {
|
|
428
|
+
console.error(chalk.red('--retry requires --workstream <id>. Specify which failed workstream to retry.'));
|
|
443
429
|
process.exit(1);
|
|
444
430
|
}
|
|
445
431
|
|
|
432
|
+
if (opts.retry && opts.allReady) {
|
|
433
|
+
console.error(chalk.red('--retry and --all-ready are mutually exclusive. Retry targets a specific workstream.'));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!opts.allReady && !opts.workstream) {
|
|
438
|
+
console.error(chalk.red('--workstream <id> or --all-ready is required. Specify which workstream(s) to launch.'));
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Dispatch to batch launch if --all-ready
|
|
443
|
+
if (opts.allReady) {
|
|
444
|
+
return missionPlanLaunchAllReady(planTarget, opts, context);
|
|
445
|
+
}
|
|
446
|
+
|
|
446
447
|
const mission = opts.mission
|
|
447
448
|
? loadMissionArtifact(root, opts.mission)
|
|
448
449
|
: loadLatestMissionArtifact(root);
|
|
@@ -467,7 +468,9 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
467
468
|
process.exit(1);
|
|
468
469
|
}
|
|
469
470
|
|
|
470
|
-
const launch =
|
|
471
|
+
const launch = opts.retry
|
|
472
|
+
? retryWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream)
|
|
473
|
+
: launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
|
|
471
474
|
if (!launch.ok) {
|
|
472
475
|
console.error(chalk.red(launch.error));
|
|
473
476
|
process.exit(1);
|
|
@@ -548,6 +551,195 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
548
551
|
}
|
|
549
552
|
}
|
|
550
553
|
|
|
554
|
+
// ── Batch launch (--all-ready) ──────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
async function missionPlanLaunchAllReady(planTarget, opts, context) {
|
|
557
|
+
const { root } = context;
|
|
558
|
+
|
|
559
|
+
const mission = opts.mission
|
|
560
|
+
? loadMissionArtifact(root, opts.mission)
|
|
561
|
+
: loadLatestMissionArtifact(root);
|
|
562
|
+
|
|
563
|
+
if (!mission) {
|
|
564
|
+
console.error(chalk.red('No mission found.'));
|
|
565
|
+
console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const plan = planTarget && planTarget !== 'latest'
|
|
570
|
+
? loadPlan(root, mission.mission_id, planTarget)
|
|
571
|
+
: loadLatestPlan(root, mission.mission_id);
|
|
572
|
+
|
|
573
|
+
if (!plan) {
|
|
574
|
+
if (planTarget && planTarget !== 'latest') {
|
|
575
|
+
console.error(chalk.red(`Plan not found: ${planTarget}`));
|
|
576
|
+
} else {
|
|
577
|
+
console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
|
|
578
|
+
console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
579
|
+
}
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (plan.status !== 'approved') {
|
|
584
|
+
console.error(chalk.red(`Plan ${plan.plan_id} is not approved (status: "${plan.status}"). Approve the plan before launching workstreams.`));
|
|
585
|
+
process.exit(1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const readyWorkstreams = getReadyWorkstreams(plan);
|
|
589
|
+
if (readyWorkstreams.length === 0) {
|
|
590
|
+
const summary = getWorkstreamStatusSummary(plan);
|
|
591
|
+
const parts = Object.entries(summary).map(([status, count]) => `${count} ${status}`);
|
|
592
|
+
console.error(chalk.red(`No ready workstreams to launch. Current distribution: ${parts.join(', ')}.`));
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const executor = opts._executeGovernedRun || executeGovernedRun;
|
|
597
|
+
const logger = opts._log || console.log;
|
|
598
|
+
const results = [];
|
|
599
|
+
let hadFailure = false;
|
|
600
|
+
|
|
601
|
+
if (!opts.json) {
|
|
602
|
+
console.log(chalk.bold(`Launching ${readyWorkstreams.length} ready workstream(s) from plan ${plan.plan_id}...\n`));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
for (let i = 0; i < readyWorkstreams.length; i++) {
|
|
606
|
+
const ws = readyWorkstreams[i];
|
|
607
|
+
const prefix = `[${i + 1}/${readyWorkstreams.length}]`;
|
|
608
|
+
|
|
609
|
+
// Skip remaining if a prior workstream failed
|
|
610
|
+
if (hadFailure) {
|
|
611
|
+
results.push({
|
|
612
|
+
workstream_id: ws.workstream_id,
|
|
613
|
+
status: 'skipped',
|
|
614
|
+
skip_reason: 'prior workstream failed',
|
|
615
|
+
});
|
|
616
|
+
if (!opts.json) {
|
|
617
|
+
console.log(`${prefix} ${chalk.dim(ws.workstream_id)} — ${chalk.dim('skipped (prior workstream failed)')}`);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Launch bookkeeping
|
|
623
|
+
const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, ws.workstream_id);
|
|
624
|
+
if (!launch.ok) {
|
|
625
|
+
hadFailure = true;
|
|
626
|
+
results.push({
|
|
627
|
+
workstream_id: ws.workstream_id,
|
|
628
|
+
status: 'launch_error',
|
|
629
|
+
error: launch.error,
|
|
630
|
+
});
|
|
631
|
+
if (!opts.json) {
|
|
632
|
+
console.log(`${prefix} ${chalk.red(ws.workstream_id)} — launch error: ${launch.error}`);
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!opts.json) {
|
|
638
|
+
process.stdout.write(`${prefix} ${chalk.cyan(ws.workstream_id)} → ${launch.chainId} ... `);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Execute
|
|
642
|
+
const chainOpts = {
|
|
643
|
+
enabled: true,
|
|
644
|
+
maxChains: 0,
|
|
645
|
+
chainOn: ['completed'],
|
|
646
|
+
cooldownSeconds: 0,
|
|
647
|
+
mission: mission.mission_id,
|
|
648
|
+
chainId: launch.chainId,
|
|
649
|
+
};
|
|
650
|
+
const runOpts = {
|
|
651
|
+
autoApprove: !!opts.autoApprove,
|
|
652
|
+
provenance: {
|
|
653
|
+
trigger: 'manual',
|
|
654
|
+
created_by: 'operator',
|
|
655
|
+
trigger_reason: `mission:${mission.mission_id} workstream:${ws.workstream_id} batch:all-ready`,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
let execution;
|
|
660
|
+
try {
|
|
661
|
+
execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
|
|
664
|
+
terminalReason: 'execution_error',
|
|
665
|
+
completedAt: new Date().toISOString(),
|
|
666
|
+
});
|
|
667
|
+
hadFailure = true;
|
|
668
|
+
results.push({
|
|
669
|
+
workstream_id: ws.workstream_id,
|
|
670
|
+
chain_id: launch.chainId,
|
|
671
|
+
status: 'needs_attention',
|
|
672
|
+
error: error.message,
|
|
673
|
+
exit_code: 1,
|
|
674
|
+
});
|
|
675
|
+
if (!opts.json) {
|
|
676
|
+
console.log(chalk.red(`needs_attention ✗ (${error.message})`));
|
|
677
|
+
}
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Record outcome
|
|
682
|
+
const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
|
|
683
|
+
const terminalReason = lastRun?.status === 'completed'
|
|
684
|
+
? 'completed'
|
|
685
|
+
: (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
|
|
686
|
+
|
|
687
|
+
markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
|
|
688
|
+
terminalReason,
|
|
689
|
+
completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const wsStatus = terminalReason === 'completed' ? 'completed' : 'needs_attention';
|
|
693
|
+
if (wsStatus === 'needs_attention') {
|
|
694
|
+
hadFailure = true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
results.push({
|
|
698
|
+
workstream_id: ws.workstream_id,
|
|
699
|
+
chain_id: launch.chainId,
|
|
700
|
+
status: wsStatus,
|
|
701
|
+
exit_code: execution.exitCode || 0,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (!opts.json) {
|
|
705
|
+
if (wsStatus === 'completed') {
|
|
706
|
+
console.log(chalk.green('completed ✓'));
|
|
707
|
+
} else {
|
|
708
|
+
console.log(chalk.red(`needs_attention ✗`));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Summary
|
|
714
|
+
const completed = results.filter((r) => r.status === 'completed').length;
|
|
715
|
+
const failed = results.filter((r) => r.status === 'needs_attention' || r.status === 'launch_error').length;
|
|
716
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
717
|
+
|
|
718
|
+
if (opts.json) {
|
|
719
|
+
console.log(JSON.stringify({
|
|
720
|
+
plan_id: plan.plan_id,
|
|
721
|
+
mission_id: mission.mission_id,
|
|
722
|
+
results,
|
|
723
|
+
summary: {
|
|
724
|
+
total: results.length,
|
|
725
|
+
completed,
|
|
726
|
+
failed,
|
|
727
|
+
skipped,
|
|
728
|
+
},
|
|
729
|
+
}, null, 2));
|
|
730
|
+
} else {
|
|
731
|
+
console.log('');
|
|
732
|
+
console.log(chalk.bold(`Summary: ${completed} completed, ${failed} failed, ${skipped} skipped`));
|
|
733
|
+
if (hadFailure) {
|
|
734
|
+
console.log(chalk.dim(' Inspect plan state with `agentxchain mission plan show latest`'));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (hadFailure) {
|
|
739
|
+
process.exit(1);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
551
743
|
// ── Plan rendering ───────────────────────────────────────────────────────────
|
|
552
744
|
|
|
553
745
|
function renderPlan(plan) {
|
|
@@ -691,7 +883,87 @@ async function callPlannerLLM(config, systemPrompt, userPrompt) {
|
|
|
691
883
|
return content;
|
|
692
884
|
}
|
|
693
885
|
|
|
886
|
+
async function createMissionPlan(root, mission, opts = {}) {
|
|
887
|
+
const { constraints, roleHints } = normalizePlannerOptions(opts);
|
|
888
|
+
const plannerOutput = await resolvePlannerOutput(root, mission, constraints, roleHints, opts);
|
|
889
|
+
const result = createPlanArtifact(root, mission, {
|
|
890
|
+
constraints,
|
|
891
|
+
roleHints,
|
|
892
|
+
plannerOutput,
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
if (!result.ok) {
|
|
896
|
+
const error = new Error('Plan validation failed.');
|
|
897
|
+
error.validationErrors = result.errors;
|
|
898
|
+
throw error;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
return result.plan;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function normalizePlannerOptions(opts = {}) {
|
|
905
|
+
return {
|
|
906
|
+
constraints: Array.isArray(opts.constraint) ? opts.constraint : (opts.constraint ? [opts.constraint] : []),
|
|
907
|
+
roleHints: Array.isArray(opts.roleHint) ? opts.roleHint : (opts.roleHint ? [opts.roleHint] : []),
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function resolvePlannerOutput(root, mission, constraints, roleHints, opts = {}) {
|
|
912
|
+
const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints);
|
|
913
|
+
|
|
914
|
+
if (opts._plannerOutput) {
|
|
915
|
+
return opts._plannerOutput;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (opts.plannerOutputFile) {
|
|
919
|
+
const plannerOutputPath = resolve(opts.plannerOutputFile);
|
|
920
|
+
let raw;
|
|
921
|
+
try {
|
|
922
|
+
raw = readFileSync(plannerOutputPath, 'utf8');
|
|
923
|
+
} catch (error) {
|
|
924
|
+
throw new Error(`Planner output file read error: ${error.message}`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const parsed = parsePlannerResponse(raw);
|
|
928
|
+
if (!parsed.ok) {
|
|
929
|
+
throw new Error(`Planner output file parse error: ${parsed.error}`);
|
|
930
|
+
}
|
|
931
|
+
return parsed.data;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const { loadConfig } = await import('../lib/config.js');
|
|
935
|
+
const config = loadConfig(root);
|
|
936
|
+
const plannerConfig = config?.mission_planner || config?.api_proxy;
|
|
937
|
+
|
|
938
|
+
if (!plannerConfig || !plannerConfig.base_url || !plannerConfig.model) {
|
|
939
|
+
throw new Error('No mission planner or api_proxy configured.\n Add "mission_planner" or "api_proxy" to agentxchain.json with base_url and model.\n Or pass planner output via --planner-output-file <path> for offline use.');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const response = await callPlannerLLM(plannerConfig, systemPrompt, userPrompt);
|
|
943
|
+
const parsed = parsePlannerResponse(response);
|
|
944
|
+
if (!parsed.ok) {
|
|
945
|
+
throw new Error(`Planner response parse error: ${parsed.error}`);
|
|
946
|
+
}
|
|
947
|
+
return parsed.data;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function renderMissionPlanError(error) {
|
|
951
|
+
const message = error?.message || 'Mission planning failed.';
|
|
952
|
+
const [firstLine, ...rest] = String(message).split('\n');
|
|
953
|
+
console.error(chalk.red(firstLine));
|
|
954
|
+
for (const line of rest) {
|
|
955
|
+
console.error(chalk.dim(line));
|
|
956
|
+
}
|
|
957
|
+
if (Array.isArray(error?.validationErrors) && error.validationErrors.length > 0) {
|
|
958
|
+
for (const validationError of error.validationErrors) {
|
|
959
|
+
console.error(chalk.red(` • ${validationError}`));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
694
964
|
function renderMissionSnapshot(snapshot) {
|
|
965
|
+
const latestPlan = snapshot.latest_plan || null;
|
|
966
|
+
|
|
695
967
|
console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
|
|
696
968
|
console.log('');
|
|
697
969
|
console.log(` Title: ${snapshot.title || '—'}`);
|
|
@@ -710,10 +982,19 @@ function renderMissionSnapshot(snapshot) {
|
|
|
710
982
|
console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
|
|
711
983
|
}
|
|
712
984
|
|
|
985
|
+
if (latestPlan) {
|
|
986
|
+
console.log('');
|
|
987
|
+
console.log(chalk.bold(' Latest plan:'));
|
|
988
|
+
console.log(` Plan ID: ${latestPlan.plan_id || '—'}`);
|
|
989
|
+
console.log(` Status: ${formatPlanStatus(latestPlan.status)}`);
|
|
990
|
+
console.log(` Completion: ${latestPlan.completion_percentage}% (${latestPlan.completed_count}/${latestPlan.workstream_count} completed)`);
|
|
991
|
+
console.log(` Workstream summary: ready ${latestPlan.ready_count}, blocked ${latestPlan.blocked_count}, launched ${latestPlan.launched_count}, completed ${latestPlan.completed_count}, needs_attention ${latestPlan.needs_attention_count}`);
|
|
992
|
+
}
|
|
993
|
+
|
|
713
994
|
if (!snapshot.chains || snapshot.chains.length === 0) {
|
|
714
995
|
console.log('');
|
|
715
996
|
console.log(chalk.dim(' No chains attached.'));
|
|
716
|
-
console.log(chalk.dim(' Use `agentxchain mission attach-chain latest`
|
|
997
|
+
console.log(chalk.dim(' Use `agentxchain run --chain --mission latest` for new work, or `agentxchain mission attach-chain latest` to repair an unbound chain.'));
|
|
717
998
|
return;
|
|
718
999
|
}
|
|
719
1000
|
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* Plans are advisory repo-local artifacts; this reader is not protocol-normative.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
import { loadAllPlans, loadLatestPlan } from '../mission-plans.js';
|
|
8
|
+
import { buildPlanProgressSummary, loadAllPlans } from '../mission-plans.js';
|
|
11
9
|
import { loadAllMissionArtifacts } from '../missions.js';
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -65,28 +63,12 @@ export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
|
|
|
65
63
|
* Build a dashboard-ready summary for a single plan.
|
|
66
64
|
*/
|
|
67
65
|
function buildPlanSummary(plan) {
|
|
66
|
+
const summary = buildPlanProgressSummary(plan);
|
|
68
67
|
const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
|
|
69
68
|
const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
70
69
|
|
|
71
|
-
const statusCounts = {};
|
|
72
|
-
for (const ws of workstreams) {
|
|
73
|
-
const status = ws.launch_status || 'unknown';
|
|
74
|
-
statusCounts[status] = (statusCounts[status] || 0) + 1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
70
|
return {
|
|
78
|
-
|
|
79
|
-
mission_id: plan.mission_id,
|
|
80
|
-
status: plan.status,
|
|
81
|
-
created_at: plan.created_at,
|
|
82
|
-
updated_at: plan.updated_at,
|
|
83
|
-
approved_at: plan.approved_at || null,
|
|
84
|
-
supersedes_plan_id: plan.supersedes_plan_id || null,
|
|
85
|
-
superseded_by_plan_id: plan.superseded_by_plan_id || null,
|
|
86
|
-
input_goal: plan.input?.goal || null,
|
|
87
|
-
workstream_count: workstreams.length,
|
|
88
|
-
launch_record_count: launchRecords.length,
|
|
89
|
-
workstream_status_counts: statusCounts,
|
|
71
|
+
...summary,
|
|
90
72
|
workstreams: workstreams.map((ws) => ({
|
|
91
73
|
workstream_id: ws.workstream_id,
|
|
92
74
|
title: ws.title,
|
package/src/lib/mission-plans.js
CHANGED
|
@@ -306,6 +306,38 @@ export function loadPlan(root, missionId, planId) {
|
|
|
306
306
|
return plans.find((p) => p.plan_id === planId) || null;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
export function buildPlanProgressSummary(plan) {
|
|
310
|
+
if (!plan || typeof plan !== 'object') return null;
|
|
311
|
+
|
|
312
|
+
const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
|
|
313
|
+
const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
|
|
314
|
+
const workstreamStatusCounts = getWorkstreamStatusSummary(plan);
|
|
315
|
+
const completedCount = workstreamStatusCounts.completed || 0;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
plan_id: plan.plan_id,
|
|
319
|
+
mission_id: plan.mission_id,
|
|
320
|
+
status: plan.status,
|
|
321
|
+
created_at: plan.created_at,
|
|
322
|
+
updated_at: plan.updated_at,
|
|
323
|
+
approved_at: plan.approved_at || null,
|
|
324
|
+
supersedes_plan_id: plan.supersedes_plan_id || null,
|
|
325
|
+
superseded_by_plan_id: plan.superseded_by_plan_id || null,
|
|
326
|
+
input_goal: plan.input?.goal || null,
|
|
327
|
+
workstream_count: workstreams.length,
|
|
328
|
+
launch_record_count: launchRecords.length,
|
|
329
|
+
workstream_status_counts: workstreamStatusCounts,
|
|
330
|
+
ready_count: workstreamStatusCounts.ready || 0,
|
|
331
|
+
blocked_count: workstreamStatusCounts.blocked || 0,
|
|
332
|
+
launched_count: workstreamStatusCounts.launched || 0,
|
|
333
|
+
completed_count: completedCount,
|
|
334
|
+
needs_attention_count: workstreamStatusCounts.needs_attention || 0,
|
|
335
|
+
completion_percentage: workstreams.length === 0
|
|
336
|
+
? 0
|
|
337
|
+
: Math.round((completedCount / workstreams.length) * 100),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
309
341
|
// ── Workstream launch ───────────────────────────────────────────────────────
|
|
310
342
|
|
|
311
343
|
export function didChainFinishSuccessfully(chainReport) {
|
|
@@ -443,6 +475,12 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
|
|
|
443
475
|
}
|
|
444
476
|
}
|
|
445
477
|
}
|
|
478
|
+
|
|
479
|
+
// Auto-complete plan when all workstreams are completed
|
|
480
|
+
const allCompleted = plan.workstreams.every((w) => w.launch_status === 'completed');
|
|
481
|
+
if (allCompleted) {
|
|
482
|
+
plan.status = 'completed';
|
|
483
|
+
}
|
|
446
484
|
} else {
|
|
447
485
|
ws.launch_status = 'needs_attention';
|
|
448
486
|
plan.status = 'needs_attention';
|
|
@@ -454,6 +492,85 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
|
|
|
454
492
|
return { ok: true, plan, workstream: ws };
|
|
455
493
|
}
|
|
456
494
|
|
|
495
|
+
/**
|
|
496
|
+
* Retry a failed workstream by resetting its status and creating a new launch record.
|
|
497
|
+
*
|
|
498
|
+
* Only workstreams with launch_status === 'needs_attention' can be retried.
|
|
499
|
+
* The old launch record is preserved for audit. A new launch record with a new
|
|
500
|
+
* chain_id is created. If the plan was in 'needs_attention' status, it returns
|
|
501
|
+
* to 'approved' since the retry represents a new attempt.
|
|
502
|
+
*
|
|
503
|
+
* @returns {{ ok: boolean, plan?: object, workstream?: object, chainId?: string, launchRecord?: object, error?: string }}
|
|
504
|
+
*/
|
|
505
|
+
export function retryWorkstream(root, missionId, planId, workstreamId, options = {}) {
|
|
506
|
+
const plan = loadPlan(root, missionId, planId);
|
|
507
|
+
if (!plan) {
|
|
508
|
+
return { ok: false, error: `Plan not found: ${planId}` };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const ws = plan.workstreams.find((w) => w.workstream_id === workstreamId);
|
|
512
|
+
if (!ws) {
|
|
513
|
+
return { ok: false, error: `Workstream not found: ${workstreamId}` };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (ws.launch_status !== 'needs_attention') {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
error: `Workstream ${workstreamId} cannot be retried (status: "${ws.launch_status}"). Only "needs_attention" workstreams can be retried.`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Generate new chain ID for the retry
|
|
524
|
+
const chainId = options.chainId || `chain-${randomUUID().slice(0, 8)}`;
|
|
525
|
+
const now = new Date().toISOString();
|
|
526
|
+
const launchRecord = {
|
|
527
|
+
workstream_id: workstreamId,
|
|
528
|
+
chain_id: chainId,
|
|
529
|
+
launched_at: now,
|
|
530
|
+
status: 'launched',
|
|
531
|
+
retry: true,
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (!Array.isArray(plan.launch_records)) {
|
|
535
|
+
plan.launch_records = [];
|
|
536
|
+
}
|
|
537
|
+
plan.launch_records.push(launchRecord);
|
|
538
|
+
ws.launch_status = 'launched';
|
|
539
|
+
|
|
540
|
+
// Restore plan status from needs_attention to approved (retry in progress)
|
|
541
|
+
if (plan.status === 'needs_attention') {
|
|
542
|
+
plan.status = 'approved';
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
plan.updated_at = now;
|
|
546
|
+
writePlanArtifact(root, missionId, plan);
|
|
547
|
+
|
|
548
|
+
return { ok: true, plan, workstream: ws, chainId, launchRecord };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// ── Batch launch helpers ───────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Return all workstreams with launch_status === 'ready', in plan order.
|
|
555
|
+
*/
|
|
556
|
+
export function getReadyWorkstreams(plan) {
|
|
557
|
+
if (!plan || !Array.isArray(plan.workstreams)) return [];
|
|
558
|
+
return plan.workstreams.filter((ws) => ws.launch_status === 'ready');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Return a summary of workstream status distribution for operator messaging.
|
|
563
|
+
*/
|
|
564
|
+
export function getWorkstreamStatusSummary(plan) {
|
|
565
|
+
if (!plan || !Array.isArray(plan.workstreams)) return {};
|
|
566
|
+
const summary = {};
|
|
567
|
+
for (const ws of plan.workstreams) {
|
|
568
|
+
const status = ws.launch_status || 'unknown';
|
|
569
|
+
summary[status] = (summary[status] || 0) + 1;
|
|
570
|
+
}
|
|
571
|
+
return summary;
|
|
572
|
+
}
|
|
573
|
+
|
|
457
574
|
// ── LLM planner prompt ──────────────────────────────────────────────────────
|
|
458
575
|
|
|
459
576
|
/**
|
package/src/lib/missions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { loadAllChainReports, loadChainReport, loadLatestChainReport } from './chain-reports.js';
|
|
4
|
+
import { buildPlanProgressSummary, loadLatestPlan } from './mission-plans.js';
|
|
4
5
|
import { getActiveRepoDecisions } from './repo-decisions.js';
|
|
5
6
|
|
|
6
7
|
const MISSION_ATTENTION_TERMINALS = new Set(['operator_abort', 'parent_validation_failed']);
|
|
@@ -139,6 +140,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
|
|
|
139
140
|
const totalRuns = chains.reduce((sum, chain) => sum + (chain.runs?.length || 0), 0);
|
|
140
141
|
const totalTurns = chains.reduce((sum, chain) => sum + (chain.total_turns || 0), 0);
|
|
141
142
|
const latestChain = chains[0] || null;
|
|
143
|
+
const latestPlan = loadLatestPlan(root, missionArtifact.mission_id);
|
|
142
144
|
const activeRepoDecisions = getActiveRepoDecisions(root);
|
|
143
145
|
|
|
144
146
|
return {
|
|
@@ -151,6 +153,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
|
|
|
151
153
|
total_turns: totalTurns,
|
|
152
154
|
latest_chain_id: latestChain?.chain_id || null,
|
|
153
155
|
latest_terminal_reason: latestChain?.terminal_reason || null,
|
|
156
|
+
latest_plan: buildPlanProgressSummary(latestPlan),
|
|
154
157
|
active_repo_decisions_count: activeRepoDecisions.length,
|
|
155
158
|
chains,
|
|
156
159
|
};
|