vigiles 10.0.0 → 11.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.
Files changed (38) hide show
  1. package/README.md +113 -83
  2. package/dist/adapters/claude-code/dialect.js +15 -0
  3. package/dist/audit-report.d.ts +1 -1
  4. package/dist/audit-report.template.html +1 -1
  5. package/dist/audit-score.d.ts +19 -12
  6. package/dist/audit-score.js +65 -11
  7. package/dist/cli.js +249 -0
  8. package/dist/core/CLAUDE.md.spec.d.ts +3 -0
  9. package/dist/core/CLAUDE.md.spec.js +26 -0
  10. package/dist/core/delegation-trifecta.d.ts +64 -0
  11. package/dist/core/delegation-trifecta.js +124 -0
  12. package/dist/core/dialect.d.ts +18 -0
  13. package/dist/core/hook-block-ineffective.d.ts +62 -0
  14. package/dist/core/hook-block-ineffective.js +153 -0
  15. package/dist/core/hook-matcher.d.ts +66 -0
  16. package/dist/core/hook-matcher.js +182 -0
  17. package/dist/core/hook-normalize.d.ts +43 -0
  18. package/dist/core/hook-normalize.js +78 -0
  19. package/dist/core/lethal-trifecta.d.ts +100 -0
  20. package/dist/core/lethal-trifecta.js +197 -0
  21. package/dist/core/plugin-dir-layout.d.ts +30 -0
  22. package/dist/core/plugin-dir-layout.js +73 -0
  23. package/dist/core/rule-meta.d.ts +82 -0
  24. package/dist/core/rule-meta.js +266 -0
  25. package/dist/core/skill-missing-fence.d.ts +47 -0
  26. package/dist/core/skill-missing-fence.js +119 -0
  27. package/dist/core/skill-resources.d.ts +27 -0
  28. package/dist/core/skill-resources.js +167 -0
  29. package/dist/core/types.d.ts +71 -0
  30. package/dist/core/validate.d.ts +1 -0
  31. package/dist/core/validate.js +26 -4
  32. package/dist/leaderboard.d.ts +1 -0
  33. package/dist/leaderboard.js +42 -4
  34. package/dist/scan.d.ts +106 -0
  35. package/dist/scan.js +251 -45
  36. package/dist/setup-plan.d.ts +6 -3
  37. package/dist/setup-plan.js +12 -2
  38. package/package.json +1 -1
