vigiles 4.0.1 → 4.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.
Files changed (58) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +1 -1
  3. package/dist/adapter-conformance.js +1 -1
  4. package/dist/adapter-registry.d.ts +45 -1
  5. package/dist/adapter-registry.js +78 -3
  6. package/dist/check.d.ts +132 -0
  7. package/dist/check.js +318 -0
  8. package/dist/cli.js +255 -73
  9. package/dist/core/compile.d.ts +1 -1
  10. package/dist/core/compile.js +1 -1
  11. package/dist/core/compose.d.ts +1 -1
  12. package/dist/core/compose.js +1 -1
  13. package/dist/core/generate-schema.d.ts +1 -1
  14. package/dist/core/generate-schema.js +4 -4
  15. package/dist/core/linters.js +2 -2
  16. package/dist/core/orphans.js +57 -14
  17. package/dist/core/proofs.js +1 -1
  18. package/dist/core/refs.d.ts +1 -1
  19. package/dist/core/refs.js +2 -2
  20. package/dist/core/sidecar.d.ts +1 -1
  21. package/dist/core/sidecar.js +1 -1
  22. package/dist/core/spec.d.ts +1 -1
  23. package/dist/core/spec.js +1 -1
  24. package/dist/core/types.d.ts +11 -1
  25. package/dist/core/validate.js +2 -2
  26. package/dist/e2e.d.ts +10 -13
  27. package/dist/e2e.js +10 -17
  28. package/dist/eval.d.ts +217 -2
  29. package/dist/eval.js +428 -18
  30. package/dist/harness-assert.d.ts +3 -0
  31. package/dist/harness-assert.js +16 -0
  32. package/dist/harness-test.d.ts +46 -0
  33. package/dist/harness-test.js +102 -0
  34. package/dist/integration.d.ts +8 -0
  35. package/dist/integration.js +10 -0
  36. package/dist/jest.d.ts +3 -1
  37. package/dist/jest.js +3 -2
  38. package/dist/run-hook.d.ts +22 -0
  39. package/dist/run-hook.js +28 -0
  40. package/dist/scan.d.ts +1 -1
  41. package/dist/scan.js +1 -1
  42. package/dist/setup-plan.d.ts +16 -1
  43. package/dist/setup-plan.js +38 -1
  44. package/dist/skill-harness.d.ts +25 -0
  45. package/dist/skill-harness.js +40 -0
  46. package/dist/test-coverage.js +8 -1
  47. package/dist/testing.d.ts +2 -0
  48. package/dist/testing.js +7 -0
  49. package/dist/unit.d.ts +4 -2
  50. package/dist/unit.js +7 -1
  51. package/dist/vitest.d.mts +3 -1
  52. package/hooks/refs-nudge.sh +1 -1
  53. package/package.json +3 -2
  54. package/skills/edit-spec/SKILL.md +21 -10
  55. package/skills/linter-docs/SKILL.md +23 -0
  56. package/skills/migrate-to-spec/SKILL.md +1 -1
  57. package/skills/strengthen/SKILL.md +1 -2
  58. package/skills/generate-rule/SKILL.md +0 -64
