vigiles 36.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/eval.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type RunEnv } from "./core/run-env.js";
1
2
  import type { HarnessLiveDriver } from "./core/live-driver.js";
2
3
  import type { AgentRunArgs, AgentRunner, EvalDriver, EvalUsage, ModelOutputParser, ParsedModelRun, RunOut } from "./core/eval-driver.js";
3
4
  export type { AgentRunArgs, AgentRunner, EvalDriver, EvalUsage, ModelOutputParser, ParsedModelRun, RunOut, } from "./core/eval-driver.js";
@@ -7,7 +8,7 @@ import { type EvalLockOptions } from "./eval-lock.js";
7
8
  import type { Check, CheckJSON } from "./check.js";
8
9
  import { type Comparison } from "./stats.js";
9
10
  import { type ToolIntercept } from "./tool-intercept.js";
10
- import { type ToolStub } from "./tool-stub.js";
11
+ import { type StubCall, type ToolStub } from "./core/stub-rules.js";
11
12
  /** One arm of the comparison: fixture overrides + settings (hooks) for this arm. */
12
13
  export interface EvalArm {
13
14
  /** Files written on top of the base fixture for this arm. */
@@ -156,37 +157,41 @@ export interface EvalSpec<M extends Metrics> {
156
157
  /** Base backoff ms (doubled each retry). Default 1000. */
157
158
  readonly retryBackoffMs?: number;
158
159
  /**
159
- * **Opt-in, default OFF.** Run each trial in an *ephemeral run environment* — a
160
- * throwaway `$HOME` + scrubbed env, re-injecting only the harness's own auth (see
161
- * `ephemeralRunEnv`). Running a model-driven skill/agent is itself a side
162
- * effect (the *model*, not the author, chose the actions), so a `git push` /
163
- * write to `~` should land in a disposable HOME, not the real `~/.gitconfig` /
164
- * `~/.ssh` / `~/.aws`. This is the cross-platform STATE-protection floor (no
165
- * kernel features), orthogonal to the bubblewrap host-confinement in
166
- * `src/sandbox.ts`.
160
+ * **Where each trial runs — required, no default.**
167
161
  *
168
- * **Ships default-OFF** because a too-narrow auth allowlist would silently break
169
- * the real `claude` CLI's authentication; leaving it off keeps every existing
170
- * eval (including one running right now) authenticating exactly as before. When
171
- * absent / `false`, the per-trial env is the caller's (`{ ...process.env,
172
- * ...arm.env }`) minus the harness's declared session identity, which no child
173
- * run inherits in either mode. See `docs/safety.md` (ephemerality) and
174
- * `research/cross-platform-sandboxing.md`.
175
- */
176
- readonly ephemeralEnv?: boolean;
177
- /**
178
- * **Tool stubs on PATH (rung R2).** A list of fake binaries to shadow on PATH
179
- * for every trial, so a skill/hook/agent that calls a CLI tool (`gh`, `psql`,
180
- * `redis-cli`, `z3`, …) and works with its RESULT can be tested against a
181
- * **recorded / author-provided canned output** — no live service. vigiles writes
182
- * one executable stub per {@link ToolStub} into a bin dir under the trial cwd and
183
- * PREPENDS that dir to the run's PATH (both the default and the ephemeral env
184
- * path), so the fake wins over the real binary.
162
+ * - `{ kind: "ephemeral" }` — a throwaway HOME (under the trial's temp dir) and
163
+ * a scrubbed environment: the OS essentials, the harness's own auth
164
+ * (`runEnv.keep`, plus its auth file copied in), the eval's own `VIGILES_*`
165
+ * variables, and nothing else. `home: { kind: "files", files }` seeds that
166
+ * HOME first (HOME-relative paths; the harness's auth file cannot be
167
+ * overwritten). Running a model-driven skill is itself a side effect, so a
168
+ * `git push` or a write to `~` lands in a disposable HOME.
169
+ * - `{ kind: "inherit", reason }` — your real HOME and environment, minus the
170
+ * parent session's identity. `reason` is printed with the report.
185
171
  *
186
- * The stubs are author/recorded fixtures, **never** model-synthesized — a
187
- * synthesized tool output looks plausible but diverges from the real
188
- * tool/version (false confidence). Absent → no change (the PATH is byte-identical
189
- * to today). See {@link ToolStub} and `research/eval-coverage-and-isolation.md`.
172
+ * There is no default because both are wrong silently in one case: inheriting
173
+ * measures the author's machine (their config, their tokens), and an
174
+ * ephemeral run fails if a user's credential is not where the harness says it
175
+ * is — loudly, on trial 1, which is why it is the one to try first. See
176
+ * `docs/safety.md` (what a child run inherits).
177
+ */
178
+ readonly env: RunEnv;
179
+ /**
180
+ * **Tool stubs on PATH (rung R2)** — fake binaries that answer PER INVOCATION.
181
+ * Build each with {@link experimental_stub}: a list of rules over the argv
182
+ * tokens, first match wins. vigiles writes a shim per stub into a temp dir
183
+ * BESIDE the trial's working dir and puts it first on the run's PATH, so a
184
+ * skill/hook/agent — or a script it runs — that calls `gh`/`psql`/`git` gets
185
+ * the recorded answer instead of the real service.
186
+ *
187
+ * An invocation no rule answers is UNANSWERED: the model sees one neutral
188
+ * stderr line and a non-zero exit, no new trials start, the report lists the
189
+ * call (`ArmReport.unansweredStubCalls`), `vigiles eval` exits 2 and
190
+ * `--update` writes no lock. Every call is recorded in `RunContext.stubCalls`,
191
+ * so `experimental_stub.called(...)` can assert one happened.
192
+ *
193
+ * The answers are author/recorded fixtures, **never** model-synthesized — a
194
+ * synthesized tool output looks plausible but diverges from the real tool.
190
195
  */
191
196
  readonly stubs?: readonly ToolStub[];
192
197
  /**
@@ -236,6 +241,12 @@ export interface ArmReport {
236
241
  readonly stats: Record<string, MetricStat>;
237
242
  /** Cost / latency / token totals + means for this arm. */
238
243
  readonly usage: ArmUsage;
244
+ /**
245
+ * Stub invocations no rule answered, across this arm's trials. Non-empty means
246
+ * the arm measured at least one answer nobody wrote: `vigiles eval` exits 2 on
247
+ * it and `--update` refuses to record the lock. Empty when the eval has no stubs.
248
+ */
249
+ readonly unansweredStubCalls: readonly StubCall[];
239
250
  }
240
251
  export interface EvalReport {
241
252
  readonly name: string;
@@ -245,10 +256,21 @@ export interface EvalReport {
245
256
  readonly totalCostUsd: number;
246
257
  /** True if a `maxCostUsd` budget cap stopped the run before all trials ran. */
247
258
  readonly aborted: boolean;
259
+ /**
260
+ * The run environment the trials used, without seeded file contents — so an
261
+ * `inherit` run's reason travels with the report and is printed with it.
262
+ * Absent on a report built before `env` existed.
263
+ */
264
+ readonly env?: {
265
+ readonly kind: "ephemeral";
266
+ } | {
267
+ readonly kind: "inherit";
268
+ readonly reason: string;
269
+ };
248
270
  }
249
271
  /**
250
272
  * Resolve the environment a trial's subprocess actually runs with — the
251
- * SECURITY-CRITICAL decision behind `ephemeralEnv`. When `replaceEnv` is set, the
273
+ * SECURITY-CRITICAL decision behind `env: { kind: "ephemeral" }`. When `replaceEnv` is set, the
252
274
  * scrubbed `env` is the COMPLETE environment, so the real `$HOME` and inherited
253
275
  * secrets are DROPPED; otherwise `env` is an overlay on `base`. In BOTH, the
254
276
  * harness's declared session identity (`runEnv.sessionIdentity`) is removed: a
@@ -501,8 +523,17 @@ export declare function measureArms(spec: ArmsMeasureSpec): Promise<ArmsCheckRep
501
523
  * than vanilla" is a p-value, not a vibe. Reuses `welchTTest` from stats.ts.
502
524
  */
503
525
  export declare function compareCheck(report: ArmsCheckReport, baseline: string, arm: string, checkIndex: number): Comparison;
504
- /** Format a {@link CheckReport}: one line per check with its rate ± se and pass^k. */
526
+ /**
527
+ * Format a {@link CheckReport}: one line per check with its rate ± se and
528
+ * pass^k, and the line saying `measure` ran with your HOME and environment
529
+ * (it has no `env` field; `INHERITED_BY_MEASURE`).
530
+ */
505
531
  export declare function formatCheckReport(report: CheckReport): string;
532
+ /**
533
+ * Format an {@link ArmsCheckReport}: each arm's checks under `[arm: name]`, and
534
+ * the inherit line ONCE at the end — every arm ran the same way.
535
+ */
536
+ export declare function formatArmsCheckReport(report: ArmsCheckReport): string;
506
537
  /**
507
538
  * The scored gate (Phase 4): throw if any check's measured rate is below its
508
539
  * threshold — the `measure` counterpart to `assertChecks` (strict). Reads the
@@ -652,6 +683,23 @@ export declare function seedEphemeralHome(throwawayHome: string, realHome: strin
652
683
  export declare function isRateLimited(out: RunOut): boolean;
653
684
  /** Map `worker` over `items` with at most `concurrency` in flight, order preserved. */
654
685
  export declare function runPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T) => Promise<R>): Promise<R[]>;