@@ -220,6 +220,77 @@ export interface RulesConfig {
220
220
  * as `scan` (mcpHookIssues).
221
221
  */
222
222
  "mcp-hook-target-resolves"?: RuleSeverity;
223
+ /**
224
+ * Flag a unit (subagent / model-invocable skill) whose declared tools hold all
225
+ * THREE legs of Simon Willison's "lethal trifecta" — read private data, ingest
226
+ * untrusted content, AND exfiltrate — a prompt-injection exfil path with no
227
+ * exploit code (Meta's Rule of Two: allow at most two). A capability SET-
228
+ * intersection over the declared contract, NOT a text scan; high-precision (only
229
+ * well-known tools map to a leg). An EXPLICIT all-three contract is a "hard"
230
+ * finding; an inherits-all unit (no contract → every leg) is "advisory". Default
231
+ * "warn" (don't-cry-wolf rollout); raise to "error" to gate CI. Same detector as
232
+ * `scan` (lethalTrifectaIssues). See docs/rules/lethal-trifecta.md.
233
+ */
234
+ "lethal-trifecta"?: RuleSeverity;
235
+ /**
236
+ * Flag a SKILL.md body referencing a bundled file (`scripts/`/`references/`/
237
+ * `assets/`, or a relative markdown link with an extension) that doesn't exist
238
+ * on disk under the skill dir — the agent reads the instruction, gets nothing,
239
+ * and silently continues. The cross-reference moat applied to the SKILL.md body.
240
+ * High-precision / FP-safe (skips URLs, `$VAR` tokens, `../` escapes, extension-
241
+ * less mentions). Default "warn"; raise to "error" to gate CI. Same detector as
242
+ * `scan` (skillResourceIssues). See docs/rules/skill-resource-resolves.md.
243
+ */
244
+ "skill-resource-resolves"?: RuleSeverity;
245
+ /**
246
+ * Flag a SKILL.md that opens with frontmatter-looking keys (`name:`,
247
+ * `description:`, …) but has NO opening `---` fence — the harness loads the
248
+ * whole file as body, so the skill has no name/description/trigger and is
249
+ * invisible (never fires). High-precision (a fixed key whitelist; markdown /
250
+ * prose lines never match). Default "warn"; raise to "error" to gate CI. Same
251
+ * detector as `scan` (skillFenceIssues). See docs/rules/skill-missing-fence.md.
252
+ */
253
+ "skill-missing-fence"?: RuleSeverity;
254
+ /**
255
+ * Flag a functional surface directory (skills/agents/commands) nested INSIDE
256
+ * the `.claude-plugin/` manifest dir, where only `plugin.json` belongs — the
257
+ * harness can't see it, so the surface is invisible (the #1 plugin-author
258
+ * mistake). Pure filesystem check, FP-safe. Default "warn"; raise to "error"
259
+ * to gate CI. Same detector as `scan` (pluginLayoutIssues). See
260
+ * docs/rules/plugin-dir-layout.md.
261
+ */
262
+ "plugin-dir-layout"?: RuleSeverity;
263
+ /**
264
+ * Flag a lethal trifecta that EMERGES across a delegation edge — a subagent
265
+ * whose effective (own ∪ delegated-to) capability holds all three legs though
266
+ * no single unit does (the combined blast radius). The capability-diff across
267
+ * the delegation tree; skips units the per-unit `lethal-trifecta` already
268
+ * flags (no double-report), FP-safe (explicit edges, wildcard-guarded). Default
269
+ * "warn"; raise to "error" to gate CI. Same detector as `scan`
270
+ * (delegationTrifecta). See docs/rules/delegation-trifecta.md.
271
+ */
272
+ "delegation-trifecta"?: RuleSeverity;
273
+ /**
274
+ * Flag a hook that LOOKS like it blocks but silently doesn't — a block decision
275
+ * (`exit 2` / `decision` / `permissionDecision`) on an event that can't veto, or
276
+ * the legacy top-level `decision` field on a permission-gated event where only
277
+ * `hookSpecificOutput.permissionDecision` works (#19009, the #1 verified hook
278
+ * pain). FP-safe (conservative literal patterns; the blocking-event sets are
279
+ * read from the dialect, so it runs only where they're declared). Default "warn";
280
+ * raise to "error" to gate CI. Same detector as `scan` (hookBlockFindings). See
281
+ * docs/rules/hook-block-ineffective.md.
282
+ */
283
+ "hook-block-ineffective"?: RuleSeverity;
284
+ /**
285
+ * Flag a hook `matcher` string that silently never fires — a close typo of a
286
+ * built-in tool (`bash`→`Bash`), or a malformed/undeclared MCP form
287
+ * (`mcp_memory_*` instead of `mcp__memory__.*`, or a server the plugin doesn't
288
+ * declare). High-precision (close-typo only; MCP gated on a declared set,
289
+ * built-ins allowlisted; wildcards/regex skipped). Default "warn"; raise to
290
+ * "error" to gate CI. Same detector as `scan` (hookMatcherFindings). See
291
+ * docs/rules/hook-matcher.md.
292
+ */
293
+ "hook-matcher"?: RuleSeverity;
223
294
  }
224
295
  /** Extract severity from a rule value (handles both simple and tuple forms). */
225
296
  export declare function ruleSeverity<T>(rule: RuleWithOptions<T> | undefined): RuleSeverity;
@@ -1,5 +1,6 @@
1
1
  import type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions } from "./types.js";
