agentxchain 2.111.0 → 2.113.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.
@@ -0,0 +1,780 @@
1
+ import chalk from 'chalk';
2
+ import { findProjectRoot, loadProjectContext } from '../lib/config.js';
3
+ import {
4
+ attachChainToMission,
5
+ buildMissionListSummary,
6
+ buildMissionSnapshot,
7
+ createMission,
8
+ loadLatestMissionArtifact,
9
+ loadLatestMissionSnapshot,
10
+ loadMissionArtifact,
11
+ loadMissionSnapshot,
12
+ } from '../lib/missions.js';
13
+ import {
14
+ approvePlanArtifact,
15
+ createPlanArtifact,
16
+ launchWorkstream,
17
+ markWorkstreamOutcome,
18
+ loadAllPlans,
19
+ loadLatestPlan,
20
+ loadPlan,
21
+ buildPlannerPrompt,
22
+ parsePlannerResponse,
23
+ validatePlannerOutput,
24
+ } from '../lib/mission-plans.js';
25
+ import { executeChainedRun } from '../lib/run-chain.js';
26
+ import { executeGovernedRun } from './run.js';
27
+
28
+ export async function missionStartCommand(opts) {
29
+ const root = findProjectRoot(opts.dir || process.cwd());
30
+ if (!root) {
31
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
32
+ process.exit(1);
33
+ }
34
+
35
+ const title = String(opts.title || '').trim();
36
+ const goal = String(opts.goal || '').trim();
37
+ if (!title) {
38
+ console.error(chalk.red('Mission title is required. Use --title <text>.'));
39
+ process.exit(1);
40
+ }
41
+ if (!goal) {
42
+ console.error(chalk.red('Mission goal is required. Use --goal <text>.'));
43
+ process.exit(1);
44
+ }
45
+
46
+ const result = createMission(root, {
47
+ missionId: opts.id,
48
+ title,
49
+ goal,
50
+ });
51
+ if (!result.ok) {
52
+ console.error(chalk.red(result.error));
53
+ process.exit(1);
54
+ }
55
+
56
+ const snapshot = buildMissionSnapshot(root, result.mission);
57
+ if (opts.json) {
58
+ console.log(JSON.stringify(snapshot, null, 2));
59
+ return;
60
+ }
61
+
62
+ console.log(chalk.green(`Created mission ${snapshot.mission_id}`));
63
+ console.log(chalk.dim(` Goal: ${snapshot.goal}`));
64
+ renderMissionSnapshot(snapshot);
65
+ }
66
+
67
+ export async function missionListCommand(opts) {
68
+ const root = findProjectRoot(opts.dir || process.cwd());
69
+ if (!root) {
70
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
71
+ process.exit(1);
72
+ }
73
+
74
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
75
+ const missions = buildMissionListSummary(root, limit);
76
+
77
+ if (opts.json) {
78
+ console.log(JSON.stringify(missions, null, 2));
79
+ return;
80
+ }
81
+
82
+ if (missions.length === 0) {
83
+ console.log(chalk.dim('No missions found.'));
84
+ console.log(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` to create one.'));
85
+ return;
86
+ }
87
+
88
+ const header = [
89
+ pad('#', 4),
90
+ pad('Mission ID', 28),
91
+ pad('Status', 18),
92
+ pad('Chains', 8),
93
+ pad('Runs', 7),
94
+ pad('Turns', 7),
95
+ pad('Decisions', 10),
96
+ pad('Updated', 22),
97
+ 'Title',
98
+ ].join(' ');
99
+
100
+ console.log(chalk.bold(header));
101
+ console.log(chalk.dim('─'.repeat(header.length)));
102
+
103
+ missions.forEach((mission, index) => {
104
+ console.log([
105
+ pad(String(index + 1), 4),
106
+ pad(mission.mission_id || '—', 28),
107
+ pad(formatMissionStatus(mission.derived_status), 18),
108
+ pad(String(mission.chain_count || 0), 8),
109
+ pad(String(mission.total_runs || 0), 7),
110
+ pad(String(mission.total_turns || 0), 7),
111
+ pad(String(mission.active_repo_decisions_count || 0), 10),
112
+ pad(formatTimestamp(mission.updated_at), 22),
113
+ mission.title || '—',
114
+ ].join(' '));
115
+ });
116
+
117
+ console.log(chalk.dim(`\n${missions.length} mission(s) shown`));
118
+ }
119
+
120
+ export async function missionShowCommand(missionId, opts) {
121
+ const root = findProjectRoot(opts.dir || process.cwd());
122
+ if (!root) {
123
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
124
+ process.exit(1);
125
+ }
126
+
127
+ const snapshot = missionId
128
+ ? loadMissionSnapshot(root, missionId)
129
+ : loadLatestMissionSnapshot(root);
130
+ if (!snapshot) {
131
+ if (missionId) {
132
+ console.error(chalk.red(`Mission not found: ${missionId}`));
133
+ process.exit(1);
134
+ }
135
+ console.log(chalk.dim('No missions found.'));
136
+ console.log(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` to create one.'));
137
+ return;
138
+ }
139
+
140
+ if (opts.json) {
141
+ console.log(JSON.stringify(snapshot, null, 2));
142
+ return;
143
+ }
144
+
145
+ renderMissionSnapshot(snapshot);
146
+ }
147
+
148
+ export async function missionAttachChainCommand(chainId, opts) {
149
+ const root = findProjectRoot(opts.dir || process.cwd());
150
+ if (!root) {
151
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
152
+ process.exit(1);
153
+ }
154
+
155
+ const mission = opts.mission
156
+ ? loadMissionArtifact(root, opts.mission)
157
+ : loadLatestMissionArtifact(root);
158
+ if (!mission) {
159
+ console.error(chalk.red('No mission found to attach to.'));
160
+ console.error(chalk.dim(' Use `agentxchain mission start --title "..." --goal "..."` first.'));
161
+ process.exit(1);
162
+ }
163
+
164
+ const result = attachChainToMission(root, mission.mission_id, chainId || 'latest');
165
+ if (!result.ok) {
166
+ console.error(chalk.red(result.error));
167
+ process.exit(1);
168
+ }
169
+
170
+ const snapshot = buildMissionSnapshot(root, result.mission);
171
+ if (opts.json) {
172
+ console.log(JSON.stringify(snapshot, null, 2));
173
+ return;
174
+ }
175
+
176
+ console.log(chalk.green(`Attached ${result.chain.chain_id} to ${snapshot.mission_id}`));
177
+ renderMissionSnapshot(snapshot);
178
+ }
179
+
180
+ // ── Mission Plan Commands ────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * agentxchain mission plan [mission_id|latest] — generate a decomposition plan.
184
+ *
185
+ * Uses LLM-assisted one-shot generation with schema validation.
186
+ * Falls back to deterministic stub when no LLM is available (for testing).
187
+ */
188
+ export async function missionPlanCommand(missionTarget, opts) {
189
+ const root = findProjectRoot(opts.dir || process.cwd());
190
+ if (!root) {
191
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
192
+ process.exit(1);
193
+ }
194
+
195
+ // Resolve mission target
196
+ const mission = missionTarget && missionTarget !== 'latest'
197
+ ? loadMissionArtifact(root, missionTarget)
198
+ : loadLatestMissionArtifact(root);
199
+
200
+ if (!mission) {
201
+ if (missionTarget && missionTarget !== 'latest') {
202
+ console.error(chalk.red(`Mission not found: ${missionTarget}`));
203
+ } else {
204
+ console.error(chalk.red('No missions found.'));
205
+ console.error(chalk.dim(' Run `agentxchain mission start --title "..." --goal "..."` first.'));
206
+ }
207
+ process.exit(1);
208
+ }
209
+
210
+ if (!mission.goal || !mission.goal.trim()) {
211
+ console.error(chalk.red(`Mission "${mission.mission_id}" has no goal text. The planner cannot operate on missing mission intent.`));
212
+ process.exit(1);
213
+ }
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
+ }
264
+ process.exit(1);
265
+ }
266
+
267
+ if (opts.json) {
268
+ console.log(JSON.stringify(result.plan, null, 2));
269
+ return;
270
+ }
271
+
272
+ console.log(chalk.green(`Created plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
273
+ renderPlan(result.plan);
274
+ }
275
+
276
+ /**
277
+ * agentxchain mission plan show [plan_id|latest] — show a decomposition plan.
278
+ */
279
+ export async function missionPlanShowCommand(planTarget, opts) {
280
+ const root = findProjectRoot(opts.dir || process.cwd());
281
+ if (!root) {
282
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
283
+ process.exit(1);
284
+ }
285
+
286
+ // Resolve mission context
287
+ const mission = opts.mission
288
+ ? loadMissionArtifact(root, opts.mission)
289
+ : loadLatestMissionArtifact(root);
290
+
291
+ if (!mission) {
292
+ console.error(chalk.red('No mission found.'));
293
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
294
+ process.exit(1);
295
+ }
296
+
297
+ // Resolve plan target
298
+ const plan = planTarget && planTarget !== 'latest'
299
+ ? loadPlan(root, mission.mission_id, planTarget)
300
+ : loadLatestPlan(root, mission.mission_id);
301
+
302
+ if (!plan) {
303
+ if (planTarget && planTarget !== 'latest') {
304
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
305
+ } else {
306
+ console.log(chalk.dim('No plans found for this mission.'));
307
+ console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
308
+ }
309
+ return;
310
+ }
311
+
312
+ if (opts.json) {
313
+ console.log(JSON.stringify(plan, null, 2));
314
+ return;
315
+ }
316
+
317
+ renderPlan(plan);
318
+ }
319
+
320
+ /**
321
+ * agentxchain mission plan list — list all plans for a mission.
322
+ */
323
+ export async function missionPlanListCommand(opts) {
324
+ const root = findProjectRoot(opts.dir || process.cwd());
325
+ if (!root) {
326
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
327
+ process.exit(1);
328
+ }
329
+
330
+ const mission = opts.mission
331
+ ? loadMissionArtifact(root, opts.mission)
332
+ : loadLatestMissionArtifact(root);
333
+
334
+ if (!mission) {
335
+ console.error(chalk.red('No mission found.'));
336
+ process.exit(1);
337
+ }
338
+
339
+ const plans = loadAllPlans(root, mission.mission_id);
340
+ const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
341
+ const limited = plans.slice(0, limit);
342
+
343
+ if (opts.json) {
344
+ console.log(JSON.stringify(limited, null, 2));
345
+ return;
346
+ }
347
+
348
+ if (limited.length === 0) {
349
+ console.log(chalk.dim('No plans found for this mission.'));
350
+ console.log(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
351
+ return;
352
+ }
353
+
354
+ const header = [
355
+ pad('#', 4),
356
+ pad('Plan ID', 36),
357
+ pad('Status', 12),
358
+ pad('Workstreams', 12),
359
+ pad('Supersedes', 36),
360
+ pad('Created', 22),
361
+ ].join(' ');
362
+
363
+ console.log(chalk.bold(header));
364
+ console.log(chalk.dim('─'.repeat(header.length)));
365
+
366
+ limited.forEach((plan, i) => {
367
+ console.log([
368
+ pad(String(i + 1), 4),
369
+ pad(plan.plan_id || '—', 36),
370
+ pad(formatPlanStatus(plan.status), 12),
371
+ pad(String(plan.workstreams?.length || 0), 12),
372
+ pad(plan.supersedes_plan_id || '—', 36),
373
+ pad(formatTimestamp(plan.created_at), 22),
374
+ ].join(' '));
375
+ });
376
+
377
+ console.log(chalk.dim(`\n${limited.length} plan(s) shown`));
378
+ }
379
+
380
+ /**
381
+ * agentxchain mission plan approve [plan_id|latest] — approve a decomposition plan.
382
+ */
383
+ export async function missionPlanApproveCommand(planTarget, opts) {
384
+ const root = findProjectRoot(opts.dir || process.cwd());
385
+ if (!root) {
386
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
387
+ process.exit(1);
388
+ }
389
+
390
+ const mission = opts.mission
391
+ ? loadMissionArtifact(root, opts.mission)
392
+ : loadLatestMissionArtifact(root);
393
+
394
+ if (!mission) {
395
+ console.error(chalk.red('No mission found.'));
396
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
397
+ process.exit(1);
398
+ }
399
+
400
+ const plan = planTarget && planTarget !== 'latest'
401
+ ? loadPlan(root, mission.mission_id, planTarget)
402
+ : loadLatestPlan(root, mission.mission_id);
403
+
404
+ if (!plan) {
405
+ if (planTarget && planTarget !== 'latest') {
406
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
407
+ } else {
408
+ console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
409
+ console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
410
+ }
411
+ process.exit(1);
412
+ }
413
+
414
+ const result = approvePlanArtifact(root, mission.mission_id, plan.plan_id);
415
+ if (!result.ok) {
416
+ console.error(chalk.red(result.error));
417
+ process.exit(1);
418
+ }
419
+
420
+ console.log(chalk.green(`Approved plan ${result.plan.plan_id} for mission ${mission.mission_id}`));
421
+ if (result.supersededPlanIds.length > 0) {
422
+ console.log(chalk.dim(` Superseded: ${result.supersededPlanIds.join(', ')}`));
423
+ }
424
+ renderPlan(result.plan);
425
+ }
426
+
427
+ /**
428
+ * agentxchain mission plan launch [plan_id|latest] --workstream <id> — launch a workstream.
429
+ *
430
+ * Validates plan approval, workstream existence, dependency satisfaction.
431
+ * Records launch_record with workstream_id → chain_id binding.
432
+ */
433
+ export async function missionPlanLaunchCommand(planTarget, opts) {
434
+ const context = loadProjectContext(opts.dir || process.cwd());
435
+ if (!context) {
436
+ console.error(chalk.red('No AgentXchain project found. Run this inside a governed project.'));
437
+ process.exit(1);
438
+ }
439
+ const { root } = context;
440
+
441
+ if (!opts.workstream) {
442
+ console.error(chalk.red('--workstream <id> is required. Specify which workstream to launch.'));
443
+ process.exit(1);
444
+ }
445
+
446
+ const mission = opts.mission
447
+ ? loadMissionArtifact(root, opts.mission)
448
+ : loadLatestMissionArtifact(root);
449
+
450
+ if (!mission) {
451
+ console.error(chalk.red('No mission found.'));
452
+ console.error(chalk.dim(' Use --mission <id> or create a mission first.'));
453
+ process.exit(1);
454
+ }
455
+
456
+ const plan = planTarget && planTarget !== 'latest'
457
+ ? loadPlan(root, mission.mission_id, planTarget)
458
+ : loadLatestPlan(root, mission.mission_id);
459
+
460
+ if (!plan) {
461
+ if (planTarget && planTarget !== 'latest') {
462
+ console.error(chalk.red(`Plan not found: ${planTarget}`));
463
+ } else {
464
+ console.error(chalk.red(`No plans found for mission ${mission.mission_id}.`));
465
+ console.error(chalk.dim(' Run `agentxchain mission plan latest` to generate one.'));
466
+ }
467
+ process.exit(1);
468
+ }
469
+
470
+ const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
471
+ if (!launch.ok) {
472
+ console.error(chalk.red(launch.error));
473
+ process.exit(1);
474
+ }
475
+
476
+ const executor = opts._executeGovernedRun || executeGovernedRun;
477
+ const logger = opts._log || console.log;
478
+ const chainOpts = {
479
+ enabled: true,
480
+ maxChains: 0,
481
+ chainOn: ['completed'],
482
+ cooldownSeconds: 0,
483
+ mission: mission.mission_id,
484
+ chainId: launch.chainId,
485
+ };
486
+ const runOpts = {
487
+ autoApprove: !!opts.autoApprove,
488
+ provenance: {
489
+ trigger: 'manual',
490
+ created_by: 'operator',
491
+ trigger_reason: `mission:${mission.mission_id} workstream:${opts.workstream}`,
492
+ },
493
+ };
494
+
495
+ let execution;
496
+ try {
497
+ execution = await executeChainedRun(context, runOpts, chainOpts, executor, logger);
498
+ } catch (error) {
499
+ markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
500
+ terminalReason: 'execution_error',
501
+ completedAt: new Date().toISOString(),
502
+ });
503
+ console.error(chalk.red(`Workstream execution failed: ${error.message}`));
504
+ process.exit(1);
505
+ }
506
+
507
+ const lastRun = execution?.chainReport?.runs?.[execution.chainReport.runs.length - 1] || null;
508
+ const terminalReason = lastRun?.status === 'completed'
509
+ ? 'completed'
510
+ : (lastRun?.status || execution?.chainReport?.terminal_reason || 'execution_error');
511
+ const outcome = markWorkstreamOutcome(root, mission.mission_id, plan.plan_id, opts.workstream, {
512
+ terminalReason,
513
+ completedAt: execution?.chainReport?.completed_at || new Date().toISOString(),
514
+ });
515
+ if (!outcome.ok) {
516
+ console.error(chalk.red(outcome.error));
517
+ process.exit(1);
518
+ }
519
+
520
+ if (opts.json) {
521
+ console.log(JSON.stringify({
522
+ workstream_id: opts.workstream,
523
+ chain_id: launch.chainId,
524
+ plan_id: launch.plan.plan_id,
525
+ mission_id: mission.mission_id,
526
+ launch_record: launch.launchRecord,
527
+ exit_code: execution.exitCode,
528
+ chain_terminal_reason: execution?.chainReport?.terminal_reason || null,
529
+ workstream_status: outcome.workstream.launch_status,
530
+ }, null, 2));
531
+ if (execution.exitCode !== 0) {
532
+ process.exit(execution.exitCode);
533
+ }
534
+ return;
535
+ }
536
+
537
+ console.log(chalk.green(`Executed workstream ${chalk.bold(opts.workstream)} → chain ${chalk.bold(launch.chainId)}`));
538
+ console.log('');
539
+ console.log(chalk.dim(` Mission: ${mission.mission_id}`));
540
+ console.log(chalk.dim(` Plan: ${launch.plan.plan_id}`));
541
+ console.log(chalk.dim(` Chain ID: ${launch.chainId}`));
542
+ console.log(chalk.dim(` Outcome: ${outcome.workstream.launch_status}`));
543
+ console.log('');
544
+ renderPlan(outcome.plan);
545
+ if (execution.exitCode !== 0) {
546
+ console.error(chalk.red(`Workstream execution ended with exit code ${execution.exitCode}.`));
547
+ process.exit(execution.exitCode);
548
+ }
549
+ }
550
+
551
+ // ── Plan rendering ───────────────────────────────────────────────────────────
552
+
553
+ function renderPlan(plan) {
554
+ console.log(chalk.bold(`Plan: ${plan.plan_id}`));
555
+ console.log('');
556
+ console.log(` Mission: ${plan.mission_id}`);
557
+ console.log(` Status: ${formatPlanStatus(plan.status)}`);
558
+ console.log(` Goal: ${plan.input?.goal || '—'}`);
559
+ console.log(` Constraints: ${plan.input?.constraints?.length ? plan.input.constraints.join('; ') : 'none'}`);
560
+ console.log(` Role hints: ${plan.input?.role_hints?.length ? plan.input.role_hints.join(', ') : 'none'}`);
561
+ console.log(` Supersedes: ${plan.supersedes_plan_id || '—'}`);
562
+ if (plan.superseded_by_plan_id) {
563
+ console.log(` Superseded by:${plan.superseded_by_plan_id}`);
564
+ }
565
+ if (plan.approved_at) {
566
+ console.log(` Approved: ${plan.approved_at}`);
567
+ }
568
+ console.log(` Created: ${plan.created_at || '—'}`);
569
+ console.log('');
570
+
571
+ if (!plan.workstreams || plan.workstreams.length === 0) {
572
+ console.log(chalk.dim(' No workstreams.'));
573
+ return;
574
+ }
575
+
576
+ const wsHeader = [
577
+ pad('#', 4),
578
+ pad('Workstream', 28),
579
+ pad('Status', 10),
580
+ pad('Roles', 20),
581
+ pad('Phases', 24),
582
+ pad('Depends On', 28),
583
+ 'Title',
584
+ ].join(' ');
585
+
586
+ console.log(chalk.bold(' Workstreams:'));
587
+ console.log(` ${chalk.dim(wsHeader)}`);
588
+ console.log(` ${chalk.dim('─'.repeat(wsHeader.length))}`);
589
+
590
+ plan.workstreams.forEach((ws, i) => {
591
+ const deps = Array.isArray(ws.depends_on) && ws.depends_on.length > 0
592
+ ? ws.depends_on.join(', ')
593
+ : '—';
594
+
595
+ console.log(` ${[
596
+ pad(String(i + 1), 4),
597
+ pad(ws.workstream_id || '—', 28),
598
+ pad(formatLaunchStatus(ws.launch_status), 10),
599
+ pad((ws.roles || []).join(', '), 20),
600
+ pad((ws.phases || []).join(', '), 24),
601
+ pad(deps, 28),
602
+ ws.title || '—',
603
+ ].join(' ')}`);
604
+ });
605
+
606
+ if (plan.launch_records?.length) {
607
+ console.log('');
608
+ console.log(chalk.bold(' Launch records:'));
609
+ for (const rec of plan.launch_records) {
610
+ const statusTag = rec.status === 'completed' ? chalk.green('completed')
611
+ : rec.status === 'failed' ? chalk.red('failed')
612
+ : chalk.cyan('launched');
613
+ console.log(` ${chalk.cyan(rec.workstream_id)} → ${rec.chain_id} [${statusTag}]`);
614
+ }
615
+ }
616
+
617
+ if (plan.workstreams.some((ws) => ws.acceptance_checks?.length)) {
618
+ console.log('');
619
+ console.log(chalk.bold(' Acceptance checks:'));
620
+ plan.workstreams.forEach((ws) => {
621
+ if (ws.acceptance_checks?.length) {
622
+ console.log(` ${chalk.cyan(ws.workstream_id)}:`);
623
+ ws.acceptance_checks.forEach((check) => {
624
+ console.log(` • ${check}`);
625
+ });
626
+ }
627
+ });
628
+ }
629
+ }
630
+
631
+ function formatPlanStatus(status) {
632
+ if (!status) return '—';
633
+ switch (status) {
634
+ case 'proposed': return chalk.blue('proposed');
635
+ case 'approved': return chalk.green('approved');
636
+ case 'superseded': return chalk.dim('superseded');
637
+ case 'needs_attention': return chalk.yellow('needs_attention');
638
+ case 'completed': return chalk.green('completed');
639
+ default: return status;
640
+ }
641
+ }
642
+
643
+ function formatLaunchStatus(status) {
644
+ if (!status) return '—';
645
+ switch (status) {
646
+ case 'ready': return chalk.green('ready');
647
+ case 'blocked': return chalk.yellow('blocked');
648
+ case 'launched': return chalk.cyan('launched');
649
+ case 'completed': return chalk.green('completed');
650
+ case 'needs_attention': return chalk.red('attention');
651
+ default: return status;
652
+ }
653
+ }
654
+
655
+ // ── LLM planner call ─────────────────────────────────────────────────────────
656
+
657
+ async function callPlannerLLM(config, systemPrompt, userPrompt) {
658
+ const url = `${config.base_url.replace(/\/$/, '')}/chat/completions`;
659
+ const headers = { 'Content-Type': 'application/json' };
660
+ if (config.api_key) {
661
+ headers['Authorization'] = `Bearer ${config.api_key}`;
662
+ }
663
+
664
+ const body = {
665
+ model: config.model,
666
+ messages: [
667
+ { role: 'system', content: systemPrompt },
668
+ { role: 'user', content: userPrompt },
669
+ ],
670
+ temperature: 0.3,
671
+ max_tokens: 4096,
672
+ };
673
+
674
+ const response = await fetch(url, {
675
+ method: 'POST',
676
+ headers,
677
+ body: JSON.stringify(body),
678
+ });
679
+
680
+ if (!response.ok) {
681
+ const text = await response.text().catch(() => '');
682
+ throw new Error(`LLM API returned ${response.status}: ${text.slice(0, 200)}`);
683
+ }
684
+
685
+ const json = await response.json();
686
+ const content = json?.choices?.[0]?.message?.content;
687
+ if (!content) {
688
+ throw new Error('LLM API returned no content in response.');
689
+ }
690
+
691
+ return content;
692
+ }
693
+
694
+ function renderMissionSnapshot(snapshot) {
695
+ console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
696
+ console.log('');
697
+ console.log(` Title: ${snapshot.title || '—'}`);
698
+ console.log(` Goal: ${snapshot.goal || '—'}`);
699
+ console.log(` Status: ${formatMissionStatus(snapshot.derived_status)}`);
700
+ console.log(` Chains: ${snapshot.chain_count || 0}`);
701
+ console.log(` Total runs: ${snapshot.total_runs || 0}`);
702
+ console.log(` Total turns: ${snapshot.total_turns || 0}`);
703
+ console.log(` Active repo decisions: ${snapshot.active_repo_decisions_count || 0}`);
704
+ console.log(` Latest chain: ${snapshot.latest_chain_id || '—'}`);
705
+ console.log(` Latest terminal: ${snapshot.latest_terminal_reason || '—'}`);
706
+ console.log(` Created: ${snapshot.created_at || '—'}`);
707
+ console.log(` Updated: ${snapshot.updated_at || '—'}`);
708
+
709
+ if (snapshot.missing_chain_ids?.length) {
710
+ console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
711
+ }
712
+
713
+ if (!snapshot.chains || snapshot.chains.length === 0) {
714
+ console.log('');
715
+ console.log(chalk.dim(' No chains attached.'));
716
+ console.log(chalk.dim(' Use `agentxchain mission attach-chain latest` after a chained run.'));
717
+ return;
718
+ }
719
+
720
+ const header = [
721
+ pad('#', 4),
722
+ pad('Chain ID', 16),
723
+ pad('Runs', 6),
724
+ pad('Turns', 7),
725
+ pad('Terminal', 26),
726
+ pad('Started', 22),
727
+ ].join(' ');
728
+
729
+ console.log('');
730
+ console.log(chalk.bold(' Chains:'));
731
+ console.log(` ${chalk.dim(header)}`);
732
+ console.log(` ${chalk.dim('─'.repeat(header.length))}`);
733
+
734
+ snapshot.chains.forEach((chain, index) => {
735
+ console.log(` ${[
736
+ pad(String(index + 1), 4),
737
+ pad(chain.chain_id || '—', 16),
738
+ pad(String(chain.runs?.length || 0), 6),
739
+ pad(String(chain.total_turns || 0), 7),
740
+ pad(formatTerminal(chain.terminal_reason), 26),
741
+ pad(formatTimestamp(chain.started_at), 22),
742
+ ].join(' ')}`);
743
+ });
744
+ }
745
+
746
+ function formatTerminal(reason) {
747
+ if (!reason) return '—';
748
+ if (reason === 'chain_limit_reached') return 'chain limit reached';
749
+ if (reason === 'non_chainable_status') return 'non-chainable status';
750
+ return reason.replace(/_/g, ' ');
751
+ }
752
+
753
+ function formatMissionStatus(status) {
754
+ if (!status) return '—';
755
+ switch (status) {
756
+ case 'planned':
757
+ return chalk.blue('planned');
758
+ case 'progressing':
759
+ return chalk.green('progressing');
760
+ case 'needs_attention':
761
+ return chalk.yellow('needs_attention');
762
+ case 'degraded':
763
+ return chalk.red('degraded');
764
+ default:
765
+ return status;
766
+ }
767
+ }
768
+
769
+ function formatTimestamp(value) {
770
+ if (!value) return '—';
771
+ try {
772
+ return new Date(value).toLocaleString();
773
+ } catch {
774
+ return value;
775
+ }
776
+ }
777
+
778
+ function pad(value, width) {
779
+ return String(value).padEnd(width);
780
+ }