@@ -461,7 +461,7 @@ function isEslintPluginRule(ruleName, basePath) {
461
461
  /**
462
462
  * Enumerate all rules for a CLI-based linter so `tryCliCheck` can emit
463
463
  * closest-match suggestions on typos. Result is cached per (linter,
464
- * basePath) so each linter's discovery CLI runs at most once per audit.
464
+ * basePath) so each linter's discovery CLI runs at most once per lint.
465
465
  */
466
466
  const CLI_RULE_SET_CACHE = new Map();
467
467
  function getCliRuleSet(linterName, basePath) {
@@ -561,7 +561,7 @@ function getCliRuleSet(linterName, basePath) {
561
561
  // The `vigiles/<id>` namespace lets specs declare mechanical checks that
562
562
  // vigiles itself runs (orphan docs, integrity, etc.) without delegating to
563
563
  // an external linter. Existence is verified at compile time against this
564
- // fixed catalog; the actual check runs at audit time.
564
+ // fixed catalog; the actual check runs at lint time.
565
565
  // ---------------------------------------------------------------------------
566
566
  const VIGILES_INTERNAL_RULES = new Set(["orphan-docs"]);
567
567
  /** @internal */ function tryVigilesInternal(ctx) {
@@ -28,6 +28,13 @@ const DEFAULT_IGNORE = [
28
28
  ".vigiles/**",
29
29
  ".git/**",
30
30
  ];
31
+ /**
32
+ * A doc carrying this marker opts out of orphan detection — the inline escape
33
+ * hatch, mirroring `vigiles-disable require-spec` and `vigiles:ignore-test`.
34
+ * Use it for an intentionally-unreferenced doc (a changelog, a top-level index)
35
+ * that nothing else links to but is not rot.
36
+ */
37
+ const DISABLE_RE = /<!--\s*vigiles-disable\s+orphan-docs\s*-->/;
31
38
  // Match markdown links ](path.md) or ](path.md#anchor)
32
39
  const LINK_RE = /\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g;
33
40
  // Match backtick code spans wrapping a path ending in .md
@@ -35,6 +42,27 @@ const BACKTICK_RE = /`([^`\s]+\.md)`/g;
35
42
  function normalizePath(p) {
36
43
  return p.replace(/^\.\//, "").replace(/\\/g, "/");
37
44
  }
45
+ /** True when a doc opts out of orphan detection via the inline disable marker. */
46
+ function isOrphanExempt(absPath) {
47
+ try {
48
+ return DISABLE_RE.test((0, node_fs_1.readFileSync)(absPath, "utf-8"));
49
+ }
50
+ catch {
51
+ return false; // unreadable — treat like any other doc
52
+ }
53
+ }
54
+ /** Discover docs under `include`, dropping any that carry the inline opt-out. */
55
+ function collectDocs(basePath, include, ignore) {
56
+ const docs = new Set();
57
+ for (const pattern of include) {
58
+ for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
59
+ if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
60
+ continue;
61
+ docs.add(normalizePath(p));
62
+ }
63
+ }
64
+ return docs;
65
+ }
38
66
  function extractRefs(content) {
39
67
  const refs = [];
40
68
  for (const m of content.matchAll(LINK_RE))
@@ -43,6 +71,23 @@ function extractRefs(content) {
43
71
  refs.push(normalizePath(m[1]));
44
72
  return refs;
45
73
  }
74
+ /**
75
+ * Repo-root-relative targets a reference could mean, from a given source file.
76
+ * Markdown links are conventionally **file-relative** (a `[x](foo.md)` in
77
+ * `research/README.md` points at `research/foo.md`, and `../docs/x.md` walks up),
78
+ * but docs also write **root-relative** paths (`research/foo.md` from anywhere).
79
+ * We credit both so a real link is never miscounted as an orphan.
80
+ */
81
+ function refTargets(sourcePath, ref) {
82
+ const targets = new Set([ref]); // root-relative reading
83
+ const dir = sourcePath.includes("/")
84
+ ? sourcePath.slice(0, sourcePath.lastIndexOf("/"))
85
+ : "";
86
+ // file-relative reading: resolve against the source's directory.
87
+ const resolved = normalizePath(node_path_1.posix.normalize(dir ? `${dir}/${ref}` : ref));
88
+ targets.add(resolved);
89
+ return [...targets];
90
+ }
46
91
  // ---------------------------------------------------------------------------
47
92
  // Public API
48
93
  // ---------------------------------------------------------------------------
@@ -63,12 +108,7 @@ function findOrphanDocs(options = {}) {
63
108
  const include = options.include ?? DEFAULT_INCLUDE;
64
109
  const userExclude = options.exclude ?? [];
65
110
  const ignore = [...DEFAULT_IGNORE, ...userExclude];
66
- const allDocs = new Set();
67
- for (const pattern of include) {
68
- const found = (0, glob_1.globSync)(pattern, { cwd: basePath, ignore });
69
- for (const p of found)
70
- allDocs.add(normalizePath(p));
71
- }
111
+ const allDocs = collectDocs(basePath, include, ignore);
72
112
  const allMarkdown = (0, glob_1.globSync)("**/*.md", {
73
113
  cwd: basePath,
74
114
  ignore: [...DEFAULT_IGNORE],
@@ -83,15 +123,17 @@ function findOrphanDocs(options = {}) {
83
123
  catch {
84
124
  continue;
85
125
  }
86
- for (const target of extractRefs(content)) {
87
- if (target === source)
88
- continue;
89
- let sources = referencedBy.get(target);
90
- if (!sources) {
91
- sources = new Set();
92
- referencedBy.set(target, sources);
126
+ for (const rawRef of extractRefs(content)) {
127
+ for (const target of refTargets(source, rawRef)) {
128
+ if (target === source)
129
+ continue;
130
+ let sources = referencedBy.get(target);
131
+ if (!sources) {
132
+ sources = new Set();
133
+ referencedBy.set(target, sources);
134
+ }
135
+ sources.add(source);
93
136
  }
94
- sources.add(source);
95
137
  }
96
138
  }
97
139
  const orphans = [];
@@ -119,6 +161,7 @@ function formatOrphanReport(report) {
119
161
  ];
120
162
  for (const o of report.orphans)
121
163
  lines.push(` ${o}`);
164
+ lines.push(" Fix: link each from another .md (README, a spec's Key Files, or a doc).", " To silence: add `<!-- vigiles-disable orphan-docs -->` to the doc, or", " exclude it via .vigilesrc.json → `orphans.exclude` (or narrow `orphans.include`).");
122
165
  return lines.join("\n");
123
166
  }
124
167
  //# sourceMappingURL=orphans.js.map
@@ -188,7 +188,7 @@ function findSimilarRules(rules, threshold = 0.5) {
188
188
  *
189
189
  * Throws a structured error on unknown rule kinds so that the caller (e.g.
190
190
  * runProofSuite) can surface a clear proof failure rather than letting an
191
- * `undefined` propagate into compressedSize and crash the audit.
191
+ * `undefined` propagate into compressedSize and crash the computation.
192
192
  */
193
193
  function ruleToText(rule) {
194
194
  switch (rule._kind) {
@@ -29,7 +29,7 @@ export declare function symbolRefs(markdown: string): SymbolRef[];
29
29
  export declare function verifySymbolRefs(markdown: string, basePath: string): SymbolRefError[];
30
30
  /**
31
31
  * Whether a span is a **linter-rule reference** that ought to be marked
32
- * (`enforce()` / inline `<!-- vigiles:enforce -->`) so the audit can verify the
32
+ * (`enforce()` / inline `<!-- vigiles:enforce -->`) so the lint can verify the
33
33
  * rule exists AND is enabled. High-signal only: a slash-scoped name with no file
34
34
  * extension. A function-call form `` `foo(args)` `` is reduced to its callee.
35
35
  *
package/dist/core/refs.js CHANGED
@@ -109,7 +109,7 @@ const IGNORE_FILE = /<!--\s*vigiles:ignore-file\s*-->/;
109
109
  const IGNORE_LINE = /<!--\s*vigiles:ignore\s*-->/;
110
110
  /**
111
111
  * Whether a span is a **linter-rule reference** that ought to be marked
112
- * (`enforce()` / inline `<!-- vigiles:enforce -->`) so the audit can verify the
112
+ * (`enforce()` / inline `<!-- vigiles:enforce -->`) so the lint can verify the
113
113
  * rule exists AND is enabled. High-signal only: a slash-scoped name with no file
114
114
  * extension. A function-call form `` `foo(args)` `` is reduced to its callee.
115
115
  *
@@ -152,7 +152,7 @@ function collectRefIssues(markdown, basePath) {
152
152
  for (const u of unmarkedCodeRefs(markdown)) {
153
153
  out.push(`line ${String(u.line)}: \`${u.text}\` is an unmarked linter-rule ` +
154
154
  `reference — mark it as \`enforce("${u.text}")\` (typed spec) or ` +
155
- `\`<!-- vigiles:enforce ${u.text} -->\` (markdown) so audit can verify ` +
155
+ `\`<!-- vigiles:enforce ${u.text} -->\` (markdown) so lint can verify ` +
156
156
  `it exists and is enabled, or add <!-- vigiles:ignore --> if it is prose`);
157
157
  }