686
+ /**
687
+ * The author-facing failure for a report whose stubs left a call unanswered, or
688
+ * undefined. One owner for the rule — `vigiles eval`'s exit 2 and the lock's
689
+ * refusal both read it — so a consumer whose `assert` calls no helper still
690
+ * fails. Pure.
691
+ */
692
+ export declare function unansweredInReport(report: Pick<EvalReport, "arms">, stubs: readonly ToolStub[]): string | undefined;
693
+ /**
694
+ * `runEval` with its author-facing output, over any runner — so the real one and
695
+ * a test's fake share one path. Besides the run it prints, to stderr, what the
696
+ * caller must not miss: a skill arm that never activates, a stub call no rule
697
+ * answered (the same text `vigiles eval` fails with — a script that calls
698
+ * `runEval` itself, as the `withServices` examples do, gets the report back and
699
+ * nothing else, so the library is not silent about it either), and what the
700
+ * run spent.
701
+ */
702
+ export declare function runEvalReporting<M extends Metrics>(spec: EvalSpec<M>, runner: AgentRunner): Promise<EvalReport>;
655
703
  export declare function runEvalWith<M extends Metrics>(input: EvalSpec<M>, runner: AgentRunner): Promise<EvalReport>;
656
704
  /** Format an eval report as a compact table for the console (mean ± se, pass^k). */
657
705
  export declare function formatEvalReport(report: EvalReport): string;