agentxchain 2.113.0 → 2.114.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 +8 -2
- package/package.json +1 -1
- package/src/commands/mission.js +312 -55
- package/src/lib/mission-plans.js +23 -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,9 @@ 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)')
|
|
488
494
|
.option('-m, --mission <mission_id>', 'Explicit mission ID')
|
|
489
495
|
.option('--auto-approve', 'Auto-approve run gates while executing the launched workstream')
|
|
490
496
|
.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,6 +15,8 @@ import {
|
|
|
13
15
|
import {
|
|
14
16
|
approvePlanArtifact,
|
|
15
17
|
createPlanArtifact,
|
|
18
|
+
getReadyWorkstreams,
|
|
19
|
+
getWorkstreamStatusSummary,
|
|
16
20
|
launchWorkstream,
|
|
17
21
|
markWorkstreamOutcome,
|
|
18
22
|
loadAllPlans,
|
|
@@ -54,6 +58,26 @@ export async function missionStartCommand(opts) {
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
const snapshot = buildMissionSnapshot(root, result.mission);
|
|
61
|
+
if (opts.plan) {
|
|
62
|
+
try {
|
|
63
|
+
const plan = await createMissionPlan(root, result.mission, opts);
|
|
64
|
+
if (opts.json) {
|
|
65
|
+
console.log(JSON.stringify({ mission: snapshot, plan }, null, 2));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.green(`Created mission ${snapshot.mission_id}`));
|
|
70
|
+
console.log(chalk.dim(` Goal: ${snapshot.goal}`));
|
|
71
|
+
console.log(chalk.green(`Created plan ${plan.plan_id} for mission ${snapshot.mission_id}`));
|
|
72
|
+
renderPlan(plan);
|
|
73
|
+
return;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error(chalk.yellow(`Mission ${snapshot.mission_id} was created, but automatic plan generation failed.`));
|
|
76
|
+
renderMissionPlanError(error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
if (opts.json) {
|
|
58
82
|
console.log(JSON.stringify(snapshot, null, 2));
|
|
59
83
|
return;
|
|
@@ -211,66 +235,21 @@ export async function missionPlanCommand(missionTarget, opts) {
|
|
|
211
235
|
console.error(chalk.red(`Mission "${mission.mission_id}" has no goal text. The planner cannot operate on missing mission intent.`));
|
|
212
236
|
process.exit(1);
|
|
213
237
|
}
|
|
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
|
-
}
|
|
238
|
+
let plan;
|
|
239
|
+
try {
|
|
240
|
+
plan = await createMissionPlan(root, mission, opts);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
renderMissionPlanError(error);
|
|
264
243
|
process.exit(1);
|
|
265
244
|
}
|
|
266
245
|
|
|
267
246
|
if (opts.json) {
|
|
268
|
-
console.log(JSON.stringify(
|
|
247
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
269
248
|
return;
|
|
270
249
|
}
|
|
271
250
|
|
|
272
|
-
console.log(chalk.green(`Created plan ${
|
|
273
|
-
renderPlan(
|
|
251
|
+
console.log(chalk.green(`Created plan ${plan.plan_id} for mission ${mission.mission_id}`));
|
|
252
|
+
renderPlan(plan);
|
|
274
253
|
}
|
|
275
254
|
|
|
276
255
|
/**
|
|
@@ -438,11 +417,22 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
438
417
|
}
|
|
439
418
|
const { root } = context;
|
|
440
419
|
|
|
441
|
-
|
|
442
|
-
|
|
420
|
+
// Mutual exclusivity guard
|
|
421
|
+
if (opts.allReady && opts.workstream) {
|
|
422
|
+
console.error(chalk.red('--all-ready and --workstream are mutually exclusive. Use one or the other.'));
|
|
443
423
|
process.exit(1);
|
|
444
424
|
}
|
|
445
425
|
|
|
426
|
+
if (!opts.allReady && !opts.workstream) {
|
|
427
|
+
console.error(chalk.red('--workstream <id> or --all-ready is required. Specify which workstream(s) to launch.'));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Dispatch to batch launch if --all-ready
|
|
432
|
+
if (opts.allReady) {
|
|
433
|
+
return missionPlanLaunchAllReady(planTarget, opts, context);
|
|
434
|
+
}
|
|
435
|
+
|
|
446
436
|
const mission = opts.mission
|
|
447
437
|
? loadMissionArtifact(root, opts.mission)
|
|
448
438
|
: loadLatestMissionArtifact(root);
|
|
@@ -548,6 +538,195 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
548
538
|
}
|
|
549
539
|
}
|
|
550
540
|
|
|
541
|
+
// ── Batch launch (--all-ready) ──────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
async function missionPlanLaunchAllReady(planTarget, opts, context) {
|
|
544
|
+
const { root } = context;
|
|
545
|
+
|
|
546
|
+
const mission = opts.mission
|
|
547
|
+
? loadMissionArtifact(root, opts.mission)
|
|
548
|
+
: loadLatestMissionArtifact(root);
|
|
549
|
+
|
|
550
|
+
if (!mission) {
|
|
551
|
+
console.error(chalk.red('No mission found.'));
|
|
552
|
+
console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const plan = planTarget && planTarget !== 'latest'
|
|
557
|
+
? loadPlan(root, mission.mission_id, planTarget)
|
|
558
|
+
: loadLatestPlan(root, mission.mission_id);
|
|
559
|
+
|
|
560
|
+
if (!plan) {
|
|
561
|
+
if (planTarget && planTarget !== 'latest') {
|
|
562
|
+
console.error(chalk.red(`Plan not found: ${planTarget}`));
|
|
563
|
+
} else {
|
|
564
|
+
console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
|
|
565
|
+
console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
|
|
566
|
+
}
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (plan.status !== 'approved') {
|
|
571
|
+
console.error(chalk.red(`Plan ${plan.plan_id} is not approved (status: "${plan.status}"). Approve the plan before launching workstreams.`));
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const readyWorkstreams = getReadyWorkstreams(plan);
|
|
576
|
+
if (readyWorkstreams.length === 0) {
|
|
577
|
+
const summary = getWorkstreamStatusSummary(plan);
|
|
578
|
+
const parts = Object.entries(summary).map(([status, count]) => `${count} ${status}`);
|
|
579
|
+
console.error(chalk.red(`No ready workstreams to launch. Current distribution: ${parts.join(', ')}.`));
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const executor = opts._executeGovernedRun || executeGovernedRun;
|
|
584
|
+
const logger = opts._log || console.log;
|
|
585
|
+
const results = [];
|
|
586
|
+
let hadFailure = false;
|
|
587
|
+
|
|
588
|
+
if (!opts.json) {
|
|
589
|
+
console.log(chalk.bold(`Launching ${readyWorkstreams.length} ready workstream(s) from plan ${plan.plan_id}...\n`));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (let i = 0; i < readyWorkstreams.length; i++) {
|
|
593
|
+
const ws = readyWorkstreams[i];
|
|
594
|
+
const prefix = `[${i + 1}/${readyWorkstreams.length}]`;
|
|
595
|
+
|
|
596
|
+
// Skip remaining if a prior workstream failed
|
|
597
|
+
if (hadFailure) {
|
|
598
|
+
results.push({
|
|
599
|
+
workstream_id: ws.workstream_id,
|
|
600
|
+
status: 'skipped',
|
|
601
|
+
skip_reason: 'prior workstream failed',
|
|
602
|
+
});
|
|
603
|
+
if (!opts.json) {
|
|
604
|
+
console.log(`${prefix} ${chalk.dim(ws.workstream_id)} — ${chalk.dim('skipped (prior workstream failed)')}`);
|
|
605
|
+
}
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Launch bookkeeping
|
|
610
|
+
const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, ws.workstream_id);
|
|
611
|
+
if (!launch.ok) {
|
|
612
|
+
hadFailure = true;
|
|
613
|
+
results.push({
|
|
614
|
+
workstream_id: ws.workstream_id,
|
|
615
|
+
status: 'launch_error',
|
|
616
|
+
error: launch.error,
|
|
617
|
+
});
|
|
618
|
+
if (!opts.json) {
|
|
619
|
+
console.log(`${prefix} ${chalk.red(ws.workstream_id)} — launch error: ${launch.error}`);
|
|
620
|
+
}
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!opts.json) {
|
|
625
|
+
process.stdout.write(`${prefix} ${chalk.cyan(ws.workstream_id)} → ${launch.chainId} ... `);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Execute
|
|
629
|
+
const chainOpts = {
|
|
630
|
+
enabled: true,
|
|
631
|
+
maxChains: 0,
|
|
632
|
+
chainOn: ['completed'],
|
|
633
|
+
cooldownSeconds: 0,
|
|
634
|
+
mission: mission.mission_id,
|
|
635
|
+
chainId: launch.chainId,
|
|
636
|
+
};
|
|
637
|
+
const runOpts = {
|
|
638
|
+
autoApprove: !!opts.autoApprove,
|
|
639
|
+
provenance: {
|
|
640
|
+
trigger: 'manual',
|
|
641
|
+
created_by: 'operator',
|
|
642
|
+
trigger_reason: `mission:${mission.mission_id} workstream:${ws.workstream_id} batch:all-ready`,
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
let execution;
|
|
647
|
+
try {
|
|
648
|
+
execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
|
|
651
|
+
terminalReason: 'execution_error',
|
|
652
|
+
completedAt: new Date().toISOString(),
|
|
653
|
+
});
|
|
654
|
+
hadFailure = true;
|
|
655
|
+
results.push({
|
|
656
|
+
workstream_id: ws.workstream_id,
|
|
657
|
+
chain_id: launch.chainId,
|
|
658
|
+
status: 'needs_attention',
|
|
659
|
+
error: error.message,
|
|
660
|
+
exit_code: 1,
|
|
661
|
+
});
|
|
662
|
+
if (!opts.json) {
|
|
663
|
+
console.log(chalk.red(`needs_attention ✗ (${error.message})`));
|
|
664
|
+
}
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Record outcome
|
|
669
|
+
const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
|
|
670
|
+
const terminalReason = lastRun?.status === 'completed'
|
|
671
|
+
? 'completed'
|
|
672
|
+
: (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
|
|
673
|
+
|
|
674
|
+
markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, ws.workstream_id, {
|
|
675
|
+
terminalReason,
|
|
676
|
+
completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const wsStatus = terminalReason === 'completed' ? 'completed' : 'needs_attention';
|
|
680
|
+
if (wsStatus === 'needs_attention') {
|
|
681
|
+
hadFailure = true;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
results.push({
|
|
685
|
+
workstream_id: ws.workstream_id,
|
|
686
|
+
chain_id: launch.chainId,
|
|
687
|
+
status: wsStatus,
|
|
688
|
+
exit_code: execution.exitCode || 0,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
if (!opts.json) {
|
|
692
|
+
if (wsStatus === 'completed') {
|
|
693
|
+
console.log(chalk.green('completed ✓'));
|
|
694
|
+
} else {
|
|
695
|
+
console.log(chalk.red(`needs_attention ✗`));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Summary
|
|
701
|
+
const completed = results.filter((r) => r.status === 'completed').length;
|
|
702
|
+
const failed = results.filter((r) => r.status === 'needs_attention' || r.status === 'launch_error').length;
|
|
703
|
+
const skipped = results.filter((r) => r.status === 'skipped').length;
|
|
704
|
+
|
|
705
|
+
if (opts.json) {
|
|
706
|
+
console.log(JSON.stringify({
|
|
707
|
+
plan_id: plan.plan_id,
|
|
708
|
+
mission_id: mission.mission_id,
|
|
709
|
+
results,
|
|
710
|
+
summary: {
|
|
711
|
+
total: results.length,
|
|
712
|
+
completed,
|
|
713
|
+
failed,
|
|
714
|
+
skipped,
|
|
715
|
+
},
|
|
716
|
+
}, null, 2));
|
|
717
|
+
} else {
|
|
718
|
+
console.log('');
|
|
719
|
+
console.log(chalk.bold(`Summary: ${completed} completed, ${failed} failed, ${skipped} skipped`));
|
|
720
|
+
if (hadFailure) {
|
|
721
|
+
console.log(chalk.dim(' Inspect plan state with `agentxchain mission plan show latest`'));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (hadFailure) {
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
551
730
|
// ── Plan rendering ───────────────────────────────────────────────────────────
|
|
552
731
|
|
|
553
732
|
function renderPlan(plan) {
|
|
@@ -691,6 +870,84 @@ async function callPlannerLLM(config, systemPrompt, userPrompt) {
|
|
|
691
870
|
return content;
|
|
692
871
|
}
|
|
693
872
|
|
|
873
|
+
async function createMissionPlan(root, mission, opts = {}) {
|
|
874
|
+
const { constraints, roleHints } = normalizePlannerOptions(opts);
|
|
875
|
+
const plannerOutput = await resolvePlannerOutput(root, mission, constraints, roleHints, opts);
|
|
876
|
+
const result = createPlanArtifact(root, mission, {
|
|
877
|
+
constraints,
|
|
878
|
+
roleHints,
|
|
879
|
+
plannerOutput,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
if (!result.ok) {
|
|
883
|
+
const error = new Error('Plan validation failed.');
|
|
884
|
+
error.validationErrors = result.errors;
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return result.plan;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function normalizePlannerOptions(opts = {}) {
|
|
892
|
+
return {
|
|
893
|
+
constraints: Array.isArray(opts.constraint) ? opts.constraint : (opts.constraint ? [opts.constraint] : []),
|
|
894
|
+
roleHints: Array.isArray(opts.roleHint) ? opts.roleHint : (opts.roleHint ? [opts.roleHint] : []),
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async function resolvePlannerOutput(root, mission, constraints, roleHints, opts = {}) {
|
|
899
|
+
const { systemPrompt, userPrompt } = buildPlannerPrompt(mission, constraints, roleHints);
|
|
900
|
+
|
|
901
|
+
if (opts._plannerOutput) {
|
|
902
|
+
return opts._plannerOutput;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (opts.plannerOutputFile) {
|
|
906
|
+
const plannerOutputPath = resolve(opts.plannerOutputFile);
|
|
907
|
+
let raw;
|
|
908
|
+
try {
|
|
909
|
+
raw = readFileSync(plannerOutputPath, 'utf8');
|
|
910
|
+
} catch (error) {
|
|
911
|
+
throw new Error(`Planner output file read error: ${error.message}`);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const parsed = parsePlannerResponse(raw);
|
|
915
|
+
if (!parsed.ok) {
|
|
916
|
+
throw new Error(`Planner output file parse error: ${parsed.error}`);
|
|
917
|
+
}
|
|
918
|
+
return parsed.data;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const { loadConfig } = await import('../lib/config.js');
|
|
922
|
+
const config = loadConfig(root);
|
|
923
|
+
const plannerConfig = config?.mission_planner || config?.api_proxy;
|
|
924
|
+
|
|
925
|
+
if (!plannerConfig || !plannerConfig.base_url || !plannerConfig.model) {
|
|
926
|
+
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.');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const response = await callPlannerLLM(plannerConfig, systemPrompt, userPrompt);
|
|
930
|
+
const parsed = parsePlannerResponse(response);
|
|
931
|
+
if (!parsed.ok) {
|
|
932
|
+
throw new Error(`Planner response parse error: ${parsed.error}`);
|
|
933
|
+
}
|
|
934
|
+
return parsed.data;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function renderMissionPlanError(error) {
|
|
938
|
+
const message = error?.message || 'Mission planning failed.';
|
|
939
|
+
const [firstLine, ...rest] = String(message).split('\n');
|
|
940
|
+
console.error(chalk.red(firstLine));
|
|
941
|
+
for (const line of rest) {
|
|
942
|
+
console.error(chalk.dim(line));
|
|
943
|
+
}
|
|
944
|
+
if (Array.isArray(error?.validationErrors) && error.validationErrors.length > 0) {
|
|
945
|
+
for (const validationError of error.validationErrors) {
|
|
946
|
+
console.error(chalk.red(` • ${validationError}`));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
694
951
|
function renderMissionSnapshot(snapshot) {
|
|
695
952
|
console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
|
|
696
953
|
console.log('');
|
package/src/lib/mission-plans.js
CHANGED
|
@@ -454,6 +454,29 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
|
|
|
454
454
|
return { ok: true, plan, workstream: ws };
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// ── Batch launch helpers ───────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Return all workstreams with launch_status === 'ready', in plan order.
|
|
461
|
+
*/
|
|
462
|
+
export function getReadyWorkstreams(plan) {
|
|
463
|
+
if (!plan || !Array.isArray(plan.workstreams)) return [];
|
|
464
|
+
return plan.workstreams.filter((ws) => ws.launch_status === 'ready');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Return a summary of workstream status distribution for operator messaging.
|
|
469
|
+
*/
|
|
470
|
+
export function getWorkstreamStatusSummary(plan) {
|
|
471
|
+
if (!plan || !Array.isArray(plan.workstreams)) return {};
|
|
472
|
+
const summary = {};
|
|
473
|
+
for (const ws of plan.workstreams) {
|
|
474
|
+
const status = ws.launch_status || 'unknown';
|
|
475
|
+
summary[status] = (summary[status] || 0) + 1;
|
|
476
|
+
}
|
|
477
|
+
return summary;
|
|
478
|
+
}
|
|
479
|
+
|
|
457
480
|
// ── LLM planner prompt ──────────────────────────────────────────────────────
|
|
458
481
|
|
|
459
482
|
/**
|