158
158
  return out;
@@ -4,7 +4,7 @@
4
4
  * Used by the post-session audit to know which targets exist and which
5
5
  * spec source / inputs each one tracks. The compile pipeline writes
6
6
  * these whenever a spec is built; readers (currently only session.ts)
7
- * consume them at audit time.
7
+ * consume them at verification time.
8
8
  *
9
9
  * This module is the ONLY place sidecars live now — the freshness rule
10
10
  * doesn't depend on them anymore.
@@ -5,7 +5,7 @@
5
5
  * Used by the post-session audit to know which targets exist and which
6
6
  * spec source / inputs each one tracks. The compile pipeline writes
7
7
  * these whenever a spec is built; readers (currently only session.ts)
8
- * consume them at audit time.
8
+ * consume them at verification time.
9
9
  *
10
10
  * This module is the ONLY place sidecars live now — the freshness rule
11
11
  * doesn't depend on them anymore.
@@ -156,7 +156,7 @@ export declare function cmd(command: NoInfer<StrictCmd>): CmdRef;
156
156
  * Reference a symbol defined in a file — verified at compile time that the
157
157
  * named file exists AND defines the named symbol (via ast-grep, cross-language).
158
158
  * Compiles to the file-qualified inline form `` `file#symbol` `` so the markdown
