@voiden/runner 2.2.0 → 2.3.0-beta.10

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/bundled-runners/versions.json +8 -6
  3. package/bundled-runners/voiden-advanced-auth-runner.js +1 -1
  4. package/bundled-runners/voiden-graphql-runner.js +1 -1
  5. package/bundled-runners/voiden-mcp-client-runner.js +1 -0
  6. package/bundled-runners/voiden-mcp-tool-runner.js +143 -0
  7. package/bundled-runners/voiden-rest-api-runner.js +1 -1
  8. package/bundled-runners/voiden-sockets-grpcs-runner.js +1 -1
  9. package/dist/cliPrint.d.ts +7 -0
  10. package/dist/cliPrint.d.ts.map +1 -0
  11. package/dist/cliPrint.js +166 -0
  12. package/dist/cliPrint.js.map +1 -0
  13. package/dist/envFile.d.ts +13 -0
  14. package/dist/envFile.d.ts.map +1 -0
  15. package/dist/envFile.js +117 -0
  16. package/dist/envFile.js.map +1 -0
  17. package/dist/headlessContext.d.ts.map +1 -1
  18. package/dist/headlessContext.js +43 -0
  19. package/dist/headlessContext.js.map +1 -1
  20. package/dist/index.js +271 -33
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib.d.ts +12 -8
  23. package/dist/lib.d.ts.map +1 -1
  24. package/dist/lib.js +16 -9
  25. package/dist/lib.js.map +1 -1
  26. package/dist/mcpServing.d.ts +39 -0
  27. package/dist/mcpServing.d.ts.map +1 -0
  28. package/dist/mcpServing.js +154 -0
  29. package/dist/mcpServing.js.map +1 -0
  30. package/dist/mcpToolCapability.d.ts +158 -0
  31. package/dist/mcpToolCapability.d.ts.map +1 -0
  32. package/dist/mcpToolCapability.js +87 -0
  33. package/dist/mcpToolCapability.js.map +1 -0
  34. package/dist/plugins/loader.d.ts.map +1 -1
  35. package/dist/plugins/loader.js +6 -0
  36. package/dist/plugins/loader.js.map +1 -1
  37. package/dist/requestContainerRegistry.d.ts +36 -0
  38. package/dist/requestContainerRegistry.d.ts.map +1 -0
  39. package/dist/requestContainerRegistry.js +42 -0
  40. package/dist/requestContainerRegistry.js.map +1 -0
  41. package/dist/resultBlock.d.ts +7 -1
  42. package/dist/resultBlock.d.ts.map +1 -1
  43. package/dist/resultBlock.js +28 -6
  44. package/dist/resultBlock.js.map +1 -1
  45. package/dist/runner.d.ts +9 -0
  46. package/dist/runner.d.ts.map +1 -1
  47. package/dist/runner.js +32 -11
  48. package/dist/runner.js.map +1 -1
  49. package/dist/runtimeVars.d.ts.map +1 -1
  50. package/dist/runtimeVars.js +18 -0
  51. package/dist/runtimeVars.js.map +1 -1
  52. package/dist/toolRegistry.d.ts +102 -0
  53. package/dist/toolRegistry.d.ts.map +1 -0
  54. package/dist/toolRegistry.js +38 -0
  55. package/dist/toolRegistry.js.map +1 -0
  56. package/dist/types.d.ts +1 -5
  57. package/dist/types.d.ts.map +1 -1
  58. package/package.json +11 -5
  59. package/dist/mcpInstall.d.ts +0 -48
  60. package/dist/mcpInstall.d.ts.map +0 -1
  61. package/dist/mcpInstall.js +0 -194
  62. package/dist/mcpInstall.js.map +0 -1
  63. package/dist/parser.d.ts +0 -24
  64. package/dist/parser.d.ts.map +0 -1
  65. package/dist/parser.js +0 -87
  66. package/dist/parser.js.map +0 -1
  67. package/dist/skillContent.d.ts +0 -8
  68. package/dist/skillContent.d.ts.map +0 -1
  69. package/dist/skillContent.js +0 -42
  70. package/dist/skillContent.js.map +0 -1
