vigiles 4.0.2 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/adapter-registry.d.ts +45 -1
- package/dist/adapter-registry.js +78 -3
- package/dist/adapters/claude-code/agent-runtime.d.ts +1 -1
- package/dist/adapters/claude-code/agent-runtime.js +1 -1
- package/dist/arg-match.d.ts +28 -0
- package/dist/arg-match.js +61 -0
- package/dist/check.d.ts +52 -1
- package/dist/check.js +121 -0
- package/dist/claude-code.d.ts +1 -0
- package/dist/claude-code.js +11 -0
- package/dist/cli.js +211 -35
- package/dist/core/types.d.ts +27 -12
- package/dist/core/validate.js +14 -3
- package/dist/eval-cache.d.ts +49 -1
- package/dist/eval-cache.js +84 -4
- package/dist/eval.d.ts +243 -14
- package/dist/eval.js +540 -28
- package/dist/integration.d.ts +15 -10
- package/dist/integration.js +27 -10
- package/dist/linting.d.ts +4 -3
- package/dist/linting.js +4 -3
- package/dist/setup-plan.d.ts +11 -0
- package/dist/setup-plan.js +29 -0
- package/dist/skill-harness.d.ts +25 -0
- package/dist/skill-harness.js +40 -0
- package/dist/test-coverage.d.ts +11 -12
- package/dist/test-coverage.js +14 -19
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +24 -2
- package/dist/tool-intercept.d.ts +101 -0
- package/dist/tool-intercept.js +165 -0
- package/dist/tool-stub.d.ts +35 -0
- package/dist/tool-stub.js +92 -0
- package/package.json +11 -17
- package/skills/test-harness/SKILL.md +56 -3
package/dist/cli.js
CHANGED
|
@@ -21,9 +21,9 @@ const types_js_1 = require("./core/types.js");
|
|
|
21
21
|
const test_coverage_js_1 = require("./test-coverage.js");
|
|
22
22
|
const scan_js_1 = require("./scan.js");
|
|
23
23
|
const adapter_registry_js_1 = require("./adapter-registry.js");
|
|
24
|
+
const skill_harness_js_1 = require("./skill-harness.js");
|
|
24
25
|
const leaderboard_js_1 = require("./leaderboard.js");
|
|
25
26
|
const compile_js_1 = require("./core/compile.js");
|
|
26
|
-
const dialect_js_1 = require("./adapters/claude-code/dialect.js");
|
|
27
27
|
const proofs_js_1 = require("./core/proofs.js");
|
|
28
28
|
const inline_js_1 = require("./core/inline.js");
|
|
29
29
|
const frontmatter_js_1 = require("./core/frontmatter.js");
|
|
@@ -32,6 +32,7 @@ const compose_js_1 = require("./core/compose.js");
|
|
|
32
32
|
const compile_generator_js_1 = require("./core/compile-generator.js");
|
|
33
33
|
const action_gate_js_1 = require("./action-gate.js");
|
|
34
34
|
const agent_runtime_js_1 = require("./adapters/claude-code/agent-runtime.js");
|
|
35
|
+
const tool_intercept_js_1 = require("./tool-intercept.js");
|
|
35
36
|
const refs_js_1 = require("./core/refs.js");
|
|
36
37
|
const mcp_js_1 = require("./core/mcp.js");
|
|
37
38
|
const skill_runtime_js_1 = require("./adapters/claude-code/skill-runtime.js");
|
|
@@ -140,12 +141,12 @@ function compileGeneratorSkillToFile(specPath, source) {
|
|
|
140
141
|
return false;
|
|
141
142
|
}
|
|
142
143
|
/** Compile a ClaudeSpec → its primary + any additional targets. */
|
|
143
|
-
function compileClaudeToFile(spec, specPath, config) {
|
|
144
|
+
function compileClaudeToFile(spec, specPath, config, dialect) {
|
|
144
145
|
const basePath = process.cwd();
|
|
145
146
|
const { markdown, errors, linterResults, targets } = (0, compile_js_1.compileClaude)(spec, {
|
|
146
147
|
basePath,
|
|
147
148
|
specFile: specPath,
|
|
148
|
-
dialect
|
|
149
|
+
dialect,
|
|
149
150
|
maxRules: config.maxRules,
|
|
150
151
|
maxTokens: config.maxTokens,
|
|
151
152
|
maxSectionLines: config.maxSectionLines,
|
|
@@ -175,15 +176,54 @@ function compileClaudeToFile(spec, specPath, config) {
|
|
|
175
176
|
console.log(` ${String(Object.keys(spec.rules).length)} rules (${String(linterCount)} linter-verified)`);
|
|
176
177
|
return true;
|
|
177
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Branch 3 of the mirror story (research/multi-harness-compile.md): when a repo
|
|
181
|
+
* declares ≥2 harnesses and nothing else fans out the instruction file, write a
|
|
182
|
+
* byte-identical copy to each other harness's instruction file (e.g. CLAUDE.md →
|
|
183
|
+
* AGENTS.md). A copy — not a symlink — because it works everywhere and carries
|
|
184
|
+
* the source's embedded integrity hash by construction, so a hand-edit of the
|
|
185
|
+
* mirror trips the existing `integrity` check. Never fights a sync tool or
|
|
186
|
+
* clobbers a target that owns its own spec.
|
|
187
|
+
*/
|
|
188
|
+
function writeInstructionMirrors(primaryOutput, harnesses) {
|
|
189
|
+
if (harnesses.length < 2)
|
|
190
|
+
return;
|
|
191
|
+
const cwd = process.cwd();
|
|
192
|
+
// A sync tool (Ruler/rulesync) owns fan-out — don't fight it.
|
|
193
|
+
if ((0, compose_js_1.detectSyncTools)(cwd).length > 0)
|
|
194
|
+
return;
|
|
195
|
+
const primaryName = (0, node_path_1.basename)(primaryOutput);
|
|
196
|
+
const primaryAbs = (0, node_path_1.resolve)(cwd, primaryOutput);
|
|
197
|
+
if (!(0, node_fs_1.existsSync)(primaryAbs))
|
|
198
|
+
return;
|
|
199
|
+
const content = (0, node_fs_1.readFileSync)(primaryAbs, "utf-8");
|
|
200
|
+
for (const name of harnesses) {
|
|
201
|
+
const adapter = (0, adapter_registry_js_1.getAdapter)(name);
|
|
202
|
+
if (!adapter)
|
|
203
|
+
continue;
|
|
204
|
+
const target = adapter.layout.instructionFile;
|
|
205
|
+
if (target === primaryName)
|
|
206
|
+
continue; // the file we just compiled
|
|
207
|
+
// Never clobber a target that has its own spec (a genuinely separate file).
|
|
208
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.resolve)(cwd, `${target}.spec.ts`)))
|
|
209
|
+
continue;
|
|
210
|
+
const targetAbs = (0, node_path_1.resolve)(cwd, target);
|
|
211
|
+
if ((0, node_fs_1.existsSync)(targetAbs) && (0, node_fs_1.readFileSync)(targetAbs, "utf-8") === content) {
|
|
212
|
+
continue; // already byte-identical
|
|
213
|
+
}
|
|
214
|
+
(0, node_fs_1.writeFileSync)(targetAbs, content);
|
|
215
|
+
console.log(` ↳ mirrored ${primaryName} → ${target} (byte-identical)`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
178
218
|
/** Compile a declarative SkillSpec → SKILL.md. */
|
|
179
|
-
function compileSkillToFile(spec, specPath) {
|
|
219
|
+
function compileSkillToFile(spec, specPath, dialect) {
|
|
180
220
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
181
221
|
const { markdown, errors } = (0, compile_js_1.compileSkill)(spec, {
|
|
182
222
|
basePath: process.cwd(),
|
|
183
223
|
specFile: specPath,
|
|
184
|
-
//
|
|
224
|
+
// The SKILL.md frontmatter profile comes from the resolved harness — a Codex
|
|
185
225
|
// repo gets a minimal (name + description) SKILL.md; CC gets the full set.
|
|
186
|
-
dialect
|
|
226
|
+
dialect,
|
|
187
227
|
});
|
|
188
228
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
189
229
|
if (errors.length === 0) {
|
|
@@ -195,12 +235,12 @@ function compileSkillToFile(spec, specPath) {
|
|
|
195
235
|
return false;
|
|
196
236
|
}
|
|
197
237
|
/** Compile a subagent spec → agents/<name>.md (with its result-contract section). */
|
|
198
|
-
function compileAgentToFile(spec, specPath) {
|
|
238
|
+
function compileAgentToFile(spec, specPath, dialect) {
|
|
199
239
|
const outputPath = specPath.replace(/\.spec\.ts$/, "");
|
|
200
240
|
const { markdown, errors } = (0, compile_js_1.compileAgent)(spec, {
|
|
201
241
|
basePath: process.cwd(),
|
|
202
242
|
specFile: specPath,
|
|
203
|
-
dialect
|
|
243
|
+
dialect,
|
|
204
244
|
});
|
|
205
245
|
(0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(process.cwd(), outputPath), markdown);
|
|
206
246
|
if (errors.length === 0) {
|
|
@@ -241,8 +281,21 @@ async function collectAgentNames() {
|
|
|
241
281
|
}
|
|
242
282
|
return names;
|
|
243
283
|
}
|
|
244
|
-
async function compile(specPaths, config) {
|
|
284
|
+
async function compile(specPaths, config, opts = {}) {
|
|
245
285
|
let allValid = true;
|
|
286
|
+
// Parse the declared harness set ONCE (alias-normalized) and feed both the
|
|
287
|
+
// dialect pick and the mirror from it — no re-parsing, no cwd-sniffing in the
|
|
288
|
+
// helpers. A loud notice (never a silent guess) on a multi-harness or
|
|
289
|
+
// ambiguous-detection pick.
|
|
290
|
+
const declaredHarnesses = (0, adapter_registry_js_1.normalizeHarnessList)(config.harness);
|
|
291
|
+
const selection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
292
|
+
root: process.cwd(),
|
|
293
|
+
flag: opts.harnessFlag,
|
|
294
|
+
configHarness: declaredHarnesses,
|
|
295
|
+
});
|
|
296
|
+
if (selection.kind === "notice")
|
|
297
|
+
console.log(`⚠ ${selection.notice}`);
|
|
298
|
+
const dialect = selection.adapter.dialect;
|
|
246
299
|
// Resolved lazily on the first railway spec — every delegate() target is
|
|
247
300
|
// checked against the agents defined anywhere in the project.
|
|
248
301
|
let knownAgents = null;
|
|
@@ -262,15 +315,36 @@ async function compile(specPaths, config) {
|
|
|
262
315
|
continue;
|
|
263
316
|
}
|
|
264
317
|
if (spec._specType === "claude") {
|
|
265
|
-
|
|
318
|
+
// Spec-target disambiguation: a CLAUDE.md.spec.ts is a claude-code file, an
|
|
319
|
+
// AGENTS.md.spec.ts a codex one — the strongest dialect signal for THIS
|
|
320
|
+
// spec. The flag still overrides; absent one, the spec's own target wins
|
|
321
|
+
// over config/detect. (Skill/agent targets don't name a harness, so they
|
|
322
|
+
// keep the run-level dialect.)
|
|
323
|
+
const targetFile = (0, node_path_1.basename)(specPath).replace(/\.spec\.ts$/, "");
|
|
324
|
+
const specDialect = opts.harnessFlag === undefined
|
|
325
|
+
? ((0, adapter_registry_js_1.adapterForInstructionFile)(targetFile)?.dialect ?? dialect)
|
|
326
|
+
: dialect;
|
|
327
|
+
if (compileClaudeToFile(spec, specPath, config, specDialect)) {
|
|
328
|
+
writeInstructionMirrors(specPath.replace(/\.spec\.ts$/, ""), declaredHarnesses);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
266
331
|
allValid = false;
|
|
332
|
+
}
|
|
267
333
|
}
|
|
268
334
|
else if (spec._specType === "skill") {
|
|
269
|
-
|
|
335
|
+
// Cross-harness verify: flag CC-only frontmatter a declared minimal-profile
|
|
336
|
+
// harness (Codex/OpenCode) would silently drop.
|
|
337
|
+
const forHarnesses = declaredHarnesses.length > 0
|
|
338
|
+
? declaredHarnesses
|
|
339
|
+
: [selection.adapter.name];
|
|
340
|
+
for (const w of (0, skill_harness_js_1.skillFrontmatterDropWarnings)(spec, forHarnesses)) {
|
|
341
|
+
console.log(`⚠ ${w}`);
|
|
342
|
+
}
|
|
343
|
+
if (!compileSkillToFile(spec, specPath, dialect))
|
|
270
344
|
allValid = false;
|
|
271
345
|
}
|
|
272
346
|
else if (spec._specType === "agent") {
|
|
273
|
-
if (!compileAgentToFile(spec, specPath))
|
|
347
|
+
if (!compileAgentToFile(spec, specPath, dialect))
|
|
274
348
|
allValid = false;
|
|
275
349
|
}
|
|
276
350
|
else if (spec._specType === "railway") {
|
|
@@ -836,8 +910,8 @@ async function runLint(restArgs, flags, config) {
|
|
|
836
910
|
}
|
|
837
911
|
}
|
|
838
912
|
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
839
|
-
// eval. Warning by default (a nudge, exit 0); set rules.untested-
|
|
840
|
-
// "error" to gate CI. See src/test-coverage.ts and docs/rules
|
|
913
|
+
// eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
|
|
914
|
+
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
841
915
|
const untested = checkUntestedSurfaces(config, silent);
|
|
842
916
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
843
917
|
// is to validate every ref; illustrative blocks opt out via
|
|
@@ -851,6 +925,16 @@ async function runLint(restArgs, flags, config) {
|
|
|
851
925
|
console.log(` ${line}`);
|
|
852
926
|
}
|
|
853
927
|
}
|
|
928
|
+
// Per-line GitHub annotations for each broken doc ref — each carries file+line,
|
|
929
|
+
// so GitHub renders it INLINE on the PR diff (not just in the summary blob).
|
|
930
|
+
// Previously this check reported to stdout only; the inline/spec checks already
|
|
931
|
+
// annotate per-line, so this closes the gap that left doc-ref findings invisible
|
|
932
|
+
// on the PR. CI-only (isGitHubActions); skipped under --json/--summary.
|
|
933
|
+
if (isGitHubActions() && !silent) {
|
|
934
|
+
for (const e of docRefReport.errors) {
|
|
935
|
+
ghAnnotate("error", `${e.kind}("${e.value}") — ${e.message}`, e.file, e.line);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
854
938
|
// 9. Verify code-shaped symbol references live (see src/refs.ts).
|
|
855
939
|
const symbolRefErrors = verifyMarkdownSymbols(files, silent);
|
|
856
940
|
// 10. Verify `vigiles:mcp server#tool` marks against live MCP servers
|
|
@@ -1735,16 +1819,44 @@ async function setup(args) {
|
|
|
1735
1819
|
console.log("\n Non-markdown agent configs detected. Use a sync tool to convert:");
|
|
1736
1820
|
console.log(" npm install -D rule-porter");
|
|
1737
1821
|
}
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1822
|
+
// Project config — record the harness(es) so compile/lint select the dialect
|
|
1823
|
+
// deterministically (no cwd sniffing), plus strict rule severities on --strict.
|
|
1824
|
+
writeProjectConfig({ harnesses, strict, written });
|
|
1825
|
+
printSetupSummary({ plan, strict, targets, needsMigration, written });
|
|
1826
|
+
}
|
|
1827
|
+
/** Canonical, de-duplicated harness list → a config value (string when one). */
|
|
1828
|
+
function harnessConfigValue(harnesses) {
|
|
1829
|
+
const canon = [...new Set(harnesses.map(adapter_registry_js_1.normalizeHarnessName))];
|
|
1830
|
+
return canon.length === 1 ? canon[0] : canon;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Merge the resolved harness(es) (and strict rule severities) into
|
|
1834
|
+
* `.vigilesrc.json` without clobbering existing keys — an existing `harness`
|
|
1835
|
+
* stays, a missing one is added, a malformed file is left untouched.
|
|
1836
|
+
*/
|
|
1837
|
+
function writeProjectConfig(opts) {
|
|
1838
|
+
const configPath = (0, node_path_1.resolve)(process.cwd(), ".vigilesrc.json");
|
|
1839
|
+
const existed = (0, node_fs_1.existsSync)(configPath);
|
|
1840
|
+
let existing = {};
|
|
1841
|
+
if (existed) {
|
|
1842
|
+
try {
|
|
1843
|
+
existing = JSON.parse((0, node_fs_1.readFileSync)(configPath, "utf-8"));
|
|
1844
|
+
}
|
|
1845
|
+
catch {
|
|
1846
|
+
return; // user-owned malformed config — never clobber it
|
|
1745
1847
|
}
|
|
1746
1848
|
}
|
|
1747
|
-
|
|
1849
|
+
const merged = (0, setup_plan_js_1.mergeProjectConfig)(existing, {
|
|
1850
|
+
harness: harnessConfigValue(opts.harnesses),
|
|
1851
|
+
strict: opts.strict,
|
|
1852
|
+
});
|
|
1853
|
+
if (!merged)
|
|
1854
|
+
return;
|
|
1855
|
+
(0, node_fs_1.writeFileSync)(configPath, JSON.stringify(merged, null, 2) + "\n");
|
|
1856
|
+
console.log(`✓ ${existed ? "Updated" : "Created"} .vigilesrc.json`);
|
|
1857
|
+
if (!opts.written.includes(".vigilesrc.json")) {
|
|
1858
|
+
opts.written.push(".vigilesrc.json");
|
|
1859
|
+
}
|
|
1748
1860
|
}
|
|
1749
1861
|
// ---------------------------------------------------------------------------
|
|
1750
1862
|
// Strengthen: guidance() → enforce() suggestions
|
|
@@ -1776,29 +1888,47 @@ function checkIntegrityForFiles(files, severity, silent) {
|
|
|
1776
1888
|
return severity === "error" ? errorCount : 0;
|
|
1777
1889
|
}
|
|
1778
1890
|
/**
|
|
1779
|
-
* Apply the `untested-
|
|
1780
|
-
* eval (see src/test-coverage.ts).
|
|
1781
|
-
*
|
|
1782
|
-
* "error" fails (exit 2)
|
|
1891
|
+
* Apply the per-kind `untested-skill` / `untested-agent` / `untested-hook` rules:
|
|
1892
|
+
* find skills/agents/hooks with no test or eval (see src/test-coverage.ts). Each
|
|
1893
|
+
* kind is gated by its OWN rule severity — a kind set to `false` is not scanned;
|
|
1894
|
+
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
1895
|
+
* untested count plus the severity-gated error count.
|
|
1783
1896
|
*/
|
|
1784
1897
|
function checkUntestedSurfaces(config, silent) {
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1898
|
+
const rules = config?.rules;
|
|
1899
|
+
const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
|
|
1900
|
+
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-agent"]);
|
|
1901
|
+
const hookSev = (0, types_js_1.ruleSeverity)(rules?.["untested-hook"]);
|
|
1902
|
+
if (!skillSev && !agentSev && !hookSev)
|
|
1787
1903
|
return { untested: 0, errors: 0 };
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1904
|
+
const sevFor = (kind) => kind === "skill" ? skillSev : kind === "agent" ? agentSev : hookSev;
|
|
1905
|
+
// Test-discovery options (testGlobs/exclude) are shared; merge them from
|
|
1906
|
+
// whichever of the three rules carries them.
|
|
1907
|
+
const opts = {
|
|
1908
|
+
...(0, types_js_1.ruleOptions)(rules?.["untested-skill"]),
|
|
1909
|
+
...(0, types_js_1.ruleOptions)(rules?.["untested-agent"]),
|
|
1910
|
+
...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
|
|
1911
|
+
};
|
|
1912
|
+
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
1913
|
+
basePath: process.cwd(),
|
|
1914
|
+
skills: skillSev !== false,
|
|
1915
|
+
agents: agentSev !== false,
|
|
1916
|
+
hooks: hookSev !== false,
|
|
1917
|
+
testGlobs: opts.testGlobs,
|
|
1918
|
+
exclude: opts.exclude,
|
|
1919
|
+
});
|
|
1790
1920
|
if (!silent) {
|
|
1791
1921
|
console.log("\nUntested surfaces:\n");
|
|
1792
1922
|
for (const line of (0, test_coverage_js_1.formatUntestedReport)(report).split("\n")) {
|
|
1793
1923
|
console.log(` ${line}`);
|
|
1794
1924
|
}
|
|
1795
1925
|
for (const s of report.untested) {
|
|
1796
|
-
ghAnnotate(
|
|
1926
|
+
ghAnnotate(sevFor(s.kind) === "error" ? "error" : "warning", `${s.kind} ${s.path} ships without a test or eval`, s.path);
|
|
1797
1927
|
}
|
|
1798
1928
|
}
|
|
1799
1929
|
return {
|
|
1800
1930
|
untested: report.untested.length,
|
|
1801
|
-
errors:
|
|
1931
|
+
errors: report.untested.filter((s) => sevFor(s.kind) === "error").length,
|
|
1802
1932
|
};
|
|
1803
1933
|
}
|
|
1804
1934
|
/**
|
|
@@ -1980,6 +2110,18 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
1980
2110
|
// Harness/eval scripts may be authored in JS or TS (see run-scripts.ts).
|
|
1981
2111
|
const defaultGlob = (0, run_scripts_js_1.scriptGlob)(kind === "test" ? "harness" : "eval");
|
|
1982
2112
|
const files = (0, run_scripts_js_1.discoverScripts)(restArgs, defaultGlob, cwd);
|
|
2113
|
+
// `--min=N`: a CI gate asserts at least N scripts actually RAN — so a bad path,
|
|
2114
|
+
// a renamed file, or a glob that matched nothing fails LOUD instead of passing
|
|
2115
|
+
// green with zero evals executed. Default 0 (off) keeps local runs ergonomic.
|
|
2116
|
+
const minFlag = args.find((a) => a.startsWith("--min="));
|
|
2117
|
+
const minRequired = minFlag
|
|
2118
|
+
? Math.max(0, Number.parseInt(minFlag.split("=")[1] ?? "", 10) || 0)
|
|
2119
|
+
: 0;
|
|
2120
|
+
if (files.length < minRequired) {
|
|
2121
|
+
console.error(`✗ vigiles ${kind}: --min=${String(minRequired)} but only ${String(files.length)} ${kind} file(s) matched — ` +
|
|
2122
|
+
"evals never executed (check the paths/globs, or that the run was reached).");
|
|
2123
|
+
process.exit(1);
|
|
2124
|
+
}
|
|
1983
2125
|
if (files.length === 0) {
|
|
1984
2126
|
console.log(`No ${defaultGlob} files found.`);
|
|
1985
2127
|
return;
|
|
@@ -1990,6 +2132,10 @@ function handleRunScripts(kind, args, restArgs) {
|
|
|
1990
2132
|
if (kind === "test" && !(0, harness_test_js_1.claudeAvailable)()) {
|
|
1991
2133
|
console.log("ℹ `claude` CLI not found — unit-tier tests run; tests that need it report SKIPPED.\n");
|
|
1992
2134
|
}
|
|
2135
|
+
// `--trials=N` (a run knob: cost/precision, doesn't change WHAT is measured) is
|
|
2136
|
+
// forwarded to scripts via env. The MODEL is deliberately NOT a CLI/env knob —
|
|
2137
|
+
// it's part of the measurement definition, so it belongs in the spec
|
|
2138
|
+
// (`model` / `minModel`), version-controlled, not a hidden override.
|
|
1993
2139
|
const trialsFlag = args.find((a) => a.startsWith("--trials="));
|
|
1994
2140
|
const env = {};
|
|
1995
2141
|
if (trialsFlag)
|
|
@@ -2016,7 +2162,7 @@ function printUsage(command) {
|
|
|
2016
2162
|
console.log(" vigiles compile [files...] Compile .spec.ts → .md");
|
|
2017
2163
|
console.log(" vigiles lint [files...] Verify references, find gaps in instruction files");
|
|
2018
2164
|
console.log(" vigiles test [files...] Run *.harness.mjs deterministic harness tests");
|
|
2019
|
-
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N)");
|
|
2165
|
+
console.log(" vigiles eval [files...] Run *.eval.mjs real-model harness evals (--trials=N, --min=N, --no-skip)");
|
|
2020
2166
|
console.log("");
|
|
2021
2167
|
console.log("Examples:");
|
|
2022
2168
|
console.log(" vigiles init Auto-detect project, create specs, wire CI");
|
|
@@ -2130,7 +2276,7 @@ function skillStartCommand(target) {
|
|
|
2130
2276
|
* PreToolUse-hook entrypoint: enforce the active subagent's allowed-tools
|
|
2131
2277
|
* contract. Reads the tool event on stdin, parses the active agent's compiled
|
|
2132
2278
|
* `.md` tool rail, and blocks (exit 2 + reason on stderr) any tool outside it —
|
|
2133
|
-
* the deterministic boundary `tools:` alone can't provide (Claude Code #
|
|
2279
|
+
* the deterministic boundary `tools:` alone can't provide (Claude Code #4740/#21460, SDK #172).
|
|
2134
2280
|
*/
|
|
2135
2281
|
function agentHookCommand() {
|
|
2136
2282
|
let raw = "";
|
|
@@ -2155,6 +2301,30 @@ function agentHookCommand() {
|
|
|
2155
2301
|
process.exit(2);
|
|
2156
2302
|
}
|
|
2157
2303
|
}
|
|
2304
|
+
/**
|
|
2305
|
+
* `vigiles intercept-tool-hook` — the PreToolUse interception hook for the
|
|
2306
|
+
* tool-call spy. Reads the intercept list from `VIGILES_INTERCEPT_TOOLS`, decides
|
|
2307
|
+
* whether the called tool should be intercepted, and if so denies the real
|
|
2308
|
+
* execution (exit 2) with a block message — the call is intercepted (prevented),
|
|
2309
|
+
* NOT executed. Allowing (return) lets the tool run for real. The model still
|
|
2310
|
+
* emits the `tool_use`, so its arguments land in the Trace for `toolWith` /
|
|
2311
|
+
* `notTool` to assert on. See src/tool-intercept.ts.
|
|
2312
|
+
*/
|
|
2313
|
+
function interceptToolHookCommand() {
|
|
2314
|
+
let raw = "";
|
|
2315
|
+
try {
|
|
2316
|
+
raw = (0, node_fs_1.readFileSync)(0, "utf-8");
|
|
2317
|
+
}
|
|
2318
|
+
catch {
|
|
2319
|
+
/* no stdin */
|
|
2320
|
+
}
|
|
2321
|
+
const intercepts = (0, tool_intercept_js_1.parseIntercepts)(process.env[tool_intercept_js_1.INTERCEPT_TOOLS_ENV] ?? "");
|
|
2322
|
+
const decision = (0, tool_intercept_js_1.interceptHookDecision)(raw, intercepts);
|
|
2323
|
+
if (decision.intercept) {
|
|
2324
|
+
console.error(decision.denyReason);
|
|
2325
|
+
process.exit(2);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2158
2328
|
/** Mark a subagent active so the PreToolUse hook enforces its tool contract. */
|
|
2159
2329
|
function agentStartCommand(target) {
|
|
2160
2330
|
if (!target) {
|
|
@@ -2188,6 +2358,9 @@ function handleSkillCommand(command, restArgs) {
|
|
|
2188
2358
|
case "agent-hook":
|
|
2189
2359
|
agentHookCommand();
|
|
2190
2360
|
return true;
|
|
2361
|
+
case "intercept-tool-hook":
|
|
2362
|
+
interceptToolHookCommand();
|
|
2363
|
+
return true;
|
|
2191
2364
|
case "action-hook":
|
|
2192
2365
|
actionHookCommand();
|
|
2193
2366
|
return true;
|
|
@@ -2367,7 +2540,10 @@ async function main() {
|
|
|
2367
2540
|
console.log("Run `vigiles init` to create one.");
|
|
2368
2541
|
process.exit(0);
|
|
2369
2542
|
}
|
|
2370
|
-
const
|
|
2543
|
+
const harnessFlag = args
|
|
2544
|
+
.find((a) => a.startsWith("--harness="))
|
|
2545
|
+
?.slice("--harness=".length);
|
|
2546
|
+
const valid = await compile(specs, config, { harnessFlag });
|
|
2371
2547
|
console.log("");
|
|
2372
2548
|
if (valid) {
|
|
2373
2549
|
console.log("Compilation complete.");
|
package/dist/core/types.d.ts
CHANGED
|
@@ -69,16 +69,13 @@ export interface OrphansConfig {
|
|
|
69
69
|
*/
|
|
70
70
|
exclude?: readonly string[];
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Shared options for the per-kind untested-* rules (`untested-skill` /
|
|
74
|
+
* `untested-agent` / `untested-hook`). Which kinds are scanned is controlled by
|
|
75
|
+
* each rule's severity (set a rule to `false` to skip that kind), so only the
|
|
76
|
+
* test-discovery knobs live here.
|
|
77
|
+
*/
|
|
73
78
|
export interface TestCoverageConfig {
|
|
74
|
-
/** Scan skills. Default true. */
|
|
75
|
-
skills?: boolean;
|
|
76
|
-
/** Scan subagents. Default true. */
|
|
77
|
-
agents?: boolean;
|
|
78
|
-
/** Scan hook scripts referenced from plugin.json / settings.json. Default true. */
|
|
79
|
-
hooks?: boolean;
|
|
80
|
-
/** Require a test for user-invoked (disable-model-invocation) skills. Default false. */
|
|
81
|
-
includeUserInvokedSkills?: boolean;
|
|
82
79
|
/** Globs of test files that count as coverage. */
|
|
83
80
|
testGlobs?: readonly string[];
|
|
84
81
|
/** Extra ignore globs. */
|
|
@@ -87,14 +84,22 @@ export interface TestCoverageConfig {
|
|
|
87
84
|
export interface RulesConfig {
|
|
88
85
|
/** Require .spec.ts for CLAUDE.md / AGENTS.md. Default: "warn". */
|
|
89
86
|
"require-spec"?: RuleSeverity;
|
|
90
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Skills are legitimately hand-written; use `untested-skill`
|
|
89
|
+
* ("every skill ships with a test/eval") instead. Default: false (off). The
|
|
90
|
+
* check still runs if you set this explicitly.
|
|
91
|
+
*/
|
|
91
92
|
"require-skill-spec"?: RuleSeverity;
|
|
92
93
|
/** Detect hand-edits to compiled markdown via SHA-256 hash. Default: "warn". */
|
|
93
94
|
integrity?: RuleSeverity;
|
|
94
95
|
/** Enforce minimum spec coverage thresholds. Default: false. ESLint-style: ["warn", { scripts: 50 }]. */
|
|
95
96
|
coverage?: RuleWithOptions<CoverageThresholds>;
|
|
96
|
-
/** Flag
|
|
97
|
-
"untested-
|
|
97
|
+
/** Flag a skill (SKILL.md) that ships with no test or eval. Default: "warn". */
|
|
98
|
+
"untested-skill"?: RuleWithOptions<TestCoverageConfig>;
|
|
99
|
+
/** Flag a subagent (agents/*.md) that ships with no test or eval. Default: "warn". */
|
|
100
|
+
"untested-agent"?: RuleWithOptions<TestCoverageConfig>;
|
|
101
|
+
/** Flag a hook script that ships with no test or eval. Default: "warn". */
|
|
102
|
+
"untested-hook"?: RuleWithOptions<TestCoverageConfig>;
|
|
98
103
|
/**
|
|
99
104
|
* Nudge (or block) when an instruction file has code-shaped references that
|
|
100
105
|
* aren't expressed as vigiles marks (so the lint can't verify them), or a
|
|
@@ -127,6 +132,16 @@ export interface VigilesConfig {
|
|
|
127
132
|
}>;
|
|
128
133
|
/** Orphan-docs check configuration. Include/exclude globs, tsconfig-style. */
|
|
129
134
|
orphans?: OrphansConfig;
|
|
135
|
+
/**
|
|
136
|
+
* The harness(es) this repo targets — selects the compile dialect / skill
|
|
137
|
+
* frontmatter profile / instruction-file shape, instead of sniffing the cwd.
|
|
138
|
+
* A single name (`"codex"`) for the common single-harness repo, or an array
|
|
139
|
+
* (`["claude-code", "codex"]`) declaring the supported set. Written by
|
|
140
|
+
* `vigiles init`. Omitted → the CLI auto-detects (backwards-compatible).
|
|
141
|
+
* Canonical adapter names; `"claude"` is accepted as an alias for
|
|
142
|
+
* `"claude-code"`. See research/multi-harness-compile.md.
|
|
143
|
+
*/
|
|
144
|
+
harness?: string | string[];
|
|
130
145
|
}
|
|
131
146
|
/** Valid marker types for rule detection. */
|
|
132
147
|
export type MarkerType = "headings" | "checkboxes";
|
package/dist/core/validate.js
CHANGED
|
@@ -32,10 +32,19 @@ const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
|
|
|
32
32
|
const DEFAULT_FILES = [INSTRUCTION_FILES[0]];
|
|
33
33
|
const DEFAULT_RULES = {
|
|
34
34
|
"require-spec": "warn",
|
|
35
|
-
|
|
35
|
+
// DEPRECATED — default OFF. Skills are legitimately hand-written (Level 0/1),
|
|
36
|
+
// so requiring a .spec.ts per SKILL.md was the wrong constraint and only added
|
|
37
|
+
// noise (it also nagged about vendored/fixture/bench skills). Use the
|
|
38
|
+
// `untested-*` rules instead — "every skill/agent/hook ships with a test or
|
|
39
|
+
// eval" is the coverage that matters. The implementation is kept: setting
|
|
40
|
+
// `require-skill-spec` explicitly still works for anyone who wants it.
|
|
41
|
+
"require-skill-spec": false,
|
|
36
42
|
integrity: "warn",
|
|
37
43
|
coverage: false,
|
|
38
|
-
|
|
44
|
+
// Per-kind surface-coverage: a skill/agent/hook must ship with a test or eval.
|
|
45
|
+
"untested-skill": "warn",
|
|
46
|
+
"untested-agent": "warn",
|
|
47
|
+
"untested-hook": "warn",
|
|
39
48
|
"unmarked-refs": "warn",
|
|
40
49
|
};
|
|
41
50
|
const DEFAULT_CONFIG = {
|
|
@@ -175,7 +184,9 @@ function validate(content, { ruleMarkers, rules: rulesConfig, filePath, dialect
|
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
}
|
|
178
|
-
// --- require-skill-spec (SKILL.md)
|
|
187
|
+
// --- require-skill-spec (SKILL.md) — DEPRECATED but still honored when a
|
|
188
|
+
// user sets it explicitly, so reading the deprecated key here is intentional.
|
|
189
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
179
190
|
const skillSeverity = activeRules["require-skill-spec"];
|
|
180
191
|
if (skillSeverity && isSkill && !disableComment.test(content)) {
|
|
181
192
|
const specPath = filePath + ".spec.ts";
|
package/dist/eval-cache.d.ts
CHANGED
|
@@ -11,6 +11,29 @@ export interface CacheKeyInput {
|
|
|
11
11
|
readonly files: Record<string, string>;
|
|
12
12
|
/** The resolved `.claude/settings.json` for the arm (or undefined). */
|
|
13
13
|
readonly settings: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Per-run env that affects model behaviour (e.g. `VIGILES_INTERCEPT_TOOLS`).
|
|
16
|
+
* Keyed because two intercept configs that share tool names — so produce
|
|
17
|
+
* identical merged `settings` — still differ in their `when`/`denyReason`, which
|
|
18
|
+
* lives only in the env. Omit when there's no model-affecting env.
|
|
19
|
+
*/
|
|
20
|
+
readonly env?: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Content digest of a natively-installed plugin dir (`--plugin-dir`), or
|
|
23
|
+
* undefined when there is none. Folded in so editing a skill INSIDE the dir
|
|
24
|
+
* invalidates the entry — a path-only key would false-replay, since the dir's
|
|
25
|
+
* files are NOT in `files` (that holds only the materialized fixture / `plugin`
|
|
26
|
+
* arm, not a native install). See {@link hashDir}.
|
|
27
|
+
*/
|
|
28
|
+
readonly pluginDirHash?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The harness BINARY version (e.g. `claude --version`). The harness evolves
|
|
31
|
+
* fast — a CLI upgrade changes the system prompt + tool definitions, which steer
|
|
32
|
+
* behaviour as much as the model does — so a cached result must invalidate when
|
|
33
|
+
* the binary changes, or a replay silently serves a result from a different
|
|
34
|
+
* harness. Resolved once per run; omit when unknown (then it doesn't partition).
|
|
35
|
+
*/
|
|
36
|
+
readonly harnessVersion?: string;
|
|
14
37
|
/** Which trial this is — distinct trials are distinct samples, cached apart. */
|
|
15
38
|
readonly trialIndex: number;
|
|
16
39
|
}
|
|
@@ -20,9 +43,23 @@ export interface CacheRecord {
|
|
|
20
43
|
/** Text files present in the cwd after the run (relative path → contents). */
|
|
21
44
|
readonly files: Record<string, string>;
|
|
22
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Cache record-format version, SALTED into every key (Jest `CACHE_VERSION` /
|
|
48
|
+
* webpack `cache.version` pattern). Bump when the `CacheRecord` shape — or how a
|
|
49
|
+
* record is produced in a way the key can't otherwise see — changes, so old
|
|
50
|
+
* entries become *unreachable* rather than deserializing into a stale shape (no
|
|
51
|
+
* brittle read-time version gate needed). A major bump means orphaned files on
|
|
52
|
+
* disk; reclaim them by deleting the cache dir.
|
|
53
|
+
*/
|
|
54
|
+
export declare const CACHE_FORMAT_VERSION = 2;
|
|
23
55
|
/** Deterministic content hash of the key inputs (order-independent). */
|
|
24
56
|
export declare function cacheKey(input: CacheKeyInput): SHA256Hash;
|
|
25
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Read a cached record by key. A MISS (no file) returns `null` — normal, the run
|
|
59
|
+
* proceeds. A CORRUPT record (file present but not valid JSON) **throws** instead
|
|
60
|
+
* of silently degrading to a re-run: a broken cassette is a real failure the CI
|
|
61
|
+
* gate must surface, not mask. The message tells you how to recover.
|
|
62
|
+
*/
|
|
26
63
|
export declare function readCache(dir: string, key: SHA256Hash): CacheRecord | null;
|
|
27
64
|
/** Write a cached record by key (creating the cache dir as needed). */
|
|
28
65
|
export declare function writeCache(dir: string, key: SHA256Hash, record: CacheRecord): void;
|
|
@@ -30,4 +67,15 @@ export declare function writeCache(dir: string, key: SHA256Hash, record: CacheRe
|
|
|
30
67
|
export declare function snapshotDir(cwd: string): Record<string, string>;
|
|
31
68
|
/** Restore a snapshot into `cwd`, recreating directories as needed. */
|
|
32
69
|
export declare function restoreDir(cwd: string, files: Record<string, string>): void;
|
|
70
|
+
/**
|
|
71
|
+
* Content digest of a directory: a lexicographically-sorted list of
|
|
72
|
+
* `relativePath:contentHash` for every file, hashed to one value. Editing,
|
|
73
|
+
* adding, removing, or moving any file changes the digest. It hashes file
|
|
74
|
+
* CONTENT (not mtime — CI checkouts reset mtimes, the classic stale-cache
|
|
75
|
+
* anti-pattern) and includes the relative path (so a rename invalidates and two
|
|
76
|
+
* files can't swap contents undetected). A flat sorted list, NOT a Merkle tree —
|
|
77
|
+
* sufficient at plugin-dir scale; the tree's incremental-recompute payoff isn't
|
|
78
|
+
* worth the complexity here (cf. Bazel/Turborepo hash content per file).
|
|
79
|
+
*/
|
|
80
|
+
export declare function hashDir(dir: string): SHA256Hash;
|
|
33
81
|
//# sourceMappingURL=eval-cache.d.ts.map
|