@fede0089/skill-eval 1.4.0 → 2.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 +19 -19
- package/dist/core/environment.js +2 -0
- package/dist/index.js +12 -10
- package/dist/runners/codex/index.js +1 -0
- package/dist/runners/codex/runner.js +330 -0
- package/dist/runners/index.js +1 -0
- package/dist/runners/registry.js +2 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ For each eval prompt, skill-eval spins up parallel agent processes with the curr
|
|
|
20
20
|
└───────┬───────┘
|
|
21
21
|
│
|
|
22
22
|
┌───────────┴───────────┐
|
|
23
|
-
─ with skill ─
|
|
23
|
+
─ with skill ─ ─ baseline (opt) ─
|
|
24
24
|
┌──────┴──────┐ ┌─────┴──────┐
|
|
25
25
|
agent 1 agent 2 agent 3 agent 4
|
|
26
26
|
│ │ │ │
|
|
@@ -32,10 +32,12 @@ For each eval prompt, skill-eval spins up parallel agent processes with the curr
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
> The `trigger` command only runs with-skill trials and checks whether the skill dispatch tool was actually invoked — no judge or baseline needed.
|
|
35
|
+
>
|
|
36
|
+
> The baseline branch is opt-in: enable it with `--compare-baseline` (no-skill control) or `--compare-ref <ref>` (historical skill versions).
|
|
35
37
|
|
|
36
38
|
## Installation
|
|
37
39
|
|
|
38
|
-
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini`) installed and on `$PATH`.
|
|
40
|
+
**Requirements:** Node.js, and the agent CLI you want to evaluate (e.g. `gemini` or `codex`) installed and on `$PATH`.
|
|
39
41
|
|
|
40
42
|
### Run without installing
|
|
41
43
|
|
|
@@ -85,6 +87,11 @@ skill-eval functional --workspace <path> --skill <path> [options] [agent]
|
|
|
85
87
|
| `-v, --debug` | no | `false` | Enable verbose debug logging |
|
|
86
88
|
| `[agent]` | no | `gemini-cli` | Agent backend to use |
|
|
87
89
|
|
|
90
|
+
Supported runners:
|
|
91
|
+
|
|
92
|
+
- `gemini-cli` (default)
|
|
93
|
+
- `codex`
|
|
94
|
+
|
|
88
95
|
### Skill directory structure
|
|
89
96
|
|
|
90
97
|
```
|
|
@@ -93,8 +100,10 @@ my-skill/
|
|
|
93
100
|
└── evals/ # evaluation suite (required)
|
|
94
101
|
├── my-evals.json # one or more eval files (*.json)
|
|
95
102
|
└── config/ # runner configuration (optional but often needed)
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
├── gemini-cli/ # copied to <worktree>/.gemini/ before each trial
|
|
104
|
+
│ └── settings.json
|
|
105
|
+
└── codex/ # copied to <worktree>/.codex/ before each trial
|
|
106
|
+
└── config.toml
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
All `.json` files in `evals/` are loaded and merged into a single suite — you can split them by feature or regression category.
|
|
@@ -132,12 +141,13 @@ All `.json` files in `evals/` are loaded and merged into a single suite — you
|
|
|
132
141
|
|
|
133
142
|
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.
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
Each runner configures its own non-interactive mode. For example, Gemini CLI uses `--approval-mode auto_edit`, while Codex uses `codex exec --json --sandbox workspace-write -c approval_policy="never"`. 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.
|
|
136
145
|
|
|
137
146
|
**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:
|
|
138
147
|
|
|
139
148
|
```
|
|
140
149
|
evals/config/gemini-cli/ → <worktree>/.gemini/
|
|
150
|
+
evals/config/codex/ → <worktree>/.codex/
|
|
141
151
|
```
|
|
142
152
|
|
|
143
153
|
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:
|
|
@@ -157,8 +167,11 @@ Refer to your runner's documentation for the full list of available settings and
|
|
|
157
167
|
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:
|
|
158
168
|
|
|
159
169
|
```sh
|
|
170
|
+
npm run test:unit # run the unit test suite
|
|
160
171
|
npm run test:trigger # trigger evaluation against mock-skill
|
|
161
172
|
npm run test:functional # functional evaluation against mock-skill
|
|
173
|
+
npm run test:trigger -- codex # run trigger evals with Codex
|
|
174
|
+
npm run test:functional -- codex # run functional evals with Codex
|
|
162
175
|
```
|
|
163
176
|
|
|
164
177
|
Results are saved to `.project-skill-evals/runs/<timestamp>/` with logs, raw eval JSONs, and an HTML report.
|
|
@@ -181,17 +194,4 @@ The factory, preflight check, and CLI all pick it up automatically.
|
|
|
181
194
|
### Adding a new report format
|
|
182
195
|
|
|
183
196
|
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
184
|
-
2.
|
|
185
|
-
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
186
|
-
er, binary: '<cli-binary-name>' },
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
The factory, preflight check, and CLI all pick it up automatically.
|
|
190
|
-
|
|
191
|
-
> 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.
|
|
192
|
-
|
|
193
|
-
### Adding a new report format
|
|
194
|
-
|
|
195
|
-
1. Create `src/reporters/<format>-reporter.ts` implementing `Reporter`.
|
|
196
|
-
2. Export it and add a case in `createReporter()` in `src/reporters/index.ts`.
|
|
197
|
-
3. Add the format string to `ReportFormat` in `src/types/index.ts`.
|
|
197
|
+
2. Add a case for it in `createReporter()` in `src/reporters/index.ts`.
|
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.
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,9 @@ import { HtmlReporter } from './reporters/index.js';
|
|
|
8
8
|
import { DEFAULT_AGENT } from './runners/registry.js';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
|
+
import { createRequire } from 'module';
|
|
11
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
const pkg = createRequire(import.meta.url)('../package.json');
|
|
12
14
|
export const program = new Command();
|
|
13
15
|
const errorHandler = (err) => {
|
|
14
16
|
if (err instanceof AppError) {
|
|
@@ -26,7 +28,7 @@ const errorHandler = (err) => {
|
|
|
26
28
|
program
|
|
27
29
|
.name('skill-eval')
|
|
28
30
|
.description('CLI to evaluate agent skills triggering and functionality')
|
|
29
|
-
.version(
|
|
31
|
+
.version(pkg.version)
|
|
30
32
|
.option('-v, --debug', 'Enable debug logging', false);
|
|
31
33
|
program.on('option:debug', () => {
|
|
32
34
|
process.env.DEBUG = 'true';
|
|
@@ -36,16 +38,16 @@ program
|
|
|
36
38
|
.description('Evaluate triggering of an agent skill')
|
|
37
39
|
.requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
|
|
38
40
|
.requiredOption('--skill <path>', 'Path to the skill directory')
|
|
39
|
-
.option('--agents <number>', 'Number of parallel agents')
|
|
40
|
-
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
41
|
+
.option('--agents <number>', 'Number of parallel agents', '4')
|
|
42
|
+
.option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
|
|
41
43
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
42
44
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
43
45
|
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
44
46
|
.action((agent, options) => {
|
|
45
47
|
const workspace = path.resolve(options.workspace);
|
|
46
48
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
47
|
-
const maxAgents = parseInt(options.agents, 10)
|
|
48
|
-
const numTrials =
|
|
49
|
+
const maxAgents = parseInt(options.agents, 10);
|
|
50
|
+
const numTrials = parseInt(options.trials, 10);
|
|
49
51
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
50
52
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
51
53
|
const compareRefs = options.compareRef || [];
|
|
@@ -53,11 +55,11 @@ program
|
|
|
53
55
|
});
|
|
54
56
|
program
|
|
55
57
|
.command('functional [agent]')
|
|
56
|
-
.description('Evaluate functional correctness of an agent skill
|
|
58
|
+
.description('Evaluate functional correctness of an agent skill against expectations')
|
|
57
59
|
.requiredOption('--workspace <path>', 'Path to the workspace/repo to evaluate against')
|
|
58
60
|
.requiredOption('--skill <path>', 'Path to the skill directory')
|
|
59
|
-
.option('--agents <number>', 'Number of parallel agents')
|
|
60
|
-
.option('--trials <number>', 'Number of trials per task for pass@k calculation')
|
|
61
|
+
.option('--agents <number>', 'Number of parallel agents', '4')
|
|
62
|
+
.option('--trials <number>', 'Number of trials per task for pass@k calculation', '3')
|
|
61
63
|
.option('--timeout <seconds>', 'Agent timeout in seconds')
|
|
62
64
|
.option('--eval-id <id>', 'Run only the eval with this ID (numeric)')
|
|
63
65
|
.option('--compare-ref [refs...]', 'Compare against historical git references')
|
|
@@ -65,8 +67,8 @@ program
|
|
|
65
67
|
.action((agent, options) => {
|
|
66
68
|
const workspace = path.resolve(options.workspace);
|
|
67
69
|
const selectedAgent = agent || DEFAULT_AGENT;
|
|
68
|
-
const maxAgents = parseInt(options.agents, 10)
|
|
69
|
-
const numTrials =
|
|
70
|
+
const maxAgents = parseInt(options.agents, 10);
|
|
71
|
+
const numTrials = parseInt(options.trials, 10);
|
|
70
72
|
const timeoutMs = options.timeout ? parseInt(options.timeout, 10) * 1000 : undefined;
|
|
71
73
|
const evalId = options.evalId !== undefined ? parseInt(options.evalId, 10) : undefined;
|
|
72
74
|
const compareRefs = options.compareRef || [];
|
|
@@ -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,8 @@
|
|
|
1
1
|
import { GeminiCliRunner } from './gemini-cli/index.js';
|
|
2
|
+
import { CodexRunner } from './codex/index.js';
|
|
2
3
|
export const RUNNER_REGISTRY = {
|
|
3
4
|
'gemini-cli': { Runner: GeminiCliRunner, binary: 'gemini' },
|
|
5
|
+
codex: { Runner: CodexRunner, binary: 'codex' },
|
|
4
6
|
};
|
|
5
7
|
/** Default agent name used when none is specified on the CLI. */
|
|
6
8
|
export const DEFAULT_AGENT = Object.keys(RUNNER_REGISTRY)[0];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fede0089/skill-eval",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI to evaluate agent skills triggering",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI to evaluate agent skills triggering and functionality",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"skill-eval": "dist/index.js"
|
|
@@ -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",
|