@fede0089/skill-eval 1.4.1 → 3.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.
- package/README.md +86 -12
- package/dist/core/environment.js +2 -0
- package/dist/runners/claude-code/index.js +1 -0
- package/dist/runners/claude-code/runner.js +279 -0
- package/dist/runners/codex/index.js +1 -0
- package/dist/runners/codex/runner.js +330 -0
- package/dist/runners/index.js +2 -0
- package/dist/runners/registry.js +4 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
##
|
|
5
|
+
## What you can test
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
skill-eval ships two commands, each targeting a different failure mode:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Triggering** (`skill-eval trigger`) — checks whether the agent actually decides to invoke the skill in the right context. A skill that never gets triggered cannot help, no matter how good its instructions are.
|
|
10
|
+
- **Functional correctness** (`skill-eval functional`) — checks whether the actions the agent takes while the skill is active match your expectations. An LLM judge grades each transcript against the expectation list you provide.
|
|
11
|
+
|
|
12
|
+
## Why run skill evals
|
|
13
|
+
|
|
14
|
+
- **Avoid regression** — it is very common that, while iterating to make a skill handle a new case, it stops solving cases it used to handle. Evals make every iteration measurable, so you can tell whether a change adds value without silently subtracting it elsewhere.
|
|
15
|
+
- **Validate against a baseline** — sometimes an agent solves a task better using its general capabilities than using a specific skill (and even if it works today, model upgrades can shift that balance). Comparing against the no-skill baseline (`--compare-baseline`) or past skill versions (`--compare-ref`) tells you whether the skill is really pulling its weight.
|
|
16
|
+
- **Statistical confidence** — LLMs are non-deterministic, so a single passing run is not evidence. Running the same expectations N times produces a pass rate (pass@k) that turns "it feels like it works" into a number you can defend.
|
|
10
17
|
|
|
11
18
|
## How it works
|
|
12
19
|
|
|
@@ -37,7 +44,7 @@ For each eval prompt, skill-eval spins up parallel agent processes with the curr
|
|
|
37
44
|
|
|
38
45
|
## Installation
|
|
39
46
|
|
|
40
|
-
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini`) installed and on `$PATH`.
|
|
47
|
+
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini`, `codex`, or `claude`) installed and on `$PATH`.
|
|
41
48
|
|
|
42
49
|
### Run without installing
|
|
43
50
|
|
|
@@ -87,6 +94,12 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
87
94
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
88
95
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
89
96
|
|
|
97
|
+
Supported runners:
|
|
98
|
+
|
|
99
|
+
- `gemini-cli` (default)
|
|
100
|
+
- `codex`
|
|
101
|
+
- `claude-code`
|
|
102
|
+
|
|
90
103
|
### Skill directory structure
|
|
91
104
|
|
|
92
105
|
```
|
|
@@ -95,8 +108,12 @@ my-skill/
|
|
|
95
108
|
└── evals/ # evaluation suite (required)
|
|
96
109
|
├── my-evals.json # one or more eval files (*.json)
|
|
97
110
|
└── config/ # runner configuration (optional but often needed)
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
├── gemini-cli/ # copied to <worktree>/.gemini/ before each trial
|
|
112
|
+
│ └── settings.json
|
|
113
|
+
├── codex/ # copied to <worktree>/.codex/ before each trial
|
|
114
|
+
│ └── config.toml
|
|
115
|
+
└── claude-code/ # copied to <worktree>/.claude/ before each trial
|
|
116
|
+
└── settings.json
|
|
100
117
|
```
|
|
101
118
|
|
|
102
119
|
All `.json` files in `evals/` are loaded and merged into a single suite — you can split them by feature or regression category.
|
|
@@ -134,15 +151,34 @@ All `.json` files in `evals/` are loaded and merged into a single suite — you
|
|
|
134
151
|
|
|
135
152
|
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.
|
|
136
153
|
|
|
137
|
-
|
|
154
|
+
Each runner configures its own non-interactive mode. For example, Gemini CLI uses `--approval-mode auto_edit`, Codex uses `codex exec --json --sandbox workspace-write -c approval_policy="never"`, and Claude Code uses `claude -p --output-format stream-json --permission-mode bypassPermissions`. If your skill needs to run shell commands, read environment variables, make network calls, or use any other tool category, refer to that runner's permission model.
|
|
138
155
|
|
|
139
156
|
**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:
|
|
140
157
|
|
|
141
158
|
```
|
|
142
|
-
evals/config/gemini-cli/
|
|
159
|
+
evals/config/gemini-cli/ → <worktree>/.gemini/
|
|
160
|
+
evals/config/codex/ → <worktree>/.codex/
|
|
161
|
+
evals/config/claude-code/ → <worktree>/.claude/
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use this to ship both settings and policies alongside your evals. Anything inside `evals/config/<runner>/` is dropped verbatim into the runner's config directory, so you can use the runner's full configuration surface — not just `settings.json`.
|
|
165
|
+
|
|
166
|
+
### Gemini CLI example
|
|
167
|
+
|
|
168
|
+
Gemini CLI reads both `settings.json` and any `*.toml` rule files under `policies/`. Drop them inside `evals/config/gemini-cli/` and skill-eval will copy them into `<worktree>/.gemini/` before each trial:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
my-skill/
|
|
172
|
+
└── evals/
|
|
173
|
+
└── config/
|
|
174
|
+
└── gemini-cli/
|
|
175
|
+
├── settings.json
|
|
176
|
+
└── policies/
|
|
177
|
+
├── allow-activate-skill.toml
|
|
178
|
+
└── allow-tools.toml
|
|
143
179
|
```
|
|
144
180
|
|
|
145
|
-
|
|
181
|
+
`settings.json` holds general configuration (telemetry, model, etc.):
|
|
146
182
|
|
|
147
183
|
```json
|
|
148
184
|
{
|
|
@@ -150,10 +186,46 @@ Use this to ship both settings and policies alongside your evals. For Gemini CLI
|
|
|
150
186
|
}
|
|
151
187
|
```
|
|
152
188
|
|
|
153
|
-
|
|
189
|
+
Policies whitelist specific tools so they run without prompting. For example, to always allow the `activate_skill` dispatch tool in non-interactive mode:
|
|
190
|
+
|
|
191
|
+
```toml
|
|
192
|
+
# evals/config/gemini-cli/policies/allow-activate-skill.toml
|
|
193
|
+
[[rule]]
|
|
194
|
+
toolName = "activate_skill"
|
|
195
|
+
decision = "allow"
|
|
196
|
+
priority = 100
|
|
197
|
+
interactive = false
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Refer to your runner's documentation for the full set of settings and policy keys (Codex uses `config.toml`, Claude Code uses `settings.json`).
|
|
154
201
|
|
|
155
202
|
> This config only applies inside the temporary worktree created for each trial. Your real workspace config is never touched.
|
|
156
203
|
|
|
204
|
+
## Reports
|
|
205
|
+
|
|
206
|
+
Each run writes to `.project-skill-evals/runs/<timestamp>/` and includes per-trial logs, the raw eval JSON, and a self-contained HTML report you can open in any browser. The report shows pass@k aggregates per eval, lets you expand each trial, and color-codes triggering vs. functional outcomes.
|
|
207
|
+
|
|
208
|
+
A published sample report is available at [fede0089.github.io/skill-eval/sample-report.html](https://fede0089.github.io/skill-eval/sample-report.html). The checked-in source is [`docs/sample-report.html`](docs/sample-report.html), with the matching screenshot at [`docs/sample-report.png`](docs/sample-report.png).
|
|
209
|
+
|
|
210
|
+
It was generated from the project root with:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
skill-eval functional --workspace . --skill mock-skill --trials 2 --compare-baseline --debug claude-code
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+

|
|
217
|
+
|
|
218
|
+
To publish or refresh the GitHub Page, commit the files under `docs/`, then enable **Settings -> Pages -> Build and deployment -> Deploy from a branch** in GitHub and select the `main` branch with the `/docs` folder. GitHub will serve the report at `https://fede0089.github.io/skill-eval/sample-report.html`.
|
|
219
|
+
|
|
220
|
+
### Debug logs
|
|
221
|
+
|
|
222
|
+
When a trial misbehaves, pass `-v` / `--debug` to capture the full transcripts to disk. Each trial writes a `task_<id>_<variant>_trial_<n>.log` file inside the run directory with two sections appended in order:
|
|
223
|
+
|
|
224
|
+
- `# SECTION: <MODE> AGENT RUN` — the initial prompt sent to the agent and its raw streamed response.
|
|
225
|
+
- `# SECTION: <MODE> JUDGE RUN` — the prompt sent to the LLM judge and its verdict (only present for `functional` runs; `trigger` is graded programmatically and produces no judge section).
|
|
226
|
+
|
|
227
|
+
Without `--debug` these files are not written, so reach for the flag when you need to see exactly what the agent — or the judge — saw.
|
|
228
|
+
|
|
157
229
|
## Try it out
|
|
158
230
|
|
|
159
231
|
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:
|
|
@@ -162,10 +234,12 @@ This repo includes a `mock-skill/` directory — a complete, working example of
|
|
|
162
234
|
npm run test:unit # run the unit test suite
|
|
163
235
|
npm run test:trigger # trigger evaluation against mock-skill
|
|
164
236
|
npm run test:functional # functional evaluation against mock-skill
|
|
237
|
+
npm run test:trigger -- codex # run trigger evals with Codex
|
|
238
|
+
npm run test:functional -- codex # run functional evals with Codex
|
|
239
|
+
npm run test:trigger -- claude-code # run trigger evals with Claude Code
|
|
240
|
+
npm run test:functional -- claude-code # run functional evals with Claude Code
|
|
165
241
|
```
|
|
166
242
|
|
|
167
|
-
Results are saved to `.project-skill-evals/runs/<timestamp>/` with logs, raw eval JSONs, and an HTML report.
|
|
168
|
-
|
|
169
243
|
## Extending
|
|
170
244
|
|
|
171
245
|
### Adding a new agent runner
|
package/dist/core/environment.js
CHANGED
|
@@ -37,9 +37,11 @@ export class EvalEnvironment {
|
|
|
37
37
|
*/
|
|
38
38
|
createWorktree(evalId) {
|
|
39
39
|
const worktreePath = path.resolve(this.workspace, '.project-skill-evals', 'worktrees', evalId);
|
|
40
|
+
const branchName = path.basename(worktreePath);
|
|
40
41
|
// Ensure the path is clean before adding a worktree.
|
|
41
42
|
// We try to remove it first in case a previous run crashed.
|
|
42
43
|
executor.spawnSync('git', ['worktree', 'remove', '--force', worktreePath], { stdio: 'ignore', cwd: this.workspace });
|
|
44
|
+
executor.spawnSync('git', ['branch', '-D', branchName], { stdio: 'ignore', cwd: this.workspace });
|
|
43
45
|
// If git worktree remove failed (e.g. path was never registered, or git
|
|
44
46
|
// metadata is stale), fall back to a physical wipe and a metadata prune so
|
|
45
47
|
// that 'git worktree add' does not exit 128 on a pre-existing path.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ClaudeCodeRunner } from './runner.js';
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import child_process from 'child_process';
|
|
4
|
+
import { Logger } from '../../utils/logger.js';
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function readSkillName(skillPath) {
|
|
9
|
+
const skillMd = path.join(skillPath, 'SKILL.md');
|
|
10
|
+
if (!fs.existsSync(skillMd))
|
|
11
|
+
return path.basename(skillPath);
|
|
12
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
13
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
14
|
+
if (!frontmatter)
|
|
15
|
+
return path.basename(skillPath);
|
|
16
|
+
const nameLine = frontmatter[1].split('\n').find((line) => line.trim().startsWith('name:'));
|
|
17
|
+
const name = nameLine?.replace(/^name:\s*/, '').trim().replace(/^["']|["']$/g, '');
|
|
18
|
+
return name || path.basename(skillPath);
|
|
19
|
+
}
|
|
20
|
+
function numberFrom(value) {
|
|
21
|
+
return typeof value === 'number' ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
function getAssistantContent(event) {
|
|
24
|
+
const eventType = String(event.type ?? '');
|
|
25
|
+
if (eventType !== 'assistant')
|
|
26
|
+
return [];
|
|
27
|
+
const message = event.message;
|
|
28
|
+
if (!isRecord(message))
|
|
29
|
+
return [];
|
|
30
|
+
const content = message.content;
|
|
31
|
+
if (!Array.isArray(content))
|
|
32
|
+
return [];
|
|
33
|
+
return content.filter(isRecord);
|
|
34
|
+
}
|
|
35
|
+
function getUserContent(event) {
|
|
36
|
+
const eventType = String(event.type ?? '');
|
|
37
|
+
if (eventType !== 'user')
|
|
38
|
+
return [];
|
|
39
|
+
const message = event.message;
|
|
40
|
+
if (!isRecord(message))
|
|
41
|
+
return [];
|
|
42
|
+
const content = message.content;
|
|
43
|
+
if (!Array.isArray(content))
|
|
44
|
+
return [];
|
|
45
|
+
return content.filter(isRecord);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Converts Claude Code stream-json events into the small NDJSON contract used
|
|
49
|
+
* by the rest of the evaluator. The normalizer is intentionally tolerant: the
|
|
50
|
+
* Claude Code event stream is richer than the evaluator needs and may grow
|
|
51
|
+
* over time. Only fields the grader/judge consume are mapped.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeClaudeJsonl(output) {
|
|
54
|
+
const normalized = [];
|
|
55
|
+
let sawResult = false;
|
|
56
|
+
for (const line of output.split('\n')) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed)
|
|
59
|
+
continue;
|
|
60
|
+
let event;
|
|
61
|
+
try {
|
|
62
|
+
event = JSON.parse(trimmed);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const item of getAssistantContent(event)) {
|
|
68
|
+
const itemType = String(item.type ?? '');
|
|
69
|
+
if (itemType === 'text' && typeof item.text === 'string' && item.text.trim()) {
|
|
70
|
+
normalized.push({ type: 'message', role: 'assistant', content: item.text });
|
|
71
|
+
}
|
|
72
|
+
else if (itemType === 'tool_use') {
|
|
73
|
+
const toolName = String(item.name ?? '');
|
|
74
|
+
const toolId = String(item.id ?? `claude-tool-${normalized.length + 1}`);
|
|
75
|
+
const input = isRecord(item.input) ? item.input : {};
|
|
76
|
+
// The internal NDJSON contract expects parameters.name to carry the skill
|
|
77
|
+
// identifier when the dispatch tool is invoked. Claude Code's Skill tool
|
|
78
|
+
// uses `input.skill` for that — remap so TriggerGrader can match it.
|
|
79
|
+
const parameters = { ...input };
|
|
80
|
+
if (toolName === 'Skill' && typeof input.skill === 'string') {
|
|
81
|
+
parameters.name = input.skill;
|
|
82
|
+
}
|
|
83
|
+
normalized.push({
|
|
84
|
+
type: 'tool_use',
|
|
85
|
+
tool_id: toolId,
|
|
86
|
+
tool_name: toolName,
|
|
87
|
+
parameters,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const item of getUserContent(event)) {
|
|
92
|
+
const itemType = String(item.type ?? '');
|
|
93
|
+
if (itemType !== 'tool_result')
|
|
94
|
+
continue;
|
|
95
|
+
const toolId = String(item.tool_use_id ?? '');
|
|
96
|
+
if (!toolId)
|
|
97
|
+
continue;
|
|
98
|
+
const isError = item.is_error === true;
|
|
99
|
+
// Claude Code attaches a tool_use_result sibling on the parent event with
|
|
100
|
+
// a `success` boolean for built-in tools (e.g. Skill). Prefer that when present.
|
|
101
|
+
const parent = event.tool_use_result;
|
|
102
|
+
const parentSuccess = isRecord(parent) ? parent.success : undefined;
|
|
103
|
+
const status = isError === true || parentSuccess === false ? 'error' : 'success';
|
|
104
|
+
normalized.push({ type: 'tool_result', tool_id: toolId, status });
|
|
105
|
+
}
|
|
106
|
+
const eventType = String(event.type ?? '');
|
|
107
|
+
if (eventType === 'result') {
|
|
108
|
+
sawResult = true;
|
|
109
|
+
const isError = event.is_error === true;
|
|
110
|
+
const usage = isRecord(event.usage) ? event.usage : undefined;
|
|
111
|
+
const inputTokens = usage ? numberFrom(usage.input_tokens) ?? 0 : 0;
|
|
112
|
+
const outputTokens = usage ? numberFrom(usage.output_tokens) ?? 0 : 0;
|
|
113
|
+
const cachedTokens = usage
|
|
114
|
+
? numberFrom(usage.cache_read_input_tokens)
|
|
115
|
+
?? numberFrom(usage.cached_input_tokens)
|
|
116
|
+
?? 0
|
|
117
|
+
: 0;
|
|
118
|
+
const stats = usage
|
|
119
|
+
? {
|
|
120
|
+
total_tokens: inputTokens + outputTokens,
|
|
121
|
+
input_tokens: inputTokens,
|
|
122
|
+
output_tokens: outputTokens,
|
|
123
|
+
cached: cachedTokens,
|
|
124
|
+
}
|
|
125
|
+
: undefined;
|
|
126
|
+
if (isError) {
|
|
127
|
+
const message = typeof event.result === 'string' && event.result.trim()
|
|
128
|
+
? event.result
|
|
129
|
+
: `Claude Code run failed (subtype: ${String(event.subtype ?? 'unknown')})`;
|
|
130
|
+
normalized.push({ type: 'result', status: 'error', error: { message }, ...(stats ? { stats } : {}) });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const response = typeof event.result === 'string' ? event.result : undefined;
|
|
134
|
+
normalized.push({
|
|
135
|
+
type: 'result',
|
|
136
|
+
status: 'success',
|
|
137
|
+
...(response ? { response } : {}),
|
|
138
|
+
...(stats ? { stats } : {}),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!sawResult) {
|
|
144
|
+
normalized.push({ type: 'result', status: 'error', error: { message: 'Claude Code produced no result event' } });
|
|
145
|
+
}
|
|
146
|
+
return normalized.map((event) => JSON.stringify(event)).join('\n');
|
|
147
|
+
}
|
|
148
|
+
export class ClaudeCodeRunner {
|
|
149
|
+
skillDispatchToolName = 'Skill';
|
|
150
|
+
linkedSkillsByWorktree = new Map();
|
|
151
|
+
async runPrompt(prompt, cwd, onLog, logPath, extraArgs = [], timeoutMs) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
let stdout = '';
|
|
154
|
+
let stderr = '';
|
|
155
|
+
let resolved = false;
|
|
156
|
+
let timeout;
|
|
157
|
+
const args = [
|
|
158
|
+
'-p', prompt,
|
|
159
|
+
'--output-format', 'stream-json',
|
|
160
|
+
'--verbose',
|
|
161
|
+
'--permission-mode', 'bypassPermissions',
|
|
162
|
+
'--no-session-persistence',
|
|
163
|
+
...extraArgs,
|
|
164
|
+
];
|
|
165
|
+
const spawnOptions = {
|
|
166
|
+
cwd,
|
|
167
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
168
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
|
+
detached: true,
|
|
170
|
+
};
|
|
171
|
+
function appendLog(chunk) {
|
|
172
|
+
if (!logPath)
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
fs.appendFileSync(logPath, chunk);
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
Logger.warn(`Failed to write Claude Code debug log at ${logPath}. Continuing. Reason: ${err}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function killProcessGroup() {
|
|
182
|
+
if (!child.pid)
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// Process might have already exited.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const child = child_process.spawn('claude', args, spawnOptions);
|
|
192
|
+
appendLog(`--- Claude Code Execution Start: ${new Date().toISOString()} ---\n`);
|
|
193
|
+
appendLog(`Command: claude ${args.join(' ')}\n\n`);
|
|
194
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
195
|
+
timeout = setTimeout(() => {
|
|
196
|
+
if (resolved)
|
|
197
|
+
return;
|
|
198
|
+
resolved = true;
|
|
199
|
+
killProcessGroup();
|
|
200
|
+
const timeoutSec = timeoutMs / 1000;
|
|
201
|
+
Logger.error(`\nClaude Code process timed out after ${timeoutSec} seconds.`);
|
|
202
|
+
appendLog(`\n\n--- Claude Code process timed out ---\n${stderr}\n`);
|
|
203
|
+
resolve({ error: `Process timeout exceeded (${timeoutSec} seconds)`, raw_output: stderr });
|
|
204
|
+
}, timeoutMs);
|
|
205
|
+
}
|
|
206
|
+
child.stdout?.on('data', (data) => {
|
|
207
|
+
const chunk = data.toString();
|
|
208
|
+
stdout += chunk;
|
|
209
|
+
appendLog(chunk);
|
|
210
|
+
});
|
|
211
|
+
child.stderr?.on('data', (data) => {
|
|
212
|
+
const chunk = data.toString();
|
|
213
|
+
stderr += chunk;
|
|
214
|
+
if (onLog) {
|
|
215
|
+
const lines = chunk.split('\n').filter((line) => line.trim() !== '');
|
|
216
|
+
if (lines.length > 0)
|
|
217
|
+
onLog(lines[lines.length - 1]);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
child.on('error', (err) => {
|
|
221
|
+
if (resolved)
|
|
222
|
+
return;
|
|
223
|
+
resolved = true;
|
|
224
|
+
if (timeout)
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
Logger.error(`Failed to start Claude Code CLI. Error: ${err.message}`);
|
|
227
|
+
appendLog(`\n\n--- Error starting Claude Code CLI ---\n${err.message}\n`);
|
|
228
|
+
resolve(null);
|
|
229
|
+
});
|
|
230
|
+
child.on('close', (code) => {
|
|
231
|
+
appendLog(`\n\n--- Claude Code exited with status ${code} ---\n`);
|
|
232
|
+
if (code !== 0 && stderr) {
|
|
233
|
+
appendLog(`--- Stderr ---\n${stderr}\n--- End Stderr ---\n`);
|
|
234
|
+
}
|
|
235
|
+
if (resolved)
|
|
236
|
+
return;
|
|
237
|
+
resolved = true;
|
|
238
|
+
if (timeout)
|
|
239
|
+
clearTimeout(timeout);
|
|
240
|
+
if (!stdout.trim()) {
|
|
241
|
+
resolve({ error: 'Empty output from Claude Code CLI', raw_output: stderr });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const normalized = normalizeClaudeJsonl(stdout);
|
|
245
|
+
if (code !== 0) {
|
|
246
|
+
resolve({
|
|
247
|
+
error: `Claude Code CLI exited with status ${code}`,
|
|
248
|
+
response: normalized,
|
|
249
|
+
raw_output: `${normalized}\n--- CLAUDE CODE STDOUT ---\n${stdout}\n--- STDERR ---\n${stderr}`,
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
resolve({
|
|
254
|
+
response: normalized,
|
|
255
|
+
raw_output: `${normalized}\n--- CLAUDE CODE STDOUT ---\n${stdout}\n--- STDERR ---\n${stderr}`,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
applyRunnerConfig(evalConfigBaseDir, worktreePath) {
|
|
261
|
+
const src = path.join(evalConfigBaseDir, 'claude-code');
|
|
262
|
+
if (!fs.existsSync(src))
|
|
263
|
+
return;
|
|
264
|
+
const dst = path.join(worktreePath, '.claude');
|
|
265
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
266
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
267
|
+
}
|
|
268
|
+
async linkSkill(absoluteSkillPath, worktreePath) {
|
|
269
|
+
const skillName = readSkillName(absoluteSkillPath);
|
|
270
|
+
const localSkillsDir = path.join(worktreePath, '.claude', 'skills');
|
|
271
|
+
const symlinkPath = path.join(localSkillsDir, skillName);
|
|
272
|
+
fs.mkdirSync(localSkillsDir, { recursive: true });
|
|
273
|
+
if (fs.existsSync(symlinkPath)) {
|
|
274
|
+
fs.rmSync(symlinkPath, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
fs.symlinkSync(absoluteSkillPath, symlinkPath, 'dir');
|
|
277
|
+
this.linkedSkillsByWorktree.set(path.resolve(worktreePath), { name: skillName });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CodexRunner } from './runner.js';
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import child_process from 'child_process';
|
|
4
|
+
import { Logger } from '../../utils/logger.js';
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function readSkillName(skillPath) {
|
|
9
|
+
const skillMd = path.join(skillPath, 'SKILL.md');
|
|
10
|
+
if (!fs.existsSync(skillMd))
|
|
11
|
+
return path.basename(skillPath);
|
|
12
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
13
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
14
|
+
if (!frontmatter)
|
|
15
|
+
return path.basename(skillPath);
|
|
16
|
+
const nameLine = frontmatter[1].split('\n').find((line) => line.trim().startsWith('name:'));
|
|
17
|
+
const name = nameLine?.replace(/^name:\s*/, '').trim().replace(/^["']|["']$/g, '');
|
|
18
|
+
return name || path.basename(skillPath);
|
|
19
|
+
}
|
|
20
|
+
function tomlString(value) {
|
|
21
|
+
return JSON.stringify(value);
|
|
22
|
+
}
|
|
23
|
+
function extractText(value) {
|
|
24
|
+
if (typeof value === 'string')
|
|
25
|
+
return [value];
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.flatMap(extractText);
|
|
28
|
+
if (!isRecord(value))
|
|
29
|
+
return [];
|
|
30
|
+
const direct = ['text', 'message', 'content', 'output_text', 'final_message']
|
|
31
|
+
.flatMap((key) => extractText(value[key]));
|
|
32
|
+
if (direct.length > 0)
|
|
33
|
+
return direct;
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
function getEventItem(event) {
|
|
37
|
+
const item = event.item ?? event.message ?? event.event;
|
|
38
|
+
return isRecord(item) ? item : undefined;
|
|
39
|
+
}
|
|
40
|
+
function getAssistantText(event) {
|
|
41
|
+
const eventType = String(event.type ?? '');
|
|
42
|
+
if (eventType === 'agent_message' || eventType === 'assistant_message') {
|
|
43
|
+
return extractText(event);
|
|
44
|
+
}
|
|
45
|
+
const item = getEventItem(event);
|
|
46
|
+
if (!item)
|
|
47
|
+
return [];
|
|
48
|
+
const itemType = String(item.type ?? '');
|
|
49
|
+
if (itemType === 'agent_message' || itemType === 'assistant_message' || itemType === 'message') {
|
|
50
|
+
return extractText(item);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
function eventSignalsSkill(event, skillName) {
|
|
55
|
+
const serialized = JSON.stringify(event).toLowerCase();
|
|
56
|
+
const normalizedSkill = skillName.toLowerCase();
|
|
57
|
+
const eventType = String(event.type ?? '').toLowerCase();
|
|
58
|
+
const item = getEventItem(event);
|
|
59
|
+
const itemType = String(item?.type ?? '').toLowerCase();
|
|
60
|
+
return (serialized.includes(normalizedSkill) &&
|
|
61
|
+
(serialized.includes('skill') ||
|
|
62
|
+
eventType.includes('skill') ||
|
|
63
|
+
itemType.includes('skill') ||
|
|
64
|
+
serialized.includes('"activate_skill"') ||
|
|
65
|
+
serialized.includes('"use_skill"')));
|
|
66
|
+
}
|
|
67
|
+
function eventIsFailure(event) {
|
|
68
|
+
const status = String(event.status ?? '').toLowerCase();
|
|
69
|
+
const item = getEventItem(event);
|
|
70
|
+
const itemStatus = String(item?.status ?? '').toLowerCase();
|
|
71
|
+
return ['error', 'failed', 'failure'].includes(status) || ['error', 'failed', 'failure'].includes(itemStatus);
|
|
72
|
+
}
|
|
73
|
+
function getErrorMessage(event) {
|
|
74
|
+
const eventType = String(event.type ?? '').toLowerCase();
|
|
75
|
+
if (!eventType.includes('error') && !eventIsFailure(event))
|
|
76
|
+
return null;
|
|
77
|
+
const message = extractText(event).join('\n').trim();
|
|
78
|
+
if (message)
|
|
79
|
+
return message;
|
|
80
|
+
const error = event.error;
|
|
81
|
+
if (typeof error === 'string')
|
|
82
|
+
return error;
|
|
83
|
+
if (isRecord(error)) {
|
|
84
|
+
const nested = extractText(error).join('\n').trim();
|
|
85
|
+
if (nested)
|
|
86
|
+
return nested;
|
|
87
|
+
}
|
|
88
|
+
return 'Codex run failed';
|
|
89
|
+
}
|
|
90
|
+
function getUsage(event) {
|
|
91
|
+
const usage = event.usage ?? event.token_usage;
|
|
92
|
+
if (isRecord(usage))
|
|
93
|
+
return usage;
|
|
94
|
+
const item = getEventItem(event);
|
|
95
|
+
const itemUsage = item?.usage ?? item?.token_usage;
|
|
96
|
+
return isRecord(itemUsage) ? itemUsage : undefined;
|
|
97
|
+
}
|
|
98
|
+
function numberFrom(value) {
|
|
99
|
+
return typeof value === 'number' ? value : undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Converts Codex exec JSONL events into the small NDJSON contract used by the
|
|
103
|
+
* rest of the evaluator. The normalizer is intentionally tolerant because the
|
|
104
|
+
* Codex JSON event stream is richer than the evaluator needs and may grow over
|
|
105
|
+
* time.
|
|
106
|
+
*/
|
|
107
|
+
export function normalizeCodexJsonl(output, skillName) {
|
|
108
|
+
const normalized = [];
|
|
109
|
+
let sawResult = false;
|
|
110
|
+
let sawSkillActivation = false;
|
|
111
|
+
let errorMessage = null;
|
|
112
|
+
let usage;
|
|
113
|
+
for (const line of output.split('\n')) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed)
|
|
116
|
+
continue;
|
|
117
|
+
let event;
|
|
118
|
+
try {
|
|
119
|
+
event = JSON.parse(trimmed);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const text of getAssistantText(event)) {
|
|
125
|
+
if (text.trim()) {
|
|
126
|
+
normalized.push({ type: 'message', role: 'assistant', content: text });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (skillName && !sawSkillActivation && eventSignalsSkill(event, skillName)) {
|
|
130
|
+
sawSkillActivation = true;
|
|
131
|
+
normalized.push({
|
|
132
|
+
type: 'tool_use',
|
|
133
|
+
tool_id: `codex-skill-${normalized.length + 1}`,
|
|
134
|
+
tool_name: 'activate_skill',
|
|
135
|
+
parameters: { name: skillName },
|
|
136
|
+
});
|
|
137
|
+
normalized.push({
|
|
138
|
+
type: 'tool_result',
|
|
139
|
+
tool_id: `codex-skill-${normalized.length}`,
|
|
140
|
+
status: eventIsFailure(event) ? 'error' : 'success',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const eventUsage = getUsage(event);
|
|
144
|
+
if (eventUsage)
|
|
145
|
+
usage = eventUsage;
|
|
146
|
+
const maybeError = getErrorMessage(event);
|
|
147
|
+
if (maybeError)
|
|
148
|
+
errorMessage = maybeError;
|
|
149
|
+
const eventType = String(event.type ?? '');
|
|
150
|
+
if (eventType === 'result') {
|
|
151
|
+
sawResult = true;
|
|
152
|
+
normalized.push(event);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!sawResult) {
|
|
156
|
+
if (errorMessage) {
|
|
157
|
+
normalized.push({ type: 'result', status: 'error', error: { message: errorMessage } });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const inputTokens = usage ? numberFrom(usage.input_tokens) ?? numberFrom(usage.inputTokens) ?? 0 : 0;
|
|
161
|
+
const outputTokens = usage ? numberFrom(usage.output_tokens) ?? numberFrom(usage.outputTokens) ?? 0 : 0;
|
|
162
|
+
const stats = usage ? {
|
|
163
|
+
total_tokens: numberFrom(usage.total_tokens) ?? numberFrom(usage.totalTokens) ?? inputTokens + outputTokens,
|
|
164
|
+
input_tokens: inputTokens,
|
|
165
|
+
output_tokens: outputTokens,
|
|
166
|
+
cached: numberFrom(usage.cached) ?? numberFrom(usage.cached_tokens) ?? numberFrom(usage.cached_input_tokens) ?? numberFrom(usage.cachedTokens) ?? 0,
|
|
167
|
+
} : undefined;
|
|
168
|
+
normalized.push({ type: 'result', status: 'success', ...(stats ? { stats } : {}) });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return normalized.map((event) => JSON.stringify(event)).join('\n');
|
|
172
|
+
}
|
|
173
|
+
export class CodexRunner {
|
|
174
|
+
skillDispatchToolName = 'activate_skill';
|
|
175
|
+
linkedSkillsByWorktree = new Map();
|
|
176
|
+
async runPrompt(prompt, cwd, onLog, logPath, extraArgs = [], timeoutMs) {
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
let stdout = '';
|
|
179
|
+
let stderr = '';
|
|
180
|
+
let resolved = false;
|
|
181
|
+
let timeout;
|
|
182
|
+
const linkedSkill = cwd ? this.linkedSkillsByWorktree.get(path.resolve(cwd)) : undefined;
|
|
183
|
+
const skillConfigArgs = linkedSkill
|
|
184
|
+
? ['-c', `skills.config=[{path=${tomlString(linkedSkill.path)},enabled=true}]`]
|
|
185
|
+
: [];
|
|
186
|
+
const instructionArgs = linkedSkill
|
|
187
|
+
? ['-c', `model_instructions_file=${tomlString(linkedSkill.instructionsPath)}`]
|
|
188
|
+
: [];
|
|
189
|
+
const args = [
|
|
190
|
+
'exec',
|
|
191
|
+
'--json',
|
|
192
|
+
'--cd', cwd ?? process.cwd(),
|
|
193
|
+
'--sandbox', 'workspace-write',
|
|
194
|
+
'-c', 'approval_policy="never"',
|
|
195
|
+
'--skip-git-repo-check',
|
|
196
|
+
'--ephemeral',
|
|
197
|
+
'--color', 'never',
|
|
198
|
+
...skillConfigArgs,
|
|
199
|
+
...instructionArgs,
|
|
200
|
+
...extraArgs,
|
|
201
|
+
prompt,
|
|
202
|
+
];
|
|
203
|
+
const spawnOptions = {
|
|
204
|
+
cwd,
|
|
205
|
+
env: { ...process.env, FORCE_COLOR: '0' },
|
|
206
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
207
|
+
detached: true,
|
|
208
|
+
};
|
|
209
|
+
function appendLog(chunk) {
|
|
210
|
+
if (!logPath)
|
|
211
|
+
return;
|
|
212
|
+
try {
|
|
213
|
+
fs.appendFileSync(logPath, chunk);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
Logger.warn(`Failed to write Codex debug log at ${logPath}. Continuing. Reason: ${err}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function killProcessGroup() {
|
|
220
|
+
if (!child.pid)
|
|
221
|
+
return;
|
|
222
|
+
try {
|
|
223
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Process might have already exited.
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const child = child_process.spawn('codex', args, spawnOptions);
|
|
230
|
+
appendLog(`--- Codex Execution Start: ${new Date().toISOString()} ---\n`);
|
|
231
|
+
appendLog(`Command: codex ${args.join(' ')}\n\n`);
|
|
232
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
233
|
+
timeout = setTimeout(() => {
|
|
234
|
+
if (resolved)
|
|
235
|
+
return;
|
|
236
|
+
resolved = true;
|
|
237
|
+
killProcessGroup();
|
|
238
|
+
const timeoutSec = timeoutMs / 1000;
|
|
239
|
+
Logger.error(`\nCodex process timed out after ${timeoutSec} seconds.`);
|
|
240
|
+
appendLog(`\n\n--- Codex process timed out ---\n${stderr}\n`);
|
|
241
|
+
resolve({ error: `Process timeout exceeded (${timeoutSec} seconds)`, raw_output: stderr });
|
|
242
|
+
}, timeoutMs);
|
|
243
|
+
}
|
|
244
|
+
child.stdout?.on('data', (data) => {
|
|
245
|
+
const chunk = data.toString();
|
|
246
|
+
stdout += chunk;
|
|
247
|
+
appendLog(chunk);
|
|
248
|
+
});
|
|
249
|
+
child.stderr?.on('data', (data) => {
|
|
250
|
+
const chunk = data.toString();
|
|
251
|
+
stderr += chunk;
|
|
252
|
+
if (onLog) {
|
|
253
|
+
const lines = chunk.split('\n').filter((line) => line.trim() !== '');
|
|
254
|
+
if (lines.length > 0)
|
|
255
|
+
onLog(lines[lines.length - 1]);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
child.on('error', (err) => {
|
|
259
|
+
if (resolved)
|
|
260
|
+
return;
|
|
261
|
+
resolved = true;
|
|
262
|
+
if (timeout)
|
|
263
|
+
clearTimeout(timeout);
|
|
264
|
+
Logger.error(`Failed to start Codex CLI. Error: ${err.message}`);
|
|
265
|
+
appendLog(`\n\n--- Error starting Codex CLI ---\n${err.message}\n`);
|
|
266
|
+
resolve(null);
|
|
267
|
+
});
|
|
268
|
+
child.on('close', (code) => {
|
|
269
|
+
appendLog(`\n\n--- Codex exited with status ${code} ---\n`);
|
|
270
|
+
if (code !== 0 && stderr) {
|
|
271
|
+
appendLog(`--- Stderr ---\n${stderr}\n--- End Stderr ---\n`);
|
|
272
|
+
}
|
|
273
|
+
if (resolved)
|
|
274
|
+
return;
|
|
275
|
+
resolved = true;
|
|
276
|
+
if (timeout)
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
if (!stdout.trim()) {
|
|
279
|
+
resolve({ error: 'Empty output from Codex CLI', raw_output: stderr });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const normalized = normalizeCodexJsonl(stdout, linkedSkill?.name);
|
|
283
|
+
if (code !== 0) {
|
|
284
|
+
resolve({
|
|
285
|
+
error: `Codex CLI exited with status ${code}`,
|
|
286
|
+
response: normalized,
|
|
287
|
+
raw_output: `${normalized}\n--- CODEX STDOUT ---\n${stdout}\n--- STDERR ---\n${stderr}`,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
resolve({
|
|
292
|
+
response: normalized,
|
|
293
|
+
raw_output: `${normalized}\n--- CODEX STDOUT ---\n${stdout}\n--- STDERR ---\n${stderr}`,
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
applyRunnerConfig(evalConfigBaseDir, worktreePath) {
|
|
299
|
+
const src = path.join(evalConfigBaseDir, 'codex');
|
|
300
|
+
if (!fs.existsSync(src))
|
|
301
|
+
return;
|
|
302
|
+
const dst = path.join(worktreePath, '.codex');
|
|
303
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
304
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
305
|
+
}
|
|
306
|
+
async linkSkill(absoluteSkillPath, worktreePath) {
|
|
307
|
+
const skillName = readSkillName(absoluteSkillPath);
|
|
308
|
+
const localSkillsDir = path.join(worktreePath, '.codex', 'skills');
|
|
309
|
+
const symlinkPath = path.join(localSkillsDir, skillName);
|
|
310
|
+
fs.mkdirSync(localSkillsDir, { recursive: true });
|
|
311
|
+
if (fs.existsSync(symlinkPath)) {
|
|
312
|
+
fs.rmSync(symlinkPath, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
fs.symlinkSync(absoluteSkillPath, symlinkPath, 'dir');
|
|
315
|
+
const instructionsPath = path.join(worktreePath, '.codex', 'skill-eval-instructions.md');
|
|
316
|
+
fs.writeFileSync(instructionsPath, [
|
|
317
|
+
'You are running inside an automated Agent Skill evaluation.',
|
|
318
|
+
'Complete the user task end-to-end without asking for approval, waiting for confirmation, or stopping after a plan.',
|
|
319
|
+
'Use the configured skill when it applies. If you use a skill, read and follow its SKILL.md instructions.',
|
|
320
|
+
'Make any required edits directly in the current working directory.',
|
|
321
|
+
'Keep the final response concise.',
|
|
322
|
+
'',
|
|
323
|
+
].join('\n'));
|
|
324
|
+
this.linkedSkillsByWorktree.set(path.resolve(worktreePath), {
|
|
325
|
+
name: skillName,
|
|
326
|
+
path: symlinkPath,
|
|
327
|
+
instructionsPath,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
package/dist/runners/index.js
CHANGED
package/dist/runners/registry.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { GeminiCliRunner } from './gemini-cli/index.js';
|
|
2
|
+
import { CodexRunner } from './codex/index.js';
|
|
3
|
+
import { ClaudeCodeRunner } from './claude-code/index.js';
|
|
2
4
|
export const RUNNER_REGISTRY = {
|
|
3
5
|
'gemini-cli': { Runner: GeminiCliRunner, binary: 'gemini' },
|
|
6
|
+
codex: { Runner: CodexRunner, binary: 'codex' },
|
|
7
|
+
'claude-code': { Runner: ClaudeCodeRunner, binary: 'claude' },
|
|
4
8
|
};
|
|
5
9
|
/** Default agent name used when none is specified on the CLI. */
|
|
6
10
|
export const DEFAULT_AGENT = Object.keys(RUNNER_REGISTRY)[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fede0089/skill-eval",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "CLI to evaluate agent skills triggering and functionality",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"prepare": "npm run build",
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"start": "node dist/index.js",
|
|
21
|
-
"test:trigger": "npm run build && node ./
|
|
22
|
-
"test:functional": "npm run build && node ./
|
|
21
|
+
"test:trigger": "npm run build && node ./scripts/run-mock-eval.mjs trigger",
|
|
22
|
+
"test:functional": "npm run build && node ./scripts/run-mock-eval.mjs functional",
|
|
23
23
|
"test:unit": "tsx --test \"tests/**/*.test.ts\""
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|