@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 +7 -1
- package/dist/commands/functional.js +6 -3
- package/dist/commands/trigger.js +3 -3
- package/dist/index.js +19 -5
- package/dist/utils/eval-loader.js +20 -3
- package/dist/utils/table-renderer.js +7 -2
- package/package.json +1 -1
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
|
-
| `--
|
|
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];
|
package/dist/commands/trigger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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');
|