package/dist/index.js CHANGED
@@ -6,6 +6,12 @@ import { fileURLToPath } from 'url';
6
6
  import chalk from 'chalk';
7
7
  import { runVoidFile } from './runner.js';
8
8
  import { resolveFiles } from './discovery.js';
9
+ import { discoverTools, verifyTools, validateTools, upsertToolStatus, registerToolsFromDecisions, planServedTools, getCommitSha } from './mcpToolCapability.js';
10
+ import { registerFixedTools } from './mcpServing.js';
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
14
+ import { createServer as createHttpServer } from 'node:http';
9
15
  import { loadEnabledPlugins } from './plugins/loader.js';
10
16
  import { exportToCsv } from './report/csv.js';
11
17
  import { sendMailReport } from './report/mail.js';
@@ -15,10 +21,8 @@ import { fetchCommunityPlugins, findCommunityPlugin, hasCommunityRunner, install
15
21
  import { installPlugin, uninstallPlugin, setPluginEnabled, setPluginVersion, getAllInstalledPlugins, readStore, STORE_DIR, } from './plugins/store.js';
16
22
  import { checkForPluginUpdates } from './plugins/updateCheck.js';
17
23
  import { getInstalledPluginInfo } from './plugins/versionInfo.js';
18
- import { parseVoidFile } from './parser.js';
19
- import { classifyBlockVersion } from '@voiden/executors';
20
- import { installMcpIntegration, uninstallMcpIntegration, getMcpStatus } from './mcpInstall.js';
21
- import { RUNNER_SKILL_MARKDOWN } from './skillContent.js';
24
+ import { classifyBlockVersion, parseVoidFile, installMcpIntegration, uninstallMcpIntegration, getMcpStatus, MCP_SKILL_MARKDOWN, } from '@voiden/executors';
25
+ import { loadEnvFile } from './envFile.js';
22
26
  import { appendSessionResults, loadSessionResults, clearSession, } from './session.js';
23
27
  // ─────────────────────────────────────────────────────────────────────────────
24
28
  // Exit codes — a stable, documented contract CI pipelines can branch on.
@@ -41,25 +45,6 @@ const JSON_SCHEMA_VERSION = '1';
41
45
  // ─────────────────────────────────────────────────────────────────────────────
42
46
  // Helpers
43
47
  // ─────────────────────────────────────────────────────────────────────────────
44
- function loadEnvFile(envPath) {
45
- const content = readFileSync(envPath, 'utf-8');
46
- const env = {};
47
- const lines = content.split('\n');
48
- for (let i = 0; i < lines.length; i++) {
49
- const line = lines[i].trim();
50
- if (!line || line.startsWith('#'))
51
- continue;
52
- const eq = line.indexOf('=');
53
- if (eq === -1)
54
- throw new Error(`Malformed line ${i + 1} in .env file: missing "="`);
55
- const key = line.slice(0, eq).trim();
56
- const val = line.slice(eq + 1).trim().replace(/^["']|["']$/g, '');
57
- if (!key)
58
- throw new Error(`Malformed line ${i + 1} in .env file: empty key`);
59
- env[key] = val;
60
- }
61
- return env;
62
- }
63
48
  function formatBytes(bytes) {
64
49
  if (bytes < 1024)
65
50
  return `${bytes}B`;
@@ -450,8 +435,10 @@ program
450
435
  ' voiden-runner run auth.void\n' +
451
436
  ' voiden-runner run ./requests/\n' +
452
437
  ' voiden-runner run auth.void users.void ./smoke/\n' +
453
- ' voiden-runner run ./ --env .env.staging --bail\n')
438
+ ' voiden-runner run ./ --env .env.staging --bail\n' +
439
+ ' voiden-runner run ./ --env .voiden/env-public.yaml --environment staging\n')
454
440
  .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
441
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
455
442
  .option('--env-var <key=value>', 'Individual environment variable override (can be used multiple times)', (val, memo) => {
456
443
  memo.push(val);
457
444
  return memo;
@@ -491,7 +478,7 @@ program
491
478
  process.exit(EXIT_USAGE_ERROR);
492
479
  }
493
480
  try {
494
- Object.assign(env, loadEnvFile(envPath));
481
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
495
482
  }
496
483
  catch (err) {
497
484
  console.error(chalk.red(` ✗ ${err.message}`));
@@ -1284,7 +1271,7 @@ pluginCmd
1284
1271
  // ── voiden-runner mcp ─────────────────────────────────────────────────────────
1285
1272
  //
1286
1273
  // Enables the AI-agent loop for CLI-only users (no Voiden app installed):
1287
- // registers @voiden/mcp-server with Claude Code / Codex, and installs a
1274
+ // registers @voiden/mcp with Claude Code / Codex, and installs a
1288
1275
  // standalone skill teaching the run/verify/write-back workflow. The Voiden
1289
1276
  // app's own Settings toggle does the equivalent for desktop users, reusing
1290
1277
  // the same registration helpers from mcpInstall.ts.
@@ -1297,32 +1284,34 @@ function resolveMcpTargets(opts) {
1297
1284
  }
1298
1285
  const mcpCmd = program
1299
1286
  .command('mcp')
1300
- .description('Enable AI-agent integration — registers @voiden/mcp-server and installs a run/verify skill');
1287
+ .description('Enable AI-agent integration — registers this project\'s fixed-tools MCP server and installs a run/verify skill');
1301
1288
  mcpCmd
1302
1289
  .command('install')
1303
- .description('Register @voiden/mcp-server with Claude Code and/or Codex, and install a skill teaching the run/verify/write-back loop.\n\n' +
1290
+ .description('Register this project with Claude Code and/or Codex points .mcp.json / config.toml at ' +
1291
+ '`voiden-runner mcp serve` (the same 4 fixed tools, standalone, no other Voiden package ' +
1292
+ 'required), and installs a skill teaching the run/verify/write-back loop.\n\n' +
1304
1293
  ' Examples:\n' +
1305
1294
  ' voiden-runner mcp install # both Claude Code and Codex\n' +
1306
1295
  ' voiden-runner mcp install --claude # Claude Code only\n' +
1307
1296
  ' voiden-runner mcp install -p ./my-project # register against a specific project dir (default: cwd)\n' +
1308
- ' voiden-runner mcp install --local-server ./dist/index.js # before publishing: point at a local build instead of npx\n')
1297
+ ' voiden-runner mcp install --local-server ./dist/index.js # point at a local build instead of npx\n')
1309
1298
  .option('--claude', 'Install for Claude Code only')
1310
1299
  .option('--codex', 'Install for Codex only')
1311
1300
  .option('-p, --project <path>', 'Project directory to register the MCP server against', '.')
1312
- .option('--local-server <path>', 'Use `node <path>` instead of `npx -y @voiden/mcp-server` — for testing against a local build before it\'s published')
1301
+ .option('--local-server <path>', 'Use `node <path> mcp serve` instead of `npx -y @voiden/runner mcp serve` — for testing against a local build')
1313
1302
  .action((opts) => {
1314
1303
  const targets = resolveMcpTargets(opts);
1315
1304
  const serverCommand = opts.localServer
1316
- ? { command: 'node', args: [resolve(opts.localServer), resolve(opts.project)] }
1305
+ ? { command: 'node', args: [resolve(opts.localServer), 'mcp', 'serve', resolve(opts.project)] }
1317
1306
  : undefined;
1318
- const installed = installMcpIntegration(opts.project, targets, RUNNER_SKILL_MARKDOWN, serverCommand);
1307
+ const installed = installMcpIntegration(opts.project, targets, MCP_SKILL_MARKDOWN, serverCommand);
1319
1308
  if (installed.length === 0) {
1320
1309
  console.log(chalk.yellow(' Nothing to install.'));
1321
1310
  return;
1322
1311
  }
1323
1312
  console.log();
1324
1313
  for (const target of installed) {
1325
- console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, @voiden/mcp-server registered for ${resolve(opts.project)}`));
1314
+ console.log(chalk.green(` ✓ ${target === 'claude' ? 'Claude Code' : 'Codex'}`) + chalk.gray(` — skill installed, fixed-tools MCP server registered for ${resolve(opts.project)}`));
1326
1315
  }
1327
1316
  if (serverCommand) {
1328
1317
  console.log(chalk.gray(` Using local build: node ${serverCommand.args[0]}`));
@@ -1363,5 +1352,254 @@ mcpCmd
1363
1352
  console.log(` server registered: ${status.codex.serverRegistered ? chalk.green('yes') : chalk.gray('no')}`);
1364
1353
  console.log();
1365
1354
  });
1355
+ mcpCmd
1356
+ .command('serve [path]')
1357
+ .description('Serve this project as an MCP server — the same tools @voiden/mcp exposes ' +
1358
+ '(list/run/write plus declared /tool capabilities), over stdio (default) or HTTP.\n\n' +
1359
+ ' Examples:\n' +
1360
+ ' voiden-runner mcp serve # stdio, current directory\n' +
1361
+ ' voiden-runner mcp serve ./api # stdio, specific project\n' +
1362
+ ' voiden-runner mcp serve --http --port 3900 # HTTP on 127.0.0.1:3900\n' +
1363
+ ' voiden-runner mcp serve --check # dry run — print what would be served, no live server\n')
1364
+ .option('--http', 'Serve over streamable HTTP instead of stdio')
1365
+ .option('-p, --port <port>', 'HTTP port (only with --http)', '3000')
1366
+ .option('--host <host>', 'HTTP bind address (only with --http) — binding beyond 127.0.0.1 is a real exposure risk', '127.0.0.1')
1367
+ .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1368
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1369
+ .option('--check', 'Print what would be served and exit, without starting a live server')
1370
+ .action(async (path, opts) => {
1371
+ const projectRoot = resolve(path ?? '.');
1372
+ const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1373
+ if (opts.env) {
1374
+ const envPath = resolve(opts.env);
1375
+ if (!existsSync(envPath)) {
1376
+ console.error(chalk.red(`Env file not found: ${envPath}`));
1377
+ process.exit(EXIT_USAGE_ERROR);
1378
+ }
1379
+ try {
1380
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
1381
+ }
1382
+ catch (err) {
1383
+ console.error(chalk.red(` ✗ ${err.message}`));
1384
+ process.exit(EXIT_USAGE_ERROR);
1385
+ }
1386
+ }
1387
+ if (opts.check) {
1388
+ // Dry run — no live server. Same underlying decision function real
1389
+ // serving uses, so this can never disagree with what actually gets
1390
+ // registered.
1391
+ const activePlugins = await loadEnabledPlugins();
1392
+ const decisions = await planServedTools(projectRoot, env, activePlugins);
1393
+ console.log(`\n${decisions.length} /tool block(s) found in ${projectRoot}\n`);
1394
+ for (const d of decisions) {
1395
+ if (d.excluded) {
1396
+ console.log(` [EXCLUDED] ${d.tool.name}`);
1397
+ for (const reason of d.excludedReasons ?? [])
1398
+ console.log(` ${reason}`);
1399
+ continue;
1400
+ }
1401
+ const label = d.served ? (d.descriptionNote ? `SERVED (${d.status.state})` : 'SERVED') : 'WITHDRAWN';
1402
+ console.log(` [${label}] ${d.tool.name} — ${d.status.state}${d.status.note ? `: ${d.status.note}` : ''}`);
1403
+ }
1404
+ console.log(chalk.gray(` (plus the 4 fixed tools: list_void_files, list_requests, run_request, write_result)`));
1405
+ console.log();
1406
+ const anyFailing = decisions.some((d) => d.excluded || d.status?.state === 'failing');
1407
+ process.exit(anyFailing ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
1408
+ }
1409
+ // Verification (real network calls) runs exactly once here, regardless
1410
+ // of transport — never repeated per HTTP request below.
1411
+ const activePlugins = await loadEnabledPlugins();
1412
+ const decisions = await planServedTools(projectRoot, env, activePlugins);
1413
+ const commitSha = getCommitSha(projectRoot);
1414
+ const servedCount = decisions.filter((d) => d.served).length;
1415
+ // Shared across calls so {{process.xxx}} runtime variables chain the
1416
+ // same way they do for the stdio path and for @voiden/mcp.
1417
+ const runtimeVars = {};
1418
+ if (opts.http) {
1419
+ const port = Number(opts.port);
1420
+ const host = opts.host;
1421
+ // A fresh McpServer + transport per HTTP request — this is how the
1422
+ // SDK's own stateless example (examples/server/simpleStatelessStreamableHttp.js)
1423
+ // does it, not an arbitrary choice: reusing one transport across
1424
+ // requests returns 500s. Cheap: registration is just schema/handler
1425
+ // wiring against the already-computed `decisions`, no re-verification.
1426
+ const httpServer = createHttpServer(async (req, res) => {
1427
+ try {
1428
+ const requestServer = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1429
+ registerFixedTools(requestServer, projectRoot, runtimeVars, activePlugins);
1430
+ registerToolsFromDecisions(requestServer, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1431
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
1432
+ await requestServer.connect(transport);
1433
+ res.on('close', () => {
1434
+ transport.close();
1435
+ requestServer.close();
1436
+ });
1437
+ await transport.handleRequest(req, res);
1438
+ }
1439
+ catch (err) {
1440
+ console.error(chalk.red(` ✗ Error handling MCP request: ${err?.message ?? String(err)}`));
1441
+ if (!res.headersSent) {
1442
+ res.writeHead(500, { 'content-type': 'application/json' }).end(JSON.stringify({
1443
+ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null,
1444
+ }));
1445
+ }
1446
+ }
1447
+ });
1448
+ httpServer.listen(port, host, () => {
1449
+ console.error(chalk.green(` ✓ voiden-runner mcp serve — listening on http://${host}:${port}/mcp`));
1450
+ console.error(chalk.gray(` ${servedCount} tool(s) served (plus list_void_files, list_requests, run_request, write_result)`));
1451
+ if (host !== '127.0.0.1' && host !== 'localhost') {
1452
+ console.error(chalk.red(` ⚠ Bound to ${host} — reachable beyond this machine. Make sure that's intended.`));
1453
+ }
1454
+ });
1455
+ // No process.exit() — stays alive until Ctrl-C, same as the stdio path below.
1456
+ }
1457
+ else {
1458
+ // stdio: one persistent server for the process lifetime — stdout is
1459
+ // reserved for the JSON-RPC stream, so nothing gets printed there.
1460
+ // Startup info goes to stderr only, same discipline @voiden/mcp's
1461
+ // own entrypoint already follows (it prints nothing).
1462
+ const server = new McpServer({ name: 'voiden-runner', version: '1.0.0' });
1463
+ registerFixedTools(server, projectRoot, runtimeVars, activePlugins);
1464
+ registerToolsFromDecisions(server, decisions, env, runtimeVars, activePlugins, commitSha, projectRoot);
1465
+ await server.connect(new StdioServerTransport());
1466
+ }
1467
+ });
1468
+ // ── voiden-runner tool ────────────────────────────────────────────────────────
1469
+ //
1470
+ // Discovery + verification for /tool blocks (voiden-mcp-tool plugin) — the
1471
+ // standalone CLI surface for the same discoverTools/verifyTools functions
1472
+ // @voiden/mcp will use for live agent-serving. No scheduling here:
1473
+ // `cadence` on a verify entry is a tag `--cadence` filters by, not something
1474
+ // this command enforces timing for — that's a human/CI decision, same as
1475
+ // deciding when to run `voiden-runner run` at all.
1476
+ const toolCmd = program
1477
+ .command('tool')
1478
+ .description('Discover and verify /tool blocks — capabilities declared for AI agents');
1479
+ toolCmd
1480
+ .command('list [paths...]')
1481
+ .description('List every /tool block found under the given path(s) (default: current directory) — does not execute anything')
1482
+ .option('--json', 'Output as JSON')
1483
+ .action(async (paths, opts) => {
1484
+ const targets = paths.length > 0 ? paths : ['.'];
1485
+ const allTools = [];
1486
+ const activePlugins = await loadEnabledPlugins();
1487
+ for (const p of targets) {
1488
+ allTools.push(...await discoverTools(resolve(p), { activePlugins }));
1489
+ }
1490
+ if (opts.json) {
1491
+ console.log(JSON.stringify(allTools, null, 2));
1492
+ return;
1493
+ }
1494
+ if (allTools.length === 0) {
1495
+ console.log(chalk.yellow(' No /tool blocks found.'));
1496
+ return;
1497
+ }
1498
+ console.log();
1499
+ for (const tool of allTools) {
1500
+ console.log(chalk.bold(` ${tool.name}`) + chalk.gray(` — ${relative(process.cwd(), tool.filePath)}${tool.sectionLabel ? ` [${tool.sectionLabel}]` : ''}`));
1501
+ if (tool.description)
1502
+ console.log(chalk.gray(` ${tool.description}`));
1503
+ // onFailure is per verify-entry now, not one tool-wide setting —
1504
+ // summarize instead of showing a single (no-longer-existing) value.
1505
+ const onFailureValues = new Set(tool.verifies.map((v) => v.onFailure || 'withdraw'));
1506
+ const onFailureSummary = tool.verifies.length === 0 ? '—' : onFailureValues.size === 1 ? [...onFailureValues][0] : 'mixed';
1507
+ console.log(chalk.gray(` params: ${tool.params.length} verifies: ${tool.verifies.length} on-failure: ${onFailureSummary}`));
1508
+ console.log();
1509
+ }
1510
+ });
1511
+ toolCmd
1512
+ .command('verify [paths...]')
1513
+ .description("Run each /tool block's verification requests and report verified / unverified / failing.\n\n" +
1514
+ ' Examples:\n' +
1515
+ ' voiden-runner tool verify\n' +
1516
+ ' voiden-runner tool verify ./api/ --cadence nightly\n' +
1517
+ ' voiden-runner tool verify --json --write\n')
1518
+ .option('--cadence <tag>', 'Only run verification requests tagged with this cadence — omit to run every entry regardless of tag')
1519
+ .option('--json', 'Output as JSON (suppresses normal output — useful for CI)')
1520
+ .option('--write', 'Write the computed status back into each /tool block. Off by default — verification always recomputes fresh and never trusts a stale write-back')
1521
+ .option('-e, --env <path>', 'Path to .env or .yaml file for variable substitution')
1522
+ .option('--environment <name>', 'Scope --env to one named environment in a multi-environment YAML file (e.g. "dev") instead of merging every environment in it together')
1523
+ .action(async (paths, opts) => {
1524
+ const targets = paths.length > 0 ? paths : ['.'];
1525
+ const env = Object.fromEntries(Object.entries(process.env).filter(([, v]) => v !== undefined));
1526
+ if (opts.env) {
1527
+ const envPath = resolve(opts.env);
1528
+ if (!existsSync(envPath)) {
1529
+ console.error(chalk.red(`Env file not found: ${envPath}`));
1530
+ process.exit(EXIT_USAGE_ERROR);
1531
+ }
1532
+ try {
1533
+ Object.assign(env, loadEnvFile(envPath, opts.environment));
1534
+ }
1535
+ catch (err) {
1536
+ console.error(chalk.red(` ✗ ${err.message}`));
1537
+ process.exit(EXIT_USAGE_ERROR);
1538
+ }
1539
+ }
1540
+ const activePlugins = await loadEnabledPlugins();
1541
+ const allTools = [];
1542
+ for (const p of targets) {
1543
+ allTools.push(...await discoverTools(resolve(p), { activePlugins }));
1544
+ }
1545
+ if (allTools.length === 0) {
1546
+ if (opts.json)
1547
+ console.log(JSON.stringify({ tools: [], issues: [] }, null, 2));
1548
+ else
1549
+ console.log(chalk.yellow(' No /tool blocks found.'));
1550
+ process.exit(EXIT_SUCCESS);
1551
+ }
1552
+ const { validTools, issues } = await validateTools(resolve(targets[0]), allTools);
1553
+ const statuses = await verifyTools(validTools, { cadence: opts.cadence, env, activePlugins, projectRoot: resolve(targets[0]) });
1554
+ if (opts.write) {
1555
+ for (const status of statuses) {
1556
+ try {
1557
+ upsertToolStatus(status.tool.filePath, status.tool.toolBlockUid, { state: status.state, note: status.note });
1558
+ }
1559
+ catch (err) {
1560
+ console.error(chalk.red(` ✗ Failed to write status for "${status.tool.name}": ${err?.message ?? String(err)}`));
1561
+ }
1562
+ }
1563
+ }
1564
+ const anyFailing = statuses.some(s => s.state === 'failing') || issues.length > 0;
1565
+ if (opts.json) {
1566
+ console.log(JSON.stringify({ tools: statuses, issues }, null, 2));
1567
+ }
1568
+ else {
1569
+ console.log();
1570
+ // Structurally excluded tools (§1.6) — reported distinctly from a
1571
+ // verification failure: this is "the contract itself doesn't add up,"
1572
+ // not "the contract's proof failed." Neither ran nor counted below.
1573
+ const excludedNames = [...new Set(issues.map(i => i.tool.name))];
1574
+ for (const name of excludedNames) {
1575
+ console.log(` ${chalk.red('✗ excluded')} ${chalk.bold(name)}`);
1576
+ for (const issue of issues.filter(i => i.tool.name === name)) {
1577
+ console.log(chalk.gray(` [${issue.check}] ${issue.message}`));
1578
+ }
1579
+ console.log();
1580
+ }
1581
+ for (const status of statuses) {
1582
+ const icon = status.state === 'verified' ? chalk.green('✓ verified ') :
1583
+ status.state === 'failing' ? chalk.red('✗ failing ') :
1584
+ chalk.yellow('○ unverified');
1585
+ console.log(` ${icon} ${chalk.bold(status.tool.name)}` + chalk.gray(` — ${relative(process.cwd(), status.tool.filePath)}${status.tool.sectionLabel ? ` [${status.tool.sectionLabel}]` : ''}`));
1586
+ if (status.note)
1587
+ console.log(chalk.gray(` ${status.note}`));
1588
+ for (const r of status.results) {
1589
+ const rIcon = r.passed ? chalk.green('✓') : chalk.red('✗');
1590
+ console.log(` ${rIcon} ${r.entry.role}: ${r.entry.sectionLabel}` + (r.reason ? chalk.gray(` (${r.reason})`) : ''));
1591
+ }
1592
+ console.log();
1593
+ }
1594
+ const verified = statuses.filter(s => s.state === 'verified').length;
1595
+ const unverified = statuses.filter(s => s.state === 'unverified').length;
1596
+ const failing = statuses.filter(s => s.state === 'failing').length;
1597
+ console.log(chalk.bold(` ${statuses.length} tool(s)`) + chalk.gray(` — ${chalk.green(verified + ' verified')}, ${chalk.yellow(unverified + ' unverified')}, ${chalk.red(failing + ' failing')}`) + (excludedNames.length > 0 ? chalk.red(`, ${excludedNames.length} excluded`) : ''));
1598
+ if (opts.write)
1599
+ console.log(chalk.gray(' Status written back into each /tool block.'));
1600
+ console.log();
1601
+ }
1602
+ process.exit(anyFailing ? EXIT_RUN_FAILURE : EXIT_SUCCESS);
1603
+ });
1366
1604
  program.parse();
1367
1605
  //# sourceMappingURL=index.js.map