@fede0089/skill-eval 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,11 +6,11 @@ A CLI tool for evaluating Agent Skills locally. Tests whether your skill trigger
6
6
 
7
7
  Skills are instructions that change how the agent behaves. But a single successful run isn't enough to trust one — agents are non-deterministic, and a good isolated result can be exactly that: an isolated case.
8
8
 
9
- Skill evals let you turn that intuition into evidence: run several comparable tasks with and without the skill, measure them against the same criteria, and validate whether the agent improves consistently against a baseline.
9
+ Skill evals let you turn that intuition into evidence: run several comparable tasks with the skill, measure them against the same criteria, and optionally compare against a baseline or historical skill branches.
10
10
 
11
11
  ## How it works
12
12
 
13
- For each eval prompt, skill-eval spins up parallel agent processes — some with the skill installed, others without (the baseline). Each agent runs headlessly and produces a transcript. An LLM judge then grades each transcript against your expectations. Results are aggregated into **pass@k** metrics, giving you a clear measure of how much your skill actually improves the agent's behavior versus the unassisted baseline.
13
+ For each eval prompt, skill-eval spins up parallel agent processes with the current skill installed by default. You can optionally add the no-skill baseline or historical skill branches for side-by-side comparison. Each agent runs headlessly and produces a transcript. An LLM judge then grades each transcript against your expectations. Results are aggregated into **pass@k** metrics, giving you a clear view of how the skill behaves in isolation or relative to comparison targets.
14
14
 
15
15
  ```
16
16
  eval prompt
@@ -20,7 +20,7 @@ For each eval prompt, skill-eval spins up parallel agent processes — some with
20
20
  └───────┬───────┘
21
21
 
22
22
  ┌───────────┴───────────┐
23
- ─ with skill ─ ─ baseline ─
23
+ ─ with skill ─ ─ baseline (opt)
24
24
  ┌──────┴──────┐ ┌─────┴──────┐
25
25
  agent 1 agent 2 agent 3 agent 4
26
26
  │ │ │ │
@@ -32,6 +32,8 @@ For each eval prompt, skill-eval spins up parallel agent processes — some with
32
32
  ```
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
+ >
36
+ > The baseline branch is opt-in: enable it with `--compare-baseline` (no-skill control) or `--compare-ref <ref>` (historical skill versions).
35
37
 
36
38
  ## Installation
37
39
 
@@ -66,7 +68,7 @@ npm link # makes `skill-eval` available globally
66
68
  # Checks that the skill is triggered (invoked) for each prompt
67
69
  skill-eval trigger --workspace <path> --skill <path> [options] [agent]
68
70
 
69
- # Checks that the skill produces correct output, measured against a baseline
71
+ # Checks that the skill produces correct output (skill-only by default)
70
72
  skill-eval functional --workspace <path> --skill <path> [options] [agent]
71
73
  ```
72
74
 
@@ -81,6 +83,7 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
81
83
  | `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
82
84
  | `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
83
85
  | `--compare-ref [refs...]` | no | — | Git references to compare against |
86
+ | `--compare-baseline` | no | `false` | Also run the no-skill baseline alongside the skill |
84
87
  | `-v, --debug` | no | `false` | Enable verbose debug logging |
85
88
  | `[agent]` | no | `gemini-cli` | Agent backend to use |
86
89
 
@@ -156,6 +159,7 @@ Refer to your runner's documentation for the full list of available settings and
156
159
  This repo includes a `mock-skill/` directory — a complete, working example of a license-generator skill with trigger and functional evals. Run it directly with:
157
160
 
158
161
  ```sh
162
+ npm run test:unit # run the unit test suite
159
163
  npm run test:trigger # trigger evaluation against mock-skill
160
164
  npm run test:functional # functional evaluation against mock-skill