2
2
  export type { ParsedRule, ValidationError, ValidationResult, ReadResult, FileResult, ValidatePathsResult, RulesConfig, VigilesConfig, MarkerType, ParseOptions, ValidateOptions, ValidatePathsOptions, ReadOptions, };
3
+ export declare const DEFAULT_RULES: Required<RulesConfig>;
3
4
  export declare function findInstructionFiles(cwd?: string, configFiles?: string[]): string[];
4
5
  export declare function loadConfig(): VigilesConfig;
5
6
  export declare function parseRules(content: string, { ruleMarkers }?: ParseOptions): ParsedRule[];
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_RULES = void 0;
3
4
  exports.findInstructionFiles = findInstructionFiles;
4
5
  exports.loadConfig = loadConfig;
5
6
  exports.parseRules = parseRules;
@@ -28,7 +29,7 @@ const VALID_MARKERS = ["headings", "checkboxes"];
28
29
  const INSTRUCTION_FILES = ["CLAUDE.md", "AGENTS.md"];
29
30
  // The default instruction file to validate when no config names one.
30
31
  const DEFAULT_FILES = [INSTRUCTION_FILES[0]];
31
- const DEFAULT_RULES = {
32
+ exports.DEFAULT_RULES = {
32
33
  "require-instructions-spec": "warn",
33
34
  // Default OFF — the consistent `require-<surface>-spec` parallel. Skills are
34
35
  // legitimately hand-written, so requiring a .spec.ts per SKILL.md is the wrong
@@ -70,10 +71,31 @@ const DEFAULT_RULES = {
70
71
  "frontmatter-valid": "warn",
71
72
  // A mcp_tool hook incomplete / targeting an undeclared server — on by default at warn.
72
73
  "mcp-hook-target-resolves": "warn",
74
+ // Lethal-trifecta capability set-intersection (read-private + ingest-untrusted +
75
+ // exfiltrate in one unit) — WARN by default (don't-cry-wolf rollout); raise to error.
76
+ "lethal-trifecta": "warn",
77
+ // A SKILL.md body referencing a missing bundled resource — WARN by default
78
+ // (don't-cry-wolf rollout, FP-safe); raise to error to gate CI.
79
+ "skill-resource-resolves": "warn",
80
+ // A SKILL.md missing its opening `---` fence (invisible skill) — WARN by
81
+ // default (FP-safe key whitelist); raise to error to gate CI.
82
+ "skill-missing-fence": "warn",
83
+ // Functional dirs nested inside `.claude-plugin/` (invisible surfaces) — WARN
84
+ // by default; raise to error to gate CI.
85
+ "plugin-dir-layout": "warn",
86
+ // A lethal trifecta emerging across a delegation edge (combined blast radius) —
87
+ // WARN by default (don't-cry-wolf rollout); raise to error to gate CI.
88
+ "delegation-trifecta": "warn",
89
+ // A hook that looks like it blocks but silently doesn't (#19009) — WARN by
90
+ // default (FP-safe literal patterns); raise to error to gate CI.
91
+ "hook-block-ineffective": "warn",
92
+ // A hook matcher that never fires (tool typo / wrong MCP form) — WARN by
93
+ // default (high-precision); raise to error to gate CI.
94
+ "hook-matcher": "warn",
73
95
  };
74
96
  const DEFAULT_CONFIG = {
75
97
  ruleMarkers: ["headings", "checkboxes"],
76
- rules: DEFAULT_RULES,
98
+ rules: exports.DEFAULT_RULES,
77
99
  files: DEFAULT_FILES,
78
100
  };
79
101
  // ---------------------------------------------------------------------------
@@ -99,7 +121,7 @@ function loadConfig() {
99
121
  const config = {
100
122
  ...DEFAULT_CONFIG,
101
123
  ...userConfig,
102
- rules: { ...DEFAULT_RULES, ...userConfig.rules },
124
+ rules: { ...exports.DEFAULT_RULES, ...userConfig.rules },
103
125
  files: Array.isArray(userConfig.files) ? userConfig.files : DEFAULT_FILES,
104
126
  };
105
127
  if (!Array.isArray(config.ruleMarkers) ||
@@ -170,7 +192,7 @@ function parseRules(content, { ruleMarkers } = {}) {
170
192
  // Core validation
171
193
  // ---------------------------------------------------------------------------
172
194
  function validate(content, { ruleMarkers, rules: rulesConfig, filePath, dialect } = {}) {
173
- const activeRules = rulesConfig ?? DEFAULT_RULES;
195
+ const activeRules = rulesConfig ?? exports.DEFAULT_RULES;
174
196
  const parsedRules = parseRules(content, { ruleMarkers });
175
197
  const enforced = parsedRules.filter((r) => r.enforcement === "enforced").length;
176
198
  const guidanceOnly = parsedRules.filter((r) => r.enforcement === "guidance").length;
@@ -26,6 +26,7 @@ export declare const W_NO_DESCRIPTION = 10;
26
26
  export declare const W_DANGLING_REF = 8;
27
27
  export declare const W_OVERLAP = 8;
28
28
  export declare const W_NO_CONTRACT = 5;
29
+ export declare const W_TRIFECTA = 20;
29
30
  /** Map a 0–100 structural-health score to its letter grade (A ≥90 … F <60). */
30
31
  export declare function gradeFor(score: number): PluginScore["grade"];
31
32
  /** One deduction: a count, its per-item weight, and the label if non-zero. */
@@ -12,7 +12,7 @@
12
12
  * model and stack on top later; this part runs anywhere in CI for free.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.W_NO_CONTRACT = exports.W_OVERLAP = exports.W_DANGLING_REF = exports.W_NO_DESCRIPTION = exports.W_MISSING_HOOK = void 0;
15
+ exports.W_TRIFECTA = exports.W_NO_CONTRACT = exports.W_OVERLAP = exports.W_DANGLING_REF = exports.W_NO_DESCRIPTION = exports.W_MISSING_HOOK = void 0;
16
16
  exports.gradeFor = gradeFor;
17
17
  exports.reportDeductions = reportDeductions;
18
18
  exports.isEmptyMachine = isEmptyMachine;
@@ -49,6 +49,7 @@ exports.W_NO_DESCRIPTION = 10; // a skill with no usable description → can't t
49
49
  exports.W_DANGLING_REF = 8; // a referenced intra-plugin file that's missing → broken path
50
50
  exports.W_OVERLAP = 8; // a description collision → the wrong skill fires
51
51
  exports.W_NO_CONTRACT = 5; // generic small-footgun weight (disallowedTools typo, invalid model/color)
52
+ exports.W_TRIFECTA = 20; // a HARD lethal-trifecta contract (all three legs, explicit) → a declared prompt-injection exfil path
52
53
  // Two things are advisory, NOT graded penalties (shown, never scored — see scoreReport):
53
54
  // - untested surfaces — a hardening gap, not breakage.
54
55
  // - an agent that inherits all tools (no `tools:` line) — see reportDeductions for why.
@@ -77,7 +78,17 @@ function reportDeductions(r) {
77
78
  const deadTools = r.agents.reduce((n, a) => n + a.toolIssues.length, 0);
78
79
  const deadMcpTools = r.agents.reduce((n, a) => n + a.mcpToolIssues.length, 0);
79
80
  const deadDisallowed = r.agents.reduce((n, a) => n + a.disallowedToolIssues.length, 0);
81
+ // HARD lethal-trifecta findings only — an EXPLICIT contract naming all three
82
+ // legs (a declared exfil path). Advisory (inherits-all) trifecta findings are
83
+ // surfaced but NEVER graded (aligned with the inherits-all stance), so they're
84
+ // excluded here.
85
+ const hardTrifecta = r.trifectaFindings.filter((f) => f.finding.severity === "hard").length;
80
86
  return [
87
+ {
88
+ n: hardTrifecta,
89
+ weight: exports.W_TRIFECTA,
90
+ label: "unit(s) holding all three lethal-trifecta legs (prompt-injection exfil path)",
91
+ },
81
92
  {
82
93
  n: missingHooks,
83
94
  weight: exports.W_MISSING_HOOK,
@@ -146,6 +157,33 @@ function reportDeductions(r) {
146
157
  weight: exports.W_DANGLING_REF,
147
158
  label: "mcp_tool hook(s) incomplete / targeting an undeclared server",
148
159
  },
160
+ {
161
+ n: r.skillResourceIssues.length,
162
+ weight: exports.W_DANGLING_REF,
163
+ label: "skill bundled-resource ref(s) that don't resolve on disk",
164
+ },
165
+ {
166
+ n: r.skillFenceIssues.length,
167
+ weight: exports.W_NO_DESCRIPTION,
168
+ label: "invisible skill(s) (frontmatter with no opening `---` fence)",
169
+ },
170
+ {
171
+ n: r.pluginLayoutIssues.length,
172
+ weight: exports.W_NO_DESCRIPTION,
173
+ label: "functional dir(s) misplaced inside `.claude-plugin/` (invisible)",
174
+ },
175
+ {
176
+ n: r.hookBlockFindings.length,
177
+ weight: exports.W_MISSING_HOOK,
178
+ label: "hook(s) that look like they block but silently don't",
179
+ },
180
+ {
181
+ n: r.hookMatcherFindings.length,
182
+ weight: exports.W_MISSING_HOOK,
183
+ label: "hook matcher(s) that never fire (typo / wrong MCP form)",
184
+ },
185
+ // NB: delegationTrifecta (like the advisory per-unit/inherits-all trifecta) is a
186
+ // ⚠ RISK, surfaced but NOT graded — only the HARD per-unit trifecta above scores.
149
187
  // NB: untested surfaces are NOT a penalty — an untested surface is a hardening
150
188
  // gap, not breakage, so it never drags the health score (it's appended as an
151
189
  // advisory note below). The score ranks what's BROKEN.
@@ -239,11 +277,11 @@ function formatLeaderboard(scores) {
239
277
  const issue = s.issues.length > 0 ? ` — ${s.issues.join("; ")}` : "";
240
278
  out.push(` ${rank} ${score} ${s.grade} ${s.name}${issue}`);
241
279
  });
242
- out.push("", "Structural health only (no model). Weights: missing hook -15, no-description", "skill -10, broken intra-plugin ref -8, dead tool/MCP ref -8.", "Inherit-all subagents and untested surfaces are advisory — shown, not scored.");
280
+ out.push("", "Structural health only (no model). Weights: lethal-trifecta unit -20, missing", "hook -15, no-description skill -10, broken intra-plugin ref -8, dead tool/MCP", "ref -8. Inherit-all subagents and untested surfaces are advisory — shown, not", "scored.");
243
281
  return out.join("\n");
244
282
  }
245
- const LEADERBOARD_METHOD = "_Structural health only (deterministic, no model): missing hook −15, " +
246
- "no-description skill −10, broken intra-plugin / dead-tool ref −8. " +
283
+ const LEADERBOARD_METHOD = "_Structural health only (deterministic, no model): lethal-trifecta unit −20, " +
284
+ "missing hook −15, no-description skill −10, broken intra-plugin / dead-tool ref −8. " +
247
285
  "Inherit-all subagents and untested surfaces are advisory (shown, not " +
248
286
  "scored). Behavioural columns (trigger-rate, collisions, egress) stack on top._";
249
287
  /**
package/dist/scan.d.ts CHANGED
@@ -20,6 +20,13 @@ import { type DescriptionOverlap } from "./core/description-overlap.js";
20
20
  import { type McpToolIssue } from "./core/mcp-tool.js";
21
21
  import { type McpContractToolError } from "./core/mcp.js";
22
22
  import { type McpHookIssue } from "./core/mcp-hook.js";
23
+ import { type TrifectaFinding } from "./core/lethal-trifecta.js";
24
+ import { type SkillResourceFinding } from "./core/skill-resources.js";
25
+ import { type SkillFenceFinding } from "./core/skill-missing-fence.js";
26
+ import { type PluginLayoutFinding } from "./core/plugin-dir-layout.js";
27
+ import { type DelegationTrifectaFinding } from "./core/delegation-trifecta.js";
28
+ import { type HookBlockFinding } from "./core/hook-block-ineffective.js";
29
+ import { type HookMatcherFinding } from "./core/hook-matcher.js";
23
30
  import { type PurityLevel, type EffectSurface } from "./core/effects.js";
24
31
  /** A named writing system. The label `unexpectedScript` reports + the config's expectation parse into this. */
25
32
  export type Script = "Latin" | "Cyrillic" | "Han" | "Japanese" | "Korean" | "Arabic" | "Hebrew" | "Greek" | "Devanagari" | "Thai";
@@ -43,6 +50,27 @@ export interface ScanSkill {
43
50
  * `audit` trigger tier / `measureTriggerRate`.
44
51
  */
45
52
  readonly descriptionScript: Script | null;
53
+ /**
54
+ * SKILL.md body references to a bundled file (`scripts/`/`references/`/`assets/`
55
+ * or a relative markdown link with an extension) that don't resolve on disk
56
+ * under the skill dir — the agent reads the instruction and gets nothing.
57
+ * Computed by `skillResourceIssues()` (one detector, no drift).
58
+ */
59
+ readonly resourceIssues: readonly SkillResourceFinding[];
60
+ /**
61
+ * Lethal-trifecta finding when a MODEL-INVOCABLE skill's declared `allowed-tools`
62
+ * hold all three legs (read-private + ingest-untrusted + exfiltrate), else null.
63
+ * A user-invoked skill is excluded (it can't be selected by attacker content).
64
+ * Computed by `lethalTrifectaIssues()` (one detector, no drift).
65
+ */
66
+ readonly trifecta: TrifectaFinding | null;
67
+ /**
68
+ * Set when the SKILL.md opens with frontmatter-looking keys (`name:`, …) but
69
+ * has NO opening `---` fence, so the whole file loads as body — no name, no
70
+ * description, no trigger (the skill is invisible). Computed by
71
+ * `skillMissingFence()` (one detector, no drift).
72
+ */
73
+ readonly fenceIssue: SkillFenceFinding | null;
46
74
  }
47
75
  export interface ScanAgent {
48
76
  readonly name: string;
@@ -68,6 +96,37 @@ export interface ScanAgent {
68
96
  * unknown-effect (MCP or unrecognized) tool names in the declared contract.
69
97
  */
70
98
  readonly effectBuckets: Pick<EffectSurface, "readOnly" | "sideEffecting" | "unknown">;
99
+ /**
100
+ * Lethal-trifecta finding when the subagent's declared tools hold all three legs
101
+ * (read-private + ingest-untrusted + exfiltrate), else null. An inherits-all
102
+ * agent (no `tools:` line) is the "advisory" case. Computed by
103
+ * `lethalTrifectaIssues()` (one detector, no drift).
104
+ */
105
+ readonly trifecta: TrifectaFinding | null;
106
+ }
107
+ /** A lethal-trifecta finding tagged with the surface (subagent/skill) that holds it. */
108
+ export interface ScanTrifectaFinding {
109
+ readonly path: string;
110
+ readonly kind: "subagent" | "skill";
111
+ readonly name: string;
112
+ readonly finding: TrifectaFinding;
113
+ }
114
+ /** A SKILL.md body resource reference that doesn't resolve, tagged with the skill path. */
115
+ export interface ScanSkillResourceFinding {
116
+ readonly path: string;
117
+ readonly name: string;
118
+ readonly finding: SkillResourceFinding;
119
+ }
120
+ /** A missing-frontmatter-fence finding tagged with the skill path. */
121
+ export interface ScanSkillFenceFinding {
122
+ readonly path: string;
123
+ readonly name: string;
124
+ readonly finding: SkillFenceFinding;
125
+ }
126
+ /** A delegation-trifecta finding tagged with the subagent path that holds it. */
127
+ export interface ScanDelegationFinding {
128
+ readonly path: string;
129
+ readonly finding: DelegationTrifectaFinding;
71
130
  }
72
131
  /** A skill/agent whose frontmatter is missing a required field (name / description). */
73
132
  export interface FrontmatterIssue {
@@ -162,6 +221,53 @@ export interface ScanReport {
162
221
  readonly mcpHookIssues: readonly McpHookIssue[];
163
222
  /** Pairs of model-invocable skills whose descriptions are near-identical (precision collision). */
164
223
  readonly descriptionOverlaps: readonly DescriptionOverlap[];
224
+ /**
225
+ * Lethal-trifecta findings across subagents + model-invocable skills — a unit
226
+ * holding all three legs (read-private + ingest-untrusted + exfiltrate). Each
227
+ * carries the surface path + kind for reporting/annotations. Shared by `scan`
228
+ * (the report) and the `lethal-trifecta` lint rule (one detector, no drift).
229
+ */
230
+ readonly trifectaFindings: readonly ScanTrifectaFinding[];
231
+ /**
232
+ * SKILL.md body references to a bundled resource that doesn't resolve on disk,
233
+ * across all skills — each carries the skill path for reporting/annotations.
234
+ * Shared by `scan` and the `skill-resource-resolves` lint rule (one detector, no
235
+ * drift).
236
+ */
237
+ readonly skillResourceIssues: readonly ScanSkillResourceFinding[];
238
+ /**
239
+ * Skills whose frontmatter-looking opening has NO `---` fence, so they load as
240
+ * pure body (invisible — no name/description/trigger). Shared by `scan` and the
241
+ * `skill-missing-fence` lint rule (one detector, no drift).
242
+ */
243
+ readonly skillFenceIssues: readonly ScanSkillFenceFinding[];
244
+ /**
245
+ * Functional surface dirs (skills/agents/commands) nested INSIDE the manifest
246
+ * dir (`.claude-plugin/`) where the harness can't see them. Shared by `scan`
247
+ * and the `plugin-dir-layout` lint rule (one detector, no drift).
248
+ */
249
+ readonly pluginLayoutIssues: readonly PluginLayoutFinding[];
250
+ /**
251
+ * Lethal trifectas that EMERGE across a delegation edge — a subagent whose
252
+ * effective (own ∪ delegated-to) capability holds all three legs though no
253
+ * single unit does. Shared by `scan` and the `delegation-trifecta` lint rule
254
+ * (one detector, no drift).
255
+ */
256
+ readonly delegationTrifecta: readonly ScanDelegationFinding[];
257
+ /**
258
+ * Hooks that LOOK like they block but silently don't — a block decision on a
259
+ * non-blocking event, or the legacy `decision` field on a permission-gated
260
+ * event (#19009, the #1 verified hook pain). Shared by `scan` and the
261
+ * `hook-block-ineffective` lint rule (one detector, no drift). Empty when the
262
+ * dialect doesn't declare its blocking-event semantics.
263
+ */
264
+ readonly hookBlockFindings: readonly HookBlockFinding[];
265
+ /**
266
+ * Hook `matcher` strings that silently never fire — a tool-name typo or a
267
+ * malformed/undeclared MCP form. Shared by `scan` and the `hook-matcher` lint
268
+ * rule (one detector, no drift).
269
+ */
270
+ readonly hookMatcherFindings: readonly HookMatcherFinding[];
165
271
  /** Skills/agents whose `---` block isn't valid YAML — informational (may still load via salvage). */
166
272
  readonly malformedFrontmatter: readonly FrontmatterParseIssue[];
167
273
  readonly warnings: readonly string[];