@fede0089/skill-eval 3.1.0 → 3.2.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
@@ -89,7 +89,8 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
89
89
  | `--trials <number>` | no | `3` | Trials per task (for pass@k) |
90
90
  | `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
91
91
  | `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
92
- | `--compare-ref [refs...]` | no | | Git references to compare against |
92
+ | `--eval-file <name>` | no | all | Run only the evals from this file in `evals/` (e.g. `edge-cases.json`) |
93
+ | `--compare-ref [refs...]` | no | — | Git references to compare against (variadic — put `[agent]` before it, not after) |
93
94
  | `--compare-baseline` | no | `false` | Also run the no-skill baseline alongside the skill |
94
95
  | `-v, --debug` | no | `false` | Enable verbose debug logging |
95
96
  | `[agent]` | no | `gemini-cli` | Agent backend to use |
@@ -118,6 +119,8 @@ my-skill/
118
119
 
119
120
  All `.json` files in `evals/` are loaded and merged into a single suite — you can split them by feature or regression category.
120
121
 
122
+ Use `--eval-file <name>` to run just one of them while iterating (the `.json` extension is optional), and combine it with `--eval-id <id>` to narrow down to a single eval inside that file.
123
+
121
124
  **Trigger eval** — `id` must be a unique integer across all eval files:
122
125
  ```json
123
126
  {
@@ -250,6 +253,9 @@ npm run test:trigger -- codex # run trigger evals with Codex
250
253
  npm run test:functional -- codex # run functional evals with Codex
251
254
  npm run test:trigger -- claude-code # run trigger evals with Claude Code
252
255
  npm run test:functional -- claude-code # run functional evals with Claude Code
256
+
257
+ npm run test:trigger -- --eval-file negative-triggers.json # only the negative-trigger evals
258
+ npm run test:trigger -- --eval-file edge-cases --eval-id 3 # a single eval inside one file
253
259
  ```
254
260
 
255
261
  ## Extending
@@ -14,10 +14,10 @@ import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.
14
14
  import { JsonReporter } from '../reporters/index.js';
15
15
  import chalk from 'chalk';
16
16
  import { git } from '../utils/git.js';
17
- export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], compareBaseline = false) {
17
+ export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], compareBaseline = false, evalFile) {
18
18
  if (!injectedSuite)
19
19
  preflight(agent, workspace, skillPath);
20
- const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
20
+ const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath, evalFile);
21
21
  // Negative evals (should_trigger: false) only make sense for the trigger command:
22
22
  // this pass instructs the agent that it MUST use the skill, which contradicts them.
23
23
  const triggerOnly = suite.tasks.filter(t => t.should_trigger === false);
@@ -26,6 +26,9 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
26
26
  throw new ConfigError(`Eval #${evalId} is trigger-only (should_trigger: false) and cannot run under 'functional'.`);
27
27
  }
28
28
  suite.tasks = suite.tasks.filter(t => t.should_trigger !== false);
29
+ if (suite.tasks.length === 0) {
30
+ throw new ConfigError(`No evals left to run: all ${triggerOnly.length} eval(s) in scope are trigger-only (should_trigger: false).`);
31
+ }
29
32
  Logger.write(chalk.dim(` Skipping ${triggerOnly.length} trigger-only eval(s)\n`));
30
33
  }
31
34
  if (evalId !== undefined) {
@@ -94,7 +97,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
94
97
  ]
95
98
  : skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
96
99
  try {
97
- renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
100
+ renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId, evalFile });
98
101
  Logger.write(`──────────────────────────────────────────────────\n`);
