agentxchain 2.7.0 → 2.8.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/README.md CHANGED
@@ -10,8 +10,10 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
10
10
 
11
11
  - [Quickstart](https://agentxchain.dev/docs/quickstart/)
12
12
  - [CLI reference](https://agentxchain.dev/docs/cli/)
13
+ - [Export schema reference](https://agentxchain.dev/docs/export-schema/)
13
14
  - [Adapter reference](https://agentxchain.dev/docs/adapters/)
14
15
  - [Protocol spec (v6)](https://agentxchain.dev/docs/protocol/)
16
+ - [Protocol reference](https://agentxchain.dev/docs/protocol-reference/)
15
17
  - [Why governed multi-agent delivery matters](https://agentxchain.dev/why/)
16
18
 
17
19
  ## Install
@@ -73,6 +73,7 @@ import { approveTransitionCommand } from '../src/commands/approve-transition.js'
73
73
  import { approveCompletionCommand } from '../src/commands/approve-completion.js';
74
74
  import { dashboardCommand } from '../src/commands/dashboard.js';
75
75
  import { exportCommand } from '../src/commands/export.js';
76
+ import { reportCommand } from '../src/commands/report.js';
76
77
  import {
77
78
  pluginInstallCommand,
78
79
  pluginListCommand,
@@ -130,6 +131,13 @@ program
130
131
  .option('--output <path>', 'Write the export artifact to a file instead of stdout')
131
132
  .action(exportCommand);
132
133
 
134
+ program
135
+ .command('report')
136
+ .description('Render a human-readable governance summary from an export artifact')
137
+ .option('--input <path>', 'Export artifact path, or "-" for stdin', '-')
138
+ .option('--format <format>', 'Output format: text, json, or markdown', 'text')
139
+ .action(reportCommand);
140
+
133
141
  program
134
142
  .command('start')
135
143
  .description('Launch legacy v3 agents in your IDE')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,51 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { loadExportArtifact } from '../lib/export-verifier.js';
4
+ import {
5
+ buildGovernanceReport,
6
+ formatGovernanceReportMarkdown,
7
+ formatGovernanceReportText,
8
+ } from '../lib/report.js';
9
+
10
+ function printAndExit(report, format, exitCode) {
11
+ if (format === 'json') {
12
+ console.log(JSON.stringify(report, null, 2));
13
+ process.exit(exitCode);
14
+ }
15
+
16
+ if (format === 'markdown') {
17
+ console.log(formatGovernanceReportMarkdown(report));
18
+ process.exit(exitCode);
19
+ }
20
+
21
+ if (format === 'text') {
22
+ if (report.overall === 'error') {
23
+ console.log(chalk.red(formatGovernanceReportText(report)));
24
+ } else if (report.overall === 'fail') {
25
+ console.log(chalk.red(formatGovernanceReportText(report)));
26
+ } else {
27
+ console.log(formatGovernanceReportText(report));
28
+ }
29
+ process.exit(exitCode);
30
+ }
31
+
32
+ console.error(`Unsupported report format "${format}". Use "text", "json", or "markdown".`);
33
+ process.exit(1);
34
+ }
35
+
36
+ export async function reportCommand(opts) {
37
+ const format = opts.format || 'text';
38
+ const loaded = loadExportArtifact(opts.input || '-', process.cwd());
39
+
40
+ if (!loaded.ok) {
41
+ printAndExit({
42
+ overall: 'error',
43
+ input: loaded.input,
44
+ message: loaded.error,
45
+ }, format, 2);
46
+ return;
47
+ }
48
+
49
+ const result = buildGovernanceReport(loaded.artifact, { input: loaded.input });
50
+ printAndExit(result.report, format, result.exitCode);
51
+ }
@@ -0,0 +1,379 @@
1
+ import { verifyExportArtifact } from './export-verifier.js';
2
+
3
+ export const GOVERNANCE_REPORT_VERSION = '0.1';
4
+
5
+ function yesNo(value) {
6
+ return value ? 'yes' : 'no';
7
+ }
8
+
9
+ function summarizeBlockedOn(blockedOn) {
10
+ if (!blockedOn) return 'none';
11
+ if (typeof blockedOn === 'string') return blockedOn;
12
+ if (typeof blockedOn !== 'object' || Array.isArray(blockedOn)) return 'present';
13
+ if (typeof blockedOn.typed_reason === 'string' && blockedOn.typed_reason.length > 0) {
14
+ return blockedOn.typed_reason;
15
+ }
16
+ if (typeof blockedOn.reason === 'string' && blockedOn.reason.length > 0) {
17
+ return blockedOn.reason;
18
+ }
19
+ return 'present';
20
+ }
21
+
22
+ function summarizeBlockedState(run) {
23
+ const blockedReason = run?.blocked_reason;
24
+ if (blockedReason && typeof blockedReason === 'object' && !Array.isArray(blockedReason)) {
25
+ if (typeof blockedReason.recovery?.typed_reason === 'string' && blockedReason.recovery.typed_reason.length > 0) {
26
+ return blockedReason.recovery.typed_reason;
27
+ }
28
+ if (typeof blockedReason.category === 'string' && blockedReason.category.length > 0) {
29
+ return blockedReason.category;
30
+ }
31
+ }
32
+ return summarizeBlockedOn(run?.blocked_on);
33
+ }
34
+
35
+ function normalizeBudgetStatus(budgetStatus) {
36
+ if (!budgetStatus || typeof budgetStatus !== 'object' || Array.isArray(budgetStatus)) {
37
+ return null;
38
+ }
39
+
40
+ const normalized = {};
41
+ if (Number.isFinite(budgetStatus.spent_usd)) {
42
+ normalized.spent_usd = budgetStatus.spent_usd;
43
+ }
44
+ if (Number.isFinite(budgetStatus.remaining_usd)) {
45
+ normalized.remaining_usd = budgetStatus.remaining_usd;
46
+ }
47
+
48
+ return Object.keys(normalized).length > 0 ? normalized : null;
49
+ }
50
+
51
+ function formatUsd(value) {
52
+ return typeof value === 'number' ? `$${value.toFixed(2)}` : 'n/a';
53
+ }
54
+
55
+ function formatStatusCounts(statusCounts) {
56
+ const entries = Object.entries(statusCounts || {}).sort(([left], [right]) => left.localeCompare(right, 'en'));
57
+ if (entries.length === 0) return 'none';
58
+ return entries.map(([status, count]) => `${status}(${count})`).join(', ');
59
+ }
60
+
61
+ function deriveRepoStatusCounts(repoStatuses) {
62
+ const counts = {};
63
+ for (const status of Object.values(repoStatuses || {})) {
64
+ const key = status || 'unknown';
65
+ counts[key] = (counts[key] || 0) + 1;
66
+ }
67
+ return counts;
68
+ }
69
+
70
+ function buildRunSubject(artifact) {
71
+ const activeTurns = artifact.summary?.active_turn_ids || [];
72
+ const retainedTurns = artifact.summary?.retained_turn_ids || [];
73
+ const activeRoles = [...new Set(
74
+ Object.values(artifact.state?.active_turns || {})
75
+ .map((turn) => turn?.assigned_role)
76
+ .filter((role) => typeof role === 'string' && role.length > 0),
77
+ )].sort((a, b) => a.localeCompare(b, 'en'));
78
+
79
+ return {
80
+ kind: 'governed_run',
81
+ project: {
82
+ id: artifact.project?.id || null,
83
+ name: artifact.project?.name || null,
84
+ template: artifact.project?.template || 'generic',
85
+ protocol_mode: artifact.project?.protocol_mode || null,
86
+ schema_version: artifact.project?.schema_version || null,
87
+ },
88
+ run: {
89
+ run_id: artifact.summary?.run_id || null,
90
+ status: artifact.summary?.status || null,
91
+ phase: artifact.summary?.phase || null,
92
+ blocked_on: artifact.state?.blocked_on || null,
93
+ blocked_reason: artifact.state?.blocked_reason || null,
94
+ active_turn_count: activeTurns.length,
95
+ retained_turn_count: retainedTurns.length,
96
+ active_turn_ids: activeTurns,
97
+ retained_turn_ids: retainedTurns,
98
+ active_roles: activeRoles,
99
+ budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
100
+ },
101
+ artifacts: {
102
+ history_entries: artifact.summary?.history_entries || 0,
103
+ decision_entries: artifact.summary?.decision_entries || 0,
104
+ hook_audit_entries: artifact.summary?.hook_audit_entries || 0,
105
+ notification_audit_entries: artifact.summary?.notification_audit_entries || 0,
106
+ dispatch_artifact_files: artifact.summary?.dispatch_artifact_files || 0,
107
+ staging_artifact_files: artifact.summary?.staging_artifact_files || 0,
108
+ intake_present: Boolean(artifact.summary?.intake_present),
109
+ coordinator_present: Boolean(artifact.summary?.coordinator_present),
110
+ },
111
+ };
112
+ }
113
+
114
+ function buildCoordinatorSubject(artifact) {
115
+ const repoStatuses = artifact.summary?.repo_run_statuses || {};
116
+ const repoStatusCounts = deriveRepoStatusCounts(repoStatuses);
117
+ const repos = Object.entries(artifact.repos || {})
118
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))
119
+ .map(([repoId, repoEntry]) => ({
120
+ repo_id: repoId,
121
+ path: repoEntry?.path || null,
122
+ ok: Boolean(repoEntry?.ok),
123
+ status: repoEntry?.ok ? repoEntry.export?.summary?.status || null : null,
124
+ run_id: repoEntry?.ok ? repoEntry.export?.summary?.run_id || null : null,
125
+ phase: repoEntry?.ok ? repoEntry.export?.summary?.phase || null : null,
126
+ project_id: repoEntry?.ok ? repoEntry.export?.project?.id || null : null,
127
+ project_name: repoEntry?.ok ? repoEntry.export?.project?.name || null : null,
128
+ error: repoEntry?.ok ? null : repoEntry?.error || null,
129
+ }));
130
+
131
+ const repoErrorCount = repos.filter((repo) => !repo.ok).length;
132
+
133
+ return {
134
+ kind: 'coordinator_workspace',
135
+ coordinator: {
136
+ project_id: artifact.coordinator?.project_id || null,
137
+ project_name: artifact.coordinator?.project_name || null,
138
+ schema_version: artifact.coordinator?.schema_version || null,
139
+ repo_count: artifact.coordinator?.repo_count || 0,
140
+ workstream_count: artifact.coordinator?.workstream_count || 0,
141
+ },
142
+ run: {
143
+ super_run_id: artifact.summary?.super_run_id || null,
144
+ status: artifact.summary?.status || null,
145
+ phase: artifact.summary?.phase || null,
146
+ barrier_count: artifact.summary?.barrier_count || 0,
147
+ repo_status_counts: repoStatusCounts,
148
+ repo_ok_count: repos.length - repoErrorCount,
149
+ repo_error_count: repoErrorCount,
150
+ },
151
+ repos,
152
+ artifacts: {
153
+ history_entries: artifact.summary?.history_entries || 0,
154
+ decision_entries: artifact.summary?.decision_entries || 0,
155
+ },
156
+ };
157
+ }
158
+
159
+ export function buildGovernanceReport(artifact, { input = 'stdin', generatedAt = new Date().toISOString() } = {}) {
160
+ const verification = verifyExportArtifact(artifact);
161
+ if (!verification.ok) {
162
+ return {
163
+ ok: false,
164
+ exitCode: 1,
165
+ report: {
166
+ overall: 'fail',
167
+ input,
168
+ message: 'Cannot build governance report from invalid export artifact.',
169
+ verification: verification.report,
170
+ },
171
+ };
172
+ }
173
+
174
+ let subject;
175
+ if (artifact.export_kind === 'agentxchain_run_export') {
176
+ subject = buildRunSubject(artifact);
177
+ } else if (artifact.export_kind === 'agentxchain_coordinator_export') {
178
+ subject = buildCoordinatorSubject(artifact);
179
+ } else {
180
+ return {
181
+ ok: false,
182
+ exitCode: 1,
183
+ report: {
184
+ overall: 'fail',
185
+ input,
186
+ message: 'Cannot build governance report from invalid export artifact.',
187
+ verification: verification.report,
188
+ },
189
+ };
190
+ }
191
+
192
+ return {
193
+ ok: true,
194
+ exitCode: 0,
195
+ report: {
196
+ report_version: GOVERNANCE_REPORT_VERSION,
197
+ overall: 'pass',
198
+ generated_at: generatedAt,
199
+ input,
200
+ export_kind: artifact.export_kind,
201
+ verification: verification.report,
202
+ subject,
203
+ },
204
+ };
205
+ }
206
+
207
+ export function formatGovernanceReportText(report) {
208
+ if (report.overall === 'error') {
209
+ return [
210
+ 'AgentXchain Governance Report',
211
+ `Input: ${report.input}`,
212
+ 'Status: ERROR',
213
+ `Message: ${report.message}`,
214
+ ].join('\n');
215
+ }
216
+
217
+ if (report.overall === 'fail') {
218
+ return [
219
+ 'AgentXchain Governance Report',
220
+ `Input: ${report.input}`,
221
+ 'Verification: FAIL',
222
+ report.message,
223
+ 'Errors:',
224
+ ...(report.verification?.errors || []).map((error) => `- ${error}`),
225
+ ].join('\n');
226
+ }
227
+
228
+ if (report.subject.kind === 'governed_run') {
229
+ const { project, run, artifacts } = report.subject;
230
+ const lines = [
231
+ 'AgentXchain Governance Report',
232
+ `Input: ${report.input}`,
233
+ `Export kind: ${report.export_kind}`,
234
+ 'Verification: PASS',
235
+ `Project: ${project.name || 'unknown'} (${project.id || 'unknown'})`,
236
+ `Template: ${project.template}`,
237
+ `Protocol: ${project.protocol_mode || 'unknown'} (config schema ${project.schema_version || 'unknown'})`,
238
+ `Run: ${run.run_id || 'none'}`,
239
+ `Status: ${run.status || 'unknown'}`,
240
+ `Phase: ${run.phase || 'unknown'}`,
241
+ `Blocked on: ${summarizeBlockedState(run)}`,
242
+ `Active turns: ${run.active_turn_count}${run.active_turn_ids.length ? ` (${run.active_turn_ids.join(', ')})` : ''}`,
243
+ `Retained turns: ${run.retained_turn_count}${run.retained_turn_ids.length ? ` (${run.retained_turn_ids.join(', ')})` : ''}`,
244
+ `Active roles: ${run.active_roles.length ? run.active_roles.join(', ') : 'none'}`,
245
+ ];
246
+
247
+ if (run.budget_status) {
248
+ lines.push(
249
+ `Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`,
250
+ );
251
+ }
252
+
253
+ lines.push(
254
+ `History entries: ${artifacts.history_entries}`,
255
+ `Decision entries: ${artifacts.decision_entries}`,
256
+ `Hook audit entries: ${artifacts.hook_audit_entries}`,
257
+ `Notification audit entries: ${artifacts.notification_audit_entries}`,
258
+ `Dispatch files: ${artifacts.dispatch_artifact_files}`,
259
+ `Staging files: ${artifacts.staging_artifact_files}`,
260
+ `Intake artifacts: ${yesNo(artifacts.intake_present)}`,
261
+ `Coordinator artifacts: ${yesNo(artifacts.coordinator_present)}`,
262
+ );
263
+
264
+ return lines.join('\n');
265
+ }
266
+
267
+ const { coordinator, run, artifacts, repos } = report.subject;
268
+ return [
269
+ 'AgentXchain Governance Report',
270
+ `Input: ${report.input}`,
271
+ `Export kind: ${report.export_kind}`,
272
+ 'Verification: PASS',
273
+ `Workspace: ${coordinator.project_name || 'unknown'} (${coordinator.project_id || 'unknown'})`,
274
+ `Coordinator schema: ${coordinator.schema_version || 'unknown'}`,
275
+ `Super run: ${run.super_run_id || 'none'}`,
276
+ `Status: ${run.status || 'unknown'}`,
277
+ `Phase: ${run.phase || 'unknown'}`,
278
+ `Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
279
+ `Workstreams: ${coordinator.workstream_count}`,
280
+ `Barriers: ${run.barrier_count}`,
281
+ `Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
282
+ `History entries: ${artifacts.history_entries}`,
283
+ `Decision entries: ${artifacts.decision_entries}`,
284
+ 'Repo details:',
285
+ ...repos.map((repo) => repo.ok
286
+ ? `- ${repo.repo_id}: ok, status ${repo.status || 'unknown'}, run ${repo.run_id || 'none'}, path ${repo.path || 'unknown'}`
287
+ : `- ${repo.repo_id}: failed export, ${repo.error || 'unknown error'}, path ${repo.path || 'unknown'}`),
288
+ ].join('\n');
289
+ }
290
+
291
+ export function formatGovernanceReportMarkdown(report) {
292
+ if (report.overall === 'error') {
293
+ return [
294
+ '# AgentXchain Governance Report',
295
+ '',
296
+ `- Input: \`${report.input}\``,
297
+ '- Status: `error`',
298
+ `- Message: ${report.message}`,
299
+ ].join('\n');
300
+ }
301
+
302
+ if (report.overall === 'fail') {
303
+ return [
304
+ '# AgentXchain Governance Report',
305
+ '',
306
+ `- Input: \`${report.input}\``,
307
+ '- Verification: `fail`',
308
+ `- Message: ${report.message}`,
309
+ '',
310
+ '## Verification Errors',
311
+ '',
312
+ ...(report.verification?.errors || []).map((error) => `- ${error}`),
313
+ ].join('\n');
314
+ }
315
+
316
+ if (report.subject.kind === 'governed_run') {
317
+ const { project, run, artifacts } = report.subject;
318
+ const lines = [
319
+ '# AgentXchain Governance Report',
320
+ '',
321
+ `- Input: \`${report.input}\``,
322
+ `- Export kind: \`${report.export_kind}\``,
323
+ '- Verification: `pass`',
324
+ `- Project: ${project.name || 'unknown'} (\`${project.id || 'unknown'}\`)`,
325
+ `- Template: \`${project.template}\``,
326
+ `- Protocol: \`${project.protocol_mode || 'unknown'}\` (config schema \`${project.schema_version || 'unknown'}\`)`,
327
+ `- Run: \`${run.run_id || 'none'}\``,
328
+ `- Status: \`${run.status || 'unknown'}\``,
329
+ `- Phase: \`${run.phase || 'unknown'}\``,
330
+ `- Blocked on: \`${summarizeBlockedState(run)}\``,
331
+ `- Active turns: ${run.active_turn_count}${run.active_turn_ids.length ? ` (\`${run.active_turn_ids.join('`, `')}\`)` : ''}`,
332
+ `- Retained turns: ${run.retained_turn_count}${run.retained_turn_ids.length ? ` (\`${run.retained_turn_ids.join('`, `')}\`)` : ''}`,
333
+ `- Active roles: ${run.active_roles.length ? `\`${run.active_roles.join('`, `')}\`` : '`none`'}`,
334
+ ];
335
+
336
+ if (run.budget_status) {
337
+ lines.push(`- Budget: spent ${formatUsd(run.budget_status.spent_usd)}, remaining ${formatUsd(run.budget_status.remaining_usd)}`);
338
+ }
339
+
340
+ lines.push(
341
+ `- History entries: ${artifacts.history_entries}`,
342
+ `- Decision entries: ${artifacts.decision_entries}`,
343
+ `- Hook audit entries: ${artifacts.hook_audit_entries}`,
344
+ `- Notification audit entries: ${artifacts.notification_audit_entries}`,
345
+ `- Dispatch files: ${artifacts.dispatch_artifact_files}`,
346
+ `- Staging files: ${artifacts.staging_artifact_files}`,
347
+ `- Intake artifacts: \`${yesNo(artifacts.intake_present)}\``,
348
+ `- Coordinator artifacts: \`${yesNo(artifacts.coordinator_present)}\``,
349
+ );
350
+
351
+ return lines.join('\n');
352
+ }
353
+
354
+ const { coordinator, run, artifacts, repos } = report.subject;
355
+ return [
356
+ '# AgentXchain Governance Report',
357
+ '',
358
+ `- Input: \`${report.input}\``,
359
+ `- Export kind: \`${report.export_kind}\``,
360
+ '- Verification: `pass`',
361
+ `- Workspace: ${coordinator.project_name || 'unknown'} (\`${coordinator.project_id || 'unknown'}\`)`,
362
+ `- Coordinator schema: \`${coordinator.schema_version || 'unknown'}\``,
363
+ `- Super run: \`${run.super_run_id || 'none'}\``,
364
+ `- Status: \`${run.status || 'unknown'}\``,
365
+ `- Phase: \`${run.phase || 'unknown'}\``,
366
+ `- Repos: ${coordinator.repo_count} total, ${run.repo_ok_count} exported cleanly, ${run.repo_error_count} failed`,
367
+ `- Workstreams: ${coordinator.workstream_count}`,
368
+ `- Barriers: ${run.barrier_count}`,
369
+ `- Repo statuses: ${formatStatusCounts(run.repo_status_counts)}`,
370
+ `- History entries: ${artifacts.history_entries}`,
371
+ `- Decision entries: ${artifacts.decision_entries}`,
372
+ '',
373
+ '## Repo Details',
374
+ '',
375
+ ...repos.map((repo) => repo.ok
376
+ ? `- \`${repo.repo_id}\`: ok, status \`${repo.status || 'unknown'}\`, run \`${repo.run_id || 'none'}\`, path \`${repo.path || 'unknown'}\``
377
+ : `- \`${repo.repo_id}\`: failed export, ${repo.error || 'unknown error'}, path \`${repo.path || 'unknown'}\``),
378
+ ].join('\n');
379
+ }