161
165
  ```
@@ -180,17 +184,4 @@ The factory, preflight check, and CLI all pick it up automatically.
180
184
  ### Adding a new report format
181
185
 
182
186
  1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
183
- 2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
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`.
187
+ 2. Add a case for it in `createReporter()` in `src/reporters/index.ts`.
@@ -13,7 +13,7 @@ import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.
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 functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = []) {
16
+ export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId, compareRefs = [], compareBaseline = false) {
17
17
  if (!injectedSuite)
18
18
  preflight(agent, workspace, skillPath);
19
19
  const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
@@ -63,10 +63,10 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
63
63
  }));
64
64
  }
65
65
  // 3. Baseline Runner
66
- const withoutSkillRunner = new EvalRunner({
66
+ const withoutSkillRunner = compareBaseline ? new EvalRunner({
67
67
  agent, workspace, skillPath, skillName: skill_name, runDir, isBaseline: true, debug, timeoutMs,
68
68
  variant: 'baseline'
69
- });
69
+ }) : undefined;
70
70
  const taskResults = [];
71
71
  let withSkillTasksAllPassedCount = 0;
72
72
  let baselineTasksAllPassedCount = 0;
@@ -76,10 +76,12 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
76
76
  let barrier = Promise.resolve();
77
77
  // Subtask labels
78
78
  const skillVersions = Array.from(variantRunners.keys());
