agentxchain 2.112.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.
@@ -1,5 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { findProjectRoot } from '../lib/config.js';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { findProjectRoot, loadProjectContext } from '../lib/config.js';
3
5
  import {
4
6
  attachChainToMission,
5
7
  buildMissionListSummary,
@@ -10,6 +12,22 @@ import {
10
12
  loadMissionArtifact,
11
13
  loadMissionSnapshot,
12
14
  } from '../lib/missions.js';
15
+ import {
16
+ approvePlanArtifact,
17
+ createPlanArtifact,
18
+ getReadyWorkstreams,
19
+ getWorkstreamStatusSummary,
20
+ launchWorkstream,
21
+ markWorkstreamOutcome,
22
+ loadAllPlans,
23
+ loadLatestPlan,
24
+ loadPlan,
25
+ buildPlannerPrompt,
26
+ parsePlannerResponse,
27
+ validatePlannerOutput,
28
+ } from '../lib/mission-plans.js';
29
+ import { executeChainedRun } from '../lib/run-chain.js';
30
+ import { executeGovernedRun } from './run.js';
13
31
 
14
32
  export async function missionStartCommand(opts) {
15
33
  const root = findProjectRoot(opts.dir || process.cwd());
@@ -40,6 +58,26 @@ export async function missionStartCommand(opts) {
40
58
  }
41
59
 
42
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
+
43
81
  if (opts.json) {
44
82
  console.log(JSON.stringify(snapshot, null, 2));
45
83
  return;
@@ -163,6 +201,753 @@ export async function missionAttachChainCommand(chainId, opts) {
163
201
  renderMissionSnapshot(snapshot);
164
202
  }
165
203
 
204
+ // ── Mission Plan Commands ────────────────────────────────────────────────────
205
+
206
+ /**
207
+ * agentxchain mission plan [mission_id|latest] — generate a decomposition plan.
208
+ *
209
+ * Uses LLM-assisted one-shot generation with schema validation.
210
+ * Falls back to deterministic stub when no LLM is available (for testing).
211
+ */
212
+ export async function missionPlanCommand(missionTarget, opts) {
213
+ const root = findProjectRoot(opts.dir || process.cwd());
214
+ if (!root) {
215
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
216
+ process.exit(1);
217
+ }
218
+
219
+ // Resolve mission target
220
+ const mission = missionTarget && missionTarget !== 'latest'
221
+ ? loadMissionArtifact(root, missionTarget)
222
+ : loadLatestMissionArtifact(root);
223
+
224
+ if (!mission) {
225
+ if (missionTarget && missionTarget !== 'latest') {
226
+ console.error(chalk.red(`Mission not found: ${missionTarget}`));
227
+ } else {
228
+ console.error(chalk.red('No missions found.'));
229
+ console.error(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` first.'));
230
+ }
231
+ process.exit(1);
232
+ }
233
+
234
+ if (!mission.goal || !mission.goal.trim()) {
235
+ console.error(chalk.red(`Mission "${mission.mission_id}" has no goal text. The planner cannot operate on missing mission intent.`));
236
+ process.exit(1);
237
+ }
238
+ let plan;
239
+ try {
240
+ plan = await createMissionPlan(root, mission, opts);
241
+ } catch (error) {
242
+ renderMissionPlanError(error);
243
+ process.exit(1);
244
+ }
245
+
246
+ if (opts.json) {
247
+ console.log(JSON.stringify(plan, null, 2));
248
+ return;
249
+ }
250
+
251
+ console.log(chalk.green(`Created plan ${plan.plan_id} for mission ${mission.mission_id}`));
252
+ renderPlan(plan);
253
+ }
254
+
255
+ /**
256
+ * agentxchain mission plan show [plan_id|latest] — show a decomposition plan.
257
+ */
258
+ export async function missionPlanShowCommand(planTarget, opts) {
259
+ const root = findProjectRoot(opts.dir || process.cwd());
260
+ if (!root) {
261
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
262
+ process.exit(1);
263
+ }
264
+
265
+ // Resolve mission context
266
+ const mission = opts.mission
267
+ ? loadMissionArtifact(root, opts.mission)
268
+ : loadLatestMissionArtifact(root);
269
+
270
+ if (!mission) {
271
+ console.error(chalk.red('No mission found.'));
272
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
273
+ process.exit(1);
274
+ }
275
+
276
+ // Resolve plan target
277
+ const plan = planTarget && planTarget !== 'latest'
278
+ ? loadPlan(root, mission.mission_id, planTarget)
279
+ : loadLatestPlan(root, mission.mission_id);
280
+
281
+ if (!plan) {
282
+ if (planTarget && planTarget !== 'latest') {
283
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
284
+ } else {
285
+ console.log(chalk.dim('No plans found for this mission.'));
286
+ console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
287
+ }
288
+ return;
289
+ }
290
+
291
+ if (opts.json) {
292
+ console.log(JSON.stringify(plan, null, 2));
293
+ return;
294
+ }
295
+
296
+ renderPlan(plan);
297
+ }
298
+
299
+ /**
300
+ * agentxchain mission plan list — list all plans for a mission.
301
+ */
302
+ export async function missionPlanListCommand(opts) {
303
+ const root = findProjectRoot(opts.dir || process.cwd());
304
+ if (!root) {
305
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
306
+ process.exit(1);
307
+ }
308
+
309
+ const mission = opts.mission
310
+ ? loadMissionArtifact(root, opts.mission)
311
+ : loadLatestMissionArtifact(root);
312
+
313
+ if (!mission) {
314
+ console.error(chalk.red('No mission found.'));
315
+ process.exit(1);
316
+ }
317
+
318
+ const plans = loadAllPlans(root, mission.mission_id);
319
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
320
+ const limited = plans.slice(0, limit);
321
+
322
+ if (opts.json) {
323
+ console.log(JSON.stringify(limited, null, 2));
324
+ return;
325
+ }
326
+
327
+ if (limited.length === 0) {
328
+ console.log(chalk.dim('No plans found for this mission.'));
329
+ console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
330
+ return;
331
+ }
332
+
333
+ const header = [
334
+ pad('#', 4),
335
+ pad('Plan ID', 36),
336
+ pad('Status', 12),
337
+ pad('Workstreams', 12),
338
+ pad('Supersedes', 36),
339
+ pad('Created', 22),
340
+ ].join(' ');
341
+
342
+ console.log(chalk.bold(header));
343
+ console.log(chalk.dim('─'.repeat(header.length)));
344
+
345
+ limited.forEach((plan, i) => {
346
+ console.log([
347
+ pad(String(i + 1), 4),
348
+ pad(plan.plan_id || '—', 36),
349
+ pad(formatPlanStatus(plan.status), 12),
350
+ pad(String(plan.workstreams?.length || 0), 12),
351
+ pad(plan.supersedes_plan_id || '—', 36),
352
+ pad(formatTimestamp(plan.created_at), 22),
353
+ ].join(' '));
354
+ });
355
+
356
+ console.log(chalk.dim(`\n${limited.length} plan(s) shown`));
357
+ }
358
+
359
+ /**
360
+ * agentxchain mission plan approve [plan_id|latest] — approve a decomposition plan.
361
+ */
362
+ export async function missionPlanApproveCommand(planTarget, opts) {
363
+ const root = findProjectRoot(opts.dir || process.cwd());
364
+ if (!root) {
365
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
366
+ process.exit(1);
367
+ }
368
+
369
+ const mission = opts.mission
370
+ ? loadMissionArtifact(root, opts.mission)
371
+ : loadLatestMissionArtifact(root);
372
+
373
+ if (!mission) {
374
+ console.error(chalk.red('No mission found.'));
375
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
376
+ process.exit(1);
377
+ }
378
+
379
+ const plan = planTarget && planTarget !== 'latest'
380
+ ? loadPlan(root, mission.mission_id, planTarget)
381
+ : loadLatestPlan(root, mission.mission_id);
382
+
383
+ if (!plan) {
384
+ if (planTarget && planTarget !== 'latest') {
385
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
386
+ } else {
387
+ console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
388
+ console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
389
+ }
390
+ process.exit(1);
391
+ }
392
+
393
+ const result = approvePlanArtifact(root, mission.mission_id, plan.plan_id);
394
+ if (!result.ok) {
395
+ console.error(chalk.red(result.error));
396
+ process.exit(1);
397
+ }
398
+
399
+ console.log(chalk.green(`Approved plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
400
+ if (result.supersededPlanIds.length > 0) {
401
+ console.log(chalk.dim(` Superseded: ${result.supersededPlanIds.join(', ')}`));
402
+ }
403
+ renderPlan(result.plan);
404
+ }
405
+
406
+ /**
407
+ * agentxchain mission plan launch [plan_id|latest] --workstream <id> — launch a workstream.
408
+ *
409
+ * Validates plan approval, workstream existence, dependency satisfaction.
410
+ * Records launch_record with workstream_id → chain_id binding.
411
+ */
412
+ export async function missionPlanLaunchCommand(planTarget, opts) {
413
+ const context = loadProjectContext(opts.dir || process.cwd());
414
+ if (!context) {
415
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
416
+ process.exit(1);
417
+ }
418
+ const { root } = context;
419
+
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.'));
423
+ process.exit(1);
424
+ }
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
+
436
+ const mission = opts.mission
437
+ ? loadMissionArtifact(root, opts.mission)
438
+ : loadLatestMissionArtifact(root);
439
+
440
+ if (!mission) {
441
+ console.error(chalk.red('No mission found.'));
442
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
443
+ process.exit(1);
444
+ }
445
+
446
+ const plan = planTarget && planTarget !== 'latest'
447
+ ? loadPlan(root, mission.mission_id, planTarget)
448
+ : loadLatestPlan(root, mission.mission_id);
449
+
450
+ if (!plan) {
451
+ if (planTarget && planTarget !== 'latest') {
452
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
453
+ } else {
454
+ console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
455
+ console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
456
+ }
457
+ process.exit(1);
458
+ }
459
+
460
+ const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
461
+ if (!launch.ok) {
462
+ console.error(chalk.red(launch.error));
463
+ process.exit(1);
464
+ }
465
+
466
+ const executor = opts._executeGovernedRun || executeGovernedRun;
467
+ const logger = opts._log || console.log;
468
+ const chainOpts = {
469
+ enabled: true,
470
+ maxChains: 0,
471
+ chainOn: ['completed'],
472
+ cooldownSeconds: 0,
473
+ mission: mission.mission_id,
474
+ chainId: launch.chainId,
475
+ };
476
+ const runOpts = {
477
+ autoApprove: !!opts.autoApprove,
478
+ provenance: {
479
+ trigger: 'manual',
480
+ created_by: 'operator',
481
+ trigger_reason: `mission:${mission.mission_id} workstream:${opts.workstream}`,
482
+ },
483
+ };
484
+
485
+ let execution;
486
+ try {
487
+ execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
488
+ } catch (error) {
489
+ markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
490
+ terminalReason: 'execution_error',
491
+ completedAt: new Date().toISOString(),
492
+ });
493
+ console.error(chalk.red(`Workstream execution failed: ${error.message}`));
494
+ process.exit(1);
495
+ }
496
+
497
+ const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
498
+ const terminalReason = lastRun?.status === 'completed'
499
+ ? 'completed'
500
+ : (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
501
+ const outcome = markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
502
+ terminalReason,
503
+ completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
504
+ });
505
+ if (!outcome.ok) {
506
+ console.error(chalk.red(outcome.error));
507
+ process.exit(1);
508
+ }
509
+
510
+ if (opts.json) {
511
+ console.log(JSON.stringify({
512
+ workstream_id: opts.workstream,
513
+ chain_id: launch.chainId,
514
+ plan_id: launch.plan.plan_id,
515
+ mission_id: mission.mission_id,
516
+ launch_record: launch.launchRecord,
517
+ exit_code: execution.exitCode,
518
+ chain_terminal_reason: execution?.chainReport?.terminal_reason || null,
519
+ workstream_status: outcome.workstream.launch_status,
520
+ }, null, 2));
521
+ if (execution.exitCode !== 0) {
522
+ process.exit(execution.exitCode);
523
+ }
524
+ return;
525
+ }
526
+
527
+ console.log(chalk.green(`Executed workstream ${chalk.bold(opts.workstream)} → chain ${chalk.bold(launch.chainId)}`));
528
+ console.log('');
529
+ console.log(chalk.dim(` Mission: ${mission.mission_id}`));
530
+ console.log(chalk.dim(` Plan: ${launch.plan.plan_id}`));
531
+ console.log(chalk.dim(` Chain ID: ${launch.chainId}`));
532
+ console.log(chalk.dim(` Outcome: ${outcome.workstream.launch_status}`));
533
+ console.log('');
534
+ renderPlan(outcome.plan);
535
+ if (execution.exitCode !== 0) {
536
+ console.error(chalk.red(`Workstream execution ended with exit code ${execution.exitCode}.`));
537
+ process.exit(execution.exitCode);
538
+ }
539
+ }
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
+
730
+ // ── Plan rendering ───────────────────────────────────────────────────────────
731
+
732
+ function renderPlan(plan) {
733
+ console.log(chalk.bold(`Plan: ${plan.plan_id}`));
734
+ console.log('');
735
+ console.log(` Mission: ${plan.mission_id}`);
736
+ console.log(` Status: ${formatPlanStatus(plan.status)}`);
737
+ console.log(` Goal: ${plan.input?.goal || '—'}`);
738
+ console.log(` Constraints: ${plan.input?.constraints?.length ? plan.input.constraints.join('; ') : 'none'}`);
739
+ console.log(` Role hints: ${plan.input?.role_hints?.length ? plan.input.role_hints.join(', ') : 'none'}`);
740
+ console.log(` Supersedes: ${plan.supersedes_plan_id || '—'}`);
741
+ if (plan.superseded_by_plan_id) {
742
+ console.log(` Superseded by:${plan.superseded_by_plan_id}`);
743
+ }
744
+ if (plan.approved_at) {
745
+ console.log(` Approved: ${plan.approved_at}`);
746
+ }
747
+ console.log(` Created: ${plan.created_at || '—'}`);
748
+ console.log('');
749
+
750
+ if (!plan.workstreams || plan.workstreams.length === 0) {
751
+ console.log(chalk.dim(' No workstreams.'));
752
+ return;
753
+ }
754
+
755
+ const wsHeader = [
756
+ pad('#', 4),
757
+ pad('Workstream', 28),
758
+ pad('Status', 10),
759
+ pad('Roles', 20),
760
+ pad('Phases', 24),
761
+ pad('Depends On', 28),
762
+ 'Title',
763
+ ].join(' ');
764
+
765
+ console.log(chalk.bold(' Workstreams:'));
766
+ console.log(` ${chalk.dim(wsHeader)}`);
767
+ console.log(` ${chalk.dim('─'.repeat(wsHeader.length))}`);
768
+
769
+ plan.workstreams.forEach((ws, i) => {
770
+ const deps = Array.isArray(ws.depends_on) && ws.depends_on.length > 0
771
+ ? ws.depends_on.join(', ')
772
+ : '—';
773
+
774
+ console.log(` ${[
775
+ pad(String(i + 1), 4),
776
+ pad(ws.workstream_id || '—', 28),
777
+ pad(formatLaunchStatus(ws.launch_status), 10),
778
+ pad((ws.roles || []).join(', '), 20),
779
+ pad((ws.phases || []).join(', '), 24),
780
+ pad(deps, 28),
781
+ ws.title || '—',
782
+ ].join(' ')}`);
783
+ });
784
+
785
+ if (plan.launch_records?.length) {
786
+ console.log('');
787
+ console.log(chalk.bold(' Launch records:'));
788
+ for (const rec of plan.launch_records) {
789
+ const statusTag = rec.status === 'completed' ? chalk.green('completed')
790
+ : rec.status === 'failed' ? chalk.red('failed')
791
+ : chalk.cyan('launched');
792
+ console.log(` ${chalk.cyan(rec.workstream_id)} → ${rec.chain_id} [${statusTag}]`);
793
+ }
794
+ }
795
+
796
+ if (plan.workstreams.some((ws) => ws.acceptance_checks?.length)) {
797
+ console.log('');
798
+ console.log(chalk.bold(' Acceptance checks:'));
799
+ plan.workstreams.forEach((ws) => {
800
+ if (ws.acceptance_checks?.length) {
801
+ console.log(` ${chalk.cyan(ws.workstream_id)}:`);
802
+ ws.acceptance_checks.forEach((check) => {
803
+ console.log(` • ${check}`);
804
+ });
805
+ }
806
+ });
807
+ }
808
+ }
809
+
810
+ function formatPlanStatus(status) {
811
+ if (!status) return '—';
812
+ switch (status) {
813
+ case 'proposed': return chalk.blue('proposed');
814
+ case 'approved': return chalk.green('approved');
815
+ case 'superseded': return chalk.dim('superseded');
816
+ case 'needs_attention': return chalk.yellow('needs_attention');
817
+ case 'completed': return chalk.green('completed');
818
+ default: return status;
819
+ }
820
+ }
821
+
822
+ function formatLaunchStatus(status) {
823
+ if (!status) return '—';
824
+ switch (status) {
825
+ case 'ready': return chalk.green('ready');
826
+ case 'blocked': return chalk.yellow('blocked');
827
+ case 'launched': return chalk.cyan('launched');
828
+ case 'completed': return chalk.green('completed');
829
+ case 'needs_attention': return chalk.red('attention');
830
+ default: return status;
831
+ }
832
+ }
833
+
834
+ // ── LLM planner call ─────────────────────────────────────────────────────────
835
+
836
+ async function callPlannerLLM(config, systemPrompt, userPrompt) {
837
+ const url = `${config.base_url.replace(/\/$/, '')}/chat/completions`;
838
+ const headers = { 'Content-Type': 'application/json' };
839
+ if (config.api_key) {
840
+ headers['Authorization'] = `Bearer ${config.api_key}`;
841
+ }
842
+
843
+ const body = {
844
+ model: config.model,
845
+ messages: [
846
+ { role: 'system', content: systemPrompt },
847
+ { role: 'user', content: userPrompt },
848
+ ],
849
+ temperature: 0.3,
850
+ max_tokens: 4096,
851
+ };
852
+
853
+ const response = await fetch(url, {
854
+ method: 'POST',
855
+ headers,
856
+ body: JSON.stringify(body),
857
+ });
858
+
859
+ if (!response.ok) {
860
+ const text = await response.text().catch(() => '');
861
+ throw new Error(`LLM API returned ${response.status}: ${text.slice(0, 200)}`);
862
+ }
863
+
864
+ const json = await response.json();
865
+ const content = json?.choices?.[0]?.message?.content;
866
+ if (!content) {
867
+ throw new Error('LLM API returned no content in response.');
868
+ }
869
+
870
+ return content;
871
+ }
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
+
166
951
  function renderMissionSnapshot(snapshot) {
167
952
  console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
168
953
  console.log('');