vigiles 15.0.2 → 15.0.3
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/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/run-scripts.d.ts +21 -9
- package/dist/adapters/claude-code/run-scripts.js +33 -22
- package/dist/adapters/codex/driver.js +3 -1
- package/dist/adapters/codex/eval.js +5 -0
- package/dist/audit-score.js +32 -6
- package/dist/check-count.d.ts +62 -3
- package/dist/check-count.js +123 -5
- package/dist/check.d.ts +55 -0
- package/dist/check.js +91 -0
- package/dist/cli.js +456 -54
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +278 -0
- package/dist/core/foreign-runner.d.ts +185 -0
- package/dist/core/foreign-runner.js +228 -0
- package/dist/core/harness-driver.d.ts +18 -1
- package/dist/core/hook-program.d.ts +319 -20
- package/dist/core/hook-program.js +743 -49
- package/dist/core/layout.d.ts +13 -0
- package/dist/core/lethal-trifecta.d.ts +93 -0
- package/dist/core/lethal-trifecta.js +409 -65
- package/dist/core/markdown.d.ts +32 -0
- package/dist/core/markdown.js +36 -0
- package/dist/core/merge-conflict.d.ts +50 -0
- package/dist/core/merge-conflict.js +84 -0
- package/dist/core/test-file-ext.d.ts +80 -0
- package/dist/core/test-file-ext.js +90 -0
- package/dist/core/types.d.ts +11 -0
- package/dist/coverage-artifact.d.ts +383 -0
- package/dist/coverage-artifact.js +586 -0
- package/dist/coverage-evidence.d.ts +87 -79
- package/dist/coverage-evidence.js +212 -211
- package/dist/coverage-probe.d.ts +125 -0
- package/dist/coverage-probe.js +700 -0
- package/dist/doc-commands.d.ts +119 -0
- package/dist/doc-commands.js +158 -0
- package/dist/eval-lock.d.ts +9 -0
- package/dist/eval-lock.js +14 -2
- package/dist/eval.js +28 -0
- package/dist/fs-walk.d.ts +80 -0
- package/dist/fs-walk.js +148 -0
- package/dist/harness-assert.js +42 -3
- package/dist/harness-test.js +31 -2
- package/dist/judge.js +4 -0
- package/dist/leaderboard.d.ts +16 -1
- package/dist/leaderboard.js +19 -2
- package/dist/load-hook.js +6 -1
- package/dist/mock-model.d.ts +58 -4
- package/dist/mock-model.js +133 -5
- package/dist/observe.d.ts +1 -1
- package/dist/observe.js +44 -1
- package/dist/plugin-loader.js +52 -13
- package/dist/run-hook.d.ts +58 -0
- package/dist/run-hook.js +70 -0
- package/dist/run-script.js +75 -0
- package/dist/scaffold-test.d.ts +3 -0
- package/dist/scaffold-test.js +10 -5
- package/dist/scan-behavioral.js +4 -0
- package/dist/scan-core.d.ts +22 -0
- package/dist/scan-core.js +46 -14
- package/dist/scan-files.js +19 -2
- package/dist/scan.d.ts +22 -5
- package/dist/scan.js +67 -13
- package/dist/skill-contract.d.ts +46 -0
- package/dist/skill-contract.js +201 -0
- package/dist/skill-refs.d.ts +67 -0
- package/dist/skill-refs.js +117 -0
- package/dist/test-coverage-files.d.ts +7 -1
- package/dist/test-coverage-files.js +66 -55
- package/dist/test-coverage.d.ts +155 -19
- package/dist/test-coverage.js +430 -91
- package/dist/testing.d.ts +8 -2
- package/dist/testing.js +19 -1
- package/dist/trigger-containment.d.ts +85 -0
- package/dist/trigger-containment.js +125 -0
- package/dist/ts-runner-caps.d.ts +16 -0
- package/dist/ts-runner-caps.js +43 -0
- package/dist/unit.d.ts +2 -2
- package/dist/unit.js +2 -1
- package/hooks/eval-lock-nudge.sh +14 -6
- package/package.json +2 -2
- package/skills/test-harness/SKILL.md +68 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Refuse to spend model calls when somebody else's test runner owns this process.
|
|
4
|
+
*
|
|
5
|
+
* ## The failure this removes
|
|
6
|
+
*
|
|
7
|
+
* A vigiles eval spawns `claude` and spends real money. It is meant to run on a
|
|
8
|
+
* schedule, from `vigiles eval`. But an author who names one `foo.test.mjs` — a
|
|
9
|
+
* name vigiles used to CREDIT as coverage — hands it to every default `npx vitest`
|
|
10
|
+
* / `npx jest` run in the repository, because that name matches their default
|
|
11
|
+
* patterns exactly (read out of the installed packages):
|
|
12
|
+
*
|
|
13
|
+
* vitest ** /*.{test,spec}.?(c|m)[jt]s?(x)
|
|
14
|
+
* jest ** /?(*.)+(spec|test).?([mc])[jt]s?(x)
|
|
15
|
+
* ** /__tests__/** /*.?([mc])[jt]s?(x) ← everything in that directory
|
|
16
|
+
*
|
|
17
|
+
* Measured 2026-08-11: a bare project with `.claude/skills/foo/foo.test.mjs` and a
|
|
18
|
+
* plain `npx vitest run` — it executed. "It lives under a dot-directory, nothing
|
|
19
|
+
* else will find it" is false. The bill arrives silently, on every push, in CI.
|
|
20
|
+
*
|
|
21
|
+
* The naming half is fixed elsewhere (those globs no longer count). This is the
|
|
22
|
+
* part that does not depend on anybody obeying a convention: the money cannot be
|
|
23
|
+
* spent from inside a foreign runner, whatever the file is called.
|
|
24
|
+
*
|
|
25
|
+
* ## 🪦 THE STATIC HALF WAS DELETED 2026-08-12, AND THIS IS NOW THE WHOLE DEFENCE
|
|
26
|
+
*
|
|
27
|
+
* There used to be a companion, `core/foreign-runner-tests.ts`: a lexical gate
|
|
28
|
+
* that read every file under a harness surface dir, decided FROM THE TEXT whether
|
|
29
|
+
* it drives an agent (`runEval(`, `runHarnessTest(`, …), and warned in the audit
|
|
30
|
+
* report when a default vitest/jest glob would collect it. It is gone, with its
|
|
31
|
+
* test file and the disk walker in `scan.ts` that fed it.
|
|
32
|
+
*
|
|
33
|
+
* It was deleted under a rule that file pre-registered, not on a whim:
|
|
34
|
+
*
|
|
35
|
+
* > A seventh false positive is not another bug to fix — it is the measurement
|
|
36
|
+
* > saying the lexical rule cannot be made tight enough, and the answer then is
|
|
37
|
+
* > to DELETE the gate and let the money guard be the whole defence.
|
|
38
|
+
*
|
|
39
|
+
* THE SEVENTH ARRIVED: `function testDriver(runEval) { runEval(fake); }`. The
|
|
40
|
+
* binding check looked at declarations and import aliases but never at PARAMETER
|
|
41
|
+
* lists, so an ordinary offline unit test injecting a fake was reported as
|
|
42
|
+
* spawning a real agent. Run, not argued — the gate over one file of that shape
|
|
43
|
+
* returned one finding, whose warning read *"It calls `runEval`, which spawns an
|
|
44
|
+
* agent"* and told the author to rename the file. That sentence was false about
|
|
45
|
+
* that file.
|
|
46
|
+
*
|
|
47
|
+
* Six shapes had already been narrowed away one at a time, and this one had been
|
|
48
|
+
* SELF-DISCLOSED in the file as a known residual ("costs one warning and needs a
|
|
49
|
+
* scope analysis this module cannot have"). Pre-disclosure is not a defence: a
|
|
50
|
+
* pre-registered stopping rule exists precisely to stop "we know, it is
|
|
51
|
+
* acceptable" from running forever, so the count was honoured rather than
|
|
52
|
+
* reasoned around.
|
|
53
|
+
*
|
|
54
|
+
* The measurement agreed independently. Across three corpora (this repo, a
|
|
55
|
+
* 43-harness consumer repo, five vendored third-party plugin repos — 3 613 js/ts
|
|
56
|
+
* files), and re-measured at deletion time over this repo's 941 files:
|
|
57
|
+
*
|
|
58
|
+
* true positives, ever 0
|
|
59
|
+
* distinct false-positive shapes reported 7
|
|
60
|
+
*
|
|
61
|
+
* Its only demonstrated effect on a real repository was an effect of NOT firing.
|
|
62
|
+
* Every error it made was the expensive kind: the remedy it printed is "rename
|
|
63
|
+
* this file", so a false warning costs someone a working test, while a missed
|
|
64
|
+
* warning costs nothing — the refusal below already fires at all four spawn doors
|
|
65
|
+
* (`judge.ts`, `eval.ts`, `scan-behavioral.ts`, `adapters/codex/eval.ts`), so a
|
|
66
|
+
* miss is at most a less friendly notice, never a bill.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ WHAT IS GENUINELY LOST: the early, report-time heads-up. An author who names
|
|
69
|
+
* a harness test `foo.test.mjs` now learns it at the refusal below rather than in
|
|
70
|
+
* the audit. That is the trade, made knowingly — an advisory that has never once
|
|
71
|
+
* been right is not worth a warning that has been wrong seven times.
|
|
72
|
+
*
|
|
73
|
+
* The removal is PINNED, not merely done: `scan-vendor.test.ts` still asserts that
|
|
74
|
+
* no `COLLECTS AND EXECUTES` warning appears on the vendored corpus, so bringing
|
|
75
|
+
* the gate back fails a test instead of passing silently.
|
|
76
|
+
*
|
|
77
|
+
* ## Why not an environment variable
|
|
78
|
+
*
|
|
79
|
+
* The obvious version is `process.env.VITEST`. It was proposed and rejected: that
|
|
80
|
+
* variable can sit in a `.env` or in a CI environment for unrelated reasons, and
|
|
81
|
+
* then a legitimate scheduled eval refuses to run — a guard that fires on correct
|
|
82
|
+
* input is a guard people delete.
|
|
83
|
+
*
|
|
84
|
+
* `process.argv[1]` is the path node was actually started with, which no stray
|
|
85
|
+
* configuration can forge. Measured, same spike:
|
|
86
|
+
*
|
|
87
|
+
* under `npx vitest run` → …/node_modules/vitest/dist/workers/forks.js
|
|
88
|
+
* under `node foo.eval.mjs`→ …/foo.eval.mjs
|
|
89
|
+
* under `vigiles eval` → the script vigiles spawned
|
|
90
|
+
*
|
|
91
|
+
* So the check is a POSITIVE identification of a known runner, not "am I the entry
|
|
92
|
+
* point". Positive is the conservative direction here: an unrecognised wrapper
|
|
93
|
+
* runs (and may cost money) rather than a legitimate run being blocked by a
|
|
94
|
+
* pattern nobody predicted.
|
|
95
|
+
*
|
|
96
|
+
* ## 🔴 `node --test` HAS NO argv SIGNAL AT ALL, and the line that claimed one
|
|
97
|
+
* was dead from the day it was written
|
|
98
|
+
*
|
|
99
|
+
* `["node --test", ["node_modules/.bin/node--test"]]` used to sit in the table
|
|
100
|
+
* below. It was added by analogy with the npx-installed runners and never
|
|
101
|
+
* measured. There is no such binary: Node's test runner is a FLAG on node
|
|
102
|
+
* itself, so nothing under `node_modules/.bin/` can appear in `argv[1]`, and the
|
|
103
|
+
* entry could not fire under any invocation. That is worse than its absence — it
|
|
104
|
+
* READ as coverage while the paid tier stayed reachable from a legacy harness
|
|
105
|
+
* named `*.test.mjs`, a name Node's own default patterns collect.
|
|
106
|
+
*
|
|
107
|
+
* Measured 2026-08-12 (Node 22.22, a fixture printing its own process facts):
|
|
108
|
+
*
|
|
109
|
+
* node --test foo.test.mjs
|
|
110
|
+
* argv[1] = /abs/foo.test.mjs ← the TEST FILE, no runner in sight
|
|
111
|
+
* execArgv = [] ← the flag is not here either
|
|
112
|
+
* NODE_TEST_CONTEXT = child-v8 ← the only signal
|
|
113
|
+
*
|
|
114
|
+
* node --test --experimental-test-isolation=none foo.test.mjs
|
|
115
|
+
* argv[1] = foo.test.mjs
|
|
116
|
+
* execArgv = ["--test", "--experimental-test-isolation=none"]
|
|
117
|
+
* NODE_TEST_CONTEXT = undefined ← in-process: no child, no var
|
|
118
|
+
*
|
|
119
|
+
* The two modes therefore need two different facts, and both are read here.
|
|
120
|
+
*
|
|
121
|
+
* ⚠️ THIS IS NOT THE `process.env.VITEST` IDIOM REJECTED ABOVE, and the
|
|
122
|
+
* difference is the reason it is allowed. `VITEST` is a USER-VISIBLE CONVENTION:
|
|
123
|
+
* a person can put it in a `.env` or a CI job for unrelated reasons, so a guard
|
|
124
|
+
* reading it fires on correct input. `NODE_TEST_CONTEXT` is set by NODE ITSELF in
|
|
125
|
+
* the child it spawns — the internal protocol between runner and test process,
|
|
126
|
+
* which nobody writes into their own environment. Same for `execArgv`: the flag
|
|
127
|
+
* list node was launched with (`--test` is explicitly refused inside
|
|
128
|
+
* `NODE_OPTIONS` — verified — so it cannot arrive from a stray env either). Do
|
|
129
|
+
* not "fix" this back into an argv fragment: for `node --test` an argv signal
|
|
130
|
+
* does not exist, so the choice is these facts or no detection at all.
|
|
131
|
+
*
|
|
132
|
+
* Pure: facts in, a name or null out. No process access, no filesystem.
|
|
133
|
+
*/
|
|
134
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
135
|
+
exports.foreignRunner = foreignRunner;
|
|
136
|
+
exports.foreignRunnerRefusal = foreignRunnerRefusal;
|
|
137
|
+
exports.refuseUnderForeignRunner = refuseUnderForeignRunner;
|
|
138
|
+
/** Path fragments that identify a runner owning the process. POSIX-normalised.
|
|
139
|
+
* Every fragment was MEASURED from a real run of that runner — see the tests. */
|
|
140
|
+
const RUNNERS = [
|
|
141
|
+
["vitest", ["node_modules/vitest/", "node_modules/.bin/vitest"]],
|
|
142
|
+
[
|
|
143
|
+
"jest",
|
|
144
|
+
[
|
|
145
|
+
"node_modules/jest/",
|
|
146
|
+
"node_modules/jest-cli/",
|
|
147
|
+
"node_modules/jest-worker/",
|
|
148
|
+
"node_modules/.bin/jest",
|
|
149
|
+
],
|
|
150
|
+
],
|
|
151
|
+
["mocha", ["node_modules/mocha/", "node_modules/.bin/mocha"]],
|
|
152
|
+
["ava", ["node_modules/ava/", "node_modules/.bin/ava"]],
|
|
153
|
+
];
|
|
154
|
+
/** The name reported for Node's built-in runner — also the command to blame. */
|
|
155
|
+
const NODE_TEST = "node --test";
|
|
156
|
+
/**
|
|
157
|
+
* The foreign test runner that started this process, or `null`.
|
|
158
|
+
*
|
|
159
|
+
* `argv1` is `process.argv[1]`; `node` carries the two facts that identify Node's
|
|
160
|
+
* built-in runner (see the header). Both are passed in rather than read, so the
|
|
161
|
+
* decision is a pure function the tests drive with values measured from real runs.
|
|
162
|
+
*/
|
|
163
|
+
function foreignRunner(argv1, node = {}) {
|
|
164
|
+
// `--test` EXACTLY: `--test-only` / `--test-name-pattern` are ordinary flags a
|
|
165
|
+
// plain `node foo.eval.mjs` may carry, and a prefix match would refuse it.
|
|
166
|
+
if ((node.nodeTestContext ?? "") !== "" ||
|
|
167
|
+
(node.execArgv ?? []).includes("--test"))
|
|
168
|
+
return NODE_TEST;
|
|
169
|
+
if (!argv1)
|
|
170
|
+
return null;
|
|
171
|
+
const p = argv1.replaceAll("\\", "/");
|
|
172
|
+
for (const [name, fragments] of RUNNERS)
|
|
173
|
+
if (fragments.some((f) => p.includes(f)))
|
|
174
|
+
return name;
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The refusal message. Separate from the check so a test can assert the WORDS —
|
|
179
|
+
* a guard that stops a run without saying what to do instead is a support ticket.
|
|
180
|
+
*/
|
|
181
|
+
function foreignRunnerRefusal(runner, what) {
|
|
182
|
+
return (`vigiles refused to spawn a model: this process is running under ${runner}.\n` +
|
|
183
|
+
` ${what} spends real model calls, and a general test runner picking it up ` +
|
|
184
|
+
`would spend them on every push.\n` +
|
|
185
|
+
` If this file is a vigiles eval, run it with \`vigiles eval\` (or \`node <file>\`) ` +
|
|
186
|
+
`and name it \`<surface>.eval.mjs\` — that name is outside ${runner}'s default patterns.\n` +
|
|
187
|
+
` If ${runner} is meant to run it, it must not call the real-model tier.`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Refuse to spend model budget when a foreign test runner started this process.
|
|
191
|
+
*
|
|
192
|
+
* 🔴 CALL THIS AT EVERY REAL-MODEL SPAWN. It used to live privately in `eval.ts`
|
|
193
|
+
* and be called from exactly one place — `spawnAgent`, the real `claude` runner —
|
|
194
|
+
* on the reasoning that the composition root funnels every paid path. Measured
|
|
195
|
+
* 2026-08-12: it funnels ONE of four.
|
|
196
|
+
*
|
|
197
|
+
* src/eval.ts spawn(claudeCodeRuntime.agentBinary) guarded
|
|
198
|
+
* src/adapters/codex/eval.ts spawnSync("codex", …) UNGUARDED
|
|
199
|
+
* src/judge.ts spawnSync("claude", …) UNGUARDED
|
|
200
|
+
* src/scan-behavioral.ts spawnSync("claude", …) UNGUARDED
|
|
201
|
+
*
|
|
202
|
+
* `measureTriggerRate(spec, { evalDriver })` calls the injected driver's runner
|
|
203
|
+
* DIRECTLY, so a Codex eval collected by a stray `npx vitest run` spent real
|
|
204
|
+
* money on every push while the guard looked complete. The irony is worth
|
|
205
|
+
* recording: the untested-skill nudge now tells Codex users to pass
|
|
206
|
+
* `{ evalDriver }` — the product recommends the shape that bypassed the guard.
|
|
207
|
+
*
|
|
208
|
+
* Lives here, in a module that imports NOTHING, so any adapter can call it
|
|
209
|
+
* without a cycle. `foreignRunner` stays pure; this is the one impure wrapper,
|
|
210
|
+
* and it is impure precisely so callers cannot forget to pass the facts.
|
|
211
|
+
*
|
|
212
|
+
* ⚠️ CALL IT OUTSIDE ANY `try` THAT SWALLOWS. `judge` and `deriveAttackReal` both
|
|
213
|
+
* wrap their spawn in `try { … } catch { return fallback }`, so a refusal thrown
|
|
214
|
+
* inside would be caught and downgraded to a score of 0 / a canned string — a
|
|
215
|
+
* silent wrong answer instead of a stop. Both call it before the `try`.
|
|
216
|
+
*/
|
|
217
|
+
function refuseUnderForeignRunner(what) {
|
|
218
|
+
// `node --test` is INVISIBLE in argv (it is a flag on node, not an installed
|
|
219
|
+
// binary), so the two facts that do identify it are read here — the one impure
|
|
220
|
+
// place — and handed to the pure decision above.
|
|
221
|
+
const runner = foreignRunner(process.argv[1], {
|
|
222
|
+
execArgv: process.execArgv,
|
|
223
|
+
nodeTestContext: process.env["NODE_TEST_CONTEXT"],
|
|
224
|
+
});
|
|
225
|
+
if (runner !== null)
|
|
226
|
+
throw new Error(foreignRunnerRefusal(runner, what));
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=foreign-runner.js.map
|
|
@@ -44,6 +44,16 @@ export interface ModelRequest {
|
|
|
44
44
|
readonly role: string;
|
|
45
45
|
readonly text: string;
|
|
46
46
|
}[];
|
|
47
|
+
/**
|
|
48
|
+
* `true` when the agent CLI made this call for its OWN bookkeeping rather than
|
|
49
|
+
* to advance the agent loop — a SIDE-CHANNEL call.
|
|
50
|
+
*
|
|
51
|
+
* MEASURED on Claude Code 2.1.228: a `post_turn_summary` classifier (it decides
|
|
52
|
+
* whether to push the user a phone notification) runs on every turn, on the
|
|
53
|
+
* user's own credential, and it declares NO tools. The mock does not let such a
|
|
54
|
+
* request consume a script entry — see `isMainLoopRequest` in `mock-model.ts`.
|
|
55
|
+
*/
|
|
56
|
+
readonly sideChannel?: boolean;
|
|
47
57
|
}
|
|
48
58
|
/** A tool the agent invoked, paired with its result (transcript mode only). */
|
|
49
59
|
export interface ToolCall {
|
|
@@ -77,8 +87,15 @@ export interface HarnessMockHandle {
|
|
|
77
87
|
readonly url: string;
|
|
78
88
|
/** Every request the mock received, flattened for assertions, in order. */
|
|
79
89
|
readonly requests: readonly ModelRequest[];
|
|
80
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Number of SCRIPT turns served so far — i.e. main-loop requests. A
|
|
92
|
+
* side-channel request ({@link ModelRequest.sideChannel}) is answered without
|
|
93
|
+
* touching the script and is NOT counted here, so this stays the agent's turn
|
|
94
|
+
* count rather than the CLI's HTTP-call count.
|
|
95
|
+
*/
|
|
81
96
|
readonly count: number;
|
|
97
|
+
/** Side-channel requests answered so far (never script-consuming). */
|
|
98
|
+
readonly sideChannelCount?: number;
|
|
82
99
|
/** Stop the mock server. */
|
|
83
100
|
close(): void | Promise<void>;
|
|
84
101
|
}
|