159
- * `audit` / `refs-hook` re-verify the same reference.
159
+ * `lint` / `refs-hook` re-verify the same reference.
160
160
  */
161
161
  export declare function symbol(file: NoInfer<StrictFile>, name: string): SymbolRef;
162
162
  /**
package/dist/core/spec.js CHANGED
@@ -93,7 +93,7 @@ function cmd(command) {
93
93
  * Reference a symbol defined in a file — verified at compile time that the
94
94
  * named file exists AND defines the named symbol (via ast-grep, cross-language).
95
95
  * Compiles to the file-qualified inline form `` `file#symbol` `` so the markdown
96
- * `audit` / `refs-hook` re-verify the same reference.
96
+ * `lint` / `refs-hook` re-verify the same reference.
97
97
  */
98
98
  function symbol(file, name) {
99
99
  return { _ref: "symbol", file: file, symbol: name };
@@ -97,7 +97,7 @@ export interface RulesConfig {
97
97
  "untested-surface"?: RuleWithOptions<TestCoverageConfig>;
98
98
  /**
99
99
  * Nudge (or block) when an instruction file has code-shaped references that
100
- * aren't expressed as vigiles marks (so the audit can't verify them), or a
100
+ * aren't expressed as vigiles marks (so the lint can't verify them), or a
101
101
  * `vigiles:symbol` mark that points at a missing symbol. Drives the
102
102
  * PostToolUse refs-hook: "warn" (default) → a non-blocking nudge, "error" →
103
103
  * block the edit, false → off.
@@ -127,6 +127,16 @@ export interface VigilesConfig {
127
127
  }>;
128
128
  /** Orphan-docs check configuration. Include/exclude globs, tsconfig-style. */
129
129
  orphans?: OrphansConfig;
130
+ /**
131
+ * The harness(es) this repo targets — selects the compile dialect / skill
132
+ * frontmatter profile / instruction-file shape, instead of sniffing the cwd.
133
+ * A single name (`"codex"`) for the common single-harness repo, or an array
134
+ * (`["claude-code", "codex"]`) declaring the supported set. Written by
135
+ * `vigiles init`. Omitted → the CLI auto-detects (backwards-compatible).
136
+ * Canonical adapter names; `"claude"` is accepted as an alias for
137
+ * `"claude-code"`. See research/multi-harness-compile.md.
138
+ */
139
+ harness?: string | string[];
130
140
  }
131
141
  /** Valid marker types for rule detection. */
132
142
  export type MarkerType = "headings" | "checkboxes";
@@ -157,9 +157,9 @@ function validate(content, { ruleMarkers, rules: rulesConfig, filePath, dialect
157
157
  const specPath = filePath + ".spec.ts";
158
158
  // Inline mode counts as a spec — any parseable
159
159
  // `<!-- vigiles:enforce ... -->` comment means the file is
160
- // verified on `vigiles audit` even without a .spec.ts sibling.
160
+ // verified on `vigiles lint` even without a .spec.ts sibling.
161
161
  // Delegate to the real parser so a malformed marker can't
162
- // satisfy require-spec with a rule that audit can't verify.
162
+ // satisfy require-spec with a rule that lint can't verify.
163
163
  const hasInline = (0, inline_js_1.hasInlineRules)(content) || (0, frontmatter_js_1.hasFrontmatterRules)(content);
164
164
  if (!(0, node_fs_1.existsSync)(specPath) && !hasInline) {
165
165
  const msg = {
package/dist/e2e.d.ts CHANGED
@@ -1,19 +1,16 @@
1
1
  /**
2
- * `vigiles/e2e` — the **deterministic** end-to-end tier (real sandbox + real
3
- * network, but a definite pass/fail).
2
+ * `vigiles/e2e` — DEPRECATED back-compat alias for [`vigiles/integration`](./integration.ts).
4
3
  *
5
- * Re-exports everything in [`vigiles/integration`](./integration.ts) and adds the
6
- * real-egress capability: `egressRoutes()` (probe whether allowlisted egress can
7
- * actually route) and by re-export — `runHook` used with `egress: { allow }`
8
- * (allowlisted real outbound). Capability contract: needs a **routable rootless
9
- * sandbox + real network**, and each test self-skips via `egressRoutes()` where
10
- * that's unavailable. Still a **verification** tier you assert pass/fail.
4
+ * There is no separate "e2e" tier: real **egress** is a *capability* of the
5
+ * harness/integration scope (`egressRoutes()` + `runHook`'s `egress: { allow }`),
6
+ * not a different kind of testthe old `e2e` barrel added exactly one symbol
7
+ * over `integration`, which is the definition of a non-tier. It now lives on
8
+ * `vigiles/integration`; this entry re-exports it unchanged so existing imports
9
+ * keep working. See `research/testing-api-design.md` Part 4 (two scopes, not
10
+ * four tiers). Prefer `vigiles/integration`.
11
11
  *
12
- * NOT here: **evals** (`runEval` / `measureTriggerRate` / `judge`). Those are a
13
- * different axis — **non-deterministic measurement** (real model, mean ± se), run
14
- * via `vigiles eval` on `*.eval.mjs`. Import them from
15
- * [`vigiles/eval`](./eval.ts) + [`vigiles/judge`](./judge.ts), not from here.
12
+ * NOT here: **evals** (`runEval` / `measure` / `measureTriggerRate` / `judge`)
13
+ * those are non-deterministic measurement (`vigiles/eval`), a different axis.
16
14
  */
17
15
  export * from "./integration.js";
18
- export { egressRoutes } from "./run-hook.js";
19
16
  //# sourceMappingURL=e2e.d.ts.map
package/dist/e2e.js CHANGED
@@ -14,26 +14,19 @@ 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.egressRoutes = void 0;
18
17
  /**
19
- * `vigiles/e2e` — the **deterministic** end-to-end tier (real sandbox + real
20
- * network, but a definite pass/fail).
18
+ * `vigiles/e2e` — DEPRECATED back-compat alias for [`vigiles/integration`](./integration.ts).
21
19
  *
22
- * Re-exports everything in [`vigiles/integration`](./integration.ts) and adds the
23
- * real-egress capability: `egressRoutes()` (probe whether allowlisted egress can
24
- * actually route) and by re-export — `runHook` used with `egress: { allow }`
25
- * (allowlisted real outbound). Capability contract: needs a **routable rootless
26
- * sandbox + real network**, and each test self-skips via `egressRoutes()` where
27
- * that's unavailable. Still a **verification** tier you assert pass/fail.
20
+ * There is no separate "e2e" tier: real **egress** is a *capability* of the
21
+ * harness/integration scope (`egressRoutes()` + `runHook`'s `egress: { allow }`),
22
+ * not a different kind of testthe old `e2e` barrel added exactly one symbol
23
+ * over `integration`, which is the definition of a non-tier. It now lives on
24
+ * `vigiles/integration`; this entry re-exports it unchanged so existing imports
25
+ * keep working. See `research/testing-api-design.md` Part 4 (two scopes, not
26
+ * four tiers). Prefer `vigiles/integration`.
28
27
  *
29
- * NOT here: **evals** (`runEval` / `measureTriggerRate` / `judge`). Those are a
30
- * different axis — **non-deterministic measurement** (real model, mean ± se), run
31
- * via `vigiles eval` on `*.eval.mjs`. Import them from
32
- * [`vigiles/eval`](./eval.ts) + [`vigiles/judge`](./judge.ts), not from here.
28
+ * NOT here: **evals** (`runEval` / `measure` / `measureTriggerRate` / `judge`)
29
+ * those are non-deterministic measurement (`vigiles/eval`), a different axis.
33
30
  */
34
31
  __exportStar(require("./integration.js"), exports);
35
- // The real-egress capability probe (the egress-using runHook is already re-exported
36
- // via the integration→unit chain).
37
- var run_hook_js_1 = require("./run-hook.js");
38
- Object.defineProperty(exports, "egressRoutes", { enumerable: true, get: function () { return run_hook_js_1.egressRoutes; } });
39
32
  //# sourceMappingURL=e2e.js.map
package/dist/eval.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { type ToolCall, type Trace } from "./harness-test.js";
2
2
  import { type CacheMode } from "./eval-cache.js";
3
+ import type { Check, CheckJSON } from "./check.js";
4
+ import { type Comparison } from "./stats.js";
3
5
  /** One arm of the comparison: fixture overrides + settings (hooks) for this arm. */
4
6
  export interface EvalArm {
5
7
  /** Files written on top of the base fixture for this arm. */
@@ -172,6 +174,127 @@ export type AgentRunner = (args: AgentRunArgs) => Promise<RunOut>;
172
174
  * {@link runEvalWith} with the real agent runner.
173
175
  */
174
176
  export declare function runEval<M extends Metrics>(spec: EvalSpec<M>): Promise<EvalReport>;
177
+ /** A task run N times, scored against a `Trace` check vocabulary. */
178
+ export interface MeasureSpec {
179
+ /** Base fixture files written for every run (path → contents). */
180
+ readonly fixture?: Record<string, string>;
181
+ /** `.claude/settings.json` (hooks/permissions) for the run. */
182
+ readonly settings?: unknown;
183
+ /** A real plugin/repo to load (materialized) — see `EvalArm.plugin`. */
184
+ readonly plugin?: string;
185
+ /** A complete plugin dir to install natively (`--plugin-dir`) so skills activate. */
186
+ readonly pluginDir?: string;
187
+ /**
188
+ * Stub each skill BODY in `pluginDir` (frontmatter/trigger surface kept) before
189
+ * the run — for checks about whether a skill FIRES (`skill()`), not what it
190
+ * produces. A selected skill stops at selection instead of running its (often
191
+ * expensive) procedure, so a description/firing run costs a fraction of the
192
+ * tokens. Do NOT combine with `judged`/quality checks: the body is gone, so
193
+ * there's nothing to grade. Requires `pluginDir`. See {@link stubSkillBody}.
194
+ */
195
+ readonly stubSkillBodies?: boolean;
196
+ /** The task prompt given to the agent. */
197
+ readonly task: string;
198
+ /**
199
+ * The checks to score across the trials. Any check over the run is accepted:
200
+ * `Trace` checks (`tool`/`skill`/`output`/`mcp`/`judged`) and resource checks
201
+ * (`cost`/`latency`/`tokens`, which read the eval-only `usage`) — all fit
202
+ * `Check<RunContext>`.
203
+ */
204
+ readonly checks: readonly Check<RunContext>[];
205
+ /** Trials. Default 5. */
206
+ readonly trials?: number;
207
+ /** Model alias. Default "sonnet" — measure on the model your users run. */
208
+ readonly model?: string;
209
+ /** Tools the agent may use. */
210
+ readonly allowedTools?: readonly string[];
211
+ /** Per-run timeout ms. */
212
+ readonly timeoutMs?: number;
213
+ /** Seconds between runs. */
214
+ readonly spacingSec?: number;
215
+ }
216
+ /** One check's measured rate across the trials. */
217
+ export interface CheckRate {
218
+ readonly check: CheckJSON;
219
+ /** Fraction of trials the check passed (0..1). */
220
+ readonly rate: number;
221
+ /** Standard error of the rate. */
222
+ readonly se: number;
223
+ /** pass^k — 1 iff the check passed on EVERY trial. */
224
+ readonly passK: number;
225
+ /** Trials observed. */
226
+ readonly n: number;
227
+ }
228
+ export interface CheckReport {
229
+ readonly n: number;
230
+ readonly perCheck: readonly CheckRate[];
231
+ }
232
+ /**
233
+ * Score a check vocabulary across trials — the scored counterpart to
234
+ * `assertChecks` (strict). Each check yields a `rate ± se` and `pass^k` over `n`
235
+ * runs. Reuses the tested `runEvalWith` aggregation (one arm), so the loop,
236
+ * cache, concurrency, and stats come for free. Exported with an injectable
237
+ * `runner` so the orchestration is unit-testable without a model.
238
+ */
239
+ export declare function measureWith(spec: MeasureSpec, runner: AgentRunner): Promise<CheckReport>;
240
+ /** Score a check vocabulary across trials against the real `claude` CLI. */
241
+ export declare function measure(spec: MeasureSpec): Promise<CheckReport>;
242
+ /** A task scored against checks across NAMED arms (the harness variable on/off). */
243
+ export interface ArmsMeasureSpec {
244
+ readonly fixture?: Record<string, string>;
245
+ /** The arms to compare (settings / plugin / pluginDir per arm). */
246
+ readonly arms: Record<string, EvalArm>;
247
+ readonly task: string;
248
+ readonly checks: readonly Check<RunContext>[];
249
+ /**
250
+ * Stub each arm's skill BODIES (frontmatter kept) before the run — the A/B
251
+ * counterpart to {@link MeasureSpec.stubSkillBodies}. For firing comparisons
252
+ * (does description variant A fire more than B?), every arm that sets
253
+ * `pluginDir` is repackaged with bodies stripped so each run stops at
254
+ * selection — a fraction of the tokens. Arms without a `pluginDir` are left
255
+ * untouched. Don't combine with `judged`/quality checks. See {@link stubSkillBody}.
256
+ */
257
+ readonly stubSkillBodies?: boolean;
258
+ readonly trials?: number;
259
+ readonly model?: string;
260
+ readonly allowedTools?: readonly string[];
261
+ readonly timeoutMs?: number;
262
+ readonly spacingSec?: number;
263
+ }
264
+ /** Per-arm {@link CheckReport}s — `arms[name].perCheck[i]` aligns across arms. */
265
+ export interface ArmsCheckReport {
266
+ readonly arms: Record<string, CheckReport>;
267
+ }
268
+ /** Score checks across arms (injectable runner). Reuses `runEvalWith`. */
269
+ export declare function measureArmsWith(spec: ArmsMeasureSpec, runner: AgentRunner): Promise<ArmsCheckReport>;
270
+ /** Score checks across arms against the real `claude` CLI. */
271
+ export declare function measureArms(spec: ArmsMeasureSpec): Promise<ArmsCheckReport>;
272
+ /**
273
+ * Welch significance on one check's rate between two arms (`arm` vs `baseline`),
274
+ * by index in `perCheck`. So "the gated arm resolves the skill significantly more
275
+ * than vanilla" is a p-value, not a vibe. Reuses `welchTTest` from stats.ts.
276
+ */
277
+ export declare function compareCheck(report: ArmsCheckReport, baseline: string, arm: string, checkIndex: number): Comparison;
278
+ /** Format a {@link CheckReport}: one line per check with its rate ± se and pass^k. */
279
+ export declare function formatCheckReport(report: CheckReport): string;
280
+ /**
281
+ * The scored gate (Phase 4): throw if any check's measured rate is below `min` —
282
+ * the `measure` counterpart to `assertChecks` (strict). Reads the rate, not a
283
+ * single run, so it never trips on one noisy trial.
284
+ */
285
+ export declare function assertRates(report: CheckReport, opts: {
286
+ min: number;
287
+ }): void;
288
+ /**
289
+ * Serialize a {@link CheckReport} to JUnit XML (Phase 4) — each check a
290
+ * `<testcase>`, failing when its rate is below `min`. Because a check is *data*,
291
+ * this falls out for free: CI test reporters, regression baselines, and a
292
+ * promptfoo bridge all consume the same shape.
293
+ */
294
+ export declare function checkReportToJUnit(report: CheckReport, opts?: {
295
+ min?: number;
296
+ name?: string;
297
+ }): string;
175
298
  /** Parse per-run cost/latency/tokens from a stream — pure, model-free. */
176
299
  export declare function parseUsage(stdout: string): EvalUsage;
177
300
  /** Aggregate per-run metrics: mean for numbers, fraction-true (0..1) for booleans. */
@@ -209,8 +332,19 @@ export declare function formatEvalReport(report: EvalReport): string;
209
332
  * (reuse the bare predicates, e.g. `(t) => skillResolved(t, "x:y")`).
210
333
  */
211
334
  export interface TriggerRateSpec {
212
- /** Plugin dir installed natively (`--plugin-dir`) so its skills/commands activate. */
213
- readonly pluginDir: string;
335
+ /**
336
+ * Plugin dir installed natively (`--plugin-dir`) so its skills/commands
337
+ * activate. Provide this OR {@link skillsDir}, not both.
338
+ */
339
+ readonly pluginDir?: string;
340
+ /**
341
+ * A directory of LOOSE skills (`<skillsDir>/<name>/SKILL.md`, e.g. a repo's
342
+ * `.claude/skills`) to trigger-test directly. vigiles packages them into a
343
+ * throwaway `--plugin-dir` for you and removes it afterward — the one-liner
344
+ * for repo-local skills that aren't a published plugin. Provide this OR
345
+ * {@link pluginDir}, not both.
346
+ */
347
+ readonly skillsDir?: string;
214
348
  /** The varied prompts to test the trigger against. */
215
349
  readonly prompts: readonly string[];
216
350
  /**
@@ -223,6 +357,30 @@ export interface TriggerRateSpec {
223
357
  readonly irrelevantPrompts?: readonly string[];
224
358
  /** Did the behaviour fire on this run? e.g. `(t) => skillResolved(t, "x:y")`. */
225
359
  readonly fired: (trace: Trace) => boolean;
360
+ /**
361
+ * Replace each skill's BODY with a no-op stub (keeping its frontmatter — name +
362
+ * description) before running. Trigger-rate is decided by the frontmatter alone
363
+ * (the model selects a skill before its body loads), so stubbing the body can't
364
+ * change what's measured but stops the run from executing an expensive
365
+ * procedure once the skill fires — far cheaper, faster, side-effect-free. All
366
+ * skills' descriptions stay present, so the selection competition is faithful.
367
+ * Default false (off) for now; recommended `true` for trigger evals. See
368
+ * {@link stubSkillBody}.
369
+ */
370
+ readonly stubSkillBodies?: boolean;
371
+ /**
372
+ * Minimum number of prompts each set (relevant + irrelevant) must have. A
373
+ * handful of prompts can't tell a real recall/precision rate from noise, so
374
+ * the run is rejected before it spends a token. Default 10; lower it
375
+ * deliberately for a genuinely narrow skill.
376
+ */
377
+ readonly minPrompts?: number;
378
+ /**
379
+ * Reject the run when two prompts in a set are closer than this in NCD
380
+ * (gzip-based distance, 0..1; 0 = identical) — near-duplicate prompts inflate
381
+ * a rate without testing varied phrasings. Default 0.3 (the rule-dup threshold).
382
+ */
383
+ readonly minDistance?: number;
226
384
  /** Trials per prompt. Default 1. */
227
385
  readonly trials?: number;
228
386
  /** Model alias. Default "haiku". */
@@ -263,6 +421,63 @@ export interface TriggerRateReport {
263
421
  /** Per-prompt stats for the irrelevant set. Present with irrelevant prompts. */
264
422
  readonly perIrrelevant?: readonly PromptTriggerStat[];
265
423
  }
424
+ /**
425
+ * Package loose `<skillsDir>/<name>/SKILL.md` skills into a throwaway plugin dir
426
+ * that `claude --plugin-dir` accepts — so repo-local skills (e.g. `.claude/skills`)
427
+ * can be trigger-tested without hand-rolling a `plugin.json`. Writes a minimal
428
+ * `.claude-plugin/plugin.json` and copies each `<name>/` (recursively, so
429
+ * `references/` etc. come along) under `skills/<name>/`. Returns the temp plugin
430
+ * dir; the caller removes it (`measureTriggerRate` does). Throws if the directory
431
+ * is missing or holds no `<name>/SKILL.md`.
432
+ */
433
+ export declare function packageSkillsDir(skillsDir: string, opts?: {
434
+ name?: string;
435
+ stub?: boolean;
436
+ }): string;
437
+ /**
438
+ * Rewrite a SKILL.md to keep its YAML frontmatter (the trigger surface — name +
439
+ * description) but replace the body with a no-op stub. Trigger-rate is a property
440
+ * of the frontmatter ONLY: the model picks a skill from its name + description
441
+ * before the body is ever loaded, so the body is causally downstream of selection
442
+ * and irrelevant to whether the skill fires. Stubbing it lets a trigger run stop
443
+ * AT selection instead of executing an expensive multi-step procedure — cheaper,
444
+ * faster, and side-effect-free, without changing what's measured. Pure.
445
+ */
446
+ export declare function stubSkillBody(skillMd: string): string;
447
+ /**
448
+ * Build a throwaway plugin dir mirroring `pluginDir`'s skills with their BODIES
449
+ * stripped (frontmatter kept) — the trigger surface a description/firing check
450
+ * needs, without paying to run each skill's procedure. Keeps the original plugin
451
+ * NAME so `<name>:<skill>` ids still match. The caller removes the returned dir.
452
+ * See {@link stubSkillBody} for why the body is irrelevant to selection.
453
+ */
454
+ export declare function stubbedPluginDir(pluginDir: string): string;
455
+ /**
456
+ * Distance between two prompts in ~0..1 (0 = identical, higher = more
457
+ * different) via Normalized Compression Distance over the normalized text.
458
+ * Reuses {@link ncd} from the proof engine.
459
+ */
460
+ export declare function promptDistance(a: string, b: string): number;
461
+ export interface PromptDiversityIssue {
462
+ readonly kind: "too-few" | "too-similar";
463
+ readonly message: string;
464
+ }
465
+ /**
466
+ * Deterministically check a prompt set is big and varied enough to measure a
467
+ * trigger rate: at least `minPrompts` entries, and no two closer than
468
+ * `minDistance` in NCD. Pure — no model. `label` names the set in messages.
469
+ */
470
+ export declare function checkPromptDiversity(prompts: readonly string[], opts?: {
471
+ minPrompts?: number;
472
+ minDistance?: number;
473
+ label?: string;
474
+ }): PromptDiversityIssue[];
475
+ /** Throw if a prompt set isn't big/varied enough. See {@link checkPromptDiversity}. */
476
+ export declare function assertPromptDiversity(prompts: readonly string[], opts?: {
477
+ minPrompts?: number;
478
+ minDistance?: number;
479
+ label?: string;
480
+ }): void;
266
481
  /**
267
482
  * Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
268
483
  * predicate evaluated per run and aggregated into an overall + per-prompt rate.