vigiles 15.0.2 → 15.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/run-scripts.d.ts +21 -9
- package/dist/adapters/claude-code/run-scripts.js +33 -22
- package/dist/adapters/codex/driver.js +3 -1
- package/dist/adapters/codex/eval.js +5 -0
- package/dist/audit-score.js +32 -6
- package/dist/check-count.d.ts +62 -3
- package/dist/check-count.js +123 -5
- package/dist/check.d.ts +55 -0
- package/dist/check.js +91 -0
- package/dist/cli.js +456 -54
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +278 -0
- package/dist/core/foreign-runner.d.ts +185 -0
- package/dist/core/foreign-runner.js +228 -0
- package/dist/core/harness-driver.d.ts +18 -1
- package/dist/core/hook-program.d.ts +319 -20
- package/dist/core/hook-program.js +743 -49
- package/dist/core/layout.d.ts +13 -0
- package/dist/core/lethal-trifecta.d.ts +93 -0
- package/dist/core/lethal-trifecta.js +409 -65
- package/dist/core/markdown.d.ts +32 -0
- package/dist/core/markdown.js +36 -0
- package/dist/core/merge-conflict.d.ts +50 -0
- package/dist/core/merge-conflict.js +84 -0
- package/dist/core/test-file-ext.d.ts +80 -0
- package/dist/core/test-file-ext.js +90 -0
- package/dist/core/types.d.ts +11 -0
- package/dist/coverage-artifact.d.ts +383 -0
- package/dist/coverage-artifact.js +586 -0
- package/dist/coverage-evidence.d.ts +87 -79
- package/dist/coverage-evidence.js +212 -211
- package/dist/coverage-probe.d.ts +125 -0
- package/dist/coverage-probe.js +700 -0
- package/dist/doc-commands.d.ts +119 -0
- package/dist/doc-commands.js +158 -0
- package/dist/eval-lock.d.ts +9 -0
- package/dist/eval-lock.js +14 -2
- package/dist/eval.js +28 -0
- package/dist/fs-walk.d.ts +80 -0
- package/dist/fs-walk.js +148 -0
- package/dist/harness-assert.js +42 -3
- package/dist/harness-test.js +31 -2
- package/dist/judge.js +4 -0
- package/dist/leaderboard.d.ts +16 -1
- package/dist/leaderboard.js +19 -2
- package/dist/load-hook.js +6 -1
- package/dist/mock-model.d.ts +58 -4
- package/dist/mock-model.js +133 -5
- package/dist/observe.d.ts +1 -1
- package/dist/observe.js +44 -1
- package/dist/plugin-loader.js +52 -13
- package/dist/run-hook.d.ts +58 -0
- package/dist/run-hook.js +70 -0
- package/dist/run-script.js +75 -0
- package/dist/scaffold-test.d.ts +3 -0
- package/dist/scaffold-test.js +10 -5
- package/dist/scan-behavioral.js +4 -0
- package/dist/scan-core.d.ts +22 -0
- package/dist/scan-core.js +46 -14
- package/dist/scan-files.js +19 -2
- package/dist/scan.d.ts +22 -5
- package/dist/scan.js +67 -13
- package/dist/skill-contract.d.ts +46 -0
- package/dist/skill-contract.js +201 -0
- package/dist/skill-refs.d.ts +67 -0
- package/dist/skill-refs.js +117 -0
- package/dist/test-coverage-files.d.ts +7 -1
- package/dist/test-coverage-files.js +66 -55
- package/dist/test-coverage.d.ts +155 -19
- package/dist/test-coverage.js +430 -91
- package/dist/testing.d.ts +8 -2
- package/dist/testing.js +19 -1
- package/dist/trigger-containment.d.ts +85 -0
- package/dist/trigger-containment.js +125 -0
- package/dist/ts-runner-caps.d.ts +16 -0
- package/dist/ts-runner-caps.js +43 -0
- package/dist/unit.d.ts +2 -2
- package/dist/unit.js +2 -1
- package/hooks/eval-lock-nudge.sh +14 -6
- package/package.json +2 -2
- package/skills/test-harness/SKILL.md +68 -2
package/dist/testing.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
export { recordCheck } from "./check-count.js";
|
|
14
14
|
export { runScript } from "./run-script.js";
|
|
15
15
|
export type { RunScriptOptions, ScriptRunResult } from "./run-script.js";
|
|
16
|
-
export { runHook, propertyHook } from "./run-hook.js";
|
|
17
|
-
export type { HookRunResult, RunHookOptions, HookInput, HookOutput, HookPropertyResult, } from "./run-hook.js";
|
|
16
|
+
export { runHook, propertyHook, fileToolEvents } from "./run-hook.js";
|
|
17
|
+
export type { HookRunResult, RunHookOptions, HookInput, HookOutput, HookPropertyResult, FileToolEventOptions, } from "./run-hook.js";
|
|
18
18
|
export { runEval, measure, measureArms, measureTriggerRate, assertRates, assertPromptDiversity, checkPromptDiversity, checkReportToJUnit, formatCheckReport, formatEvalReport, formatTriggerRateReport, claudeEvalDriver, parseClaudeRun, stubSkillBody, } from "./eval.js";
|
|
19
19
|
export type { EvalArm, EvalDriver, EvalSpec, EvalReport, EvalUsage, MeasureSpec, ArmsMeasureSpec, ArmReport, ArmUsage, ArmsCheckReport, CheckRate, CheckReport, MetricStat, Metrics, ModelOutputParser, ParsedModelRun, PromptDiversityIssue, PromptTriggerStat, RunContext, RunOut, SelectionTrialResult, TriggerRateReport, TriggerRateSpec, AgentRunArgs, AgentRunner, } from "./eval.js";
|
|
20
20
|
export * from "./harness-assert.js";
|
|
@@ -24,5 +24,11 @@ export { hookFired } from "./check.js";
|
|
|
24
24
|
export { judge } from "./judge.js";
|
|
25
25
|
export * from "./tool-stub.js";
|
|
26
26
|
export { runHarnessTest, runHarness } from "./harness-test.js";
|
|
27
|
+
export { commandsIn, mustInclude, mustNotInclude } from "./doc-commands.js";
|
|
28
|
+
export type { DocCommand } from "./doc-commands.js";
|
|
29
|
+
export { skillContract } from "./skill-contract.js";
|
|
30
|
+
export { compareContainment, formatContainment, } from "./trigger-containment.js";
|
|
31
|
+
export type { ContainmentInput, ContainmentVerdict, } from "./trigger-containment.js";
|
|
32
|
+
export type { SkillContract, SkillContractOptions } from "./skill-contract.js";
|
|
27
33
|
export type { HarnessTestSpec, Trace, SubagentTrace, HarnessTestResult, RunHarnessTestOptions, ModelTurn, ModelRequest, ToolCall, HookFire, HarnessTestDriver, SandboxMode, } from "./harness-test.js";
|
|
28
34
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.js
CHANGED
|
@@ -30,7 +30,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
30
30
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
31
31
|
};
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = exports.loadHook = exports.stubSkillBody = exports.parseClaudeRun = exports.claudeEvalDriver = exports.formatTriggerRateReport = exports.formatEvalReport = exports.formatCheckReport = exports.checkReportToJUnit = exports.checkPromptDiversity = exports.assertPromptDiversity = exports.assertRates = exports.measureTriggerRate = exports.measureArms = exports.measure = exports.runEval = exports.propertyHook = exports.runHook = exports.runScript = exports.recordCheck = void 0;
|
|
33
|
+
exports.formatContainment = exports.compareContainment = exports.skillContract = exports.mustNotInclude = exports.mustInclude = exports.commandsIn = exports.runHarness = exports.runHarnessTest = exports.judge = exports.hookFired = exports.loadHook = exports.stubSkillBody = exports.parseClaudeRun = exports.claudeEvalDriver = exports.formatTriggerRateReport = exports.formatEvalReport = exports.formatCheckReport = exports.checkReportToJUnit = exports.checkPromptDiversity = exports.assertPromptDiversity = exports.assertRates = exports.measureTriggerRate = exports.measureArms = exports.measure = exports.runEval = exports.fileToolEvents = exports.propertyHook = exports.runHook = exports.runScript = exports.recordCheck = void 0;
|
|
34
34
|
// --- reporting: how much did this script actually do? ---
|
|
35
35
|
// `vigiles test` can otherwise see only an exit code, so a file that runs NOTHING
|
|
36
36
|
// prints the same `✓` as one that ran and passed (measured 2026-08-08 on a file
|
|
@@ -49,6 +49,7 @@ Object.defineProperty(exports, "runScript", { enumerable: true, get: function ()
|
|
|
49
49
|
var run_hook_js_1 = require("./run-hook.js");
|
|
50
50
|
Object.defineProperty(exports, "runHook", { enumerable: true, get: function () { return run_hook_js_1.runHook; } });
|
|
51
51
|
Object.defineProperty(exports, "propertyHook", { enumerable: true, get: function () { return run_hook_js_1.propertyHook; } });
|
|
52
|
+
Object.defineProperty(exports, "fileToolEvents", { enumerable: true, get: function () { return run_hook_js_1.fileToolEvents; } });
|
|
52
53
|
// --- eval tier: runEval / measure / trigger-rate ---
|
|
53
54
|
var eval_js_1 = require("./eval.js");
|
|
54
55
|
Object.defineProperty(exports, "runEval", { enumerable: true, get: function () { return eval_js_1.runEval; } });
|
|
@@ -95,4 +96,21 @@ __exportStar(require("./tool-stub.js"), exports);
|
|
|
95
96
|
var harness_test_js_1 = require("./harness-test.js");
|
|
96
97
|
Object.defineProperty(exports, "runHarnessTest", { enumerable: true, get: function () { return harness_test_js_1.runHarnessTest; } });
|
|
97
98
|
Object.defineProperty(exports, "runHarness", { enumerable: true, get: function () { return harness_test_js_1.runHarness; } });
|
|
99
|
+
// A document's own rule about its own COMMANDS, made checkable. vigiles cannot
|
|
100
|
+
// infer "always pass -g" from prose; what was missing was a cheap way to declare
|
|
101
|
+
// it — measured at ~95 lines of markdown parsing around ~5 lines of rule, which
|
|
102
|
+
// is why nobody wrote the check and the rule stayed prose.
|
|
103
|
+
var doc_commands_js_1 = require("./doc-commands.js");
|
|
104
|
+
Object.defineProperty(exports, "commandsIn", { enumerable: true, get: function () { return doc_commands_js_1.commandsIn; } });
|
|
105
|
+
Object.defineProperty(exports, "mustInclude", { enumerable: true, get: function () { return doc_commands_js_1.mustInclude; } });
|
|
106
|
+
Object.defineProperty(exports, "mustNotInclude", { enumerable: true, get: function () { return doc_commands_js_1.mustNotInclude; } });
|
|
107
|
+
// A skill's own `allowed-tools:` frontmatter, read back as checks — the wiring
|
|
108
|
+
// from a declaration to the existing check vocabulary, not new machinery.
|
|
109
|
+
var skill_contract_js_1 = require("./skill-contract.js");
|
|
110
|
+
Object.defineProperty(exports, "skillContract", { enumerable: true, get: function () { return skill_contract_js_1.skillContract; } });
|
|
111
|
+
// Is a WEAKER model a valid lower bound for trigger-rate? Pure comparison of two
|
|
112
|
+
// runs; the open question the model floor makes people ask.
|
|
113
|
+
var trigger_containment_js_1 = require("./trigger-containment.js");
|
|
114
|
+
Object.defineProperty(exports, "compareContainment", { enumerable: true, get: function () { return trigger_containment_js_1.compareContainment; } });
|
|
115
|
+
Object.defineProperty(exports, "formatContainment", { enumerable: true, get: function () { return trigger_containment_js_1.formatContainment; } });
|
|
98
116
|
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is a WEAKER model a valid lower bound for trigger-rate?
|
|
3
|
+
*
|
|
4
|
+
* THE QUESTION. `measureTriggerRate` floors at Sonnet, because a weaker model
|
|
5
|
+
* under-selects: the same skill measured 0.50 on haiku and 0.90 on sonnet, so a
|
|
6
|
+
* haiku number reads as "bad description" when the description is fine. The
|
|
7
|
+
* standing counter-proposal is the ordinary worst-case argument — tune against
|
|
8
|
+
* the weakest supported model and anything stronger is covered, the way you test
|
|
9
|
+
* against the oldest supported runtime. It is a good argument, and it is valid
|
|
10
|
+
* only if one thing is true:
|
|
11
|
+
*
|
|
12
|
+
* CONTAINMENT: fires on the weak model => fires on the strong one
|
|
13
|
+
*
|
|
14
|
+
* If containment holds, the weak model is a genuine floor: cheaper, faster, and
|
|
15
|
+
* it buys margin. If it does not, the weak model is not a floor at all — it is a
|
|
16
|
+
* DIFFERENT router, and a prompt it fires on may be one the strong model
|
|
17
|
+
* declines, so tuning against it optimises a configuration nobody runs.
|
|
18
|
+
*
|
|
19
|
+
* Selection is routing, not raw capability: a stronger model can legitimately
|
|
20
|
+
* route ELSEWHERE — doing the work itself, or picking a more specific sibling —
|
|
21
|
+
* which is why containment was genuinely open rather than obviously true.
|
|
22
|
+
*
|
|
23
|
+
* MEASURED 2026-08-11, and it DOES NOT HOLD. 21 skills x 4 prompts, one trial
|
|
24
|
+
* each, whole-harness against 37 competing skills, haiku against sonnet: of 84
|
|
25
|
+
* shared prompts, 3 fired on haiku and NOT on sonnet — and one skill scored
|
|
26
|
+
* haiku 1.00 against sonnet 0.75, i.e. the weak model is not seeing less, it is
|
|
27
|
+
* routing differently. Read that as "containment is NOT established" (which is
|
|
28
|
+
* all the worst-case argument needs to fail — it requires containment to hold,
|
|
29
|
+
* not to usually hold) rather than "it breaks often": at one trial per prompt
|
|
30
|
+
* three breaks in 84 could still be dispersion. The model floor stays.
|
|
31
|
+
*
|
|
32
|
+
* Pure: give it two reports (or anything carrying `perPrompt`) and it classifies
|
|
33
|
+
* every prompt the two have in common.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 HARNESS-AGNOSTIC BY CONSTRUCTION, BUT NOT EQUALLY TRUSTWORTHY EVERYWHERE.
|
|
36
|
+
* This compares whatever two runs report, so it runs on Codex reports unchanged —
|
|
37
|
+
* and should NOT be trusted there yet. Codex emits no skill-selection event, so
|
|
38
|
+
* `fired` is INFERRED (from a `SKILL.md` read), and `measureTriggerRate` flags the
|
|
39
|
+
* whole tier experimental for that reason. The inference error lands exactly where
|
|
40
|
+
* this function looks: a prompt wrongly marked fired on one model and not the
|
|
41
|
+
* other manufactures a `weakOnly` entry, i.e. a counterexample made of noise.
|
|
42
|
+
* Read a containment verdict as strong as its weakest input signal — on Claude
|
|
43
|
+
* Code, where firing is an observed event, that is a real comparison.
|
|
44
|
+
*/
|
|
45
|
+
/** The minimum a report must expose to be compared — structural, no import cycle. */
|
|
46
|
+
export interface ContainmentInput {
|
|
47
|
+
/** Identifies the surface these prompts belong to (skill name, usually). */
|
|
48
|
+
readonly subject: string;
|
|
49
|
+
readonly perPrompt: readonly {
|
|
50
|
+
readonly prompt: string;
|
|
51
|
+
readonly rate: number;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface ContainmentVerdict {
|
|
55
|
+
/** Prompts present in BOTH inputs — the only ones a comparison can speak about. */
|
|
56
|
+
readonly compared: number;
|
|
57
|
+
/** Fired on both. */
|
|
58
|
+
readonly both: readonly string[];
|
|
59
|
+
/** Fired on neither. */
|
|
60
|
+
readonly neither: readonly string[];
|
|
61
|
+
/**
|
|
62
|
+
* Fired on the WEAK model but not the strong one. Each is a counterexample to
|
|
63
|
+
* "the weak model is a floor" — the whole point of the comparison.
|
|
64
|
+
*/
|
|
65
|
+
readonly weakOnly: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* Fired on the STRONG model but not the weak one. EXPECTED, and not a defect:
|
|
68
|
+
* it is the weak model under-selecting, which is the known reason for the
|
|
69
|
+
* model floor. Reported separately so the two are never conflated.
|
|
70
|
+
*/
|
|
71
|
+
readonly strongOnly: readonly string[];
|
|
72
|
+
/** `weakOnly.length === 0` — the weak model never fired where the strong did not. */
|
|
73
|
+
readonly holds: boolean;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Classify every prompt the two runs share.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ At one trial per prompt every cell is a SINGLE observation, so one
|
|
79
|
+
* `weakOnly` may be noise rather than a counterexample. Read a handful as
|
|
80
|
+
* evidence and one as a reason to re-run at higher trials.
|
|
81
|
+
*/
|
|
82
|
+
export declare function compareContainment(weak: readonly ContainmentInput[], strong: readonly ContainmentInput[]): ContainmentVerdict;
|
|
83
|
+
/** Human-readable verdict — the counterexamples in full, since they are the result. */
|
|
84
|
+
export declare function formatContainment(v: ContainmentVerdict): string;
|
|
85
|
+
//# sourceMappingURL=trigger-containment.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Is a WEAKER model a valid lower bound for trigger-rate?
|
|
4
|
+
*
|
|
5
|
+
* THE QUESTION. `measureTriggerRate` floors at Sonnet, because a weaker model
|
|
6
|
+
* under-selects: the same skill measured 0.50 on haiku and 0.90 on sonnet, so a
|
|
7
|
+
* haiku number reads as "bad description" when the description is fine. The
|
|
8
|
+
* standing counter-proposal is the ordinary worst-case argument — tune against
|
|
9
|
+
* the weakest supported model and anything stronger is covered, the way you test
|
|
10
|
+
* against the oldest supported runtime. It is a good argument, and it is valid
|
|
11
|
+
* only if one thing is true:
|
|
12
|
+
*
|
|
13
|
+
* CONTAINMENT: fires on the weak model => fires on the strong one
|
|
14
|
+
*
|
|
15
|
+
* If containment holds, the weak model is a genuine floor: cheaper, faster, and
|
|
16
|
+
* it buys margin. If it does not, the weak model is not a floor at all — it is a
|
|
17
|
+
* DIFFERENT router, and a prompt it fires on may be one the strong model
|
|
18
|
+
* declines, so tuning against it optimises a configuration nobody runs.
|
|
19
|
+
*
|
|
20
|
+
* Selection is routing, not raw capability: a stronger model can legitimately
|
|
21
|
+
* route ELSEWHERE — doing the work itself, or picking a more specific sibling —
|
|
22
|
+
* which is why containment was genuinely open rather than obviously true.
|
|
23
|
+
*
|
|
24
|
+
* MEASURED 2026-08-11, and it DOES NOT HOLD. 21 skills x 4 prompts, one trial
|
|
25
|
+
* each, whole-harness against 37 competing skills, haiku against sonnet: of 84
|
|
26
|
+
* shared prompts, 3 fired on haiku and NOT on sonnet — and one skill scored
|
|
27
|
+
* haiku 1.00 against sonnet 0.75, i.e. the weak model is not seeing less, it is
|
|
28
|
+
* routing differently. Read that as "containment is NOT established" (which is
|
|
29
|
+
* all the worst-case argument needs to fail — it requires containment to hold,
|
|
30
|
+
* not to usually hold) rather than "it breaks often": at one trial per prompt
|
|
31
|
+
* three breaks in 84 could still be dispersion. The model floor stays.
|
|
32
|
+
*
|
|
33
|
+
* Pure: give it two reports (or anything carrying `perPrompt`) and it classifies
|
|
34
|
+
* every prompt the two have in common.
|
|
35
|
+
*
|
|
36
|
+
* 🔴 HARNESS-AGNOSTIC BY CONSTRUCTION, BUT NOT EQUALLY TRUSTWORTHY EVERYWHERE.
|
|
37
|
+
* This compares whatever two runs report, so it runs on Codex reports unchanged —
|
|
38
|
+
* and should NOT be trusted there yet. Codex emits no skill-selection event, so
|
|
39
|
+
* `fired` is INFERRED (from a `SKILL.md` read), and `measureTriggerRate` flags the
|
|
40
|
+
* whole tier experimental for that reason. The inference error lands exactly where
|
|
41
|
+
* this function looks: a prompt wrongly marked fired on one model and not the
|
|
42
|
+
* other manufactures a `weakOnly` entry, i.e. a counterexample made of noise.
|
|
43
|
+
* Read a containment verdict as strong as its weakest input signal — on Claude
|
|
44
|
+
* Code, where firing is an observed event, that is a real comparison.
|
|
45
|
+
*/
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.compareContainment = compareContainment;
|
|
48
|
+
exports.formatContainment = formatContainment;
|
|
49
|
+
/**
|
|
50
|
+
* Separator for the composite key, written as a code point so no control
|
|
51
|
+
* character sits in this source. NUL rather than a printable character because a
|
|
52
|
+
* prompt is a sentence and may contain any printable one — splitting on a space
|
|
53
|
+
* cuts in the wrong place.
|
|
54
|
+
*/
|
|
55
|
+
const SEP = String.fromCharCode(0);
|
|
56
|
+
const index = (rows) => {
|
|
57
|
+
const m = new Map();
|
|
58
|
+
for (const r of rows)
|
|
59
|
+
for (const p of r.perPrompt)
|
|
60
|
+
m.set(`${r.subject}${SEP}${p.prompt}`, p.rate > 0);
|
|
61
|
+
return m;
|
|
62
|
+
};
|
|
63
|
+
/** Strip the composite key back to a readable `subject: prompt`. */
|
|
64
|
+
const readable = (k) => {
|
|
65
|
+
const at = k.indexOf(SEP);
|
|
66
|
+
return at === -1 ? k : `${k.slice(0, at)}: ${k.slice(at + 1)}`;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Classify every prompt the two runs share.
|
|
70
|
+
*
|
|
71
|
+
* ⚠️ At one trial per prompt every cell is a SINGLE observation, so one
|
|
72
|
+
* `weakOnly` may be noise rather than a counterexample. Read a handful as
|
|
73
|
+
* evidence and one as a reason to re-run at higher trials.
|
|
74
|
+
*/
|
|
75
|
+
function compareContainment(weak, strong) {
|
|
76
|
+
const w = index(weak);
|
|
77
|
+
const s = index(strong);
|
|
78
|
+
const both = [];
|
|
79
|
+
const neither = [];
|
|
80
|
+
const weakOnly = [];
|
|
81
|
+
const strongOnly = [];
|
|
82
|
+
for (const [k, firedWeak] of w) {
|
|
83
|
+
if (!s.has(k))
|
|
84
|
+
continue;
|
|
85
|
+
const firedStrong = s.get(k) === true;
|
|
86
|
+
if (firedWeak && firedStrong)
|
|
87
|
+
both.push(readable(k));
|
|
88
|
+
else if (!firedWeak && !firedStrong)
|
|
89
|
+
neither.push(readable(k));
|
|
90
|
+
else if (firedWeak)
|
|
91
|
+
weakOnly.push(readable(k));
|
|
92
|
+
else
|
|
93
|
+
strongOnly.push(readable(k));
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
compared: both.length + neither.length + weakOnly.length + strongOnly.length,
|
|
97
|
+
both,
|
|
98
|
+
neither,
|
|
99
|
+
weakOnly,
|
|
100
|
+
strongOnly,
|
|
101
|
+
holds: weakOnly.length === 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Human-readable verdict — the counterexamples in full, since they are the result. */
|
|
105
|
+
function formatContainment(v) {
|
|
106
|
+
if (v.compared === 0)
|
|
107
|
+
return "no prompts in common — the two runs do not describe the same set.";
|
|
108
|
+
const pct = (n) => `${((n / v.compared) * 100).toFixed(0)}%`;
|
|
109
|
+
const lines = [
|
|
110
|
+
`prompts compared: ${String(v.compared)}`,
|
|
111
|
+
` both ${String(v.both.length).padStart(3)} ${pct(v.both.length)}`,
|
|
112
|
+
` neither ${String(v.neither.length).padStart(3)} ${pct(v.neither.length)}`,
|
|
113
|
+
` weak only ${String(v.weakOnly.length).padStart(3)} ${pct(v.weakOnly.length)}`,
|
|
114
|
+
` strong only ${String(v.strongOnly.length).padStart(3)} ${pct(v.strongOnly.length)}`,
|
|
115
|
+
"",
|
|
116
|
+
`CONTAINMENT (weak fires => strong fires): ${v.holds ? "HOLDS" : "DOES NOT HOLD"}`,
|
|
117
|
+
];
|
|
118
|
+
if (!v.holds) {
|
|
119
|
+
lines.push("", `${String(v.weakOnly.length)} prompt(s) fired on the weak model but NOT the strong one —`, "each is a counterexample to treating the weak model as a floor:", ...v.weakOnly.map((k) => ` ${k}`));
|
|
120
|
+
}
|
|
121
|
+
lines.push("", `${String(v.strongOnly.length)} fired on the strong model only — expected (the weak model` +
|
|
122
|
+
" under-selects), not a containment failure.");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=trigger-containment.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Node runtime capabilities that decide how a TypeScript script is run. */
|
|
2
|
+
export interface NodeCaps {
|
|
3
|
+
/** `tsx` is installed locally (the preferred, version-agnostic TS loader). */
|
|
4
|
+
readonly tsx: boolean;
|
|
5
|
+
/** Node supports `--experimental-strip-types` (>= 22.6). */
|
|
6
|
+
readonly stripTypes: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Detect TS-running capabilities for a project root. */
|
|
9
|
+
export declare function detectNodeCaps(cwd: string): NodeCaps;
|
|
10
|
+
/**
|
|
11
|
+
* Is there ANY path by which `vigiles test` could execute a TypeScript script
|
|
12
|
+
* here? The exact disjunction `interpreterArgs` branches on — one predicate, so
|
|
13
|
+
* the suggester and the runner cannot drift back apart.
|
|
14
|
+
*/
|
|
15
|
+
export declare function canRunTypeScript(caps: NodeCaps): boolean;
|
|
16
|
+
//# sourceMappingURL=ts-runner-caps.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectNodeCaps = detectNodeCaps;
|
|
4
|
+
exports.canRunTypeScript = canRunTypeScript;
|
|
5
|
+
/**
|
|
6
|
+
* Can this project RUN a TypeScript script — and therefore, may we suggest one?
|
|
7
|
+
*
|
|
8
|
+
* Extracted from `adapters/claude-code/run-scripts.ts` (which still re-exports it)
|
|
9
|
+
* because two places need the same answer and they were giving opposite ones:
|
|
10
|
+
*
|
|
11
|
+
* - the RUNNER (`interpreterArgs`) refuses a `.ts` script with no `tsx` and no
|
|
12
|
+
* native type stripping, and
|
|
13
|
+
* - the SUGGESTER (`testFileExt`) recommended `.ts` from a `tsconfig.json`
|
|
14
|
+
* alone, never asking.
|
|
15
|
+
*
|
|
16
|
+
* On Node 20 — which this repo's own CI still runs — a project holding a
|
|
17
|
+
* `tsconfig.json` but no local `tsx` got told to write `foo.harness.ts`, and
|
|
18
|
+
* `vigiles test` then declined to run the very file it had asked for. A finding
|
|
19
|
+
* whose remedy the tool rejects is worse than no finding: the author does the work
|
|
20
|
+
* and is told no.
|
|
21
|
+
*
|
|
22
|
+
* Harness-agnostic on purpose: this is a fact about the Node process and the
|
|
23
|
+
* project's `node_modules`, not about Claude Code, so the harness-agnostic
|
|
24
|
+
* detectors may depend on it without reaching into an adapter.
|
|
25
|
+
*/
|
|
26
|
+
const node_fs_1 = require("node:fs");
|
|
27
|
+
const node_path_1 = require("node:path");
|
|
28
|
+
/** Detect TS-running capabilities for a project root. */
|
|
29
|
+
function detectNodeCaps(cwd) {
|
|
30
|
+
const tsx = (0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules/tsx/package.json")) ||
|
|
31
|
+
(0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, "node_modules/.bin/tsx"));
|
|
32
|
+
const stripTypes = process.allowedNodeEnvironmentFlags.has("--experimental-strip-types");
|
|
33
|
+
return { tsx, stripTypes };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Is there ANY path by which `vigiles test` could execute a TypeScript script
|
|
37
|
+
* here? The exact disjunction `interpreterArgs` branches on — one predicate, so
|
|
38
|
+
* the suggester and the runner cannot drift back apart.
|
|
39
|
+
*/
|
|
40
|
+
function canRunTypeScript(caps) {
|
|
41
|
+
return caps.tsx || caps.stripTypes;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=ts-runner-caps.js.map
|
package/dist/unit.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export * from "./harness-assert.js";
|
|
15
15
|
export { loadHook } from "./load-hook.js";
|
|
16
|
-
export { runHook, parseHookOutput, decideHook, propertyHook, } from "./run-hook.js";
|
|
17
|
-
export type { HookInput, HookOutput, HookRunResult, RunHookOptions, HookPropertyResult, } from "./run-hook.js";
|
|
16
|
+
export { runHook, parseHookOutput, decideHook, propertyHook, fileToolEvents, } from "./run-hook.js";
|
|
17
|
+
export type { HookInput, HookOutput, HookRunResult, RunHookOptions, HookPropertyResult, FileToolEventOptions, } from "./run-hook.js";
|
|
18
18
|
export { runScript } from "./run-script.js";
|
|
19
19
|
export type { RunScriptOptions, ScriptRunResult } from "./run-script.js";
|
|
20
20
|
export * from "./check.js";
|
package/dist/unit.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.formatGuardrailReport = exports.assertBlocksDisasters = exports.unblockedDisasters = exports.verifyGuardrail = exports.DISASTER_CATALOG = exports.hookFired = exports.runScript = exports.propertyHook = exports.decideHook = exports.parseHookOutput = exports.runHook = exports.loadHook = void 0;
|
|
17
|
+
exports.formatGuardrailReport = exports.assertBlocksDisasters = exports.unblockedDisasters = exports.verifyGuardrail = exports.DISASTER_CATALOG = exports.hookFired = exports.runScript = exports.fileToolEvents = exports.propertyHook = exports.decideHook = exports.parseHookOutput = exports.runHook = exports.loadHook = void 0;
|
|
18
18
|
/**
|
|
19
19
|
* `vigiles/unit` — the **no-capability** harness-testing surface.
|
|
20
20
|
*
|
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "runHook", { enumerable: true, get: function () {
|
|
|
39
39
|
Object.defineProperty(exports, "parseHookOutput", { enumerable: true, get: function () { return run_hook_js_1.parseHookOutput; } });
|
|
40
40
|
Object.defineProperty(exports, "decideHook", { enumerable: true, get: function () { return run_hook_js_1.decideHook; } });
|
|
41
41
|
Object.defineProperty(exports, "propertyHook", { enumerable: true, get: function () { return run_hook_js_1.propertyHook; } });
|
|
42
|
+
Object.defineProperty(exports, "fileToolEvents", { enumerable: true, get: function () { return run_hook_js_1.fileToolEvents; } });
|
|
42
43
|
// The PRIMITIVE beneath runHook: run any program, get what it did. A hook has a
|
|
43
44
|
// DECISION, a script has EFFECTS — two questions, so two result types.
|
|
44
45
|
var run_script_js_1 = require("./run-script.js");
|
package/hooks/eval-lock-nudge.sh
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# PostToolUse hook —
|
|
3
|
-
# surface or an *.eval.* script
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
2
|
+
# PostToolUse hook — the edit-time test reminders. After the agent edits a
|
|
3
|
+
# skill/agent surface or an *.eval.* script, inject a NON-BLOCKING nudge:
|
|
4
|
+
#
|
|
5
|
+
# 1. the surface has no test/eval at all, or has a deterministic harness but
|
|
6
|
+
# was never EVALUATED (nothing has measured that its description still
|
|
7
|
+
# fires). Reuses the `untested-skill` detector and points at the
|
|
8
|
+
# `test-harness` skill for the tier→API table. Before this, `untested-skill`
|
|
9
|
+
# only ran when a human typed `vigiles lint` — a rule that fires on request
|
|
10
|
+
# is prose, not policy.
|
|
11
|
+
# 2. committed eval locks may now be stale → re-run `vigiles eval --update`.
|
|
12
|
+
# Self-gating: silent until you've committed a lock.
|
|
13
|
+
#
|
|
14
|
+
# Never blocks the edit. Runs as its OWN PostToolUse entry so its stdout stays
|
|
15
|
+
# clean JSON. See docs/harness-testing.md and docs/rules/untested-skill.md.
|
|
8
16
|
|
|
9
17
|
set -uo pipefail
|
|
10
18
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "15.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "15.0.3",
|
|
4
|
+
"description": "Audit, test and measure the harness your AI agent runs on — grade your CLAUDE.md / AGENTS.md, skills, subagents and hooks, run them against a scripted model, and measure whether they actually fire.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: test-harness
|
|
3
3
|
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
|
4
|
-
description: Install vigiles and test a Claude Code harness — hooks, skills, settings, CLAUDE.md — by picking the right tier (unit / deterministic / eval) and writing a test that passes. Use
|
|
4
|
+
description: Install vigiles and test a Claude Code harness — hooks, skills, agents, settings, CLAUDE.md — by picking the right tier (unit / deterministic / eval) and writing a test that passes. Use to check that a hook fires or blocks, that a skill triggers, that injected context lands; or to observe a run — which tools it called, whether it stayed inside its declared allowed-tools, what files and side effects it produced, how to intercept a call without executing it.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Test the Claude Code **harness** — the hooks, skills, settings, and CLAUDE.md that
|
|
@@ -23,15 +23,79 @@ Match what you're testing to the cheapest tier that can answer it:
|
|
|
23
23
|
| "Is the hook actually **wired into** the assembled plugin and does it fire in a real session?" | **Deterministic** | free, no API key (real `claude` + scripted mock) | `runHarnessTest` + `scriptModel` |
|
|
24
24
|
| "Did the injected context (a SessionStart hook, a `/command`) actually **reach the model**?" | **Deterministic** | free, no API key | `runHarnessTest` → `trace.modelRequests` / `assertRequestContains` |
|
|
25
25
|
| "Does this skill's **description trigger** when it should (recall) **and stay quiet** when it shouldn't (precision)?" | **Eval** | **paid** (real model) | `measureTriggerRate` (+ `irrelevantPrompts`) → `assertTriggerRate({ min, maxFalsePositive })` |
|
|
26
|
+
| "Can I measure triggering on a **cheaper model** and trust it as a floor?" | **Eval** | **paid** (two runs) | `compareContainment(weak, strong)` → `formatContainment` |
|
|
26
27
|
| "Is this exact skill's **output any good**?" — absolute quality, no on/off baseline (the default for testing one skill) | **Eval** | **paid** (real model) | `measure({ checks: [judged(rubric)] })` → `assertRates({ min })` |
|
|
27
28
|
| "Does this harness change **move what the agent does**, _relative_ to off?" — A/B lift, regression, signal vs noise | **Eval** | **paid** (real model) | `runEval` (arms) + `assertSignificant` |
|
|
28
29
|
|
|
29
30
|
Most harness questions — block/allow, wired-in, context-landed — never need a
|
|
30
31
|
model. Only "does the model trigger / behave differently" needs the eval tier.
|
|
31
32
|
|
|
33
|
+
⚠️ **A trigger-rate of 0% on EVERY prompt is a wiring bug until proven otherwise.**
|
|
34
|
+
It reads like a verdict on the description, and three separate setup mistakes
|
|
35
|
+
produce it: a **bare** id in `fired` where the namespaced `<plugin>:<skill>` is
|
|
36
|
+
required; `pluginDir` where a loose `.claude/skills` needs **`skillsDir`**; and a
|
|
37
|
+
missing **`fixture`**, since a run starts in an empty directory and a prompt about
|
|
38
|
+
a file that isn't there is one the model is right to decline. Rule all three out
|
|
39
|
+
before reporting it. (A partial rate is a real number — don't second-guess it.)
|
|
40
|
+
|
|
41
|
+
**Don't tune against a cheaper model until you've checked it's actually a floor.**
|
|
42
|
+
`compareContainment(weak, strong)` answers that: it reports prompts that fired on
|
|
43
|
+
the weak model but NOT the strong one, and each one means the weak model is not a
|
|
44
|
+
lower bound but a _different router_. Prompts that fired only on the strong model
|
|
45
|
+
are expected and are not a failure. Measured once (21 skills, 84 prompts, haiku
|
|
46
|
+
vs sonnet): **3 weak-only, and one skill higher on haiku** — so containment is
|
|
47
|
+
not established, which is why the floor stays.
|
|
48
|
+
|
|
32
49
|
If the unit and deterministic tiers can both answer it, **prefer unit**: it's
|
|
33
50
|
faster and reaches events the deterministic mock can't drive.
|
|
34
51
|
|
|
52
|
+
## Step 0.4 — Observing a run (what it CALLED, WROTE, and TOUCHED)
|
|
53
|
+
|
|
54
|
+
The table above is keyed on the harness _surface_ under test. Half the real
|
|
55
|
+
questions are keyed on the **observation** instead — "what did this skill
|
|
56
|
+
actually do?" — and they have answers already. Reach for these before building
|
|
57
|
+
anything; every one of them ships today.
|
|
58
|
+
|
|
59
|
+
| The question you're actually asking | Use |
|
|
60
|
+
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
61
|
+
| Which tools did it call, and with what arguments? | `trace.toolCalls` · `tool` / `toolWith` checks · `parseToolCalls` (`vigiles/e2e`) |
|
|
62
|
+
| Did it call a tool it must not? | `notTool(name)` |
|
|
63
|
+
| Did it call **only** tools from a known set? | `onlyTools([...])` — the white-list, symmetric to `assertWroteOnly` |
|
|
64
|
+
| Did it stay inside the `allowed-tools` its own frontmatter declares? | `skillContract(dir).surface` — builds that check FROM the declaration |
|
|
65
|
+
| What files did the run write? | `filesWritten` · `wrote(path)` / `didNotWrite(path)` · `r.file(path)` |
|
|
66
|
+
| Did it write **only** where it was supposed to? | `assertWroteOnly([...])` / `assertNoWrite()` — needs `{ sandbox: "auto" }` |
|
|
67
|
+
| Run a tool call but **don't let it execute** — capture the args instead | the `interceptTools` option on `measure` / `runEval` (a `ToolIntercept[]`) |
|
|
68
|
+
| Did a subagent do it, and which one? | `subagent(name, [...])` · `SubagentTrace` |
|
|
69
|
+
| Was it an MCP tool? | `mcp(server, toolName)` |
|
|
70
|
+
| Assert the whole effect boundary deterministically | `assertChecks` + the checks above (see `examples/harness/effect-boundary.harness.mjs`) |
|
|
71
|
+
|
|
72
|
+
`interceptTools` is the one worth knowing about, because it is not obvious it
|
|
73
|
+
exists: it denies a tool its **real execution** via an auto-wired `PreToolUse`
|
|
74
|
+
hook while still recording the call and its arguments into the trace. That is
|
|
75
|
+
how you test a skill that would otherwise mutate a real external service — a
|
|
76
|
+
calendar, an upload — without mocking anything yourself.
|
|
77
|
+
|
|
78
|
+
**Verify a skill against its own declaration** with `skillContract` — it reads
|
|
79
|
+
the `allowed-tools:` the skill already claims and hands back ready checks, so
|
|
80
|
+
the claim is verified instead of restated:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { skillContract, assertChecks } from "vigiles/testing";
|
|
84
|
+
|
|
85
|
+
const c = skillContract(".claude/skills/my-skill");
|
|
86
|
+
assertChecks(trace, [c.activation, ...c.surface]);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Two of its states are **findings**, not clean bills, and their `surface` check
|
|
90
|
+
fails rather than passing on nothing: `undeclared` (no `allowed-tools:` line, so
|
|
91
|
+
the skill inherits _every_ tool) and `malformed` (frontmatter that isn't valid
|
|
92
|
+
YAML, so a strict loader reads no contract at all — one unquoted `: ` does it).
|
|
93
|
+
|
|
94
|
+
⚠️ **What is still NOT checked.** `onlyTools` compares tool _names_, so a narrow
|
|
95
|
+
allowlist entry like `Bash(node scripts/x.mjs:*)` is satisfied by any `Bash` call
|
|
96
|
+
at all. Scope inside a tool is unverified — say so rather than implying the
|
|
97
|
+
assertion is total.
|
|
98
|
+
|
|
35
99
|
## Step 0.5 — Set honest expectations (what's testable, and at what cost)
|
|
36
100
|
|
|
37
101
|
Be explicit with the user about which bucket each surface falls into — never let
|
|
@@ -122,10 +186,12 @@ mock model, assert the hook fired (or the context landed):
|
|
|
122
186
|
```ts
|
|
123
187
|
import {
|
|
124
188
|
runHarnessTest,
|
|
125
|
-
scriptModel,
|
|
126
189
|
assertHookFired,
|
|
127
190
|
assertRequestContains,
|
|
128
191
|
} from "vigiles/testing";
|
|
192
|
+
// `scriptModel` is the Claude-Code TRANSPORT, deliberately not re-exported from
|
|
193
|
+
// the harness-agnostic `vigiles/testing` — import it from the harness package:
|
|
194
|
+
import { scriptModel } from "vigiles/claude-code";
|
|
129
195
|
|
|
130
196
|
const r = await runHarnessTest({
|
|
131
197
|
pluginDir: "./", // or { settings: { hooks: {...} } }
|