vigiles 31.0.0 → 32.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.
@@ -8,6 +8,7 @@
8
8
  * Configurable thresholds in .vigilesrc.json trigger warnings or errors when
9
9
  * coverage drops below the minimum.
10
10
  */
11
+ import { type GlobIgnore } from "./glob-ignore.js";
11
12
  import type { CoverageThresholds } from "./types.js";
12
13
  import type { ClaudeSpec } from "./spec.js";
13
14
  export interface CoverageMetric {
@@ -35,11 +36,11 @@ export declare function readNpmScripts(basePath: string): string[];
35
36
  * Collect commands documented in specs by loading spec source files directly.
36
37
  * Reads the structured `commands` field — no markdown parsing.
37
38
  */
38
- export declare function collectDocumentedCommands(basePath: string, specs: ClaudeSpec[] | undefined, ignore: readonly string[]): Set<string>;
39
+ export declare function collectDocumentedCommands(basePath: string, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): Set<string>;
39
40
  /**
40
41
  * Compute script coverage: what % of npm scripts are documented in specs.
41
42
  */
42
- export declare function computeScriptCoverage(basePath: string, threshold: number | undefined, specs: ClaudeSpec[] | undefined, ignore: readonly string[]): CoverageMetric;
43
+ export declare function computeScriptCoverage(basePath: string, threshold: number | undefined, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): CoverageMetric;
43
44
  /**
44
45
  * Compute linter rule coverage from pre-computed totals.
45
46
  * The actual linter scanning is done by the existing discover() in cli.ts.
@@ -48,7 +49,7 @@ export declare function computeLinterRuleCoverage(enabled: number, documented: n
48
49
  /**
49
50
  * Check all coverage metrics against thresholds.
50
51
  */
51
- export declare function checkCoverage(basePath: string, thresholds: CoverageThresholds, linterEnabled: number, linterDocumented: number, specs: ClaudeSpec[] | undefined, ignore: readonly string[]): CoverageReport;
52
+ export declare function checkCoverage(basePath: string, thresholds: CoverageThresholds, linterEnabled: number, linterDocumented: number, specs: ClaudeSpec[] | undefined, ignore: GlobIgnore): CoverageReport;
52
53
  /**
53
54
  * Format coverage report as human-readable text.
54
55
  */
@@ -20,6 +20,7 @@ const node_fs_1 = require("node:fs");
20
20
  const integrity_js_1 = require("./integrity.js");
21
21
  const node_path_1 = require("node:path");
22
22
  const glob_1 = require("glob");
23
+ const glob_ignore_js_1 = require("./glob-ignore.js");
23
24
  const compile_js_1 = require("./compile.js");
24
25
  // ---------------------------------------------------------------------------
25
26
  // Script coverage
@@ -50,12 +51,12 @@ function collectDocumentedCommands(basePath, specs, ignore) {
50
51
  // Fallback: scan compiled markdown for spec file references, then
51
52
  // load the compiled JS spec from dist/. If that fails, try to
52
53
  // extract commands from the compiled output (last resort).
53
- // `ignore` is the repo's ExcludeSet string face (src/exclude.ts): the floor
54
- // plus `.vigilesrc.json#exclude`, required so this fallback cannot walk a
54
+ // `ignore` is the repo's `ExcludeSet.globIgnore` (src/exclude.ts, correct
55
+ // from any cwd) or patterns relative to `basePath`, required so this fallback cannot walk a
55
56
  // vendored corpus the repo excluded (#192). The CLI always passes `specs`,
56
57
  // so this branch is reached by the library path and tests only.
57
58
  const mdFiles = (0, glob_1.globSync)("**/*.md", {
58
- ignore: [...ignore],
59
+ ignore: (0, glob_ignore_js_1.withIgnored)([], ignore),
59
60
  cwd: basePath,
60
61
  });
61
62
  for (const mdFile of mdFiles) {
@@ -17,6 +17,7 @@
17
17
  * in markdown is explicitly out of scope — use eslint-plugin-markdown or
18
18
  * twoslash for that.
19
19
  */
20
+ import { type GlobIgnore } from "./glob-ignore.js";
20
21
  export type DocRefKind = "enforce" | "file" | "cmd" | "ref";
21
22
  export interface DocRef {
22
23
  readonly file: string;
@@ -40,7 +41,8 @@ export interface DocRefReport {
40
41
  }
41
42
  export interface FindDocRefsOptions {
42
43
  readonly basePath?: string;
43
- readonly ignore?: readonly string[];
44
+ /** The repo exclude (`ExcludeSet.globIgnore`), or patterns relative to `basePath`. */
45
+ readonly ignore?: GlobIgnore;
44
46
  }
45
47
  interface ExtractResult {
46
48
  refs: DocRef[];
@@ -29,6 +29,7 @@ const node_fs_1 = require("node:fs");
29
29
  const typescript_1 = __importDefault(require("typescript"));
30
30
  const node_path_1 = require("node:path");
31
31
  const glob_1 = require("glob");
32
+ const glob_ignore_js_1 = require("./glob-ignore.js");
32
33
  const linters_js_1 = require("./linters.js");
33
34
  const compile_js_1 = require("./compile.js");
34
35
  // ---------------------------------------------------------------------------
@@ -239,7 +240,7 @@ function validateRefs(refs, basePath) {
239
240
  */
240
241
  function findDocRefs(options = {}) {
241
242
  const basePath = options.basePath ?? process.cwd();
242
- const ignore = [...DEFAULT_IGNORE, ...(options.ignore ?? [])];
243
+ const ignore = (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, options.ignore);
243
244
  const files = (0, glob_1.globSync)("**/*.md", { cwd: basePath, ignore });
244
245
  const allRefs = [];
245
246
  let filesIgnored = 0;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The frame of a path — WHICH DIRECTORY it is relative to — named by its TYPE
3
+ * rather than by a comment (#281).
4
+ *
5
+ * 🔴 WHY THIS EXISTS. Every path the CLI handled was a bare `string`, so a path
6
+ * relative to a nested bundle, one relative to the repository and an absolute one
7
+ * were the same type, and nothing stopped a caller from printing the wrong one.
8
+ * `overBundles` handed each check ONE `root: string` that had to be the discovery
9
+ * base, the base of the config's globs AND the frame of every printed path at
10
+ * once. Under `bundles: "all"` that root was the nested bundle, so `include`
11
+ * resolved from the wrong directory and `::warning file=skills/x/SKILL.md` named a
12
+ * file that did not exist from where the command ran — while a root-bundle skill
13
+ * of the same name made the annotation land on the WRONG real file, which looks
14
+ * like success.
15
+ *
16
+ * Three frames, two of them branded:
17
+ * - {@link RepoPath} — relative to {@link Frame.root}, the directory whose
18
+ * `.vigilesrc.json` governs the run. Every printed or annotated path.
19
+ * - {@link BundlePath} — relative to one {@link Bundle}'s own directory. What a
20
+ * per-bundle scan reports.
21
+ * - an absolute path — a plain `string`, the only thing the filesystem takes.
22
+ *
23
+ * The brand is `Opaque` from `ts-essentials` (a `unique symbol` key, so no object
24
+ * literal can forge it), imported as a TYPE ONLY: nothing of it survives the
25
+ * build. It must not reach a public `.d.ts` either — this module is internal and
26
+ * the API report (`api-surface/*.api.md`) is the gate that would show a leak.
27
+ *
28
+ * 🔴 THE ONE CONVERSION POINT. A `RepoPath` is minted here and nowhere else:
29
+ * {@link Frame.repo} from an absolute path, {@link Bundle.path} from a
30
+ * `BundlePath`. A cast to `RepoPath` outside this file is refused by ESLint
31
+ * (`no-restricted-syntax` in `eslint.config.mjs`), so "where did this path get its
32
+ * frame" always has the answer "in frame.ts".
33
+ *
34
+ * WHAT IS UNREPRESENTABLE AND WHAT IS ONLY DETECTABLE. The CLI's output boundary
35
+ * (`ghAnnotate`, the per-bundle check context) takes `RepoPath`, so handing it a
36
+ * bundle-relative or absolute string is a tsc error. Upstream of that, scan-core
37
+ * keeps reporting plain bundle-relative strings, because its browser twin
38
+ * (`scan-files.ts`) shares those shapes and has no filesystem to have a frame
39
+ * over. {@link Bundle.scanned} is the seam where such a string gets its frame;
40
+ * a relative string that is secretly repo-relative still passes it, and that
41
+ * residue is caught by tests, not by the compiler.
42
+ *
43
+ * NODE-FREE: path ops come from `../posix-path.js`, and the caller supplies the
44
+ * working directory, so this module has no process state of its own.
45
+ */
46
+ import type { Opaque } from "ts-essentials";
47
+ /** A `/`-separated path relative to {@link Frame.root}. `.` is the root itself. */
48
+ export type RepoPath = Opaque<string, "RepoPath">;
49
+ /** A `/`-separated path relative to one {@link Bundle}'s own directory. */
50
+ export type BundlePath = Opaque<string, "BundlePath">;
51
+ /** One scored directory: the lint target itself, or a nested plugin bundle. */
52
+ export interface Bundle {
53
+ /** Absolute directory of the bundle. */
54
+ readonly abs: string;
55
+ /** Where the bundle sits, from the frame root (`.` when it IS the root). */
56
+ readonly at: RepoPath;
57
+ /** A path this bundle's scan reported, re-expressed from the frame root. */
58
+ path(p: BundlePath): RepoPath;
59
+ /**
60
+ * The seam for scan-core's plain strings, which are bundle-relative by
61
+ * contract (see the header). An ABSOLUTE string is accepted too and converted
62
+ * directly — `hook-block-ineffective` reports its script that way.
63
+ */
64
+ scanned(p: string): RepoPath;
65
+ }
66
+ /** The single owner of "relative to which directory", built once per command. */
67
+ export interface Frame {
68
+ /** Absolute directory every {@link RepoPath} is relative to. */
69
+ readonly root: string;
70
+ /** The one conversion from an absolute path. Throws on a relative input. */
71
+ repo(abs: string): RepoPath;
72
+ /** Describe the bundle at an absolute directory, in this frame. */
73
+ bundle(abs: string): Bundle;
74
+ }
75
+ /**
76
+ * The frame for a command run from `cwd` against `target` (absolute).
77
+ *
78
+ * The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
79
+ * walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
80
+ * target (`vigiles lint ../other`) is somebody else's repository, and its paths
81
+ * mean something from ITS root; relative to `cwd` they would all start with
82
+ * `../`, and a foreign lint must never let the caller's own files satisfy the
83
+ * target's `sharedDirs` references. That exception was `sharedDirsRootFor` in
84
+ * the CLI, applied to `sharedDirs` alone; it is owned here now, so `include`,
85
+ * `exclude`, `sharedDirs` and every printed path agree on one root.
86
+ */
87
+ export declare function frameFor(cwd: string, target: string): Frame;
88
+ /** The frame rooted at an absolute directory. */
89
+ export declare function frameAt(root: string): Frame;
90
+ //# sourceMappingURL=frame.d.ts.map
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.frameFor = frameFor;
4
+ exports.frameAt = frameAt;
5
+ const posix_path_js_1 = require("../posix-path.js");
6
+ /**
7
+ * Node on Windows hands out `C:\\repo`, and `../posix-path.js` recognises only a leading `/`
8
+ * as absolute. The frame works in `/`-separated form, so every path entering it passes here:
9
+ * backslashes become `/`, and a drive-letter path (`C:/…`) counts as absolute.
10
+ */
11
+ function slashed(p) {
12
+ return p.replaceAll("\\", "/");
13
+ }
14
+ function isAbs(p) {
15
+ return (0, posix_path_js_1.isAbsolute)(p) || /^[A-Za-z]:\//.test(p);
16
+ }
17
+ /** `rel` is `abs`'s position below `root` when it does not climb out. */
18
+ function isBelow(rel) {
19
+ return rel === "" || (rel !== ".." && !rel.startsWith("../"));
20
+ }
21
+ /**
22
+ * The frame for a command run from `cwd` against `target` (absolute).
23
+ *
24
+ * The root is `cwd` — where `loadConfig()` read `.vigilesrc.json` (it does not
25
+ * walk up; measured 2026-09-23) — UNLESS the target lies outside it. A foreign
26
+ * target (`vigiles lint ../other`) is somebody else's repository, and its paths
27
+ * mean something from ITS root; relative to `cwd` they would all start with
28
+ * `../`, and a foreign lint must never let the caller's own files satisfy the
29
+ * target's `sharedDirs` references. That exception was `sharedDirsRootFor` in
30
+ * the CLI, applied to `sharedDirs` alone; it is owned here now, so `include`,
31
+ * `exclude`, `sharedDirs` and every printed path agree on one root.
32
+ */
33
+ function frameFor(cwd, target) {
34
+ return frameAt(isBelow((0, posix_path_js_1.relative)(slashed(cwd), slashed(target))) ? cwd : target);
35
+ }
36
+ /** The frame rooted at an absolute directory. */
37
+ function frameAt(root) {
38
+ const base = (0, posix_path_js_1.normalize)(slashed(root));
39
+ const repo = (raw) => {
40
+ const abs = slashed(raw);
41
+ if (!isAbs(abs))
42
+ throw new Error(`frame.repo() takes an absolute path, got "${raw}" — a relative string has no frame to convert from`);
43
+ return ((0, posix_path_js_1.relative)(base, abs) || ".");
44
+ };
45
+ const bundle = (raw) => {
46
+ const dir = (0, posix_path_js_1.normalize)(slashed(raw));
47
+ const path = (p) => repo((0, posix_path_js_1.join)(dir, p));
48
+ return {
49
+ abs: dir,
50
+ at: repo(dir),
51
+ path,
52
+ // The ONE place a plain string is taken to be bundle-relative.
53
+ scanned: (p) => (isAbs(slashed(p)) ? repo(p) : path(p)),
54
+ };
55
+ };
56
+ return { root: base, repo, bundle };
57
+ }
58
+ //# sourceMappingURL=frame.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One `ignore` for `globSync` out of a detector's own string floor plus the
3
+ * repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
4
+ * never both, so a detector that has both needs this union.
5
+ *
6
+ * 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
7
+ * `ExcludeSet` used to hand detectors a string list whose patterns were relative
8
+ * to the repository root, with the precondition "only correct for a glob rooted
9
+ * AT that root" written in a comment. Two callers globbed from a nested bundle
10
+ * and broke it silently: a root-relative `skills/lonely` dropped
11
+ * `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
12
+ * `ExcludeSet.globIgnore` computes each candidate's position from the repo root
13
+ * itself, so it is correct from ANY glob `cwd`; the string face was retired.
14
+ *
15
+ * A plain string list is still accepted — for a direct library caller whose
16
+ * patterns are, by that caller's own contract, relative to the `cwd` it globs
17
+ * from. That is the one frame a string can safely carry.
18
+ */
19
+ import { type IgnoreLike } from "glob";
20
+ /** What a detector takes for "also skip these". */
21
+ export type GlobIgnore = readonly string[] | IgnoreLike;
22
+ /**
23
+ * `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
24
+ * `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
25
+ * lists, so a string-only caller sees exactly the glob behaviour it always had.
26
+ */
27
+ export declare function withIgnored(floor: readonly string[], extra: GlobIgnore | undefined): string[] | IgnoreLike;
28
+ //# sourceMappingURL=glob-ignore.d.ts.map
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withIgnored = withIgnored;
4
+ /**
5
+ * One `ignore` for `globSync` out of a detector's own string floor plus the
6
+ * repository's exclude — glob takes EITHER a pattern list OR one `IgnoreLike`,
7
+ * never both, so a detector that has both needs this union.
8
+ *
9
+ * 🔴 WHY THE REPO EXCLUDE ARRIVES AS AN `IgnoreLike` AND NOT AS STRINGS (#281).
10
+ * `ExcludeSet` used to hand detectors a string list whose patterns were relative
11
+ * to the repository root, with the precondition "only correct for a glob rooted
12
+ * AT that root" written in a comment. Two callers globbed from a nested bundle
13
+ * and broke it silently: a root-relative `skills/lonely` dropped
14
+ * `plugins/p/skills/lonely`, and the real exclusion never reached the bundle.
15
+ * `ExcludeSet.globIgnore` computes each candidate's position from the repo root
16
+ * itself, so it is correct from ANY glob `cwd`; the string face was retired.
17
+ *
18
+ * A plain string list is still accepted — for a direct library caller whose
19
+ * patterns are, by that caller's own contract, relative to the `cwd` it globs
20
+ * from. That is the one frame a string can safely carry.
21
+ */
22
+ const glob_1 = require("glob");
23
+ function isPatternList(x) {
24
+ return Array.isArray(x);
25
+ }
26
+ /**
27
+ * `floor` (patterns relative to the glob's `cwd`) plus `extra`, in the form
28
+ * `globSync`'s `ignore` option accepts. Stays a plain list when both halves are
29
+ * lists, so a string-only caller sees exactly the glob behaviour it always had.
30
+ */
31
+ function withIgnored(floor, extra) {
32
+ if (extra === undefined)
33
+ return [...floor];
34
+ if (isPatternList(extra))
35
+ return [...floor, ...extra];
36
+ const own = new glob_1.Ignore([...floor], {});
37
+ return {
38
+ ignored: (p) => own.ignored(p) || extra.ignored?.(p) === true,
39
+ childrenIgnored: (p) => own.childrenIgnored(p) || extra.childrenIgnored?.(p) === true,
40
+ };
41
+ }
42
+ //# sourceMappingURL=glob-ignore.js.map
@@ -11,6 +11,7 @@
11
11
  * references (markdown links and backtick paths). Works against source
12
12
  * README plus compiled CLAUDE.md — no spec loading required.
13
13
  */
14
+ import { type GlobIgnore } from "./glob-ignore.js";
14
15
  import type { PluginLayout } from "./layout.js";
15
16
  export interface OrphanReport {
16
17
  /** Include globs that were scanned. */
@@ -35,12 +36,13 @@ export interface FindOrphansOptions {
35
36
  /** Glob patterns to exclude within the include scope (orphan CANDIDACY only). */
36
37
  readonly exclude?: readonly string[];
37
38
  /**
38
- * The repo-wide `.vigilesrc.json#exclude` (the ExcludeSet string face,
39
- * src/exclude.ts). Applied to BOTH walks — candidates AND the reference scan —
39
+ * The repo-wide `.vigilesrc.json#exclude` (`ExcludeSet.globIgnore`,
40
+ * src/exclude.ts — correct from any glob cwd; a plain list is relative to
41
+ * `basePath`). Applied to BOTH walks — candidates AND the reference scan —
40
42
  * so an excluded corpus can neither be an orphan nor keep one alive (#192).
41
43
  * The CLI always passes it; a direct library caller may omit it.
42
44
  */
43
- readonly repoExclude?: readonly string[];
45
+ readonly repoExclude?: GlobIgnore;
44
46
  /**
45
47
  * Harnesses whose surface files (instruction file, `SKILL.md`, subagents,
46
48
  * commands) are load-bearing by location and thus never orphan CANDIDATES
@@ -18,6 +18,7 @@ exports.formatOrphanReport = formatOrphanReport;
18
18
  const node_fs_1 = require("node:fs");
19
19
  const node_path_1 = require("node:path");
20
20
  const glob_1 = require("glob");
21
+ const glob_ignore_js_1 = require("./glob-ignore.js");
21
22
  // ---------------------------------------------------------------------------
22
23
  // Internals
23
24
  // ---------------------------------------------------------------------------
@@ -100,7 +101,7 @@ function isOrphanExempt(absPath) {
100
101
  function collectDocs(basePath, include, ignore, layouts) {
101
102
  const docs = new Set();
102
103
  for (const pattern of include) {
103
- for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore: [...ignore] })) {
104
+ for (const p of (0, glob_1.globSync)(pattern, { cwd: basePath, ignore })) {
104
105
  if (isHarnessLoadedFile(p, layouts))
105
106
  continue; // harness files are never orphans
106
107
  if (isOrphanExempt((0, node_path_1.resolve)(basePath, p)))
@@ -160,13 +161,13 @@ function findOrphanDocs(options = {}) {
160
161
  // CANDIDATE nor a SOURCE of references (a link from inside it must not keep a
161
162
  // doc alive). The rule's own `exclude` only narrows candidacy: a doc kept out
162
163
  // of the orphan list can still reference others. Union, never override.
163
- const repoExclude = options.repoExclude ?? [];
164
- const ignore = [...DEFAULT_IGNORE, ...repoExclude, ...userExclude];
164
+ const repoExclude = options.repoExclude;
165
+ const ignore = (0, glob_ignore_js_1.withIgnored)([...DEFAULT_IGNORE, ...userExclude], repoExclude);
165
166
  const layouts = options.layouts ?? [];
166
167
  const allDocs = collectDocs(basePath, include, ignore, layouts);
167
168
  const allMarkdown = (0, glob_1.globSync)("**/*.md", {
168
169
  cwd: basePath,
169
- ignore: [...DEFAULT_IGNORE, ...repoExclude],
170
+ ignore: (0, glob_ignore_js_1.withIgnored)(DEFAULT_IGNORE, repoExclude),
170
171
  });
171
172
  const referencedBy = new Map();
172
173
  for (const mdPath of allMarkdown) {
package/dist/eval.d.ts CHANGED
@@ -25,9 +25,21 @@ export interface EvalArm {
25
25
  * arm, so its skills/commands/agents activate the real way — the real model
26
26
  * can trigger a skill by its description (vs. `plugin`, which materializes a
27
27
  * file subset that does not register skills). Point at a COMPLETE plugin. Lets
28
- * an arm be "skill installed" vs "off" to measure real activation.
28
+ * an arm be "skill installed" vs "off" to measure real activation. Provide this
29
+ * OR {@link skillsDir}, not both.
29
30
  */
30
31
  readonly pluginDir?: string;
32
+ /**
33
+ * A directory of LOOSE skills (`<skillsDir>/<name>/SKILL.md`, e.g. a repo's
34
+ * `.claude/skills`) to install for this arm. vigiles packages them into a
35
+ * throwaway `--plugin-dir` (manifest + `skills/<name>/`, each skill dir copied
36
+ * whole) for the run and removes it afterward — the one-liner for repo-local
37
+ * skills that aren't a published plugin. The skills install under the
38
+ * namespace `vigiles-loose-skills`, so a `skill()` check / `skillResolved`
39
+ * matches `vigiles-loose-skills:<name>` (the report's `namespace` says so).
40
+ * Provide this OR {@link pluginDir}, not both.
41
+ */
42
+ readonly skillsDir?: string;
31
43
  /**
32
44
  * Tools to intercept for this arm (the tool-call spy). Each
33
45
  * {@link ToolIntercept} is denied its real execution by an auto-wired PreToolUse
@@ -83,6 +95,15 @@ export interface EvalSpec<M extends Metrics> {
83
95
  readonly fixture?: Record<string, string>;
84
96
  /** The arms to compare, by name. */
85
97
  readonly arms: Record<string, EvalArm>;
98
+ /**
99
+ * Stub each arm's skill BODIES (frontmatter kept) before the run — for firing
100
+ * comparisons (does the skill get SELECTED?), where a selected skill should
101
+ * stop at selection instead of running its (often expensive) procedure. Every
102
+ * arm with a `pluginDir` / `skillsDir` is repackaged with bodies stripped; arms
103
+ * without one are untouched. Don't combine with quality metrics: the body is
104
+ * gone, so there is nothing to grade. See {@link stubSkillBody}.
105
+ */
106
+ readonly stubSkillBodies?: boolean;
86
107
  /** The task prompt given to the agent. */
87
108
  readonly task: string;
88
109
  /** Compute this run's metrics from its outcome. */
@@ -328,15 +349,24 @@ export interface MeasureSpec {
328
349
  readonly settings?: unknown;
329
350
  /** A real plugin/repo to load (materialized) — see `EvalArm.plugin`. */
330
351
  readonly plugin?: string;
331
- /** A complete plugin dir to install natively (`--plugin-dir`) so skills activate. */
352
+ /**
353
+ * A complete plugin dir to install natively (`--plugin-dir`) so skills activate
354
+ * — see {@link EvalArm.pluginDir}. Provide this OR `skillsDir`, not both.
355
+ */
332
356
  readonly pluginDir?: string;
333
357
  /**
334
- * Stub each skill BODY in `pluginDir` (frontmatter/trigger surface kept) before
335
- * the run — for checks about whether a skill FIRES (`skill()`), not what it
336
- * produces. A selected skill stops at selection instead of running its (often
337
- * expensive) procedure, so a description/firing run costs a fraction of the
338
- * tokens. Do NOT combine with `judged`/quality checks: the body is gone, so
339
- * there's nothing to grade. Requires `pluginDir`. See {@link stubSkillBody}.
358
+ * A loose `<skillsDir>/<name>/SKILL.md` directory (e.g. a repo's `.claude/skills`)
359
+ * to install, auto-packaged into a throwaway plugin — see {@link EvalArm.skillsDir}.
360
+ * Installs under `vigiles-loose-skills`, so `skill("vigiles-loose-skills:<name>")`.
361
+ */
362
+ readonly skillsDir?: string;
363
+ /**
364
+ * Stub each skill BODY (frontmatter/trigger surface kept) before the run — for
365
+ * checks about whether a skill FIRES (`skill()`), not what it produces. A
366
+ * selected skill stops at selection instead of running its (often expensive)
367
+ * procedure, so a description/firing run costs a fraction of the tokens. Do NOT
368
+ * combine with `judged`/quality checks: the body is gone, so there's nothing to
369
+ * grade. Requires `pluginDir` or `skillsDir`. See {@link stubSkillBody}.
340
370
  */
341
371
  readonly stubSkillBodies?: boolean;
342
372
  /** Tools to intercept (the tool-call spy) — see {@link EvalArm.interceptTools}. */
@@ -385,6 +415,15 @@ export interface CheckReport {
385
415
  readonly perCheck: readonly CheckRate[];
386
416
  /** Cost / latency / token totals for the run (the same source as `runEval`). */
387
417
  readonly usage: ArmUsage;
418
+ /**
419
+ * The plugin namespace the skills actually installed under — the `<plugin>`
420
+ * half of the `<plugin>:<skill>` id a `skill()` check matches. Undefined when
421
+ * the run installed nothing (no `pluginDir` / `skillsDir`). Reported because
422
+ * with `skillsDir` the name is chosen by the packager, not the caller, so the
423
+ * most common cause of a 0% `skill()` rate was a value the caller never saw.
424
+ * Mirrors {@link TriggerRateReport.namespace}.
425
+ */
426
+ readonly namespace?: string;
388
427
  }
389
428
  /**
390
429
  * Score a check vocabulary across trials — the scored counterpart to
@@ -407,8 +446,8 @@ export interface ArmsMeasureSpec {
407
446
  * Stub each arm's skill BODIES (frontmatter kept) before the run — the A/B
408
447
  * counterpart to {@link MeasureSpec.stubSkillBodies}. For firing comparisons
409
448
  * (does description variant A fire more than B?), every arm that sets
410
- * `pluginDir` is repackaged with bodies stripped so each run stops at
411
- * selection — a fraction of the tokens. Arms without a `pluginDir` are left
449
+ * `pluginDir` / `skillsDir` is repackaged with bodies stripped so each run
450
+ * stops at selection — a fraction of the tokens. Arms without one are left
412
451
  * untouched. Don't combine with `judged`/quality checks. See {@link stubSkillBody}.
413
452
  */
414
453
  readonly stubSkillBodies?: boolean;
@@ -593,7 +632,7 @@ export declare function seedEphemeralHome(throwawayHome: string, realHome: strin
593
632
  export declare function isRateLimited(out: RunOut): boolean;
594
633
  /** Map `worker` over `items` with at most `concurrency` in flight, order preserved. */
595
634
  export declare function runPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T) => Promise<R>): Promise<R[]>;
596
- export declare function runEvalWith<M extends Metrics>(spec: EvalSpec<M>, runner: AgentRunner): Promise<EvalReport>;
635
+ export declare function runEvalWith<M extends Metrics>(input: EvalSpec<M>, runner: AgentRunner): Promise<EvalReport>;
597
636
  /** Format an eval report as a compact table for the console (mean ± se, pass^k). */
598
637
  export declare function formatEvalReport(report: EvalReport): string;
599
638
  /**