vigiles 4.0.2 → 5.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 CHANGED
@@ -27,10 +27,10 @@ deterministic layer for the harness: it **lints** the references your instructio
27
27
  files make and **tests** that your hooks and skills actually fire. Two independent
28
28
  pillars — adopt either, or both:
29
29
 
30
- | | Pillar | What it does |
31
- | ----- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
- | **①** | **Lint your instruction files** | Every linter rule, file path, script, and code symbol your CLAUDE.md cites is checked against reality, so stale references can't silently mislead the agent. → [guide](docs/verifying-instruction-files.md) |
33
- | **②** | **Test your harness** | Your hooks and skills are code — vigiles tests they actually fire, **deterministically and free** (no model, no API key) before you pay for an eval. → [guide](docs/harness-testing.md) |
30
+ | | Pillar | What it does |
31
+ | ----- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | **①** | **Lint your instruction files** | Every linter rule, file path, script, and code symbol your CLAUDE.md cites is checked against reality, so stale references can't silently mislead the agent. → [guide](docs/verifying-instruction-files.md) |
33
+ | **②** | **Test your harness** | Your hooks and skills are code — vigiles tests they actually fire, **deterministically and free** (no model, no API key); and when a question _does_ need a real-model eval, it runs on your **Claude subscription**, not metered API. → [guide](docs/harness-testing.md) |
34
34
 
35
35
  Neither pillar depends on the other — pick the one that hurts today. **Works with
36
36
  Claude Code and Codex** ([`vigiles/codex`](docs/harnesses.md)) behind a five-port
