vigiles 5.0.1 → 5.2.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 +15 -9
- package/dist/adapters/claude-code/adapter.js +1 -0
- package/dist/adapters/claude-code/agent-runtime.d.ts +30 -6
- package/dist/adapters/claude-code/agent-runtime.js +66 -37
- package/dist/adapters/claude-code/dialect.js +37 -2
- package/dist/adapters/claude-code/effect-region.d.ts +9 -0
- package/dist/adapters/claude-code/effect-region.js +45 -0
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/skill-runtime.d.ts +25 -0
- package/dist/adapters/claude-code/skill-runtime.js +48 -0
- package/dist/adapters/codex/adapter.js +3 -0
- package/dist/adapters/codex/eval.d.ts +94 -0
- package/dist/adapters/codex/eval.js +227 -0
- package/dist/adapters/codex/layout.js +3 -0
- package/dist/adapters/opencode/adapter.js +1 -0
- package/dist/adapters/opencode/layout.js +3 -0
- package/dist/check.d.ts +8 -0
- package/dist/check.js +27 -3
- package/dist/cli.js +712 -21
- package/dist/codex.d.ts +1 -0
- package/dist/codex.js +3 -0
- package/dist/core/adapter.d.ts +10 -0
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +405 -0
- package/dist/core/compile.d.ts +3 -1
- package/dist/core/compile.js +169 -74
- package/dist/core/description-overlap.d.ts +27 -0
- package/dist/core/description-overlap.js +53 -0
- package/dist/core/dialect.d.ts +18 -0
- package/dist/core/effects.d.ts +172 -0
- package/dist/core/effects.js +245 -0
- package/dist/core/frontmatter-read.d.ts +25 -0
- package/dist/core/frontmatter-read.js +138 -0
- package/dist/core/hook-events.d.ts +34 -0
- package/dist/core/hook-events.js +48 -0
- package/dist/core/layout.d.ts +6 -0
- package/dist/core/mcp-config.d.ts +20 -0
- package/dist/core/mcp-config.js +40 -0
- package/dist/core/mcp-hook.d.ts +35 -0
- package/dist/core/mcp-hook.js +70 -0
- package/dist/core/mcp-tool.d.ts +50 -0
- package/dist/core/mcp-tool.js +61 -0
- package/dist/core/orphans.js +21 -0
- package/dist/core/spec.d.ts +142 -3
- package/dist/core/spec.js +48 -0
- package/dist/core/tool-contract.d.ts +68 -0
- package/dist/core/tool-contract.js +113 -0
- package/dist/core/types.d.ts +91 -2
- package/dist/core/validate.js +23 -1
- package/dist/eval.d.ts +69 -13
- package/dist/eval.js +106 -51
- package/dist/harness-test.d.ts +7 -0
- package/dist/harness-test.js +19 -7
- package/dist/leaderboard.d.ts +2 -0
- package/dist/leaderboard.js +63 -3
- package/dist/optimize.d.ts +74 -0
- package/dist/optimize.js +94 -0
- package/dist/plugin-loader.d.ts +1 -0
- package/dist/plugin-loader.js +71 -18
- package/dist/scaffold-test.d.ts +30 -0
- package/dist/scaffold-test.js +158 -0
- package/dist/scan-behavioral.d.ts +73 -0
- package/dist/scan-behavioral.js +150 -0
- package/dist/scan.d.ts +166 -1
- package/dist/scan.js +622 -55
- package/dist/score-explainer.d.ts +69 -0
- package/dist/score-explainer.js +169 -0
- package/dist/test-coverage.d.ts +7 -0
- package/dist/test-coverage.js +39 -24
- package/package.json +2 -1
- package/skills/{migrate-to-spec → adopt-spec}/SKILL.md +4 -6
- package/skills/edit-spec/SKILL.md +1 -1
package/dist/core/types.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface OrphansConfig {
|
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Shared options for the per-kind untested-* rules (`untested-skill` /
|
|
74
|
-
* `untested-
|
|
74
|
+
* `untested-subagent` / `untested-hook`). Which kinds are scanned is controlled by
|
|
75
75
|
* each rule's severity (set a rule to `false` to skip that kind), so only the
|
|
76
76
|
* test-discovery knobs live here.
|
|
77
77
|
*/
|
|
@@ -97,7 +97,7 @@ export interface RulesConfig {
|
|
|
97
97
|
/** Flag a skill (SKILL.md) that ships with no test or eval. Default: "warn". */
|
|
98
98
|
"untested-skill"?: RuleWithOptions<TestCoverageConfig>;
|
|
99
99
|
/** Flag a subagent (agents/*.md) that ships with no test or eval. Default: "warn". */
|
|
100
|
-
"untested-
|
|
100
|
+
"untested-subagent"?: RuleWithOptions<TestCoverageConfig>;
|
|
101
101
|
/** Flag a hook script that ships with no test or eval. Default: "warn". */
|
|
102
102
|
"untested-hook"?: RuleWithOptions<TestCoverageConfig>;
|
|
103
103
|
/**
|
|
@@ -108,6 +108,95 @@ export interface RulesConfig {
|
|
|
108
108
|
* block the edit, false → off.
|
|
109
109
|
*/
|
|
110
110
|
"unmarked-refs"?: RuleSeverity;
|
|
111
|
+
/**
|
|
112
|
+
* Cross-reference each subagent's `tools:` rail against the harness tool
|
|
113
|
+
* catalog — flag a never-available tool or a close typo (the moat). Only
|
|
114
|
+
* high-confidence issues are reported (a bare unrecognized tool is likely
|
|
115
|
+
* plugin/MCP-provided, never flagged). Off unless set; "warn" surfaces,
|
|
116
|
+
* "error" gates CI. Same detector as `scan` + `compileAgent`.
|
|
117
|
+
*/
|
|
118
|
+
"subagent-tool-contract"?: RuleSeverity;
|
|
119
|
+
/**
|
|
120
|
+
* Flag a hook registered under an event name the harness doesn't define (a
|
|
121
|
+
* typo → the hook never fires). High-precision: close typos only, never a
|
|
122
|
+
* framework/custom event. Default "warn"; "error" gates CI. Same detector as
|
|
123
|
+
* `scan`.
|
|
124
|
+
*/
|
|
125
|
+
"hook-events"?: RuleSeverity;
|
|
126
|
+
/**
|
|
127
|
+
* Flag a skill/agent missing a required frontmatter field — a skill needs
|
|
128
|
+
* `name` (to load), an agent needs `name` + `description`. A broken surface
|
|
129
|
+
* that won't register. Default "warn"; "error" gates CI. Same detector as `scan`.
|
|
130
|
+
*/
|
|
131
|
+
"subagent-frontmatter"?: RuleSeverity;
|
|
132
|
+
/**
|
|
133
|
+
* Flag a declared MCP server that can't start — neither a `command` (stdio)
|
|
134
|
+
* nor a `url` (http/sse). Default "warn"; "error" gates CI. Same detector as
|
|
135
|
+
* `scan`. (JSON `.mcp.json`/manifest `mcpServers`; Codex TOML not yet parsed.)
|
|
136
|
+
*/
|
|
137
|
+
"mcp-config"?: RuleSeverity;
|
|
138
|
+
/**
|
|
139
|
+
* RECOMMEND (not require) that a SKILL.md declares an explicit `name` +
|
|
140
|
+
* `description` rather than relying on the dir-name / first-paragraph
|
|
141
|
+
* fallbacks — a more reliable trigger surface. The skill still loads without
|
|
142
|
+
* them, so this is a best-practice nudge: default "warn"; set "error" to
|
|
143
|
+
* enforce on your own skills. Same detector as `scan` (skillMetaIssues).
|
|
144
|
+
*/
|
|
145
|
+
"skill-frontmatter"?: RuleSeverity;
|
|
146
|
+
/**
|
|
147
|
+
* Cross-reference an `mcp__server__tool` in a subagent's contract against the
|
|
148
|
+
* plugin's declared `mcpServers` — flag a server the plugin doesn't declare
|
|
149
|
+
* (the MCP half of the tool moat; `subagent-tool-contract` checks the built-in
|
|
150
|
+
* half). High-precision: only flags when the plugin SHIPS a declared set,
|
|
151
|
+
* allowlists harness built-ins (`ide`), and skips the plugin-namespaced
|
|
152
|
+
* `mcp__plugin_…` form. Default "warn"; "error" gates CI. Same detector as
|
|
153
|
+
* `scan` (mcpToolIssues).
|
|
154
|
+
*/
|
|
155
|
+
"mcp-tool-resolves"?: RuleSeverity;
|
|
156
|
+
/**
|
|
157
|
+
* Flag a hook command that references a script file which doesn't exist on
|
|
158
|
+
* disk (with `${CLAUDE_PLUGIN_ROOT}` resolved) — the hook silently never runs.
|
|
159
|
+
* FP-safe: skips unresolved `$VAR` paths, existence-guarded one-liners, and
|
|
160
|
+
* inline commands. Matches Anthropic's own `claude plugin validate`. Default
|
|
161
|
+
* "warn"; "error" gates CI. Same detector as `scan` (hooks status "missing").
|
|
162
|
+
*/
|
|
163
|
+
"hook-script-exists"?: RuleSeverity;
|
|
164
|
+
/**
|
|
165
|
+
* Cross-reference a subagent's `disallowedTools:` block-list against the
|
|
166
|
+
* catalog — the deny-side mirror of `subagent-tool-contract`. A close typo there
|
|
167
|
+
* blocks NOTHING (you meant to deny `Bash`, wrote `Bsh`), leaving the tool
|
|
168
|
+
* available. High-precision: close-typo only (a never-available tool is
|
|
169
|
+
* harmless to list, a bare unknown is likely a plugin tool). Default "warn";
|
|
170
|
+
* "error" gates CI. Same detector as `scan` (disallowedToolIssues).
|
|
171
|
+
*/
|
|
172
|
+
"disallowed-tools-contract"?: RuleSeverity;
|
|
173
|
+
/**
|
|
174
|
+
* Flag two model-invocable skills whose descriptions are near-identical — the
|
|
175
|
+
* selector can't tell them apart, so the wrong one fires (a precision
|
|
176
|
+
* collision). A DETERMINISTIC NCD proxy for a `--trigger`-class behavioral bug;
|
|
177
|
+
* calibrated FP-safe (only basically-identical text, below the sweep's
|
|
178
|
+
* most-similar distinct pair). Default "warn"; "error" gates CI. Same detector
|
|
179
|
+
* as `scan` (descriptionOverlaps).
|
|
180
|
+
*/
|
|
181
|
+
"description-overlap"?: RuleSeverity;
|
|
182
|
+
/**
|
|
183
|
+
* Flag a skill/agent whose `---` frontmatter block EXISTS but isn't valid YAML
|
|
184
|
+
* — fields may not parse as intended. CAVEAT: a real YAML parser (js-yaml) is
|
|
185
|
+
* stricter than some loaders, so a one-line `description:` containing a `: `
|
|
186
|
+
* colon or an `<example>` block is flagged even though it may still load.
|
|
187
|
+
* Hence default "warn" (a nudge), not "error" — verify before enforcing. Same
|
|
188
|
+
* detector as `scan` (malformedFrontmatter).
|
|
189
|
+
*/
|
|
190
|
+
"frontmatter-valid"?: RuleSeverity;
|
|
191
|
+
/**
|
|
192
|
+
* Flag a `type: "mcp_tool"` hook action that's incomplete (missing `server` /
|
|
193
|
+
* `tool`) or targets a server the plugin doesn't declare in `mcpServers` — the
|
|
194
|
+
* hook silently never dispatches. High-precision: the undeclared-server half is
|
|
195
|
+
* gated on the plugin shipping a declared set and allowlists built-ins (`ide`),
|
|
196
|
+
* mirroring `mcp-tool-resolves`. Default "warn"; "error" gates CI. Same detector
|
|
197
|
+
* as `scan` (mcpHookIssues).
|
|
198
|
+
*/
|
|
199
|
+
"mcp-hook-target-resolves"?: RuleSeverity;
|
|
111
200
|
}
|
|
112
201
|
/** Extract severity from a rule value (handles both simple and tuple forms). */
|
|
113
202
|
export declare function ruleSeverity<T>(rule: RuleWithOptions<T> | undefined): RuleSeverity;
|
package/dist/core/validate.js
CHANGED
|
@@ -43,9 +43,31 @@ const DEFAULT_RULES = {
|
|
|
43
43
|
coverage: false,
|
|
44
44
|
// Per-kind surface-coverage: a skill/agent/hook must ship with a test or eval.
|
|
45
45
|
"untested-skill": "warn",
|
|
46
|
-
"untested-
|
|
46
|
+
"untested-subagent": "warn",
|
|
47
47
|
"untested-hook": "warn",
|
|
48
48
|
"unmarked-refs": "warn",
|
|
49
|
+
// High-precision (never-available + close typos only), so on by default at warn.
|
|
50
|
+
"subagent-tool-contract": "warn",
|
|
51
|
+
// High-precision (close typos only), on by default at warn.
|
|
52
|
+
"hook-events": "warn",
|
|
53
|
+
// Missing required frontmatter (name/description) — on by default at warn.
|
|
54
|
+
"subagent-frontmatter": "warn",
|
|
55
|
+
// A declared MCP server with no command/url can't start — on by default at warn.
|
|
56
|
+
"mcp-config": "warn",
|
|
57
|
+
// Best-practice nudge (skills load without frontmatter) — warn, not error.
|
|
58
|
+
"skill-frontmatter": "warn",
|
|
59
|
+
// High-precision (gated on a declared MCP set; built-ins allowlisted) — warn.
|
|
60
|
+
"mcp-tool-resolves": "warn",
|
|
61
|
+
// A hook script referenced but missing never runs — on by default at warn.
|
|
62
|
+
"hook-script-exists": "warn",
|
|
63
|
+
// High-precision (close-typo only) deny-list mirror of subagent-tool-contract.
|
|
64
|
+
"disallowed-tools-contract": "warn",
|
|
65
|
+
// Deterministic NCD precision proxy (near-identical skill descriptions) — warn.
|
|
66
|
+
"description-overlap": "warn",
|
|
67
|
+
// Malformed-YAML frontmatter — WARN only (js-yaml is stricter than some loaders).
|
|
68
|
+
"frontmatter-valid": "warn",
|
|
69
|
+
// A mcp_tool hook incomplete / targeting an undeclared server — on by default at warn.
|
|
70
|
+
"mcp-hook-target-resolves": "warn",
|
|
49
71
|
};
|
|
50
72
|
const DEFAULT_CONFIG = {
|
|
51
73
|
ruleMarkers: ["headings", "checkboxes"],
|
package/dist/eval.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ToolCall, type Trace } from "./harness-test.js";
|
|
1
|
+
import { parseToolCalls, parseHooks, parseSubagents, type ToolCall, type Trace } from "./harness-test.js";
|
|
2
2
|
import { type CacheMode } from "./eval-cache.js";
|
|
3
3
|
import type { Check, CheckJSON } from "./check.js";
|
|
4
4
|
import { type Comparison } from "./stats.js";
|
|
@@ -250,6 +250,9 @@ export type AgentRunner = (args: AgentRunArgs) => Promise<RunOut>;
|
|
|
250
250
|
* leak the host environment into an untrusted, model-driven run.
|
|
251
251
|
*/
|
|
252
252
|
export declare function resolveSpawnEnv(a: Pick<AgentRunArgs, "env" | "replaceEnv">, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
253
|
+
/** The real `claude`-spawning runner (composition root). Exported so other
|
|
254
|
+
* real-model entries (e.g. `scan --trigger`) bind the same runner. */
|
|
255
|
+
export declare function spawnAgent(a: AgentRunArgs): Promise<RunOut>;
|
|
253
256
|
/**
|
|
254
257
|
* Run the eval: every arm × every trial against the real `claude` CLI, with the
|
|
255
258
|
* metric computed per run and aggregated per arm. Requires `claude` on PATH and
|
|
@@ -392,6 +395,24 @@ export declare function checkReportToJUnit(report: CheckReport, opts?: {
|
|
|
392
395
|
}): string;
|
|
393
396
|
/** Parse per-run cost/latency/tokens from a stream — pure, model-free. */
|
|
394
397
|
export declare function parseUsage(stdout: string): EvalUsage;
|
|
398
|
+
/**
|
|
399
|
+
* The harness-specific half of a run trace: how a real model's raw stdout maps
|
|
400
|
+
* to the common fields. Claude Code's `parseClaudeRun` reads its stream-json; a
|
|
401
|
+
* second harness (Codex) supplies its own parser of `codex exec --json` JSONL, so
|
|
402
|
+
* the eval tier (`measureTriggerRate`/`runEval`) isn't bound to Claude's format.
|
|
403
|
+
* The non-harness fields (cwd/exitCode/stdout/file/sh) stay in `makeContext`.
|
|
404
|
+
*/
|
|
405
|
+
export interface ParsedModelRun {
|
|
406
|
+
readonly turns: number;
|
|
407
|
+
readonly output: string;
|
|
408
|
+
readonly toolCalls: ReturnType<typeof parseToolCalls>;
|
|
409
|
+
readonly hooks: ReturnType<typeof parseHooks>;
|
|
410
|
+
readonly subagents: ReturnType<typeof parseSubagents>;
|
|
411
|
+
readonly usage: EvalUsage;
|
|
412
|
+
}
|
|
413
|
+
export type ModelOutputParser = (out: RunOut) => ParsedModelRun;
|
|
414
|
+
/** Parse Claude Code's stream-json stdout into the common trace fields. */
|
|
415
|
+
export declare function parseClaudeRun(out: RunOut): ParsedModelRun;
|
|
395
416
|
/** Aggregate per-run metrics: mean for numbers, fraction-true (0..1) for booleans. */
|
|
396
417
|
export declare function aggregate(rows: readonly Metrics[]): Record<string, number>;
|
|
397
418
|
/**
|
|
@@ -593,6 +614,22 @@ export interface TriggerRateSpec {
|
|
|
593
614
|
readonly timeoutMs?: number;
|
|
594
615
|
/** Seconds to wait between runs (avoid rate-limit bursts). Default 4. */
|
|
595
616
|
readonly spacingSec?: number;
|
|
617
|
+
/**
|
|
618
|
+
* Files (path → contents) seeded into every run's cwd before the prompt — the
|
|
619
|
+
* filesystem CONTEXT the skill is measured in. The default empty cwd is faithful
|
|
620
|
+
* for opening-move skills ("describe a feature", "debug this") but biased-low for
|
|
621
|
+
* skills whose trigger is a repo STATE ("in a git repo", "dirty tree"); seed that
|
|
622
|
+
* state here so recall is honest instead of an artifact of the cold start. Mirrors
|
|
623
|
+
* `MeasureSpec.fixture`. See `research/plugin-behavioral-findings.md`.
|
|
624
|
+
*/
|
|
625
|
+
readonly fixture?: Record<string, string>;
|
|
626
|
+
/**
|
|
627
|
+
* How many runs to execute in parallel across the whole prompts × trials grid.
|
|
628
|
+
* Default 1 (serial, the politest to rate limits). Raise it to cut wall-clock on
|
|
629
|
+
* a large prompt set or roster sweep — the `spacingSec` pause still applies per
|
|
630
|
+
* run, so it stays best-effort polite. Mirrors `EvalSpec.concurrency`.
|
|
631
|
+
*/
|
|
632
|
+
readonly concurrency?: number;
|
|
596
633
|
}
|
|
597
634
|
/** Per-prompt trigger result: how many of its trials fired. */
|
|
598
635
|
export interface PromptTriggerStat {
|
|
@@ -630,7 +667,29 @@ export interface TriggerRateReport {
|
|
|
630
667
|
* description). A non-zero count is the whole-harness measurement.
|
|
631
668
|
*/
|
|
632
669
|
readonly competitors: number;
|
|
670
|
+
/**
|
|
671
|
+
* Runs EXCLUDED because the turn errored / was rate-limited (detected by the
|
|
672
|
+
* driver's `runError`), present only when > 0. These are NOT counted in `n` or
|
|
673
|
+
* as misses — so `rate` reflects only valid runs. A large `errored` relative to
|
|
674
|
+
* `n` means the measurement is thin (e.g. a Codex usage limit was hit); re-run.
|
|
675
|
+
*/
|
|
676
|
+
readonly errored?: number;
|
|
633
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* An eval-tier transport: how to RUN a real harness turn and PARSE its output.
|
|
680
|
+
* The default is Claude Code (`claudeEvalDriver`); a second harness supplies its
|
|
681
|
+
* own (e.g. `codexEvalDriver` from `vigiles/codex`) and passes it as
|
|
682
|
+
* `measureTriggerRate(spec, { evalDriver })` — the eval-tier analog of
|
|
683
|
+
* `runHarnessTest`'s `{ adapter }`. `runError` lets the loop drop an
|
|
684
|
+
* errored/rate-limited turn instead of scoring it as a miss.
|
|
685
|
+
*/
|
|
686
|
+
export interface EvalDriver {
|
|
687
|
+
readonly runner: AgentRunner;
|
|
688
|
+
readonly parse: ModelOutputParser;
|
|
689
|
+
readonly runError?: (out: RunOut) => string | null;
|
|
690
|
+
}
|
|
691
|
+
/** The default (Claude Code) eval driver: real `claude` + stream-json parsing. */
|
|
692
|
+
export declare const claudeEvalDriver: EvalDriver;
|
|
634
693
|
/**
|
|
635
694
|
* Package loose `<skillsDir>/<name>/SKILL.md` skills into a throwaway plugin dir
|
|
636
695
|
* that `claude --plugin-dir` accepts — so repo-local skills (e.g. `.claude/skills`)
|
|
@@ -707,20 +766,17 @@ export declare function packageInstallSet(opts: {
|
|
|
707
766
|
dir: string;
|
|
708
767
|
added: number;
|
|
709
768
|
};
|
|
769
|
+
export declare function measureTriggerRateWith(spec: TriggerRateSpec, runner: AgentRunner, parse?: ModelOutputParser, runError?: (out: RunOut) => string | null): Promise<TriggerRateReport>;
|
|
710
770
|
/**
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* `measureTriggerRate` is this with the real agent runner.
|
|
717
|
-
*/
|
|
718
|
-
export declare function measureTriggerRateWith(spec: TriggerRateSpec, runner: AgentRunner): Promise<TriggerRateReport>;
|
|
719
|
-
/**
|
|
720
|
-
* Measure a skill/behaviour's real trigger rate across prompts × trials against
|
|
721
|
-
* the real `claude` CLI. Requires `claude` + model auth.
|
|
771
|
+
* Measure a skill/behaviour's real trigger rate across prompts × trials. Defaults
|
|
772
|
+
* to the real `claude` CLI (`claudeEvalDriver`); pass `{ evalDriver }` to drive a
|
|
773
|
+
* second harness — e.g. `measureTriggerRate(spec, { evalDriver: codexEvalDriver })`
|
|
774
|
+
* from `vigiles/codex` (the eval-tier analog of `runHarnessTest`'s `{ adapter }`).
|
|
775
|
+
* Requires that harness's binary + auth.
|
|
722
776
|
*/
|
|
723
|
-
export declare function measureTriggerRate(spec: TriggerRateSpec
|
|
777
|
+
export declare function measureTriggerRate(spec: TriggerRateSpec, opts?: {
|
|
778
|
+
evalDriver?: EvalDriver;
|
|
779
|
+
}): Promise<TriggerRateReport>;
|
|
724
780
|
/** Format a trigger-rate report: overall %, then each prompt's rate. */
|
|
725
781
|
export declare function formatTriggerRateReport(report: TriggerRateReport): string;
|
|
726
782
|
//# sourceMappingURL=eval.d.ts.map
|
package/dist/eval.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EPHEMERAL_HOME_KEEP = void 0;
|
|
3
|
+
exports.claudeEvalDriver = exports.EPHEMERAL_HOME_KEEP = void 0;
|
|
4
4
|
exports.resolveSpawnEnv = resolveSpawnEnv;
|
|
5
|
+
exports.spawnAgent = spawnAgent;
|
|
5
6
|
exports.runEval = runEval;
|
|
6
7
|
exports.measureWith = measureWith;
|
|
7
8
|
exports.measure = measure;
|
|
@@ -12,6 +13,7 @@ exports.formatCheckReport = formatCheckReport;
|
|
|
12
13
|
exports.assertRates = assertRates;
|
|
13
14
|
exports.checkReportToJUnit = checkReportToJUnit;
|
|
14
15
|
exports.parseUsage = parseUsage;
|
|
16
|
+
exports.parseClaudeRun = parseClaudeRun;
|
|
15
17
|
exports.aggregate = aggregate;
|
|
16
18
|
exports.aggregateStats = aggregateStats;
|
|
17
19
|
exports.aggregateUsage = aggregateUsage;
|
|
@@ -91,6 +93,8 @@ function resolveSpawnEnv(a, base = process.env) {
|
|
|
91
93
|
return a.replaceEnv ? (a.env ?? {}) : { ...base, ...a.env };
|
|
92
94
|
}
|
|
93
95
|
/* v8 ignore start -- real claude subprocess; exercised by bench/, not the unit gate */
|
|
96
|
+
/** The real `claude`-spawning runner (composition root). Exported so other
|
|
97
|
+
* real-model entries (e.g. `scan --trigger`) bind the same runner. */
|
|
94
98
|
function spawnAgent(a) {
|
|
95
99
|
return new Promise((resolvePromise) => {
|
|
96
100
|
const args = [
|
|
@@ -392,21 +396,31 @@ function usageFrom(result) {
|
|
|
392
396
|
function parseUsage(stdout) {
|
|
393
397
|
return usageFrom((0, harness_test_js_1.parseResultEvent)(stdout));
|
|
394
398
|
}
|
|
395
|
-
|
|
399
|
+
/** Parse Claude Code's stream-json stdout into the common trace fields. */
|
|
400
|
+
function parseClaudeRun(out) {
|
|
396
401
|
const result = (0, harness_test_js_1.parseResultEvent)(out.stdout);
|
|
397
|
-
const turns = typeof result?.num_turns === "number" ? result.num_turns : 0;
|
|
398
|
-
const output = typeof result?.result === "string" ? result.result : "";
|
|
399
402
|
return {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
stdout: out.stdout,
|
|
403
|
-
turns,
|
|
403
|
+
turns: typeof result?.num_turns === "number" ? result.num_turns : 0,
|
|
404
|
+
output: typeof result?.result === "string" ? result.result : "",
|
|
404
405
|
toolCalls: (0, harness_test_js_1.parseToolCalls)(out.stdout),
|
|
405
406
|
hooks: (0, harness_test_js_1.parseHooks)(out.stdout),
|
|
406
|
-
output,
|
|
407
407
|
subagents: (0, harness_test_js_1.parseSubagents)(out.stdout),
|
|
408
408
|
usage: usageFrom(result),
|
|
409
|
-
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function makeContext(cwd, out, parse = parseClaudeRun) {
|
|
412
|
+
const p = parse(out);
|
|
413
|
+
return {
|
|
414
|
+
cwd,
|
|
415
|
+
exitCode: out.code,
|
|
416
|
+
stdout: out.stdout,
|
|
417
|
+
turns: p.turns,
|
|
418
|
+
toolCalls: p.toolCalls,
|
|
419
|
+
hooks: p.hooks,
|
|
420
|
+
output: p.output,
|
|
421
|
+
subagents: p.subagents,
|
|
422
|
+
usage: p.usage,
|
|
423
|
+
// The eval tier drives the real API (no mock between the agent and the model),
|
|
410
424
|
// so the requests can't be captured here — modelRequests is harness-tier only.
|
|
411
425
|
modelRequests: [],
|
|
412
426
|
file: (p) => {
|
|
@@ -993,6 +1007,11 @@ function formatEvalReport(report) {
|
|
|
993
1007
|
}
|
|
994
1008
|
return lines.join("\n");
|
|
995
1009
|
}
|
|
1010
|
+
/** The default (Claude Code) eval driver: real `claude` + stream-json parsing. */
|
|
1011
|
+
exports.claudeEvalDriver = {
|
|
1012
|
+
runner: spawnAgent,
|
|
1013
|
+
parse: parseClaudeRun,
|
|
1014
|
+
};
|
|
996
1015
|
/**
|
|
997
1016
|
* Package loose `<skillsDir>/<name>/SKILL.md` skills into a throwaway plugin dir
|
|
998
1017
|
* that `claude --plugin-dir` accepts — so repo-local skills (e.g. `.claude/skills`)
|
|
@@ -1284,42 +1303,65 @@ function resolveTriggerPluginDir(spec) {
|
|
|
1284
1303
|
};
|
|
1285
1304
|
}
|
|
1286
1305
|
/** Run one prompt set × trials through `runner`, aggregating fired counts. */
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
hasSettings: false,
|
|
1302
|
-
pluginDir: cfg.pluginDir,
|
|
1303
|
-
timeoutMs: cfg.timeoutMs,
|
|
1304
|
-
});
|
|
1305
|
-
if (cfg.fired(makeContext(cwd, out)))
|
|
1306
|
-
fired++;
|
|
1307
|
-
}
|
|
1308
|
-
finally {
|
|
1309
|
-
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
1310
|
-
await sleep(cfg.spacing);
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
perPrompt.push({
|
|
1314
|
-
prompt,
|
|
1315
|
-
fired,
|
|
1316
|
-
trials: cfg.trials,
|
|
1317
|
-
rate: cfg.trials > 0 ? fired / cfg.trials : 0,
|
|
1306
|
+
/** Run one trigger trial in a throwaway cwd (fixture seeded) → fired 0/1. */
|
|
1307
|
+
async function runTriggerTrial(prompt, cfg, runner) {
|
|
1308
|
+
const cwd = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "vigiles-trigger-"));
|
|
1309
|
+
try {
|
|
1310
|
+
if (cfg.fixture)
|
|
1311
|
+
writeFiles(cwd, cfg.fixture);
|
|
1312
|
+
const out = await runner({
|
|
1313
|
+
task: prompt,
|
|
1314
|
+
cwd,
|
|
1315
|
+
model: cfg.model,
|
|
1316
|
+
tools: cfg.tools,
|
|
1317
|
+
hasSettings: false,
|
|
1318
|
+
pluginDir: cfg.pluginDir,
|
|
1319
|
+
timeoutMs: cfg.timeoutMs,
|
|
1318
1320
|
});
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
+
// An errored/rate-limited turn is NOT a "skill didn't fire" miss — it's
|
|
1322
|
+
// excluded from the rate, so e.g. a Codex usage limit can't read as recall 0.
|
|
1323
|
+
if (cfg.runError?.(out))
|
|
1324
|
+
return { fired: 0, errored: true };
|
|
1325
|
+
return {
|
|
1326
|
+
fired: cfg.fired(makeContext(cwd, out, cfg.parse)) ? 1 : 0,
|
|
1327
|
+
errored: false,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
finally {
|
|
1331
|
+
(0, node_fs_1.rmSync)(cwd, { recursive: true, force: true });
|
|
1332
|
+
await sleep(cfg.spacing);
|
|
1321
1333
|
}
|
|
1322
|
-
|
|
1334
|
+
}
|
|
1335
|
+
/** A count for reporting: the number if positive, else undefined (omit zero). */
|
|
1336
|
+
const positiveOrUndefined = (n) => n > 0 ? n : undefined;
|
|
1337
|
+
async function runTriggerSet(prompts, cfg, runner) {
|
|
1338
|
+
// Flatten prompts × trials into one work list so concurrency spans both.
|
|
1339
|
+
const jobs = prompts.flatMap((prompt, promptIndex) => Array.from({ length: cfg.trials }, () => ({ prompt, promptIndex })));
|
|
1340
|
+
const outcomes = await runPool(jobs, cfg.concurrency, (job) => runTriggerTrial(job.prompt, cfg, runner));
|
|
1341
|
+
// Re-aggregate per prompt, preserving input order; errored runs don't count.
|
|
1342
|
+
const firedBy = new Array(prompts.length).fill(0);
|
|
1343
|
+
const trialsBy = new Array(prompts.length).fill(0);
|
|
1344
|
+
let errored = 0;
|
|
1345
|
+
jobs.forEach((job, i) => {
|
|
1346
|
+
if (outcomes[i].errored) {
|
|
1347
|
+
errored += 1;
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
firedBy[job.promptIndex] += outcomes[i].fired;
|
|
1351
|
+
trialsBy[job.promptIndex] += 1;
|
|
1352
|
+
});
|
|
1353
|
+
const perPrompt = prompts.map((prompt, i) => ({
|
|
1354
|
+
prompt,
|
|
1355
|
+
fired: firedBy[i],
|
|
1356
|
+
trials: trialsBy[i],
|
|
1357
|
+
rate: trialsBy[i] > 0 ? firedBy[i] / trialsBy[i] : 0,
|
|
1358
|
+
}));
|
|
1359
|
+
return {
|
|
1360
|
+
perPrompt,
|
|
1361
|
+
fired: firedBy.reduce((a, b) => a + b, 0),
|
|
1362
|
+
n: trialsBy.reduce((a, b) => a + b, 0),
|
|
1363
|
+
errored,
|
|
1364
|
+
};
|
|
1323
1365
|
}
|
|
1324
1366
|
/**
|
|
1325
1367
|
* Trigger-rate orchestration — every prompt × trial via `runner`, the `fired`
|
|
@@ -1329,9 +1371,8 @@ async function runTriggerSet(prompts, cfg, runner) {
|
|
|
1329
1371
|
* injectable `runner` so the loop is unit-testable without a model;
|
|
1330
1372
|
* `measureTriggerRate` is this with the real agent runner.
|
|
1331
1373
|
*/
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
// before spending a token (and before packaging a skillsDir).
|
|
1374
|
+
/** Diversity pre-flight: reject a too-small / near-duplicate prompt set (both sides). */
|
|
1375
|
+
function assertTriggerDiversity(spec) {
|
|
1335
1376
|
const diversity = {
|
|
1336
1377
|
minPrompts: spec.minPrompts,
|
|
1337
1378
|
minDistance: spec.minDistance,
|
|
@@ -1343,6 +1384,10 @@ async function measureTriggerRateWith(spec, runner) {
|
|
|
1343
1384
|
label: "irrelevantPrompts",
|
|
1344
1385
|
});
|
|
1345
1386
|
}
|
|
1387
|
+
}
|
|
1388
|
+
async function measureTriggerRateWith(spec, runner, parse = parseClaudeRun, runError) {
|
|
1389
|
+
// Deterministic gate FIRST — before spending a token (or packaging a skillsDir).
|
|
1390
|
+
assertTriggerDiversity(spec);
|
|
1346
1391
|
// Model floor (default Sonnet): trigger-rate under-measures selection on a
|
|
1347
1392
|
// weaker model, so FAIL before spending a token rather than report a
|
|
1348
1393
|
// false-negative recall. The floor lives in the spec (`minModel`), not an env
|
|
@@ -1365,6 +1410,10 @@ async function measureTriggerRateWith(spec, runner) {
|
|
|
1365
1410
|
spacing: (spec.spacingSec ?? 4) * 1000,
|
|
1366
1411
|
pluginDir,
|
|
1367
1412
|
fired: spec.fired,
|
|
1413
|
+
fixture: spec.fixture,
|
|
1414
|
+
concurrency: Math.max(1, spec.concurrency ?? 1),
|
|
1415
|
+
parse,
|
|
1416
|
+
runError,
|
|
1368
1417
|
};
|
|
1369
1418
|
try {
|
|
1370
1419
|
const relevant = await runTriggerSet(spec.prompts, cfg, runner);
|
|
@@ -1373,6 +1422,7 @@ async function measureTriggerRateWith(spec, runner) {
|
|
|
1373
1422
|
n: relevant.n,
|
|
1374
1423
|
perPrompt: relevant.perPrompt,
|
|
1375
1424
|
competitors,
|
|
1425
|
+
errored: positiveOrUndefined(relevant.errored),
|
|
1376
1426
|
};
|
|
1377
1427
|
if ((spec.irrelevantPrompts?.length ?? 0) === 0)
|
|
1378
1428
|
return base;
|
|
@@ -1380,6 +1430,7 @@ async function measureTriggerRateWith(spec, runner) {
|
|
|
1380
1430
|
const fires = relevant.fired + irrelevant.fired;
|
|
1381
1431
|
return {
|
|
1382
1432
|
...base,
|
|
1433
|
+
errored: positiveOrUndefined(relevant.errored + irrelevant.errored),
|
|
1383
1434
|
falsePositiveRate: irrelevant.n > 0 ? irrelevant.fired / irrelevant.n : 0,
|
|
1384
1435
|
precision: fires > 0 ? relevant.fired / fires : undefined,
|
|
1385
1436
|
perIrrelevant: irrelevant.perPrompt,
|
|
@@ -1393,11 +1444,15 @@ async function measureTriggerRateWith(spec, runner) {
|
|
|
1393
1444
|
}
|
|
1394
1445
|
/* v8 ignore start -- real claude subprocess; thin wrapper over measureTriggerRateWith */
|
|
1395
1446
|
/**
|
|
1396
|
-
* Measure a skill/behaviour's real trigger rate across prompts × trials
|
|
1397
|
-
* the real `claude` CLI
|
|
1447
|
+
* Measure a skill/behaviour's real trigger rate across prompts × trials. Defaults
|
|
1448
|
+
* to the real `claude` CLI (`claudeEvalDriver`); pass `{ evalDriver }` to drive a
|
|
1449
|
+
* second harness — e.g. `measureTriggerRate(spec, { evalDriver: codexEvalDriver })`
|
|
1450
|
+
* from `vigiles/codex` (the eval-tier analog of `runHarnessTest`'s `{ adapter }`).
|
|
1451
|
+
* Requires that harness's binary + auth.
|
|
1398
1452
|
*/
|
|
1399
|
-
async function measureTriggerRate(spec) {
|
|
1400
|
-
|
|
1453
|
+
async function measureTriggerRate(spec, opts = {}) {
|
|
1454
|
+
const d = opts.evalDriver ?? exports.claudeEvalDriver;
|
|
1455
|
+
return measureTriggerRateWith(spec, d.runner, d.parse, d.runError);
|
|
1401
1456
|
}
|
|
1402
1457
|
/* v8 ignore stop */
|
|
1403
1458
|
/** Format a trigger-rate report: overall %, then each prompt's rate. */
|
package/dist/harness-test.d.ts
CHANGED
|
@@ -110,6 +110,13 @@ export interface SubagentTrace {
|
|
|
110
110
|
readonly name: string;
|
|
111
111
|
/** The tools the subagent invoked (events tagged with the Task's id). */
|
|
112
112
|
readonly toolCalls: readonly ToolCall[];
|
|
113
|
+
/**
|
|
114
|
+
* The subagent's RETURNED text — the dispatch tool_result the orchestrator
|
|
115
|
+
* receives back. This is where a `result()` contract's `vigiles:ok`/`vigiles:err`
|
|
116
|
+
* block lands, so `subagent(name, [output(/vigiles:ok/)])` can assert the typed
|
|
117
|
+
* outcome. "" if not captured.
|
|
118
|
+
*/
|
|
119
|
+
readonly output: string;
|
|
113
120
|
}
|
|
114
121
|
export interface HarnessTestResult extends Trace {
|
|
115
122
|
readonly exitCode: number;
|
package/dist/harness-test.js
CHANGED
|
@@ -130,6 +130,7 @@ function parseToolCalls(streamJson) {
|
|
|
130
130
|
*/
|
|
131
131
|
function parseSubagents(streamJson) {
|
|
132
132
|
const tasks = new Map(); // dispatch id → subagent name
|
|
133
|
+
const dispatchOutput = new Map(); // dispatch id → returned text
|
|
133
134
|
const byParent = new Map();
|
|
134
135
|
const groupFor = (parent) => {
|
|
135
136
|
let g = byParent.get(parent);
|
|
@@ -162,7 +163,10 @@ function parseSubagents(streamJson) {
|
|
|
162
163
|
// A subagent dispatch is any top-level tool_use whose input carries a
|
|
163
164
|
// `subagent_type` — the dispatch tool is named "Agent" on the live CLI
|
|
164
165
|
// (older docs say "Task"), so match the input field, NOT the tool name,
|
|
165
|
-
// to survive the rename. Confirmed against real claude output.
|
|
166
|
+
// to survive the rename. Confirmed against real claude output. CC NOTE:
|
|
167
|
+
// under `--plugin-dir` the value is NAMESPACED `plugin:agent` (captured
|
|
168
|
+
// "reviewer-spec:code-reviewer"); the bare agent name is matched in the
|
|
169
|
+
// `subagent()` check (src/check.ts), so the full id is preserved here.
|
|
166
170
|
const sub = b.input?.subagent_type;
|
|
167
171
|
if (typeof sub === "string")
|
|
168
172
|
tasks.set(id, sub);
|
|
@@ -170,12 +174,20 @@ function parseSubagents(streamJson) {
|
|
|
170
174
|
if (parent)
|
|
171
175
|
groupFor(parent).uses.push({ id, name: b.name, input: b.input });
|
|
172
176
|
}
|
|
173
|
-
else if (b.type === "tool_result"
|
|
177
|
+
else if (b.type === "tool_result") {
|
|
174
178
|
const id = typeof b.tool_use_id === "string" ? b.tool_use_id : "";
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
if (parent) {
|
|
180
|
+
groupFor(parent).results.set(id, {
|
|
181
|
+
text: contentText(b.content),
|
|
182
|
+
isError: b.is_error === true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (id) {
|
|
186
|
+
// A top-level tool_result whose id is a subagent dispatch is the SUB's
|
|
187
|
+
// RETURN to the orchestrator (where a result() vigiles:ok/err block
|
|
188
|
+
// lands). Record it; matched to its dispatch by id below.
|
|
189
|
+
dispatchOutput.set(id, contentText(b.content));
|
|
190
|
+
}
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
193
|
}
|
|
@@ -188,7 +200,7 @@ function parseSubagents(streamJson) {
|
|
|
188
200
|
resultText: g?.results.get(u.id)?.text ?? "",
|
|
189
201
|
isError: g?.results.get(u.id)?.isError ?? false,
|
|
190
202
|
}));
|
|
191
|
-
out.push({ name, toolCalls });
|
|
203
|
+
out.push({ name, toolCalls, output: dispatchOutput.get(taskId) ?? "" });
|
|
192
204
|
}
|
|
193
205
|
return out;
|
|
194
206
|
}
|
package/dist/leaderboard.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface PluginScore {
|
|
|
21
21
|
readonly issues: readonly string[];
|
|
22
22
|
readonly report: ScanReport;
|
|
23
23
|
}
|
|
24
|
+
/** Map a 0–100 structural-health score to its letter grade (A ≥90 … F <60). */
|
|
25
|
+
export declare function gradeFor(score: number): PluginScore["grade"];
|
|
24
26
|
/** Deterministic structural-health score for one scanned plugin. */
|
|
25
27
|
export declare function scoreReport(r: ScanReport): {
|
|
26
28
|
score: number;
|