agentxchain 2.76.0 → 2.77.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
@@ -2,6 +2,8 @@
2
2
 
3
3
  CLI for governed multi-agent software delivery.
4
4
 
5
+ AgentXchain coordinates multiple AI agents — PM, developer, QA, architect, and any custom roles — to work together on a codebase with built-in governance: structured turns, mandatory challenge between agents, phase gates, human approvals, and an append-only audit trail. Think of it as the operating system for AI software teams.
6
+
5
7
  The canonical mode is governed delivery: orchestrator-owned state, structured turn results, phase gates, mandatory challenge, and explicit human approvals where required.
6
8
 
7
9
  Legacy IDE-window coordination is still shipped as a compatibility mode for teams that want lock-based handoff in Cursor, VS Code, or Claude Code.
@@ -36,6 +38,13 @@ npm install -g agentxchain
36
38
  agentxchain --version
37
39
  ```
38
40
 
41
+ Or via Homebrew (macOS/Linux):
42
+
43
+ ```bash
44
+ brew tap shivamtiwari93/tap
45
+ brew install agentxchain
46
+ ```
47
+
39
48
  For a zero-install one-off command, use the package-bound form:
40
49
 
41
50
  ```bash
@@ -75,6 +75,7 @@ import { approveTransitionCommand } from '../src/commands/approve-transition.js'
75
75
  import { approveCompletionCommand } from '../src/commands/approve-completion.js';
76
76
  import { dashboardCommand } from '../src/commands/dashboard.js';
77
77
  import { exportCommand } from '../src/commands/export.js';
78
+ import { auditCommand } from '../src/commands/audit.js';
78
79
  import { restoreCommand } from '../src/commands/restore.js';
79
80
  import { restartCommand } from '../src/commands/restart.js';
80
81
  import { reportCommand } from '../src/commands/report.js';
@@ -111,6 +112,7 @@ import { intakeStatusCommand } from '../src/commands/intake-status.js';
111
112
  import { demoCommand } from '../src/commands/demo.js';
112
113
  import { historyCommand } from '../src/commands/history.js';
113
114
  import { eventsCommand } from '../src/commands/events.js';
115
+ import { connectorCheckCommand } from '../src/commands/connector.js';
114
116
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
115
117
 
116
118
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -149,6 +151,12 @@ program
149
151
  .option('--output <path>', 'Write the export artifact to a file instead of stdout')
150
152
  .action(exportCommand);
151
153
 
154
+ program
155
+ .command('audit')
156
+ .description('Render a governance audit directly from the current governed project or coordinator workspace')
157
+ .option('--format <format>', 'Output format: text, json, or markdown', 'text')
158
+ .action(auditCommand);
159
+
152
160
  program
153
161
  .command('restore')
154
162
  .description('Restore governed continuity roots from a run export artifact')
@@ -259,6 +267,17 @@ program
259
267
  .option('-j, --json', 'Output as JSON')
260
268
  .action(doctorCommand);
261
269
 
270
+ const connectorCmd = program
271
+ .command('connector')
272
+ .description('Probe governed runtime connectors directly');
273
+
274
+ connectorCmd
275
+ .command('check [runtime_id]')
276
+ .description('Run live connector probes for all non-manual runtimes or one named runtime')
277
+ .option('-j, --json', 'Output as JSON')
278
+ .option('--timeout <ms>', 'Per-probe timeout in milliseconds', '8000')
279
+ .action(connectorCheckCommand);
280
+
262
281
  program
263
282
  .command('demo')
