@fede0089/skill-eval 1.0.0 → 1.1.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 +33 -7
- package/dist/commands/functional.js +130 -94
- package/dist/commands/trigger.js +114 -58
- package/dist/core/environment.js +18 -6
- package/dist/index.js +6 -2
- package/dist/reporters/html-reporter.js +71 -103
- package/dist/utils/exec.js +9 -2
- package/dist/utils/git.js +30 -0
- package/dist/utils/table-renderer.js +75 -72
- package/package.json +4 -1
- package/dist/commands/rate.js +0 -20
- package/dist/commands/show.js +0 -43
- package/dist/commands/view.js +0 -30
- package/dist/core/config.js +0 -58
- package/dist/core/reporters/html-reporter.js +0 -354
- package/dist/core/reporters/index.js +0 -9
- package/dist/core/reporters/json-reporter.js +0 -7
- package/dist/core/reporters/reporter.js +0 -1
- package/dist/core/runner.js +0 -75
- package/dist/core/runners/factory.js +0 -18
- package/dist/core/runners/gemini-cli.runner.js +0 -138
- package/dist/core/runners/index.js +0 -3
- package/dist/core/runners/runner.interface.js +0 -1
package/README.md
CHANGED
|
@@ -33,20 +33,33 @@ For each eval prompt, skill-eval spins up parallel agent processes — some with
|
|
|
33
33
|
|
|
34
34
|
> The `trigger` command only runs with-skill trials and checks whether the skill dispatch tool was actually invoked — no judge or baseline needed.
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini`) installed and on `$PATH`.
|
|
39
|
+
|
|
40
|
+
### Run without installing
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npx @fede0089/skill-eval --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Install globally
|
|
37
47
|
|
|
38
48
|
```sh
|
|
39
|
-
|
|
49
|
+
npm install -g @fede0089/skill-eval
|
|
50
|
+
skill-eval --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### From source
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
git clone https://github.com/fede0089/skill-eval.git
|
|
40
57
|
cd skill-eval
|
|
41
58
|
npm install
|
|
42
59
|
npm run build
|
|
43
|
-
npm link
|
|
60
|
+
npm link # makes `skill-eval` available globally
|
|
44
61
|
```
|
|
45
62
|
|
|
46
|
-
This makes `skill-eval` available globally in your terminal.
|
|
47
|
-
|
|
48
|
-
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini`) installed and on `$PATH`.
|
|
49
|
-
|
|
50
63
|
## Commands
|
|
51
64
|
|
|
52
65
|
```sh
|
|
@@ -67,6 +80,7 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
67
80
|
| `--trials <number>` | no | `3` | Trials per task (for pass@k) |
|
|
68
81
|
| `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
|
|
69
82
|
| `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
|
|
83
|
+
| `--compare-ref [refs...]` | no | — | Git references to compare against |
|
|
70
84
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
71
85
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
72
86
|
|
|
@@ -168,3 +182,15 @@ The factory, preflight check, and CLI all pick it up automatically.
|
|
|
168
182
|
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
169
183
|
2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
|
|
170
184
|
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
185
|
+
er, binary: '<cli-binary-name>' },
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The factory, preflight check, and CLI all pick it up automatically.
|
|
189
|
+
|
|
190
|
+
> Implement `applyRunnerConfig(evalConfigBaseDir, worktreePath)` to copy `evalConfigBaseDir/<your-agent>/` into the appropriate config directory in the worktree (e.g. `.claude/` for a Claude runner). No-op silently if the directory doesn't exist.
|
|
191
|
+
|
|
192
|
+
### Adding a new report format
|
|
193
|
+
|
|
194
|
+
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
195
|
+
2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
|
|
196
|
+
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
@@ -11,7 +11,9 @@ import { preflight } from '../core/preflight.js';
|
|
|
11
11
|
import { withRetry } from '../core/trial-utils.js';
|
|
12
12
|
import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.js';
|
|
13
13
|
import { JsonReporter } from '../reporters/index.js';
|
|
14
|
-
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { git } from '../utils/git.js';
|
|
16
|
+
export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = []) {
|
|
15
17
|
if (!injectedSuite)
|
|
16
18
|
preflight(agent, workspace, skillPath);
|
|
17
19
|
const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
|
|
@@ -35,39 +37,45 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
35
37
|
const timestamp = startTime.toISOString().replace(/[:.]/g, '-');
|
|
36
38
|
const runDir = path.resolve(workspace, '.project-skill-evals', 'runs', timestamp);
|
|
37
39
|
fs.mkdirSync(runDir, { recursive: true });
|
|
40
|
+
const refPathBase = path.resolve(workspace, '.project-skill-evals', 'skill-refs');
|
|
41
|
+
const variantRunners = new Map();
|
|
42
|
+
// 1. Local Runner
|
|
43
|
+
variantRunners.set('local', new EvalRunner({
|
|
44
|
+
agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: false, debug, timeoutMs
|
|
45
|
+
}));
|
|
46
|
+
// 2. Historical Runners
|
|
47
|
+
for (const ref of compareRefs) {
|
|
48
|
+
const refDir = path.join(refPathBase, ref);
|
|
49
|
+
Logger.write(` Extracting ref '${ref}'... `);
|
|
50
|
+
git.extractSkillRef(skillPath, ref, refDir);
|
|
51
|
+
Logger.write(chalk.green('Done\n'));
|
|
52
|
+
variantRunners.set(`ref:${ref}`, new EvalRunner({
|
|
53
|
+
agent,
|
|
54
|
+
workspace: refDir, // Run inside extracted repo
|
|
55
|
+
skillPath: path.join(refDir, path.relative(workspace, skillPath)), // Same relative path
|
|
56
|
+
skillName: skill_name,
|
|
57
|
+
runDir,
|
|
58
|
+
isBaseline: false,
|
|
59
|
+
debug,
|
|
60
|
+
timeoutMs
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
// 3. Baseline Runner
|
|
64
|
+
const withoutSkillRunner = new EvalRunner({
|
|
65
|
+
agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: true, debug, timeoutMs
|
|
66
|
+
});
|
|
38
67
|
const taskResults = [];
|
|
39
68
|
let withSkillTasksAllPassedCount = 0;
|
|
40
69
|
let baselineTasksAllPassedCount = 0;
|
|
41
|
-
// Per-task trial storage, indexed by task.id
|
|
42
|
-
const withoutSkillTrialsByTask = new Map();
|
|
43
|
-
const withoutSkillRunner = new EvalRunner({
|
|
44
|
-
agent,
|
|
45
|
-
workspace,
|
|
46
|
-
skillPath,
|
|
47
|
-
skillName: skill_name,
|
|
48
|
-
runDir,
|
|
49
|
-
isBaseline: true,
|
|
50
|
-
debug,
|
|
51
|
-
timeoutMs
|
|
52
|
-
});
|
|
53
|
-
const withSkillRunner = new EvalRunner({
|
|
54
|
-
agent,
|
|
55
|
-
workspace,
|
|
56
|
-
skillPath,
|
|
57
|
-
skillName: skill_name,
|
|
58
|
-
runDir,
|
|
59
|
-
isBaseline: false,
|
|
60
|
-
debug,
|
|
61
|
-
timeoutMs
|
|
62
|
-
});
|
|
63
70
|
const pool = new AgentPool(maxAgents);
|
|
64
71
|
const ui = new ListrEvalUI();
|
|
65
72
|
// Barrier chain: each prompt waits until the previous prompt's trials have all acquired slots.
|
|
66
73
|
let barrier = Promise.resolve();
|
|
67
|
-
// Subtask labels
|
|
74
|
+
// Subtask labels
|
|
75
|
+
const skillVersions = Array.from(variantRunners.keys());
|
|
68
76
|
const subtaskLabels = [
|
|
69
77
|
...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
|
|
70
|
-
...Array.from({ length: numTrials }, (_, i) =>
|
|
78
|
+
...skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`))
|
|
71
79
|
];
|
|
72
80
|
try {
|
|
73
81
|
renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
|
|
@@ -85,7 +93,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
85
93
|
subtaskLabels,
|
|
86
94
|
task: async (uiCtx, multi) => {
|
|
87
95
|
await thisBarrier;
|
|
88
|
-
//
|
|
96
|
+
// 1. Without Skill trials
|
|
89
97
|
const baselineTrialPromises = [];
|
|
90
98
|
for (let idx = 0; idx < numTrials; idx++) {
|
|
91
99
|
const trialId = idx + 1;
|
|
@@ -112,60 +120,74 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
112
120
|
}).finally(release);
|
|
113
121
|
baselineTrialPromises.push(p);
|
|
114
122
|
}
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const reason =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
multi
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
123
|
+
// 2. Skill variants trials
|
|
124
|
+
const variantTrialsPromises = {};
|
|
125
|
+
let currentSubtaskIdx = numTrials;
|
|
126
|
+
for (const [version, runner] of variantRunners.entries()) {
|
|
127
|
+
variantTrialsPromises[version] = [];
|
|
128
|
+
for (let idx = 0; idx < numTrials; idx++) {
|
|
129
|
+
currentSubtaskIdx++;
|
|
130
|
+
const subtaskId = currentSubtaskIdx;
|
|
131
|
+
const runnerTrialId = idx + 1;
|
|
132
|
+
const trialCtx = multi?.getTrialCtx(subtaskId) ?? uiCtx;
|
|
133
|
+
const release = await pool.acquire();
|
|
134
|
+
const p = withRetry((attempt) => runner.runFunctionalTask(task, i, runnerTrialId, trialCtx, attempt)
|
|
135
|
+
.catch((error) => ({
|
|
136
|
+
id: runnerTrialId,
|
|
137
|
+
transcript: { error: error instanceof Error ? error.message : String(error) },
|
|
138
|
+
assertionResults: [{ assertion: `${version} Execution`, passed: false, reason: String(error) }],
|
|
139
|
+
trialPassed: false,
|
|
140
|
+
isError: true
|
|
141
|
+
})), 2, 1000, (nextAttempt, lastTrial) => {
|
|
142
|
+
const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
|
|
143
|
+
trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
|
|
144
|
+
}).then(trial => {
|
|
145
|
+
if (multi) {
|
|
146
|
+
const reason = trial.assertionResults.find(r => !r.passed)?.reason;
|
|
147
|
+
const passedCount = trial.assertionResults.filter(r => r.passed).length;
|
|
148
|
+
const totalCount = trial.assertionResults.length;
|
|
149
|
+
multi.markTrialComplete(subtaskId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
|
|
150
|
+
}
|
|
151
|
+
return trial;
|
|
152
|
+
}).finally(release);
|
|
153
|
+
variantTrialsPromises[version].push(p);
|
|
154
|
+
}
|
|
142
155
|
}
|
|
143
|
-
// All
|
|
156
|
+
// All slots for this prompt have been acquired — signal the next prompt.
|
|
144
157
|
resolveBarrier();
|
|
145
|
-
const [woTrials,
|
|
158
|
+
const [woTrials, ...versionsTrials] = await Promise.all([
|
|
146
159
|
Promise.all(baselineTrialPromises),
|
|
147
|
-
Promise.all(
|
|
160
|
+
...Object.values(variantTrialsPromises).map(ps => Promise.all(ps))
|
|
148
161
|
]);
|
|
149
|
-
withoutSkillTrialsByTask.set(task.id, woTrials);
|
|
150
162
|
const woPassedCount = woTrials.filter(t => t.trialPassed).length;
|
|
151
163
|
if (woPassedCount === woTrials.length)
|
|
152
164
|
baselineTasksAllPassedCount++;
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
165
|
+
const taskSkillTrials = {};
|
|
166
|
+
const variantNames = Object.keys(variantTrialsPromises);
|
|
167
|
+
let localAllPassed = true;
|
|
168
|
+
for (let vIdx = 0; vIdx < variantNames.length; vIdx++) {
|
|
169
|
+
const vName = variantNames[vIdx];
|
|
170
|
+
const vTrials = versionsTrials[vIdx];
|
|
171
|
+
taskSkillTrials[vName] = vTrials.map(t => ({ ...t, transcript: undefined }));
|
|
172
|
+
if (vName === 'local') {
|
|
173
|
+
const passedCount = vTrials.filter(t => t.trialPassed).length;
|
|
174
|
+
if (passedCount === vTrials.length) {
|
|
175
|
+
withSkillTasksAllPassedCount++;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
localAllPassed = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
156
182
|
const taskResult = {
|
|
157
183
|
taskId: task.id,
|
|
158
184
|
prompt: task.prompt,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
withoutSkillTrials: withoutSkillTrials.map(t => ({ ...t, transcript: undefined }))
|
|
185
|
+
baselineTrials: woTrials.map(t => ({ ...t, transcript: undefined })),
|
|
186
|
+
skillTrials: taskSkillTrials
|
|
162
187
|
};
|
|
163
188
|
taskResults.push(taskResult);
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
else if (!multi) {
|
|
168
|
-
const failureReason = wiTrials.find(t => !t.trialPassed)?.assertionResults.find(r => !r.passed)?.reason || 'With Skill failed';
|
|
189
|
+
if (!localAllPassed && !multi) {
|
|
190
|
+
const failureReason = taskSkillTrials['local'].find(t => !t.trialPassed)?.assertionResults.find(r => !r.passed)?.reason || 'With Skill failed';
|
|
169
191
|
throw new Error(failureReason);
|
|
170
192
|
}
|
|
171
193
|
}
|
|
@@ -173,46 +195,60 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
|
|
|
173
195
|
}
|
|
174
196
|
await ui.run(tasks.length);
|
|
175
197
|
// ==== REPORTING ====
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
198
|
+
const allSkillVersions = taskResults.length > 0 ? Object.keys(taskResults[0].skillTrials) : ['local'];
|
|
199
|
+
const allVersions = ['baseline', ...allSkillVersions];
|
|
200
|
+
const scores = {};
|
|
201
|
+
const passAtK = {};
|
|
202
|
+
const assertionPassRate = {};
|
|
203
|
+
const tokenStats = {};
|
|
204
|
+
const durationStats = {};
|
|
205
|
+
// 1. Baseline Metrics
|
|
206
|
+
const { passAtK: woPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.baselineTrials);
|
|
207
|
+
const woAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.baselineTrials);
|
|
208
|
+
const woPercentage = Math.round(woAssertionPassRate * 100);
|
|
209
|
+
scores['baseline'] = `${woPercentage}%`;
|
|
210
|
+
passAtK['baseline'] = Math.round(woPassAtK * 1000) / 1000;
|
|
211
|
+
assertionPassRate['baseline'] = Math.round(woAssertionPassRate * 1000) / 1000;
|
|
212
|
+
const woTokens = aggregateTokenStats(taskResults.flatMap(r => r.baselineTrials));
|
|
213
|
+
if (woTokens)
|
|
214
|
+
tokenStats['baseline'] = woTokens;
|
|
215
|
+
const woDuration = aggregateDurationStats(taskResults.flatMap(r => r.baselineTrials));
|
|
216
|
+
if (woDuration)
|
|
217
|
+
durationStats['baseline'] = woDuration;
|
|
218
|
+
// 2. Skill Variants Metrics
|
|
219
|
+
for (const version of allSkillVersions) {
|
|
220
|
+
const { passAtK: wiPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.skillTrials[version] || []);
|
|
221
|
+
const wiAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.skillTrials[version] || []);
|
|
222
|
+
const wiPercentage = Math.round(wiAssertionPassRate * 100);
|
|
223
|
+
scores[version] = `${wiPercentage}%`;
|
|
224
|
+
passAtK[version] = Math.round(wiPassAtK * 1000) / 1000;
|
|
225
|
+
assertionPassRate[version] = Math.round(wiAssertionPassRate * 1000) / 1000;
|
|
226
|
+
const wiTokens = aggregateTokenStats(taskResults.flatMap(r => r.skillTrials[version] || []));
|
|
227
|
+
if (wiTokens)
|
|
228
|
+
tokenStats[version] = wiTokens;
|
|
229
|
+
const wiDuration = aggregateDurationStats(taskResults.flatMap(r => r.skillTrials[version] || []));
|
|
230
|
+
if (wiDuration)
|
|
231
|
+
durationStats[version] = wiDuration;
|
|
232
|
+
}
|
|
187
233
|
const report = {
|
|
188
234
|
timestamp: startTime.toISOString(),
|
|
189
235
|
skill_name,
|
|
190
236
|
agent,
|
|
191
237
|
metrics: {
|
|
192
|
-
withSkillScore: `${withSkillPercentage}%`,
|
|
193
|
-
withoutSkillScore: `${withoutSkillPercentage}%`,
|
|
194
|
-
skillUplift: `${skillUplift > 0 ? '+' : ''}${skillUplift}%`,
|
|
195
238
|
passedCount: withSkillTasksAllPassedCount,
|
|
196
239
|
totalCount: tasks.length,
|
|
197
240
|
numTrials,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
assertionPassRate
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
? { withSkill: withSkillTokenStats, withoutSkill: withoutSkillTokenStats }
|
|
204
|
-
: undefined,
|
|
205
|
-
durationStats: (withSkillDurationStats || withoutSkillDurationStats)
|
|
206
|
-
? { withSkill: withSkillDurationStats, withoutSkill: withoutSkillDurationStats }
|
|
207
|
-
: undefined
|
|
241
|
+
scores,
|
|
242
|
+
passAtK,
|
|
243
|
+
assertionPassRate,
|
|
244
|
+
tokenStats,
|
|
245
|
+
durationStats
|
|
208
246
|
},
|
|
209
247
|
results: taskResults
|
|
210
248
|
};
|
|
211
249
|
Logger.write(`\nEVALUATION SUMMARY\n`);
|
|
212
250
|
Logger.write(`──────────────────────────────────────────────────\n`);
|
|
213
251
|
renderFunctionalTable(report);
|
|
214
|
-
const upliftSign = skillUplift > 0 ? '+' : '';
|
|
215
|
-
Logger.write(`\n Skill Uplift: ${upliftSign}${skillUplift}%\n`);
|
|
216
252
|
Logger.write('\n');
|
|
217
253
|
new JsonReporter().generate(report, runDir);
|
|
218
254
|
reporter.generate(report, runDir);
|
package/dist/commands/trigger.js
CHANGED
|
@@ -6,12 +6,14 @@ import * as evalLoader from '../utils/eval-loader.js';
|
|
|
6
6
|
import { ListrEvalUI } from '../utils/ui.js';
|
|
7
7
|
import { EvalRunner } from '../core/eval-runner.js';
|
|
8
8
|
import { AgentPool } from '../core/agent-pool.js';
|
|
9
|
-
import { aggregatePassAtK, aggregateTokenStats, aggregateDurationStats } from '../core/statistics.js';
|
|
9
|
+
import { aggregatePassAtK, aggregateAssertionPassRate, aggregateTokenStats, aggregateDurationStats } from '../core/statistics.js';
|
|
10
10
|
import { preflight } from '../core/preflight.js';
|
|
11
11
|
import { withRetry } from '../core/trial-utils.js';
|
|
12
12
|
import { renderTriggerTable, renderRunHeader } from '../utils/table-renderer.js';
|
|
13
13
|
import { JsonReporter } from '../reporters/index.js';
|
|
14
|
-
|
|
14
|
+
import chalk from 'chalk';
|
|
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 = []) {
|
|
15
17
|
if (!injectedSuite)
|
|
16
18
|
preflight(agent, workspace, skillPath);
|
|
17
19
|
const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
|
|
@@ -36,21 +38,38 @@ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4,
|
|
|
36
38
|
const timestamp = startTime.toISOString().replace(/[:.]/g, '-');
|
|
37
39
|
const runDir = path.resolve(workspace, '.project-skill-evals', 'runs', timestamp);
|
|
38
40
|
fs.mkdirSync(runDir, { recursive: true });
|
|
41
|
+
const refPathBase = path.resolve(workspace, '.project-skill-evals', 'skill-refs');
|
|
42
|
+
const variantRunners = new Map();
|
|
43
|
+
// 1. Local Runner
|
|
44
|
+
variantRunners.set('local', new EvalRunner({
|
|
45
|
+
agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: false, debug, timeoutMs
|
|
46
|
+
}));
|
|
47
|
+
// 2. Historical Runners
|
|
48
|
+
for (const ref of compareRefs) {
|
|
49
|
+
const refDir = path.join(refPathBase, ref);
|
|
50
|
+
Logger.write(` Extracting ref '${ref}'... `);
|
|
51
|
+
git.extractSkillRef(skillPath, ref, refDir);
|
|
52
|
+
Logger.write(chalk.green('Done\n'));
|
|
53
|
+
variantRunners.set(`ref:${ref}`, new EvalRunner({
|
|
54
|
+
agent,
|
|
55
|
+
workspace: refDir,
|
|
56
|
+
skillPath: path.join(refDir, path.relative(workspace, skillPath)),
|
|
57
|
+
skillName: skill_name,
|
|
58
|
+
runDir,
|
|
59
|
+
isBaseline: false,
|
|
60
|
+
debug,
|
|
61
|
+
timeoutMs
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
39
64
|
const taskResults = [];
|
|
40
65
|
let tasksPassedCount = 0;
|
|
41
|
-
const runner = new EvalRunner({
|
|
42
|
-
agent,
|
|
43
|
-
workspace,
|
|
44
|
-
skillPath,
|
|
45
|
-
skillName: skill_name,
|
|
46
|
-
runDir,
|
|
47
|
-
debug,
|
|
48
|
-
timeoutMs
|
|
49
|
-
});
|
|
50
66
|
const pool = new AgentPool(maxAgents);
|
|
51
67
|
const ui = new ListrEvalUI();
|
|
52
68
|
// Barrier chain: each prompt waits until the previous prompt's trials have all acquired slots.
|
|
53
69
|
let barrier = Promise.resolve();
|
|
70
|
+
// Subtask labels
|
|
71
|
+
const skillVersions = Array.from(variantRunners.keys());
|
|
72
|
+
const subtaskLabels = skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
|
|
54
73
|
try {
|
|
55
74
|
renderRunHeader({ command: 'trigger', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
|
|
56
75
|
Logger.write(`--- Trigger Pass ---\n`);
|
|
@@ -65,54 +84,74 @@ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4,
|
|
|
65
84
|
ui.addTask({
|
|
66
85
|
id: task.id,
|
|
67
86
|
title: taskLabel,
|
|
68
|
-
|
|
87
|
+
subtaskLabels,
|
|
69
88
|
task: async (uiCtx, multi) => {
|
|
70
89
|
await thisBarrier;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
const variantTrialsPromises = {};
|
|
91
|
+
let currentSubtaskIdx = 0;
|
|
92
|
+
for (const [version, runner] of variantRunners.entries()) {
|
|
93
|
+
variantTrialsPromises[version] = [];
|
|
94
|
+
for (let idx = 0; idx < numTrials; idx++) {
|
|
95
|
+
currentSubtaskIdx++;
|
|
96
|
+
const subtaskId = currentSubtaskIdx;
|
|
97
|
+
const runnerTrialId = idx + 1;
|
|
98
|
+
const trialCtx = multi?.getTrialCtx(subtaskId) ?? uiCtx;
|
|
99
|
+
const release = await pool.acquire();
|
|
100
|
+
const p = withRetry((attempt) => runner.runTriggerTask(task, i, runnerTrialId, trialCtx, attempt)
|
|
101
|
+
.catch((error) => ({
|
|
102
|
+
id: runnerTrialId,
|
|
103
|
+
transcript: { error: error instanceof Error ? error.message : String(error) },
|
|
104
|
+
assertionResults: [{
|
|
105
|
+
assertion: `${version} Execution`,
|
|
106
|
+
passed: false,
|
|
107
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
108
|
+
}],
|
|
109
|
+
trialPassed: false,
|
|
110
|
+
isError: true
|
|
111
|
+
})), 2, 1000, (nextAttempt, lastTrial) => {
|
|
112
|
+
const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
|
|
113
|
+
trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
|
|
114
|
+
}).then(trial => {
|
|
115
|
+
if (multi) {
|
|
116
|
+
const reason = trial.assertionResults.find(r => !r.passed)?.reason;
|
|
117
|
+
const passedCount = trial.assertionResults.filter(r => r.passed).length;
|
|
118
|
+
const totalCount = trial.assertionResults.length;
|
|
119
|
+
multi.markTrialComplete(subtaskId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
|
|
120
|
+
}
|
|
121
|
+
return trial;
|
|
122
|
+
}).finally(release);
|
|
123
|
+
variantTrialsPromises[version].push(p);
|
|
124
|
+
}
|
|
98
125
|
}
|
|
99
|
-
// All
|
|
126
|
+
// All slots for this prompt have acquired a slot — signal the next prompt.
|
|
100
127
|
resolveBarrier();
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
128
|
+
const versionsTrials = await Promise.all(Object.values(variantTrialsPromises).map(ps => Promise.all(ps)));
|
|
129
|
+
const taskSkillTrials = {};
|
|
130
|
+
const variantNames = Object.keys(variantTrialsPromises);
|
|
131
|
+
let localAllPassed = true;
|
|
132
|
+
for (let vIdx = 0; vIdx < variantNames.length; vIdx++) {
|
|
133
|
+
const vName = variantNames[vIdx];
|
|
134
|
+
const vTrials = versionsTrials[vIdx];
|
|
135
|
+
taskSkillTrials[vName] = vTrials.map(t => ({ ...t, transcript: undefined }));
|
|
136
|
+
if (vName === 'local') {
|
|
137
|
+
const passedCount = vTrials.filter(t => t.trialPassed).length;
|
|
138
|
+
if (passedCount === vTrials.length) {
|
|
139
|
+
tasksPassedCount++;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
localAllPassed = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
104
146
|
const taskResult = {
|
|
105
147
|
taskId: task.id,
|
|
106
148
|
prompt: task.prompt,
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
baselineTrials: [],
|
|
150
|
+
skillTrials: taskSkillTrials
|
|
109
151
|
};
|
|
110
152
|
taskResults.push(taskResult);
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
else if (!multi) {
|
|
115
|
-
const failureReason = trials.find(t => !t.trialPassed)?.assertionResults.find(r => !r.passed)?.reason || 'Task failed evaluation';
|
|
153
|
+
if (!localAllPassed && !multi) {
|
|
154
|
+
const failureReason = taskSkillTrials['local'].find(t => t.trialPassed === false)?.assertionResults.find(r => !r.passed)?.reason || 'Task failed evaluation';
|
|
116
155
|
throw new Error(failureReason);
|
|
117
156
|
}
|
|
118
157
|
}
|
|
@@ -120,22 +159,39 @@ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4,
|
|
|
120
159
|
}
|
|
121
160
|
await ui.run(tasks.length);
|
|
122
161
|
// Compute aggregate metrics and build report
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const
|
|
162
|
+
const allSkillVersions = taskResults.length > 0 ? Object.keys(taskResults[0].skillTrials) : ['local'];
|
|
163
|
+
const scores = {};
|
|
164
|
+
const passAtK = {};
|
|
165
|
+
const assertionPassRate = {};
|
|
166
|
+
const tokenStats = {};
|
|
167
|
+
const durationStats = {};
|
|
168
|
+
for (const version of allSkillVersions) {
|
|
169
|
+
const { passAtK: vPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.skillTrials[version] || []);
|
|
170
|
+
const percentage = Math.round(vPassAtK * 100);
|
|
171
|
+
scores[version] = `${percentage}%`;
|
|
172
|
+
passAtK[version] = Math.round(vPassAtK * 1000) / 1000;
|
|
173
|
+
const vAssertionRate = aggregateAssertionPassRate(taskResults, r => r.skillTrials[version] || []);
|
|
174
|
+
assertionPassRate[version] = Math.round(vAssertionRate * 1000) / 1000;
|
|
175
|
+
const vTokens = aggregateTokenStats(taskResults.flatMap(r => r.skillTrials[version] || []));
|
|
176
|
+
if (vTokens)
|
|
177
|
+
tokenStats[version] = vTokens;
|
|
178
|
+
const vDuration = aggregateDurationStats(taskResults.flatMap(r => r.skillTrials[version] || []));
|
|
179
|
+
if (vDuration)
|
|
180
|
+
durationStats[version] = vDuration;
|
|
181
|
+
}
|
|
127
182
|
const report = {
|
|
128
183
|
timestamp: startTime.toISOString(),
|
|
129
184
|
skill_name,
|
|
130
185
|
agent,
|
|
131
186
|
metrics: {
|
|
132
|
-
withSkillScore: `${percentage}%`,
|
|
133
187
|
passedCount: tasksPassedCount,
|
|
134
188
|
totalCount: tasks.length,
|
|
135
189
|
numTrials,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
190
|
+
scores,
|
|
191
|
+
passAtK,
|
|
192
|
+
assertionPassRate,
|
|
193
|
+
tokenStats,
|
|
194
|
+
durationStats
|
|
139
195
|
},
|
|
140
196
|
results: taskResults
|
|
141
197
|
};
|
package/dist/core/environment.js
CHANGED
|
@@ -11,13 +11,25 @@ export class EvalEnvironment {
|
|
|
11
11
|
async setup() {
|
|
12
12
|
}
|
|
13
13
|
async teardown() {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const evalsDir = path.resolve(this.workspace, '.project-skill-evals');
|
|
15
|
+
// 1. Cleanup Worktrees
|
|
16
|
+
const worktreesDir = path.join(evalsDir, 'worktrees');
|
|
17
|
+
if (fs.existsSync(worktreesDir)) {
|
|
18
|
+
for (const entry of fs.readdirSync(worktreesDir)) {
|
|
19
|
+
this.removeWorktree(path.join(worktreesDir, entry));
|
|
20
|
+
}
|
|
21
|
+
executor.spawnSync('git', ['worktree', 'prune'], { stdio: 'ignore', cwd: this.workspace });
|
|
22
|
+
}
|
|
23
|
+
// 2. Cleanup Skill Refs
|
|
24
|
+
const skillRefsDir = path.join(evalsDir, 'skill-refs');
|
|
25
|
+
if (fs.existsSync(skillRefsDir)) {
|
|
26
|
+
try {
|
|
27
|
+
fs.rmSync(skillRefsDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
Logger.warn(`Failed to remove skill-refs directory at ${skillRefsDir}. Manual cleanup may be required.`);
|
|
31
|
+
}
|
|
19
32
|
}
|
|
20
|
-
executor.spawnSync('git', ['worktree', 'prune'], { stdio: 'ignore', cwd: this.workspace });
|
|
21
33
|
}
|
|
22
34
|
/**
|
|
23
35
|
* Creates a temporary git worktree for a specific evaluation.
|