agentxchain 2.114.0 → 2.115.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.
@@ -491,6 +491,7 @@ missionPlanCmd
491
491
  .description('Launch workstream(s) from an approved plan (default: latest plan)')
492
492
  .option('-w, --workstream <id>', 'Workstream ID to launch (mutually exclusive with --all-ready)')
493
493
  .option('--all-ready', 'Launch all ready workstreams sequentially (mutually exclusive with --workstream)')
494
+ .option('--retry', 'Retry a failed workstream (requires --workstream, only for needs_attention status)')
494
495
  .option('-m, --mission <mission_id>', 'Explicit mission ID')
495
496
  .option('--auto-approve', 'Auto-approve run gates while executing the launched workstream')
496
497
  .option('-j, --json', 'Output as JSON')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.114.0",
3
+ "version": "2.115.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@ import {
18
18
  getReadyWorkstreams,
19
19
  getWorkstreamStatusSummary,
20
20
  launchWorkstream,
21
+ retryWorkstream,
21
22
  markWorkstreamOutcome,
22
23
  loadAllPlans,
23
24
  loadLatestPlan,
@@ -423,6 +424,16 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
423
424
  process.exit(1);
424
425
  }
425
426
 
427
+ if (opts.retry && !opts.workstream) {
428
+ console.error(chalk.red('--retry requires --workstream <id>. Specify which failed workstream to retry.'));
429
+ process.exit(1);
430
+ }
431
+
432
+ if (opts.retry && opts.allReady) {
433
+ console.error(chalk.red('--retry and --all-ready are mutually exclusive. Retry targets a specific workstream.'));
434
+ process.exit(1);
435
+ }
436
+
426
437
  if (!opts.allReady && !opts.workstream) {
427
438
  console.error(chalk.red('--workstream <id> or --all-ready is required. Specify which workstream(s) to launch.'));
428
439
  process.exit(1);
@@ -457,7 +468,9 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
457
468
  process.exit(1);
458
469
  }
459
470
 