@@ -93,10 +93,42 @@ assert(r.blocked); // a red ✗ here means your hook silently lets it through
93
93
  ```
94
94
 
95
95
  Three tiers, cheapest first: **`runHook`** (a hook's logic), **`runHarnessTest`**
96
- (the real agent CLI against a scripted mock model), **`runEval`** (the real model
97
- A/B with a significance gate). **Testing a skill?** `measureTriggerRate` checks
98
- its description actually **fires** across varied prompts (recall) without
99
- hijacking unrelated ones (precision). **[Full guide →](docs/harness-testing.md)**
96
+ (the real agent CLI against a scripted mock model), and the real-model scored tier
97
+ (**`measure`** / **`runEval`**). **Testing a skill?** Two questions, both covered:
98
+ does its description **fire** (`measureTriggerRate` — recall across varied prompts
99
+ without hijacking unrelated ones, precision), **and** does its guidance actually
100
+ **work**. For "is this exact skill any good?" score the output directly —
101
+ `measure({ checks: [judged(rubric)] })` + `assertRates` (the **absolute** oracle,
102
+ what promptfoo/DeepEval lead with; no on/off baseline needed). When you need the
103
+ **relative** lift over no-skill — regression, or proving the change isn't noise —
104
+ A/B it on-vs-off with `runEval` + `assertSignificant`. Description _and_ behavior,
105
+ not just one. **Need a safety property** — that the agent
106
+ **didn't** push to the wrong branch or call a paid API? `notTool` + `interceptTools`
107
+ intercept the tool in the real hook layer, so the attempt is caught and the side
108
+ effect never happens. **[Full guide →](docs/harness-testing.md)** · vigiles runs
109
+ foreign code (and a real model) safely by default — **[safety model →](docs/safety.md)**
110
+
111
+ Most of what real plugins do is testable cheaply — fire / trigger / contract /
112
+ safety, plus **record-replay** for the tool/API results a skill consumes (recorded
113
+ once from the real tool, replayed deterministically — no live service, no Docker).
114
+ That covers ~90%+ of real plugin surface on your subscription; the rare case that
115
+ needs a real browser or database **composes with Docker** rather than us
116
+ reinventing the sandbox. **[What we test, how →](research/eval-coverage-and-isolation.md)**
117
+
118
+ **Affordable by design — the eval you can actually run.** Almost nobody evals
119
+ their harness, because the usual tools (promptfoo, DeepEval, …) hit the API SDK
120
+ and bill **per token on every run**. vigiles inverts that: most questions are
121
+ answered with **no model at all** (free, every commit), and when you do reach for
122
+ a real-model eval, vigiles drives your `claude` CLI — so it runs on the **Pro/Max
123
+ subscription you already pay for**, not metered API billing. CI runs only the free
124
+ deterministic tiers; you run the real-model eval where the subscription already is
125
+ — a Claude Code session or locally — when it's worth it, not on every PR.
126
+
127
+ This affordability story is **ToS-clean**: vigiles drives _your own_ `claude` CLI
128
+ to test _your own_ harness on _your own_ subscription — the same thing you do when
129
+ you run Claude Code. (The Claude Agent SDK's ToS restricts _productizing_ claude.ai
130
+ login/limits in a third-party offering; running your own tests on your own sub is
131
+ exactly the supported posture, not that.)
100
132
 
101
133
  ## Quick start
102
134
 
@@ -31,12 +31,56 @@ export interface DetectResult {
31
31
  export declare function detectAdapterResult(root: string): DetectResult;
32
32
  /** The detected adapter (highest specificity), else the default (Claude Code). */
33
33
  export declare function detectAdapter(root: string): HarnessAdapter;
34
- /** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
34
+ /** Lower-case, trim, and map a short alias to its canonical adapter name. */
35
+ export declare function normalizeHarnessName(name: string): string;
36
+ /** Look up a registered adapter by `name` (alias-aware, e.g. `claude`). */
35
37
  export declare function getAdapter(name: string): HarnessAdapter | undefined;
38
+ /**
39
+ * The adapter whose instruction file is `filename` (e.g. `AGENTS.md` → codex,
40
+ * `CLAUDE.md` → claude-code), if any. The per-spec disambiguation signal: a
41
+ * `<file>.spec.ts` compiles a `<file>` instruction file, so the filename names
42
+ * the harness more specifically than config/detect for THAT spec.
43
+ */
44
+ export declare function adapterForInstructionFile(filename: string): HarnessAdapter | undefined;
36
45
  /**
37
46
  * Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
38
47
  * if unknown); otherwise auto-detect from `root`. The single entry point the CLI
39
48
  * uses so detection + override live in one place.
40
49
  */
41
50
  export declare function resolveAdapter(root: string, harness?: string): HarnessAdapter;
51
+ /** Normalize a config `harness` value (string | string[]) to a canonical list. */
52
+ export declare function normalizeHarnessList(harness?: string | readonly string[]): string[];
53
+ /**
54
+ * The adapter chosen for a single-dialect operation. A discriminated union so an
55
+ * invalid state — a "notice" with no message, or a clean pick carrying a stray
56
+ * string — is unrepresentable: `kind: "ok"` has no `notice`, `kind: "notice"`
57
+ * always carries a non-empty one. Both variants carry the `adapter`.
58
+ */
59
+ export type HarnessSelection = {
60
+ readonly kind: "ok";
61
+ readonly adapter: HarnessAdapter;
62
+ } | {
63
+ readonly kind: "notice";
64
+ readonly adapter: HarnessAdapter;
65
+ readonly notice: string;
66
+ };
67
+ /**
68
+ * Resolve the single harness a compile/lint operation should use, with explicit
69
+ * precedence — the deterministic replacement for sniffing the cwd:
70
+ *
71
+ * 1. `--harness=` flag (wins; throws if unknown).
72
+ * 2. config `harness` resolving to a single entry → use it.
73
+ * 3. config `harness` with multiple entries → use the first, with a loud notice.
74
+ * 4. no config → auto-detect, with a loud notice when the repo is ambiguous.
75
+ *
76
+ * `configHarness` is parsed once (alias-normalized) at the call site and passed
77
+ * in; this function re-normalizes idempotently so it's safe either way. Pure
78
+ * (besides reading `root`'s layout for detection) so the precedence is
79
+ * unit-testable without a real compile. See research/multi-harness-compile.md.
80
+ */
81
+ export declare function resolveHarnessSelection(opts: {
82
+ root: string;
83
+ flag?: string;
84
+ configHarness?: string | readonly string[];
85
+ }): HarnessSelection;
42
86
  //# sourceMappingURL=adapter-registry.d.ts.map
@@ -3,8 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ADAPTERS = exports.defaultAdapter = void 0;
4
4
  exports.detectAdapterResult = detectAdapterResult;
5
5
  exports.detectAdapter = detectAdapter;
6
+ exports.normalizeHarnessName = normalizeHarnessName;
6
7
  exports.getAdapter = getAdapter;
8
+ exports.adapterForInstructionFile = adapterForInstructionFile;
7
9
  exports.resolveAdapter = resolveAdapter;
10
+ exports.normalizeHarnessList = normalizeHarnessList;
11
+ exports.resolveHarnessSelection = resolveHarnessSelection;
8
12
  const adapter_js_1 = require("./adapters/claude-code/adapter.js");
9
13
  const adapter_js_2 = require("./adapters/codex/adapter.js");
10
14
  /** The default adapter when detection finds no harness markers. */
@@ -32,9 +36,32 @@ function detectAdapterResult(root) {
32
36
  function detectAdapter(root) {
33
37
  return detectAdapterResult(root).adapter;
34
38
  }
35
- /** Look up a registered adapter by `name` (e.g. for a `--harness` override). */
39
+ /**
40
+ * Short-name aliases accepted anywhere a harness name is supplied (config,
41
+ * `--harness=`). `init` historically uses `"claude"`; the canonical adapter name
42
+ * is `"claude-code"`. Normalizing here keeps selection and the registry in sync.
43
+ */
44
+ const HARNESS_ALIASES = {
45
+ claude: "claude-code",
46
+ };
47
+ /** Lower-case, trim, and map a short alias to its canonical adapter name. */
48
+ function normalizeHarnessName(name) {
49
+ const n = name.trim().toLowerCase();
50
+ return HARNESS_ALIASES[n] ?? n;
51
+ }
52
+ /** Look up a registered adapter by `name` (alias-aware, e.g. `claude`). */
36
53
  function getAdapter(name) {
37
- return exports.ADAPTERS.find((a) => a.name === name);
54
+ const canonical = normalizeHarnessName(name);
55
+ return exports.ADAPTERS.find((a) => a.name === canonical);
56
+ }
57
+ /**
58
+ * The adapter whose instruction file is `filename` (e.g. `AGENTS.md` → codex,
59
+ * `CLAUDE.md` → claude-code), if any. The per-spec disambiguation signal: a
60
+ * `<file>.spec.ts` compiles a `<file>` instruction file, so the filename names
61
+ * the harness more specifically than config/detect for THAT spec.
62
+ */
63
+ function adapterForInstructionFile(filename) {
64
+ return exports.ADAPTERS.find((a) => a.layout.instructionFile === filename);
38
65
  }
39
66
  /**
40
67
  * Resolve the adapter for a command: an explicit `--harness <name>` wins (throws
@@ -42,7 +69,7 @@ function getAdapter(name) {
42
69
  * uses so detection + override live in one place.
43
70
  */
44
71
  function resolveAdapter(root, harness) {
45
- if (harness !== undefined) {
72
+ if (harness !== undefined && harness !== "") {
46
73
  const a = getAdapter(harness);
47
74
  if (!a) {
48
75
  const known = exports.ADAPTERS.map((x) => x.name).join(", ");
@@ -52,4 +79,52 @@ function resolveAdapter(root, harness) {
52
79
  }
53
80
  return detectAdapter(root);
54
81
  }
82
+ /** Normalize a config `harness` value (string | string[]) to a canonical list. */
83
+ function normalizeHarnessList(harness) {
84
+ if (harness === undefined)
85
+ return [];
86
+ const arr = Array.isArray(harness) ? harness : [harness];
87
+ return arr.map(normalizeHarnessName).filter(Boolean);
88
+ }
89
+ /**
90
+ * Resolve the single harness a compile/lint operation should use, with explicit
91
+ * precedence — the deterministic replacement for sniffing the cwd:
92
+ *
93
+ * 1. `--harness=` flag (wins; throws if unknown).
94
+ * 2. config `harness` resolving to a single entry → use it.
95
+ * 3. config `harness` with multiple entries → use the first, with a loud notice.
96
+ * 4. no config → auto-detect, with a loud notice when the repo is ambiguous.
97
+ *
98
+ * `configHarness` is parsed once (alias-normalized) at the call site and passed
99
+ * in; this function re-normalizes idempotently so it's safe either way. Pure
100
+ * (besides reading `root`'s layout for detection) so the precedence is
101
+ * unit-testable without a real compile. See research/multi-harness-compile.md.
102
+ */
103
+ function resolveHarnessSelection(opts) {
104
+ const { root, flag, configHarness } = opts;
105
+ if (flag !== undefined && flag !== "") {
106
+ return { kind: "ok", adapter: resolveAdapter(root, flag) };
107
+ }
108
+ const list = normalizeHarnessList(configHarness);
109
+ if (list.length === 1) {
110
+ return { kind: "ok", adapter: resolveAdapter(root, list[0]) };
111
+ }
112
+ if (list.length > 1) {
113
+ const adapter = resolveAdapter(root, list[0]);
114
+ return {
115
+ kind: "notice",
116
+ adapter,
117
+ notice: `repo targets ${list.join(", ")} — compiling for ${adapter.name}; override with --harness=`,
118
+ };
119
+ }
120
+ const det = detectAdapterResult(root);
121
+ if (det.ambiguousWith.length > 0) {
122
+ return {
123
+ kind: "notice",
124
+ adapter: det.adapter,
125
+ notice: `repo matches ${[det.adapter.name, ...det.ambiguousWith].join(", ")} — set "harness" in .vigilesrc.json or use --harness=`,
126
+ };
127
+ }
128
+ return { kind: "ok", adapter: det.adapter };
129
+ }
55
130
  //# sourceMappingURL=adapter-registry.js.map
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A subagent declares an allowed-tools contract in its frontmatter (`tools:`).
5
5
  * But that field is documentation, not a hard runtime boundary (Claude Code
6
- * issue #54898): permissions are session-wide, a subagent inherits the parent
6
+ * #4740/#21460, SDK #172): permissions are session-wide, a subagent inherits the parent
7
7
  * session's grants, and `tools:` only filters what's *offered* — it can't deny
8
8
  * what the session allows. The deterministic layer that actually closes the gap
9
9
  * is a **PreToolUse hook** that blocks any tool the active agent's contract
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * A subagent declares an allowed-tools contract in its frontmatter (`tools:`).
6
6
  * But that field is documentation, not a hard runtime boundary (Claude Code
7
- * issue #54898): permissions are session-wide, a subagent inherits the parent
7
+ * #4740/#21460, SDK #172): permissions are session-wide, a subagent inherits the parent
8
8
  * session's grants, and `tools:` only filters what's *offered* — it can't deny
9
9
  * what the session allows. The deterministic layer that actually closes the gap
10
10
  * is a **PreToolUse hook** that blocks any tool the active agent's contract
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `ArgMatcher` — a small, serializable matcher over a tool call's `input`.
3
+ *
4
+ * Shared by the check vocabulary (`toolWith` / `notTool` in `src/check.ts`) and
5
+ * the tool-interception seam (`src/tool-intercept.ts`), so "did the agent call
6
+ * this tool with these arguments?" means the same thing whether you're *asserting*
7
+ * on a captured call or *intercepting* one before it runs. Pure + model-free.
8
+ */
9
+ /**
10
+ * A declarative matcher over a tool call's `input`, keyed by **dot-path** (e.g.
11
+ * `"body.prompt"`). Each value is matched against the value at that path: a
12
+ * `RegExp` is a pattern over the stringified value (use this for "contains"), and
13
+ * a `string`/`number`/`boolean` is an **exact** match (use this for "equals", e.g.
14
+ * a push target). All keys must match (AND). Serializable, so anything carrying
15
+ * one still round-trips through `toJSON`.
16
+ */
17
+ export type ArgMatcher = Record<string, string | number | boolean | RegExp>;
18
+ /** Render any tool-input value as a string for matching / messages. */
19
+ export declare function stringifyValue(v: unknown): string;
20
+ /** Resolve a dot-path (`"a.b.c"`) within an arbitrary value, or undefined. */
21
+ export declare function getPath(obj: unknown, path: string): unknown;
22
+ /** Does `input` satisfy every entry of `matcher`? (RegExp = pattern, else exact.) */
23
+ export declare function matchesArgs(input: unknown, matcher: ArgMatcher): boolean;
24
+ /** A human-readable form of a matcher for failure messages. */
25
+ export declare function describeArgs(matcher: ArgMatcher): string;
26
+ /** Serialize a matcher for `toJSON` (RegExp → its string form). */
27
+ export declare function serializeArgs(matcher: ArgMatcher): Record<string, string | number | boolean>;
28
+ //# sourceMappingURL=arg-match.d.ts.map
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * `ArgMatcher` — a small, serializable matcher over a tool call's `input`.
4
+ *
5
+ * Shared by the check vocabulary (`toolWith` / `notTool` in `src/check.ts`) and
6
+ * the tool-interception seam (`src/tool-intercept.ts`), so "did the agent call
7
+ * this tool with these arguments?" means the same thing whether you're *asserting*
8
+ * on a captured call or *intercepting* one before it runs. Pure + model-free.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.stringifyValue = stringifyValue;
12
+ exports.getPath = getPath;
13
+ exports.matchesArgs = matchesArgs;
14
+ exports.describeArgs = describeArgs;
15
+ exports.serializeArgs = serializeArgs;
16
+ /** Render any tool-input value as a string for matching / messages. */
17
+ function stringifyValue(v) {
18
+ if (typeof v === "string")
19
+ return v;
20
+ if (typeof v === "object" && v !== null) {
21
+ try {
22
+ return JSON.stringify(v) ?? "[object]";
23
+ }
24
+ catch {
25
+ return "[object]";
26
+ }
27
+ }
28
+ return String(v);
29
+ }
30
+ /** Resolve a dot-path (`"a.b.c"`) within an arbitrary value, or undefined. */
31
+ function getPath(obj, path) {
32
+ let cur = obj;
33
+ for (const part of path.split(".")) {
34
+ if (cur === null || typeof cur !== "object")
35
+ return undefined;
36
+ cur = cur[part];
37
+ }
38
+ return cur;
39
+ }
40
+ /** Does `input` satisfy every entry of `matcher`? (RegExp = pattern, else exact.) */
41
+ function matchesArgs(input, matcher) {
42
+ return Object.entries(matcher).every(([key, m]) => {
43
+ const value = getPath(input, key);
44
+ return m instanceof RegExp ? m.test(stringifyValue(value)) : value === m;
45
+ });
46
+ }
47
+ /** A human-readable form of a matcher for failure messages. */
48
+ function describeArgs(matcher) {
49
+ return Object.entries(matcher)
50
+ .map(([k, m]) => `${k}=${m instanceof RegExp ? String(m) : JSON.stringify(m)}`)
51
+ .join(", ");
52
+ }
53
+ /** Serialize a matcher for `toJSON` (RegExp → its string form). */
54
+ function serializeArgs(matcher) {
55
+ const out = {};
56
+ for (const [k, m] of Object.entries(matcher)) {
57
+ out[k] = m instanceof RegExp ? String(m) : m;
58
+ }
59
+ return out;
60
+ }
61
+ //# sourceMappingURL=arg-match.js.map
package/dist/check.d.ts CHANGED
@@ -18,6 +18,8 @@
18
18
  */
19
19
  import type { Trace } from "./harness-test.js";
20
20
  import type { HookRunResult } from "./run-hook.js";
21
+ import { type ArgMatcher } from "./arg-match.js";
22
+ export type { ArgMatcher };
21
23
  /** The outcome of evaluating one check against one result. */
22
24
  export interface CheckResult {
23
25
  /** Did the check hold? */
@@ -54,6 +56,28 @@ export declare function evalChecks<T>(target: T, checks: readonly Check<T>[]): C
54
56
  export declare function assertChecks<T>(target: T, checks: readonly Check<T>[]): void;
55
57
  /** The agent invoked a tool by this name (regardless of result). */
56
58
  export declare function tool(name: string): Check<Trace>;
59
+ /**
60
+ * The agent used tool `name` with at least one call whose `input` matches `args`
61
+ * — the **argument** half of the tool-call spy. Asserts not just *that* a tool was
62
+ * reached but *how* it was called (the image-request body carried the style suffix,
63
+ * the panel spawn requested a non-expert), which a completion grader can't see.
64
+ *
65
+ * Cross-reference: ≈ promptfoo `is-valid-function-call` / DeepEval `ToolCorrectnessMetric`.
66
+ */
67
+ export declare function toolWith(name: string, args: ArgMatcher): Check<Trace>;
68
+ /**
69
+ * The agent did NOT use tool `name` — the **safety / negative** assertion. With
70
+ * `args`, only calls whose `input` matches are forbidden (so "did not push to
71
+ * `main`" still allows pushing elsewhere; "no paid API call" forbids it outright).
72
+ * This is the highest-value, most-overlooked check, and the one a
73
+ * completion-grading eval structurally cannot make: it sees the agent's *decision*
74
+ * to act, not just its final text.
75
+ *
76
+ * Cross-reference: this negative/safety assertion of a *decision not to act* has
77
+ * no promptfoo / DeepEval / Inspect equivalent — they grade what the agent DID,
78
+ * not what it correctly refrained from doing.
79
+ */
80
+ export declare function notTool(name: string, args?: ArgMatcher): Check<Trace>;
57
81
  /** A skill resolved to this id (`<plugin>:<skill>`) without erroring. */
58
82
  export declare function skill(id: string): Check<Trace>;
59
83
  /** The agent's final output contains a substring / matches a RegExp. */
@@ -112,8 +136,13 @@ interface UsageTrace {
112
136
  readonly usage: {
113
137
  readonly costUsd: number;
114
138
  readonly durationMs: number;
139
+ /** Fresh (uncached) input tokens, billed at full input price. */
115
140
  readonly inputTokens: number;
116
141
  readonly outputTokens: number;
142
+ /** Tokens written to the prompt cache this run (~1.25× input price). */
143
+ readonly cacheCreationTokens: number;
144
+ /** Tokens served from the prompt cache this run (~0.1× input price). */
145
+ readonly cacheReadTokens: number;
117
146
  };
118
147
  }
119
148
  /** The run cost at most `maxUsd`. */
@@ -128,5 +157,27 @@ export declare function latency(opts: {
128
157
  export declare function tokens(opts: {
129
158
  max: number;
130
159
  }): Check<UsageTrace>;
131
- export {};
160
+ /**
161
+ * The run used at most `max` **fresh (uncached) input** tokens. The honest input
162
+ * side of a cost claim: a skill or CLAUDE.md injection adds input every turn, so
163
+ * a "compression" win on output can be erased here. (Cache reads are separate —
164
+ * see `cacheTokens`.)
165
+ */
166
+ export declare function inputTokens(opts: {
167
+ max: number;
168
+ }): Check<UsageTrace>;
169
+ /** The run used at most `max` **output** tokens — the generation side. */
170
+ export declare function outputTokens(opts: {
171
+ max: number;
172
+ }): Check<UsageTrace>;
173
+ /**
174
+ * Bound the prompt-cache token classes a run uses. `maxCreation` caps tokens
175
+ * **written** to the cache (the ~1.25× write premium — a fresh/cold prompt);
176
+ * `maxRead` caps tokens **served** from cache (~0.1× input). Each constraint is
177
+ * checked only when provided; the check passes when every provided bound holds.
178
+ */
179
+ export declare function cacheTokens(opts: {
180
+ maxCreation?: number;
181
+ maxRead?: number;
182
+ }): Check<UsageTrace>;
132
183
  //# sourceMappingURL=check.d.ts.map
package/dist/check.js CHANGED
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.evalChecks = evalChecks;
4
4
  exports.assertChecks = assertChecks;
5
5
  exports.tool = tool;
6
+ exports.toolWith = toolWith;
7
+ exports.notTool = notTool;
6
8
  exports.skill = skill;
7
9
  exports.output = output;
8
10
  exports.hookFired = hookFired;
@@ -17,7 +19,11 @@ exports.judged = judged;
17
19
  exports.cost = cost;
18
20
  exports.latency = latency;
19
21
  exports.tokens = tokens;
22
+ exports.inputTokens = inputTokens;
23
+ exports.outputTokens = outputTokens;
24
+ exports.cacheTokens = cacheTokens;
20
25
  const judge_js_1 = require("./judge.js");
26
+ const arg_match_js_1 = require("./arg-match.js");
21
27
  /** Evaluate every check against a target. Pure — the shared core of `expect`
22
28
  * (strict) and `measure` (scored). */
23
29
  function evalChecks(target, checks) {
@@ -71,6 +77,63 @@ function tool(name) {
71
77
  toJSON: () => ({ kind: "tool", name }),
72
78
  };
73
79
  }
80
+ /**
81
+ * The agent used tool `name` with at least one call whose `input` matches `args`
82
+ * — the **argument** half of the tool-call spy. Asserts not just *that* a tool was
83
+ * reached but *how* it was called (the image-request body carried the style suffix,
84
+ * the panel spawn requested a non-expert), which a completion grader can't see.
85
+ *
86
+ * Cross-reference: ≈ promptfoo `is-valid-function-call` / DeepEval `ToolCorrectnessMetric`.
87
+ */
88
+ function toolWith(name, args) {
89
+ return {
90
+ kind: "toolWith",
91
+ eval: (t) => {
92
+ const calls = t.toolCalls.filter((c) => c.name === name);
93
+ const want = (0, arg_match_js_1.describeArgs)(args);
94
+ if (calls.length === 0) {
95
+ return no(`expected the agent to use tool "${name}" (with ${want}), but it used ${distinctToolNames(t.toolCalls)}`);
96
+ }
97
+ return calls.some((c) => (0, arg_match_js_1.matchesArgs)(c.input, args))
98
+ ? ok(`agent used "${name}" with ${want}`)
99
+ : no(`agent used "${name}" but never with ${want} (saw ${calls
100
+ .map((c) => truncate((0, arg_match_js_1.stringifyValue)(c.input), 60))
101
+ .join("; ")})`);
102
+ },
103
+ toJSON: () => ({ kind: "toolWith", name, args: (0, arg_match_js_1.serializeArgs)(args) }),
104
+ };
105
+ }
106
+ /**
107
+ * The agent did NOT use tool `name` — the **safety / negative** assertion. With
108
+ * `args`, only calls whose `input` matches are forbidden (so "did not push to
109
+ * `main`" still allows pushing elsewhere; "no paid API call" forbids it outright).
110
+ * This is the highest-value, most-overlooked check, and the one a
111
+ * completion-grading eval structurally cannot make: it sees the agent's *decision*
112
+ * to act, not just its final text.
113
+ *
114
+ * Cross-reference: this negative/safety assertion of a *decision not to act* has
115
+ * no promptfoo / DeepEval / Inspect equivalent — they grade what the agent DID,
116
+ * not what it correctly refrained from doing.
117
+ */
118
+ function notTool(name, args) {
119
+ return {
120
+ kind: "notTool",
121
+ eval: (t) => {
122
+ const calls = t.toolCalls.filter((c) => c.name === name);
123
+ const offending = args
124
+ ? calls.filter((c) => (0, arg_match_js_1.matchesArgs)(c.input, args))
125
+ : calls;
126
+ const what = args ? `"${name}" with ${(0, arg_match_js_1.describeArgs)(args)}` : `"${name}"`;
127
+ const first = offending[0];
128
+ if (!first)
129
+ return ok(`agent never used ${what}`);
130
+ return no(`expected the agent NOT to use ${what}, but it did (${truncate((0, arg_match_js_1.stringifyValue)(first.input), 60)})`);
131
+ },
132
+ toJSON: () => args
133
+ ? { kind: "notTool", name, args: (0, arg_match_js_1.serializeArgs)(args) }
134
+ : { kind: "notTool", name },
135
+ };
136
+ }
74
137
  /** A skill resolved to this id (`<plugin>:<skill>`) without erroring. */
75
138
  function skill(id) {
76
139
  return {
@@ -315,4 +378,62 @@ function tokens(opts) {
315
378
  toJSON: () => ({ kind: "tokens", max: opts.max }),
316
379
  };
317
380
  }
381
+ /**
382
+ * The run used at most `max` **fresh (uncached) input** tokens. The honest input
383
+ * side of a cost claim: a skill or CLAUDE.md injection adds input every turn, so
384
+ * a "compression" win on output can be erased here. (Cache reads are separate —
385
+ * see `cacheTokens`.)
386
+ */
387
+ function inputTokens(opts) {
388
+ return {
389
+ kind: "inputTokens",
390
+ eval: (t) => {
391
+ const v = t.usage.inputTokens;
392
+ return v <= opts.max
393
+ ? ok(`${String(v)} input tokens ≤ ${String(opts.max)}`)
394
+ : no(`expected ≤ ${String(opts.max)} input tokens, got ${String(v)}`);
395
+ },
396
+ toJSON: () => ({ kind: "inputTokens", max: opts.max }),
397
+ };
398
+ }
399
+ /** The run used at most `max` **output** tokens — the generation side. */
400
+ function outputTokens(opts) {
401
+ return {
402
+ kind: "outputTokens",
403
+ eval: (t) => {
404
+ const v = t.usage.outputTokens;
405
+ return v <= opts.max
406
+ ? ok(`${String(v)} output tokens ≤ ${String(opts.max)}`)
407
+ : no(`expected ≤ ${String(opts.max)} output tokens, got ${String(v)}`);
408
+ },
409
+ toJSON: () => ({ kind: "outputTokens", max: opts.max }),
410
+ };
411
+ }
412
+ /**
413
+ * Bound the prompt-cache token classes a run uses. `maxCreation` caps tokens
414
+ * **written** to the cache (the ~1.25× write premium — a fresh/cold prompt);
415
+ * `maxRead` caps tokens **served** from cache (~0.1× input). Each constraint is
416
+ * checked only when provided; the check passes when every provided bound holds.
417
+ */
418
+ function cacheTokens(opts) {
419
+ return {
420
+ kind: "cacheTokens",
421
+ eval: (t) => {
422
+ const created = t.usage.cacheCreationTokens;
423
+ const read = t.usage.cacheReadTokens;
424
+ if (opts.maxCreation !== undefined && created > opts.maxCreation) {
425
+ return no(`expected ≤ ${String(opts.maxCreation)} cache-creation tokens, got ${String(created)}`);
426
+ }
427
+ if (opts.maxRead !== undefined && read > opts.maxRead) {
428
+ return no(`expected ≤ ${String(opts.maxRead)} cache-read tokens, got ${String(read)}`);
429
+ }
430
+ return ok(`cache tokens within bounds (created ${String(created)}, read ${String(read)})`);
431
+ },
432
+ toJSON: () => ({
433
+ kind: "cacheTokens",
434
+ ...(opts.maxCreation !== undefined && { maxCreation: opts.maxCreation }),
435
+ ...(opts.maxRead !== undefined && { maxRead: opts.maxRead }),
436
+ }),
437
+ };
438
+ }
318
439
  //# sourceMappingURL=check.js.map
@@ -7,6 +7,7 @@
7
7
  */
8
8
  export * from "./adapters/claude-code/plugin-loader.js";
9
9
  export * from "./mock-model.js";
10
+ export { claudeCodeDriver, buildClaudeArgs, parseClaudeRun, claudeAvailable, } from "./harness-test.js";
10
11
  export * from "./adapters/claude-code/dialect.js";
11
12
  export * from "./adapters/claude-code/layout.js";
12
13
  export * from "./adapters/claude-code/runtime.js";
@@ -14,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.claudeAvailable = exports.parseClaudeRun = exports.buildClaudeArgs = exports.claudeCodeDriver = void 0;
17
18
  /**
18
19
  * `vigiles/claude-code` — the Claude Code-specific harness pieces a *different*
19
20
  * harness would swap out: the plugin/repo loader (reads real Claude Code plugin
@@ -23,6 +24,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
23
24
  */
24
25
  __exportStar(require("./adapters/claude-code/plugin-loader.js"), exports);
25
26
  __exportStar(require("./mock-model.js"), exports);
27
+ // The Claude-Code harness-test transport — the default driver + its argv/parse
28
+ // helpers + the `claude` capability probe. Agnostic users never need these
29
+ // (`runHarnessTest` defaults to the CC driver), but they're exposed here — beside
30
+ // the Codex driver in `vigiles/codex` — for CC-specific tests/tooling. They are
31
+ // deliberately NOT on the agnostic `vigiles/testing` surface.
32
+ var harness_test_js_1 = require("./harness-test.js");
33
+ Object.defineProperty(exports, "claudeCodeDriver", { enumerable: true, get: function () { return harness_test_js_1.claudeCodeDriver; } });
34
+ Object.defineProperty(exports, "buildClaudeArgs", { enumerable: true, get: function () { return harness_test_js_1.buildClaudeArgs; } });
35
+ Object.defineProperty(exports, "parseClaudeRun", { enumerable: true, get: function () { return harness_test_js_1.parseClaudeRun; } });
36
+ Object.defineProperty(exports, "claudeAvailable", { enumerable: true, get: function () { return harness_test_js_1.claudeAvailable; } });
26
37
  __exportStar(require("./adapters/claude-code/dialect.js"), exports);
27
38
  __exportStar(require("./adapters/claude-code/layout.js"), exports);
28
39
  __exportStar(require("./adapters/claude-code/runtime.js"), exports);