@fede0089/skill-eval 1.0.6 → 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 +13 -0
- 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 +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,7 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
80
80
|
| `--trials <number>` | no | `3` | Trials per task (for pass@k) |
|
|
81
81
|
| `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
|
|
82
82
|
| `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
|
|
83
|
+
| `--compare-ref [refs...]` | no | — | Git references to compare against |
|
|
83
84
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
84
85
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
85
86
|
|
|
@@ -181,3 +182,15 @@ The factory, preflight check, and CLI all pick it up automatically.
|
|
|
181
182
|
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
182
183
|
2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
|
|
183
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.
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,7 @@ program
|
|
|
40
40
|
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
41
41
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
42
42
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
43
|
+
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
43
44
|
.action((agent, options) => {
|
|
44
45
|
const workspace = path.resolve(options.workspace);
|
|
45
46
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
@@ -47,7 +48,8 @@ program
|
|
|
47
48
|
const numTrials = options.trials !== undefined ? (parseInt(options.trials, 10) || 3) : 3;
|
|
48
49
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
49
50
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
50
|
-
|
|
51
|
+
const compareRefs = options.compareRef || [];
|
|
52
|
+
triggerCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs).catch(errorHandler);
|
|
51
53
|
});
|
|
52
54
|
program
|
|
53
55
|
.command('functional [agent]')
|
|
@@ -58,6 +60,7 @@ program
|
|
|
58
60
|
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
59
61
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
60
62
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
63
|
+
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
61
64
|
.action((agent, options) => {
|
|
62
65
|
const workspace = path.resolve(options.workspace);
|
|
63
66
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
@@ -65,7 +68,8 @@ program
|
|
|
65
68
|
const numTrials = options.trials !== undefined ? (parseInt(options.trials, 10) || 3) : 3;
|
|
66
69
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
67
70
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
68
|
-
|
|
71
|
+
const compareRefs = options.compareRef || [];
|
|
72
|
+
functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs).catch(errorHandler);
|
|
69
73
|
});
|
|
70
74
|
const isMain = process.argv[1] && (() => {
|
|
71
75
|
try {
|
|
@@ -32,7 +32,7 @@ function passColorClass(val) {
|
|
|
32
32
|
return 'red';
|
|
33
33
|
}
|
|
34
34
|
function isFunctional(report) {
|
|
35
|
-
return report.metrics.
|
|
35
|
+
return report.metrics.assertionPassRate['baseline'] !== undefined;
|
|
36
36
|
}
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
38
38
|
// Metrics Grid
|
|
@@ -47,71 +47,44 @@ function renderDeltaCell(base, target, format, cellClass) {
|
|
|
47
47
|
return `<span class="${cellClass} ${cls}">${sign}${pct}%</span>`;
|
|
48
48
|
}
|
|
49
49
|
function renderMetricsGrid(report) {
|
|
50
|
-
const { metrics } = report;
|
|
50
|
+
const { metrics, results } = report;
|
|
51
51
|
const functional = isFunctional(report);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
53
|
+
const allVersions = functional ? ['baseline', ...skillVersions] : skillVersions;
|
|
54
|
+
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
55
|
+
const successRows = `<tr>
|
|
56
|
+
<td>Success Rate</td>
|
|
57
|
+
${allVersions.map(v => {
|
|
58
|
+
const val = metrics.assertionPassRate[v] ?? metrics.passAtK[v] ?? 0;
|
|
59
|
+
return `<td><span class="metric-val ${passColorClass(val)}">${formatPercent(val)}</span></td>`;
|
|
60
|
+
}).join('')}
|
|
61
|
+
</tr>`;
|
|
62
|
+
const tokenRows = `<tr>
|
|
63
|
+
<td>Tokens (avg)</td>
|
|
64
|
+
${allVersions.map(v => {
|
|
65
|
+
const stats = metrics.tokenStats?.[v];
|
|
66
|
+
return `<td>${stats ? `<span class="metric-val">${formatTokens(stats.avgTotal)}</span><div class="metric-sub">avg total</div>` : '<span class="metric-val muted">—</span>'}</td>`;
|
|
67
|
+
}).join('')}
|
|
68
|
+
</tr>`;
|
|
69
|
+
const timeRows = `<tr>
|
|
70
|
+
<td>Time (avg)</td>
|
|
71
|
+
${allVersions.map(v => {
|
|
72
|
+
const stats = metrics.durationStats?.[v];
|
|
73
|
+
return `<td>${stats ? `<span class="metric-val">${formatDuration(stats.avgMs)}</span>` : '<span class="metric-val muted">—</span>'}</td>`;
|
|
74
|
+
}).join('')}
|
|
75
|
+
</tr>`;
|
|
76
|
+
return `<div class="metrics-grid">
|
|
62
77
|
<table>
|
|
63
78
|
<thead>
|
|
64
|
-
<tr><th></th
|
|
79
|
+
<tr><th></th>${headerCells}</tr>
|
|
65
80
|
</thead>
|
|
66
81
|
<tbody>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<td><span class="metric-val ${passColorClass(tk)}">${formatPercent(tk)}</span></td>
|
|
71
|
-
<td><span class="metric-val ${upliftClass}">${escapeHtml(metrics.skillUplift ?? '0%')}</span></td>
|
|
72
|
-
</tr>
|
|
73
|
-
<tr>
|
|
74
|
-
<td>Tokens (avg)</td>
|
|
75
|
-
<td>${wo ? `<span class="metric-val">${formatTokens(wo.avgTotal)}</span><div class="metric-sub">avg total</div>` : '<span class="metric-val muted">—</span>'}</td>
|
|
76
|
-
<td>${wi ? `<span class="metric-val">${formatTokens(wi.avgTotal)}</span><div class="metric-sub">avg total</div>` : '<span class="metric-val muted">—</span>'}</td>
|
|
77
|
-
<td>${wo && wi ? renderDeltaCell(wo.avgTotal, wi.avgTotal, formatTokens, 'metric-val') : '<span class="metric-val muted">—</span>'}</td>
|
|
78
|
-
</tr>
|
|
79
|
-
<tr>
|
|
80
|
-
<td>Time (avg)</td>
|
|
81
|
-
<td>${wod ? `<span class="metric-val">${formatDuration(wod.avgMs)}</span>` : '<span class="metric-val muted">—</span>'}</td>
|
|
82
|
-
<td>${wid ? `<span class="metric-val">${formatDuration(wid.avgMs)}</span>` : '<span class="metric-val muted">—</span>'}</td>
|
|
83
|
-
<td>${wod && wid ? renderDeltaCell(wod.avgMs, wid.avgMs, formatDuration, 'metric-val') : '<span class="metric-val muted">—</span>'}</td>
|
|
84
|
-
</tr>
|
|
82
|
+
${successRows}
|
|
83
|
+
${tokenRows}
|
|
84
|
+
${timeRows}
|
|
85
85
|
</tbody>
|
|
86
86
|
</table>
|
|
87
87
|
</div>`;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
const k = metrics.passAtK ?? 0;
|
|
91
|
-
const wi = metrics.tokenStats?.withSkill;
|
|
92
|
-
const wid = metrics.durationStats?.withSkill;
|
|
93
|
-
return `<div class="metrics-grid">
|
|
94
|
-
<table>
|
|
95
|
-
<thead>
|
|
96
|
-
<tr><th></th><th>Score</th></tr>
|
|
97
|
-
</thead>
|
|
98
|
-
<tbody>
|
|
99
|
-
<tr>
|
|
100
|
-
<td>Success Rate</td>
|
|
101
|
-
<td><span class="metric-val ${passColorClass(k)}">${formatPercent(k)}</span></td>
|
|
102
|
-
</tr>
|
|
103
|
-
<tr>
|
|
104
|
-
<td>Tokens (avg)</td>
|
|
105
|
-
<td>${wi ? `<span class="metric-val">${formatTokens(wi.avgTotal)}</span><div class="metric-sub">avg total</div>` : '<span class="metric-val muted">—</span>'}</td>
|
|
106
|
-
</tr>
|
|
107
|
-
<tr>
|
|
108
|
-
<td>Time (avg)</td>
|
|
109
|
-
<td>${wid ? `<span class="metric-val">${formatDuration(wid.avgMs)}</span>` : '<span class="metric-val muted">—</span>'}</td>
|
|
110
|
-
</tr>
|
|
111
|
-
</tbody>
|
|
112
|
-
</table>
|
|
113
|
-
</div>`;
|
|
114
|
-
}
|
|
115
88
|
}
|
|
116
89
|
// ---------------------------------------------------------------------------
|
|
117
90
|
// Trial details
|
|
@@ -160,63 +133,58 @@ function avgTrialDuration(trials) {
|
|
|
160
133
|
return null;
|
|
161
134
|
return Math.round(withDuration.reduce((s, t) => s + t.durationMs, 0) / withDuration.length);
|
|
162
135
|
}
|
|
163
|
-
function renderTaskMiniGrid(result) {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
: '<span class="metric-val-sm muted">—</span>'
|
|
136
|
+
function renderTaskMiniGrid(result, isFunctionalEval) {
|
|
137
|
+
const skillVersions = Object.keys(result.skillTrials);
|
|
138
|
+
const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
|
|
139
|
+
const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
|
|
140
|
+
const successRows = `<tr>
|
|
141
|
+
<td>Success Rate</td>
|
|
142
|
+
${allVersions.map(v => {
|
|
143
|
+
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
144
|
+
const rate = trials.length ? Math.round(computeAssertionPassRate(trials) * 100) : 0;
|
|
145
|
+
return `<td><span class="metric-val-sm ${passColorClass(rate / 100)}">${rate}%</span></td>`;
|
|
146
|
+
}).join('')}
|
|
147
|
+
</tr>`;
|
|
148
|
+
const tokenRows = `<tr>
|
|
149
|
+
<td>Tokens (avg)</td>
|
|
150
|
+
${allVersions.map(v => {
|
|
151
|
+
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
152
|
+
const tokens = avgTrialTokens(trials);
|
|
153
|
+
return `<td>${tokens != null ? `<span class="metric-val-sm">${formatTokens(tokens)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>`;
|
|
154
|
+
}).join('')}
|
|
155
|
+
</tr>`;
|
|
156
|
+
const timeRows = `<tr>
|
|
157
|
+
<td>Time (avg)</td>
|
|
158
|
+
${allVersions.map(v => {
|
|
159
|
+
const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
|
|
160
|
+
const ms = avgTrialDuration(trials);
|
|
161
|
+
return `<td>${ms != null ? `<span class="metric-val-sm">${formatDuration(ms)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>`;
|
|
162
|
+
}).join('')}
|
|
163
|
+
</tr>`;
|
|
181
164
|
return `<div class="metrics-grid-sm">
|
|
182
165
|
<table>
|
|
183
166
|
<thead>
|
|
184
|
-
<tr><th></th
|
|
167
|
+
<tr><th></th>${headerCells}</tr>
|
|
185
168
|
</thead>
|
|
186
169
|
<tbody>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<td><span class="metric-val-sm ${passColorClass(tk / 100)}">${tk}%</span></td>
|
|
191
|
-
<td><span class="metric-val-sm ${rateDeltaClass}">${rateDeltaSign}${rateDelta}%</span></td>
|
|
192
|
-
</tr>
|
|
193
|
-
<tr>
|
|
194
|
-
<td>Tokens (avg)</td>
|
|
195
|
-
<td>${woTokens != null ? `<span class="metric-val-sm">${formatTokens(woTokens)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>
|
|
196
|
-
<td>${wiTokens != null ? `<span class="metric-val-sm">${formatTokens(wiTokens)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>
|
|
197
|
-
<td>${tokenDeltaCell}</td>
|
|
198
|
-
</tr>
|
|
199
|
-
<tr>
|
|
200
|
-
<td>Time (avg)</td>
|
|
201
|
-
<td>${woMs != null ? `<span class="metric-val-sm">${formatDuration(woMs)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>
|
|
202
|
-
<td>${wiMs != null ? `<span class="metric-val-sm">${formatDuration(wiMs)}</span>` : '<span class="metric-val-sm muted">—</span>'}</td>
|
|
203
|
-
<td>${durationDeltaCell}</td>
|
|
204
|
-
</tr>
|
|
170
|
+
${successRows}
|
|
171
|
+
${tokenRows}
|
|
172
|
+
${timeRows}
|
|
205
173
|
</tbody>
|
|
206
174
|
</table>
|
|
207
175
|
</div>`;
|
|
208
176
|
}
|
|
209
177
|
function renderTaskDetails(result, isFunctionalEval) {
|
|
210
178
|
const sections = [];
|
|
211
|
-
|
|
212
|
-
|
|
179
|
+
sections.push(renderTaskMiniGrid(result, isFunctionalEval));
|
|
180
|
+
if (isFunctionalEval && result.baselineTrials && result.baselineTrials.length > 0) {
|
|
181
|
+
sections.push('<div class="trial-group-label">Baseline</div>');
|
|
182
|
+
sections.push(...result.baselineTrials.map(t => renderTrial(t)));
|
|
213
183
|
}
|
|
214
|
-
|
|
215
|
-
sections.push(
|
|
216
|
-
sections.push(...result.
|
|
217
|
-
sections.push('<div class="trial-group-label">With Skill</div>');
|
|
184
|
+
for (const version of Object.keys(result.skillTrials)) {
|
|
185
|
+
sections.push(`<div class="trial-group-label">${version}</div>`);
|
|
186
|
+
sections.push(...result.skillTrials[version].map(t => renderTrial(t)));
|
|
218
187
|
}
|
|
219
|
-
sections.push(...result.trials.map(t => renderTrial(t)));
|
|
220
188
|
return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
|
|
221
189
|
}
|
|
222
190
|
// ---------------------------------------------------------------------------
|
|
@@ -241,7 +209,7 @@ export function generateHtml(report) {
|
|
|
241
209
|
const { skill_name, agent, timestamp, metrics } = report;
|
|
242
210
|
const functional = isFunctional(report);
|
|
243
211
|
const evalType = functional ? 'Functional' : 'Trigger';
|
|
244
|
-
const overallScore = metrics.passAtK ?? 0;
|
|
212
|
+
const overallScore = metrics.passAtK['local'] ?? 0;
|
|
245
213
|
const statusClass = passColorClass(overallScore);
|
|
246
214
|
const formattedDate = new Date(timestamp).toLocaleString();
|
|
247
215
|
return `<!DOCTYPE html>
|
package/dist/utils/exec.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { execSync, spawnSync } from 'child_process';
|
|
2
2
|
export const executor = {
|
|
3
3
|
execSync(command, options) {
|
|
4
|
-
return execSync(command,
|
|
4
|
+
return execSync(command, {
|
|
5
|
+
...options,
|
|
6
|
+
shell: process.env.SHELL || '/bin/sh',
|
|
7
|
+
env: { ...process.env, ...options?.env }
|
|
8
|
+
});
|
|
5
9
|
},
|
|
6
10
|
spawnSync(command, args, options) {
|
|
7
|
-
return spawnSync(command, args,
|
|
11
|
+
return spawnSync(command, args, {
|
|
12
|
+
...options,
|
|
13
|
+
env: { ...process.env, ...options?.env }
|
|
14
|
+
});
|
|
8
15
|
}
|
|
9
16
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { executor } from './exec.js';
|
|
4
|
+
import { ExecutionError } from '../core/errors.js';
|
|
5
|
+
export const git = {
|
|
6
|
+
extractSkillRef(skillPath, ref, targetDir) {
|
|
7
|
+
// 1. Identify repo root
|
|
8
|
+
let repoRoot;
|
|
9
|
+
try {
|
|
10
|
+
repoRoot = executor.execSync('git rev-parse --show-toplevel', { cwd: skillPath }).toString().trim();
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
throw new ExecutionError(`Path is not inside a git repository: ${skillPath}`);
|
|
14
|
+
}
|
|
15
|
+
// 2. Resolve relative path of skill within repo
|
|
16
|
+
const relativeSkillPath = path.relative(repoRoot, path.resolve(skillPath));
|
|
17
|
+
// 3. Create target directory
|
|
18
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
19
|
+
// 4. Extract via git archive
|
|
20
|
+
try {
|
|
21
|
+
const cmd = `git archive ${ref} | tar -x -C ${targetDir}`;
|
|
22
|
+
executor.execSync(cmd, { cwd: repoRoot });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
throw new ExecutionError(`Failed to extract git reference '${ref}': ${err instanceof Error ? err.message : String(err)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
// Also keep named export for backward compatibility if needed
|
|
30
|
+
export const extractSkillRef = git.extractSkillRef;
|
|
@@ -126,42 +126,60 @@ export function renderRunHeader(config) {
|
|
|
126
126
|
export function renderTriggerTable(report) {
|
|
127
127
|
const { results, metrics } = report;
|
|
128
128
|
const numTrials = metrics.numTrials || 1;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
// Identify all skill versions present in the results
|
|
130
|
+
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
131
|
+
const header = ['ID', 'Prompt'];
|
|
132
|
+
if (numTrials > 1) {
|
|
133
|
+
for (const version of skillVersions) {
|
|
134
|
+
header.push(`${version} Trials`, `${version} Rate`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
for (const version of skillVersions) {
|
|
139
|
+
header.push(`${version} Rate`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const tableData = [header];
|
|
132
143
|
let hasPartialErrors = false;
|
|
133
144
|
for (const result of results) {
|
|
134
145
|
const promptSnippet = result.prompt.substring(0, 40) + (result.prompt.length > 40 ? '...' : '');
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const row = [result.taskId.toString(), promptSnippet];
|
|
147
|
+
for (const version of skillVersions) {
|
|
148
|
+
const trials = result.skillTrials[version] || [];
|
|
149
|
+
const p1Cell = formatPassAt1(trials);
|
|
150
|
+
const someError = trials.some(t => t.isError);
|
|
151
|
+
const allError = trials.length > 0 && trials.every(t => t.isError);
|
|
152
|
+
if (someError && !allError)
|
|
153
|
+
hasPartialErrors = true;
|
|
154
|
+
if (numTrials > 1) {
|
|
155
|
+
const errorCount = trials.filter(t => t.isError).length;
|
|
156
|
+
const passedCount = trials.filter(t => t.trialPassed).length;
|
|
157
|
+
const trialsBase = `${passedCount}/${trials.length}`;
|
|
158
|
+
const trialsStr = errorCount > 0 ? `${trialsBase} (${errorCount}!)` : trialsBase;
|
|
159
|
+
const trialsCell = passedCount === trials.length ? chalk.green(trialsStr) : errorCount > 0 ? chalk.yellow(trialsStr) : chalk.red(trialsStr);
|
|
160
|
+
row.push(trialsCell, p1Cell);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
row.push(p1Cell);
|
|
164
|
+
}
|
|
150
165
|
}
|
|
166
|
+
tableData.push(row);
|
|
151
167
|
}
|
|
152
168
|
Logger.table(tableData);
|
|
153
169
|
if (hasPartialErrors) {
|
|
154
170
|
Logger.write(chalk.yellow('\n * Some trials did not complete due to infrastructure errors. success rate is computed over the trials that ran.'));
|
|
155
171
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
for (const version of skillVersions) {
|
|
173
|
+
const percentage = Math.round((metrics.passAtK[version] || 0) * 100);
|
|
174
|
+
Logger.write(`\n ${version} Success Rate: ${percentage}%`);
|
|
175
|
+
const tokenStats = metrics.tokenStats?.[version];
|
|
176
|
+
if (tokenStats) {
|
|
177
|
+
Logger.write(`\n Avg Tokens (${version}): ${formatTokenStatsLine(tokenStats)}`);
|
|
178
|
+
}
|
|
179
|
+
const durationStats = metrics.durationStats?.[version];
|
|
180
|
+
if (durationStats) {
|
|
181
|
+
Logger.write(`\n Avg Time (${version}): ${formatDuration(durationStats.avgMs)}`);
|
|
182
|
+
}
|
|
165
183
|
}
|
|
166
184
|
}
|
|
167
185
|
/**
|
|
@@ -169,61 +187,46 @@ export function renderTriggerTable(report) {
|
|
|
169
187
|
*/
|
|
170
188
|
export function renderFunctionalTable(report) {
|
|
171
189
|
const { results, metrics } = report;
|
|
172
|
-
|
|
190
|
+
// Identify all versions present (baseline + skill versions)
|
|
191
|
+
const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
|
|
192
|
+
const allVersions = ['baseline', ...skillVersions];
|
|
193
|
+
const header = ['ID', 'Prompt'];
|
|
194
|
+
for (const version of allVersions) {
|
|
195
|
+
header.push(version);
|
|
196
|
+
}
|
|
197
|
+
const tableData = [header];
|
|
173
198
|
let hasPartialErrors = false;
|
|
174
199
|
for (const result of results) {
|
|
175
200
|
const promptSnippet = result.prompt.substring(0, 40) + (result.prompt.length > 40 ? '...' : '');
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const withSkillSomeError = withSkillTrials.some(t => t.isError);
|
|
181
|
-
const withSkillAllError = withSkillTrials.length > 0 && withSkillTrials.every(t => t.isError);
|
|
182
|
-
if ((baselineSomeError && !baselineAllError) || (withSkillSomeError && !withSkillAllError))
|
|
201
|
+
const row = [result.taskId.toString(), promptSnippet];
|
|
202
|
+
// Baseline
|
|
203
|
+
const woTrials = result.baselineTrials || [];
|
|
204
|
+
if (woTrials.some(t => t.isError) && !woTrials.every(t => t.isError))
|
|
183
205
|
hasPartialErrors = true;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
row.push(formatAssertionRate(woTrials));
|
|
207
|
+
// Skills
|
|
208
|
+
for (const version of skillVersions) {
|
|
209
|
+
const wiTrials = result.skillTrials[version] || [];
|
|
210
|
+
if (wiTrials.some(t => t.isError) && !wiTrials.every(t => t.isError))
|
|
211
|
+
hasPartialErrors = true;
|
|
212
|
+
row.push(formatAssertionRate(wiTrials));
|
|
213
|
+
}
|
|
214
|
+
tableData.push(row);
|
|
190
215
|
}
|
|
191
216
|
Logger.table(tableData);
|
|
192
217
|
if (hasPartialErrors) {
|
|
193
218
|
Logger.write(chalk.yellow('\n * Some trials did not complete due to infrastructure errors. success rate is computed over the trials that ran.'));
|
|
194
219
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (baselineTokenStats || withSkillTokenStats) {
|
|
202
|
-
if (baselineTokenStats)
|
|
203
|
-
Logger.write(`\n Tokens (w/o skill): ${formatTokenStatsLine(baselineTokenStats)}`);
|
|
204
|
-
if (withSkillTokenStats)
|
|
205
|
-
Logger.write(`\n Tokens (w/ skill): ${formatTokenStatsLine(withSkillTokenStats)}`);
|
|
206
|
-
if (baselineTokenStats && withSkillTokenStats && baselineTokenStats.avgTotal > 0) {
|
|
207
|
-
const delta = withSkillTokenStats.avgTotal - baselineTokenStats.avgTotal;
|
|
208
|
-
const deltaSign = delta >= 0 ? '+' : '';
|
|
209
|
-
const deltaPct = Math.round((delta / baselineTokenStats.avgTotal) * 100);
|
|
210
|
-
const deltaStr = `${deltaSign}${formatTokens(Math.abs(delta))} (${deltaSign}${deltaPct}%)`;
|
|
211
|
-
Logger.write(`\n Token Delta: ${delta >= 0 ? chalk.yellow(deltaStr) : chalk.green(deltaStr)}`);
|
|
220
|
+
for (const version of allVersions) {
|
|
221
|
+
const rate = Math.round(((metrics.assertionPassRate[version] ?? metrics.passAtK[version]) || 0) * 100);
|
|
222
|
+
Logger.write(`\n ${version} Rate: ${rate}%`);
|
|
223
|
+
const stats = metrics.tokenStats?.[version];
|
|
224
|
+
if (stats) {
|
|
225
|
+
Logger.write(`\n Tokens (${version}): ${formatTokenStatsLine(stats)}`);
|
|
212
226
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (baselineDurationStats || withSkillDurationStats) {
|
|
217
|
-
if (baselineDurationStats)
|
|
218
|
-
Logger.write(`\n Time (w/o skill): ${formatDuration(baselineDurationStats.avgMs)} avg`);
|
|
219
|
-
if (withSkillDurationStats)
|
|
220
|
-
Logger.write(`\n Time (w/ skill): ${formatDuration(withSkillDurationStats.avgMs)} avg`);
|
|
221
|
-
if (baselineDurationStats && withSkillDurationStats && baselineDurationStats.avgMs > 0) {
|
|
222
|
-
const delta = withSkillDurationStats.avgMs - baselineDurationStats.avgMs;
|
|
223
|
-
const deltaSign = delta >= 0 ? '+' : '';
|
|
224
|
-
const deltaPct = Math.round((delta / baselineDurationStats.avgMs) * 100);
|
|
225
|
-
const deltaStr = `${deltaSign}${formatDuration(Math.abs(delta))} (${deltaSign}${deltaPct}%)`;
|
|
226
|
-
Logger.write(`\n Time Delta: ${delta >= 0 ? chalk.yellow(deltaStr) : chalk.green(deltaStr)}`);
|
|
227
|
+
const dStats = metrics.durationStats?.[version];
|
|
228
|
+
if (dStats) {
|
|
229
|
+
Logger.write(`\n Time (${version}): ${formatDuration(dStats.avgMs)} avg`);
|
|
227
230
|
}
|
|
228
231
|
}
|
|
229
232
|
}
|