vigiles 25.0.0 → 25.1.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/dist/adapters/codex/eval.d.ts +18 -0
- package/dist/adapters/codex/eval.js +27 -0
- package/dist/eval-cache.d.ts +7 -0
- package/dist/eval-lock.d.ts +20 -0
- package/dist/eval.d.ts +117 -4
- package/dist/eval.js +164 -32
- package/dist/scan-behavioral.d.ts +10 -0
- package/dist/scan-behavioral.js +1 -0
- package/package.json +1 -1
|
@@ -61,6 +61,24 @@ export declare function codexSkillFired(run: {
|
|
|
61
61
|
* turn. Pure fs — unit-testable without a binary.
|
|
62
62
|
*/
|
|
63
63
|
export declare function installCodexSkills(pluginDir: string, cwd: string): number;
|
|
64
|
+
/**
|
|
65
|
+
* Refuse an `effort` this adapter cannot honour, LOUDLY.
|
|
66
|
+
*
|
|
67
|
+
* The Claude runner pins the reasoning budget through `--effort` plus the env var
|
|
68
|
+
* it sits under. Codex exposes no mapping we have measured — its config carries a
|
|
69
|
+
* `model_reasoning_effort` key, but nothing here has driven the real binary
|
|
70
|
+
* through it, so claiming support would assert something unverified.
|
|
71
|
+
*
|
|
72
|
+
* The alternative — forwarding `task`/`cwd`/`timeoutMs` and dropping `effort` on
|
|
73
|
+
* the floor, as this runner does today — is the silent CC-only path the
|
|
74
|
+
* harness-parity rule forbids: a spec would declare `effort: "low"`, the lock
|
|
75
|
+
* would RECORD `low`, and the run would happen at whatever Codex defaults to.
|
|
76
|
+
* A stale number is recoverable; a confidently mislabelled one is not.
|
|
77
|
+
*
|
|
78
|
+
* Pure so the deferral itself is tested rather than living inside the ignored
|
|
79
|
+
* subprocess region.
|
|
80
|
+
*/
|
|
81
|
+
export declare function refuseCodexEffort(effort: string | number | undefined): void;
|
|
64
82
|
/**
|
|
65
83
|
* The Codex eval-tier `AgentRunner`: install the run's skills into `.codex/skills`
|
|
66
84
|
* (Codex's discovery path, vs Claude's `--plugin-dir`), then drive a real
|
|
@@ -32,6 +32,7 @@ exports.parseCodexEvalRun = parseCodexEvalRun;
|
|
|
32
32
|
exports.codexRunError = codexRunError;
|
|
33
33
|
exports.codexSkillFired = codexSkillFired;
|
|
34
34
|
exports.installCodexSkills = installCodexSkills;
|
|
35
|
+
exports.refuseCodexEffort = refuseCodexEffort;
|
|
35
36
|
exports.codexEvalAgentRunner = codexEvalAgentRunner;
|
|
36
37
|
exports.codexEvalRunner = codexEvalRunner;
|
|
37
38
|
const node_child_process_1 = require("node:child_process");
|
|
@@ -172,6 +173,31 @@ function installCodexSkills(pluginDir, cwd) {
|
|
|
172
173
|
}
|
|
173
174
|
return n;
|
|
174
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Refuse an `effort` this adapter cannot honour, LOUDLY.
|
|
178
|
+
*
|
|
179
|
+
* The Claude runner pins the reasoning budget through `--effort` plus the env var
|
|
180
|
+
* it sits under. Codex exposes no mapping we have measured — its config carries a
|
|
181
|
+
* `model_reasoning_effort` key, but nothing here has driven the real binary
|
|
182
|
+
* through it, so claiming support would assert something unverified.
|
|
183
|
+
*
|
|
184
|
+
* The alternative — forwarding `task`/`cwd`/`timeoutMs` and dropping `effort` on
|
|
185
|
+
* the floor, as this runner does today — is the silent CC-only path the
|
|
186
|
+
* harness-parity rule forbids: a spec would declare `effort: "low"`, the lock
|
|
187
|
+
* would RECORD `low`, and the run would happen at whatever Codex defaults to.
|
|
188
|
+
* A stale number is recoverable; a confidently mislabelled one is not.
|
|
189
|
+
*
|
|
190
|
+
* Pure so the deferral itself is tested rather than living inside the ignored
|
|
191
|
+
* subprocess region.
|
|
192
|
+
*/
|
|
193
|
+
function refuseCodexEffort(effort) {
|
|
194
|
+
if (effort === undefined)
|
|
195
|
+
return;
|
|
196
|
+
throw new Error(`effort (${JSON.stringify(effort)}) is not supported on the Codex adapter: ` +
|
|
197
|
+
`vigiles has not measured a mapping for it, so honouring the spec here ` +
|
|
198
|
+
`would record an effort the run did not use. Drop \`effort\` for this ` +
|
|
199
|
+
`harness, or run the eval on Claude Code.`);
|
|
200
|
+
}
|
|
175
201
|
/* v8 ignore start -- real codex subprocess; validated against the binary, not the unit gate */
|
|
176
202
|
/**
|
|
177
203
|
* The Codex eval-tier `AgentRunner`: install the run's skills into `.codex/skills`
|
|
@@ -180,6 +206,7 @@ function installCodexSkills(pluginDir, cwd) {
|
|
|
180
206
|
* codexEvalDriver })` dispatches through.
|
|
181
207
|
*/
|
|
182
208
|
function codexEvalAgentRunner(args) {
|
|
209
|
+
refuseCodexEffort(args.effort);
|
|
183
210
|
if (args.pluginDir)
|
|
184
211
|
installCodexSkills(args.pluginDir, args.cwd);
|
|
185
212
|
return Promise.resolve(codexEvalRunner({
|
package/dist/eval-cache.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export type CacheMode = "off" | "read" | "readwrite";
|
|
|
6
6
|
export interface CacheKeyInput {
|
|
7
7
|
readonly task: string;
|
|
8
8
|
readonly model: string;
|
|
9
|
+
/**
|
|
10
|
+
* Reasoning budget (`--effort`). Keyed for the same reason `model` is: it moves
|
|
11
|
+
* the output distribution, so a replay across effort levels would serve a result
|
|
12
|
+
* the caller did not ask for. `undefined` (the harness default) drops out of the
|
|
13
|
+
* hash via JSON, so entries recorded before effort existed stay valid.
|
|
14
|
+
*/
|
|
15
|
+
readonly effort?: string | number;
|
|
9
16
|
readonly tools: readonly string[];
|
|
10
17
|
/** The resolved fixture + arm + plugin files written before the run. */
|
|
11
18
|
readonly files: Record<string, string>;
|
package/dist/eval-lock.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export declare const DEFAULT_LOCK_DIR = ".vigiles/eval-locks";
|
|
|
33
33
|
export interface EvalLockInputs {
|
|
34
34
|
/** Model id used (folded in; a floating alias can't detect weight drift — warned). */
|
|
35
35
|
readonly model: string;
|
|
36
|
+
/**
|
|
37
|
+
* Reasoning budget (`--effort`) the run was pinned to, or undefined for the
|
|
38
|
+
* harness default. Hashed because it steers the model — the criterion this
|
|
39
|
+
* interface already states — so a committed report recorded at one effort is
|
|
40
|
+
* STALE for a run at another. `undefined` is dropped by `JSON.stringify`, so
|
|
41
|
+
* locks committed before effort existed keep their hash and still replay.
|
|
42
|
+
*
|
|
43
|
+
* Caveat kept honest: "omitted" means the harness's own default, which is
|
|
44
|
+
* per-model and can move between builds — reproducible only modulo that, the
|
|
45
|
+
* same class of provenance caveat as `harnessVersion` below.
|
|
46
|
+
*/
|
|
47
|
+
readonly effort?: string | number;
|
|
36
48
|
/**
|
|
37
49
|
* A hand-bumped behavior epoch the project owns (`.vigilesrc.json`
|
|
38
50
|
* `eval.apiVersion`), bumped when a harness-side change YOU made (a CLAUDE.md
|
|
@@ -71,6 +83,13 @@ export interface EvalLock {
|
|
|
71
83
|
readonly inputsHash: string;
|
|
72
84
|
/** The model id the report was produced against (for the drift warning). */
|
|
73
85
|
readonly model: string;
|
|
86
|
+
/**
|
|
87
|
+
* The effort the report was produced at, or undefined for the harness default
|
|
88
|
+
* (provenance; already in the hash). Recorded because the complaint that
|
|
89
|
+
* motivated effort support was not only that it could not be SET — it was that
|
|
90
|
+
* nothing in the run record said which effort produced the numbers.
|
|
91
|
+
*/
|
|
92
|
+
readonly effort?: string | number;
|
|
74
93
|
/** The harness version token at record time (provenance; already in the hash). */
|
|
75
94
|
readonly harnessVersionKey: string;
|
|
76
95
|
/** The behavior epoch at record time (provenance; already in the hash). */
|
|
@@ -142,6 +161,7 @@ export declare function buildLock(args: {
|
|
|
142
161
|
readonly name: string;
|
|
143
162
|
readonly inputsHash: string;
|
|
144
163
|
readonly model: string;
|
|
164
|
+
readonly effort?: string | number;
|
|
145
165
|
readonly harnessVersionKey: string;
|
|
146
166
|
readonly evalApiVersion: number;
|
|
147
167
|
readonly builtAt: string;
|
package/dist/eval.d.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface EvalArm {
|
|
|
46
46
|
* use the eval-level model. See `research/eval-architecture.md` (model strategy).
|
|
47
47
|
*/
|
|
48
48
|
readonly model?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Reasoning budget for the run (`claude --effort`, e.g. `"low"` or an integer).
|
|
51
|
+
* Lives here beside `model` because it is part of the MEASUREMENT — it moves the
|
|
52
|
+
* output distribution, not the sample size — so it is hashed into the lock and
|
|
53
|
+
* the cache, and never read from an env var. Omit for the harness default.
|
|
54
|
+
*/
|
|
55
|
+
readonly effort?: string | number;
|
|
49
56
|
}
|
|
50
57
|
/** Per-run resource use, parsed from the terminal `result` event (0 when absent). */
|
|
51
58
|
export interface EvalUsage {
|
|
@@ -95,6 +102,13 @@ export interface EvalSpec<M extends Metrics> {
|
|
|
95
102
|
readonly trials?: number;
|
|
96
103
|
/** Model alias. Default "haiku". */
|
|
97
104
|
readonly model?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Reasoning budget for the run (`claude --effort`, e.g. `"low"` or an integer).
|
|
107
|
+
* Lives here beside `model` because it is part of the MEASUREMENT — it moves the
|
|
108
|
+
* output distribution, not the sample size — so it is hashed into the lock and
|
|
109
|
+
* the cache, and never read from an env var. Omit for the harness default.
|
|
110
|
+
*/
|
|
111
|
+
readonly effort?: string | number;
|
|
98
112
|
/** Tools the agent may use. Default: Read Edit Write Bash. */
|
|
99
113
|
readonly allowedTools?: readonly string[];
|
|
100
114
|
/** Per-run timeout ms. Default 240000. */
|
|
@@ -229,6 +243,19 @@ export interface AgentRunArgs {
|
|
|
229
243
|
readonly task: string;
|
|
230
244
|
readonly cwd: string;
|
|
231
245
|
readonly model: string;
|
|
246
|
+
/**
|
|
247
|
+
* Reasoning-budget level for the run (`claude --effort`). Part of the
|
|
248
|
+
* MEASUREMENT, not a run knob: it changes the model's output distribution, not
|
|
249
|
+
* the sample size — so it lives on the spec next to `model` (never an env),
|
|
250
|
+
* and it is hashed into both the cache key and the eval lock. Deliberately
|
|
251
|
+
* `string | number` rather than a literal union: the binary accepts an alias
|
|
252
|
+
* map, is case-insensitive, and takes an integer budget, and its own valid set
|
|
253
|
+
* MOVED between builds (2.1.42 had no `xhigh`, 2.1.257 does) — a hard-coded
|
|
254
|
+
* union would reject a valid level after any upstream addition. A wrong value
|
|
255
|
+
* is caught at RUNTIME instead, by {@link effortRejection}, which is what the
|
|
256
|
+
* binary actually tells us. Omit for the harness default.
|
|
257
|
+
*/
|
|
258
|
+
readonly effort?: string | number;
|
|
232
259
|
readonly tools: readonly string[];
|
|
233
260
|
readonly hasSettings: boolean;
|
|
234
261
|
readonly pluginDir: string | undefined;
|
|
@@ -260,10 +287,74 @@ export type AgentRunner = (args: AgentRunArgs) => Promise<RunOut>;
|
|
|
260
287
|
* regression to an always-merge would otherwise silently defeat ephemerality and
|
|
261
288
|
* leak the host environment into an untrusted, model-driven run.
|
|
262
289
|
*/
|
|
263
|
-
export declare function resolveSpawnEnv(a: Pick<AgentRunArgs, "env" | "replaceEnv">, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
264
|
-
/**
|
|
265
|
-
*
|
|
266
|
-
|
|
290
|
+
export declare function resolveSpawnEnv(a: Pick<AgentRunArgs, "env" | "replaceEnv" | "effort">, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
291
|
+
/**
|
|
292
|
+
* The env var name the harness reads for the reasoning budget. It sits ABOVE the
|
|
293
|
+
* `--effort` flag in the CLI's own precedence chain, so passing the flag alone
|
|
294
|
+
* does NOT pin the level.
|
|
295
|
+
*/
|
|
296
|
+
export declare const EFFORT_ENV_VAR = "CLAUDE_CODE_EFFORT_LEVEL";
|
|
297
|
+
/**
|
|
298
|
+
* Pin the effort the run actually gets, so the recorded effort is the effort
|
|
299
|
+
* that ran.
|
|
300
|
+
*
|
|
301
|
+
* WHY THIS EXISTS AND WHY IT IS NOT OPTIONAL. Effort has THREE inputs — the
|
|
302
|
+
* `--effort` flag, the `effortLevel` settings key, and `CLAUDE_CODE_EFFORT_LEVEL`
|
|
303
|
+
* — and the env var wins over the flag. `EPHEMERAL_ALLOW_PREFIXES` passes
|
|
304
|
+
* `CLAUDE_*` through by design (the CLI reads several such knobs and dropping one
|
|
305
|
+
* is the failure mode), so an ambient `CLAUDE_CODE_EFFORT_LEVEL=max` in the
|
|
306
|
+
* author's shell survives even the SCRUBBED ephemeral env. Without this pin,
|
|
307
|
+
* hashing effort into the lock would make the lock CONFIDENTLY WRONG: it would
|
|
308
|
+
* record `low` over a run that executed at `max` — the exact defect the feature
|
|
309
|
+
* exists to prevent, reintroduced by the fix for it.
|
|
310
|
+
*
|
|
311
|
+
* Both directions matter, so both are handled:
|
|
312
|
+
* - effort DECLARED → set the var, overriding whatever the shell had.
|
|
313
|
+
* - effort OMITTED → DELETE an inherited var, so "omit" means the harness
|
|
314
|
+
* default rather than "whatever this machine happened to
|
|
315
|
+
* export". An omitted effort must not be a hidden input.
|
|
316
|
+
*/
|
|
317
|
+
export declare function pinEffortEnv(env: NodeJS.ProcessEnv, effort: string | number | undefined): NodeJS.ProcessEnv;
|
|
318
|
+
/**
|
|
319
|
+
* The harness's own rejection of an `--effort` value, or null. Pure.
|
|
320
|
+
*
|
|
321
|
+
* The CLI does NOT fail on a bad level — it prints this to stderr and silently
|
|
322
|
+
* runs at its default. That silent substitution is precisely the bug class this
|
|
323
|
+
* feature addresses (a number produced by a configuration nobody asked for), so
|
|
324
|
+
* a rejected value must never become a sample. Matched on the binary's own
|
|
325
|
+
* wording, the same shape as {@link isRateLimited}.
|
|
326
|
+
*/
|
|
327
|
+
export declare function effortRejection(out: RunOut): string | null;
|
|
328
|
+
/**
|
|
329
|
+
* Wrap a runner so a run the harness rejected on `--effort` FAILS LOUDLY.
|
|
330
|
+
*
|
|
331
|
+
* Applied ONCE, around the real runner, rather than as a guard repeated at each
|
|
332
|
+
* of the five `runner(...)` call sites — a guard per call site is the shape that
|
|
333
|
+
* left four of five compilers unprotected in #173.
|
|
334
|
+
*
|
|
335
|
+
* It THROWS rather than counting the trial as `runError`. A `runError` trial is
|
|
336
|
+
* dropped from the denominator, which is right for a transient (a rate limit) and
|
|
337
|
+
* wrong here: an unusable effort value is deterministic and repeatable, so every
|
|
338
|
+
* trial fails it and the run would report a rate computed over ZERO samples. A
|
|
339
|
+
* configuration mistake should stop the run and name itself.
|
|
340
|
+
*/
|
|
341
|
+
export declare function withEffortGuard(runner: AgentRunner): AgentRunner;
|
|
342
|
+
/**
|
|
343
|
+
* Build the real runner's argv. Pure and exported so the FLAGS are provable —
|
|
344
|
+
* `spawnAgentRaw` is `v8 ignore`d (it spawns a subprocess), so an argv assembled
|
|
345
|
+
* inline there could not be asserted at all. Mirrors `buildCodexArgs`.
|
|
346
|
+
*/
|
|
347
|
+
export declare function buildAgentArgs(a: AgentRunArgs): string[];
|
|
348
|
+
/**
|
|
349
|
+
* The real `claude`-spawning runner (composition root). Exported so other
|
|
350
|
+
* real-model entries (e.g. the `audit` trigger tier) bind the same runner.
|
|
351
|
+
*
|
|
352
|
+
* The effort guard is composed in HERE, at the single definition, rather than at
|
|
353
|
+
* each of the places that bind this runner — so every consumer, including ones
|
|
354
|
+
* not yet written, is covered by construction. Guarding each call site instead is
|
|
355
|
+
* the shape that left four of five compilers unprotected in #173.
|
|
356
|
+
*/
|
|
357
|
+
export declare const spawnAgent: AgentRunner;
|
|
267
358
|
/**
|
|
268
359
|
* Run the eval: every arm × every trial against the real `claude` CLI, with the
|
|
269
360
|
* metric computed per run and aggregated per arm. Requires `claude` on PATH and
|
|
@@ -320,6 +411,13 @@ export interface MeasureSpec {
|
|
|
320
411
|
readonly trials?: number;
|
|
321
412
|
/** Model alias. Default "sonnet" — measure on the model your users run. */
|
|
322
413
|
readonly model?: string;
|
|
414
|
+
/**
|
|
415
|
+
* Reasoning budget for the run (`claude --effort`, e.g. `"low"` or an integer).
|
|
416
|
+
* Lives here beside `model` because it is part of the MEASUREMENT — it moves the
|
|
417
|
+
* output distribution, not the sample size — so it is hashed into the lock and
|
|
418
|
+
* the cache, and never read from an env var. Omit for the harness default.
|
|
419
|
+
*/
|
|
420
|
+
readonly effort?: string | number;
|
|
323
421
|
/** Tools the agent may use. */
|
|
324
422
|
readonly allowedTools?: readonly string[];
|
|
325
423
|
/** Per-run timeout ms. */
|
|
@@ -375,6 +473,13 @@ export interface ArmsMeasureSpec {
|
|
|
375
473
|
readonly model?: string;
|
|
376
474
|
readonly allowedTools?: readonly string[];
|
|
377
475
|
readonly timeoutMs?: number;
|
|
476
|
+
/**
|
|
477
|
+
* Reasoning budget for the run (`claude --effort`, e.g. `"low"` or an integer).
|
|
478
|
+
* Lives here beside `model` because it is part of the MEASUREMENT — it moves the
|
|
479
|
+
* output distribution, not the sample size — so it is hashed into the lock and
|
|
480
|
+
* the cache, and never read from an env var. Omit for the harness default.
|
|
481
|
+
*/
|
|
482
|
+
readonly effort?: string | number;
|
|
378
483
|
readonly spacingSec?: number;
|
|
379
484
|
}
|
|
380
485
|
/** Per-arm {@link CheckReport}s — `arms[name].perCheck[i]` aligns across arms. */
|
|
@@ -471,6 +576,7 @@ export declare function runSkillSelectionTrial(args: {
|
|
|
471
576
|
readonly runner: AgentRunner;
|
|
472
577
|
readonly parse?: ModelOutputParser;
|
|
473
578
|
readonly model: string;
|
|
579
|
+
readonly effort?: string | number;
|
|
474
580
|
readonly tools?: readonly string[];
|
|
475
581
|
readonly timeoutMs?: number;
|
|
476
582
|
readonly fixture?: Record<string, string>;
|
|
@@ -658,6 +764,13 @@ export interface TriggerRateSpec {
|
|
|
658
764
|
* 0.50 on haiku vs 0.90 on Sonnet). Override for a cheaper-but-pessimistic run.
|
|
659
765
|
*/
|
|
660
766
|
readonly model?: string;
|
|
767
|
+
/**
|
|
768
|
+
* Reasoning budget for the run (`claude --effort`, e.g. `"low"` or an integer).
|
|
769
|
+
* Lives here beside `model` because it is part of the MEASUREMENT — it moves the
|
|
770
|
+
* output distribution, not the sample size — so it is hashed into the lock and
|
|
771
|
+
* the cache, and never read from an env var. Omit for the harness default.
|
|
772
|
+
*/
|
|
773
|
+
readonly effort?: string | number;
|
|
661
774
|
/**
|
|
662
775
|
* Minimum model tier this eval may run on (haiku<sonnet<opus by family). The
|
|
663
776
|
* run **fails** if the resolved `model` is weaker — trigger-rate under-measures
|
package/dist/eval.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.claudeEvalDriver = exports.EPHEMERAL_HOME_KEEP = void 0;
|
|
3
|
+
exports.claudeEvalDriver = exports.EPHEMERAL_HOME_KEEP = exports.spawnAgent = exports.EFFORT_ENV_VAR = void 0;
|
|
4
4
|
exports.resolveSpawnEnv = resolveSpawnEnv;
|
|
5
|
-
exports.
|
|
5
|
+
exports.pinEffortEnv = pinEffortEnv;
|
|
6
|
+
exports.effortRejection = effortRejection;
|
|
7
|
+
exports.withEffortGuard = withEffortGuard;
|
|
8
|
+
exports.buildAgentArgs = buildAgentArgs;
|
|
6
9
|
exports.unregisteredSkillFiles = unregisteredSkillFiles;
|
|
7
10
|
exports.runEval = runEval;
|
|
8
11
|
exports.measureWith = measureWith;
|
|
@@ -99,35 +102,136 @@ function writeFiles(cwd, files) {
|
|
|
99
102
|
* leak the host environment into an untrusted, model-driven run.
|
|
100
103
|
*/
|
|
101
104
|
function resolveSpawnEnv(a, base = process.env) {
|
|
102
|
-
|
|
105
|
+
const resolved = a.replaceEnv ? (a.env ?? {}) : { ...base, ...a.env };
|
|
106
|
+
return pinEffortEnv(resolved, a.effort);
|
|
103
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The env var name the harness reads for the reasoning budget. It sits ABOVE the
|
|
110
|
+
* `--effort` flag in the CLI's own precedence chain, so passing the flag alone
|
|
111
|
+
* does NOT pin the level.
|
|
112
|
+
*/
|
|
113
|
+
exports.EFFORT_ENV_VAR = "CLAUDE_CODE_EFFORT_LEVEL";
|
|
114
|
+
/**
|
|
115
|
+
* Pin the effort the run actually gets, so the recorded effort is the effort
|
|
116
|
+
* that ran.
|
|
117
|
+
*
|
|
118
|
+
* WHY THIS EXISTS AND WHY IT IS NOT OPTIONAL. Effort has THREE inputs — the
|
|
119
|
+
* `--effort` flag, the `effortLevel` settings key, and `CLAUDE_CODE_EFFORT_LEVEL`
|
|
120
|
+
* — and the env var wins over the flag. `EPHEMERAL_ALLOW_PREFIXES` passes
|
|
121
|
+
* `CLAUDE_*` through by design (the CLI reads several such knobs and dropping one
|
|
122
|
+
* is the failure mode), so an ambient `CLAUDE_CODE_EFFORT_LEVEL=max` in the
|
|
123
|
+
* author's shell survives even the SCRUBBED ephemeral env. Without this pin,
|
|
124
|
+
* hashing effort into the lock would make the lock CONFIDENTLY WRONG: it would
|
|
125
|
+
* record `low` over a run that executed at `max` — the exact defect the feature
|
|
126
|
+
* exists to prevent, reintroduced by the fix for it.
|
|
127
|
+
*
|
|
128
|
+
* Both directions matter, so both are handled:
|
|
129
|
+
* - effort DECLARED → set the var, overriding whatever the shell had.
|
|
130
|
+
* - effort OMITTED → DELETE an inherited var, so "omit" means the harness
|
|
131
|
+
* default rather than "whatever this machine happened to
|
|
132
|
+
* export". An omitted effort must not be a hidden input.
|
|
133
|
+
*/
|
|
134
|
+
function pinEffortEnv(env, effort) {
|
|
135
|
+
// Rebuilt WITHOUT the key rather than deleting or assigning `undefined`:
|
|
136
|
+
// omission has to be provable here, and whether a spawn drops an
|
|
137
|
+
// `undefined`-valued env entry is a Node-version detail we should not lean on.
|
|
138
|
+
const { [exports.EFFORT_ENV_VAR]: _inherited, ...rest } = env;
|
|
139
|
+
return effort === undefined
|
|
140
|
+
? rest
|
|
141
|
+
: { ...rest, [exports.EFFORT_ENV_VAR]: String(effort) };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The harness's own rejection of an `--effort` value, or null. Pure.
|
|
145
|
+
*
|
|
146
|
+
* The CLI does NOT fail on a bad level — it prints this to stderr and silently
|
|
147
|
+
* runs at its default. That silent substitution is precisely the bug class this
|
|
148
|
+
* feature addresses (a number produced by a configuration nobody asked for), so
|
|
149
|
+
* a rejected value must never become a sample. Matched on the binary's own
|
|
150
|
+
* wording, the same shape as {@link isRateLimited}.
|
|
151
|
+
*/
|
|
152
|
+
function effortRejection(out) {
|
|
153
|
+
const text = `${out.stderr ?? ""}\n${out.stdout}`;
|
|
154
|
+
const m = /Unknown --effort value[^\n]*/.exec(text);
|
|
155
|
+
return m ? m[0].trim() : null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Wrap a runner so a run the harness rejected on `--effort` FAILS LOUDLY.
|
|
159
|
+
*
|
|
160
|
+
* Applied ONCE, around the real runner, rather than as a guard repeated at each
|
|
161
|
+
* of the five `runner(...)` call sites — a guard per call site is the shape that
|
|
162
|
+
* left four of five compilers unprotected in #173.
|
|
163
|
+
*
|
|
164
|
+
* It THROWS rather than counting the trial as `runError`. A `runError` trial is
|
|
165
|
+
* dropped from the denominator, which is right for a transient (a rate limit) and
|
|
166
|
+
* wrong here: an unusable effort value is deterministic and repeatable, so every
|
|
167
|
+
* trial fails it and the run would report a rate computed over ZERO samples. A
|
|
168
|
+
* configuration mistake should stop the run and name itself.
|
|
169
|
+
*/
|
|
170
|
+
function withEffortGuard(runner) {
|
|
171
|
+
// 🔴 DELIBERATELY NOT `async`. An `async` wrapper turns the wrapped runner's
|
|
172
|
+
// SYNCHRONOUS throws into rejected promises, and the real runner refuses
|
|
173
|
+
// synchronously on purpose — `refuseDuringEvalLoad` / `refuseUnderForeignRunner`
|
|
174
|
+
// stop a paid eval from billing when a foreign test runner collects it. Making
|
|
175
|
+
// this `async` silently downgraded those refusals from "throws at the call" to
|
|
176
|
+
// "returns a promise that rejects", which `assert.throws` cannot see and an
|
|
177
|
+
// un-awaited caller would not notice. Caught by the full suite, not by the
|
|
178
|
+
// targeted one; pinned below by `withEffortGuard preserves a SYNCHRONOUS throw`.
|
|
179
|
+
return (a) => {
|
|
180
|
+
const pending = runner(a);
|
|
181
|
+
return pending.then((out) => {
|
|
182
|
+
const rejection = effortRejection(out);
|
|
183
|
+
if (rejection !== null) {
|
|
184
|
+
throw new Error(`the harness rejected effort ${JSON.stringify(a.effort)}: ${rejection}`);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Build the real runner's argv. Pure and exported so the FLAGS are provable —
|
|
192
|
+
* `spawnAgentRaw` is `v8 ignore`d (it spawns a subprocess), so an argv assembled
|
|
193
|
+
* inline there could not be asserted at all. Mirrors `buildCodexArgs`.
|
|
194
|
+
*/
|
|
195
|
+
function buildAgentArgs(a) {
|
|
196
|
+
return [
|
|
197
|
+
"-p",
|
|
198
|
+
a.task,
|
|
199
|
+
// stream-json (+ --verbose, required with -p) so the per-turn tool_use
|
|
200
|
+
// events survive into `ctx.toolCalls` — the unified Trace, same as the
|
|
201
|
+
// harness tier. The terminal `result` event still carries num_turns/output.
|
|
202
|
+
"--output-format",
|
|
203
|
+
"stream-json",
|
|
204
|
+
"--verbose",
|
|
205
|
+
"--model",
|
|
206
|
+
a.model,
|
|
207
|
+
...(a.effort !== undefined ? ["--effort", String(a.effort)] : []),
|
|
208
|
+
"--permission-mode",
|
|
209
|
+
"acceptEdits",
|
|
210
|
+
...(a.pluginDir !== undefined
|
|
211
|
+
? ["--plugin-dir", (0, node_path_1.resolve)(a.pluginDir)]
|
|
212
|
+
: []),
|
|
213
|
+
...(a.hasSettings ? ["--settings", "settings.json"] : []),
|
|
214
|
+
"--allowedTools",
|
|
215
|
+
...a.tools,
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The real `claude`-spawning runner (composition root). Exported so other
|
|
220
|
+
* real-model entries (e.g. the `audit` trigger tier) bind the same runner.
|
|
221
|
+
*
|
|
222
|
+
* The effort guard is composed in HERE, at the single definition, rather than at
|
|
223
|
+
* each of the places that bind this runner — so every consumer, including ones
|
|
224
|
+
* not yet written, is covered by construction. Guarding each call site instead is
|
|
225
|
+
* the shape that left four of five compilers unprotected in #173.
|
|
226
|
+
*/
|
|
227
|
+
exports.spawnAgent = withEffortGuard(spawnAgentRaw);
|
|
104
228
|
/* v8 ignore start -- real claude subprocess; exercised by bench/, not the unit gate */
|
|
105
|
-
/** The
|
|
106
|
-
|
|
107
|
-
function spawnAgent(a) {
|
|
229
|
+
/** The unguarded spawn itself; wrapped by {@link spawnAgent}, never bound raw. */
|
|
230
|
+
function spawnAgentRaw(a) {
|
|
108
231
|
(0, eval_load_phase_js_1.refuseDuringEvalLoad)("spawning `claude`");
|
|
109
232
|
(0, foreign_runner_js_1.refuseUnderForeignRunner)("spawning `claude`");
|
|
110
233
|
return new Promise((resolvePromise) => {
|
|
111
|
-
const args =
|
|
112
|
-
"-p",
|
|
113
|
-
a.task,
|
|
114
|
-
// stream-json (+ --verbose, required with -p) so the per-turn tool_use
|
|
115
|
-
// events survive into `ctx.toolCalls` — the unified Trace, same as the
|
|
116
|
-
// harness tier. The terminal `result` event still carries num_turns/output.
|
|
117
|
-
"--output-format",
|
|
118
|
-
"stream-json",
|
|
119
|
-
"--verbose",
|
|
120
|
-
"--model",
|
|
121
|
-
a.model,
|
|
122
|
-
"--permission-mode",
|
|
123
|
-
"acceptEdits",
|
|
124
|
-
...(a.pluginDir !== undefined
|
|
125
|
-
? ["--plugin-dir", (0, node_path_1.resolve)(a.pluginDir)]
|
|
126
|
-
: []),
|
|
127
|
-
...(a.hasSettings ? ["--settings", "settings.json"] : []),
|
|
128
|
-
"--allowedTools",
|
|
129
|
-
...a.tools,
|
|
130
|
-
];
|
|
234
|
+
const args = buildAgentArgs(a);
|
|
131
235
|
const child = (0, node_child_process_1.spawn)(runtime_js_1.claudeCodeRuntime.agentBinary, args, {
|
|
132
236
|
cwd: a.cwd,
|
|
133
237
|
// The security-critical env resolution (overlay vs. scrubbed replacement)
|
|
@@ -194,7 +298,7 @@ function warnUnregisteredSkillArms(arms) {
|
|
|
194
298
|
}
|
|
195
299
|
async function runEval(spec) {
|
|
196
300
|
warnUnregisteredSkillArms(spec.arms);
|
|
197
|
-
const report = await runEvalWith(spec, spawnAgent);
|
|
301
|
+
const report = await runEvalWith(spec, exports.spawnAgent);
|
|
198
302
|
// Surface what the run spent — tokens + API-equivalent $, and a LOUD warning if
|
|
199
303
|
// it was billed to a metered API key instead of the subscription. See eval-cost.ts.
|
|
200
304
|
(0, eval_cost_js_1.emitCostSummary)((0, eval_cost_js_1.costFromEvalReport)(report));
|
|
@@ -237,6 +341,7 @@ async function measureWith(spec, runner) {
|
|
|
237
341
|
task: spec.task,
|
|
238
342
|
trials: spec.trials ?? 5,
|
|
239
343
|
model: spec.model ?? "sonnet",
|
|
344
|
+
effort: spec.effort,
|
|
240
345
|
allowedTools: spec.allowedTools,
|
|
241
346
|
timeoutMs: spec.timeoutMs,
|
|
242
347
|
spacingSec: spec.spacingSec,
|
|
@@ -266,7 +371,7 @@ async function measureWith(spec, runner) {
|
|
|
266
371
|
/* v8 ignore start -- real claude subprocess; thin wrapper over measureWith */
|
|
267
372
|
/** Score a check vocabulary across trials against the real `claude` CLI. */
|
|
268
373
|
async function measure(spec) {
|
|
269
|
-
const report = await measureWith(spec, spawnAgent);
|
|
374
|
+
const report = await measureWith(spec, exports.spawnAgent);
|
|
270
375
|
(0, eval_cost_js_1.emitCostSummary)((0, eval_cost_js_1.costFromArm)(report.usage));
|
|
271
376
|
return report;
|
|
272
377
|
}
|
|
@@ -283,6 +388,7 @@ async function measureArmsWith(spec, runner) {
|
|
|
283
388
|
task: spec.task,
|
|
284
389
|
trials: spec.trials ?? 5,
|
|
285
390
|
model: spec.model ?? "sonnet",
|
|
391
|
+
effort: spec.effort,
|
|
286
392
|
allowedTools: spec.allowedTools,
|
|
287
393
|
timeoutMs: spec.timeoutMs,
|
|
288
394
|
spacingSec: spec.spacingSec,
|
|
@@ -337,7 +443,7 @@ function stubArmPluginDirs(arms) {
|
|
|
337
443
|
/** Score checks across arms against the real `claude` CLI. */
|
|
338
444
|
async function measureArms(spec) {
|
|
339
445
|
warnUnregisteredSkillArms(spec.arms);
|
|
340
|
-
const report = await measureArmsWith(spec, spawnAgent);
|
|
446
|
+
const report = await measureArmsWith(spec, exports.spawnAgent);
|
|
341
447
|
// Sum every arm's spend — an A/B run pays for both arms.
|
|
342
448
|
(0, eval_cost_js_1.emitCostSummary)((0, eval_cost_js_1.sumCosts)(Object.values(report.arms).map((a) => (0, eval_cost_js_1.costFromArm)(a.usage))));
|
|
343
449
|
return report;
|
|
@@ -552,6 +658,7 @@ async function runSkillSelectionTrial(args) {
|
|
|
552
658
|
task: args.prompt,
|
|
553
659
|
cwd,
|
|
554
660
|
model: args.model,
|
|
661
|
+
effort: args.effort,
|
|
555
662
|
tools: args.tools ?? ["Read", "Edit", "Write", "Bash", "Skill"],
|
|
556
663
|
hasSettings: false,
|
|
557
664
|
pluginDir: args.pluginDir,
|
|
@@ -637,6 +744,7 @@ async function runWithCache(runArgs, keyParts, runner, cfg) {
|
|
|
637
744
|
const key = (0, eval_cache_js_1.cacheKey)({
|
|
638
745
|
task: runArgs.task,
|
|
639
746
|
model: runArgs.model,
|
|
747
|
+
effort: runArgs.effort,
|
|
640
748
|
tools: runArgs.tools,
|
|
641
749
|
files: keyParts.files,
|
|
642
750
|
settings: keyParts.settings,
|
|
@@ -967,6 +1075,7 @@ async function executeTrial(spec, arm, trialIndex, runner, cfg) {
|
|
|
967
1075
|
cwd,
|
|
968
1076
|
// A model comparison is a harness A/B: an arm may override the model.
|
|
969
1077
|
model: arm.model ?? cfg.model,
|
|
1078
|
+
effort: arm.effort ?? cfg.effort,
|
|
970
1079
|
tools: cfg.tools,
|
|
971
1080
|
hasSettings,
|
|
972
1081
|
pluginDir: arm.pluginDir,
|
|
@@ -1100,8 +1209,16 @@ async function withEvalLock(args, produce) {
|
|
|
1100
1209
|
}
|
|
1101
1210
|
if (!isDatedModel(args.model))
|
|
1102
1211
|
warnFloatingModel(args.model);
|
|
1212
|
+
// OVERLAP, DELIBERATE — do not delete this as dead. Effort reaches the hash
|
|
1213
|
+
// twice: here (the CHOKEPOINT every seam passes through, so a future seam that
|
|
1214
|
+
// forgets to fold effort into its own `inputs` is still covered) and inside
|
|
1215
|
+
// each seam's `inputs` (which alone can see a PER-ARM override this line
|
|
1216
|
+
// cannot). Measured 2026-09-01: removing either one alone leaves the suite
|
|
1217
|
+
// green; removing BOTH fails `changing effort makes a committed lock STALE`.
|
|
1218
|
+
// That is two populations covered, not one line duplicated.
|
|
1103
1219
|
const inputsHash = (0, eval_lock_js_1.evalInputsHash)({
|
|
1104
1220
|
model: args.model,
|
|
1221
|
+
effort: args.effort,
|
|
1105
1222
|
evalApiVersion: lock.evalApiVersion,
|
|
1106
1223
|
inputs: args.inputs,
|
|
1107
1224
|
});
|
|
@@ -1116,6 +1233,7 @@ async function withEvalLock(args, produce) {
|
|
|
1116
1233
|
name: args.name,
|
|
1117
1234
|
inputsHash,
|
|
1118
1235
|
model: args.model,
|
|
1236
|
+
effort: args.effort,
|
|
1119
1237
|
harnessVersionKey: harnessVersion(),
|
|
1120
1238
|
evalApiVersion: lock.evalApiVersion,
|
|
1121
1239
|
builtAt: new Date().toISOString(),
|
|
@@ -1166,6 +1284,10 @@ function evalArmsInputs(spec, cfg) {
|
|
|
1166
1284
|
const absRoot = arm.plugin ? (0, node_path_1.resolve)(process.cwd(), arm.plugin) : "";
|
|
1167
1285
|
arms[name] = {
|
|
1168
1286
|
model: arm.model ?? cfg.model,
|
|
1287
|
+
// The per-ARM half of the overlap documented at `inputsHash` — the
|
|
1288
|
+
// chokepoint sees only the eval-level effort, so an arm that overrides it
|
|
1289
|
+
// would otherwise hash identically to its sibling.
|
|
1290
|
+
effort: arm.effort ?? cfg.effort,
|
|
1169
1291
|
tools: [...cfg.tools].sort(),
|
|
1170
1292
|
files: stripPluginRoot(resolved.files, absRoot),
|
|
1171
1293
|
settings: stripPluginRoot(resolved.settings, absRoot),
|
|
@@ -1203,6 +1325,7 @@ async function runEvalWith(spec, runner) {
|
|
|
1203
1325
|
const backoffMs = spec.retryBackoffMs ?? 1000;
|
|
1204
1326
|
const cfg = {
|
|
1205
1327
|
model: spec.model ?? "haiku",
|
|
1328
|
+
effort: spec.effort,
|
|
1206
1329
|
tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash"],
|
|
1207
1330
|
timeoutMs: spec.timeoutMs ?? 240000,
|
|
1208
1331
|
cache: spec.cache ?? "off",
|
|
@@ -1232,7 +1355,7 @@ async function runEvalWith(spec, runner) {
|
|
|
1232
1355
|
// resolveHarness/hashDir work). `check` replays the committed report below
|
|
1233
1356
|
// without ever entering the run pool — so no model is driven in CI.
|
|
1234
1357
|
const inputs = lock.mode === "off" ? undefined : evalArmsInputs(spec, cfg);
|
|
1235
|
-
return withEvalLock({ name: spec.name, inputs, model: cfg.model, lock }, async () => {
|
|
1358
|
+
return withEvalLock({ name: spec.name, inputs, model: cfg.model, effort: cfg.effort, lock }, async () => {
|
|
1236
1359
|
const results = await runPool(units, concurrency, worker);
|
|
1237
1360
|
const { arms, totalCostUsd } = aggregateArms(Object.keys(spec.arms), results);
|
|
1238
1361
|
return { name: spec.name ?? "eval", trials, arms, totalCostUsd, aborted };
|
|
@@ -1285,7 +1408,7 @@ function formatEvalReport(report) {
|
|
|
1285
1408
|
* The asymmetry reflects default-vs-injected, not a hexagonal violation.
|
|
1286
1409
|
*/
|
|
1287
1410
|
exports.claudeEvalDriver = {
|
|
1288
|
-
runner: spawnAgent,
|
|
1411
|
+
runner: exports.spawnAgent,
|
|
1289
1412
|
parse: parseClaudeRun,
|
|
1290
1413
|
harness: "claude-code",
|
|
1291
1414
|
};
|
|
@@ -1591,6 +1714,7 @@ async function runTriggerTrial(prompt, cfg, runner) {
|
|
|
1591
1714
|
task: prompt,
|
|
1592
1715
|
cwd,
|
|
1593
1716
|
model: cfg.model,
|
|
1717
|
+
effort: cfg.effort,
|
|
1594
1718
|
tools: cfg.tools,
|
|
1595
1719
|
hasSettings: false,
|
|
1596
1720
|
pluginDir: cfg.pluginDir,
|
|
@@ -1690,6 +1814,7 @@ async function measureTriggerRateWith(spec, runner, parse = parseClaudeRun, runE
|
|
|
1690
1814
|
// Sonnet, not haiku: trigger-rate is a selection measurement and haiku
|
|
1691
1815
|
// under-selects, producing false-negative recall (see TriggerRateSpec.model).
|
|
1692
1816
|
model,
|
|
1817
|
+
effort: spec.effort,
|
|
1693
1818
|
tools: spec.allowedTools ?? ["Read", "Edit", "Write", "Bash", "Skill"],
|
|
1694
1819
|
timeoutMs: spec.timeoutMs ?? 240000,
|
|
1695
1820
|
spacing: (spec.spacingSec ?? 4) * 1000,
|
|
@@ -1715,6 +1840,7 @@ async function measureTriggerRateWith(spec, runner, parse = parseClaudeRun, runE
|
|
|
1715
1840
|
? [...spec.irrelevantPrompts]
|
|
1716
1841
|
: undefined,
|
|
1717
1842
|
model: cfg.model,
|
|
1843
|
+
effort: cfg.effort,
|
|
1718
1844
|
tools: [...cfg.tools].sort(),
|
|
1719
1845
|
fixture: spec.fixture,
|
|
1720
1846
|
competitors,
|
|
@@ -1723,7 +1849,13 @@ async function measureTriggerRateWith(spec, runner, parse = parseClaudeRun, runE
|
|
|
1723
1849
|
// STALE if the eval is switched to another harness.
|
|
1724
1850
|
harness,
|
|
1725
1851
|
};
|
|
1726
|
-
return await withEvalLock({
|
|
1852
|
+
return await withEvalLock({
|
|
1853
|
+
name: spec.name,
|
|
1854
|
+
inputs: triggerInputs,
|
|
1855
|
+
model: cfg.model,
|
|
1856
|
+
effort: cfg.effort,
|
|
1857
|
+
lock,
|
|
1858
|
+
}, async () => {
|
|
1727
1859
|
const relevant = await runTriggerSet(spec.prompts, cfg, runner);
|
|
1728
1860
|
const base = {
|
|
1729
1861
|
rate: relevant.n > 0 ? relevant.fired / relevant.n : 0,
|
|
@@ -89,6 +89,16 @@ export interface SelectionOptions {
|
|
|
89
89
|
readonly trials?: number;
|
|
90
90
|
/** Selector model — defaults to Sonnet (a weaker model under-selects). */
|
|
91
91
|
readonly model?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Reasoning budget (`claude --effort`) for the selector, or undefined for the
|
|
94
|
+
* harness default. Present for {@link measureSelectionMatrix}, the ASSERTABLE
|
|
95
|
+
* test primitive, where the configuration a number came from has to be pinnable.
|
|
96
|
+
*
|
|
97
|
+
* Deliberately NOT exposed as an `audit` CLI flag: `audit` is a local report,
|
|
98
|
+
* not a reproducibility surface, and a flag nobody can act on is surface without
|
|
99
|
+
* a use. The audit probe therefore leaves this unset and runs at the default.
|
|
100
|
+
*/
|
|
101
|
+
readonly effort?: string | number;
|
|
92
102
|
/** Parallel runs across the prompts × trials grid (default 1). */
|
|
93
103
|
readonly concurrency?: number;
|
|
94
104
|
/** Which harness drives it (default `"claude-code"`; others report n/a). */
|
package/dist/scan-behavioral.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "25.
|
|
3
|
+
"version": "25.1.0",
|
|
4
4
|
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|