460
- const launch = launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
471
+ const launch = opts.retry
472
+ ? retryWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream)
473
+ : launchWorkstream(root, mission.mission_id, plan.plan_id, opts.workstream);
461
474
  if (!launch.ok) {
462
475
  console.error(chalk.red(launch.error));
463
476
  process.exit(1);
@@ -949,6 +962,8 @@ function renderMissionPlanError(error) {
949
962
  }
950
963
 
951
964
  function renderMissionSnapshot(snapshot) {
965
+ const latestPlan = snapshot.latest_plan || null;
966
+
952
967
  console.log(chalk.bold(`Mission: ${snapshot.mission_id}`));
953
968
  console.log('');
954
969
  console.log(` Title: ${snapshot.title || '—'}`);
@@ -967,10 +982,19 @@ function renderMissionSnapshot(snapshot) {
967
982
  console.log(` Missing chains: ${snapshot.missing_chain_ids.join(', ')}`);
968
983
  }
969
984
 
985
+ if (latestPlan) {
986
+ console.log('');
987
+ console.log(chalk.bold(' Latest plan:'));
988
+ console.log(` Plan ID: ${latestPlan.plan_id || '—'}`);
989
+ console.log(` Status: ${formatPlanStatus(latestPlan.status)}`);
990
+ console.log(` Completion: ${latestPlan.completion_percentage}% (${latestPlan.completed_count}/${latestPlan.workstream_count} completed)`);
991
+ console.log(` Workstream summary: ready ${latestPlan.ready_count}, blocked ${latestPlan.blocked_count}, launched ${latestPlan.launched_count}, completed ${latestPlan.completed_count}, needs_attention ${latestPlan.needs_attention_count}`);
992
+ }
993
+
970
994
  if (!snapshot.chains || snapshot.chains.length === 0) {
971
995
  console.log('');
972
996
  console.log(chalk.dim(' No chains attached.'));
973
- console.log(chalk.dim(' Use `agentxchain mission attach-chain latest` after a chained run.'));
997
+ console.log(chalk.dim(' Use `agentxchain run --chain --mission latest` for new work, or `agentxchain mission attach-chain latest` to repair an unbound chain.'));
974
998
  return;
975
999
  }
976
1000
 
@@ -5,9 +5,7 @@
5
5
  * Plans are advisory repo-local artifacts; this reader is not protocol-normative.
6
6
  */
7
7
 
8
- import { existsSync, readdirSync } from 'fs';
9
- import { join } from 'path';
10
- import { loadAllPlans, loadLatestPlan } from '../mission-plans.js';
8
+ import { buildPlanProgressSummary, loadAllPlans } from '../mission-plans.js';
11
9
  import { loadAllMissionArtifacts } from '../missions.js';
12
10
 
13
11
  /**
@@ -65,28 +63,12 @@ export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
65
63
  * Build a dashboard-ready summary for a single plan.
66
64
  */
67
65
  function buildPlanSummary(plan) {
66
+ const summary = buildPlanProgressSummary(plan);
68
67
  const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
69
68
  const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
70
69
 
71
- const statusCounts = {};
72
- for (const ws of workstreams) {
73
- const status = ws.launch_status || 'unknown';
74
- statusCounts[status] = (statusCounts[status] || 0) + 1;
75
- }
76
-
77
70
  return {
78
- plan_id: plan.plan_id,
79
- mission_id: plan.mission_id,
80
- status: plan.status,
81
- created_at: plan.created_at,
82
- updated_at: plan.updated_at,
83
- approved_at: plan.approved_at || null,
84
- supersedes_plan_id: plan.supersedes_plan_id || null,
85
- superseded_by_plan_id: plan.superseded_by_plan_id || null,
86
- input_goal: plan.input?.goal || null,
87
- workstream_count: workstreams.length,
88
- launch_record_count: launchRecords.length,
89
- workstream_status_counts: statusCounts,
71
+ ...summary,
90
72
  workstreams: workstreams.map((ws) => ({
91
73
  workstream_id: ws.workstream_id,
92
74
  title: ws.title,
@@ -306,6 +306,38 @@ export function loadPlan(root, missionId, planId) {
306
306
  return plans.find((p) => p.plan_id === planId) || null;
307
307
  }
308
308
 
309
+ export function buildPlanProgressSummary(plan) {
310
+ if (!plan || typeof plan !== 'object') return null;
311
+
312
+ const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
313
+ const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
314
+ const workstreamStatusCounts = getWorkstreamStatusSummary(plan);
315
+ const completedCount = workstreamStatusCounts.completed || 0;
316
+
317
+ return {
318
+ plan_id: plan.plan_id,
319
+ mission_id: plan.mission_id,
320
+ status: plan.status,
321
+ created_at: plan.created_at,
322
+ updated_at: plan.updated_at,
323
+ approved_at: plan.approved_at || null,
324
+ supersedes_plan_id: plan.supersedes_plan_id || null,
325
+ superseded_by_plan_id: plan.superseded_by_plan_id || null,
326
+ input_goal: plan.input?.goal || null,
327
+ workstream_count: workstreams.length,
328
+ launch_record_count: launchRecords.length,
329
+ workstream_status_counts: workstreamStatusCounts,
330
+ ready_count: workstreamStatusCounts.ready || 0,
331
+ blocked_count: workstreamStatusCounts.blocked || 0,
332
+ launched_count: workstreamStatusCounts.launched || 0,
333
+ completed_count: completedCount,
334
+ needs_attention_count: workstreamStatusCounts.needs_attention || 0,
335
+ completion_percentage: workstreams.length === 0
336
+ ? 0
337
+ : Math.round((completedCount / workstreams.length) * 100),
338
+ };
339
+ }
340
+
309
341
  // ── Workstream launch ───────────────────────────────────────────────────────
310
342
 
311
343
  export function didChainFinishSuccessfully(chainReport) {
@@ -443,6 +475,12 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
443
475
  }
444
476
  }
445
477
  }
478
+
479
+ // Auto-complete plan when all workstreams are completed
480
+ const allCompleted = plan.workstreams.every((w) => w.launch_status === 'completed');
481
+ if (allCompleted) {
482
+ plan.status = 'completed';
483
+ }
446
484
  } else {
447
485
  ws.launch_status = 'needs_attention';
448
486
  plan.status = 'needs_attention';
@@ -454,6 +492,62 @@ export function markWorkstreamOutcome(root, missionId, planId, workstreamId, { t
454
492
  return { ok: true, plan, workstream: ws };
455
493
  }
456
494
 
495
+ /**
496
+ * Retry a failed workstream by resetting its status and creating a new launch record.
497
+ *
498
+ * Only workstreams with launch_status === 'needs_attention' can be retried.
499
+ * The old launch record is preserved for audit. A new launch record with a new
500
+ * chain_id is created. If the plan was in 'needs_attention' status, it returns
501
+ * to 'approved' since the retry represents a new attempt.
502
+ *
503
+ * @returns {{ ok: boolean, plan?: object, workstream?: object, chainId?: string, launchRecord?: object, error?: string }}
504
+ */
505
+ export function retryWorkstream(root, missionId, planId, workstreamId, options = {}) {
506
+ const plan = loadPlan(root, missionId, planId);
507
+ if (!plan) {
508
+ return { ok: false, error: `Plan not found: ${planId}` };
509
+ }
510
+
511
+ const ws = plan.workstreams.find((w) => w.workstream_id === workstreamId);
512
+ if (!ws) {
513
+ return { ok: false, error: `Workstream not found: ${workstreamId}` };
514
+ }
515
+
516
+ if (ws.launch_status !== 'needs_attention') {
517
+ return {
518
+ ok: false,
519
+ error: `Workstream ${workstreamId} cannot be retried (status: "${ws.launch_status}"). Only "needs_attention" workstreams can be retried.`,
520
+ };
521
+ }
522
+
523
+ // Generate new chain ID for the retry
524
+ const chainId = options.chainId || `chain-${randomUUID().slice(0, 8)}`;
525
+ const now = new Date().toISOString();
526
+ const launchRecord = {
527
+ workstream_id: workstreamId,
528
+ chain_id: chainId,
529
+ launched_at: now,
530
+ status: 'launched',
531
+ retry: true,
532
+ };
533
+
534
+ if (!Array.isArray(plan.launch_records)) {
535
+ plan.launch_records = [];
536
+ }
537
+ plan.launch_records.push(launchRecord);
538
+ ws.launch_status = 'launched';
539
+
540
+ // Restore plan status from needs_attention to approved (retry in progress)
541
+ if (plan.status === 'needs_attention') {
542
+ plan.status = 'approved';
543
+ }
544
+
545
+ plan.updated_at = now;
546
+ writePlanArtifact(root, missionId, plan);
547
+
548
+ return { ok: true, plan, workstream: ws, chainId, launchRecord };
549
+ }
550
+
457
551
  // ── Batch launch helpers ───────────────────────────────────────────────────
458
552
 
459
553
  /**
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { loadAllChainReports, loadChainReport, loadLatestChainReport } from './chain-reports.js';
4
+ import { buildPlanProgressSummary, loadLatestPlan } from './mission-plans.js';
4
5
  import { getActiveRepoDecisions } from './repo-decisions.js';
5
6
 
6
7
  const MISSION_ATTENTION_TERMINALS = new Set(['operator_abort', 'parent_validation_failed']);
@@ -139,6 +140,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
139
140
  const totalRuns = chains.reduce((sum, chain) => sum + (chain.runs?.length || 0), 0);
140
141
  const totalTurns = chains.reduce((sum, chain) => sum + (chain.total_turns || 0), 0);
141
142
  const latestChain = chains[0] || null;
143
+ const latestPlan = loadLatestPlan(root, missionArtifact.mission_id);
142
144
  const activeRepoDecisions = getActiveRepoDecisions(root);
143
145
 
144
146
  return {
@@ -151,6 +153,7 @@ export function buildMissionSnapshot(root, missionArtifact) {
151
153
  total_turns: totalTurns,
152
154
  latest_chain_id: latestChain?.chain_id || null,
153
155
  latest_terminal_reason: latestChain?.terminal_reason || null,
156
+ latest_plan: buildPlanProgressSummary(latestPlan),
154
157
  active_repo_decisions_count: activeRepoDecisions.length,
155
158
  chains,
156
159
  };