264
283
  .description('Run a complete governed lifecycle demo (no API keys required)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.76.0",
3
+ "version": "2.77.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,75 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { buildCoordinatorExport, buildRunExport } from '../lib/export.js';
4
+ import {
5
+ buildGovernanceReport,
6
+ formatGovernanceReportMarkdown,
7
+ formatGovernanceReportText,
8
+ } from '../lib/report.js';
9
+
10
+ function detectAuditKind(cwd) {
11
+ const runResult = buildRunExport(cwd);
12
+ if (runResult.ok) {
13
+ return {
14
+ ok: true,
15
+ input: cwd,
16
+ artifact: runResult.export,
17
+ };
18
+ }
19
+
20
+ const coordinatorResult = buildCoordinatorExport(cwd);
21
+ if (coordinatorResult.ok) {
22
+ return {
23
+ ok: true,
24
+ input: cwd,
25
+ artifact: coordinatorResult.export,
26
+ };
27
+ }
28
+
29
+ return {
30
+ ok: false,
31
+ error: runResult.error || coordinatorResult.error || 'No governed project or coordinator workspace found.',
32
+ };
33
+ }
34
+
35
+ function printAndExit(report, format, exitCode) {
36
+ if (format === 'json') {
37
+ console.log(JSON.stringify(report, null, 2));
38
+ process.exit(exitCode);
39
+ }
40
+
41
+ if (format === 'markdown') {
42
+ console.log(formatGovernanceReportMarkdown(report));
43
+ process.exit(exitCode);
44
+ }
45
+
46
+ if (format === 'text') {
47
+ if (report.overall === 'error' || report.overall === 'fail') {
48
+ console.log(chalk.red(formatGovernanceReportText(report)));
49
+ } else {
50
+ console.log(formatGovernanceReportText(report));
51
+ }
52
+ process.exit(exitCode);
53
+ }
54
+
55
+ console.error(`Unsupported audit format "${format}". Use "text", "json", or "markdown".`);
56
+ process.exit(2);
57
+ }
58
+
59
+ export async function auditCommand(options) {
60
+ const format = options.format || 'text';
61
+ const cwd = process.cwd();
62
+ const resolved = detectAuditKind(cwd);
63
+
64
+ if (!resolved.ok) {
65
+ printAndExit({
66
+ overall: 'error',
67
+ input: cwd,
68
+ message: resolved.error,
69
+ }, format, 2);
70
+ return;
71
+ }
72
+
73
+ const result = buildGovernanceReport(resolved.artifact, { input: resolved.input });
74
+ printAndExit(result.report, format, result.exitCode);
75
+ }
@@ -0,0 +1,122 @@
1
+ import chalk from 'chalk';
2
+
3
+ import { loadProjectContext } from '../lib/config.js';
4
+ import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
5
+
6
+ function printJson(result, exitCode) {
7
+ console.log(JSON.stringify(result, null, 2));
8
+ process.exit(exitCode);
9
+ }
10
+
11
+ function printText(result, exitCode) {
12
+ if (result.connectors.length === 0) {
13
+ console.log('');
14
+ console.log(chalk.bold(' AgentXchain Connector Check'));
15
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
16
+ console.log('');
17
+ console.log(` ${chalk.green('PASS')} No non-manual runtimes are configured`);
18
+ console.log('');
19
+ process.exit(exitCode);
20
+ }
21
+
22
+ console.log('');
23
+ console.log(chalk.bold(' AgentXchain Connector Check'));
24
+ console.log(chalk.dim(' ' + '─'.repeat(44)));
25
+ console.log(` ${chalk.dim(`Timeout: ${result.timeout_ms}ms per connector`)}`);
26
+ console.log('');
27
+
28
+ for (const connector of result.connectors) {
29
+ const badge = connector.level === 'pass' ? chalk.green('PASS') : chalk.red('FAIL');
30
+ console.log(` ${badge} ${connector.runtime_id} (${connector.type})`);
31
+ console.log(` ${chalk.dim('Target:')} ${connector.target}`);
32
+ console.log(` ${chalk.dim('Probe:')} ${connector.probe_kind}`);
33
+ if (connector.endpoint) {
34
+ console.log(` ${chalk.dim('URL:')} ${connector.endpoint}`);
35
+ }
36
+ if (connector.status_code != null) {
37
+ console.log(` ${chalk.dim('HTTP:')} ${connector.status_code}`);
38
+ }
39
+ if (connector.auth_env) {
40
+ console.log(` ${chalk.dim('Auth:')} ${connector.auth_env}`);
41
+ }
42
+ if (connector.latency_ms != null) {
43
+ console.log(` ${chalk.dim('Time:')} ${connector.latency_ms}ms`);
44
+ }
45
+ console.log(` ${chalk.dim('Detail:')} ${connector.detail}`);
46
+ }
47
+
48
+ console.log('');
49
+ const summary = result.overall === 'pass'
50
+ ? chalk.green(` ✓ ${result.pass_count}/${result.connectors.length} connectors passed`)
51
+ : chalk.red(` ${result.fail_count} connector failure(s), ${result.pass_count} passed`);
52
+ console.log(summary);
53
+ console.log('');
54
+ process.exit(exitCode);
55
+ }
56
+
57
+ export async function connectorCheckCommand(runtimeId, options = {}) {
58
+ const context = loadProjectContext();
59
+ if (!context) {
60
+ const payload = { overall: 'error', error: 'No governed agentxchain.json found.' };
61
+ if (options.json) {
62
+ printJson(payload, 2);
63
+ return;
64
+ }
65
+ console.error(chalk.red('No governed agentxchain.json found. Run this inside a governed project.'));
66
+ process.exit(2);
67
+ }
68
+
69
+ if (context.config.protocol_mode !== 'governed') {
70
+ const payload = { overall: 'error', error: 'connector check only supports governed projects.' };
71
+ if (options.json) {
72
+ printJson(payload, 2);
73
+ return;
74
+ }
75
+ console.error(chalk.red('connector check only supports governed projects.'));
76
+ process.exit(2);
77
+ }
78
+
79
+ const timeoutMs = Number.parseInt(options.timeout || DEFAULT_TIMEOUT_MS, 10);
80
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
81
+ const payload = { overall: 'error', error: 'Timeout must be a positive integer.' };
82
+ if (options.json) {
83
+ printJson(payload, 2);
84
+ return;
85
+ }
86
+ console.error(chalk.red('Timeout must be a positive integer.'));
87
+ process.exit(2);
88
+ }
89
+
90
+ const result = await probeConfiguredConnectors(context.config, {
91
+ runtimeId: runtimeId || null,
92
+ timeoutMs,
93
+ onProbeStart: options.json ? null : (probeRuntimeId, runtime) => {
94
+ console.log(` ${chalk.dim('…')} Probing ${chalk.bold(probeRuntimeId)} ${chalk.dim(`(${runtime.type})`)}`);
95
+ },
96
+ });
97
+
98
+ if (!result.ok && result.error) {
99
+ const payload = { overall: 'error', error: result.error };
100
+ if (options.json) {
101
+ printJson(payload, result.exitCode || 2);
102
+ return;
103
+ }
104
+ console.error(chalk.red(result.error));
105
+ process.exit(result.exitCode || 2);
106
+ }
107
+
108
+ const payload = {
109
+ overall: result.overall,
110
+ timeout_ms: result.timeout_ms,
111
+ pass_count: result.pass_count,
112
+ fail_count: result.fail_count,
113
+ connectors: result.connectors,
114
+ };
115
+
116
+ if (options.json) {
117
+ printJson(payload, result.exitCode);
118
+ return;
119
+ }
120
+
121
+ printText(payload, result.exitCode);
122
+ }
@@ -608,7 +608,8 @@ All acceptance criteria met. OBJ-002 (clock skew) noted for follow-up. OBJ-003 (
608
608
  console.log('');
609
609
  console.log(` ${chalk.dim('1.')} ${chalk.bold('Scaffold')} agentxchain init --governed --goal "Your project mission"`);
610
610
  console.log(` ${chalk.dim('2.')} ${chalk.bold('Verify')} agentxchain doctor`);
611
- console.log(` ${chalk.dim('3.')} ${chalk.bold('First turn')} agentxchain run`);
611
+ console.log(` ${chalk.dim('3.')} ${chalk.bold('Probe')} agentxchain connector check`);
612
+ console.log(` ${chalk.dim('4.')} ${chalk.bold('First turn')} agentxchain run`);
612
613
  console.log('');
613
614
  console.log(` ${chalk.bold('Docs:')} https://agentxchain.dev/docs/quickstart`);
614
615
  console.log('');
@@ -7,6 +7,7 @@ import { validateProject } from '../lib/validation.js';
7
7
  import { getWatchPid } from './watch.js';
8
8
  import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
9
9
  import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
10
+ import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
10
11
 
11
12
  export async function doctorCommand(opts = {}) {
12
13
  const root = findProjectRoot(process.cwd());
@@ -136,9 +137,11 @@ function governedDoctor(root, rawConfig, opts) {
136
137
 
137
138
  if (opts.json) {
138
139
  const projectId = rawConfig?.project?.id || rawConfig?.project?.name || 'unknown';
140
+ const versionSurface = getGovernedVersionSurface(rawConfig);
139
141
  console.log(JSON.stringify({
140
142
  project: projectId,
141
- config_version: 4,
143
+ ...versionSurface,
144
+ config_version: versionSurface.config_generation,
142
145
  overall,
143
146
  checks,
144
147
  fail_count: failCount,
@@ -149,7 +152,8 @@ function governedDoctor(root, rawConfig, opts) {
149
152
  console.log('');
150
153
  console.log(chalk.bold(' AgentXchain Governed Doctor'));
151
154
  console.log(chalk.dim(' ' + '─'.repeat(44)));
152
- console.log(chalk.dim(` Project: ${projectId} (v4)`));
155
+ console.log(chalk.dim(` Project: ${projectId}`));
156
+ console.log(chalk.dim(` Versioning: ${formatGovernedVersionLabel(rawConfig)}`));
153
157
  console.log('');
154
158
 
155
159
  for (const c of checks) {
@@ -971,6 +971,7 @@ async function initGoverned(opts) {
971
971
  }
972
972
  console.log(` ${chalk.bold('agentxchain template validate')} ${chalk.dim('# prove the scaffold contract before the first turn')}`);
973
973
  console.log(` ${chalk.bold('agentxchain doctor')} ${chalk.dim('# verify runtimes, config, and readiness')}`);
974
+ console.log(` ${chalk.bold('agentxchain connector check')} ${chalk.dim('# live-probe configured runtimes before the first turn')}`);
974
975
  console.log(` ${chalk.bold('git add -A')} ${chalk.dim('# stage the governed scaffold')}`);
975
976
  console.log(` ${chalk.bold('git commit -m "initial governed scaffold"')} ${chalk.dim('# checkpoint the starting state')}`);
976
977
  console.log(` ${chalk.bold('agentxchain step')} ${chalk.dim('# run the first governed turn')}`);
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadConfig, loadProjectContext } from '../lib/config.js';
3
3
  import { validateGovernedProject, validateProject } from '../lib/validation.js';
4
+ import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
4
5
 
5
6
  export async function validateCommand(opts) {
6
7
  const context = loadProjectContext();
@@ -21,9 +22,13 @@ export async function validateCommand(opts) {
21
22
  });
22
23
 
23
24
  if (opts.json) {
25
+ const governedVersionSurface = context.config.protocol_mode === 'governed'
26
+ ? getGovernedVersionSurface(context.rawConfig)
27
+ : {};
24
28
  console.log(JSON.stringify({
25
29
  ...validation,
26
30
  protocol_mode: context.config.protocol_mode,
31
+ ...governedVersionSurface,
27
32
  version: context.version,
28
33
  }, null, 2));
29
34
  } else {
@@ -31,7 +36,11 @@ export async function validateCommand(opts) {
31
36
  console.log(chalk.bold(` AgentXchain Validate (${mode})`));
32
37
  console.log(chalk.dim(' ' + '─'.repeat(44)));
33
38
  console.log(chalk.dim(` Root: ${context.root}`));
34
- console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (v${context.version})`));
39
+ if (context.config.protocol_mode === 'governed') {
40
+ console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (${formatGovernedVersionLabel(context.rawConfig)})`));
41
+ } else {
42
+ console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (v${context.version})`));
43
+ }
35
44
  console.log('');
36
45
 
37
46
  if (validation.ok) {
@@ -0,0 +1,353 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ import {
4
+ buildProviderHeaders,
5
+ buildProviderRequest,
6
+ PROVIDER_ENDPOINTS,
7
+ } from './adapters/api-proxy-adapter.js';
8
+
9
+ const PROBEABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
10
+ const DEFAULT_TIMEOUT_MS = 8_000;
11
+
12
+ function formatCommand(command, args = []) {
13
+ if (Array.isArray(command)) {
14
+ return command.join(' ');
15
+ }
16
+ if (typeof command === 'string' && command.length > 0) {
17
+ return [command, ...(Array.isArray(args) ? args : [])].join(' ');
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function formatTarget(runtime) {
23
+ switch (runtime?.type) {
24
+ case 'api_proxy':
25
+ return [runtime.provider, runtime.model].filter(Boolean).join(' / ') || 'unknown target';
26
+ case 'remote_agent':
27
+ return runtime.url || 'unknown target';
28
+ case 'mcp':
29
+ return runtime.transport === 'streamable_http'
30
+ ? (runtime.url || 'unknown target')
31
+ : (formatCommand(runtime.command, runtime.args) || 'unknown target');
32
+ case 'local_cli':
33
+ return formatCommand(runtime.command, runtime.args) || 'unknown target';
34
+ default:
35
+ return 'unknown target';
36
+ }
37
+ }
38
+
39
+ function commandHead(runtime) {
40
+ if (Array.isArray(runtime?.command)) {
41
+ return runtime.command[0] || null;
42
+ }
43
+ if (typeof runtime?.command === 'string' && runtime.command.trim()) {
44
+ return runtime.command.trim().split(/\s+/)[0];
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function resolveBinary(command) {
50
+ const resolver = process.platform === 'win32' ? 'where' : 'which';
51
+ execFileSync(resolver, [command], { stdio: 'ignore' });
52
+ }
53
+
54
+ function resolveProviderEndpoint(runtime) {
55
+ if (typeof runtime?.base_url === 'string' && runtime.base_url.trim()) {
56
+ return runtime.base_url.trim();
57
+ }
58
+
59
+ const provider = String(runtime?.provider || '').trim().toLowerCase();
60
+ const endpointTemplate = PROVIDER_ENDPOINTS[provider];
61
+ if (!endpointTemplate) return null;
62
+
63
+ if (endpointTemplate.includes('{model}')) {
64
+ return endpointTemplate.replace('{model}', encodeURIComponent(runtime.model || ''));
65
+ }
66
+ return endpointTemplate;
67
+ }
68
+
69
+ function classifyHttpFailure(status, bodyText, authEnv) {
70
+ if (status === 401 || status === 403) {
71
+ return {
72
+ level: 'fail',
73
+ detail: authEnv
74
+ ? `${authEnv} was rejected by the remote endpoint`
75
+ : 'Remote endpoint rejected the request as unauthorized',
76
+ };
77
+ }
78
+ if (status === 404) {
79
+ return {
80
+ level: 'fail',
81
+ detail: 'Configured endpoint or model was not found',
82
+ };
83
+ }
84
+ if (status === 429) {
85
+ return {
86
+ level: 'fail',
87
+ detail: 'Remote endpoint is reachable but currently rate limited',
88
+ };
89
+ }
90
+ return {
91
+ level: 'fail',
92
+ detail: bodyText
93
+ ? `Remote endpoint returned HTTP ${status}: ${bodyText.slice(0, 160)}`
94
+ : `Remote endpoint returned HTTP ${status}`,
95
+ };
96
+ }
97
+
98
+ async function probeHttp({ url, method = 'GET', headers = {}, body, timeoutMs }) {
99
+ const startedAt = Date.now();
100
+ try {
101
+ const response = await fetch(url, {
102
+ method,
103
+ headers,
104
+ body,
105
+ signal: AbortSignal.timeout(timeoutMs),
106
+ });
107
+ const latencyMs = Date.now() - startedAt;
108
+ const responseText = await response.text();
109
+ return {
110
+ ok: true,
111
+ statusCode: response.status,
112
+ latencyMs,
113
+ responseText,
114
+ };
115
+ } catch (error) {
116
+ return {
117
+ ok: false,
118
+ latencyMs: Date.now() - startedAt,
119
+ error,
120
+ };
121
+ }
122
+ }
123
+
124
+ async function probeLocalCommand(runtimeId, runtime, probeKindLabel) {
125
+ const head = commandHead(runtime);
126
+ const base = {
127
+ runtime_id: runtimeId,
128
+ type: runtime.type,
129
+ target: formatTarget(runtime),
130
+ probe_kind: probeKindLabel,
131
+ };
132
+
133
+ if (!head) {
134
+ return {
135
+ ...base,
136
+ level: 'fail',
137
+ detail: 'No command configured',
138
+ };
139
+ }
140
+
141
+ try {
142
+ resolveBinary(head);
143
+ return {
144
+ ...base,
145
+ level: 'pass',
146
+ command: head,
147
+ detail: `${head} is available on PATH`,
148
+ };
149
+ } catch {
150
+ return {
151
+ ...base,
152
+ level: 'fail',
153
+ command: head,
154
+ detail: `${head} was not found on PATH`,
155
+ };
156
+ }
157
+ }
158
+
159
+ async function probeApiProxy(runtimeId, runtime, timeoutMs) {
160
+ const base = {
161
+ runtime_id: runtimeId,
162
+ type: runtime.type,
163
+ target: formatTarget(runtime),
164
+ probe_kind: 'live_api_request',
165
+ auth_env: runtime.auth_env || null,
166
+ };
167
+
168
+ if (runtime.auth_env && !process.env[runtime.auth_env]) {
169
+ return {
170
+ ...base,
171
+ level: 'fail',
172
+ detail: `${runtime.auth_env} is not set`,
173
+ };
174
+ }
175
+
176
+ const endpoint = resolveProviderEndpoint(runtime);
177
+ if (!endpoint) {
178
+ return {
179
+ ...base,
180
+ level: 'fail',
181
+ detail: `No probe endpoint is defined for provider "${runtime.provider || 'unknown'}"`,
182
+ };
183
+ }
184
+
185
+ const provider = String(runtime.provider || '').trim().toLowerCase();
186
+ const apiKey = runtime.auth_env ? process.env[runtime.auth_env] : '';
187
+ const requestBody = JSON.stringify(buildProviderRequest(provider, 'ping', '', runtime.model, 1));
188
+ const headers = buildProviderHeaders(provider, apiKey);
189
+ const result = await probeHttp({
190
+ url: endpoint,
191
+ method: 'POST',
192
+ headers,
193
+ body: requestBody,
194
+ timeoutMs,
195
+ });
196
+
197
+ if (!result.ok) {
198
+ return {
199
+ ...base,
200
+ endpoint,
201
+ level: 'fail',
202
+ latency_ms: result.latencyMs,
203
+ detail: `Probe failed: ${result.error?.name === 'TimeoutError' ? 'timed out' : (result.error?.message || 'network error')}`,
204
+ };
205
+ }
206
+
207
+ if (result.statusCode >= 200 && result.statusCode < 300) {
208
+ return {
209
+ ...base,
210
+ endpoint,
211
+ status_code: result.statusCode,
212
+ latency_ms: result.latencyMs,
213
+ level: 'pass',
214
+ detail: `${runtime.provider} accepted the live probe request`,
215
+ };
216
+ }
217
+
218
+ const classified = classifyHttpFailure(result.statusCode, result.responseText, runtime.auth_env);
219
+ return {
220
+ ...base,
221
+ endpoint,
222
+ status_code: result.statusCode,
223
+ latency_ms: result.latencyMs,
224
+ ...classified,
225
+ };
226
+ }
227
+
228
+ async function probeHttpRuntime(runtimeId, runtime, timeoutMs) {
229
+ const endpoint = runtime.url;
230
+ const base = {
231
+ runtime_id: runtimeId,
232
+ type: runtime.type,
233
+ target: formatTarget(runtime),
234
+ probe_kind: 'live_http_ping',
235
+ endpoint,
236
+ };
237
+
238
+ const result = await probeHttp({
239
+ url: endpoint,
240
+ method: 'GET',
241
+ headers: runtime.headers || {},
242
+ timeoutMs,
243
+ });
244
+
245
+ if (!result.ok) {
246
+ return {
247
+ ...base,
248
+ level: 'fail',
249
+ latency_ms: result.latencyMs,
250
+ detail: `Probe failed: ${result.error?.name === 'TimeoutError' ? 'timed out' : (result.error?.message || 'network error')}`,
251
+ };
252
+ }
253
+
254
+ if ((result.statusCode >= 200 && result.statusCode < 300) || result.statusCode === 405) {
255
+ return {
256
+ ...base,
257
+ level: 'pass',
258
+ status_code: result.statusCode,
259
+ latency_ms: result.latencyMs,
260
+ detail: result.statusCode === 405
261
+ ? 'Endpoint is reachable but rejects GET (expected for some RPC transports)'
262
+ : 'Endpoint responded to the live probe',
263
+ };
264
+ }
265
+
266
+ const classified = classifyHttpFailure(result.statusCode, result.responseText, null);
267
+ return {
268
+ ...base,
269
+ status_code: result.statusCode,
270
+ latency_ms: result.latencyMs,
271
+ ...classified,
272
+ };
273
+ }
274
+
275
+ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
276
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
277
+
278
+ if (!runtime || !PROBEABLE_RUNTIME_TYPES.has(runtime.type)) {
279
+ return {
280
+ runtime_id: runtimeId,
281
+ type: runtime?.type || 'unknown',
282
+ target: formatTarget(runtime),
283
+ probe_kind: 'unsupported',
284
+ level: 'fail',
285
+ detail: `Runtime type "${runtime?.type || 'unknown'}" cannot be probed`,
286
+ };
287
+ }
288
+
289
+ if (runtime.type === 'local_cli') {
290
+ return probeLocalCommand(runtimeId, runtime, 'command_presence');
291
+ }
292
+
293
+ if (runtime.type === 'api_proxy') {
294
+ return probeApiProxy(runtimeId, runtime, timeoutMs);
295
+ }
296
+
297
+ if (runtime.type === 'mcp') {
298
+ if (runtime.transport === 'streamable_http') {
299
+ return probeHttpRuntime(runtimeId, runtime, timeoutMs);
300
+ }
301
+ return probeLocalCommand(runtimeId, runtime, 'command_presence');
302
+ }
303
+
304
+ return probeHttpRuntime(runtimeId, runtime, timeoutMs);
305
+ }
306
+
307
+ export async function probeConfiguredConnectors(config, options = {}) {
308
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
309
+ const requestedRuntimeId = options.runtimeId || null;
310
+ const onProbeStart = typeof options.onProbeStart === 'function' ? options.onProbeStart : null;
311
+
312
+ const runtimeEntries = Object.entries(config?.runtimes || {})
313
+ .filter(([, runtime]) => PROBEABLE_RUNTIME_TYPES.has(runtime?.type))
314
+ .sort(([left], [right]) => left.localeCompare(right, 'en'));
315
+
316
+ if (requestedRuntimeId) {
317
+ const match = runtimeEntries.find(([runtimeId]) => runtimeId === requestedRuntimeId);
318
+ if (!match) {
319
+ return {
320
+ ok: false,
321
+ error: `Unknown connector runtime "${requestedRuntimeId}"`,
322
+ exitCode: 2,
323
+ };
324
+ }
325
+ const [runtimeId, runtime] = match;
326
+ onProbeStart?.(runtimeId, runtime);
327
+ const connector = await probeConnectorRuntime(runtimeId, runtime, { timeoutMs });
328
+ return summarizeResults([connector], timeoutMs);
329
+ }
330
+
331
+ const connectors = [];
332
+ for (const [runtimeId, runtime] of runtimeEntries) {
333
+ onProbeStart?.(runtimeId, runtime);
334
+ connectors.push(await probeConnectorRuntime(runtimeId, runtime, { timeoutMs }));
335
+ }
336
+ return summarizeResults(connectors, timeoutMs);
337
+ }
338
+
339
+ function summarizeResults(connectors, timeoutMs) {
340
+ const failCount = connectors.filter((item) => item.level === 'fail').length;
341
+ const passCount = connectors.filter((item) => item.level === 'pass').length;
342
+ return {
343
+ ok: failCount === 0,
344
+ exitCode: failCount === 0 ? 0 : 1,
345
+ overall: failCount === 0 ? 'pass' : 'fail',
346
+ timeout_ms: timeoutMs,
347
+ pass_count: passCount,
348
+ fail_count: failCount,
349
+ connectors,
350
+ };
351
+ }
352
+
353
+ export { DEFAULT_TIMEOUT_MS, PROBEABLE_RUNTIME_TYPES };
@@ -0,0 +1,40 @@
1
+ export const CURRENT_PROTOCOL_VERSION = 'v6';
2
+ export const CURRENT_CONFIG_GENERATION = 4;
3
+
4
+ export function getGovernedConfigSchemaVersion(rawConfig) {
5
+ if (!rawConfig || typeof rawConfig !== 'object') {
6
+ return null;
7
+ }
8
+
9
+ if (typeof rawConfig.schema_version === 'string' && rawConfig.schema_version.trim()) {
10
+ return rawConfig.schema_version.trim();
11
+ }
12
+
13
+ if (rawConfig.schema_version === 4) {
14
+ return '1.0';
15
+ }
16
+
17
+ return null;
18
+ }
19
+
20
+ export function getGovernedVersionSurface(rawConfig) {
21
+ return {
22
+ protocol_version: CURRENT_PROTOCOL_VERSION,
23
+ config_generation: CURRENT_CONFIG_GENERATION,
24
+ config_schema_version: getGovernedConfigSchemaVersion(rawConfig),
25
+ };
26
+ }
27
+
28
+ export function formatGovernedVersionLabel(rawConfig) {
29
+ const surface = getGovernedVersionSurface(rawConfig);
30
+ const parts = [
31
+ `protocol ${surface.protocol_version}`,
32
+ `config generation v${surface.config_generation}`,
33
+ ];
34
+
35
+ if (surface.config_schema_version) {
36
+ parts.push(`config schema ${surface.config_schema_version}`);
37
+ }
38
+
39
+ return parts.join(', ');
40
+ }
package/src/lib/report.js CHANGED
@@ -149,6 +149,67 @@ function formatDurationCompact(ms) {
149
149
  return `${hrs}h ${remainMins}m`;
150
150
  }
151
151
 
152
+ function formatTokenCount(value) {
153
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 'n/a';
154
+ return value.toLocaleString('en-US');
155
+ }
156
+
157
+ export function computeCostSummary(turns) {
158
+ if (!Array.isArray(turns) || turns.length === 0) return null;
159
+
160
+ let totalUsd = 0;
161
+ let costedTurnCount = 0;
162
+ let hasAnyTokens = false;
163
+ let totalInputTokens = 0;
164
+ let totalOutputTokens = 0;
165
+ const roleMap = new Map();
166
+ const phaseMap = new Map();
167
+
168
+ for (const t of turns) {
169
+ const costUsd = typeof t.cost_usd === 'number' && Number.isFinite(t.cost_usd) ? t.cost_usd : 0;
170
+ const hasFiniteCost = typeof t.cost_usd === 'number' && Number.isFinite(t.cost_usd);
171
+ if (hasFiniteCost) {
172
+ totalUsd += costUsd;
173
+ costedTurnCount++;
174
+ }
175
+
176
+ const inTok = typeof t.input_tokens === 'number' && Number.isFinite(t.input_tokens) ? t.input_tokens : 0;
177
+ const outTok = typeof t.output_tokens === 'number' && Number.isFinite(t.output_tokens) ? t.output_tokens : 0;
178
+ if (t.input_tokens != null || t.output_tokens != null) hasAnyTokens = true;
179
+ totalInputTokens += inTok;
180
+ totalOutputTokens += outTok;
181
+
182
+ // Aggregate by role
183
+ const role = t.role || 'unknown';
184
+ if (!roleMap.has(role)) roleMap.set(role, { role, usd: 0, turns: 0, input_tokens: 0, output_tokens: 0 });
185
+ const roleEntry = roleMap.get(role);
186
+ roleEntry.usd += costUsd;
187
+ roleEntry.turns++;
188
+ roleEntry.input_tokens += inTok;
189
+ roleEntry.output_tokens += outTok;
190
+
191
+ // Aggregate by phase
192
+ const phase = t.phase || 'unknown';
193
+ if (!phaseMap.has(phase)) phaseMap.set(phase, { phase, usd: 0, turns: 0 });
194
+ const phaseEntry = phaseMap.get(phase);
195
+ phaseEntry.usd += costUsd;
196
+ phaseEntry.turns++;
197
+ }
198
+
199
+ const byRole = [...roleMap.values()].sort((a, b) => a.role.localeCompare(b.role, 'en'));
200
+ const byPhase = [...phaseMap.values()].sort((a, b) => a.phase.localeCompare(b.phase, 'en'));
201
+
202
+ return {
203
+ total_usd: totalUsd,
204
+ total_input_tokens: hasAnyTokens ? totalInputTokens : null,
205
+ total_output_tokens: hasAnyTokens ? totalOutputTokens : null,
206
+ turn_count: turns.length,
207
+ costed_turn_count: costedTurnCount,
208
+ by_role: byRole,
209
+ by_phase: byPhase,
210
+ };
211
+ }
212
+
152
213
  function formatTurnTimelineTime(turn) {
153
214
  const acceptedAt = turn.accepted_at || 'n/a';
154
215
  const duration = formatDurationCompact(turn.duration_ms);
@@ -188,6 +249,8 @@ function extractHistoryTimeline(artifact) {
188
249
  decisions: Array.isArray(e.decisions) ? e.decisions.map((d) => d?.id || d).filter(Boolean) : [],
189
250
  objections: Array.isArray(e.objections) ? e.objections.map((o) => o?.id || o).filter(Boolean) : [],
190
251
  cost_usd: typeof e.cost?.total_usd === 'number' ? e.cost.total_usd : null,
252
+ input_tokens: typeof e.cost?.input_tokens === 'number' && Number.isFinite(e.cost.input_tokens) ? e.cost.input_tokens : null,
253
+ output_tokens: typeof e.cost?.output_tokens === 'number' && Number.isFinite(e.cost.output_tokens) ? e.cost.output_tokens : null,
191
254
  started_at: e.started_at || null,
192
255
  duration_ms: typeof e.duration_ms === 'number' ? e.duration_ms : null,
193
256
  accepted_at: e.accepted_at || null,
@@ -808,6 +871,7 @@ function buildRunSubject(artifact) {
808
871
  retained_turn_ids: retainedTurns,
809
872
  active_roles: activeRoles,
810
873
  budget_status: normalizeBudgetStatus(artifact.state?.budget_status),
874
+ cost_summary: computeCostSummary(turns),
811
875
  created_at: timing.created_at,
812
876
  completed_at: timing.completed_at,
813
877
  duration_seconds: timing.duration_seconds,
@@ -1086,6 +1150,30 @@ export function formatGovernanceReportText(report) {
1086
1150
  `Coordinator artifacts: ${yesNo(artifacts.coordinator_present)}`,
1087
1151
  );
1088
1152
 
1153
+ if (run.cost_summary) {
1154
+ const cs = run.cost_summary;
1155
+ lines.push('', 'Cost Summary:');
1156
+ lines.push(` Total: ${formatUsd(cs.total_usd)} across ${cs.turn_count} turn${cs.turn_count !== 1 ? 's' : ''} (${cs.costed_turn_count} with cost data)`);
1157
+ if (cs.total_input_tokens != null || cs.total_output_tokens != null) {
1158
+ lines.push(` Tokens: ${formatTokenCount(cs.total_input_tokens)} input / ${formatTokenCount(cs.total_output_tokens)} output`);
1159
+ }
1160
+ if (cs.by_role.length > 0) {
1161
+ lines.push(' By role:');
1162
+ for (const r of cs.by_role) {
1163
+ const tokens = r.input_tokens || r.output_tokens
1164
+ ? `, ${formatTokenCount(r.input_tokens)} in / ${formatTokenCount(r.output_tokens)} out`
1165
+ : '';
1166
+ lines.push(` ${r.role}: ${formatUsd(r.usd)} (${r.turns} turn${r.turns !== 1 ? 's' : ''}${tokens})`);
1167
+ }
1168
+ }
1169
+ if (cs.by_phase.length > 0) {
1170
+ lines.push(' By phase:');
1171
+ for (const p of cs.by_phase) {
1172
+ lines.push(` ${p.phase}: ${formatUsd(p.usd)} (${p.turns} turn${p.turns !== 1 ? 's' : ''})`);
1173
+ }
1174
+ }
1175
+ }
1176
+
1089
1177
  if (run.turns && run.turns.length > 0) {
1090
1178
  lines.push('', 'Turn Timeline:');
1091
1179
  for (let i = 0; i < run.turns.length; i++) {
@@ -1519,6 +1607,27 @@ export function formatGovernanceReportMarkdown(report) {
1519
1607
  `- Coordinator artifacts: \`${yesNo(artifacts.coordinator_present)}\``,
1520
1608
  );
1521
1609
 
1610
+ if (run.cost_summary) {
1611
+ const cs = run.cost_summary;
1612
+ lines.push('', '## Cost Summary', '');
1613
+ lines.push(`**Total:** ${formatUsd(cs.total_usd)} across ${cs.turn_count} turn${cs.turn_count !== 1 ? 's' : ''} (${cs.costed_turn_count} with cost data)`);
1614
+ if (cs.total_input_tokens != null || cs.total_output_tokens != null) {
1615
+ lines.push(`**Tokens:** ${formatTokenCount(cs.total_input_tokens)} input / ${formatTokenCount(cs.total_output_tokens)} output`);
1616
+ }
1617
+ if (cs.by_role.length > 0) {
1618
+ lines.push('', '| Role | Cost | Turns | Input Tokens | Output Tokens |', '|------|------|-------|--------------|---------------|');
1619
+ for (const r of cs.by_role) {
1620
+ lines.push(`| ${r.role} | ${formatUsd(r.usd)} | ${r.turns} | ${formatTokenCount(r.input_tokens)} | ${formatTokenCount(r.output_tokens)} |`);
1621
+ }
1622
+ }
1623
+ if (cs.by_phase.length > 0) {
1624
+ lines.push('', '| Phase | Cost | Turns |', '|-------|------|-------|');
1625
+ for (const p of cs.by_phase) {
1626
+ lines.push(`| ${p.phase} | ${formatUsd(p.usd)} | ${p.turns} |`);
1627
+ }
1628
+ }
1629
+ }
1630
+
1522
1631
  if (run.turns && run.turns.length > 0) {
1523
1632
  lines.push('', '## Turn Timeline', '', '| # | Role | Phase | Summary | Files | Cost | Time |', '|---|------|-------|---------|-------|------|------|');
1524
1633
  for (let i = 0; i < run.turns.length; i++) {