@fede0089/skill-eval 1.0.0 → 1.0.6

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
@@ -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
- ## Quick Start
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
- git clone <this-repo>
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
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fede0089/skill-eval",
3
- "version": "1.0.0",
3
+ "version": "1.0.6",
4
4
  "description": "CLI to evaluate agent skills triggering",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -12,6 +12,9 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "scripts": {
15
+ "release:patch": "npm version patch && git push --follow-tags",
16
+ "release:minor": "npm version minor && git push --follow-tags",
17
+ "release:major": "npm version major && git push --follow-tags",
15
18
  "prepare": "npm run build",
16
19
  "build": "tsc",
17
20
  "start": "node dist/index.js",
@@ -1,20 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.rateCommand = rateCommand;
4
- const logger_1 = require("../utils/logger");
5
- const errors_1 = require("../core/errors");
6
- async function rateCommand(score, options) {
7
- const rating = parseInt(score, 10);
8
- if (isNaN(rating) || rating < 1 || rating > 5) {
9
- throw new errors_1.ValidationError('Rating must be a number between 1 and 5.');
10
- }
11
- logger_1.Logger.info('\n==========================================');
12
- logger_1.Logger.info(' EXPERIENCE RATING VIEW ');
13
- logger_1.Logger.info('==========================================');
14
- logger_1.Logger.info(`Rating: ${'★'.repeat(rating)}${'☆'.repeat(5 - rating)}`);
15
- if (options.comment) {
16
- logger_1.Logger.info(`Comment: ${options.comment}`);
17
- }
18
- logger_1.Logger.info('==========================================');
19
- logger_1.Logger.info('Thank you for your feedback!\n');
20
- }
@@ -1,43 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import { Logger } from '../utils/logger.js';
5
- import { AppError } from '../core/errors.js';
6
- import { renderTriggerTable, renderFunctionalTable } from '../utils/table-renderer.js';
7
- import { JsonReporter } from '../core/reporters/index.js';
8
- export async function showCommand(workspace, reporter = new JsonReporter()) {
9
- const runsDir = path.resolve(workspace, '.project-skill-evals', 'runs');
10
- if (!fs.existsSync(runsDir)) {
11
- throw new AppError('No evaluation runs found. Run an evaluation first.');
12
- }
13
- const runs = fs.readdirSync(runsDir)
14
- .filter(dir => fs.statSync(path.join(runsDir, dir)).isDirectory())
15
- .sort((a, b) => b.localeCompare(a)); // Sort descending to get latest
16
- if (runs.length === 0) {
17
- throw new AppError('No evaluation runs found. Run an evaluation first.');
18
- }
19
- const latestRun = runs[0];
20
- const summaryPath = path.join(runsDir, latestRun, 'summary.json');
21
- if (!fs.existsSync(summaryPath)) {
22
- throw new AppError(`Summary file not found for the latest run: ${latestRun}`);
23
- }
24
- const report = JSON.parse(fs.readFileSync(summaryPath, 'utf-8'));
25
- const { skill_name, agent, metrics, results, timestamp } = report;
26
- const numTrials = metrics.numTrials || 1;
27
- const isFunctional = metrics.withoutSkillScore !== undefined;
28
- Logger.write(`\n${chalk.bold('LATEST EVALUATION RESULTS')}\n`);
29
- Logger.write(`Timestamp: ${new Date(timestamp).toLocaleString()}\n`);
30
- Logger.write(`Skill: ${skill_name}\n`);
31
- Logger.write(`Agent: ${agent}\n`);
32
- Logger.write(`Type: ${isFunctional ? 'Functional' : 'Trigger'}\n`);
33
- Logger.write(`──────────────────────────────────────────────────\n`);
34
- if (isFunctional) {
35
- renderFunctionalTable(report);
36
- }
37
- else {
38
- renderTriggerTable(report);
39
- }
40
- Logger.write('\n');
41
- const runDir = path.join(runsDir, latestRun);
42
- reporter.generate(report, runDir);
43
- }
@@ -1,30 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.viewCommand = viewCommand;
4
- const logger_1 = require("../utils/logger");
5
- async function viewCommand(options) {
6
- const ease = parseInt(options.ease || '0', 10);
7
- const speed = parseInt(options.speed || '0', 10);
8
- const accuracy = parseInt(options.accuracy || '0', 10);
9
- const drawStars = (score) => {
10
- const stars = '★'.repeat(score);
11
- const empty = '☆'.repeat(5 - score);
12
- return `[ ${stars}${empty} ]`;
13
- };
14
- logger_1.Logger.info('\n┌──────────────────────────────────────────┐');
15
- logger_1.Logger.info('│ NUEVA VISTA DE CALIFICACIÓN │');
16
- logger_1.Logger.info('├──────────────────────────────────────────┤');
17
- logger_1.Logger.info(`│ Facilidad de uso: ${drawStars(ease)} │`);
18
- logger_1.Logger.info(`│ Velocidad: ${drawStars(speed)} │`);
19
- logger_1.Logger.info(`│ Precisión: ${drawStars(accuracy)} │`);
20
- logger_1.Logger.info('├──────────────────────────────────────────┤');
21
- if (options.comment) {
22
- logger_1.Logger.info(`│ Comentario: │`);
23
- // Basic wrapping for the comment
24
- const comment = options.comment.substring(0, 38);
25
- logger_1.Logger.info(`│ ${comment.padEnd(40)} │`);
26
- }
27
- logger_1.Logger.info('├──────────────────────────────────────────┤');
28
- logger_1.Logger.info('│ ¡Gracias por calificar tu experiencia! │');
29
- logger_1.Logger.info('└──────────────────────────────────────────┘\n');
30
- }
@@ -1,58 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { ConfigError } from './errors.js';
4
- const CONFIG_FILE = '.skill-eval.json';
5
- /**
6
- * Loads configuration from a `.skill-eval.json` file in the given directory.
7
- * Returns an empty object if the file does not exist (config is optional).
8
- *
9
- * CLI flags always take precedence over config file values — this function
10
- * only provides defaults for flags not explicitly passed on the command line.
11
- *
12
- * @throws ConfigError on malformed JSON or type mismatches.
13
- */
14
- export function loadConfig(cwd) {
15
- const configPath = path.join(cwd, CONFIG_FILE);
16
- if (!fs.existsSync(configPath)) {
17
- return {};
18
- }
19
- let raw;
20
- try {
21
- raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
22
- }
23
- catch (err) {
24
- throw new ConfigError(`Failed to parse ${CONFIG_FILE}: ${err instanceof Error ? err.message : String(err)}`);
25
- }
26
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
27
- throw new ConfigError(`${CONFIG_FILE} must be a JSON object.`);
28
- }
29
- const config = raw;
30
- const result = {};
31
- if ('agent' in config) {
32
- if (typeof config.agent !== 'string')
33
- throw new ConfigError(`${CONFIG_FILE}: 'agent' must be a string.`);
34
- result.agent = config.agent;
35
- }
36
- if ('concurrency' in config) {
37
- if (typeof config.concurrency !== 'number')
38
- throw new ConfigError(`${CONFIG_FILE}: 'concurrency' must be a number.`);
39
- result.concurrency = config.concurrency;
40
- }
41
- if ('trials' in config) {
42
- if (typeof config.trials !== 'number')
43
- throw new ConfigError(`${CONFIG_FILE}: 'trials' must be a number.`);
44
- result.trials = config.trials;
45
- }
46
- if ('report' in config) {
47
- if (config.report !== 'html' && config.report !== 'json') {
48
- throw new ConfigError(`${CONFIG_FILE}: 'report' must be 'html' or 'json'.`);
49
- }
50
- result.report = config.report;
51
- }
52
- if ('skill' in config) {
53
- if (typeof config.skill !== 'string')
54
- throw new ConfigError(`${CONFIG_FILE}: 'skill' must be a string.`);
55
- result.skill = config.skill;
56
- }
57
- return result;
58
- }
@@ -1,354 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { Logger } from '../../utils/logger.js';
4
- export class HtmlReporter {
5
- generate(report, runDir) {
6
- const html = generateHtml(report);
7
- const htmlPath = path.join(runDir, 'report.html');
8
- fs.writeFileSync(htmlPath, html, 'utf-8');
9
- Logger.write(`\n Report: file://${htmlPath}\n`);
10
- }
11
- }
12
- // ---------------------------------------------------------------------------
13
- // HTML generation (module-private)
14
- // ---------------------------------------------------------------------------
15
- function escapeHtml(s) {
16
- return s
17
- .replace(/&/g, '&amp;')
18
- .replace(/</g, '&lt;')
19
- .replace(/>/g, '&gt;')
20
- .replace(/"/g, '&quot;')
21
- .replace(/'/g, '&#39;');
22
- }
23
- function formatPercent(val) {
24
- return `${Math.round(val * 100)}%`;
25
- }
26
- function passColorClass(val) {
27
- if (val >= 0.8)
28
- return 'green';
29
- if (val >= 0.5)
30
- return 'amber';
31
- return 'red';
32
- }
33
- function isFunctional(report) {
34
- return report.metrics.withoutSkillScore !== undefined;
35
- }
36
- // ---------------------------------------------------------------------------
37
- // Cards
38
- // ---------------------------------------------------------------------------
39
- function renderCard(label, value, colorClass) {
40
- return `<div class="card"><div class="card-value ${colorClass}">${escapeHtml(value)}</div><div class="card-label">${escapeHtml(label)}</div></div>`;
41
- }
42
- function renderMetricsCards(report) {
43
- const { metrics } = report;
44
- const numTrials = metrics.numTrials ?? 1;
45
- const cards = [];
46
- if (isFunctional(report)) {
47
- const bk = metrics.withoutSkillPassAtK ?? 0;
48
- const tk = metrics.passAtK ?? 0;
49
- const upliftRaw = parseInt(metrics.skillUplift ?? '0', 10);
50
- const upliftClass = upliftRaw > 0 ? 'green' : upliftRaw < 0 ? 'red' : 'amber';
51
- cards.push(renderCard('Without Skill p@1', formatPercent(bk), passColorClass(bk)));
52
- if (numTrials > 1) {
53
- const bn = metrics.withoutSkillPassAtN ?? 0;
54
- cards.push(renderCard(`Without Skill p@${numTrials}`, formatPercent(bn), passColorClass(bn)));
55
- }
56
- cards.push(renderCard('With Skill p@1', formatPercent(tk), passColorClass(tk)));
57
- if (numTrials > 1) {
58
- const tn = metrics.passAtN ?? 0;
59
- cards.push(renderCard(`With Skill p@${numTrials}`, formatPercent(tn), passColorClass(tn)));
60
- }
61
- cards.push(renderCard('Skill Uplift', escapeHtml(metrics.skillUplift ?? '0%'), upliftClass));
62
- }
63
- else {
64
- const k = metrics.passAtK ?? 0;
65
- cards.push(renderCard('pass@1', formatPercent(k), passColorClass(k)));
66
- if (numTrials > 1) {
67
- const n = metrics.passAtN ?? 0;
68
- cards.push(renderCard(`pass@${numTrials}`, formatPercent(n), passColorClass(n)));
69
- }
70
- cards.push(renderCard('Tasks passed', `${metrics.passedCount}/${metrics.totalCount}`, passColorClass(metrics.passedCount / Math.max(metrics.totalCount, 1))));
71
- }
72
- return `<div class="cards">${cards.join('')}</div>`;
73
- }
74
- // ---------------------------------------------------------------------------
75
- // Chart
76
- // ---------------------------------------------------------------------------
77
- function renderChart(report) {
78
- const { results, metrics } = report;
79
- const numTrials = metrics.numTrials ?? 1;
80
- const labels = results.map(r => `Task #${r.taskId}`);
81
- let datasets;
82
- if (isFunctional(report)) {
83
- const withoutSkillData = results.map(r => {
84
- const bt = r.withoutSkillTrials ?? [];
85
- return bt.length === 0 ? 0 : Math.round((bt.filter(t => t.trialPassed).length / bt.length) * 100);
86
- });
87
- const withSkillData = results.map(r => Math.round((r.trials.filter(t => t.trialPassed).length / Math.max(r.trials.length, 1)) * 100));
88
- datasets = [
89
- { label: 'Without Skill p@1', data: withoutSkillData, backgroundColor: '#94a3b8', borderRadius: 4 },
90
- { label: 'With Skill p@1', data: withSkillData, backgroundColor: '#3b82f6', borderRadius: 4 },
91
- ];
92
- }
93
- else {
94
- const passData = results.map(r => Math.round((r.trials.filter(t => t.trialPassed).length / Math.max(r.trials.length, 1)) * 100));
95
- datasets = [{
96
- label: numTrials > 1 ? 'pass@1' : 'Score',
97
- data: passData,
98
- backgroundColor: passData.map(v => v >= 80 ? '#22c55e' : v >= 50 ? '#f59e0b' : '#ef4444'),
99
- borderRadius: 4,
100
- }];
101
- }
102
- const chartData = JSON.stringify({ labels, datasets });
103
- return `
104
- <div class="chart-wrap">
105
- <script type="application/json" id="chart-data">${chartData}</script>
106
- <canvas id="eval-chart"></canvas>
107
- </div>`;
108
- }
109
- // ---------------------------------------------------------------------------
110
- // Trial details
111
- // ---------------------------------------------------------------------------
112
- function renderAssertions(assertions) {
113
- if (assertions.length === 0)
114
- return '<p class="muted">No assertions recorded.</p>';
115
- return assertions.map(a => {
116
- const icon = a.passed ? '✓' : '✗';
117
- const cls = a.passed ? 'assert-pass' : 'assert-fail';
118
- const grader = a.graderType ? `<span class="badge">${escapeHtml(a.graderType)}</span>` : '';
119
- return `<div class="assertion ${cls}">
120
- <span class="assert-icon">${icon}</span>
121
- <div class="assert-body">
122
- <div class="assert-text">${escapeHtml(a.assertion)}${grader}</div>
123
- ${a.reason ? `<div class="assert-reason">${escapeHtml(a.reason)}</div>` : ''}
124
- </div>
125
- </div>`;
126
- }).join('');
127
- }
128
- function renderTrial(trial, prefix) {
129
- const cls = trial.trialPassed ? 'trial-pass' : 'trial-fail';
130
- const badge = trial.trialPassed
131
- ? '<span class="pill green">PASS</span>'
132
- : '<span class="pill red">FAIL</span>';
133
- return `<div class="trial ${cls}">
134
- <div class="trial-header">${escapeHtml(prefix)} Trial ${trial.id} ${badge}</div>
135
- <div class="trial-assertions">${renderAssertions(trial.assertionResults)}</div>
136
- </div>`;
137
- }
138
- function renderTaskDetails(result, isFunctionalEval) {
139
- const sections = [];
140
- if (isFunctionalEval && result.withoutSkillTrials && result.withoutSkillTrials.length > 0) {
141
- sections.push('<div class="trial-group-label">Without Skill</div>');
142
- sections.push(...result.withoutSkillTrials.map(t => renderTrial(t, 'Without Skill')));
143
- sections.push('<div class="trial-group-label">With Skill</div>');
144
- }
145
- sections.push(...result.trials.map(t => renderTrial(t, 'With Skill')));
146
- return `<div class="task-details" id="details-${result.taskId}">${sections.join('')}</div>`;
147
- }
148
- // ---------------------------------------------------------------------------
149
- // Task table
150
- // ---------------------------------------------------------------------------
151
- function renderTaskTable(report) {
152
- const { results, metrics } = report;
153
- const numTrials = metrics.numTrials ?? 1;
154
- const functional = isFunctional(report);
155
- const headerCells = functional
156
- ? ['#', 'Prompt', 'W/o p@1', 'W/ p@1', 'Details']
157
- : numTrials > 1
158
- ? ['#', 'Prompt', 'pass@1', `pass@${numTrials}`, 'Details']
159
- : ['#', 'Prompt', 'Status', 'Details'];
160
- const headerRow = `<tr>${headerCells.map(h => `<th>${escapeHtml(h)}</th>`).join('')}</tr>`;
161
- const rows = results.map(result => {
162
- const prompt = escapeHtml(result.prompt);
163
- const trials = result.trials;
164
- const bt = result.withoutSkillTrials ?? [];
165
- let statCells;
166
- if (functional) {
167
- const bp1 = bt.length ? Math.round((bt.filter(t => t.trialPassed).length / bt.length) * 100) : 0;
168
- const tp1 = trials.length ? Math.round((trials.filter(t => t.trialPassed).length / trials.length) * 100) : 0;
169
- statCells = `<td class="${passColorClass(bp1 / 100)}">${bp1}%</td><td class="${passColorClass(tp1 / 100)}">${tp1}%</td>`;
170
- }
171
- else if (numTrials > 1) {
172
- const p1 = Math.round((trials.filter(t => t.trialPassed).length / Math.max(trials.length, 1)) * 100);
173
- const passed = trials.filter(t => t.trialPassed).length;
174
- const pn = trials.length > 0 ? Math.round((passed / trials.length) * 100) : 0;
175
- statCells = `<td class="${passColorClass(p1 / 100)}">${p1}%</td><td class="${passColorClass(pn / 100)}">${pn}%</td>`;
176
- }
177
- else {
178
- const passed = trials[0]?.trialPassed ?? false;
179
- statCells = `<td class="${passed ? 'green' : 'red'}">${passed ? 'PASS' : 'FAIL'}</td>`;
180
- }
181
- const detailsBtn = `<button class="details-btn" data-target="details-${result.taskId}">▶</button>`;
182
- const detailsRow = `<tr class="details-row"><td colspan="${headerCells.length}">${renderTaskDetails(result, functional)}</td></tr>`;
183
- return `<tr><td>${result.taskId}</td><td class="prompt-cell">${prompt}</td>${statCells}<td>${detailsBtn}</td></tr>${detailsRow}`;
184
- }).join('');
185
- return `<div class="table-wrap"><table><thead>${headerRow}</thead><tbody>${rows}</tbody></table></div>`;
186
- }
187
- // ---------------------------------------------------------------------------
188
- // Full document
189
- // ---------------------------------------------------------------------------
190
- export function generateHtml(report) {
191
- const { skill_name, agent, timestamp, metrics } = report;
192
- const functional = isFunctional(report);
193
- const evalType = functional ? 'Functional' : 'Trigger';
194
- const overallScore = metrics.passAtK ?? 0;
195
- const statusClass = passColorClass(overallScore);
196
- const formattedDate = new Date(timestamp).toLocaleString();
197
- return `<!DOCTYPE html>
198
- <html lang="en">
199
- <head>
200
- <meta charset="UTF-8">
201
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
202
- <title>Skill Eval — ${escapeHtml(skill_name)}</title>
203
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
204
- <style>
205
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
206
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8fafc; color: #1e293b; font-size: 14px; }
207
- a { color: #3b82f6; }
208
-
209
- /* Layout */
210
- .container { max-width: 960px; margin: 0 auto; padding: 24px 16px 48px; }
211
-
212
- /* Header */
213
- .header { background: #1e293b; color: #f1f5f9; padding: 24px 28px; border-radius: 10px; margin-bottom: 24px; }
214
- .header h1 { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
215
- .header-meta { display: flex; gap: 24px; flex-wrap: wrap; font-size: 13px; color: #94a3b8; }
216
- .header-meta span b { color: #e2e8f0; }
217
- .status-bar { height: 4px; border-radius: 2px; margin-top: 16px; }
218
- .status-bar.green { background: #22c55e; }
219
- .status-bar.amber { background: #f59e0b; }
220
- .status-bar.red { background: #ef4444; }
221
-
222
- /* Cards */
223
- .cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 24px; }
224
- .card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px 20px; min-width: 120px; flex: 1; }
225
- .card-value { font-size: 28px; font-weight: 700; line-height: 1; margin-bottom: 4px; }
226
- .card-label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
227
-
228
- /* Color utilities */
229
- .green { color: #16a34a; }
230
- .amber { color: #d97706; }
231
- .red { color: #dc2626; }
232
-
233
- /* Section */
234
- .section { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 20px; overflow: hidden; }
235
- .section-title { font-weight: 600; font-size: 13px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 12px 16px; border-bottom: 1px solid #f1f5f9; }
236
-
237
- /* Chart */
238
- .chart-wrap { padding: 16px; }
239
- #eval-chart { max-height: 300px; }
240
-
241
- /* Table */
242
- .table-wrap { overflow-x: auto; }
243
- table { width: 100%; border-collapse: collapse; }
244
- th { background: #f8fafc; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; padding: 10px 12px; text-align: left; border-bottom: 1px solid #e2e8f0; }
245
- td { padding: 10px 12px; border-bottom: 1px solid #f1f5f9; vertical-align: top; }
246
- tr:last-child td { border-bottom: none; }
247
- .prompt-cell { max-width: 360px; word-break: break-word; color: #334155; }
248
-
249
- /* Details */
250
- .details-btn { background: none; border: 1px solid #e2e8f0; border-radius: 4px; cursor: pointer; padding: 2px 8px; font-size: 11px; color: #64748b; transition: background 0.15s; }
251
- .details-btn:hover { background: #f1f5f9; }
252
- .details-btn.open { color: #3b82f6; border-color: #3b82f6; }
253
- .details-row > td { padding: 0; background: #f8fafc; }
254
- .task-details { display: none; padding: 12px 16px; }
255
- .task-details.visible { display: block; }
256
- .trial-group-label { font-size: 11px; font-weight: 600; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.05em; margin: 8px 0 4px; }
257
-
258
- /* Trials */
259
- .trial { border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px; overflow: hidden; }
260
- .trial-header { display: flex; align-items: center; gap: 8px; padding: 8px 12px; font-weight: 500; font-size: 13px; background: #f8fafc; }
261
- .trial-pass .trial-header { border-left: 3px solid #22c55e; }
262
- .trial-fail .trial-header { border-left: 3px solid #ef4444; }
263
- .trial-assertions { padding: 8px 12px; display: flex; flex-direction: column; gap: 6px; }
264
-
265
- /* Pills */
266
- .pill { display: inline-block; font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 99px; letter-spacing: 0.05em; }
267
- .pill.green { background: #dcfce7; color: #15803d; }
268
- .pill.red { background: #fee2e2; color: #b91c1c; }
269
-
270
- /* Badge */
271
- .badge { display: inline-block; font-size: 10px; font-weight: 500; padding: 1px 6px; border-radius: 4px; background: #e2e8f0; color: #475569; margin-left: 6px; vertical-align: middle; }
272
-
273
- /* Assertions */
274
- .assertion { display: flex; gap: 8px; }
275
- .assert-icon { flex-shrink: 0; font-size: 14px; margin-top: 1px; }
276
- .assert-pass .assert-icon { color: #16a34a; }
277
- .assert-fail .assert-icon { color: #dc2626; }
278
- .assert-body { flex: 1; min-width: 0; }
279
- .assert-text { font-size: 13px; color: #1e293b; word-break: break-word; }
280
- .assert-reason { font-size: 12px; color: #64748b; margin-top: 2px; word-break: break-word; }
281
- .muted { color: #94a3b8; font-size: 13px; }
282
- </style>
283
- </head>
284
- <body>
285
- <div class="container">
286
-
287
- <!-- Header -->
288
- <div class="header">
289
- <h1>${escapeHtml(skill_name)}</h1>
290
- <div class="header-meta">
291
- <span><b>Agent</b> ${escapeHtml(agent)}</span>
292
- <span><b>Type</b> ${evalType}</span>
293
- <span><b>Date</b> ${escapeHtml(formattedDate)}</span>
294
- <span><b>Score</b> ${escapeHtml(metrics.withSkillScore)}</span>
295
- </div>
296
- <div class="status-bar ${statusClass}"></div>
297
- </div>
298
-
299
- <!-- Metric Cards -->
300
- ${renderMetricsCards(report)}
301
-
302
- <!-- Chart -->
303
- <div class="section">
304
- <div class="section-title">Pass rate by task</div>
305
- ${renderChart(report)}
306
- </div>
307
-
308
- <!-- Task Table -->
309
- <div class="section">
310
- <div class="section-title">Task results</div>
311
- ${renderTaskTable(report)}
312
- </div>
313
-
314
- </div>
315
- <script>
316
- (function () {
317
- // Chart
318
- const rawData = document.getElementById('chart-data');
319
- if (rawData) {
320
- const data = JSON.parse(rawData.textContent || '{}');
321
- const ctx = document.getElementById('eval-chart');
322
- if (ctx) {
323
- new Chart(ctx, {
324
- type: 'bar',
325
- data: data,
326
- options: {
327
- indexAxis: 'y',
328
- responsive: true,
329
- plugins: { legend: { display: ${functional ? 'true' : 'false'} } },
330
- scales: {
331
- x: { min: 0, max: 100, ticks: { callback: v => v + '%' }, grid: { color: '#f1f5f9' } },
332
- y: { grid: { display: false } }
333
- }
334
- }
335
- });
336
- }
337
- }
338
-
339
- // Accordion
340
- document.querySelectorAll('.details-btn').forEach(function (btn) {
341
- btn.addEventListener('click', function () {
342
- const target = document.getElementById(btn.getAttribute('data-target'));
343
- if (!target) return;
344
- const isOpen = target.classList.contains('visible');
345
- target.classList.toggle('visible', !isOpen);
346
- btn.classList.toggle('open', !isOpen);
347
- btn.textContent = isOpen ? '▶' : '▼';
348
- });
349
- });
350
- }());
351
- </script>
352
- </body>
353
- </html>`;
354
- }
@@ -1,9 +0,0 @@
1
- import { HtmlReporter } from './html-reporter.js';
2
- import { JsonReporter } from './json-reporter.js';
3
- export { HtmlReporter } from './html-reporter.js';
4
- export { JsonReporter } from './json-reporter.js';
5
- export function createReporter(format) {
6
- if (format === 'html')
7
- return new HtmlReporter();
8
- return new JsonReporter();
9
- }
@@ -1,7 +0,0 @@
1
- import path from 'path';
2
- import { Logger } from '../../utils/logger.js';
3
- export class JsonReporter {
4
- generate(_report, runDir) {
5
- Logger.write(`\n Report: file://${path.join(runDir, 'summary.json')}\n`);
6
- }
7
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,75 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HeadlessRunner = void 0;
4
- const child_process_1 = require("child_process");
5
- class HeadlessRunner {
6
- agent;
7
- constructor(agent) {
8
- this.agent = agent;
9
- }
10
- /**
11
- * Runs the prompt through a headless isolated gemini instance in auto_edit mode.
12
- * Auto Edit mode automatically allows tools meant for modifying files, falling back interactively
13
- * only for severe system-level actions (which we assume tests shouldn't invoke, or will fail if they do in non-interactive).
14
- * @param prompt The evaluation prompt text
15
- * @returns Parsed JSON output from Gemini
16
- */
17
- runPrompt(prompt) {
18
- if (this.agent !== 'gemini-cli') {
19
- console.error(`\n[Runner] Agent '${this.agent}' is not supported yet.`);
20
- return null;
21
- }
22
- // Escaping the prompt just in case. spawnSync handles formatting but better safe.
23
- try {
24
- const child = (0, child_process_1.spawnSync)('gemini', [
25
- '-p', prompt,
26
- '-o', 'json',
27
- '--approval-mode', 'auto_edit'
28
- ], {
29
- encoding: 'utf-8',
30
- // Not passing default stdio because we need to parse stdout
31
- // silently without printing all the tool interactions to the user terminal.
32
- stdio: ['ignore', 'pipe', 'pipe']
33
- });
34
- if (child.error) {
35
- console.error(`\n[Runner] Failed to start gemini CLI. Error: ${child.error.message}`);
36
- return null;
37
- }
38
- // If exit code is not 0, there was likely a crash or unhanded exception in gemini cli.
39
- if (child.status !== 0) {
40
- console.error(`\n[Runner] Gemini CLI exited with status ${child.status}`);
41
- if (child.stderr) {
42
- console.error(`[Runner Stderr] ${child.stderr.trim()}`);
43
- }
44
- }
45
- const rawOutput = child.stdout;
46
- if (!rawOutput || rawOutput.trim() === '') {
47
- console.error(`\n[Runner] Received empty output from Gemini CLI. Cannot evaluate.`);
48
- return null;
49
- }
50
- // We need to safely extract the JSON part because Gemini CLI might emit
51
- // non-JSON warnings like "MCP issues detected. Run /mcp list for status." to stdout.
52
- // We'll search for the first '{' to begin our JSON payload.
53
- let jsonPart = rawOutput.trim();
54
- const firstBraceIndex = jsonPart.indexOf('{');
55
- if (firstBraceIndex > 0) {
56
- jsonPart = jsonPart.substring(firstBraceIndex);
57
- }
58
- try {
59
- const parsed = JSON.parse(jsonPart);
60
- return parsed;
61
- }
62
- catch (parseError) {
63
- console.error(`\n[Runner] Failed to parse JSON output: ${parseError}`);
64
- // Dump first 300 chars of raw to debug
65
- console.error(`[Runner Output Preview] ${rawOutput.substring(0, 300)}`);
66
- return null;
67
- }
68
- }
69
- catch (e) {
70
- console.error(`\n[Runner] Unexpected error running process: ${e}`);
71
- return null;
72
- }
73
- }
74
- }
75
- exports.HeadlessRunner = HeadlessRunner;
@@ -1,18 +0,0 @@
1
- import { GeminiCliRunner } from './gemini-cli.runner.js';
2
- export class RunnerFactory {
3
- /**
4
- * Factory method to create an agent runner based on the agent name.
5
- * Add new agent implementations here.
6
- * @param agent The name of the agent to run
7
- * @returns An implementation of AgentRunner
8
- * @throws Error if the agent is not supported
9
- */
10
- static create(agent) {
11
- switch (agent) {
12
- case 'gemini-cli':
13
- return new GeminiCliRunner();
14
- default:
15
- throw new Error(`Agent '${agent}' is not supported yet.`);
16
- }
17
- }
18
- }
@@ -1,138 +0,0 @@
1
- import * as fs from 'fs';
2
- import child_process from 'child_process';
3
- import { Logger } from '../../utils/logger.js';
4
- export class GeminiCliRunner {
5
- /**
6
- * Runs the prompt through an isolated gemini instance.
7
- * Default mode is headless using --approval-mode auto_edit.
8
- *
9
- * @param prompt The evaluation prompt text
10
- * @param cwd Optional execution directory
11
- * @param onLog Callback to receive real-time logs (from stderr)
12
- * @param logPath Optional file path to save raw execution logs
13
- * @returns Raw output from Gemini
14
- */
15
- async runPrompt(prompt, cwd, onLog, logPath, extraArgs = []) {
16
- return new Promise((resolve) => {
17
- let stdout = '';
18
- let stderr = '';
19
- let resolved = false;
20
- // Use -p and --approval-mode auto_edit for headless mode.
21
- const args = ['-p', prompt, '--approval-mode', 'auto_edit', ...extraArgs];
22
- const spawnOptions = {
23
- cwd: cwd,
24
- env: { ...process.env, FORCE_COLOR: '1' }
25
- };
26
- const child = child_process.spawn('gemini', args, spawnOptions);
27
- // Setup log stream if path is provided
28
- let logStream = null;
29
- let logStreamDone = true; // Default to true if no logPath
30
- if (logPath) {
31
- try {
32
- logStream = fs.createWriteStream(logPath, { flags: 'a' });
33
- logStreamDone = false;
34
- logStream.on('finish', () => {
35
- logStreamDone = true;
36
- checkAllDone();
37
- });
38
- logStream.write(`--- Gemini CLI Execution Start: ${new Date().toISOString()} ---\n`);
39
- logStream.write(`Command: gemini ${args.join(' ')}\n\n`);
40
- }
41
- catch (err) {
42
- Logger.warn(`Failed to create log file at ${logPath} — verbose output will not be saved. Continuing. Reason: ${err}`);
43
- logStreamDone = true;
44
- }
45
- }
46
- // Safety timeout: 5 minutes (300,000 ms)
47
- const timeout = setTimeout(() => {
48
- if (!resolved) {
49
- resolved = true;
50
- child.kill('SIGKILL');
51
- if (logStream) {
52
- logStream.write('\n\n--- Gemini CLI process timed out ---\n');
53
- logStream.end();
54
- }
55
- Logger.error('\nGemini CLI process timed out after 5 minutes.');
56
- resolve({ error: 'Process timeout exceeded (5 minutes)', raw_output: stderr });
57
- }
58
- }, 300000);
59
- // Track completion of streams
60
- let stdoutDone = false;
61
- let stderrDone = false;
62
- let processDone = false;
63
- function checkAllDone() {
64
- if (stdoutDone && stderrDone && processDone && logStreamDone && !resolved) {
65
- resolved = true;
66
- clearTimeout(timeout);
67
- if (!stdout || stdout.trim() === '') {
68
- return resolve({ error: 'Empty output from Gemini CLI', raw_output: stderr });
69
- }
70
- return resolve({
71
- response: stdout.trim(),
72
- raw_output: `${stdout}\n--- STDERR ---\n${stderr}`
73
- });
74
- }
75
- }
76
- if (child.stdout) {
77
- child.stdout.on('data', (data) => {
78
- const chunk = data.toString();
79
- stdout += chunk;
80
- if (logStream)
81
- logStream.write(chunk);
82
- });
83
- child.stdout.on('end', () => {
84
- stdoutDone = true;
85
- checkAllDone();
86
- });
87
- }
88
- else {
89
- stdoutDone = true;
90
- }
91
- if (child.stderr) {
92
- child.stderr.on('data', (data) => {
93
- const chunk = data.toString();
94
- stderr += chunk;
95
- if (onLog) {
96
- const lines = chunk.split('\n').filter((l) => l.trim() !== '');
97
- if (lines.length > 0) {
98
- onLog(lines[lines.length - 1]);
99
- }
100
- }
101
- });
102
- child.stderr.on('end', () => {
103
- stderrDone = true;
104
- checkAllDone();
105
- });
106
- }
107
- else {
108
- stderrDone = true;
109
- }
110
- child.on('error', (err) => {
111
- if (!resolved) {
112
- resolved = true;
113
- clearTimeout(timeout);
114
- if (logStream) {
115
- logStream.write(`\n\n--- Error starting Gemini CLI: ${err.message} ---\n`);
116
- logStream.end();
117
- }
118
- Logger.error(`Failed to start gemini CLI. Error: ${err.message}`);
119
- resolve(null);
120
- }
121
- });
122
- child.on('close', (code) => {
123
- if (logStream) {
124
- logStream.write(`\n\n--- Gemini CLI exited with status ${code} ---\n`);
125
- logStream.end();
126
- }
127
- if (code !== 0 && !resolved) {
128
- Logger.error(`Gemini CLI exited with status ${code}`);
129
- if (stderr) {
130
- Logger.debug(`Gemini CLI Stderr: ${stderr.trim()}`);
131
- }
132
- }
133
- processDone = true;
134
- checkAllDone();
135
- });
136
- });
137
- }
138
- }
@@ -1,3 +0,0 @@
1
- export * from './runner.interface.js';
2
- export * from './gemini-cli.runner.js';
3
- export * from './factory.js';
@@ -1 +0,0 @@
1
- export {};