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 +9 -0
- package/bin/agentxchain.js +19 -0
- package/package.json +1 -1
- package/src/commands/audit.js +75 -0
- package/src/commands/connector.js +122 -0
- package/src/commands/demo.js +2 -1
- package/src/commands/doctor.js +6 -2
- package/src/commands/init.js +1 -0
- package/src/commands/validate.js +10 -1
- package/src/lib/connector-probe.js +353 -0
- package/src/lib/protocol-version.js +40 -0
- package/src/lib/report.js +109 -0
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
|
package/bin/agentxchain.js
CHANGED
|
@@ -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
|
@@ -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
|
+
}
|
package/src/commands/demo.js
CHANGED
|
@@ -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('
|
|
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('');
|
package/src/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
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}
|
|
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) {
|
package/src/commands/init.js
CHANGED
|
@@ -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')}`);
|
package/src/commands/validate.js
CHANGED
|
@@ -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
|
-
|
|
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++) {
|