agentxchain 2.112.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.
@@ -122,7 +122,7 @@ import { eventsCommand } from '../src/commands/events.js';
122
122
  import { connectorCheckCommand } from '../src/commands/connector.js';
123
123
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
124
124
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
125
- import { missionAttachChainCommand, missionListCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
125
+ import { missionAttachChainCommand, missionListCommand, missionPlanApproveCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
126
126
 
127
127
  const __dirname = dirname(fileURLToPath(import.meta.url));
128
128
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -457,6 +457,53 @@ missionCmd
457
457
  .option('-d, --dir <path>', 'Project directory')
458
458
  .action(missionAttachChainCommand);
459
459
 
460
+ const missionPlanCmd = missionCmd
461
+ .command('plan [mission_id]')
462
+ .description('Generate a decomposition plan for a mission (default: latest mission)')
463
+ .option('--constraint <text>', 'Add a constraint to the planner (repeatable)', collectOption, [])
464
+ .option('--role-hint <role>', 'Hint available roles to the planner (repeatable)', collectOption, [])
465
+ .option('-j, --json', 'Output as JSON')
466
+ .option('-d, --dir <path>', 'Project directory')
467
+ .action(missionPlanCommand);
468
+
469
+ missionPlanCmd
470
+ .command('show [plan_id]')
471
+ .description('Show a decomposition plan (default: latest plan)')
472
+ .option('-m, --mission <mission_id>', 'Explicit mission ID')
473
+ .option('-j, --json', 'Output as JSON')
474
+ .option('-d, --dir <path>', 'Project directory')
475
+ .action(missionPlanShowCommand);
476
+
477
+ missionPlanCmd
478
+ .command('approve [plan_id]')
479
+ .description('Approve a decomposition plan (default: latest plan)')
480
+ .option('-m, --mission <mission_id>', 'Explicit mission ID')
481
+ .option('-d, --dir <path>', 'Project directory')
482
+ .action(missionPlanApproveCommand);
483
+
484
+ missionPlanCmd
485
+ .command('launch [plan_id]')
486
+ .description('Launch a workstream from an approved plan (default: latest plan)')
487
+ .requiredOption('-w, --workstream <id>', 'Workstream ID to launch')
488
+ .option('-m, --mission <mission_id>', 'Explicit mission ID')
489
+ .option('--auto-approve', 'Auto-approve run gates while executing the launched workstream')
490
+ .option('-j, --json', 'Output as JSON')
491
+ .option('-d, --dir <path>', 'Project directory')
492
+ .action(missionPlanLaunchCommand);
493
+
494
+ missionPlanCmd
495
+ .command('list')
496
+ .description('List all plans for a mission')
497
+ .option('-m, --mission <mission_id>', 'Explicit mission ID')
498
+ .option('-l, --limit <n>', 'Max plans to show (default: 20)')
499
+ .option('-j, --json', 'Output as JSON')
500
+ .option('-d, --dir <path>', 'Project directory')
501
+ .action(missionPlanListCommand);
502
+
503
+ function collectOption(value, previous) {
504
+ return previous.concat([value]);
505
+ }
506
+
460
507
  program
461
508
  .command('validate')
462
509
  .description('Validate project protocol artifacts')
package/dashboard/app.js CHANGED
@@ -37,7 +37,7 @@ const VIEWS = {
37
37
  'cross-repo': { fetch: ['coordinatorState', 'coordinatorHistory'], render: renderCrossRepo },
38
38
  blockers: { fetch: ['coordinatorBlockers'], render: renderBlockers },
39
39
  artifacts: { fetch: ['workflowKitArtifacts'], render: renderArtifacts },
40
- mission: { fetch: ['missions'], render: renderMission },
40
+ mission: { fetch: ['missions', 'plans'], render: renderMission },
41
41
  chain: { fetch: ['chainReports'], render: renderChain },
42
42
  'run-history': { fetch: ['runHistory'], render: renderRunHistory },
43
43
  timeouts: { fetch: ['timeouts'], render: renderTimeouts },
@@ -63,6 +63,7 @@ const API_MAP = {
63
63
  coordinatorRepoStatusRows: '/api/coordinator/repo-status',
64
64
  workflowKitArtifacts: '/api/workflow-kit-artifacts',
65
65
  missions: '/api/missions',
66
+ plans: '/api/plans',
66
67
  chainReports: '/api/chain-reports',
67
68
  connectors: '/api/connectors',
68
69
  runHistory: '/api/run-history',
@@ -132,7 +132,178 @@ function renderRecentMissions(missions) {
132
132
  return html;
133
133
  }
134
134
 
135
- export function render({ missions }) {
135
+ function formatPlanStatus(status) {
136
+ switch (status) {
137
+ case 'proposed':
138
+ return badge('proposed', '#38bdf8');
139
+ case 'approved':
140
+ return badge('approved', 'var(--green)');
141
+ case 'superseded':
142
+ return badge('superseded', 'var(--text-dim)');
143
+ case 'needs_attention':
144
+ return badge('needs attention', 'var(--yellow)');
145
+ default:
146
+ return badge(status || 'unknown', 'var(--text-dim)');
147
+ }
148
+ }
149
+
150
+ function formatLaunchStatus(status) {
151
+ switch (status) {
152
+ case 'ready':
153
+ return badge('ready', 'var(--green)');
154
+ case 'blocked':
155
+ return badge('blocked', 'var(--red)');
156
+ case 'launched':
157
+ return badge('launched', '#38bdf8');
158
+ case 'completed':
159
+ return badge('completed', 'var(--green)');
160
+ case 'needs_attention':
161
+ return badge('needs attention', 'var(--yellow)');
162
+ default:
163
+ return badge(status || 'unknown', 'var(--text-dim)');
164
+ }
165
+ }
166
+
167
+ function renderLatestPlan(plan) {
168
+ if (!plan) {
169
+ return `<div class="section"><h3>Latest Plan</h3><p class="section-subtitle">No decomposition plan found. Run <code>agentxchain mission plan &lt;mission_id&gt;</code> to generate one.</p></div>`;
170
+ }
171
+
172
+ const workstreams = Array.isArray(plan.workstreams) ? plan.workstreams : [];
173
+ const launchRecords = Array.isArray(plan.launch_records) ? plan.launch_records : [];
174
+ const statusCounts = plan.workstream_status_counts || {};
175
+
176
+ let html = `<div class="section"><h3>Latest Plan</h3><dl class="detail-list">`;
177
+ html += `<dt>Plan ID</dt><dd class="mono" title="${esc(plan.plan_id || '')}">${esc(truncateId(plan.plan_id, 30))}</dd>`;
178
+ html += `<dt>Status</dt><dd>${formatPlanStatus(plan.status)}</dd>`;
179
+ html += `<dt>Mission</dt><dd class="mono">${esc(plan.mission_id || '—')}</dd>`;
180
+ html += `<dt>Created</dt><dd>${formatDate(plan.created_at)}</dd>`;
181
+ if (plan.approved_at) {
182
+ html += `<dt>Approved</dt><dd>${formatDate(plan.approved_at)}</dd>`;
183
+ }
184
+ html += `<dt>Workstreams</dt><dd>${plan.workstream_count || 0}</dd>`;
185
+ html += `<dt>Launched</dt><dd>${plan.launch_record_count || 0}</dd>`;
186
+ if (Object.keys(statusCounts).length > 0) {
187
+ const countsStr = Object.entries(statusCounts).map(([k, v]) => `${k}: ${v}`).join(', ');
188
+ html += `<dt>Status Breakdown</dt><dd>${esc(countsStr)}</dd>`;
189
+ }
190
+ if (plan.supersedes_plan_id) {
191
+ html += `<dt>Supersedes</dt><dd class="mono">${esc(truncateId(plan.supersedes_plan_id, 30))}</dd>`;
192
+ }
193
+ html += `</dl></div>`;
194
+
195
+ // Workstreams table
196
+ if (workstreams.length > 0) {
197
+ html += `<div class="section"><h3>Workstreams</h3>
198
+ <table class="data-table">
199
+ <thead>
200
+ <tr>
201
+ <th>#</th>
202
+ <th>ID</th>
203
+ <th>Title</th>
204
+ <th>Status</th>
205
+ <th>Roles</th>
206
+ <th>Phases</th>
207
+ <th>Dependencies</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>`;
211
+
212
+ workstreams.forEach((ws, index) => {
213
+ const roles = Array.isArray(ws.roles) ? ws.roles.join(', ') : '—';
214
+ const phases = Array.isArray(ws.phases) ? ws.phases.join(', ') : '—';
215
+ const deps = Array.isArray(ws.depends_on) && ws.depends_on.length > 0 ? ws.depends_on.join(', ') : 'none';
216
+
217
+ html += `<tr>
218
+ <td style="color:var(--text-dim)">${index + 1}</td>
219
+ <td class="mono">${esc(ws.workstream_id || '—')}</td>
220
+ <td>${esc(ws.title || '—')}</td>
221
+ <td>${formatLaunchStatus(ws.launch_status)}</td>
222
+ <td>${esc(roles)}</td>
223
+ <td>${esc(phases)}</td>
224
+ <td class="mono">${esc(deps)}</td>
225
+ </tr>`;
226
+ });
227
+
228
+ html += '</tbody></table></div>';
229
+ }
230
+
231
+ // Launch records table
232
+ if (launchRecords.length > 0) {
233
+ html += `<div class="section"><h3>Launch Records</h3>
234
+ <table class="data-table">
235
+ <thead>
236
+ <tr>
237
+ <th>#</th>
238
+ <th>Workstream</th>
239
+ <th>Chain ID</th>
240
+ <th>Status</th>
241
+ <th>Terminal</th>
242
+ <th>Launched</th>
243
+ <th>Completed</th>
244
+ </tr>
245
+ </thead>
246
+ <tbody>`;
247
+
248
+ launchRecords.forEach((lr, index) => {
249
+ html += `<tr>
250
+ <td style="color:var(--text-dim)">${index + 1}</td>
251
+ <td class="mono">${esc(lr.workstream_id || '—')}</td>
252
+ <td class="mono" title="${esc(lr.chain_id || '')}">${esc(truncateId(lr.chain_id))}</td>
253
+ <td>${formatLaunchStatus(lr.status)}</td>
254
+ <td>${esc(lr.terminal_reason || '—')}</td>
255
+ <td>${formatDate(lr.launched_at)}</td>
256
+ <td>${formatDate(lr.completed_at)}</td>
257
+ </tr>`;
258
+ });
259
+
260
+ html += '</tbody></table></div>';
261
+ }
262
+
263
+ return html;
264
+ }
265
+
266
+ function renderRecentPlans(plans) {
267
+ if (!Array.isArray(plans) || plans.length <= 1) {
268
+ return '';
269
+ }
270
+
271
+ // Skip the first (latest) since it's rendered in detail above
272
+ const olderPlans = plans.slice(1);
273
+ if (olderPlans.length === 0) return '';
274
+
275
+ let html = `<div class="section"><h3>Previous Plans</h3>
276
+ <table class="data-table">
277
+ <thead>
278
+ <tr>
279
+ <th>#</th>
280
+ <th>Plan ID</th>
281
+ <th>Mission</th>
282
+ <th>Status</th>
283
+ <th>Workstreams</th>
284
+ <th>Launched</th>
285
+ <th>Created</th>
286
+ </tr>
287
+ </thead>
288
+ <tbody>`;
289
+
290
+ olderPlans.forEach((plan, index) => {
291
+ html += `<tr>
292
+ <td style="color:var(--text-dim)">${index + 1}</td>
293
+ <td class="mono" title="${esc(plan.plan_id || '')}">${esc(truncateId(plan.plan_id, 20))}</td>
294
+ <td class="mono">${esc(truncateId(plan.mission_id, 18))}</td>
295
+ <td>${formatPlanStatus(plan.status)}</td>
296
+ <td>${plan.workstream_count || 0}</td>
297
+ <td>${plan.launch_record_count || 0}</td>
298
+ <td>${formatDate(plan.created_at)}</td>
299
+ </tr>`;
300
+ });
301
+
302
+ html += '</tbody></table></div>';
303
+ return html;
304
+ }
305
+
306
+ export function render({ missions, plans }) {
136
307
  if (!missions || typeof missions !== 'object') {
137
308
  return `<div class="placeholder"><h2>Mission</h2><p>No mission data available. Create a mission and bind a chain to populate this view.</p></div>`;
138
309
  }
@@ -171,6 +342,14 @@ export function render({ missions }) {
171
342
  }
172
343
 
173
344
  html += renderAttachedChains(latest);
345
+
346
+ // Plan visibility
347
+ const planData = plans && typeof plans === 'object' ? plans : null;
348
+ const latestPlan = planData?.latest || null;
349
+ const allPlans = Array.isArray(planData?.plans) ? planData.plans : [];
350
+ html += renderLatestPlan(latestPlan);
351
+ html += renderRecentPlans(allPlans);
352
+
174
353
  html += renderRecentMissions(missionList);
175
354
  html += '</div>';
176
355
  return html;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.112.0",
3
+ "version": "2.113.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {