vigiles 12.6.0 → 12.7.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/dist/adapters/claude-code/layout.js +4 -0
- package/dist/cli.js +105 -66
- package/dist/core/layout.d.ts +11 -0
- package/dist/core/skill-resources.d.ts +16 -0
- package/dist/core/skill-resources.js +32 -4
- package/dist/core/types.d.ts +12 -0
- package/dist/plugin-loader.d.ts +9 -0
- package/dist/plugin-loader.js +93 -16
- package/dist/scan.d.ts +4 -1
- package/dist/scan.js +36 -5
- package/dist/test-coverage.js +29 -3
- package/package.json +1 -1
|
@@ -9,6 +9,10 @@ exports.claudeCodeLayout = {
|
|
|
9
9
|
settingsFormat: "json",
|
|
10
10
|
instructionFile: "CLAUDE.md",
|
|
11
11
|
surfaceDirs: ["skills", "agents", "commands"],
|
|
12
|
+
// A plain Claude Code USER keeps skills/agents/commands under `.claude/`, not at
|
|
13
|
+
// the repo root (that's the published-plugin shape). Read both so a normal repo
|
|
14
|
+
// isn't seen as an empty machine.
|
|
15
|
+
userSurfaceRoot: ".claude",
|
|
12
16
|
skillDir: "skills",
|
|
13
17
|
agentDir: "agents",
|
|
14
18
|
commandDir: "commands",
|
package/dist/cli.js
CHANGED
|
@@ -924,18 +924,49 @@ function verifyMarkdownModeRules(files, silent, config) {
|
|
|
924
924
|
* --summary Print a single-line summary (for SessionStart hooks)
|
|
925
925
|
* --json Print structured JSON report (for CI integration)
|
|
926
926
|
*/
|
|
927
|
+
/**
|
|
928
|
+
* The repo root that `sharedDirs` resolve against. The caller's cwd (where
|
|
929
|
+
* `.vigilesrc.json` lives) is used ONLY when the scan target is INSIDE it — e.g.
|
|
930
|
+
* `lint packages/foo` from the repo root, where the shared tree is an ancestor of
|
|
931
|
+
* the scoped subdir. When the target is NOT under cwd (`lint path/to/other-repo`),
|
|
932
|
+
* we resolve against the TARGET itself, so a foreign-repo lint never lets the
|
|
933
|
+
* caller's own files satisfy the target's bundled resources (scoped-lint integrity).
|
|
934
|
+
*/
|
|
935
|
+
function sharedDirsRootFor(scanTarget) {
|
|
936
|
+
const cwd = process.cwd();
|
|
937
|
+
const target = (0, node_path_1.resolve)(scanTarget);
|
|
938
|
+
const rel = (0, node_path_1.relative)(cwd, target);
|
|
939
|
+
const underCwd = rel === "" || (!rel.startsWith("..") && !(0, node_path_1.isAbsolute)(rel));
|
|
940
|
+
return underCwd ? cwd : target;
|
|
941
|
+
}
|
|
927
942
|
async function runLint(restArgs, flags, config) {
|
|
928
943
|
const summary = flags.includes("--summary");
|
|
929
944
|
const json = flags.includes("--json");
|
|
930
945
|
const silent = summary || json;
|
|
931
946
|
const files = findInstructionFiles(restArgs, config?.exclude);
|
|
947
|
+
// P0-2: scope the surface checks (subagent contracts, skill resources, MCP, …)
|
|
948
|
+
// to an explicit DIRECTORY target when one is given, instead of always scanning
|
|
949
|
+
// the whole working dir and reporting surfaces the user didn't point at. Only a
|
|
950
|
+
// SINGLE existing directory narrows; a file / several paths / none → cwd, so
|
|
951
|
+
// bare `vigiles lint` (the CI-common case) stays byte-identical. scanPlugin
|
|
952
|
+
// reads only under this root, so a surface outside it can never enter the report.
|
|
953
|
+
// Computed BEFORE the harness resolves so auto-detection keys on the TARGET, not
|
|
954
|
+
// cwd — `lint path/to/codex-repo` picks that repo's harness, not the caller's.
|
|
955
|
+
const positional = restArgs.filter((a) => !a.startsWith("--"));
|
|
956
|
+
const scanRoot = positional.length === 1 &&
|
|
957
|
+
(0, node_fs_1.existsSync)(positional[0]) &&
|
|
958
|
+
(0, node_fs_1.lstatSync)(positional[0]).isDirectory()
|
|
959
|
+
? (0, node_path_1.resolve)(positional[0])
|
|
960
|
+
: process.cwd();
|
|
932
961
|
// Resolve the active harness ONCE so the harness-specific checks below run
|
|
933
962
|
// against the right adapter's dialect (tool/event catalogs) and surfaces —
|
|
934
963
|
// not a hard-coded Claude Code default. A subagent-surface rule reports n/a
|
|
935
|
-
// on a harness without subagents (Codex) rather than scanning nothing.
|
|
964
|
+
// on a harness without subagents (Codex) rather than scanning nothing. Detect
|
|
965
|
+
// against `scanRoot` (the target), so a scoped scan of another-harness repo
|
|
966
|
+
// uses that repo's layout/dialect.
|
|
936
967
|
const harnessFlag = harnessFlagFrom(flags);
|
|
937
968
|
const lintSelection = (0, adapter_registry_js_1.resolveHarnessSelection)({
|
|
938
|
-
root:
|
|
969
|
+
root: scanRoot,
|
|
939
970
|
flag: harnessFlag,
|
|
940
971
|
configHarness: (0, adapter_registry_js_1.normalizeHarnessList)(config?.harness),
|
|
941
972
|
});
|
|
@@ -998,71 +1029,71 @@ async function runLint(restArgs, flags, config) {
|
|
|
998
1029
|
// 7b. Untested-surface check — skills/agents/hooks shipping without a test or
|
|
999
1030
|
// eval. Warning by default (a nudge, exit 0); set rules.untested-{skill,agent,
|
|
1000
1031
|
// hook} to "error" to gate CI. See src/test-coverage.ts and docs/rules/.
|
|
1001
|
-
const untested = checkUntestedSurfaces(config, silent, adapter);
|
|
1032
|
+
const untested = checkUntestedSurfaces(config, silent, adapter, scanRoot);
|
|
1002
1033
|
// 7c. Subagent tool-contract check — cross-reference each subagent's `tools:`
|
|
1003
1034
|
// rail against the harness catalog (the moat). n/a on a harness with no
|
|
1004
1035
|
// subagents. Off by default unless a severity is configured; warning surfaces
|
|
1005
1036
|
// a typo/never-available tool, error gates CI.
|
|
1006
|
-
const toolContract = checkSubagentToolContracts(config, silent, adapter);
|
|
1037
|
+
const toolContract = checkSubagentToolContracts(config, silent, adapter, scanRoot);
|
|
1007
1038
|
// 7d. Hook-event check — a hook registered under an event the harness doesn't
|
|
1008
1039
|
// define never fires. High-precision (close typos only). Off unless configured.
|
|
1009
|
-
const hookEvents = checkHookEvents(config, silent, adapter);
|
|
1040
|
+
const hookEvents = checkHookEvents(config, silent, adapter, scanRoot);
|
|
1010
1041
|
// 7e. Subagent-frontmatter check — a subagent missing required frontmatter
|
|
1011
1042
|
// (name + description) won't register. n/a on a harness with no subagents.
|
|
1012
|
-
const frontmatter = checkFrontmatterSchema(config, silent, adapter);
|
|
1043
|
+
const frontmatter = checkFrontmatterSchema(config, silent, adapter, scanRoot);
|
|
1013
1044
|
// 7f. MCP-config check — a declared MCP server with no command/url can't start.
|
|
1014
|
-
const mcpConfig = checkMcpConfig(config, silent, adapter);
|
|
1045
|
+
const mcpConfig = checkMcpConfig(config, silent, adapter, scanRoot);
|
|
1015
1046
|
// 7g. Skill-frontmatter — RECOMMEND explicit name/description on skills (a
|
|
1016
1047
|
// reliable trigger surface). Best-practice nudge; skills load without it.
|
|
1017
|
-
const skillFm = checkSkillFrontmatter(config, silent, adapter);
|
|
1048
|
+
const skillFm = checkSkillFrontmatter(config, silent, adapter, scanRoot);
|
|
1018
1049
|
// 7h. MCP tool-resolution — an `mcp__server__tool` in a contract whose server
|
|
1019
1050
|
// the plugin doesn't declare can't resolve (the MCP half of the tool moat).
|
|
1020
|
-
const mcpToolResolves = checkMcpToolResolves(config, silent, adapter);
|
|
1051
|
+
const mcpToolResolves = checkMcpToolResolves(config, silent, adapter, scanRoot);
|
|
1021
1052
|
// 7i. Hook-script existence — a hook command referencing a missing script file
|
|
1022
1053
|
// never runs (matches Anthropic's own `claude plugin validate`).
|
|
1023
|
-
const hookScripts = checkHookScriptExists(config, silent, adapter);
|
|
1054
|
+
const hookScripts = checkHookScriptExists(config, silent, adapter, scanRoot);
|
|
1024
1055
|
// 7j. Disallowed-tools — a `disallowedTools:` block-list typo blocks nothing
|
|
1025
1056
|
// (the deny-side mirror of subagent-tool-contract; close-typo only).
|
|
1026
|
-
const disallowedTools = checkDisallowedTools(config, silent, adapter);
|
|
1057
|
+
const disallowedTools = checkDisallowedTools(config, silent, adapter, scanRoot);
|
|
1027
1058
|
// 7k. Description-overlap — two model-invocable skills with near-identical
|
|
1028
1059
|
// descriptions collide in the selector (deterministic NCD precision proxy).
|
|
1029
|
-
const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter);
|
|
1060
|
+
const descriptionOverlap = checkDescriptionOverlap(config, silent, adapter, scanRoot);
|
|
1030
1061
|
// 7k². Skill-description-budget — a model-invocable skill whose description is
|
|
1031
1062
|
// so long the trigger signal is buried (heuristic proxy; degrades recall +
|
|
1032
1063
|
// precision). Generous 500-char budget; warn-tier, never gates.
|
|
1033
|
-
const descriptionBudget = checkDescriptionBudget(config, silent, adapter);
|
|
1064
|
+
const descriptionBudget = checkDescriptionBudget(config, silent, adapter, scanRoot);
|
|
1034
1065
|
// 7l. Frontmatter-valid — a `---` block that isn't valid YAML (warn; js-yaml is
|
|
1035
1066
|
// stricter than some loaders, so verify before enforcing).
|
|
1036
|
-
const frontmatterValid = checkFrontmatterValid(config, silent, adapter);
|
|
1067
|
+
const frontmatterValid = checkFrontmatterValid(config, silent, adapter, scanRoot);
|
|
1037
1068
|
// 7m. MCP hook-target — a `type: mcp_tool` hook action that's incomplete or
|
|
1038
1069
|
// targets an undeclared server (the moat applied to the hook surface).
|
|
1039
|
-
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter);
|
|
1070
|
+
const mcpHookTargets = checkMcpHookTargets(config, silent, adapter, scanRoot);
|
|
1040
1071
|
// 7n. Prefer-compiled-hooks — ONE discovery nudge (not per-hook) toward
|
|
1041
1072
|
// compiled `vigiles/hook` artifacts when hand-written hooks ship. Recommendation.
|
|
1042
|
-
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter);
|
|
1073
|
+
const preferCompiledHooks = checkPreferCompiledHooks(config, silent, adapter, scanRoot);
|
|
1043
1074
|
// 7o. Lethal-trifecta — a unit (subagent / model-invocable skill) whose tools
|
|
1044
1075
|
// hold all three legs (read-private + ingest-untrusted + exfiltrate) is a
|
|
1045
1076
|
// prompt-injection exfil path (Rule of Two). Capability SET-intersection.
|
|
1046
|
-
const lethalTrifecta = checkLethalTrifecta(config, silent, adapter);
|
|
1077
|
+
const lethalTrifecta = checkLethalTrifecta(config, silent, adapter, scanRoot);
|
|
1047
1078
|
// 7p. Skill-resource — a SKILL.md body referencing a bundled file that doesn't
|
|
1048
1079
|
// exist on disk under the skill dir (the agent gets nothing). FP-safe.
|
|
1049
|
-
const skillResources = checkSkillResourceResolves(config, silent, adapter);
|
|
1080
|
+
const skillResources = checkSkillResourceResolves(config, silent, adapter, scanRoot);
|
|
1050
1081
|
// 7q. Skill-missing-fence — a SKILL.md opening with `name:`/`description:` but no
|
|
1051
1082
|
// `---` fence loads as plain body (invisible — no name/description/trigger).
|
|
1052
|
-
const skillFence = checkSkillMissingFence(config, silent, adapter);
|
|
1083
|
+
const skillFence = checkSkillMissingFence(config, silent, adapter, scanRoot);
|
|
1053
1084
|
// 7r. Plugin-dir-layout — functional surface dirs (skills/agents/commands) nested
|
|
1054
1085
|
// inside the `.claude-plugin/` manifest dir where the harness can't see them.
|
|
1055
|
-
const pluginLayout = checkPluginDirLayout(config, silent, adapter);
|
|
1086
|
+
const pluginLayout = checkPluginDirLayout(config, silent, adapter, scanRoot);
|
|
1056
1087
|
// 7s. Delegation-trifecta — a lethal trifecta that emerges across a delegation
|
|
1057
1088
|
// edge (a subagent's own ∪ delegated-to capability) though no single unit trips it.
|
|
1058
|
-
const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter);
|
|
1089
|
+
const delegationTrifecta = checkDelegationTrifecta(config, silent, adapter, scanRoot);
|
|
1059
1090
|
// 7t. Hook-block-ineffective — a hook that looks like it blocks but silently
|
|
1060
1091
|
// doesn't (block decision on a non-blocking event, or the legacy `decision`
|
|
1061
1092
|
// field on a permission-gated event). The #1 verified hook pain (#19009).
|
|
1062
|
-
const hookBlock = checkHookBlockIneffective(config, silent, adapter);
|
|
1093
|
+
const hookBlock = checkHookBlockIneffective(config, silent, adapter, scanRoot);
|
|
1063
1094
|
// 7u. Hook-matcher — a hook `matcher` that never fires (tool-name typo, or a
|
|
1064
1095
|
// malformed/undeclared MCP form).
|
|
1065
|
-
const hookMatcher = checkHookMatcher(config, silent, adapter);
|
|
1096
|
+
const hookMatcher = checkHookMatcher(config, silent, adapter, scanRoot);
|
|
1066
1097
|
// 8. Validate vigiles builder calls inside markdown code blocks. Default
|
|
1067
1098
|
// is to validate every ref; illustrative blocks opt out via
|
|
1068
1099
|
// `<!-- vigiles:ignore -->` (single block) or
|
|
@@ -2456,7 +2487,7 @@ function checkIntegrityForFiles(files, severity, silent) {
|
|
|
2456
2487
|
* "warn" prints but never fails CI; "error" fails (exit 2). Returns the raw
|
|
2457
2488
|
* untested count plus the severity-gated error count.
|
|
2458
2489
|
*/
|
|
2459
|
-
function checkUntestedSurfaces(config, silent, adapter) {
|
|
2490
|
+
function checkUntestedSurfaces(config, silent, adapter, scanRoot) {
|
|
2460
2491
|
const rules = config?.rules;
|
|
2461
2492
|
const skillSev = (0, types_js_1.ruleSeverity)(rules?.["untested-skill"]);
|
|
2462
2493
|
const agentSev = (0, types_js_1.ruleSeverity)(rules?.["untested-subagent"]);
|
|
@@ -2472,7 +2503,7 @@ function checkUntestedSurfaces(config, silent, adapter) {
|
|
|
2472
2503
|
...(0, types_js_1.ruleOptions)(rules?.["untested-hook"]),
|
|
2473
2504
|
};
|
|
2474
2505
|
const report = (0, test_coverage_js_1.findUntestedSurfaces)({
|
|
2475
|
-
basePath:
|
|
2506
|
+
basePath: scanRoot,
|
|
2476
2507
|
layout: adapter.layout,
|
|
2477
2508
|
skills: skillSev !== false,
|
|
2478
2509
|
agents: agentSev !== false,
|
|
@@ -2515,7 +2546,7 @@ function reportNotApplicable(check, surface, adapter, silent) {
|
|
|
2515
2546
|
* (plugin/MCP-provided) is never a false alarm. Warning by default; set
|
|
2516
2547
|
* `subagent-tool-contract: "error"` to gate CI. Returns the issue + error counts.
|
|
2517
2548
|
*/
|
|
2518
|
-
function checkSubagentToolContracts(config, silent, adapter) {
|
|
2549
|
+
function checkSubagentToolContracts(config, silent, adapter, scanRoot) {
|
|
2519
2550
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-tool-contract"]);
|
|
2520
2551
|
if (!sev)
|
|
2521
2552
|
return { issues: 0, errors: 0 };
|
|
@@ -2528,7 +2559,7 @@ function checkSubagentToolContracts(config, silent, adapter) {
|
|
|
2528
2559
|
// `agents/` path, so a harness with a different subagent dir Just Works.
|
|
2529
2560
|
let agents;
|
|
2530
2561
|
try {
|
|
2531
|
-
agents = (0, scan_js_1.scanPlugin)(
|
|
2562
|
+
agents = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents;
|
|
2532
2563
|
}
|
|
2533
2564
|
catch {
|
|
2534
2565
|
return { issues: 0, errors: 0 };
|
|
@@ -2558,7 +2589,7 @@ function checkSubagentToolContracts(config, silent, adapter) {
|
|
|
2558
2589
|
* `hookEventIssues` (the shared detector, high-precision: close typos only, never
|
|
2559
2590
|
* a framework/custom event). Warning by default; "error" gates CI.
|
|
2560
2591
|
*/
|
|
2561
|
-
function checkHookEvents(config, silent, adapter) {
|
|
2592
|
+
function checkHookEvents(config, silent, adapter, scanRoot) {
|
|
2562
2593
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-events"]);
|
|
2563
2594
|
if (!sev)
|
|
2564
2595
|
return { issues: 0, errors: 0 };
|
|
@@ -2568,7 +2599,7 @@ function checkHookEvents(config, silent, adapter) {
|
|
|
2568
2599
|
}
|
|
2569
2600
|
let found;
|
|
2570
2601
|
try {
|
|
2571
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2602
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookEventIssues;
|
|
2572
2603
|
}
|
|
2573
2604
|
catch {
|
|
2574
2605
|
return { issues: 0, errors: 0 };
|
|
@@ -2589,7 +2620,7 @@ function checkHookEvents(config, silent, adapter) {
|
|
|
2589
2620
|
* of a real one) — it silently falls back / is ignored. Reuses `scanPlugin`'s
|
|
2590
2621
|
* `frontmatterIssues` + `frontmatterValueIssues`. Warning by default; "error" gates CI.
|
|
2591
2622
|
*/
|
|
2592
|
-
function checkFrontmatterSchema(config, silent, adapter) {
|
|
2623
|
+
function checkFrontmatterSchema(config, silent, adapter, scanRoot) {
|
|
2593
2624
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["subagent-frontmatter"]);
|
|
2594
2625
|
if (!sev)
|
|
2595
2626
|
return { issues: 0, errors: 0 };
|
|
@@ -2599,7 +2630,7 @@ function checkFrontmatterSchema(config, silent, adapter) {
|
|
|
2599
2630
|
}
|
|
2600
2631
|
let found;
|
|
2601
2632
|
try {
|
|
2602
|
-
const r = (0, scan_js_1.scanPlugin)(
|
|
2633
|
+
const r = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect);
|
|
2603
2634
|
found = [...r.frontmatterIssues, ...r.frontmatterValueIssues];
|
|
2604
2635
|
}
|
|
2605
2636
|
catch {
|
|
@@ -2622,13 +2653,13 @@ function checkFrontmatterSchema(config, silent, adapter) {
|
|
|
2622
2653
|
* default; set "error" to enforce it on your own skills. Reuses `scanPlugin`'s
|
|
2623
2654
|
* `skillMetaIssues`.
|
|
2624
2655
|
*/
|
|
2625
|
-
function checkSkillFrontmatter(config, silent, adapter) {
|
|
2656
|
+
function checkSkillFrontmatter(config, silent, adapter, scanRoot) {
|
|
2626
2657
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-frontmatter"]);
|
|
2627
2658
|
if (!sev)
|
|
2628
2659
|
return { issues: 0, errors: 0 };
|
|
2629
2660
|
let found;
|
|
2630
2661
|
try {
|
|
2631
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2662
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).skillMetaIssues;
|
|
2632
2663
|
}
|
|
2633
2664
|
catch {
|
|
2634
2665
|
return { issues: 0, errors: 0 };
|
|
@@ -2649,13 +2680,13 @@ function checkSkillFrontmatter(config, silent, adapter) {
|
|
|
2649
2680
|
* lane stays first-class — so it fires once and the message links the guide.
|
|
2650
2681
|
* Reuses `scanPlugin`'s `manualHookCount` (one-detector-no-drift).
|
|
2651
2682
|
*/
|
|
2652
|
-
function checkPreferCompiledHooks(config, silent, adapter) {
|
|
2683
|
+
function checkPreferCompiledHooks(config, silent, adapter, scanRoot) {
|
|
2653
2684
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["prefer-compiled-hooks"]);
|
|
2654
2685
|
if (!sev)
|
|
2655
2686
|
return { issues: 0, errors: 0 };
|
|
2656
2687
|
let count;
|
|
2657
2688
|
try {
|
|
2658
|
-
count = (0, scan_js_1.scanPlugin)(
|
|
2689
|
+
count = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).manualHookCount;
|
|
2659
2690
|
}
|
|
2660
2691
|
catch {
|
|
2661
2692
|
return { issues: 0, errors: 0 };
|
|
@@ -2675,13 +2706,13 @@ function checkPreferCompiledHooks(config, silent, adapter) {
|
|
|
2675
2706
|
* (stdio) nor a `url` (http/sse) can't start. Reuses `scanPlugin`'s `mcpIssues`.
|
|
2676
2707
|
* Warning by default; "error" gates CI.
|
|
2677
2708
|
*/
|
|
2678
|
-
function checkMcpConfig(config, silent, adapter) {
|
|
2709
|
+
function checkMcpConfig(config, silent, adapter, scanRoot) {
|
|
2679
2710
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-config"]);
|
|
2680
2711
|
if (!sev)
|
|
2681
2712
|
return { issues: 0, errors: 0 };
|
|
2682
2713
|
let found;
|
|
2683
2714
|
try {
|
|
2684
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2715
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).mcpIssues;
|
|
2685
2716
|
}
|
|
2686
2717
|
catch {
|
|
2687
2718
|
return { issues: 0, errors: 0 };
|
|
@@ -2702,7 +2733,7 @@ function checkMcpConfig(config, silent, adapter) {
|
|
|
2702
2733
|
* `disallowedToolIssues` (close-typo only — high-precision). Warning by default;
|
|
2703
2734
|
* "error" gates CI.
|
|
2704
2735
|
*/
|
|
2705
|
-
function checkDisallowedTools(config, silent, adapter) {
|
|
2736
|
+
function checkDisallowedTools(config, silent, adapter, scanRoot) {
|
|
2706
2737
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["disallowed-tools-contract"]);
|
|
2707
2738
|
if (!sev)
|
|
2708
2739
|
return { issues: 0, errors: 0 };
|
|
@@ -2712,7 +2743,7 @@ function checkDisallowedTools(config, silent, adapter) {
|
|
|
2712
2743
|
}
|
|
2713
2744
|
let found;
|
|
2714
2745
|
try {
|
|
2715
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2746
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents.flatMap((a) => a.disallowedToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
2716
2747
|
}
|
|
2717
2748
|
catch {
|
|
2718
2749
|
return { issues: 0, errors: 0 };
|
|
@@ -2734,13 +2765,13 @@ function checkDisallowedTools(config, silent, adapter) {
|
|
|
2734
2765
|
* colon / `<example>` is flagged though it may still load — hence WARN by default
|
|
2735
2766
|
* (verify before setting "error").
|
|
2736
2767
|
*/
|
|
2737
|
-
function checkFrontmatterValid(config, silent, adapter) {
|
|
2768
|
+
function checkFrontmatterValid(config, silent, adapter, scanRoot) {
|
|
2738
2769
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["frontmatter-valid"]);
|
|
2739
2770
|
if (!sev)
|
|
2740
2771
|
return { issues: 0, errors: 0 };
|
|
2741
2772
|
let found;
|
|
2742
2773
|
try {
|
|
2743
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2774
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).malformedFrontmatter;
|
|
2744
2775
|
}
|
|
2745
2776
|
catch {
|
|
2746
2777
|
return { issues: 0, errors: 0 };
|
|
@@ -2761,13 +2792,13 @@ function checkFrontmatterValid(config, silent, adapter) {
|
|
|
2761
2792
|
* `scanPlugin`'s `descriptionOverlaps` (calibrated FP-safe: only basically
|
|
2762
2793
|
* identical text). Warning by default; "error" gates CI.
|
|
2763
2794
|
*/
|
|
2764
|
-
function checkDescriptionOverlap(config, silent, adapter) {
|
|
2795
|
+
function checkDescriptionOverlap(config, silent, adapter, scanRoot) {
|
|
2765
2796
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["description-overlap"]);
|
|
2766
2797
|
if (!sev)
|
|
2767
2798
|
return { issues: 0, errors: 0 };
|
|
2768
2799
|
let found;
|
|
2769
2800
|
try {
|
|
2770
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2801
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).descriptionOverlaps;
|
|
2771
2802
|
}
|
|
2772
2803
|
catch {
|
|
2773
2804
|
return { issues: 0, errors: 0 };
|
|
@@ -2788,13 +2819,13 @@ function checkDescriptionOverlap(config, silent, adapter) {
|
|
|
2788
2819
|
* deterministic heuristic proxy (generous 500-char budget). Reuses `scanPlugin`'s
|
|
2789
2820
|
* `descriptionBudgetIssues`. Warning by default; "error" gates CI.
|
|
2790
2821
|
*/
|
|
2791
|
-
function checkDescriptionBudget(config, silent, adapter) {
|
|
2822
|
+
function checkDescriptionBudget(config, silent, adapter, scanRoot) {
|
|
2792
2823
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-description-budget"]);
|
|
2793
2824
|
if (!sev)
|
|
2794
2825
|
return { issues: 0, errors: 0 };
|
|
2795
2826
|
let found;
|
|
2796
2827
|
try {
|
|
2797
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2828
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).descriptionBudgetIssues;
|
|
2798
2829
|
}
|
|
2799
2830
|
catch {
|
|
2800
2831
|
return { issues: 0, errors: 0 };
|
|
@@ -2817,13 +2848,13 @@ function checkDescriptionBudget(config, silent, adapter) {
|
|
|
2817
2848
|
* and skills, so it is NOT gated on the `subagents` capability — a skill-only
|
|
2818
2849
|
* harness still has the surface.
|
|
2819
2850
|
*/
|
|
2820
|
-
function checkLethalTrifecta(config, silent, adapter) {
|
|
2851
|
+
function checkLethalTrifecta(config, silent, adapter, scanRoot) {
|
|
2821
2852
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["lethal-trifecta"]);
|
|
2822
2853
|
if (!sev)
|
|
2823
2854
|
return { issues: 0, errors: 0 };
|
|
2824
2855
|
let found;
|
|
2825
2856
|
try {
|
|
2826
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2857
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).trifectaFindings;
|
|
2827
2858
|
}
|
|
2828
2859
|
catch {
|
|
2829
2860
|
return { issues: 0, errors: 0 };
|
|
@@ -2845,13 +2876,18 @@ function checkLethalTrifecta(config, silent, adapter) {
|
|
|
2845
2876
|
* nothing. Reuses `scanPlugin`'s `skillResourceIssues` (high-precision / FP-safe,
|
|
2846
2877
|
* one detector, no drift). Warning by default; "error" gates CI.
|
|
2847
2878
|
*/
|
|
2848
|
-
function checkSkillResourceResolves(config, silent, adapter) {
|
|
2879
|
+
function checkSkillResourceResolves(config, silent, adapter, scanRoot) {
|
|
2849
2880
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-resource-resolves"]);
|
|
2850
2881
|
if (!sev)
|
|
2851
2882
|
return { issues: 0, errors: 0 };
|
|
2852
2883
|
let found;
|
|
2853
2884
|
try {
|
|
2854
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2885
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect, {
|
|
2886
|
+
sharedDirs: config?.sharedDirs,
|
|
2887
|
+
// sharedDirs live at the repo root that OWNS the scan target — cwd for a
|
|
2888
|
+
// scoped subdir of this repo, the target itself for a foreign-repo lint.
|
|
2889
|
+
sharedDirsRoot: sharedDirsRootFor(scanRoot),
|
|
2890
|
+
}).skillResourceIssues;
|
|
2855
2891
|
}
|
|
2856
2892
|
catch {
|
|
2857
2893
|
return { issues: 0, errors: 0 };
|
|
@@ -2873,13 +2909,13 @@ function checkSkillResourceResolves(config, silent, adapter) {
|
|
|
2873
2909
|
* Reuses `scanPlugin`'s `skillFenceIssues` (one detector, no drift). Warning by
|
|
2874
2910
|
* default; "error" gates CI.
|
|
2875
2911
|
*/
|
|
2876
|
-
function checkSkillMissingFence(config, silent, adapter) {
|
|
2912
|
+
function checkSkillMissingFence(config, silent, adapter, scanRoot) {
|
|
2877
2913
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["skill-missing-fence"]);
|
|
2878
2914
|
if (!sev)
|
|
2879
2915
|
return { issues: 0, errors: 0 };
|
|
2880
2916
|
let found;
|
|
2881
2917
|
try {
|
|
2882
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2918
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).skillFenceIssues;
|
|
2883
2919
|
}
|
|
2884
2920
|
catch {
|
|
2885
2921
|
return { issues: 0, errors: 0 };
|
|
@@ -2901,13 +2937,13 @@ function checkSkillMissingFence(config, silent, adapter) {
|
|
|
2901
2937
|
* `pluginLayoutIssues` (one detector, no drift). Warning by default; "error"
|
|
2902
2938
|
* gates CI.
|
|
2903
2939
|
*/
|
|
2904
|
-
function checkPluginDirLayout(config, silent, adapter) {
|
|
2940
|
+
function checkPluginDirLayout(config, silent, adapter, scanRoot) {
|
|
2905
2941
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["plugin-dir-layout"]);
|
|
2906
2942
|
if (!sev)
|
|
2907
2943
|
return { issues: 0, errors: 0 };
|
|
2908
2944
|
let found;
|
|
2909
2945
|
try {
|
|
2910
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2946
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).pluginLayoutIssues;
|
|
2911
2947
|
}
|
|
2912
2948
|
catch {
|
|
2913
2949
|
return { issues: 0, errors: 0 };
|
|
@@ -2929,13 +2965,13 @@ function checkPluginDirLayout(config, silent, adapter) {
|
|
|
2929
2965
|
* gates CI. Surfaces across the subagent graph, so it is NOT gated on a
|
|
2930
2966
|
* capability the way a surface-specific rule is.
|
|
2931
2967
|
*/
|
|
2932
|
-
function checkDelegationTrifecta(config, silent, adapter) {
|
|
2968
|
+
function checkDelegationTrifecta(config, silent, adapter, scanRoot) {
|
|
2933
2969
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["delegation-trifecta"]);
|
|
2934
2970
|
if (!sev)
|
|
2935
2971
|
return { issues: 0, errors: 0 };
|
|
2936
2972
|
let found;
|
|
2937
2973
|
try {
|
|
2938
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
2974
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).delegationTrifecta;
|
|
2939
2975
|
}
|
|
2940
2976
|
catch {
|
|
2941
2977
|
return { issues: 0, errors: 0 };
|
|
@@ -2957,7 +2993,7 @@ function checkDelegationTrifecta(config, silent, adapter) {
|
|
|
2957
2993
|
* permission-gated event (#19009, the #1 verified hook pain). Reuses `scanPlugin`'s
|
|
2958
2994
|
* `hookBlockFindings` (one detector, no drift). Warning by default; "error" gates CI.
|
|
2959
2995
|
*/
|
|
2960
|
-
function checkHookBlockIneffective(config, silent, adapter) {
|
|
2996
|
+
function checkHookBlockIneffective(config, silent, adapter, scanRoot) {
|
|
2961
2997
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-block-ineffective"]);
|
|
2962
2998
|
if (!sev)
|
|
2963
2999
|
return { issues: 0, errors: 0 };
|
|
@@ -2967,7 +3003,7 @@ function checkHookBlockIneffective(config, silent, adapter) {
|
|
|
2967
3003
|
}
|
|
2968
3004
|
let found;
|
|
2969
3005
|
try {
|
|
2970
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
3006
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookBlockFindings;
|
|
2971
3007
|
}
|
|
2972
3008
|
catch {
|
|
2973
3009
|
return { issues: 0, errors: 0 };
|
|
@@ -2989,7 +3025,7 @@ function checkHookBlockIneffective(config, silent, adapter) {
|
|
|
2989
3025
|
* Reuses `scanPlugin`'s `hookMatcherFindings` (one detector, no drift). Warning
|
|
2990
3026
|
* by default; "error" gates CI.
|
|
2991
3027
|
*/
|
|
2992
|
-
function checkHookMatcher(config, silent, adapter) {
|
|
3028
|
+
function checkHookMatcher(config, silent, adapter, scanRoot) {
|
|
2993
3029
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-matcher"]);
|
|
2994
3030
|
if (!sev)
|
|
2995
3031
|
return { issues: 0, errors: 0 };
|
|
@@ -2999,7 +3035,7 @@ function checkHookMatcher(config, silent, adapter) {
|
|
|
2999
3035
|
}
|
|
3000
3036
|
let found;
|
|
3001
3037
|
try {
|
|
3002
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
3038
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hookMatcherFindings;
|
|
3003
3039
|
}
|
|
3004
3040
|
catch {
|
|
3005
3041
|
return { issues: 0, errors: 0 };
|
|
@@ -3020,7 +3056,7 @@ function checkHookMatcher(config, silent, adapter) {
|
|
|
3020
3056
|
* `mcpHookIssues` (high-precision: declared-set gated, built-ins allowlisted).
|
|
3021
3057
|
* Warning by default; "error" gates CI.
|
|
3022
3058
|
*/
|
|
3023
|
-
function checkMcpHookTargets(config, silent, adapter) {
|
|
3059
|
+
function checkMcpHookTargets(config, silent, adapter, scanRoot) {
|
|
3024
3060
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-hook-target-resolves"]);
|
|
3025
3061
|
if (!sev)
|
|
3026
3062
|
return { issues: 0, errors: 0 };
|
|
@@ -3030,7 +3066,7 @@ function checkMcpHookTargets(config, silent, adapter) {
|
|
|
3030
3066
|
}
|
|
3031
3067
|
let found;
|
|
3032
3068
|
try {
|
|
3033
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
3069
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).mcpHookIssues;
|
|
3034
3070
|
}
|
|
3035
3071
|
catch {
|
|
3036
3072
|
return { issues: 0, errors: 0 };
|
|
@@ -3052,7 +3088,7 @@ function checkMcpHookTargets(config, silent, adapter) {
|
|
|
3052
3088
|
* existence-guarded one-liners, inline commands). Matches Anthropic's own
|
|
3053
3089
|
* `claude plugin validate`. Warning by default; "error" gates CI.
|
|
3054
3090
|
*/
|
|
3055
|
-
function checkHookScriptExists(config, silent, adapter) {
|
|
3091
|
+
function checkHookScriptExists(config, silent, adapter, scanRoot) {
|
|
3056
3092
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["hook-script-exists"]);
|
|
3057
3093
|
if (!sev)
|
|
3058
3094
|
return { issues: 0, errors: 0 };
|
|
@@ -3062,7 +3098,7 @@ function checkHookScriptExists(config, silent, adapter) {
|
|
|
3062
3098
|
}
|
|
3063
3099
|
let missing;
|
|
3064
3100
|
try {
|
|
3065
|
-
missing = (0, scan_js_1.scanPlugin)(
|
|
3101
|
+
missing = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).hooks.filter((h) => h.status === "missing");
|
|
3066
3102
|
}
|
|
3067
3103
|
catch {
|
|
3068
3104
|
return { issues: 0, errors: 0 };
|
|
@@ -3087,7 +3123,7 @@ function checkHookScriptExists(config, silent, adapter) {
|
|
|
3087
3123
|
* — high-precision (gated on a declared set, built-ins allowlisted, the
|
|
3088
3124
|
* plugin-namespaced form skipped). Warning by default; "error" gates CI.
|
|
3089
3125
|
*/
|
|
3090
|
-
function checkMcpToolResolves(config, silent, adapter) {
|
|
3126
|
+
function checkMcpToolResolves(config, silent, adapter, scanRoot) {
|
|
3091
3127
|
const sev = (0, types_js_1.ruleSeverity)(config?.rules?.["mcp-tool-resolves"]);
|
|
3092
3128
|
if (!sev)
|
|
3093
3129
|
return { issues: 0, errors: 0 };
|
|
@@ -3097,7 +3133,7 @@ function checkMcpToolResolves(config, silent, adapter) {
|
|
|
3097
3133
|
}
|
|
3098
3134
|
let found;
|
|
3099
3135
|
try {
|
|
3100
|
-
found = (0, scan_js_1.scanPlugin)(
|
|
3136
|
+
found = (0, scan_js_1.scanPlugin)(scanRoot, adapter.layout, adapter.dialect).agents.flatMap((a) => a.mcpToolIssues.map((i) => ({ message: i.message, path: a.path })));
|
|
3101
3137
|
}
|
|
3102
3138
|
catch {
|
|
3103
3139
|
return { issues: 0, errors: 0 };
|
|
@@ -5038,7 +5074,10 @@ async function main() {
|
|
|
5038
5074
|
const adapter = harnessFlag
|
|
5039
5075
|
? (0, adapter_registry_js_1.resolveAdapter)(root, harnessFlag)
|
|
5040
5076
|
: det.adapter;
|
|
5041
|
-
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect
|
|
5077
|
+
const report = (0, scan_js_1.scanPlugin)(targets[0], adapter.layout, adapter.dialect, {
|
|
5078
|
+
sharedDirs: config.sharedDirs,
|
|
5079
|
+
sharedDirsRoot: sharedDirsRootFor(targets[0]),
|
|
5080
|
+
});
|
|
5042
5081
|
if (!json) {
|
|
5043
5082
|
console.log(`Detected harness: ${adapter.name}`);
|
|
5044
5083
|
if (!harnessFlag && det.ambiguousWith.length > 0) {
|
package/dist/core/layout.d.ts
CHANGED
|
@@ -29,6 +29,17 @@ export interface PluginLayout {
|
|
|
29
29
|
readonly instructionFile: string;
|
|
30
30
|
/** Surface dirs materialized into the sandbox, e.g. skills/agents/commands. */
|
|
31
31
|
readonly surfaceDirs: readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* Project-level dir under which an END-USER (not a plugin author) keeps the
|
|
34
|
+
* same surfaces, e.g. `.claude` → `.claude/skills`, `.claude/agents`. When set,
|
|
35
|
+
* the loader reads each surface from BOTH `<root>/<surface>` (the plugin /
|
|
36
|
+
* skills-library shape) AND `<root>/<userSurfaceRoot>/<surface>` (the shape a
|
|
37
|
+
* plain Claude Code user has), normalizing to the same materialized key. Most
|
|
38
|
+
* Claude Code users are NOT publishing a plugin — their skills live here, so
|
|
39
|
+
* without this the loader would see an empty machine for a normal repo.
|
|
40
|
+
* Undefined ⇒ only the primary location is read (backwards-compatible).
|
|
41
|
+
*/
|
|
42
|
+
readonly userSurfaceRoot?: string;
|
|
32
43
|
/** Skills dir, holding the nested `<dir>/<name>/SKILL.md`, e.g. `skills`. */
|
|
33
44
|
readonly skillDir: string;
|
|
34
45
|
/** Subagents dir, holding flat `<dir>/<name>.md`, e.g. `agents` (`""` = none). */
|
|
@@ -14,6 +14,22 @@ export interface SkillResourceFinding {
|
|
|
14
14
|
export interface SkillResourceOptions {
|
|
15
15
|
/** Injectable existence check (default: node:fs existsSync). */
|
|
16
16
|
readonly existsSync?: (p: string) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Repo root, used only together with `sharedDirs` (below). Off by default.
|
|
19
|
+
*/
|
|
20
|
+
readonly repoRoot?: string;
|
|
21
|
+
/**
|
|
22
|
+
* OPT-IN shared-resource dirs — top-level dir names a repo shares across skills
|
|
23
|
+
* (`.vigilesrc.json` `sharedDirs`, e.g. `["scripts", "references"]`). Many skill
|
|
24
|
+
* libraries keep ONE top-level `scripts/` tree instead of a copy beside every
|
|
25
|
+
* SKILL.md, so a ref like `scripts/promptfoo/x.py` lives at the repo root. When
|
|
26
|
+
* a ref's FIRST path segment is a declared shared dir, it may ALSO resolve
|
|
27
|
+
* against `repoRoot`. Scoped to declared dirs on PURPOSE: a repo that sets no
|
|
28
|
+
* `sharedDirs` is byte-identical to before (skill-dir-only), and even with it a
|
|
29
|
+
* ref OUTSIDE a shared dir still can't be masked by a same-named repo-root file.
|
|
30
|
+
* The controlled fix for feedback P1-4 (opt-in, never a default behavior change).
|
|
31
|
+
*/
|
|
32
|
+
readonly sharedDirs?: readonly string[];
|
|
17
33
|
}
|
|
18
34
|
/**
|
|
19
35
|
* The bundled-resource references in a SKILL.md body that don't resolve on disk
|
|
@@ -72,11 +72,28 @@ function localResourceTarget(rawTarget) {
|
|
|
72
72
|
// and almost always a plugin-root or runtime path, not a bundled file.
|
|
73
73
|
if (target.includes("$"))
|
|
74
74
|
return null;
|
|
75
|
-
//
|
|
76
|
-
//
|
|
75
|
+
// SKIP: `~/`-rooted paths — the user's home / machine-global config, referenced
|
|
76
|
+
// intentionally from OUTSIDE the repo (JIT routing like `Read ~/.claude/docs/x.md`).
|
|
77
|
+
// Not a repo-bundled resource; unverifiable from the repo and never a "broken ref".
|
|
78
|
+
if (target === "~" || target.startsWith("~/"))
|
|
79
|
+
return null;
|
|
80
|
+
// Drop a URL fragment / query suffix so `references/api.md#auth` AND
|
|
81
|
+
// `references/schema.json?raw=1` resolve to the file. Done BEFORE the glob skip
|
|
82
|
+
// below so a legitimate `?query` suffix on a real bundled ref isn't mistaken for
|
|
83
|
+
// a glob `?` and wrongly skipped — the file must still be checked. (Only after
|
|
84
|
+
// the scheme check above, so we never mangle a URL.)
|
|
77
85
|
const path = target.replace(/[?#].*$/, "");
|
|
78
86
|
if (path.length === 0)
|
|
79
87
|
return null;
|
|
88
|
+
// SKIP: globs and template placeholders — a ref carrying a glob metacharacter
|
|
89
|
+
// (`*`) or a brace/angle-bracket placeholder (`{trivial,…}`, `<linter>`) is a
|
|
90
|
+
// directory CONVENTION or an example, not a concrete file (`references/*.md`,
|
|
91
|
+
// `references/linter-cards/{a,b}/<linter>.md`). Resolving it as a literal path and
|
|
92
|
+
// reporting "missing" is a false positive (feedback P1-3). `?` is intentionally
|
|
93
|
+
// NOT in this class — it's the query separator stripped above; a genuine `?`-glob
|
|
94
|
+
// truncates to an extensionless path and is dropped by HAS_EXT below anyway.
|
|
95
|
+
if (/[*{}<>]/.test(path))
|
|
96
|
+
return null;
|
|
80
97
|
// SKIP: a `../` escape OUT of the skill dir — undecidable / not a bundled
|
|
81
98
|
// resource (it points at a sibling skill or the repo). A leading `./` is fine.
|
|
82
99
|
const normalized = path.replace(/^\.\//, "");
|
|
@@ -134,6 +151,18 @@ function candidatesInLine(line, lineNo) {
|
|
|
134
151
|
*/
|
|
135
152
|
function skillResourceIssues(skillBody, skillDir, opts = {}) {
|
|
136
153
|
const exists = opts.existsSync ?? node_fs_1.existsSync;
|
|
154
|
+
const sharedDirs = new Set(opts.sharedDirs ?? []);
|
|
155
|
+
// A ref resolves if it exists under the skill's own dir. If (and only if) its
|
|
156
|
+
// first segment is a DECLARED shared dir, it may also resolve against the repo
|
|
157
|
+
// root — the opt-in shared-tree case. No shared dirs → skill-dir-only (unchanged).
|
|
158
|
+
const resolvesAnywhere = (rel) => {
|
|
159
|
+
if (exists((0, node_path_1.resolve)(skillDir, rel)))
|
|
160
|
+
return true;
|
|
161
|
+
const firstSeg = rel.split("/")[0];
|
|
162
|
+
return (opts.repoRoot !== undefined &&
|
|
163
|
+
sharedDirs.has(firstSeg) &&
|
|
164
|
+
exists((0, node_path_1.resolve)(opts.repoRoot, rel)));
|
|
165
|
+
};
|
|
137
166
|
const findings = [];
|
|
138
167
|
const seen = new Set();
|
|
139
168
|
const lines = skillBody.split("\n");
|
|
@@ -146,8 +175,7 @@ function skillResourceIssues(skillBody, skillDir, opts = {}) {
|
|
|
146
175
|
if (inFence)
|
|
147
176
|
continue;
|
|
148
177
|
for (const c of candidatesInLine(lines[i], i + 1)) {
|
|
149
|
-
|
|
150
|
-
if (exists(full))
|
|
178
|
+
if (resolvesAnywhere(c.resolved))
|
|
151
179
|
continue;
|
|
152
180
|
// De-dupe the same missing file referenced several times in the body.
|
|
153
181
|
const key = `${c.kind}:${c.resolved}`;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -333,6 +333,18 @@ export interface VigilesConfig {
|
|
|
333
333
|
* excluded.
|
|
334
334
|
*/
|
|
335
335
|
exclude?: readonly string[];
|
|
336
|
+
/**
|
|
337
|
+
* Top-level dir names shared across skills, e.g. `["scripts", "references"]`.
|
|
338
|
+
* OPT-IN: many skill libraries keep ONE top-level `scripts/`/`references/` tree
|
|
339
|
+
* rather than a copy beside every `SKILL.md`, so a bundled ref like
|
|
340
|
+
* `scripts/promptfoo/x.py` lives at the REPO ROOT. When a `SKILL.md` body ref's
|
|
341
|
+
* first path segment is a declared shared dir, `skill-resource-resolves` / audit
|
|
342
|
+
* ALSO resolves it against the repo root, not only the skill's own dir. Scoped
|
|
343
|
+
* to declared dirs on purpose: a repo that omits this key behaves exactly as
|
|
344
|
+
* before (skill-dir-only resolution), and a ref outside a shared dir is never
|
|
345
|
+
* masked by a same-named repo-root file. See feedback P1-4.
|
|
346
|
+
*/
|
|
347
|
+
sharedDirs?: readonly string[];
|
|
336
348
|
/**
|
|
337
349
|
* The harness(es) this repo targets — selects the compile dialect / skill
|
|
338
350
|
* frontmatter profile / instruction-file shape, instead of sniffing the cwd.
|
package/dist/plugin-loader.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface LoadedPlugin {
|
|
|
14
14
|
* read it in a test, or just to know what the deterministic run won't reach.
|
|
15
15
|
*/
|
|
16
16
|
readonly warnings: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* Map from a materialized `files` key to the ABSOLUTE on-disk path it was read
|
|
19
|
+
* from. A surface can be materialized under a canonical key (`.claude/skills/
|
|
20
|
+
* foo/SKILL.md`) while living on disk at a different root (the repo-root
|
|
21
|
+
* `skills/` OR the project-level `.claude/skills/`), so a consumer that needs
|
|
22
|
+
* the real dir (e.g. resolving a skill's bundled resources) must reverse-map
|
|
23
|
+
* through this instead of guessing from the key. Present for every surface file.
|
|
24
|
+
*/
|
|
25
|
+
readonly sources: Record<string, string>;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Load the real harness at `pluginPath`. Returns the resolved settings (hooks),
|
package/dist/plugin-loader.js
CHANGED
|
@@ -36,6 +36,7 @@ exports.resolveHarness = resolveHarness;
|
|
|
36
36
|
const node_fs_1 = require("node:fs");
|
|
37
37
|
const node_path_1 = require("node:path");
|
|
38
38
|
const toml_1 = require("@iarna/toml");
|
|
39
|
+
const hash_js_1 = require("./core/hash.js");
|
|
39
40
|
const MAX_SKILL_FILE_BYTES = 256 * 1024;
|
|
40
41
|
/** Parse a JSON file, or null on any error (missing / malformed). */
|
|
41
42
|
function safeReadJson(path) {
|
|
@@ -140,32 +141,108 @@ function loadPlugin(pluginPath, layout) {
|
|
|
140
141
|
? JSON.parse(JSON.stringify(hooks).replaceAll(layout.pluginRootToken, root))
|
|
141
142
|
: undefined;
|
|
142
143
|
const files = {};
|
|
144
|
+
const sources = {};
|
|
143
145
|
const instructions = (0, node_path_1.join)(root, layout.instructionFile);
|
|
144
146
|
if ((0, node_fs_1.existsSync)(instructions)) {
|
|
145
147
|
files[layout.instructionFile] = (0, node_fs_1.readFileSync)(instructions, "utf-8");
|
|
148
|
+
sources[layout.instructionFile] = instructions;
|
|
146
149
|
}
|
|
147
|
-
|
|
148
|
-
// assembled context is present in the sandbox (best-effort — headless
|
|
149
|
-
// activation of plugin skills/subagents/commands is not guaranteed; the body
|
|
150
|
-
// is present for the agent to read either way). Counting what we materialize
|
|
151
|
-
// also lets us warn about surfaces the deterministic tier can't drive.
|
|
152
|
-
const counts = {};
|
|
153
|
-
for (const surface of layout.surfaceDirs) {
|
|
154
|
-
const dir = (0, node_path_1.join)(root, surface);
|
|
155
|
-
if (!(0, node_fs_1.existsSync)(dir) || !(0, node_fs_1.statSync)(dir).isDirectory())
|
|
156
|
-
continue;
|
|
157
|
-
const tree = readTree(dir, root);
|
|
158
|
-
for (const [rel, content] of Object.entries(tree)) {
|
|
159
|
-
files[(0, node_path_1.join)(layout.materializeRoot, rel)] = content;
|
|
160
|
-
}
|
|
161
|
-
counts[surface] = Object.keys(tree).length;
|
|
162
|
-
}
|
|
150
|
+
const counts = materializeSurfaces(root, layout, files, sources);
|
|
163
151
|
return {
|
|
164
152
|
settings: resolvedHooks ? { hooks: resolvedHooks } : {},
|
|
165
153
|
files,
|
|
154
|
+
sources,
|
|
166
155
|
warnings: pluginWarnings(root, counts, resolvedHooks, files, layout),
|
|
167
156
|
};
|
|
168
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A surface holds a LOADABLE file — a `<name>/SKILL.md` for skills, a `.md` for
|
|
160
|
+
* agents/commands. A stray non-surface file (`skills/README.md`, `.gitkeep`) does
|
|
161
|
+
* NOT count, else it would mark the root populated and shadow a plain user's real
|
|
162
|
+
* `.claude/skills`.
|
|
163
|
+
*/
|
|
164
|
+
function surfaceHasLoadable(layout, surface, tree) {
|
|
165
|
+
const keys = Object.keys(tree);
|
|
166
|
+
return surface === layout.skillDir
|
|
167
|
+
? keys.some((k) => (0, node_path_1.basename)(k) === "SKILL.md")
|
|
168
|
+
: keys.some((k) => k.endsWith(".md"));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Classify the repo shape from disk, with EXPLICIT precedence:
|
|
172
|
+
* 1. a `<root>/SKILL.md` → the target IS one skill dir (single-skill).
|
|
173
|
+
* 2. any root-surface with LOADABLE content, OR a plugin manifest / hooks
|
|
174
|
+
* convention → read the ROOT surfaces. A plugin ships from its manifest even
|
|
175
|
+
* with no root surface dirs, so its dev `.claude/…` is never a fallback.
|
|
176
|
+
* 3. else, if the layout declares a user-surface root → a plain user repo.
|
|
177
|
+
* 4. else → nothing loadable.
|
|
178
|
+
* Pure over the pre-read `rootTrees` + a few existence checks — one place to test.
|
|
179
|
+
*/
|
|
180
|
+
function classifySurfaceSource(root, layout, rootTrees) {
|
|
181
|
+
if (layout.skillDir && (0, node_fs_1.existsSync)((0, node_path_1.join)(root, "SKILL.md"))) {
|
|
182
|
+
return { kind: "single-skill", skillName: (0, node_path_1.basename)(root) };
|
|
183
|
+
}
|
|
184
|
+
const rootHasLoadable = layout.surfaceDirs.some((s) => surfaceHasLoadable(layout, s, rootTrees.get(s) ?? {}));
|
|
185
|
+
const isPluginShaped = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.manifestPath)) ||
|
|
186
|
+
(0, node_fs_1.existsSync)((0, node_path_1.join)(root, layout.hooksConventionPath));
|
|
187
|
+
if (rootHasLoadable || isPluginShaped)
|
|
188
|
+
return { kind: "root" };
|
|
189
|
+
if (layout.userSurfaceRoot !== undefined) {
|
|
190
|
+
return { kind: "user", sub: layout.userSurfaceRoot };
|
|
191
|
+
}
|
|
192
|
+
return { kind: "none" };
|
|
193
|
+
}
|
|
194
|
+
function materializeSurfaces(root, layout, files, sources) {
|
|
195
|
+
const counts = {};
|
|
196
|
+
const isDir = (p) => (0, node_fs_1.existsSync)(p) && (0, node_fs_1.statSync)(p).isDirectory();
|
|
197
|
+
// Read each ROOT-level surface tree once (keys relative to the surface dir).
|
|
198
|
+
const rootTrees = new Map();
|
|
199
|
+
for (const surface of layout.surfaceDirs) {
|
|
200
|
+
const dir = (0, node_path_1.join)(root, surface);
|
|
201
|
+
if (isDir(dir))
|
|
202
|
+
rootTrees.set(surface, readTree(dir, dir));
|
|
203
|
+
}
|
|
204
|
+
const add = (key, content, onDisk) => {
|
|
205
|
+
files[key] = content;
|
|
206
|
+
sources[key] = onDisk;
|
|
207
|
+
};
|
|
208
|
+
const source = classifySurfaceSource(root, layout, rootTrees);
|
|
209
|
+
switch (source.kind) {
|
|
210
|
+
case "single-skill": {
|
|
211
|
+
// Materialize the WHOLE skill dir under the canonical skills key, so its
|
|
212
|
+
// bundled resources (scripts/references/assets) ship too, not just SKILL.md.
|
|
213
|
+
const tree = readTree(root, root);
|
|
214
|
+
for (const [rel, content] of Object.entries(tree)) {
|
|
215
|
+
add((0, node_path_1.join)(layout.materializeRoot, layout.skillDir, source.skillName, rel), content, (0, node_path_1.join)(root, rel));
|
|
216
|
+
}
|
|
217
|
+
counts[layout.skillDir] = Object.keys(tree).length;
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
case "root":
|
|
221
|
+
case "user": {
|
|
222
|
+
const base = source.kind === "user" ? (0, node_path_1.join)(root, source.sub) : root;
|
|
223
|
+
for (const surface of layout.surfaceDirs) {
|
|
224
|
+
const dir = (0, node_path_1.join)(base, surface);
|
|
225
|
+
// Root surfaces were pre-read; user surfaces are read fresh here.
|
|
226
|
+
const tree = source.kind === "root"
|
|
227
|
+
? (rootTrees.get(surface) ?? {})
|
|
228
|
+
: isDir(dir)
|
|
229
|
+
? readTree(dir, dir)
|
|
230
|
+
: {};
|
|
231
|
+
for (const [rel, content] of Object.entries(tree)) {
|
|
232
|
+
add((0, node_path_1.join)(layout.materializeRoot, surface, rel), content, (0, node_path_1.join)(dir, rel));
|
|
233
|
+
}
|
|
234
|
+
counts[surface] = Object.keys(tree).length;
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case "none":
|
|
239
|
+
break;
|
|
240
|
+
/* v8 ignore next 2 -- exhaustiveness guard, unreachable given SurfaceSource */
|
|
241
|
+
default:
|
|
242
|
+
(0, hash_js_1.assertNever)(source);
|
|
243
|
+
}
|
|
244
|
+
return counts;
|
|
245
|
+
}
|
|
169
246
|
/**
|
|
170
247
|
* Flag surfaces present-but-not-deterministically-exercisable. Subagents
|
|
171
248
|
* (`agents/`) and slash commands (`commands/`) are materialized into the sandbox
|
package/dist/scan.d.ts
CHANGED
|
@@ -301,7 +301,10 @@ export declare function isManagedHookCommand(command: string): boolean;
|
|
|
301
301
|
/** The `prefer-compiled-hooks` recommendation message (shared by `lint` + `scan`). */
|
|
302
302
|
export declare function preferCompiledHooksMessage(count: number): string;
|
|
303
303
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
304
|
-
export declare function scanPlugin(dir: string, layout?: PluginLayout, dialect?: HarnessDialect
|
|
304
|
+
export declare function scanPlugin(dir: string, layout?: PluginLayout, dialect?: HarnessDialect, opts?: {
|
|
305
|
+
sharedDirs?: readonly string[];
|
|
306
|
+
sharedDirsRoot?: string;
|
|
307
|
+
}): ScanReport;
|
|
305
308
|
/**
|
|
306
309
|
* LIVE MCP tool resolution for a scanned plugin — the dynamic check no static
|
|
307
310
|
* linter can do: it STARTS each declared MCP server and checks every
|
package/dist/scan.js
CHANGED
|
@@ -151,11 +151,18 @@ function onDiskPath(materializedKey, materializeRoot) {
|
|
|
151
151
|
: materializedKey;
|
|
152
152
|
}
|
|
153
153
|
function scanSkills(files, cls, ctx) {
|
|
154
|
-
const { root, materializeRoot, dialect } = ctx;
|
|
154
|
+
const { root, materializeRoot, dialect, sharedDirs } = ctx;
|
|
155
|
+
// `sharedDirs` are declared relative to the REPO root (config location), which
|
|
156
|
+
// is `root` for a whole-repo scan but a PARENT when the scan is scoped to a
|
|
157
|
+
// subdir. Resolve them against that, not the scoped subdir.
|
|
158
|
+
const sharedDirsRoot = ctx.sharedDirsRoot ?? root;
|
|
155
159
|
const out = [];
|
|
156
160
|
for (const [path, md] of Object.entries(files)) {
|
|
157
161
|
if (!cls.isSkill(path))
|
|
158
162
|
continue;
|
|
163
|
+
// Prefer the real on-disk dir (a `.claude/skills/…` skill materializes under
|
|
164
|
+
// the same canonical key as a repo-root one, but lives elsewhere on disk).
|
|
165
|
+
const onDiskDir = ctx.sources?.[path];
|
|
159
166
|
const fm = frontmatter(md);
|
|
160
167
|
// A skill's trigger surface is its frontmatter `description` OR — when that's
|
|
161
168
|
// absent — Claude Code's fallback to the first body paragraph. Only when
|
|
@@ -166,8 +173,18 @@ function scanSkills(files, cls, ctx) {
|
|
|
166
173
|
// Bundled-resource refs resolve against the skill's OWN dir (resources ship
|
|
167
174
|
// beside the SKILL.md), built from the plugin root + the file's ON-DISK dir
|
|
168
175
|
// (the materialize-root prefix the loader added is stripped back off).
|
|
169
|
-
const skillDir =
|
|
170
|
-
|
|
176
|
+
const skillDir = onDiskDir
|
|
177
|
+
? (0, node_path_1.dirname)(onDiskDir)
|
|
178
|
+
: (0, node_path_1.resolve)(root, (0, node_path_1.dirname)(onDiskPath(path, materializeRoot)));
|
|
179
|
+
// Bundled refs resolve against the skill's OWN dir. A repo that shares a
|
|
180
|
+
// top-level tree across skills (`sharedDirs` in .vigilesrc.json) ALSO resolves
|
|
181
|
+
// a ref under one of those declared dirs against the repo root — OPT-IN, so a
|
|
182
|
+
// repo that doesn't set it is byte-identical to before (no masking of a real
|
|
183
|
+
// missing bundled resource). See feedback P1-4.
|
|
184
|
+
const resourceIssues = (0, skill_resources_js_1.skillResourceIssues)(skillBody(md), skillDir, {
|
|
185
|
+
repoRoot: sharedDirsRoot,
|
|
186
|
+
sharedDirs,
|
|
187
|
+
});
|
|
171
188
|
// The lethal trifecta is a property of what a unit CAN do, which for a skill is
|
|
172
189
|
// its declared `allowed-tools` (the CC skill tool contract). Only a model-
|
|
173
190
|
// invocable skill can be hijacked by attacker content, so a user-invoked one is
|
|
@@ -702,7 +719,7 @@ function summarizePurity(agents) {
|
|
|
702
719
|
}, { pure: 0, bounded: 0, unrestricted: 0 });
|
|
703
720
|
}
|
|
704
721
|
/** Scan a plugin/repo directory and report its surfaces + structural issues. */
|
|
705
|
-
function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
722
|
+
function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect, opts = {}) {
|
|
706
723
|
const lay = layout ?? layout_js_1.claudeCodeLayout;
|
|
707
724
|
const cls = makeClassifier(lay);
|
|
708
725
|
const loaded = (0, plugin_loader_js_1.loadPlugin)(dir, lay);
|
|
@@ -734,6 +751,9 @@ function scanPlugin(dir, layout, dialect = dialect_js_1.claudeCodeDialect) {
|
|
|
734
751
|
root: (0, node_path_1.resolve)(dir),
|
|
735
752
|
materializeRoot: lay.materializeRoot,
|
|
736
753
|
dialect,
|
|
754
|
+
sources: loaded.sources,
|
|
755
|
+
sharedDirs: opts.sharedDirs,
|
|
756
|
+
sharedDirsRoot: opts.sharedDirsRoot,
|
|
737
757
|
});
|
|
738
758
|
const puritySummary = summarizePurity(agents);
|
|
739
759
|
const { trifectaFindings, skillResourceFindings, skillFenceFindings } = collectSurfaceFindings(agents, skills);
|
|
@@ -958,7 +978,18 @@ function formatScanReport(r) {
|
|
|
958
978
|
out.push(...section("MCP hook targets", r.mcpHookIssues.map((i) => ` ✗ ${i.message}`)));
|
|
959
979
|
out.push(...section("Description overlap (precision risk)", r.descriptionOverlaps.map((o) => ` ⚠ ${o.message}`)));
|
|
960
980
|
out.push(...section("Description budget (trigger-signal risk)", r.descriptionBudgetIssues.map((o) => ` ⚠ ${o.message}`)));
|
|
961
|
-
out.push(...section("Lethal trifecta (prompt-injection exfil risk)",
|
|
981
|
+
out.push(...section("Lethal trifecta (prompt-injection exfil risk)",
|
|
982
|
+
// The section header already carries the count. HARD findings name their
|
|
983
|
+
// specific legs (keep the message). ADVISORY (inherits-all) findings all
|
|
984
|
+
// carry the SAME boilerplate paragraph — at bulk that's a wall of identical
|
|
985
|
+
// text, so collapse each to a one-liner (feedback P2-6). Gate on "no NEW
|
|
986
|
+
// trifecta" with `vigiles lint` (the lethal-trifecta rule), not by eyeballing.
|
|
987
|
+
r.trifectaFindings.map((t) => {
|
|
988
|
+
const mark = t.finding.severity === "hard" ? "✗" : "⚠";
|
|
989
|
+
return t.finding.severity === "hard"
|
|
990
|
+
? ` ${mark} ${t.kind} ${t.name} (${t.path}): ${t.finding.message}`
|
|
991
|
+
: ` ${mark} ${t.kind} ${t.name} (${t.path}) — inherits-all contract holds all three legs (declare a tools list dropping one)`;
|
|
992
|
+
})));
|
|
962
993
|
out.push(...section("Skill bundled resources", r.skillResourceIssues.map((s) => ` ✗ ${s.name}: ${s.finding.ref} (line ${String(s.finding.line)}) — bundled resource not found`)));
|
|
963
994
|
out.push(...section("Invisible skills (missing frontmatter fence)", r.skillFenceIssues.map((s) => ` ✗ ${s.name} (${s.path}): opens with \`${s.finding.key}:\` but no \`---\` fence — loads as body, never fires`)));
|
|
964
995
|
out.push(...section("Misplaced plugin directories", r.pluginLayoutIssues.map((p) => ` ✗ ${p.message}`)));
|
package/dist/test-coverage.js
CHANGED
|
@@ -86,6 +86,22 @@ function discoverSkills(basePath, ignore, layout) {
|
|
|
86
86
|
ignored: content.includes(IGNORE_MARKER),
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
+
// Single-skill-directory target: a bare `SKILL.md` AT the base (the dir you
|
|
90
|
+
// pointed lint/audit at). The globs above only match `<skillDir>/*/SKILL.md`
|
|
91
|
+
// NESTED under the base, so without this the untested-skill check would silently
|
|
92
|
+
// vanish for exactly the single-skill target that scoping now supports.
|
|
93
|
+
const rootSkill = (0, node_path_1.join)(basePath, "SKILL.md");
|
|
94
|
+
if ((0, node_fs_1.existsSync)(rootSkill)) {
|
|
95
|
+
const name = (0, node_path_1.basename)(basePath);
|
|
96
|
+
const content = read(rootSkill);
|
|
97
|
+
out.push({
|
|
98
|
+
kind: "skill",
|
|
99
|
+
path: "SKILL.md",
|
|
100
|
+
name,
|
|
101
|
+
tokens: [`${layout.skillDir}/${name}`, `:${name}`],
|
|
102
|
+
ignored: content.includes(IGNORE_MARKER),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
89
105
|
return out;
|
|
90
106
|
}
|
|
91
107
|
function discoverAgents(basePath, ignore, layout) {
|
|
@@ -171,7 +187,13 @@ function discoverTests(basePath, globs, ignore) {
|
|
|
171
187
|
/** Colocated: a test inside a skill dir, or a name-prefixed sibling of an agent/hook. */
|
|
172
188
|
function isColocated(surface, testPath) {
|
|
173
189
|
if (surface.kind === "skill") {
|
|
174
|
-
|
|
190
|
+
const dir = (0, node_path_1.dirname)(surface.path);
|
|
191
|
+
// A root `SKILL.md` (single-skill-dir target) lives at ".", so any TOP-LEVEL
|
|
192
|
+
// test is colocated — globSync returns those without a "./" prefix, which a
|
|
193
|
+
// bare `startsWith("./")` would miss (false "untested").
|
|
194
|
+
return dir === "."
|
|
195
|
+
? (0, node_path_1.dirname)(testPath) === "."
|
|
196
|
+
: testPath.startsWith(`${dir}/`);
|
|
175
197
|
}
|
|
176
198
|
return ((0, node_path_1.dirname)(testPath) === (0, node_path_1.dirname)(surface.path) &&
|
|
177
199
|
(0, node_path_1.basename)(testPath).startsWith(`${surface.name}.`));
|
|
@@ -223,10 +245,14 @@ function findUntestedSurfaces(options = {}) {
|
|
|
223
245
|
}
|
|
224
246
|
/** Suggested colocated test path for an untested surface (shown in the warning). */
|
|
225
247
|
function suggestedTestPath(surface) {
|
|
248
|
+
// A root skill lives at ".", so drop the "./" prefix — the suggested path then
|
|
249
|
+
// matches what globSync actually discovers at the top level.
|
|
250
|
+
const dir = (0, node_path_1.dirname)(surface.path);
|
|
251
|
+
const prefix = dir === "." ? "" : `${dir}/`;
|
|
226
252
|
if (surface.kind === "skill") {
|
|
227
|
-
return `${
|
|
253
|
+
return `${prefix}${surface.name}.eval.mjs`;
|
|
228
254
|
}
|
|
229
|
-
return `${
|
|
255
|
+
return `${prefix}${surface.name}.harness.mjs`;
|
|
230
256
|
}
|
|
231
257
|
/** Format an untested-surface report as human-readable text. */
|
|
232
258
|
function formatUntestedReport(report) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.7.0",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|