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.
- package/bin/agentxchain.js +54 -1
- package/dashboard/app.js +2 -1
- package/dashboard/components/mission.js +180 -1
- package/package.json +1 -1
- package/src/commands/mission.js +786 -1
- package/src/lib/dashboard/bridge-server.js +9 -0
- package/src/lib/dashboard/file-watcher.js +10 -6
- package/src/lib/dashboard/plan-reader.js +108 -0
- package/src/lib/dashboard/state-reader.js +11 -0
- package/src/lib/mission-plans.js +543 -0
- package/src/lib/run-chain.js +5 -3
package/src/commands/mission.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
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('');
|