vigiles 35.0.0 → 37.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/dist/adapters/codex/driver.d.ts +3 -0
- package/dist/adapters/codex/driver.js +6 -0
- package/dist/cli-commands.d.ts +1 -1
- package/dist/cli-commands.js +1 -0
- package/dist/cli-main.js +13 -0
- package/dist/cli.js +51 -0
- package/dist/core/eval-driver.d.ts +9 -1
- package/dist/core/eval-spec-parse.d.ts +14 -0
- package/dist/core/eval-spec-parse.js +168 -0
- package/dist/core/harness-driver.d.ts +9 -2
- package/dist/core/run-env.d.ts +46 -1
- package/dist/core/run-env.js +59 -0
- package/dist/core/stub-rules.d.ts +199 -0
- package/dist/core/stub-rules.js +397 -0
- package/dist/eval-cache.d.ts +19 -1
- package/dist/eval-cache.js +1 -1
- package/dist/eval-entry.d.ts +28 -0
- package/dist/eval-entry.js +63 -10
- package/dist/eval-surface.d.ts +2 -0
- package/dist/eval.d.ts +80 -32
- package/dist/eval.js +293 -91
- package/dist/harness-test.d.ts +61 -23
- package/dist/harness-test.js +125 -48
- package/dist/services.d.ts +1 -1
- package/dist/services.js +1 -1
- package/dist/stub.d.ts +57 -0
- package/dist/stub.js +103 -0
- package/dist/test.d.ts +4 -1
- package/dist/test.js +8 -4
- package/dist/tool-stub.d.ts +24 -29
- package/dist/tool-stub.js +63 -74
- package/package.json +1 -1
- package/skills/test-harness/references/observing-a-run.md +13 -12
- package/skills/test-harness/references/writing-tests.md +1 -0
package/dist/tool-stub.d.ts
CHANGED
|
@@ -1,35 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export interface ToolStub {
|
|
7
|
-
/** The binary name to shadow on PATH (e.g. `"gh"`, `"psql"`). */
|
|
8
|
-
readonly name: string;
|
|
9
|
-
/** Canned stdout the fake prints. Default `""`. */
|
|
10
|
-
readonly stdout?: string;
|
|
11
|
-
/** Canned stderr the fake writes to fd 2. Default: none. */
|
|
12
|
-
readonly stderr?: string;
|
|
13
|
-
/** Exit code the fake returns. Default `0`. */
|
|
14
|
-
readonly exitCode?: number;
|
|
1
|
+
import { type StubCall, type ToolStub } from "./core/stub-rules.js";
|
|
2
|
+
/** Where a run's stubs live. `binDir` goes first on the run's PATH. */
|
|
3
|
+
export interface StubDir {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly binDir: string;
|
|
15
6
|
}
|
|
7
|
+
/** How a shim reaches the stub runtime: this node, and this vigiles' `cli.js`. */
|
|
8
|
+
export interface StubLauncher {
|
|
9
|
+
readonly node: string;
|
|
10
|
+
readonly cli: string;
|
|
11
|
+
}
|
|
12
|
+
/** The shim for one stub. Pure. */
|
|
13
|
+
export declare function stubShim(launcher: StubLauncher, root: string, name: string): string;
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
export declare function renderToolStub(stub: ToolStub): string;
|
|
22
|
-
/**
|
|
23
|
-
* Write one executable POSIX shell stub per entry into `binDir` (which must
|
|
24
|
-
* already exist). Each file is named exactly `stub.name` and `chmod 0o755` so it
|
|
25
|
-
* is directly executable once `binDir` is on PATH. Pure-ish — fs only, no spawn.
|
|
15
|
+
* Write a fresh stub root for `stubs`: a shim and a rules file per stub. The
|
|
16
|
+
* stubs must already be parsed (the spec boundary did it). Remove it with
|
|
17
|
+
* {@link removeStubDir}.
|
|
26
18
|
*/
|
|
27
|
-
export declare function
|
|
19
|
+
export declare function writeStubDir(stubs: readonly ToolStub[], launcher: StubLauncher): StubDir;
|
|
20
|
+
/** Every invocation the run's stubs recorded, in order. Empty when none was called. */
|
|
21
|
+
export declare function readStubCalls(dir: StubDir): readonly StubCall[];
|
|
28
22
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* so a `parentDir` cleanup also clears it).
|
|
23
|
+
* Forget every call recorded so far. The log is the stubs' only state (an
|
|
24
|
+
* `inOrder` rule counts its prior answers in it), so a cleared log is a stub
|
|
25
|
+
* that has never been called. Safe when no call was logged yet.
|
|
33
26
|
*/
|
|
34
|
-
export declare function
|
|
27
|
+
export declare function resetStubCalls(dir: StubDir): void;
|
|
28
|
+
/** Remove a stub root. Safe on one already gone. */
|
|
29
|
+
export declare function removeStubDir(dir: StubDir): void;
|
|
35
30
|
//# sourceMappingURL=tool-stub.d.ts.map
|
package/dist/tool-stub.js
CHANGED
|
@@ -1,92 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.
|
|
5
|
-
exports.
|
|
3
|
+
exports.stubShim = stubShim;
|
|
4
|
+
exports.writeStubDir = writeStubDir;
|
|
5
|
+
exports.readStubCalls = readStubCalls;
|
|
6
|
+
exports.resetStubCalls = resetStubCalls;
|
|
7
|
+
exports.removeStubDir = removeStubDir;
|
|
6
8
|
/**
|
|
7
|
-
* vigiles —
|
|
9
|
+
* vigiles — tool stubs on PATH: the EFFECTS half (rung R2 of the eval coverage
|
|
10
|
+
* model). The rules, the decision and the log format are pure, in
|
|
11
|
+
* `core/stub-rules.ts`; this file writes them to disk and reads the log back.
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* a
|
|
12
|
-
* **recorded / author-provided canned result** — a standard PATH-shim +
|
|
13
|
-
* VCR-style record/replay technique, not a novel invention. See
|
|
14
|
-
* `research/eval-coverage-and-isolation.md` (the three-rung model).
|
|
13
|
+
* Layout of one stub root — a temp dir BESIDE the run's working dir, never in
|
|
14
|
+
* it, so the agent does not find vigiles plumbing in its project (models did
|
|
15
|
+
* `ls`/`cat` a stub dir that lived inside the work dir):
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* a documented follow-on — NOT implemented here.
|
|
17
|
+
* <root>/bin/<name> a two-line sh shim, first on the run's PATH
|
|
18
|
+
* <root>/rules/<name>.json the encoded rules (`encodeStub`)
|
|
19
|
+
* <root>/calls.jsonl one line per invocation, every stub of the run
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* The shim execs `node <vigiles cli.js> hook-runtime stub <root> <name> "$@"`,
|
|
22
|
+
* a fast path in the dispatcher that loads only the pure decision — the same
|
|
23
|
+
* shape as the intercept hook, which also calls back into the CLI by absolute
|
|
24
|
+
* path because `npx vigiles` does not resolve from a throwaway dir.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* sub-command / flags) is a follow-on.
|
|
26
|
+
* The canned answers are author/recorded fixtures, NEVER model-synthesized: a
|
|
27
|
+
* synthesized `gh` output looks plausible and diverges from the real tool.
|
|
29
28
|
*/
|
|
30
29
|
const node_fs_1 = require("node:fs");
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return Buffer.from(s, "utf-8").toString("base64");
|
|
30
|
+
const stub_rules_js_1 = require("./core/stub-rules.js");
|
|
31
|
+
const tmp_root_js_1 = require("./core/tmp-root.js");
|
|
32
|
+
/** A POSIX single-quoted word: the only quoting `sh` never re-reads. */
|
|
33
|
+
const shQuote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
|
|
34
|
+
/** The shim for one stub. Pure. */
|
|
35
|
+
function stubShim(launcher, root, name) {
|
|
36
|
+
const words = [
|
|
37
|
+
launcher.node,
|
|
38
|
+
launcher.cli,
|
|
39
|
+
"hook-runtime",
|
|
40
|
+
"stub",
|
|
41
|
+
root,
|
|
42
|
+
name,
|
|
43
|
+
];
|
|
44
|
+
return `#!/bin/sh\nexec ${words.map(shQuote).join(" ")} "$@"\n`;
|
|
47
45
|
}
|
|
48
46
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
47
|
+
* Write a fresh stub root for `stubs`: a shim and a rules file per stub. The
|
|
48
|
+
* stubs must already be parsed (the spec boundary did it). Remove it with
|
|
49
|
+
* {@link removeStubDir}.
|
|
52
50
|
*/
|
|
53
|
-
function
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
lines.push(`exit ${String(stub.exitCode ?? 0)}`);
|
|
66
|
-
return lines.join("\n") + "\n";
|
|
51
|
+
function writeStubDir(stubs, launcher) {
|
|
52
|
+
const root = (0, tmp_root_js_1.makeTmpDir)("stubs");
|
|
53
|
+
const { binDir, rulesDir } = (0, stub_rules_js_1.stubPaths)(root);
|
|
54
|
+
(0, node_fs_1.mkdirSync)(binDir, { recursive: true });
|
|
55
|
+
(0, node_fs_1.mkdirSync)(rulesDir, { recursive: true });
|
|
56
|
+
stubs.forEach((stub) => {
|
|
57
|
+
const paths = (0, stub_rules_js_1.stubPaths)(root, stub.name);
|
|
58
|
+
(0, node_fs_1.writeFileSync)(paths.rules, (0, stub_rules_js_1.encodeStub)(stub), "utf8");
|
|
59
|
+
(0, node_fs_1.writeFileSync)(paths.bin, stubShim(launcher, root, stub.name), "utf8");
|
|
60
|
+
(0, node_fs_1.chmodSync)(paths.bin, 0o755);
|
|
61
|
+
});
|
|
62
|
+
return { root, binDir };
|
|
67
63
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*/
|
|
73
|
-
function writeToolStubs(binDir, stubs) {
|
|
74
|
-
for (const stub of stubs) {
|
|
75
|
-
const file = (0, node_path_1.join)(binDir, stub.name);
|
|
76
|
-
(0, node_fs_1.writeFileSync)(file, renderToolStub(stub), "utf-8");
|
|
77
|
-
(0, node_fs_1.chmodSync)(file, 0o755);
|
|
78
|
-
}
|
|
64
|
+
/** Every invocation the run's stubs recorded, in order. Empty when none was called. */
|
|
65
|
+
function readStubCalls(dir) {
|
|
66
|
+
const { log } = (0, stub_rules_js_1.stubPaths)(dir.root);
|
|
67
|
+
return (0, node_fs_1.existsSync)(log) ? (0, stub_rules_js_1.parseStubLog)((0, node_fs_1.readFileSync)(log, "utf8")) : [];
|
|
79
68
|
}
|
|
80
69
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* so a `parentDir` cleanup also clears it).
|
|
70
|
+
* Forget every call recorded so far. The log is the stubs' only state (an
|
|
71
|
+
* `inOrder` rule counts its prior answers in it), so a cleared log is a stub
|
|
72
|
+
* that has never been called. Safe when no call was logged yet.
|
|
85
73
|
*/
|
|
86
|
-
function
|
|
87
|
-
(0, node_fs_1.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
74
|
+
function resetStubCalls(dir) {
|
|
75
|
+
(0, node_fs_1.rmSync)((0, stub_rules_js_1.stubPaths)(dir.root).log, { force: true });
|
|
76
|
+
}
|
|
77
|
+
/** Remove a stub root. Safe on one already gone. */
|
|
78
|
+
function removeStubDir(dir) {
|
|
79
|
+
(0, node_fs_1.rmSync)(dir.root, { recursive: true, force: true });
|
|
91
80
|
}
|
|
92
81
|
//# sourceMappingURL=tool-stub.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "37.0.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",
|
|
@@ -10,18 +10,19 @@ questions are keyed on the **observation** instead — "what did this skill
|
|
|
10
10
|
actually do?" — and they have answers already. Reach for these before building
|
|
11
11
|
anything; every one of them ships today.
|
|
12
12
|
|
|
13
|
-
| The question you're actually asking | Use
|
|
14
|
-
| ----------------------------------------------------------------------- |
|
|
15
|
-
| Which tools did it call, and with what arguments? | `trace.toolCalls` · `tool` / `toolWith` checks · `parseToolCalls` (`vigiles`)
|
|
16
|
-
| Did it call a tool it must not? | `notTool(name)`
|
|
17
|
-
| Did it call **only** tools from a known set? | `onlyTools([...])` — the white-list, symmetric to `assertWroteOnly`
|
|
18
|
-
| Did it stay inside the `allowed-tools` its own frontmatter declares? | `skillContract(dir).surface` — builds that check FROM the declaration
|
|
19
|
-
| What files did the run write? | `filesWritten` · `wrote(path)` / `didNotWrite(path)` · `r.file(path)`
|
|
20
|
-
| Did it write **only** where it was supposed to? | `assertWroteOnly([...])` / `assertNoWrite()` — needs `{ sandbox: "auto" }`
|
|
21
|
-
| Run a tool call but **don't let it execute** — capture the args instead | the `interceptTools` option on `measure` / `runEval` (a `ToolIntercept[]`)
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
13
|
+
| The question you're actually asking | Use |
|
|
14
|
+
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
+
| Which tools did it call, and with what arguments? | `trace.toolCalls` · `tool` / `toolWith` checks · `parseToolCalls` (`vigiles`) |
|
|
16
|
+
| Did it call a tool it must not? | `notTool(name)` |
|
|
17
|
+
| Did it call **only** tools from a known set? | `onlyTools([...])` — the white-list, symmetric to `assertWroteOnly` |
|
|
18
|
+
| Did it stay inside the `allowed-tools` its own frontmatter declares? | `skillContract(dir).surface` — builds that check FROM the declaration |
|
|
19
|
+
| What files did the run write? | `filesWritten` · `wrote(path)` / `didNotWrite(path)` · `r.file(path)` |
|
|
20
|
+
| Did it write **only** where it was supposed to? | `assertWroteOnly([...])` / `assertNoWrite()` — needs `{ sandbox: "auto" }` |
|
|
21
|
+
| Run a tool call but **don't let it execute** — capture the args instead | the `interceptTools` option on `measure` / `runEval` (a `ToolIntercept[]`) |
|
|
22
|
+
| Let a CLI the agent (or its script) calls **answer** — `gh`, `psql` | `stubs: [experimental_stub(name, rules)]` on `runEval`; read `ctx.stubCalls`, check with `experimental_stub.called(...)` — an argv no rule answers fails the eval |
|
|
23
|
+
| Did a subagent do it, and which one? | `subagent(name, [...])` · `SubagentTrace` |
|
|
24
|
+
| Was it an MCP tool? | `mcp(server, toolName)` |
|
|
25
|
+
| Assert the whole effect boundary deterministically | `assertChecks` + the checks above (see `examples/harness/effect-boundary.harness.mjs`) |
|
|
25
26
|
|
|
26
27
|
`interceptTools` is the one worth knowing about, because it is not obvious it
|
|
27
28
|
exists: it denies a tool its **real execution** via an auto-wired `PreToolUse`
|
|
@@ -90,6 +90,7 @@ import { defineEval, assertSignificant } from "vigiles";
|
|
|
90
90
|
|
|
91
91
|
export default defineEval({
|
|
92
92
|
runEval: {
|
|
93
|
+
env: { kind: "ephemeral" }, // required: a throwaway HOME, or { kind: "inherit", reason }
|
|
93
94
|
arms: { off: {}, on: { pluginDir: "./" } },
|
|
94
95
|
task: "…a task the harness change should affect…",
|
|
95
96
|
measure: (ctx) => ({ ok: /* a bare predicate over the trace */ true }),
|