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.
@@ -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 a workstream from an approved plan (default: latest plan)')
487
- .requiredOption('-w, --workstream <id>', 'Workstream ID to launch')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.113.0",
3
+ "version": "2.114.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- const constraints = Array.isArray(opts.constraint) ? opts.constraint : (opts.constraint ? [opts.constraint] : []);
216
- const roleHints = Array.isArray(opts.roleHint) ? opts.roleHint : (opts.roleHint ? [opts.roleHint] : []);
217
-
218
- // Build prompt and attempt LLM call, or use deterministic fallback
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(result.plan, null, 2));
247
+ console.log(JSON.stringify(plan, null, 2));
269
248
  return;
270
249
  }
271
250
 
272
- console.log(chalk.green(`Created plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
273
- renderPlan(result.plan);
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
- if (!opts.workstream) {
442
- console.error(chalk.red('--workstream <id> is required. Specify which workstream to launch.'));
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('');
@@ -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
  /**