79
- const subtaskLabels = [
80
- ...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
81
- ...skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`))
82
- ];
79
+ const subtaskLabels = compareBaseline
80
+ ? [
81
+ ...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
82
+ ...skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`))
83
+ ]
84
+ : skillVersions.flatMap(v => Array.from({ length: numTrials }, (_, i) => `${v} ${i + 1}`));
83
85
  try {
84
86
  renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
85
87
  Logger.write(`──────────────────────────────────────────────────\n`);
@@ -98,34 +100,36 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
98
100
  await thisBarrier;
99
101
  // 1. Without Skill trials
100
102
  const baselineTrialPromises = [];
101
- for (let idx = 0; idx < numTrials; idx++) {
102
- const trialId = idx + 1;
103
- const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
104
- const release = await pool.acquire();
105
- const p = withRetry((attempt) => withoutSkillRunner.runFunctionalTask(task, i, trialId, trialCtx, attempt)
106
- .catch((error) => ({
107
- id: trialId,
108
- transcript: { error: error instanceof Error ? error.message : String(error) },
109
- assertionResults: [{ assertion: 'Without Skill Execution', passed: false, reason: String(error) }],
110
- trialPassed: false,
111
- isError: true
112
- })), 2, 1000, (nextAttempt, lastTrial) => {
113
- const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
114
- trialCtx.updateLog(`Retry ${nextAttempt}/2 ${reason.substring(0, 50)}`);
115
- }).then(trial => {
116
- if (multi) {
117
- const reason = trial.assertionResults.find(r => !r.passed)?.reason;
118
- const passedCount = trial.assertionResults.filter(r => r.passed).length;
119
- const totalCount = trial.assertionResults.length;
120
- multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
121
- }
122
- return trial;
123
- }).finally(release);
124
- baselineTrialPromises.push(p);
103
+ if (withoutSkillRunner) {
104
+ for (let idx = 0; idx < numTrials; idx++) {
105
+ const trialId = idx + 1;
106
+ const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
107
+ const release = await pool.acquire();
108
+ const p = withRetry((attempt) => withoutSkillRunner.runFunctionalTask(task, i, trialId, trialCtx, attempt)
109
+ .catch((error) => ({
110
+ id: trialId,
111
+ transcript: { error: error instanceof Error ? error.message : String(error) },
112
+ assertionResults: [{ assertion: 'Without Skill Execution', passed: false, reason: String(error) }],
113
+ trialPassed: false,
114
+ isError: true
115
+ })), 2, 1000, (nextAttempt, lastTrial) => {
116
+ const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
117
+ trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
118
+ }).then(trial => {
119
+ if (multi) {
120
+ const reason = trial.assertionResults.find(r => !r.passed)?.reason;
121
+ const passedCount = trial.assertionResults.filter(r => r.passed).length;
122
+ const totalCount = trial.assertionResults.length;
123
+ multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
124
+ }
125
+ return trial;
126
+ }).finally(release);
127
+ baselineTrialPromises.push(p);
128
+ }
125
129
  }
126
130
  // 2. Skill variants trials
127
131
  const variantTrialsPromises = {};
128
- let currentSubtaskIdx = numTrials;
132
+ let currentSubtaskIdx = compareBaseline ? numTrials : 0;
129
133
  for (const [version, runner] of variantRunners.entries()) {
130
134
  variantTrialsPromises[version] = [];
131
135
  for (let idx = 0; idx < numTrials; idx++) {
@@ -158,19 +162,17 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
158
162
  }
159
163
  // All slots for this prompt have been acquired — signal the next prompt.
160
164
  resolveBarrier();
161
- const [woTrials, ...versionsTrials] = await Promise.all([
162
- Promise.all(baselineTrialPromises),
163
- ...Object.values(variantTrialsPromises).map(ps => Promise.all(ps))
164
- ]);
165
+ const variantsTrials = await Promise.all(Object.values(variantTrialsPromises).map(ps => Promise.all(ps)));
166
+ const woTrials = withoutSkillRunner ? await Promise.all(baselineTrialPromises) : [];
165
167
  const woPassedCount = woTrials.filter(t => t.trialPassed).length;
166
- if (woPassedCount === woTrials.length)
168
+ if (withoutSkillRunner && woPassedCount === woTrials.length)
167
169
  baselineTasksAllPassedCount++;
168
170
  const taskSkillTrials = {};
169
171
  const variantNames = Object.keys(variantTrialsPromises);
170
172
  let localAllPassed = true;
171
173
  for (let vIdx = 0; vIdx < variantNames.length; vIdx++) {
172
174
  const vName = variantNames[vIdx];
173
- const vTrials = versionsTrials[vIdx];
175
+ const vTrials = variantsTrials[vIdx];
174
176
  taskSkillTrials[vName] = vTrials.map(t => ({ ...t, transcript: undefined }));
175
177
  if (vName === 'local') {
176
178
  const passedCount = vTrials.filter(t => t.trialPassed).length;
@@ -185,7 +187,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
185
187
  const taskResult = {
186
188
  taskId: task.id,
187
189
  prompt: task.prompt,
188
- baselineTrials: woTrials.map(t => ({ ...t, transcript: undefined })),
190
+ baselineTrials: withoutSkillRunner ? woTrials.map(t => ({ ...t, transcript: undefined })) : [],
189
191
  skillTrials: taskSkillTrials
190
192
  };
191
193
  taskResults.push(taskResult);
@@ -199,25 +201,28 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
199
201
  await ui.run(tasks.length);
200
202
  // ==== REPORTING ====
201
203
  const allSkillVersions = taskResults.length > 0 ? Object.keys(taskResults[0].skillTrials) : ['local'];
202
- const allVersions = ['baseline', ...allSkillVersions];
204
+ const hasBaseline = compareBaseline;
205
+ const allVersions = hasBaseline ? ['baseline', ...allSkillVersions] : allSkillVersions;
203
206
  const scores = {};
204
207
  const passAtK = {};
205
208
  const assertionPassRate = {};
206
209
  const tokenStats = {};
207
210
  const durationStats = {};
208
211
  // 1. Baseline Metrics
209
- const { passAtK: woPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.baselineTrials);
210
- const woAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.baselineTrials);
211
- const woPercentage = Math.round(woAssertionPassRate * 100);
212
- scores['baseline'] = `${woPercentage}%`;
213
- passAtK['baseline'] = Math.round(woPassAtK * 1000) / 1000;
214
- assertionPassRate['baseline'] = Math.round(woAssertionPassRate * 1000) / 1000;
215
- const woTokens = aggregateTokenStats(taskResults.flatMap(r => r.baselineTrials));
216
- if (woTokens)
217
- tokenStats['baseline'] = woTokens;
218
- const woDuration = aggregateDurationStats(taskResults.flatMap(r => r.baselineTrials));
219
- if (woDuration)
220
- durationStats['baseline'] = woDuration;
212
+ if (hasBaseline) {
213
+ const { passAtK: woPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.baselineTrials);
214
+ const woAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.baselineTrials);
215
+ const woPercentage = Math.round(woAssertionPassRate * 100);
216
+ scores['baseline'] = `${woPercentage}%`;
217
+ passAtK['baseline'] = Math.round(woPassAtK * 1000) / 1000;
218
+ assertionPassRate['baseline'] = Math.round(woAssertionPassRate * 1000) / 1000;
219
+ const woTokens = aggregateTokenStats(taskResults.flatMap(r => r.baselineTrials));
220
+ if (woTokens)
221
+ tokenStats['baseline'] = woTokens;
222
+ const woDuration = aggregateDurationStats(taskResults.flatMap(r => r.baselineTrials));
223
+ if (woDuration)
224
+ durationStats['baseline'] = woDuration;
225
+ }
221
226
  // 2. Skill Variants Metrics
222
227
  for (const version of allSkillVersions) {
223
228
  const { passAtK: wiPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.skillTrials[version] || []);
@@ -235,6 +240,7 @@ export async function functionalCommand(agent, workspace, skillPath, maxAgents =
235
240
  }
236
241
  const report = {
237
242
  timestamp: startTime.toISOString(),
243
+ command: 'functional',
238
244
  skill_name,
239
245
  agent,
240
246
  metrics: {
@@ -183,6 +183,7 @@ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4,
183
183
  }
184
184
  const report = {
185
185
  timestamp: startTime.toISOString(),
186
+ command: 'trigger',
186
187
  skill_name,
187
188
  agent,
188
189
  metrics: {
package/dist/index.js CHANGED
@@ -8,7 +8,9 @@ import { HtmlReporter } from './reporters/index.js';
8
8
  import { DEFAULT_AGENT } from './runners/registry.js';
9
9
  import * as path from 'path';
10
10
  import * as fs from 'fs';
11
+ import { createRequire } from 'module';
11
12
  import { fileURLToPath } from 'url';
13
+ const pkg = createRequire(import.meta.url)('../package.json');
12
14
  export const program = new Command();
13
15
  const errorHandler = (err) => {
14
16
  if (err instanceof AppError) {
@@ -26,7 +28,7 @@ const errorHandler = (err) => {
26
28
  program
27
29
  .name('skill-eval')
28
30
  .description('CLI to evaluate agent skills triggering and functionality')
29
- .version('1.0.0')
31
+ .version(pkg.version)
30
32
  .option('-v, --debug', 'Enable debug logging', false);
31
33
  program.on('option:debug', () => {
32
34
  process.env.DEBUG = 'true';
@@ -36,16 +38,16 @@ program
36
38
  .description('Evaluate triggering of an agent skill')
37
39
  .requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
38
40
  .requiredOption('--skill <path>', 'Path to the skill directory')
39
- .option('--agents <number>', 'Number of parallel agents')
40
- .option('--trials <number>', 'Number of trials per task for pass@k calculation')
41
+ .option('--agents <number>', 'Number of parallel agents', '4')
42
+ .option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
41
43
  .option('--timeout <seconds>', 'Agent timeout in seconds')
42
44
  .option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
43
45
  .option('--compare-ref [refs...]', 'Compare against historical git references')
44
46
  .action((agent, options) => {
45
47
  const workspace = path.resolve(options.workspace);
46
48
  const selectedAgent = agent || DEFAULT_AGENT;
47
- const maxAgents = parseInt(options.agents, 10) || 4;
48
- const numTrials = options.trials !== undefined ? (parseInt(options.trials, 10) || 3) : 3;
49
+ const maxAgents = parseInt(options.agents, 10);
50
+ const numTrials = parseInt(options.trials, 10);
49
51
  const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
50
52
  const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
51
53
  const compareRefs = options.compareRef || [];
@@ -53,23 +55,25 @@ program
53
55
  });
54
56
  program
55
57
  .command('functional [agent]')
56
- .description('Evaluate functional correctness of an agent skill based on assertions')
58
+ .description('Evaluate functional correctness of an agent skill against expectations')
57
59
  .requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
58
60
  .requiredOption('--skill <path>', 'Path to the skill directory')
59
- .option('--agents <number>', 'Number of parallel agents')
60
- .option('--trials <number>', 'Number of trials per task for pass@k calculation')
61
+ .option('--agents <number>', 'Number of parallel agents', '4')
62
+ .option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
61
63
  .option('--timeout <seconds>', 'Agent timeout in seconds')
62
64
  .option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
63
65
  .option('--compare-ref [refs...]', 'Compare against historical git references')
66
+ .option('--compare-baseline', 'Also run the no-skill baseline alongside the skill')
64
67
  .action((agent, options) => {
65
68
  const workspace = path.resolve(options.workspace);
66
69
  const selectedAgent = agent || DEFAULT_AGENT;
67
- const maxAgents = parseInt(options.agents, 10) || 4;
68
- const numTrials = options.trials !== undefined ? (parseInt(options.trials, 10) || 3) : 3;
70
+ const maxAgents = parseInt(options.agents, 10);
71
+ const numTrials = parseInt(options.trials, 10);
69
72
  const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
70
73
  const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
71
74
  const compareRefs = options.compareRef || [];
72
- functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs).catch(errorHandler);
75
+ const compareBaseline = !!options.compareBaseline;
76
+ functionalCommand(selectedAgent, workspace, options.skill, maxAgents, undefined, numTrials, new HtmlReporter(), timeoutMs, evalId, compareRefs, compareBaseline).catch(errorHandler);
73
77
  });
74
78
  const isMain = process.argv[1] && (() => {
75
79
  try {
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { Logger } from '../utils/logger.js';
4
- import { formatTokens, formatDuration } from '../utils/table-renderer.js';
4
+ import { formatTokens, formatDuration, hasFunctionalBaseline } from '../utils/table-renderer.js';
5
5
  import { computeAssertionPassRate } from '../core/statistics.js';
6
6
  export class HtmlReporter {
7
7
  generate(report, runDir) {
@@ -32,7 +32,7 @@ function passColorClass(val) {
32
32
  return 'red';
33
33
  }
34
34
  function isFunctional(report) {
35
- return report.metrics.assertionPassRate['baseline'] !== undefined;
35
+ return report.command === 'functional' || hasFunctionalBaseline(report);
36
36
  }
37
37
  // ---------------------------------------------------------------------------
38
38
  // Metrics Grid
@@ -50,7 +50,7 @@ function renderMetricsGrid(report) {
50
50
  const { metrics, results } = report;
51
51
  const functional = isFunctional(report);
52
52
  const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
53
- const allVersions = functional ? ['baseline', ...skillVersions] : skillVersions;
53
+ const allVersions = functional && hasFunctionalBaseline(report) ? ['baseline', ...skillVersions] : skillVersions;
54
54
  const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
55
55
  const successRows = `<tr>
56
56
  <td>Success Rate</td>
@@ -112,7 +112,9 @@ function renderExpectationCell(trials, expIdx, variant, taskId, clickable, total
112
112
  const detail = `<tr class="exp-detail-row" id="${escapeHtml(detailId)}"><td colspan="${totalCols}"><div class="exp-detail-inner"><div class="exp-detail-header">Judge per trial · <span class="variant-pill">${escapeHtml(variant)}</span></div>${lines}</div></td></tr>`;
113
113
  return { cell, detail };
114
114
  }
115
- function renderExpectationsTable(result, allVersions, isFunctionalEval) {
115
+ function renderExpectationsTable(result, isFunctionalEval) {
116
+ const skillVersions = Object.keys(result.skillTrials);
117
+ const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
116
118
  let canonical = [];
117
119
  for (const v of allVersions) {
118
120
  const trials = v === 'baseline' ? result.baselineTrials : result.skillTrials[v];
@@ -159,7 +161,7 @@ function avgTrialDuration(trials) {
159
161
  }
160
162
  function renderTaskMiniGrid(result, isFunctionalEval) {
161
163
  const skillVersions = Object.keys(result.skillTrials);
162
- const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
164
+ const allVersions = isFunctionalEval && (result.baselineTrials?.length ?? 0) > 0 ? ['baseline', ...skillVersions] : skillVersions;
163
165
  const headerCells = allVersions.map(v => `<th>${v}</th>`).join('');
164
166
  const successRows = `<tr>
165
167
  <td>Success Rate</td>
@@ -199,11 +201,9 @@ function renderTaskMiniGrid(result, isFunctionalEval) {
199
201
  </div>`;
200
202
  }
201
203
  function renderTaskDetails(result, isFunctionalEval) {
202
- const skillVersions = Object.keys(result.skillTrials);
203
- const allVersions = isFunctionalEval ? ['baseline', ...skillVersions] : skillVersions;
204
204
  const sections = [];
205
205
  sections.push(renderTaskMiniGrid(result, isFunctionalEval));
206
- sections.push(renderExpectationsTable(result, allVersions, isFunctionalEval));
206
+ sections.push(renderExpectationsTable(result, isFunctionalEval));
207
207
  return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
208
208
  }
209
209
  // ---------------------------------------------------------------------------
@@ -51,6 +51,12 @@ export function formatTokens(n) {
51
51
  return `${Math.round(n / 1_000)}K`;
52
52
  return `${n}`;
53
53
  }
54
+ export function hasFunctionalBaseline(report) {
55
+ if (report.results.some(result => (result.baselineTrials?.length ?? 0) > 0)) {
56
+ return true;
57
+ }
58
+ return report.metrics.passAtK['baseline'] !== undefined || report.metrics.assertionPassRate['baseline'] !== undefined;
59
+ }
54
60
  function formatTokenStatsLine(stats) {
55
61
  const total = formatTokens(stats.avgTotal);
56
62
  const input = formatTokens(stats.avgInput);
@@ -189,7 +195,8 @@ export function renderFunctionalTable(report) {
189
195
  const { results, metrics } = report;
190
196
  // Identify all versions present (baseline + skill versions)
191
197
  const skillVersions = results.length > 0 ? Object.keys(results[0].skillTrials) : ['local'];
192
- const allVersions = ['baseline', ...skillVersions];
198
+ const hasBaseline = hasFunctionalBaseline(report);
199
+ const allVersions = hasBaseline ? ['baseline', ...skillVersions] : skillVersions;
193
200
  const header = ['ID', 'Prompt'];
194
201
  for (const version of allVersions) {
195
202
  header.push(version);
@@ -201,9 +208,11 @@ export function renderFunctionalTable(report) {
201
208
  const row = [result.taskId.toString(), promptSnippet];
202
209
  // Baseline
203
210
  const woTrials = result.baselineTrials || [];
204
- if (woTrials.some(t => t.isError) && !woTrials.every(t => t.isError))
205
- hasPartialErrors = true;
206
- row.push(formatAssertionRate(woTrials));
211
+ if (hasBaseline) {
212
+ if (woTrials.some(t => t.isError) && !woTrials.every(t => t.isError))
213
+ hasPartialErrors = true;
214
+ row.push(formatAssertionRate(woTrials));
215
+ }
207
216
  // Skills
208
217
  for (const version of skillVersions) {
209
218
  const wiTrials = result.skillTrials[version] || [];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fede0089/skill-eval",
3
- "version": "1.3.0",
4
- "description": "CLI to evaluate agent skills triggering",
3
+ "version": "1.4.1",
4
+ "description": "CLI to evaluate agent skills triggering and functionality",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "skill-eval": "dist/index.js"