99
102
  for (let i = 0; i < tasks.length; i++) {
100
103
  const task = tasks[i];
@@ -13,10 +13,10 @@ import { renderTriggerTable, renderRunHeader } from '../utils/table-renderer.js'
13
13
  import { JsonReporter } from '../reporters/index.js';
14
14
  import chalk from 'chalk';
15
15
  import { git } from '../utils/git.js';
16
- export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = []) {
16
+ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], evalFile) {
17
17
  if (!injectedSuite)
18
18
  preflight(agent, workspace, skillPath);
19
- const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
19
+ const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath, evalFile);
20
20
  if (evalId !== undefined) {
21
21
  suite.tasks = suite.tasks.filter(t => t.id === evalId);
22
22
  if (suite.tasks.length === 0) {
@@ -73,7 +73,7 @@ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4,
73
73
  const skillVersions = Array.from(variantRunners.keys());
74
74
  const subtaskLabels = skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
75
75
  try {
76
- renderRunHeader({ command: 'trigger', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
76
+ renderRunHeader({ command: 'trigger', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId, evalFile });
77
77
  Logger.write(`--- Trigger Pass ---\n`);
78
78
  Logger.write(`──────────────────────────────────────────────────\n`);
79
79
  for (let i = 0; i < tasks.length; i++) {
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import { triggerCommand } from './commands/trigger.js';
4
4
  import { functionalCommand } from './commands/functional.js';
5
5
  import { Logger } from './utils/logger.js';
6
- import { AppError } from './core/errors.js';
6
+ import { AppError, ConfigError } from './core/errors.js';
7
7
  import { HtmlReporter } from './reporters/index.js';
8
8
  import { DEFAULT_AGENT } from './runners/registry.js';
9
9
  import * as path from 'path';
@@ -25,6 +25,18 @@ const errorHandler = (err) => {
25
25
  }
26
26
  process.exit(1);
27
27
  };
28
+ /**
29
+ * A variadic option used without values (`--compare-ref` on its own) is parsed as
30
+ * `true` by commander; reject it instead of iterating over a boolean.
31
+ */
32
+ const parseCompareRefs = (value) => {
33
+ if (value === undefined)
34
+ return [];
35
+ if (Array.isArray(value))
36
+ return value;
37
+ errorHandler(new ConfigError('--compare-ref requires at least one git reference (e.g. --compare-ref HEAD~1).'));
38
+ return [];
39
+ };
28
40
  program
29
41
  .name('skill-eval')
30
42
  .description('CLI to evaluate agent skills triggering and functionality')
@@ -42,6 +54,7 @@ program
42
54
  .option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
43
55
  .option('--timeout <seconds>', 'Agent timeout in seconds')
44
56
  .option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
57
+ .option('--eval-file <name>', 'Run only the evals from this file in evals/ (e.g. edge-cases.json)')
45
58
  .option('--compare-ref [refs...]', 'Compare against historical git references')
46
59
  .action((agent, options) => {
47
60
  const workspace = path.resolve(options.workspace);
@@ -50,8 +63,8 @@ program
50
63
  const numTrials = parseInt(options.trials, 10);
51
64
  const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
52
65
  const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
53
- const compareRefs = options.compareRef || [];
54
- triggerCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs).catch(errorHandler);
66
+ const compareRefs = parseCompareRefs(options.compareRef);
67
+ triggerCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, options.evalFile).catch(errorHandler);
55
68
  });
56
69
  program
57
70
  .command('functional [agent]')
@@ -62,6 +75,7 @@ program
62
75
  .option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
63
76
  .option('--timeout <seconds>', 'Agent timeout in seconds')
64
77
  .option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
78
+ .option('--eval-file <name>', 'Run only the evals from this file in evals/ (e.g. edge-cases.json)')
65
79
  .option('--compare-ref [refs...]', 'Compare against historical git references')
66
80
  .option('--compare-baseline', 'Also run the no-skill baseline alongside the skill')
67
81
  .action((agent, options) => {
@@ -71,9 +85,9 @@ program
71
85
  const numTrials = parseInt(options.trials, 10);
72
86
  const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
73
87
  const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
74
- const compareRefs = options.compareRef || [];
88
+ const compareRefs = parseCompareRefs(options.compareRef);
75
89
  const compareBaseline = !!options.compareBaseline;
76
- functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, compareBaseline).catch(errorHandler);
90
+ functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, compareBaseline, options.evalFile).catch(errorHandler);
77
91
  });
78
92
  const isMain = process.argv[1] && (() => {
79
93
  try {
@@ -1,20 +1,36 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { ConfigError } from '../core/errors.js';
4
+ /**
5
+ * Normalizes an --eval-file value to the bare file name used inside the evals directory.
6
+ * Accepts 'edge-cases', 'edge-cases.json' and 'path/to/evals/edge-cases.json' alike.
7
+ */
8
+ function normalizeEvalFileName(evalFile) {
9
+ const base = path.basename(evalFile.trim());
10
+ return base.endsWith('.json') ? base : `${base}.json`;
11
+ }
4
12
  /**
5
13
  * Loads and merges all JSON evaluation files from a skill's evals directory.
6
14
  * Aligned with Anthropic's recommendation to split evals by capability/regression.
7
15
  * Supports legacy 'tasks' and 'assertions' internally while maintaining 'evals' and 'expectations' in files.
16
+ * When 'evalFile' is given, only that file is loaded instead of the whole suite.
8
17
  */
9
- export function loadEvalSuite(skillPath) {
18
+ export function loadEvalSuite(skillPath, evalFile) {
10
19
  const evalsDir = path.resolve(skillPath, 'evals');
11
20
  if (!fs.existsSync(evalsDir)) {
12
21
  throw new ConfigError(`Could not find evals directory at ${evalsDir}`);
13
22
  }
14
- const files = fs.readdirSync(evalsDir).filter(file => file.endsWith('.json'));
23
+ let files = fs.readdirSync(evalsDir).filter(file => file.endsWith('.json'));
15
24
  if (files.length === 0) {
16
25
  throw new ConfigError(`No JSON evaluation files found in ${evalsDir}`);
17
26
  }
27
+ if (evalFile !== undefined) {
28
+ const wanted = normalizeEvalFileName(evalFile);
29
+ if (!files.includes(wanted)) {
30
+ throw new ConfigError(`Eval file '${wanted}' not found in ${evalsDir}. Available: ${[...files].sort().join(', ')}.`);
31
+ }
32
+ files = [wanted];
33
+ }
18
34
  let mergedSkillName = '';
19
35
  const mergedTasks = [];
20
36
  for (const file of files) {
@@ -59,7 +75,8 @@ export function loadEvalSuite(skillPath) {
59
75
  mergedTasks.push(...mappedTasks);
60
76
  }
61
77
  if (mergedTasks.length === 0) {
62
- throw new ConfigError(`No evaluations found in any of the JSON files in ${evalsDir}`);
78
+ const scope = evalFile !== undefined ? `${files[0]} in ${evalsDir}` : `any of the JSON files in ${evalsDir}`;
79
+ throw new ConfigError(`No evaluations found in ${scope}`);
63
80
  }
64
81
  return {
65
82
  skill_name: mergedSkillName,
@@ -98,7 +98,7 @@ function boxLabel(key, value) {
98
98
  * Always shown regardless of debug mode.
99
99
  */
100
100
  export function renderRunHeader(config) {
101
- const { command, skillName, agent, workspace, tasks, trials, maxAgents, timeoutMs, runDir, evalId } = config;
101
+ const { command, skillName, agent, workspace, tasks, trials, maxAgents, timeoutMs, runDir, evalId, evalFile } = config;
102
102
  let timeoutStr = 'None';
103
103
  if (timeoutMs && timeoutMs > 0) {
104
104
  const timeoutSec = timeoutMs / 1000;
@@ -111,7 +111,12 @@ export function renderRunHeader(config) {
111
111
  const dashes = '─'.repeat(BOX_INNER - titleLabel.length);
112
112
  const top = chalk.gray('┌─ ') + chalk.bold(titleLabel) + ' ' + chalk.gray(dashes + '┐');
113
113
  const bottom = chalk.gray('└' + '─'.repeat(BOX_INNER + 2) + '┘');
114
- const commandPart = evalId !== undefined ? `${command} · eval #${evalId}` : command;
114
+ const filterParts = [];
115
+ if (evalFile !== undefined)
116
+ filterParts.push(evalFile);
117
+ if (evalId !== undefined)
118
+ filterParts.push(`eval #${evalId}`);
119
+ const commandPart = [command, ...filterParts].join(' · ');
115
120
  const title = chalk.bold.cyan(skillName) + chalk.gray(` · ${commandPart}`);
116
121
  const runLine = `${tasks} task${tasks !== 1 ? 's' : ''} · ${trials} trial${trials !== 1 ? 's' : ''} · agents ${maxAgents}`;
117
122
  process.stdout.write('\n');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fede0089/skill-eval",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "CLI to evaluate agent skills triggering and functionality",
5
5
  "main": "dist/index.js",
6
6
  "bin": {