@fede0089/skill-eval 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/dist/commands/functional.js +225 -0
  4. package/dist/commands/rate.js +20 -0
  5. package/dist/commands/show.js +43 -0
  6. package/dist/commands/trigger.js +154 -0
  7. package/dist/commands/view.js +30 -0
  8. package/dist/core/agent-pool.js +40 -0
  9. package/dist/core/config.js +58 -0
  10. package/dist/core/environment.js +83 -0
  11. package/dist/core/errors.js +29 -0
  12. package/dist/core/eval-runner.js +306 -0
  13. package/dist/core/evaluator.js +242 -0
  14. package/dist/core/preflight.js +36 -0
  15. package/dist/core/reporters/html-reporter.js +354 -0
  16. package/dist/core/reporters/index.js +9 -0
  17. package/dist/core/reporters/json-reporter.js +7 -0
  18. package/dist/core/reporters/reporter.js +1 -0
  19. package/dist/core/runner.js +75 -0
  20. package/dist/core/runners/factory.js +18 -0
  21. package/dist/core/runners/gemini-cli.runner.js +138 -0
  22. package/dist/core/runners/index.js +3 -0
  23. package/dist/core/runners/runner.interface.js +1 -0
  24. package/dist/core/statistics.js +79 -0
  25. package/dist/core/trial-utils.js +58 -0
  26. package/dist/index.js +80 -0
  27. package/dist/reporters/html-reporter.js +384 -0
  28. package/dist/reporters/index.js +2 -0
  29. package/dist/reporters/json-reporter.js +10 -0
  30. package/dist/reporters/reporter.js +1 -0
  31. package/dist/runners/gemini-cli/index.js +1 -0
  32. package/dist/runners/gemini-cli/runner.js +231 -0
  33. package/dist/runners/index.js +2 -0
  34. package/dist/runners/registry.js +16 -0
  35. package/dist/runners/runner.interface.js +1 -0
  36. package/dist/types/index.js +1 -0
  37. package/dist/utils/eval-loader.js +66 -0
  38. package/dist/utils/exec.js +9 -0
  39. package/dist/utils/logger.js +80 -0
  40. package/dist/utils/ndjson.js +85 -0
  41. package/dist/utils/table-renderer.js +229 -0
  42. package/dist/utils/ui.js +166 -0
  43. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Federico Mete
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # skill-eval
2
+
3
+ A CLI tool for evaluating Agent Skills locally. Tests whether your skill triggers reliably and produces the right output, using an LLM as the judge.
4
+
5
+ ## Why skill evals?
6
+
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
+
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.
10
+
11
+ ## How it works
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.
14
+
15
+ ```
16
+ eval prompt
17
+
18
+ ┌───────▼───────┐
19
+ │ skill-eval │
20
+ └───────┬───────┘
21
+
22
+ ┌───────────┴───────────┐
23
+ ─ with skill ─ ─ baseline ─
24
+ ┌──────┴──────┐ ┌─────┴──────┐
25
+ agent 1 agent 2 agent 3 agent 4
26
+ │ │ │ │
27
+ judge judge judge judge
28
+ └──────┬──────┘ └──────┬──────┘
29
+ └──────────┬────────────┘
30
+
31
+ pass@k
32
+ ```
33
+
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
+ ## Quick Start
37
+
38
+ ```sh
39
+ git clone <this-repo>
40
+ cd skill-eval
41
+ npm install
42
+ npm run build
43
+ npm link
44
+ ```
45
+
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
+ ## Commands
51
+
52
+ ```sh
53
+ # Checks that the skill is triggered (invoked) for each prompt
54
+ skill-eval trigger --workspace <path> --skill <path> [options] [agent]
55
+
56
+ # Checks that the skill produces correct output, measured against a baseline
57
+ skill-eval functional --workspace <path> --skill <path> [options] [agent]
58
+ ```
59
+
60
+ ### Options
61
+
62
+ | Flag | Required | Default | Description |
63
+ |------|----------|---------|-------------|
64
+ | `--workspace <path>` | yes | — | Path to the repo the agent will run in |
65
+ | `--skill <path>` | yes | — | Path to the skill directory |
66
+ | `--agents <number>` | no | `4` | Number of parallel agent processes |
67
+ | `--trials <number>` | no | `3` | Trials per task (for pass@k) |
68
+ | `--timeout <seconds>` | no | none | Kill the agent after this many seconds |
69
+ | `--eval-id <id>` | no | all | Run only the eval with this numeric ID |
70
+ | `-v, --debug` | no | `false` | Enable verbose debug logging |
71
+ | `[agent]` | no | `gemini-cli` | Agent backend to use |
72
+
73
+ ### Skill directory structure
74
+
75
+ ```
76
+ my-skill/
77
+ ├── SKILL.md # skill definition (required)
78
+ └── evals/ # evaluation suite (required)
79
+ ├── my-evals.json # one or more eval files (*.json)
80
+ └── config/ # runner configuration (optional but often needed)
81
+ └── gemini-cli/ # runner-specific config folder
82
+ └── settings.json # copied to <worktree>/.gemini/ before each trial
83
+ ```
84
+
85
+ All `.json` files in `evals/` are loaded and merged into a single suite — you can split them by feature or regression category.
86
+
87
+ **Trigger eval** — `id` must be a unique integer across all eval files:
88
+ ```json
89
+ {
90
+ "skill_name": "my-skill",
91
+ "evals": [
92
+ { "id": 1, "prompt": "Do the thing that my skill handles" }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ **Functional eval** — add `expectations` for the LLM judge to evaluate:
98
+ ```json
99
+ {
100
+ "skill_name": "my-skill",
101
+ "evals": [
102
+ {
103
+ "id": 1,
104
+ "prompt": "Create a file called hello.txt containing the word 'world'",
105
+ "expectations": [
106
+ "A file named hello.txt was created",
107
+ "The file contains the text 'world'"
108
+ ]
109
+ }
110
+ ]
111
+ }
112
+ ```
113
+
114
+ ## Permissions
115
+
116
+ **This is the most common cause of eval failures.**
117
+
118
+ skill-eval runs the agent headlessly — stdin is closed, there is no terminal. If the agent encounters a tool that requires interactive approval, it will either fail immediately or hang until the trial timeout kills it.
119
+
120
+ The runner already uses `--approval-mode auto_edit`, which auto-approves standard file operations (create, edit, delete). But if your skill needs to run shell commands, read environment variables, make network calls, or use any other tool category — those still require explicit permission.
121
+
122
+ **Solution:** place a config file inside your skill at `evals/config/<runner>/`. Before every trial, skill-eval automatically copies that directory into the agent's config location inside the isolated worktree:
123
+
124
+ ```
125
+ evals/config/gemini-cli/ → <worktree>/.gemini/
126
+ ```
127
+
128
+ Use this to ship both settings and policies alongside your evals. For Gemini CLI, for example, you can use `settings.json` to configure tool permissions and approval policies so that every tool your skill relies on runs without prompting:
129
+
130
+ ```json
131
+ {
132
+ "telemetry": { "enabled": false }
133
+ }
134
+ ```
135
+
136
+ Refer to your runner's documentation for the full list of available settings and policy keys.
137
+
138
+ > This config only applies inside the temporary worktree created for each trial. Your real workspace config is never touched.
139
+
140
+ ## Try it out
141
+
142
+ 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:
143
+
144
+ ```sh
145
+ npm run test:trigger # trigger evaluation against mock-skill
146
+ npm run test:functional # functional evaluation against mock-skill
147
+ ```
148
+
149
+ Results are saved to `.project-skill-evals/runs/<timestamp>/` with logs, raw eval JSONs, and an HTML report.
150
+
151
+ ## Extending
152
+
153
+ ### Adding a new agent runner
154
+
155
+ 1. Create `src/runners/<your-agent>/runner.ts` implementing the `AgentRunner` interface (see `src/runners/runner.interface.ts`).
156
+ 2. Export it from `src/runners/<your-agent>/index.ts`.
157
+ 3. Register it in `src/runners/registry.ts`:
158
+ ```ts
159
+ '<your-agent>': { Runner: YourRunner, binary: '<cli-binary-name>' },
160
+ ```
161
+
162
+ The factory, preflight check, and CLI all pick it up automatically.
163
+
164
+ > 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.
165
+
166
+ ### Adding a new report format
167
+
168
+ 1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
169
+ 2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
170
+ 3. Add the format string to `ReportFormat` in `src/types/index.ts`.
@@ -0,0 +1,225 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { EvalEnvironment } from '../core/environment.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import * as evalLoader from '../utils/eval-loader.js';
6
+ import { ListrEvalUI } from '../utils/ui.js';
7
+ import { EvalRunner } from '../core/eval-runner.js';
8
+ import { AgentPool } from '../core/agent-pool.js';
9
+ import { aggregatePassAtK, aggregateAssertionPassRate, aggregateTokenStats, aggregateDurationStats } from '../core/statistics.js';
10
+ import { preflight } from '../core/preflight.js';
11
+ import { withRetry } from '../core/trial-utils.js';
12
+ import { renderFunctionalTable, renderRunHeader } from '../utils/table-renderer.js';
13
+ import { JsonReporter } from '../reporters/index.js';
14
+ export async function functionalCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId) {
15
+ if (!injectedSuite)
16
+ preflight(agent, workspace, skillPath);
17
+ const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
18
+ if (evalId !== undefined) {
19
+ suite.tasks = suite.tasks.filter(t => t.id === evalId);
20
+ if (suite.tasks.length === 0) {
21
+ console.error(`No eval found with id ${evalId}`);
22
+ process.exit(1);
23
+ }
24
+ }
25
+ const { skill_name, tasks } = suite;
26
+ const env = new EvalEnvironment({ workspace });
27
+ await env.setup();
28
+ // Ensure worktrees are cleaned up even when the process is interrupted (Ctrl+C).
29
+ const cleanup = () => { env.teardown().finally(() => process.exit(1)); };
30
+ process.once('SIGINT', cleanup);
31
+ process.once('SIGTERM', cleanup);
32
+ // Setup Artifacts Directory (Always create, even if not in debug mode, for 'show' command)
33
+ const debug = !!process.env.DEBUG;
34
+ const startTime = new Date();
35
+ const timestamp = startTime.toISOString().replace(/[:.]/g, '-');
36
+ const runDir = path.resolve(workspace, '.project-skill-evals', 'runs', timestamp);
37
+ fs.mkdirSync(runDir, { recursive: true });
38
+ const taskResults = [];
39
+ let withSkillTasksAllPassedCount = 0;
40
+ 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
+ const pool = new AgentPool(maxAgents);
64
+ const ui = new ListrEvalUI();
65
+ // Barrier chain: each prompt waits until the previous prompt's trials have all acquired slots.
66
+ let barrier = Promise.resolve();
67
+ // Subtask labels: Without Skill 1..N then With Skill 1..N for each prompt
68
+ const subtaskLabels = [
69
+ ...Array.from({ length: numTrials }, (_, i) => `Without Skill ${i + 1}`),
70
+ ...Array.from({ length: numTrials }, (_, i) => `With Skill ${i + 1}`)
71
+ ];
72
+ try {
73
+ renderRunHeader({ command: 'functional', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
74
+ Logger.write(`──────────────────────────────────────────────────\n`);
75
+ for (let i = 0; i < tasks.length; i++) {
76
+ const task = tasks[i];
77
+ const promptSnippet = `${task.prompt.substring(0, 50)}${task.prompt.length > 50 ? '...' : ''}`;
78
+ const taskLabel = `${promptSnippet} (#${task.id})`;
79
+ const thisBarrier = barrier;
80
+ let resolveBarrier;
81
+ barrier = new Promise(r => { resolveBarrier = r; });
82
+ ui.addTask({
83
+ id: task.id,
84
+ title: taskLabel,
85
+ subtaskLabels,
86
+ task: async (uiCtx, multi) => {
87
+ await thisBarrier;
88
+ // ── Without Skill trials (subtask IDs 1..numTrials) ──────────────
89
+ const baselineTrialPromises = [];
90
+ for (let idx = 0; idx < numTrials; idx++) {
91
+ const trialId = idx + 1;
92
+ const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
93
+ const release = await pool.acquire();
94
+ const p = withRetry((attempt) => withoutSkillRunner.runFunctionalTask(task, i, trialId, trialCtx, attempt)
95
+ .catch((error) => ({
96
+ id: trialId,
97
+ transcript: { error: error instanceof Error ? error.message : String(error) },
98
+ assertionResults: [{ assertion: 'Without Skill Execution', passed: false, reason: String(error) }],
99
+ trialPassed: false,
100
+ isError: true
101
+ })), 2, 1000, (nextAttempt, lastTrial) => {
102
+ const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
103
+ trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
104
+ }).then(trial => {
105
+ if (multi) {
106
+ const reason = trial.assertionResults.find(r => !r.passed)?.reason;
107
+ const passedCount = trial.assertionResults.filter(r => r.passed).length;
108
+ const totalCount = trial.assertionResults.length;
109
+ multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
110
+ }
111
+ return trial;
112
+ }).finally(release);
113
+ baselineTrialPromises.push(p);
114
+ }
115
+ // ── With Skill trials (subtask IDs numTrials+1..2*numTrials) ─────
116
+ const withSkillTrialPromises = [];
117
+ for (let idx = 0; idx < numTrials; idx++) {
118
+ const subtaskId = numTrials + idx + 1; // Listr subtask position
119
+ const runnerTrialId = idx + 1; // 1-based trial number within WI pass
120
+ const trialCtx = multi?.getTrialCtx(subtaskId) ?? uiCtx;
121
+ const release = await pool.acquire();
122
+ const p = withRetry((attempt) => withSkillRunner.runFunctionalTask(task, i, runnerTrialId, trialCtx, attempt)
123
+ .catch((error) => ({
124
+ id: runnerTrialId,
125
+ transcript: { error: error instanceof Error ? error.message : String(error) },
126
+ assertionResults: [{ assertion: 'With Skill Execution', passed: false, reason: String(error) }],
127
+ trialPassed: false,
128
+ isError: true
129
+ })), 2, 1000, (nextAttempt, lastTrial) => {
130
+ const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
131
+ trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
132
+ }).then(trial => {
133
+ if (multi) {
134
+ const reason = trial.assertionResults.find(r => !r.passed)?.reason;
135
+ const passedCount = trial.assertionResults.filter(r => r.passed).length;
136
+ const totalCount = trial.assertionResults.length;
137
+ multi.markTrialComplete(subtaskId, trial.trialPassed, reason, trial.isError, passedCount, totalCount);
138
+ }
139
+ return trial;
140
+ }).finally(release);
141
+ withSkillTrialPromises.push(p);
142
+ }
143
+ // All WO + WI slots for this prompt have been acquired — signal the next prompt.
144
+ resolveBarrier();
145
+ const [woTrials, wiTrials] = await Promise.all([
146
+ Promise.all(baselineTrialPromises),
147
+ Promise.all(withSkillTrialPromises)
148
+ ]);
149
+ withoutSkillTrialsByTask.set(task.id, woTrials);
150
+ const woPassedCount = woTrials.filter(t => t.trialPassed).length;
151
+ if (woPassedCount === woTrials.length)
152
+ baselineTasksAllPassedCount++;
153
+ const wiPassedCount = wiTrials.filter(t => t.trialPassed).length;
154
+ const score = wiTrials.length > 0 ? wiPassedCount / wiTrials.length : 0;
155
+ const withoutSkillTrials = withoutSkillTrialsByTask.get(task.id) ?? [];
156
+ const taskResult = {
157
+ taskId: task.id,
158
+ prompt: task.prompt,
159
+ score,
160
+ trials: wiTrials.map(t => ({ ...t, transcript: undefined })),
161
+ withoutSkillTrials: withoutSkillTrials.map(t => ({ ...t, transcript: undefined }))
162
+ };
163
+ taskResults.push(taskResult);
164
+ if (wiPassedCount === wiTrials.length) {
165
+ withSkillTasksAllPassedCount++;
166
+ }
167
+ else if (!multi) {
168
+ const failureReason = wiTrials.find(t => !t.trialPassed)?.assertionResults.find(r => !r.passed)?.reason || 'With Skill failed';
169
+ throw new Error(failureReason);
170
+ }
171
+ }
172
+ });
173
+ }
174
+ await ui.run(tasks.length);
175
+ // ==== REPORTING ====
176
+ const { passAtK } = aggregatePassAtK(taskResults, numTrials, r => r.trials);
177
+ const { passAtK: withoutSkillPassAtK } = aggregatePassAtK(taskResults, numTrials, r => r.withoutSkillTrials ?? []);
178
+ const assertionPassRate = aggregateAssertionPassRate(taskResults, r => r.trials);
179
+ const withoutSkillAssertionPassRate = aggregateAssertionPassRate(taskResults, r => r.withoutSkillTrials ?? []);
180
+ const withSkillPercentage = Math.round(assertionPassRate * 100);
181
+ const withoutSkillPercentage = Math.round(withoutSkillAssertionPassRate * 100);
182
+ const skillUplift = withSkillPercentage - withoutSkillPercentage;
183
+ const withSkillTokenStats = aggregateTokenStats(taskResults.flatMap(r => r.trials)) ?? undefined;
184
+ const withoutSkillTokenStats = aggregateTokenStats(taskResults.flatMap(r => r.withoutSkillTrials ?? [])) ?? undefined;
185
+ const withSkillDurationStats = aggregateDurationStats(taskResults.flatMap(r => r.trials)) ?? undefined;
186
+ const withoutSkillDurationStats = aggregateDurationStats(taskResults.flatMap(r => r.withoutSkillTrials ?? [])) ?? undefined;
187
+ const report = {
188
+ timestamp: startTime.toISOString(),
189
+ skill_name,
190
+ agent,
191
+ metrics: {
192
+ withSkillScore: `${withSkillPercentage}%`,
193
+ withoutSkillScore: `${withoutSkillPercentage}%`,
194
+ skillUplift: `${skillUplift > 0 ? '+' : ''}${skillUplift}%`,
195
+ passedCount: withSkillTasksAllPassedCount,
196
+ totalCount: tasks.length,
197
+ numTrials,
198
+ passAtK: Math.round(passAtK * 1000) / 1000,
199
+ withoutSkillPassAtK: Math.round(withoutSkillPassAtK * 1000) / 1000,
200
+ assertionPassRate: Math.round(assertionPassRate * 1000) / 1000,
201
+ withoutSkillAssertionPassRate: Math.round(withoutSkillAssertionPassRate * 1000) / 1000,
202
+ tokenStats: (withSkillTokenStats || withoutSkillTokenStats)
203
+ ? { withSkill: withSkillTokenStats, withoutSkill: withoutSkillTokenStats }
204
+ : undefined,
205
+ durationStats: (withSkillDurationStats || withoutSkillDurationStats)
206
+ ? { withSkill: withSkillDurationStats, withoutSkill: withoutSkillDurationStats }
207
+ : undefined
208
+ },
209
+ results: taskResults
210
+ };
211
+ Logger.write(`\nEVALUATION SUMMARY\n`);
212
+ Logger.write(`──────────────────────────────────────────────────\n`);
213
+ renderFunctionalTable(report);
214
+ const upliftSign = skillUplift > 0 ? '+' : '';
215
+ Logger.write(`\n Skill Uplift: ${upliftSign}${skillUplift}%\n`);
216
+ Logger.write('\n');
217
+ new JsonReporter().generate(report, runDir);
218
+ reporter.generate(report, runDir);
219
+ }
220
+ finally {
221
+ process.off('SIGINT', cleanup);
222
+ process.off('SIGTERM', cleanup);
223
+ await env.teardown();
224
+ }
225
+ }
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,43 @@
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
+ }
@@ -0,0 +1,154 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { EvalEnvironment } from '../core/environment.js';
4
+ import { Logger } from '../utils/logger.js';
5
+ import * as evalLoader from '../utils/eval-loader.js';
6
+ import { ListrEvalUI } from '../utils/ui.js';
7
+ import { EvalRunner } from '../core/eval-runner.js';
8
+ import { AgentPool } from '../core/agent-pool.js';
9
+ import { aggregatePassAtK, aggregateTokenStats, aggregateDurationStats } from '../core/statistics.js';
10
+ import { preflight } from '../core/preflight.js';
11
+ import { withRetry } from '../core/trial-utils.js';
12
+ import { renderTriggerTable, renderRunHeader } from '../utils/table-renderer.js';
13
+ import { JsonReporter } from '../reporters/index.js';
14
+ export async function triggerCommand(agent, workspace, skillPath, maxAgents = 4, injectedSuite, numTrials = 3, reporter = new JsonReporter(), timeoutMs, evalId) {
15
+ if (!injectedSuite)
16
+ preflight(agent, workspace, skillPath);
17
+ const suite = injectedSuite || evalLoader.loadEvalSuite(skillPath);
18
+ if (evalId !== undefined) {
19
+ suite.tasks = suite.tasks.filter(t => t.id === evalId);
20
+ if (suite.tasks.length === 0) {
21
+ console.error(`No eval found with id ${evalId}`);
22
+ process.exit(1);
23
+ }
24
+ }
25
+ const { skill_name, tasks } = suite;
26
+ // Setup Environment (global setup)
27
+ const env = new EvalEnvironment({ workspace });
28
+ await env.setup();
29
+ // Ensure worktrees are cleaned up even when the process is interrupted (Ctrl+C).
30
+ const cleanup = () => { env.teardown().finally(() => process.exit(1)); };
31
+ process.once('SIGINT', cleanup);
32
+ process.once('SIGTERM', cleanup);
33
+ // Setup Artifacts Directory (Always create, even if not in debug mode, for 'show' command)
34
+ const debug = !!process.env.DEBUG;
35
+ const startTime = new Date();
36
+ const timestamp = startTime.toISOString().replace(/[:.]/g, '-');
37
+ const runDir = path.resolve(workspace, '.project-skill-evals', 'runs', timestamp);
38
+ fs.mkdirSync(runDir, { recursive: true });
39
+ const taskResults = [];
40
+ 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
+ const pool = new AgentPool(maxAgents);
51
+ const ui = new ListrEvalUI();
52
+ // Barrier chain: each prompt waits until the previous prompt's trials have all acquired slots.
53
+ let barrier = Promise.resolve();
54
+ try {
55
+ renderRunHeader({ command: 'trigger', skillName: skill_name, agent, workspace, tasks: tasks.length, trials: numTrials, maxAgents, timeoutMs, runDir, evalId });
56
+ Logger.write(`--- Trigger Pass ---\n`);
57
+ Logger.write(`──────────────────────────────────────────────────\n`);
58
+ for (let i = 0; i < tasks.length; i++) {
59
+ const task = tasks[i];
60
+ const promptSnippet = `${task.prompt.substring(0, 50)}${task.prompt.length > 50 ? '...' : ''}`;
61
+ const taskLabel = `${promptSnippet} (#${task.id})`;
62
+ const thisBarrier = barrier;
63
+ let resolveBarrier;
64
+ barrier = new Promise(r => { resolveBarrier = r; });
65
+ ui.addTask({
66
+ id: task.id,
67
+ title: taskLabel,
68
+ numTrials,
69
+ task: async (uiCtx, multi) => {
70
+ await thisBarrier;
71
+ const trialPromises = [];
72
+ for (let idx = 0; idx < numTrials; idx++) {
73
+ const trialId = idx + 1;
74
+ const trialCtx = multi?.getTrialCtx(trialId) ?? uiCtx;
75
+ const release = await pool.acquire();
76
+ const p = withRetry((attempt) => runner.runTriggerTask(task, i, trialId, trialCtx, attempt)
77
+ .catch((error) => ({
78
+ id: trialId,
79
+ transcript: { error: error instanceof Error ? error.message : String(error) },
80
+ assertionResults: [{
81
+ assertion: 'Runner Execution',
82
+ passed: false,
83
+ reason: error instanceof Error ? error.message : String(error)
84
+ }],
85
+ trialPassed: false,
86
+ isError: true
87
+ })), 2, 1000, (nextAttempt, lastTrial) => {
88
+ const reason = lastTrial.assertionResults[0]?.reason ?? 'infrastructure error';
89
+ trialCtx.updateLog(`Retry ${nextAttempt}/2 — ${reason.substring(0, 50)}`);
90
+ }).then(trial => {
91
+ if (multi) {
92
+ const reason = trial.assertionResults.find(r => !r.passed)?.reason;
93
+ multi.markTrialComplete(trialId, trial.trialPassed, reason, trial.isError);
94
+ }
95
+ return trial;
96
+ }).finally(release);
97
+ trialPromises.push(p);
98
+ }
99
+ // All trials for this prompt have acquired a slot — signal the next prompt.
100
+ resolveBarrier();
101
+ const trials = await Promise.all(trialPromises);
102
+ const passedCount = trials.filter(t => t.trialPassed).length;
103
+ const score = trials.length > 0 ? passedCount / trials.length : 0;
104
+ const taskResult = {
105
+ taskId: task.id,
106
+ prompt: task.prompt,
107
+ score,
108
+ trials: trials.map(t => ({ ...t, transcript: undefined }))
109
+ };
110
+ taskResults.push(taskResult);
111
+ if (passedCount === trials.length) {
112
+ tasksPassedCount++;
113
+ }
114
+ else if (!multi) {
115
+ const failureReason = trials.find(t => !t.trialPassed)?.assertionResults.find(r => !r.passed)?.reason || 'Task failed evaluation';
116
+ throw new Error(failureReason);
117
+ }
118
+ }
119
+ });
120
+ }
121
+ await ui.run(tasks.length);
122
+ // Compute aggregate metrics and build report
123
+ const { passAtK } = aggregatePassAtK(taskResults, numTrials, r => r.trials);
124
+ const percentage = Math.round(passAtK * 100);
125
+ const withSkillTokenStats = aggregateTokenStats(taskResults.flatMap(r => r.trials)) ?? undefined;
126
+ const withSkillDurationStats = aggregateDurationStats(taskResults.flatMap(r => r.trials)) ?? undefined;
127
+ const report = {
128
+ timestamp: startTime.toISOString(),
129
+ skill_name,
130
+ agent,
131
+ metrics: {
132
+ withSkillScore: `${percentage}%`,
133
+ passedCount: tasksPassedCount,
134
+ totalCount: tasks.length,
135
+ numTrials,
136
+ passAtK: Math.round(passAtK * 1000) / 1000,
137
+ tokenStats: withSkillTokenStats ? { withSkill: withSkillTokenStats } : undefined,
138
+ durationStats: withSkillDurationStats ? { withSkill: withSkillDurationStats } : undefined
139
+ },
140
+ results: taskResults
141
+ };
142
+ Logger.write(`\nEVALUATION SUMMARY\n`);
143
+ Logger.write(`──────────────────────────────────────────────────\n`);
144
+ renderTriggerTable(report);
145
+ Logger.write('\n\n');
146
+ new JsonReporter().generate(report, runDir);
147
+ reporter.generate(report, runDir);
148
+ }
149
+ finally {
150
+ process.off('SIGINT', cleanup);
151
+ process.off('SIGTERM', cleanup);
152
+ await env.teardown();
153
+ }
154
+ }
@@ -0,0 +1,30 @@
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
+ }