@weareikko/eslint-plugin-skill 0.0.1 → 0.1.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 (105) hide show
  1. package/README.md +162 -2
  2. package/configs/check-refs.workflow.yml +25 -0
  3. package/configs/eslint.config.example.js +23 -0
  4. package/configs/lint-skills.workflow.yml +26 -0
  5. package/dist/dedup/analyze.d.ts +26 -0
  6. package/dist/dedup/analyze.d.ts.map +1 -0
  7. package/dist/dedup/cli.d.ts +10 -0
  8. package/dist/dedup/cli.d.ts.map +1 -0
  9. package/dist/dedup/cli.js +37 -0
  10. package/dist/dedup/cli.js.map +1 -0
  11. package/dist/dedup/discover.d.ts +16 -0
  12. package/dist/dedup/discover.d.ts.map +1 -0
  13. package/dist/dedup/hash.d.ts +17 -0
  14. package/dist/dedup/hash.d.ts.map +1 -0
  15. package/dist/dedup/index.d.ts +4 -0
  16. package/dist/dedup/index.d.ts.map +1 -0
  17. package/dist/dedup/index.js +2 -0
  18. package/dist/dedup/report.d.ts +6 -0
  19. package/dist/dedup/report.d.ts.map +1 -0
  20. package/dist/dedup/types.d.ts +92 -0
  21. package/dist/dedup/types.d.ts.map +1 -0
  22. package/dist/index.d.ts +109 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +1236 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/report-ClPKWYJy.js +266 -0
  27. package/dist/report-ClPKWYJy.js.map +1 -0
  28. package/dist/rules/body-max-length.d.ts +16 -0
  29. package/dist/rules/body-max-length.d.ts.map +1 -0
  30. package/dist/rules/description-has-trigger.d.ts +16 -0
  31. package/dist/rules/description-has-trigger.d.ts.map +1 -0
  32. package/dist/rules/description-length.d.ts +16 -0
  33. package/dist/rules/description-length.d.ts.map +1 -0
  34. package/dist/rules/description-no-html.d.ts +13 -0
  35. package/dist/rules/description-no-html.d.ts.map +1 -0
  36. package/dist/rules/descriptive-reference-filenames.d.ts +15 -0
  37. package/dist/rules/descriptive-reference-filenames.d.ts.map +1 -0
  38. package/dist/rules/h1-matches-name.d.ts +13 -0
  39. package/dist/rules/h1-matches-name.d.ts.map +1 -0
  40. package/dist/rules/large-reference-toc.d.ts +16 -0
  41. package/dist/rules/large-reference-toc.d.ts.map +1 -0
  42. package/dist/rules/meta-prefers-dispatch-table.d.ts +19 -0
  43. package/dist/rules/meta-prefers-dispatch-table.d.ts.map +1 -0
  44. package/dist/rules/multi-domain-has-variants.d.ts +16 -0
  45. package/dist/rules/multi-domain-has-variants.d.ts.map +1 -0
  46. package/dist/rules/name-kebab-case.d.ts +10 -0
  47. package/dist/rules/name-kebab-case.d.ts.map +1 -0
  48. package/dist/rules/name-matches-dir.d.ts +10 -0
  49. package/dist/rules/name-matches-dir.d.ts.map +1 -0
  50. package/dist/rules/no-broken-relative-links.d.ts +14 -0
  51. package/dist/rules/no-broken-relative-links.d.ts.map +1 -0
  52. package/dist/rules/no-multiple-h1.d.ts +15 -0
  53. package/dist/rules/no-multiple-h1.d.ts.map +1 -0
  54. package/dist/rules/no-orphan-references.d.ts +15 -0
  55. package/dist/rules/no-orphan-references.d.ts.map +1 -0
  56. package/dist/rules/no-unknown-frontmatter-keys.d.ts +18 -0
  57. package/dist/rules/no-unknown-frontmatter-keys.d.ts.map +1 -0
  58. package/dist/rules/procedural-has-steps.d.ts +15 -0
  59. package/dist/rules/procedural-has-steps.d.ts.map +1 -0
  60. package/dist/rules/reference-has-content.d.ts +15 -0
  61. package/dist/rules/reference-has-content.d.ts.map +1 -0
  62. package/dist/rules/refs-one-level-deep.d.ts +15 -0
  63. package/dist/rules/refs-one-level-deep.d.ts.map +1 -0
  64. package/dist/rules/require-frontmatter.d.ts +11 -0
  65. package/dist/rules/require-frontmatter.d.ts.map +1 -0
  66. package/dist/rules/valid-allowed-tools.d.ts +15 -0
  67. package/dist/rules/valid-allowed-tools.d.ts.map +1 -0
  68. package/dist/types.d.ts +15 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/utils/ast.d.ts +21 -0
  71. package/dist/utils/ast.d.ts.map +1 -0
  72. package/dist/utils/frontmatter.d.ts +24 -0
  73. package/dist/utils/frontmatter.d.ts.map +1 -0
  74. package/dist/utils/kind.d.ts +41 -0
  75. package/dist/utils/kind.d.ts.map +1 -0
  76. package/dist/utils/links.d.ts +23 -0
  77. package/dist/utils/links.d.ts.map +1 -0
  78. package/dist/utils/references.d.ts +7 -0
  79. package/dist/utils/references.d.ts.map +1 -0
  80. package/dist/utils/skill-file.d.ts +32 -0
  81. package/dist/utils/skill-file.d.ts.map +1 -0
  82. package/dist/utils/skill-fs.d.ts +30 -0
  83. package/dist/utils/skill-fs.d.ts.map +1 -0
  84. package/docs/check-refs.md +60 -0
  85. package/docs/rules/body-max-length.md +23 -0
  86. package/docs/rules/description-has-trigger.md +28 -0
  87. package/docs/rules/description-length.md +27 -0
  88. package/docs/rules/description-no-html.md +22 -0
  89. package/docs/rules/descriptive-reference-filenames.md +26 -0
  90. package/docs/rules/h1-matches-name.md +36 -0
  91. package/docs/rules/large-reference-toc.md +26 -0
  92. package/docs/rules/meta-prefers-dispatch-table.md +30 -0
  93. package/docs/rules/multi-domain-has-variants.md +26 -0
  94. package/docs/rules/name-kebab-case.md +31 -0
  95. package/docs/rules/name-matches-dir.md +29 -0
  96. package/docs/rules/no-broken-relative-links.md +25 -0
  97. package/docs/rules/no-multiple-h1.md +31 -0
  98. package/docs/rules/no-orphan-references.md +25 -0
  99. package/docs/rules/no-unknown-frontmatter-keys.md +37 -0
  100. package/docs/rules/procedural-has-steps.md +43 -0
  101. package/docs/rules/reference-has-content.md +26 -0
  102. package/docs/rules/refs-one-level-deep.md +26 -0
  103. package/docs/rules/require-frontmatter.md +34 -0
  104. package/docs/rules/valid-allowed-tools.md +27 -0
  105. package/package.json +55 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/utils/skill-file.ts","../src/utils/frontmatter.ts","../src/utils/ast.ts","../src/rules/require-frontmatter.ts","../src/rules/name-kebab-case.ts","../src/rules/name-matches-dir.ts","../src/rules/description-length.ts","../src/rules/description-has-trigger.ts","../src/rules/description-no-html.ts","../src/rules/no-multiple-h1.ts","../src/rules/h1-matches-name.ts","../src/rules/body-max-length.ts","../src/utils/links.ts","../src/utils/skill-fs.ts","../src/rules/no-broken-relative-links.ts","../src/rules/no-unknown-frontmatter-keys.ts","../src/rules/refs-one-level-deep.ts","../src/utils/references.ts","../src/rules/no-orphan-references.ts","../src/rules/descriptive-reference-filenames.ts","../src/rules/large-reference-toc.ts","../src/rules/valid-allowed-tools.ts","../src/utils/kind.ts","../src/rules/procedural-has-steps.ts","../src/rules/reference-has-content.ts","../src/rules/multi-domain-has-variants.ts","../src/rules/meta-prefers-dispatch-table.ts","../src/index.ts"],"sourcesContent":["/**\n * Helpers for recognizing and locating Claude Code skill files.\n *\n * Kept dependency-free and side-effect-free so the compiled output ports\n * cleanly to oxlint's ESLint-compatible JS plugin runtime.\n */\n\n/**\n * The canonical filename of a Claude Code Agent Skill.\n */\nexport const SKILL_FILENAME = \"SKILL.md\";\n\n/**\n * Extract the basename of a POSIX or Windows path without relying on `node:path`,\n * so the logic is identical regardless of the host runtime's path semantics.\n */\nexport function basename(filename: string): string {\n\tif (typeof filename !== \"string\") {\n\t\treturn \"\";\n\t}\n\tconst normalized = filename.replace(/\\\\/g, \"/\");\n\tconst trimmed = normalized.endsWith(\"/\")\n\t\t? normalized.slice(0, -1)\n\t\t: normalized;\n\tconst slash = trimmed.lastIndexOf(\"/\");\n\treturn slash === -1 ? trimmed : trimmed.slice(slash + 1);\n}\n\n/**\n * The name of the directory that directly contains the given file.\n */\nexport function dirName(filename: string): string {\n\tif (typeof filename !== \"string\") {\n\t\treturn \"\";\n\t}\n\tconst normalized = filename.replace(/\\\\/g, \"/\");\n\tconst trimmed = normalized.endsWith(\"/\")\n\t\t? normalized.slice(0, -1)\n\t\t: normalized;\n\tconst slash = trimmed.lastIndexOf(\"/\");\n\tif (slash === -1) {\n\t\treturn \"\";\n\t}\n\treturn basename(trimmed.slice(0, slash));\n}\n\n/**\n * Whether the linted file is a skill definition (`SKILL.md`).\n *\n * Rules call this first and no-op when it returns `false`, so the plugin is\n * safe even when its config globs every `*.md` file in a repository.\n */\nexport function isSkillFile(filename: string): boolean {\n\treturn basename(filename) === SKILL_FILENAME;\n}\n\n/**\n * Whether the linted file is a bundled reference document — a `.md` file\n * (other than `SKILL.md`) living under a `references/` directory.\n */\nexport function isReferenceFile(filename: string): boolean {\n\tif (typeof filename !== \"string\" || !filename.endsWith(\".md\")) {\n\t\treturn false;\n\t}\n\tif (basename(filename) === SKILL_FILENAME) {\n\t\treturn false;\n\t}\n\treturn filename.replace(/\\\\/g, \"/\").split(\"/\").includes(\"references\");\n}\n","/**\n * Parse the YAML frontmatter of a skill and resolve source locations for\n * individual keys, so rules can report precise `loc` ranges.\n *\n * `@eslint/markdown` (with `languageOptions.frontmatter: \"yaml\"`) surfaces\n * frontmatter as a single `yaml` mdast node whose `.value` is the raw YAML text\n * *without* the enclosing `---` fences. The node's `position.start.line` points\n * at the opening fence, so a key found on line `i` (0-indexed) of `.value` sits\n * on file line `node.position.start.line + 1 + i`.\n */\n\nimport type { Yaml } from \"mdast\";\nimport { parse as parseYaml } from \"yaml\";\n\nimport type { SourceRange } from \"../types\";\n\nexport interface Frontmatter {\n\t/** Parsed frontmatter object, or `null` when it isn't a mapping. */\n\tdata: Record<string, unknown> | null;\n\t/** Parse error message, if any. */\n\terror: string | null;\n\t/** The raw YAML text (fences excluded). */\n\traw: string;\n\t/** Best-effort source location for a top-level key. */\n\tkeyLoc: (key: string) => SourceRange;\n}\n\nexport function parseFrontmatter(node: Yaml): Frontmatter {\n\tconst raw = typeof node.value === \"string\" ? node.value : \"\";\n\tconst baseLine = node.position?.start?.line ?? 1;\n\t// Content begins one line after the opening `---` fence.\n\tconst contentStartLine = baseLine + 1;\n\tconst nodeLoc: SourceRange = node.position ?? {\n\t\tstart: { line: 1, column: 1 },\n\t\tend: { line: 1, column: 1 },\n\t};\n\n\tlet data: Record<string, unknown> | null = null;\n\tlet error: string | null = null;\n\ttry {\n\t\tconst parsed: unknown = parseYaml(raw);\n\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\tdata = parsed as Record<string, unknown>;\n\t\t} else if (parsed === null || parsed === undefined) {\n\t\t\tdata = {};\n\t\t} else {\n\t\t\terror = \"frontmatter is not a key/value mapping\";\n\t\t}\n\t} catch (e) {\n\t\terror = e instanceof Error ? e.message : String(e);\n\t}\n\n\tconst lines = raw.split(\"\\n\");\n\n\tfunction keyLoc(key: string): SourceRange {\n\t\tconst pattern = new RegExp(`^(\\\\s*)(${escapeRegExp(key)})\\\\s*:`);\n\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\tconst match = pattern.exec(lines[i]);\n\t\t\tif (match) {\n\t\t\t\tconst line = contentStartLine + i;\n\t\t\t\tconst column = match[1].length + 1;\n\t\t\t\treturn {\n\t\t\t\t\tstart: { line, column },\n\t\t\t\t\tend: { line, column: column + key.length },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\treturn { start: nodeLoc.start, end: nodeLoc.end };\n\t}\n\n\treturn { data, error, raw, keyLoc };\n}\n\nfunction escapeRegExp(value: string): string {\n\treturn value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n","/**\n * Tiny mdast traversal helpers.\n *\n * Rules use a single `root(node)` visitor and walk from there, which keeps them\n * independent of visitor ordering and free of `:exit`/esquery selectors — the\n * subset that ports directly to oxlint's JS plugin runtime.\n */\n\nimport type { Nodes, Root, Yaml } from \"mdast\";\n\n/**\n * The frontmatter `yaml` node, if present as a direct child of root.\n */\nexport function findFrontmatter(root: Root): Yaml | undefined {\n\treturn root.children.find((child): child is Yaml => child.type === \"yaml\");\n}\n\n/**\n * All nodes of a given type anywhere in the tree, in document order.\n */\nexport function collect<T extends Nodes = Nodes>(\n\tnode: Nodes,\n\ttype: T[\"type\"],\n): T[] {\n\tconst out: T[] = [];\n\tconst walk = (current: Nodes): void => {\n\t\tif (current.type === type) {\n\t\t\tout.push(current as T);\n\t\t}\n\t\tif (\"children\" in current) {\n\t\t\tfor (const child of current.children) {\n\t\t\t\twalk(child as Nodes);\n\t\t\t}\n\t\t}\n\t};\n\twalk(node);\n\treturn out;\n}\n\n/**\n * Concatenated text content of a node's subtree.\n */\nexport function textContent(node: Nodes): string {\n\tif (\"value\" in node && typeof node.value === \"string\") {\n\t\treturn node.value;\n\t}\n\tif (\"children\" in node) {\n\t\treturn node.children.map((child) => textContent(child as Nodes)).join(\"\");\n\t}\n\treturn \"\";\n}\n","/**\n * @fileoverview Require `name` and `description` frontmatter on a skill.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\n\ntype MessageIds =\n\t| \"missingFrontmatter\"\n\t| \"invalidFrontmatter\"\n\t| \"missingKey\"\n\t| \"emptyKey\";\n\nconst rule: MarkdownRuleDefinition<{ RuleOptions: []; MessageIds: MessageIds }> =\n\t{\n\t\tmeta: {\n\t\t\ttype: \"problem\",\n\t\t\tdocs: {\n\t\t\t\tdescription:\n\t\t\t\t\t\"Require `name` and `description` frontmatter on a skill\",\n\t\t\t\trecommended: true,\n\t\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/require-frontmatter.md\",\n\t\t\t},\n\t\t\tschema: [],\n\t\t\tmessages: {\n\t\t\t\tmissingFrontmatter:\n\t\t\t\t\t\"SKILL.md must start with YAML frontmatter containing `name` and `description`.\",\n\t\t\t\tinvalidFrontmatter:\n\t\t\t\t\t\"Skill frontmatter is not valid YAML: {{reason}}\",\n\t\t\t\tmissingKey: \"Skill frontmatter is missing a `{{key}}` field.\",\n\t\t\t\temptyKey: \"Skill frontmatter field `{{key}}` must not be empty.\",\n\t\t\t},\n\t\t},\n\n\t\tcreate(context) {\n\t\t\tif (!isSkillFile(context.filename)) {\n\t\t\t\treturn {};\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\troot(root) {\n\t\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\t\tif (!node) {\n\t\t\t\t\t\tcontext.report({ loc: TOP, messageId: \"missingFrontmatter\" });\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst { data, error, keyLoc } = parseFrontmatter(node);\n\t\t\t\t\tif (error || !data) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tloc: node.position ?? TOP,\n\t\t\t\t\t\t\tmessageId: \"invalidFrontmatter\",\n\t\t\t\t\t\t\tdata: { reason: error ?? \"unknown error\" },\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const key of [\"name\", \"description\"]) {\n\t\t\t\t\t\tif (!(key in data)) {\n\t\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\t\tloc: node.position ?? TOP,\n\t\t\t\t\t\t\t\tmessageId: \"missingKey\",\n\t\t\t\t\t\t\t\tdata: { key },\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst value = data[key];\n\t\t\t\t\t\tif (typeof value !== \"string\" || value.trim() === \"\") {\n\t\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\t\tloc: keyLoc(key),\n\t\t\t\t\t\t\t\tmessageId: \"emptyKey\",\n\t\t\t\t\t\t\t\tdata: { key },\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t};\n\nexport default rule;\n","/**\n * @fileoverview Require the skill `name` to be letter-start kebab-case.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;\nconst MAX_LENGTH = 64;\n// The Agent Skills spec forbids names built from reserved words.\nconst RESERVED = [\"anthropic\", \"claude\"];\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"notKebab\" | \"tooLong\" | \"reservedWord\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Require the skill `name` to be a valid Agent Skills identifier\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/name-kebab-case.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tnotKebab:\n\t\t\t\t\"Skill `name` must be letter-start kebab-case (e.g. `my-skill`), got `{{name}}`.\",\n\t\t\ttooLong:\n\t\t\t\t\"Skill `name` must be at most {{max}} characters; got {{length}}.\",\n\t\t\treservedWord:\n\t\t\t\t\"Skill `name` must not contain the reserved word `{{word}}`.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data || typeof data.name !== \"string\" || data.name === \"\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst name = data.name;\n\t\t\t\tconst loc = keyLoc(\"name\");\n\n\t\t\t\tif (!KEBAB.test(name)) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc,\n\t\t\t\t\t\tmessageId: \"notKebab\",\n\t\t\t\t\t\tdata: { name },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (name.length > MAX_LENGTH) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc,\n\t\t\t\t\t\tmessageId: \"tooLong\",\n\t\t\t\t\t\tdata: { max: MAX_LENGTH, length: name.length },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst lowered = name.toLowerCase();\n\t\t\t\tconst reserved = RESERVED.find((word) => lowered.includes(word));\n\t\t\t\tif (reserved) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc,\n\t\t\t\t\t\tmessageId: \"reservedWord\",\n\t\t\t\t\t\tdata: { word: reserved },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require the skill `name` to match its containing directory.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile, dirName } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"mismatch\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Require the skill `name` to match its containing directory\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/name-matches-dir.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tmismatch:\n\t\t\t\t\"Skill `name` (`{{name}}`) must match its directory name (`{{dir}}`).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst dir = dirName(context.filename);\n\t\t// Without a real path (e.g. RuleTester default `<input>`), there is no\n\t\t// directory to compare against.\n\t\tif (!dir) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data || typeof data.name !== \"string\" || data.name === \"\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (data.name !== dir) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: keyLoc(\"name\"),\n\t\t\t\t\t\tmessageId: \"mismatch\",\n\t\t\t\t\t\tdata: { name: data.name, dir },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Cap the length of the skill `description`.\n *\n * The description is the only text an agent sees when deciding whether to load\n * a skill, so it must stay compact. The default ceiling mirrors the documented\n * 1024-character frontmatter budget.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst DEFAULT_MAX = 1024;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [{ max?: number }?];\n\tMessageIds: \"tooLong\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription: \"Cap the length of the skill `description`\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/description-length.md\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tmax: { type: \"integer\", minimum: 1 },\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\ttooLong:\n\t\t\t\t\"Skill `description` is {{length}} characters; keep it at or under {{max}}.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst max = context.options[0]?.max ?? DEFAULT_MAX;\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data || typeof data.description !== \"string\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst length = data.description.length;\n\t\t\t\tif (length > max) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: keyLoc(\"description\"),\n\t\t\t\t\t\tmessageId: \"tooLong\",\n\t\t\t\t\t\tdata: { length, max },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require the `description` to express when the skill applies.\n *\n * An agent routes to a skill purely from its description. A description that\n * only says *what* the skill is, without signalling *when* to use it, routes\n * poorly. This rule checks for a trigger cue (configurable).\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst DEFAULT_PATTERNS = [\n\t\"use when\",\n\t\"use this when\",\n\t\"use for\",\n\t\"used when\",\n\t\"used for\",\n\t\"when the user\",\n\t\"when you\",\n\t\"when asked\",\n\t\"trigger\",\n\t\"invoke\",\n\t\"for when\",\n];\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [{ patterns?: string[] }?];\n\tMessageIds: \"noTrigger\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Require the `description` to express when the skill applies\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/description-has-trigger.md\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tpatterns: {\n\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\titems: { type: \"string\" },\n\t\t\t\t\t\tminItems: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\tnoTrigger:\n\t\t\t\t\"Skill `description` should state when to use the skill (e.g. a phrase like {{examples}}).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst patterns = context.options[0]?.patterns ?? DEFAULT_PATTERNS;\n\t\tconst lowered = patterns.map((p) => p.toLowerCase());\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (\n\t\t\t\t\t!data ||\n\t\t\t\t\ttypeof data.description !== \"string\" ||\n\t\t\t\t\tdata.description.trim() === \"\"\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst haystack = data.description.toLowerCase();\n\t\t\t\tconst hasTrigger = lowered.some((p) => haystack.includes(p));\n\t\t\t\tif (!hasTrigger) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: keyLoc(\"description\"),\n\t\t\t\t\t\tmessageId: \"noTrigger\",\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\texamples: patterns\n\t\t\t\t\t\t\t\t.slice(0, 2)\n\t\t\t\t\t\t\t\t.map((p) => `\"${p}\"`)\n\t\t\t\t\t\t\t\t.join(\", \"),\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Disallow HTML/XML tags in the skill `description`.\n *\n * Descriptions are consumed as plain routing text. Embedded tags add noise and\n * can interfere with how the description is parsed and displayed.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\n// Matches an opening, closing, or self-closing tag such as <b>, </b>, <br/>,\n// or <role attr=\"x\">. Deliberately conservative: requires a letter after `<`.\nconst TAG = /<\\/?[a-zA-Z][^>]*>/;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"hasTag\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription: \"Disallow HTML/XML tags in the skill `description`\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/description-no-html.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\thasTag:\n\t\t\t\t\"Skill `description` must not contain HTML/XML tags (found `{{tag}}`).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data || typeof data.description !== \"string\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst match = TAG.exec(data.description);\n\t\t\t\tif (match) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: keyLoc(\"description\"),\n\t\t\t\t\t\tmessageId: \"hasTag\",\n\t\t\t\t\t\tdata: { tag: match[0] },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Disallow more than one top-level H1 in the skill body.\n *\n * The standard skill format does not require an H1 — the official examples use\n * `##`-first bodies — so this rule does not demand a title. But two `#` H1s in\n * one body reads like separate documents merged together, so any H1 after the\n * first is flagged.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\nimport type { Heading } from \"mdast\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { collect } from \"../utils/ast\";\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"multiple\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription: \"Disallow more than one top-level H1 in the skill body\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/no-multiple-h1.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tmultiple:\n\t\t\t\t\"Skill body must have at most one `#` H1 title; found {{count}}.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst h1s = collect<Heading>(root, \"heading\").filter(\n\t\t\t\t\t(h) => h.depth === 1,\n\t\t\t\t);\n\t\t\t\tif (h1s.length > 1) {\n\t\t\t\t\t// Report each extra H1 after the first.\n\t\t\t\t\tfor (const extra of h1s.slice(1)) {\n\t\t\t\t\t\tif (extra.position) {\n\t\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\t\tloc: extra.position,\n\t\t\t\t\t\t\t\tmessageId: \"multiple\",\n\t\t\t\t\t\t\t\tdata: { count: h1s.length },\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require the body H1 to correspond to the skill `name`.\n *\n * The comparison is normalized so both `# my-skill` and `# My Skill` are\n * accepted for `name: my-skill`.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\nimport type { Heading } from \"mdast\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter, collect, textContent } from \"../utils/ast\";\n\nfunction normalize(value: string): string {\n\treturn value\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, \" \")\n\t\t.trim();\n}\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"mismatch\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\t// Opt-in: not part of any preset. The standard format neither\n\t\t\t// requires an H1 nor ties it to `name` (which is a display label).\n\t\t\tdescription: \"Require the body H1 to correspond to the skill `name`\",\n\t\t\trecommended: false,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/h1-matches-name.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tmismatch:\n\t\t\t\t\"Body H1 (`{{title}}`) should correspond to the skill `name` (`{{name}}`).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data } = parseFrontmatter(node);\n\t\t\t\tif (!data || typeof data.name !== \"string\" || data.name === \"\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst h1 = collect<Heading>(root, \"heading\").find(\n\t\t\t\t\t(h) => h.depth === 1,\n\t\t\t\t);\n\t\t\t\tif (!h1 || !h1.position) {\n\t\t\t\t\treturn; // `no-multiple-h1` owns the heading-structure cases.\n\t\t\t\t}\n\t\t\t\tconst title = textContent(h1).trim();\n\t\t\t\tif (normalize(title) !== normalize(data.name)) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: h1.position,\n\t\t\t\t\t\tmessageId: \"mismatch\",\n\t\t\t\t\t\tdata: { title, name: data.name },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Keep the skill body short to preserve progressive disclosure.\n *\n * A SKILL.md is an entry point, not a manual: heavy detail belongs in\n * referenced files that are loaded on demand. This rule flags bodies that grow\n * past a line budget (frontmatter excluded).\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst DEFAULT_MAX_LINES = 500;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [{ maxLines?: number }?];\n\tMessageIds: \"tooLong\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Keep the skill body short to preserve progressive disclosure\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/body-max-length.md\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tmaxLines: { type: \"integer\", minimum: 1 },\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\ttooLong:\n\t\t\t\t\"Skill body is {{lines}} lines; keep it at or under {{max}} and move detail into referenced files.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst max = context.options[0]?.maxLines ?? DEFAULT_MAX_LINES;\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst totalLines = root.position?.end?.line ?? 0;\n\t\t\t\tconst fm = findFrontmatter(root);\n\t\t\t\tconst bodyStart = fm ? (fm.position?.end?.line ?? 0) : 0;\n\t\t\t\tconst bodyLines = Math.max(0, totalLines - bodyStart);\n\t\t\t\tif (bodyLines > max) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: {\n\t\t\t\t\t\t\tstart: { line: bodyStart + 1, column: 1 },\n\t\t\t\t\t\t\tend: { line: totalLines, column: 1 },\n\t\t\t\t\t\t},\n\t\t\t\t\t\tmessageId: \"tooLong\",\n\t\t\t\t\t\tdata: { lines: bodyLines, max },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * Helpers for working with links and images in a skill body.\n */\n\nimport type { Image, Link, Root } from \"mdast\";\n\nimport { collect } from \"./ast\";\n\nexport interface LinkTarget {\n\turl: string;\n\tkind: \"link\" | \"image\";\n\tnode: Link | Image;\n}\n\n/**\n * Whether a URL points at a repo-relative path (not an anchor, scheme, or\n * protocol-relative URL).\n */\nexport function isRelativeUrl(url: string): boolean {\n\tif (!url || url.startsWith(\"#\")) {\n\t\treturn false;\n\t}\n\tif (url.startsWith(\"//\") || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) {\n\t\treturn false;\n\t}\n\treturn true;\n}\n\n/**\n * Strip a URL's fragment and query so it can be resolved to a filesystem path.\n */\nexport function urlToPath(url: string): string {\n\tlet path = url;\n\tconst hash = path.indexOf(\"#\");\n\tif (hash !== -1) {\n\t\tpath = path.slice(0, hash);\n\t}\n\tconst query = path.indexOf(\"?\");\n\tif (query !== -1) {\n\t\tpath = path.slice(0, query);\n\t}\n\ttry {\n\t\treturn decodeURIComponent(path);\n\t} catch {\n\t\treturn path;\n\t}\n}\n\n/**\n * All link and image targets in the tree, in document order.\n */\nexport function collectLinkTargets(root: Root): LinkTarget[] {\n\treturn [\n\t\t...collect<Link>(root, \"link\").map(\n\t\t\t(node): LinkTarget => ({ url: node.url, kind: \"link\", node }),\n\t\t),\n\t\t...collect<Image>(root, \"image\").map(\n\t\t\t(node): LinkTarget => ({ url: node.url, kind: \"image\", node }),\n\t\t),\n\t];\n}\n","/**\n * The file access seam of the rules that look past the linted file\n * (`no-broken-relative-links`, `no-orphan-references`, the `kind` rules).\n *\n * By default they read the disk. A host that keeps its skills somewhere else\n * (a database, a remote store) passes every file of the linted skills in the\n * shared setting `settings.skill.files` — a map from path to content — and\n * lints them in memory with ESLint's `Linter`. The paths are resolved like\n * the linted `filename`, so `/skills/demo/SKILL.md` and\n * `/skills/demo/references/a.md` are siblings exactly as on disk. A flat\n * config matches only files under the `Linter`'s `cwd`, so pass the virtual\n * root as `cwd` (`new Linter({ cwd: \"/skills\" })`).\n */\n\nimport { existsSync, readdirSync } from \"node:fs\";\nimport { join, resolve, sep } from \"node:path\";\n\n/** The shape of `settings.skill`. */\nexport interface SkillSettings {\n\t/** Every file of the linted skills, keyed by path. Replaces the disk when present. */\n\tfiles?: Record<string, string>;\n}\n\n/** What the rules need from a file system. */\nexport interface SkillFs {\n\t/** Whether a file or a directory exists at an absolute path. */\n\texists(path: string): boolean;\n\t/** Every `.md` file under a directory, recursively, as absolute paths. */\n\tmarkdownFilesUnder(dir: string): string[];\n}\n\nfunction diskMarkdownFilesUnder(dir: string): string[] {\n\tif (!existsSync(dir)) {\n\t\treturn [];\n\t}\n\tconst out: string[] = [];\n\tconst walk = (current: string): void => {\n\t\tlet entries;\n\t\ttry {\n\t\t\tentries = readdirSync(current, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tconst full = join(current, entry.name);\n\t\t\tif (entry.isDirectory()) {\n\t\t\t\twalk(full);\n\t\t\t} else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n\t\t\t\tout.push(full);\n\t\t\t}\n\t\t}\n\t};\n\twalk(dir);\n\treturn out;\n}\n\nconst diskFs: SkillFs = {\n\texists: existsSync,\n\tmarkdownFilesUnder: diskMarkdownFilesUnder,\n};\n\n/** A file system over an in-memory map of path → content. */\nexport function memoryFs(files: Record<string, string>): SkillFs {\n\tconst paths = Object.keys(files).map((path) => resolve(path));\n\tconst under = (dir: string): string[] => {\n\t\tconst prefix = resolve(dir) + sep;\n\t\treturn paths.filter((path) => path.startsWith(prefix));\n\t};\n\n\treturn {\n\t\texists: (path) => {\n\t\t\tconst target = resolve(path);\n\t\t\treturn paths.includes(target) || under(target).length > 0;\n\t\t},\n\t\tmarkdownFilesUnder: (dir) => under(dir).filter((path) => path.endsWith(\".md\")),\n\t};\n}\n\n/** The file system a rule must use, chosen from the shared settings. */\nexport function skillFs(settings: Readonly<Record<string, unknown>> | undefined): SkillFs {\n\tconst files = (settings?.skill as SkillSettings | undefined)?.files;\n\treturn files ? memoryFs(files) : diskFs;\n}\n","/**\n * @fileoverview Flag relative links/images whose target does not exist.\n *\n * Skills lean on referenced files for progressive disclosure; a dead relative\n * link means the agent will try to open something that isn't there. Absolute\n * URLs (`https:`, `mailto:`) and pure anchors (`#section`) are ignored.\n */\n\nimport { dirname, resolve } from \"node:path\";\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { collectLinkTargets, isRelativeUrl, urlToPath } from \"../utils/links\";\nimport { skillFs } from \"../utils/skill-fs\";\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"broken\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Flag relative links/images whose target does not exist\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/no-broken-relative-links.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tbroken:\n\t\t\t\t\"Relative {{kind}} target `{{url}}` does not exist relative to this skill.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst baseDir = dirname(context.filename);\n\t\t// Without a resolvable path we cannot check the filesystem.\n\t\tif (!baseDir || baseDir === \".\" || context.filename === \"<input>\") {\n\t\t\treturn {};\n\t\t}\n\n\t\tconst fs = skillFs(context.settings);\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tfor (const { node, kind, url } of collectLinkTargets(root)) {\n\t\t\t\t\tif (!isRelativeUrl(url) || !node.position) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst target = resolve(baseDir, urlToPath(url));\n\t\t\t\t\tif (!fs.exists(target)) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tloc: node.position,\n\t\t\t\t\t\t\tmessageId: \"broken\",\n\t\t\t\t\t\t\tdata: { kind, url },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Flag unknown top-level frontmatter keys on a skill.\n *\n * The Agent Skills standard defines a fixed set of frontmatter fields\n * (`name` + `description` required; a handful of optional ones). An unexpected\n * key is almost always a typo (`descriptoin`) or a misplaced field, which the\n * runtime silently ignores — so it never takes effect and is easy to miss.\n * Extra keys can be allowed explicitly via the `allow` option.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\n// Required + optional keys defined by the Agent Skills standard and the\n// Claude Code extensions to it.\nconst KNOWN_KEYS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n\t\"when_to_use\",\n\t\"disable-model-invocation\",\n\t// Claude Code command/skill extensions.\n\t\"argument-hint\",\n\t\"model\",\n\t\"context\",\n]);\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [{ allow?: string[] }?];\n\tMessageIds: \"unknownKey\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription: \"Flag unknown top-level frontmatter keys on a skill\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/no-unknown-frontmatter-keys.md\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tallow: {\n\t\t\t\t\t\ttype: \"array\",\n\t\t\t\t\t\titems: { type: \"string\" },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\tunknownKey:\n\t\t\t\t\"Unknown frontmatter key `{{key}}` — likely a typo or an ignored field.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst allow = new Set(context.options[0]?.allow ?? []);\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tfor (const key of Object.keys(data)) {\n\t\t\t\t\tif (KNOWN_KEYS.has(key) || allow.has(key)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: keyLoc(key),\n\t\t\t\t\t\tmessageId: \"unknownKey\",\n\t\t\t\t\t\tdata: { key },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require reference links to stay one level deep from SKILL.md.\n *\n * Official guidance: every reference file should link directly from SKILL.md\n * and live at most one directory below it. Deeply nested reference chains are\n * an anti-pattern — Claude may only partially read (e.g. `head -100`) files\n * reached indirectly, so it can act on incomplete information.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { collectLinkTargets, isRelativeUrl, urlToPath } from \"../utils/links\";\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"tooDeep\" | \"escapes\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription: \"Require reference links to stay one level deep from SKILL.md\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/refs-one-level-deep.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\ttooDeep:\n\t\t\t\t\"Reference `{{url}}` is more than one directory deep; keep reference files one level below SKILL.md.\",\n\t\t\tescapes:\n\t\t\t\t\"Reference `{{url}}` points outside the skill directory; keep reference files one level below SKILL.md.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tfor (const { node, url } of collectLinkTargets(root)) {\n\t\t\t\t\tif (!isRelativeUrl(url) || !node.position) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst segments = urlToPath(url)\n\t\t\t\t\t\t.split(\"/\")\n\t\t\t\t\t\t.filter((s) => s && s !== \".\");\n\t\t\t\t\tconst dirs = segments.slice(0, -1);\n\t\t\t\t\tif (dirs.includes(\"..\")) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tloc: node.position,\n\t\t\t\t\t\t\tmessageId: \"escapes\",\n\t\t\t\t\t\t\tdata: { url },\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (dirs.length > 1) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tloc: node.position,\n\t\t\t\t\t\t\tmessageId: \"tooDeep\",\n\t\t\t\t\t\t\tdata: { url },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * Shared helpers for a skill's `references/` directory.\n */\n\nimport { join } from \"node:path\";\n\nimport type { SkillFs } from \"./skill-fs\";\n\n/** Every `.md` file under a skill's `references/` directory, recursively. */\nexport function referenceMarkdownFiles(skillDir: string, fs: SkillFs): string[] {\n\treturn fs.markdownFilesUnder(join(skillDir, \"references\"));\n}\n","/**\n * @fileoverview Flag reference files not linked from SKILL.md.\n *\n * A `.md` file sitting in `references/` that nothing in SKILL.md links to is\n * dead weight: Claude is told to load reference files via the pointers in\n * SKILL.md, so an unlinked one is never read. It's usually a leftover, a\n * rename that broke the link, or a missing pointer.\n */\n\nimport { dirname, relative, resolve } from \"node:path\";\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { collectLinkTargets, isRelativeUrl, urlToPath } from \"../utils/links\";\nimport { referenceMarkdownFiles } from \"../utils/references\";\nimport { skillFs } from \"../utils/skill-fs\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"orphan\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription: \"Flag reference files not linked from SKILL.md\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/no-orphan-references.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\torphan:\n\t\t\t\t\"Reference file `{{file}}` is not linked from SKILL.md and will never be read.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst skillDir = dirname(context.filename);\n\t\tconst files = referenceMarkdownFiles(skillDir, skillFs(context.settings));\n\t\tif (!skillDir || skillDir === \".\" || context.filename === \"<input>\" || files.length === 0) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst linked = new Set(\n\t\t\t\t\tcollectLinkTargets(root)\n\t\t\t\t\t\t.filter(({ url }) => isRelativeUrl(url))\n\t\t\t\t\t\t.map(({ url }) => resolve(skillDir, urlToPath(url))),\n\t\t\t\t);\n\n\t\t\t\tfor (const file of files) {\n\t\t\t\t\tif (!linked.has(file)) {\n\t\t\t\t\t\tcontext.report({\n\t\t\t\t\t\t\tloc: TOP,\n\t\t\t\t\t\t\tmessageId: \"orphan\",\n\t\t\t\t\t\t\tdata: { file: relative(skillDir, file) },\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require reference files to have descriptive names.\n *\n * Claude decides whether to open a reference file partly from its name, so\n * generic names like `doc2.md` or `untitled.md` carry no signal. Official\n * guidance: name files descriptively (`form_validation_rules.md`, not\n * `doc2.md`).\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isReferenceFile, basename } from \"../utils/skill-file\";\n\n// Generic, low-signal stems: doc, ref, reference, file, page, untitled, temp,\n// tmp, new, copy, note(s), document — optionally suffixed with a number.\nconst GENERIC =\n\t/^(docs?|references?|files?|pages?|untitled|temp|tmp|new|copy|notes?|documents?)[-_ ]?\\d*$/i;\n// A bare number like `1.md` or `01.md`.\nconst NUMERIC = /^\\d+$/;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"generic\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription: \"Require reference files to have descriptive names\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/descriptive-reference-filenames.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tgeneric:\n\t\t\t\t\"Reference file `{{name}}` has a generic name; use a descriptive one (e.g. `form_validation_rules.md`).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isReferenceFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst name = basename(context.filename);\n\t\tconst stem = name.replace(/\\.md$/i, \"\");\n\n\t\treturn {\n\t\t\troot() {\n\t\t\t\tif (GENERIC.test(stem) || NUMERIC.test(stem)) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },\n\t\t\t\t\t\tmessageId: \"generic\",\n\t\t\t\t\t\tdata: { name },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Require large reference files to include a table of contents.\n *\n * Official guidance: reference files over ~300 lines should include a table of\n * contents so Claude can navigate to the relevant section instead of reading\n * (or partially reading) the whole file.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\nimport type { Heading, Link } from \"mdast\";\n\nimport { isReferenceFile } from \"../utils/skill-file\";\nimport { collect, textContent } from \"../utils/ast\";\n\nconst DEFAULT_MAX_LINES = 300;\nconst TOC_HEADING = /^(table of contents|contents|toc)$/i;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [{ maxLines?: number }?];\n\tMessageIds: \"missingToc\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Require large reference files to include a table of contents\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/large-reference-toc.md\",\n\t\t},\n\t\tschema: [\n\t\t\t{\n\t\t\t\ttype: \"object\",\n\t\t\t\tproperties: {\n\t\t\t\t\tmaxLines: { type: \"integer\", minimum: 1 },\n\t\t\t\t},\n\t\t\t\tadditionalProperties: false,\n\t\t\t},\n\t\t],\n\t\tmessages: {\n\t\t\tmissingToc:\n\t\t\t\t\"Reference file is {{lines}} lines; add a table of contents (over {{max}} lines).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isReferenceFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst max = context.options[0]?.maxLines ?? DEFAULT_MAX_LINES;\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst lines = root.position?.end?.line ?? 0;\n\t\t\t\tif (lines <= max) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst headings = collect<Heading>(root, \"heading\");\n\t\t\t\tconst hasTocHeading = headings.some((h) =>\n\t\t\t\t\tTOC_HEADING.test(textContent(h).trim()),\n\t\t\t\t);\n\t\t\t\t// A cluster of in-page anchor links also counts as a TOC.\n\t\t\t\tconst anchorLinks = collect<Link>(root, \"link\").filter((n) =>\n\t\t\t\t\tn.url.startsWith(\"#\"),\n\t\t\t\t);\n\t\t\t\tconst hasAnchorToc = anchorLinks.length >= 3;\n\n\t\t\t\tif (!hasTocHeading && !hasAnchorToc) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } },\n\t\t\t\t\t\tmessageId: \"missingToc\",\n\t\t\t\t\t\tdata: { lines, max },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Validate the shape of the optional `allowed-tools` frontmatter field.\n *\n * `allowed-tools` (a Claude Code extension to the Agent Skills standard) must\n * be a non-empty array of non-empty strings when present. An empty array\n * grants no tools at all — almost certainly not the intent — and a non-array\n * value is silently ignored by the runtime.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"notArray\" | \"empty\" | \"invalidEntry\";\n}> = {\n\tmeta: {\n\t\ttype: \"problem\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Validate the shape of the optional `allowed-tools` frontmatter field\",\n\t\t\trecommended: true,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/valid-allowed-tools.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tnotArray: \"`allowed-tools` must be an array of tool names.\",\n\t\t\tempty: \"`allowed-tools` must not be empty; omit the field to allow all tools.\",\n\t\t\tinvalidEntry: \"`allowed-tools` entries must be non-empty strings.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data, keyLoc } = parseFrontmatter(node);\n\t\t\t\tif (!data || !(\"allowed-tools\" in data)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst value = data[\"allowed-tools\"];\n\t\t\t\tconst loc = keyLoc(\"allowed-tools\");\n\n\t\t\t\tif (!Array.isArray(value)) {\n\t\t\t\t\tcontext.report({ loc, messageId: \"notArray\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (value.length === 0) {\n\t\t\t\t\tcontext.report({ loc, messageId: \"empty\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (value.some((v) => typeof v !== \"string\" || v.trim() === \"\")) {\n\t\t\t\t\tcontext.report({ loc, messageId: \"invalidEntry\" });\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * Skill KIND: a taxonomy for applying different lint expectations to\n * different shapes of skill.\n *\n * Grounded in official Claude Code guidance, which distinguishes \"Reference\n * content\" (inline domain knowledge) from \"Task content\" (step-by-step\n * actions, often gated with `disable-model-invocation`), plus the\n * `skill-creator` meta-skill's multi-domain/variant pattern\n * (`references/{aws,gcp,azure}.md`). `tool-wrapper` and `meta` are defensible\n * extensions, not explicitly named in any single source — see\n * docs/design/skill-taxonomy-and-rules.md.\n *\n * A skill may declare its kind explicitly via `metadata.kind` in frontmatter;\n * this is the only signal the lint rules act on, since heuristic detection is\n * inherently fuzzy and a wrong guess should never cause a false positive.\n * `detectKind` is exposed separately as an informative, best-effort signal —\n * it never gates a rule.\n */\n\nexport const SKILL_KINDS = [\n\t\"procedural\",\n\t\"reference\",\n\t\"multi-domain\",\n\t\"tool-wrapper\",\n\t\"meta\",\n] as const;\n\nexport type SkillKind = (typeof SKILL_KINDS)[number];\n\nfunction isSkillKind(value: unknown): value is SkillKind {\n\treturn (\n\t\ttypeof value === \"string\" && (SKILL_KINDS as readonly string[]).includes(value)\n\t);\n}\n\n/**\n * The skill's explicitly declared kind, via `metadata.kind` in frontmatter.\n * Returns `undefined` when absent, malformed, or not a recognized kind.\n */\nexport function declaredKind(\n\tdata: Record<string, unknown> | null | undefined,\n): SkillKind | undefined {\n\tconst metadata = data?.metadata;\n\tif (!metadata || typeof metadata !== \"object\" || Array.isArray(metadata)) {\n\t\treturn undefined;\n\t}\n\tconst kind = (metadata as Record<string, unknown>).kind;\n\treturn isSkillKind(kind) ? kind : undefined;\n}\n\nexport interface DetectKindInput {\n\tdata: Record<string, unknown> | null | undefined;\n\t/** Number of ordered/numbered lists found in the body. */\n\torderedListCount: number;\n\t/** Number of `.md` files under the skill's `references/` directory. */\n\treferenceFileCount: number;\n\t/** Whether the body links to another skill (the `[[skill-name]]` convention). */\n\tlinksToOtherSkills: boolean;\n}\n\n/**\n * Best-effort auto-detected kind from observable signals, for tooling and\n * documentation purposes. This is advisory only — no rule fails or warns\n * based on a guess, only on an explicit `metadata.kind` declaration.\n */\nexport function detectKind(input: DetectKindInput): SkillKind | undefined {\n\tconst declared = declaredKind(input.data);\n\tif (declared) {\n\t\treturn declared;\n\t}\n\n\tconst allowedTools = input.data?.[\"allowed-tools\"];\n\tconst hasAllowedTools = Array.isArray(allowedTools) && allowedTools.length > 0;\n\tconst disableModelInvocation = input.data?.[\"disable-model-invocation\"] === true;\n\n\tconst scores: Record<SkillKind, number> = {\n\t\tmeta: input.linksToOtherSkills ? 2 : 0,\n\t\t\"tool-wrapper\": hasAllowedTools ? 2 : 0,\n\t\tprocedural:\n\t\t\t(disableModelInvocation ? 2 : 0) + (input.orderedListCount >= 1 ? 1 : 0),\n\t\t// >= 2 reference files is the more specific \"multi-domain\" signal; it\n\t\t// must outscore the generic \"reference\" signal so a tie resolves to it.\n\t\t\"multi-domain\": input.referenceFileCount >= 2 ? 2 : 0,\n\t\treference: input.referenceFileCount >= 1 ? 1 : 0,\n\t};\n\n\tlet best: SkillKind | undefined;\n\tlet bestScore = 0;\n\tfor (const kind of SKILL_KINDS) {\n\t\tif (scores[kind] > bestScore) {\n\t\t\tbest = kind;\n\t\t\tbestScore = scores[kind];\n\t\t}\n\t}\n\treturn best;\n}\n","/**\n * @fileoverview Expect a procedural skill's body to contain ordered steps.\n *\n * Opt-in: only fires when the skill explicitly declares itself as\n * `metadata.kind: procedural`. A procedural (task/workflow) skill exists to\n * walk the agent through a sequence of actions; a declared procedural skill\n * with no ordered list at all is either mis-declared or missing its steps.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\nimport type { List } from \"mdast\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter, collect } from \"../utils/ast\";\nimport { declaredKind } from \"../utils/kind\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"noSteps\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription: \"Expect a procedural skill's body to contain ordered steps\",\n\t\t\trecommended: false,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/procedural-has-steps.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tnoSteps:\n\t\t\t\t\"Skill is declared `metadata.kind: procedural` but its body has no ordered/numbered steps.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data } = parseFrontmatter(node);\n\t\t\t\tif (declaredKind(data) !== \"procedural\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst hasOrderedList = collect<List>(root, \"list\").some(\n\t\t\t\t\t(list) => list.ordered,\n\t\t\t\t);\n\t\t\t\tif (!hasOrderedList) {\n\t\t\t\t\tcontext.report({ loc: TOP, messageId: \"noSteps\" });\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Expect a reference skill to carry at least one reference file.\n *\n * Opt-in: only fires when the skill explicitly declares itself as\n * `metadata.kind: reference`. A reference (domain-knowledge) skill exists to\n * supply background material on demand; a declared reference skill with an\n * empty or missing `references/` directory has nothing to supply.\n */\n\nimport { dirname } from \"node:path\";\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\nimport { declaredKind } from \"../utils/kind\";\nimport { referenceMarkdownFiles } from \"../utils/references\";\nimport { skillFs } from \"../utils/skill-fs\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"noReferences\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Expect a reference skill to carry at least one reference file\",\n\t\t\trecommended: false,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/reference-has-content.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tnoReferences:\n\t\t\t\t\"Skill is declared `metadata.kind: reference` but its `references/` directory is empty or missing.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst skillDir = dirname(context.filename);\n\t\tif (!skillDir || skillDir === \".\" || context.filename === \"<input>\") {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data } = parseFrontmatter(node);\n\t\t\t\tif (declaredKind(data) !== \"reference\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (referenceMarkdownFiles(skillDir, skillFs(context.settings)).length === 0) {\n\t\t\t\t\tcontext.report({ loc: TOP, messageId: \"noReferences\" });\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Expect a multi-domain skill to carry at least two variant references.\n *\n * Opt-in: only fires when the skill explicitly declares itself as\n * `metadata.kind: multi-domain`. The multi-domain pattern (e.g.\n * `references/{aws,gcp,azure}.md`, with `SKILL.md` holding the selection\n * logic) only earns its complexity when there is more than one variant to\n * select between.\n */\n\nimport { dirname } from \"node:path\";\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter } from \"../utils/ast\";\nimport { declaredKind } from \"../utils/kind\";\nimport { referenceMarkdownFiles } from \"../utils/references\";\nimport { skillFs } from \"../utils/skill-fs\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\nconst MIN_VARIANTS = 2;\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"tooFewVariants\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Expect a multi-domain skill to carry at least two variant references\",\n\t\t\trecommended: false,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/multi-domain-has-variants.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\ttooFewVariants:\n\t\t\t\t\"Skill is declared `metadata.kind: multi-domain` but has fewer than {{min}} reference files to select between.\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\t\tconst skillDir = dirname(context.filename);\n\t\tif (!skillDir || skillDir === \".\" || context.filename === \"<input>\") {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data } = parseFrontmatter(node);\n\t\t\t\tif (declaredKind(data) !== \"multi-domain\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (referenceMarkdownFiles(skillDir, skillFs(context.settings)).length < MIN_VARIANTS) {\n\t\t\t\t\tcontext.report({\n\t\t\t\t\t\tloc: TOP,\n\t\t\t\t\t\tmessageId: \"tooFewVariants\",\n\t\t\t\t\t\tdata: { min: MIN_VARIANTS },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @fileoverview Suggest a markdown table for a meta/router skill's dispatch list.\n *\n * Opt-in: only fires when the skill explicitly declares itself as\n * `metadata.kind: meta`. A meta (router) skill exists to dispatch requests to\n * other skills. A single-scenario empirical test (see\n * docs/research/meta-dispatch-table-vs-prose.md) found a markdown table costs\n * ~21% fewer tokens than equivalent prose for this kind of dispatch content,\n * with no measured routing-accuracy difference — so this is a cost-only\n * suggestion, not a correctness rule. Given that single-test evidence base,\n * this rule is deliberately kept out of every preset.\n */\n\nimport type { MarkdownRuleDefinition } from \"@eslint/markdown\";\nimport type { Table } from \"mdast\";\n\nimport { isSkillFile } from \"../utils/skill-file\";\nimport { parseFrontmatter } from \"../utils/frontmatter\";\nimport { findFrontmatter, collect } from \"../utils/ast\";\nimport { declaredKind } from \"../utils/kind\";\n\nconst TOP = { start: { line: 1, column: 1 }, end: { line: 1, column: 1 } };\n\nconst rule: MarkdownRuleDefinition<{\n\tRuleOptions: [];\n\tMessageIds: \"preferTable\";\n}> = {\n\tmeta: {\n\t\ttype: \"suggestion\",\n\t\tdocs: {\n\t\t\tdescription:\n\t\t\t\t\"Suggest a markdown table for a meta/router skill's dispatch list\",\n\t\t\trecommended: false,\n\t\t\turl: \"https://github.com/weareikko/motto/blob/main/packages/eslint-plugin-skill/docs/rules/meta-prefers-dispatch-table.md\",\n\t\t},\n\t\tschema: [],\n\t\tmessages: {\n\t\t\tpreferTable:\n\t\t\t\t\"Skill is declared `metadata.kind: meta` with no markdown table; consider a table for the dispatch list — measured ~21% cheaper than prose with no accuracy difference (see docs/research/meta-dispatch-table-vs-prose.md).\",\n\t\t},\n\t},\n\n\tcreate(context) {\n\t\tif (!isSkillFile(context.filename)) {\n\t\t\treturn {};\n\t\t}\n\n\t\treturn {\n\t\t\troot(root) {\n\t\t\t\tconst node = findFrontmatter(root);\n\t\t\t\tif (!node) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst { data } = parseFrontmatter(node);\n\t\t\t\tif (declaredKind(data) !== \"meta\") {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (collect<Table>(root, \"table\").length === 0) {\n\t\t\t\t\tcontext.report({ loc: TOP, messageId: \"preferTable\" });\n\t\t\t\t}\n\t\t\t},\n\t\t};\n\t},\n};\n\nexport default rule;\n","/**\n * @weareikko/eslint-plugin-skill — lint Claude Code Agent Skills (`SKILL.md`).\n *\n * The plugin exposes a set of rules plus two ready-to-use flat configs. Each\n * config is self-contained: it targets `**​/SKILL.md`, wires up the\n * `@eslint/markdown` language with YAML frontmatter, and turns on the rules.\n */\n\nimport markdown from \"@eslint/markdown\";\nimport type { ESLint, Linter } from \"eslint\";\n\nimport requireFrontmatter from \"./rules/require-frontmatter\";\nimport nameKebabCase from \"./rules/name-kebab-case\";\nimport nameMatchesDir from \"./rules/name-matches-dir\";\nimport descriptionLength from \"./rules/description-length\";\nimport descriptionHasTrigger from \"./rules/description-has-trigger\";\nimport descriptionNoHtml from \"./rules/description-no-html\";\nimport noMultipleH1 from \"./rules/no-multiple-h1\";\nimport h1MatchesName from \"./rules/h1-matches-name\";\nimport bodyMaxLength from \"./rules/body-max-length\";\nimport noBrokenRelativeLinks from \"./rules/no-broken-relative-links\";\nimport noUnknownFrontmatterKeys from \"./rules/no-unknown-frontmatter-keys\";\nimport refsOneLevelDeep from \"./rules/refs-one-level-deep\";\nimport noOrphanReferences from \"./rules/no-orphan-references\";\nimport descriptiveReferenceFilenames from \"./rules/descriptive-reference-filenames\";\nimport largeReferenceToc from \"./rules/large-reference-toc\";\nimport validAllowedTools from \"./rules/valid-allowed-tools\";\nimport proceduralHasSteps from \"./rules/procedural-has-steps\";\nimport referenceHasContent from \"./rules/reference-has-content\";\nimport multiDomainHasVariants from \"./rules/multi-domain-has-variants\";\nimport metaPrefersDispatchTable from \"./rules/meta-prefers-dispatch-table\";\n\nconst plugin = {\n\tmeta: {\n\t\tname: __PKG_NAME__,\n\t\tversion: __PKG_VERSION__,\n\t},\n\trules: {\n\t\t\"require-frontmatter\": requireFrontmatter,\n\t\t\"name-kebab-case\": nameKebabCase,\n\t\t\"name-matches-dir\": nameMatchesDir,\n\t\t\"description-length\": descriptionLength,\n\t\t\"description-has-trigger\": descriptionHasTrigger,\n\t\t\"description-no-html\": descriptionNoHtml,\n\t\t\"no-multiple-h1\": noMultipleH1,\n\t\t\"h1-matches-name\": h1MatchesName,\n\t\t\"body-max-length\": bodyMaxLength,\n\t\t\"no-broken-relative-links\": noBrokenRelativeLinks,\n\t\t\"no-unknown-frontmatter-keys\": noUnknownFrontmatterKeys,\n\t\t\"refs-one-level-deep\": refsOneLevelDeep,\n\t\t\"no-orphan-references\": noOrphanReferences,\n\t\t\"descriptive-reference-filenames\": descriptiveReferenceFilenames,\n\t\t\"large-reference-toc\": largeReferenceToc,\n\t\t\"valid-allowed-tools\": validAllowedTools,\n\t\t\"procedural-has-steps\": proceduralHasSteps,\n\t\t\"reference-has-content\": referenceHasContent,\n\t\t\"multi-domain-has-variants\": multiDomainHasVariants,\n\t\t\"meta-prefers-dispatch-table\": metaPrefersDispatchTable,\n\t},\n\tconfigs: {} as Record<string, Linter.Config>,\n} satisfies ESLint.Plugin;\n\n/**\n * Rule severities for the `recommended` preset. `warn` marks quality signals\n * that shouldn't necessarily fail CI; `strict` promotes them to `error`.\n */\nconst RECOMMENDED_RULES: Linter.RulesRecord = {\n\t\"skill/require-frontmatter\": \"error\",\n\t\"skill/name-kebab-case\": \"error\",\n\t\"skill/name-matches-dir\": \"error\",\n\t\"skill/description-length\": \"error\",\n\t\"skill/description-no-html\": \"error\",\n\t\"skill/no-multiple-h1\": \"error\",\n\t\"skill/no-broken-relative-links\": \"error\",\n\t\"skill/no-unknown-frontmatter-keys\": \"error\",\n\t\"skill/refs-one-level-deep\": \"error\",\n\t\"skill/valid-allowed-tools\": \"error\",\n\t\"skill/description-has-trigger\": \"warn\",\n\t\"skill/body-max-length\": \"warn\",\n\t\"skill/no-orphan-references\": \"warn\",\n\t\"skill/descriptive-reference-filenames\": \"warn\",\n\t\"skill/large-reference-toc\": \"warn\",\n\t// `h1-matches-name` is intentionally omitted: the standard skill format does\n\t// not require an H1, and the frontmatter `name` is a display label, not the\n\t// body title. Teams that adopt the \"H1 mirrors name\" convention can opt in.\n\t// `meta-prefers-dispatch-table` is intentionally omitted: it's backed by a\n\t// single empirical test (docs/research/meta-dispatch-table-vs-prose.md),\n\t// not the multiply-corroborated evidence behind the rules above.\n};\n\nconst STRICT_RULES: Linter.RulesRecord = Object.fromEntries(\n\tObject.keys(RECOMMENDED_RULES).map((id) => [id, \"error\"]),\n);\n\nfunction makeConfig(name: string, rules: Linter.RulesRecord): Linter.Config {\n\treturn {\n\t\tname,\n\t\tfiles: [\"**/SKILL.md\", \"**/references/**/*.md\"],\n\t\tplugins: { markdown, skill: plugin },\n\t\tlanguage: \"markdown/gfm\",\n\t\tlanguageOptions: { frontmatter: \"yaml\" },\n\t\trules,\n\t};\n}\n\nplugin.configs.recommended = makeConfig(\"skill/recommended\", RECOMMENDED_RULES);\nplugin.configs.strict = makeConfig(\"skill/strict\", STRICT_RULES);\n\nexport default plugin;\n"],"mappings":"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"}
@@ -0,0 +1,266 @@
1
+ import { basename, dirname, join, relative, resolve } from "node:path";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ //#region src/dedup/hash.ts
5
+ /**
6
+ * Content normalization and hashing for cross-skill reference comparison.
7
+ */
8
+ /**
9
+ * Normalize markdown content so incidental formatting differences (line
10
+ * endings, trailing whitespace, a stray blank line) don't defeat
11
+ * identical-content detection.
12
+ */
13
+ function normalize(content) {
14
+ return content.replace(/\r\n/g, "\n").split("\n").map((line) => line.replace(/\s+$/, "")).join("\n").replace(/\n{3,}/g, "\n\n").trim();
15
+ }
16
+ /** SHA-1 hash of the normalized content. */
17
+ function hashContent(content) {
18
+ return createHash("sha1").update(normalize(content)).digest("hex");
19
+ }
20
+ /**
21
+ * Word-shingle Jaccard similarity between two texts, for near-duplicate
22
+ * detection. Returns a value in [0, 1].
23
+ */
24
+ function similarity(a, b, shingleSize = 5) {
25
+ const shinglesOf = (text) => {
26
+ const words = normalize(text).toLowerCase().split(/\s+/).filter(Boolean);
27
+ const shingles = /* @__PURE__ */ new Set();
28
+ for (let i = 0; i <= words.length - shingleSize; i++) shingles.add(words.slice(i, i + shingleSize).join(" "));
29
+ return shingles;
30
+ };
31
+ const setA = shinglesOf(a);
32
+ const setB = shinglesOf(b);
33
+ if (setA.size === 0 || setB.size === 0) return 0;
34
+ let intersection = 0;
35
+ for (const shingle of setA) if (setB.has(shingle)) intersection++;
36
+ const union = setA.size + setB.size - intersection;
37
+ return union === 0 ? 0 : intersection / union;
38
+ }
39
+ //#endregion
40
+ //#region src/dedup/discover.ts
41
+ /**
42
+ * Discover skills and their reference files under a root directory.
43
+ */
44
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
45
+ "node_modules",
46
+ ".git",
47
+ "dist",
48
+ "coverage"
49
+ ]);
50
+ /** Recursively find every directory containing a `SKILL.md` file. */
51
+ function findSkillDirs(root) {
52
+ const out = [];
53
+ const walk = (dir) => {
54
+ let entries;
55
+ try {
56
+ entries = readdirSync(dir, { withFileTypes: true });
57
+ } catch {
58
+ return;
59
+ }
60
+ if (entries.some((e) => e.isFile() && e.name === "SKILL.md")) out.push(dir);
61
+ for (const entry of entries) if (entry.isDirectory() && !IGNORED_DIRS.has(entry.name)) walk(join(dir, entry.name));
62
+ };
63
+ walk(root);
64
+ return out.sort();
65
+ }
66
+ /**
67
+ * The comparison scope a skill belongs to under the "plugin" strategy: the
68
+ * parent of the nearest ancestor directory literally named `skills` (i.e. the
69
+ * plugin directory for sibling skills under `plugins/<x>/skills/*`). Skills
70
+ * with no such ancestor fall back to `root`.
71
+ */
72
+ function scopeKeyFor(skillDir, root) {
73
+ const parent = dirname(skillDir);
74
+ if (basename(parent) === "skills") return dirname(parent);
75
+ return root;
76
+ }
77
+ /** Recursively list `.md` files under a directory, relative-path keyed. */
78
+ function markdownFilesRelative(dir, base = dir) {
79
+ const out = {};
80
+ let entries;
81
+ try {
82
+ entries = readdirSync(dir, { withFileTypes: true });
83
+ } catch {
84
+ return out;
85
+ }
86
+ for (const entry of entries) {
87
+ const full = join(dir, entry.name);
88
+ if (entry.isDirectory()) Object.assign(out, markdownFilesRelative(full, base));
89
+ else if (entry.isFile() && entry.name.endsWith(".md")) out[relative(base, full)] = full;
90
+ }
91
+ return out;
92
+ }
93
+ /** Gather every reference file for a skill, with content + hash loaded. */
94
+ function referenceFilesFor(skillDir) {
95
+ const skillName = basename(skillDir);
96
+ const referencesDir = join(skillDir, "references");
97
+ try {
98
+ if (!statSync(referencesDir).isDirectory()) return [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ const files = markdownFilesRelative(referencesDir);
103
+ return Object.entries(files).map(([relPath, absPath]) => {
104
+ const raw = readFileSync(absPath, "utf8");
105
+ return {
106
+ skillName,
107
+ skillDir,
108
+ relPath,
109
+ absPath,
110
+ hash: hashContent(raw),
111
+ raw
112
+ };
113
+ });
114
+ }
115
+ //#endregion
116
+ //#region src/dedup/analyze.ts
117
+ /**
118
+ * Cross-skill reference de-duplication analysis.
119
+ *
120
+ * ESLint (and oxlint) rules see one file at a time, so they cannot reliably
121
+ * detect that two DIFFERENT skills carry duplicated or drifted copies of the
122
+ * same reference content — that is a repo/collection-level analysis. This
123
+ * module implements it directly: group sibling skills into a comparison
124
+ * scope, hash their `references/**​/*.md` files, and classify:
125
+ *
126
+ * - byte-identical content across skills → `duplicate` (maintenance burden)
127
+ * - same filename, different content across skills → `divergence` (the
128
+ * copies forked — a silent correctness hazard, since a fix to one will not
129
+ * reach the other)
130
+ * - (opt-in) different filenames, highly similar content → `near-duplicate`
131
+ * (a possible copy that was renamed instead of shared)
132
+ */
133
+ var DEFAULT_NEAR_DUPLICATE_THRESHOLD = .75;
134
+ var DEFAULT_MEMORY_SCOPE = "skills";
135
+ function analyzeReferences(root, options = {}) {
136
+ const absRoot = resolve(root);
137
+ const scopeStrategy = options.scope ?? "plugin";
138
+ const scopes = /* @__PURE__ */ new Map();
139
+ for (const skillDir of findSkillDirs(absRoot)) addToScope(scopes, scopeStrategy === "repo" ? absRoot : scopeKeyFor(skillDir, absRoot), referenceFilesFor(skillDir));
140
+ return classify(scopes, options);
141
+ }
142
+ /**
143
+ * The same analysis over skills held in memory (a database, a remote store),
144
+ * for hosts that never write their skills to disk. Each skill is compared
145
+ * within its `scope` (default: one scope for every skill passed). A file's
146
+ * `absPath` in the findings is `<name>/references/<relPath>`.
147
+ */
148
+ function analyzeSkills(skills, options = {}) {
149
+ const scopes = /* @__PURE__ */ new Map();
150
+ for (const skill of skills) addToScope(scopes, skill.scope ?? DEFAULT_MEMORY_SCOPE, skill.references.map(({ relPath, content }) => ({
151
+ skillName: skill.name,
152
+ skillDir: skill.name,
153
+ relPath,
154
+ absPath: `${skill.name}/references/${relPath}`,
155
+ hash: hashContent(content),
156
+ raw: content
157
+ })));
158
+ return classify(scopes, options);
159
+ }
160
+ function addToScope(scopes, scope, files) {
161
+ if (files.length === 0) return;
162
+ const bucket = scopes.get(scope) ?? [];
163
+ bucket.push(...files);
164
+ scopes.set(scope, bucket);
165
+ }
166
+ /** Classify every scope's reference files into duplicates, divergences and near-duplicates. */
167
+ function classify(allScopes, options) {
168
+ const ignore = new Set(options.ignoreRelPaths ?? []);
169
+ const scopes = new Map([...allScopes].map(([scope, files]) => [scope, files.filter((f) => !ignore.has(f.relPath))]));
170
+ const duplicates = [];
171
+ const divergences = [];
172
+ const nearDuplicates = [];
173
+ for (const [scope, files] of scopes) {
174
+ const byRelPath = /* @__PURE__ */ new Map();
175
+ for (const file of files) {
176
+ const bucket = byRelPath.get(file.relPath) ?? [];
177
+ bucket.push(file);
178
+ byRelPath.set(file.relPath, bucket);
179
+ }
180
+ for (const [relPath, group] of byRelPath) {
181
+ if (new Set(group.map((f) => f.skillName)).size < 2) continue;
182
+ const byHash = /* @__PURE__ */ new Map();
183
+ for (const file of group) {
184
+ const bucket = byHash.get(file.hash) ?? [];
185
+ bucket.push(file);
186
+ byHash.set(file.hash, bucket);
187
+ }
188
+ if (byHash.size === 1) {
189
+ const [hash] = byHash.keys();
190
+ duplicates.push({
191
+ type: "duplicate",
192
+ severity: "warn",
193
+ scope,
194
+ relPath,
195
+ hash,
196
+ files: group.map((f) => ({
197
+ skillName: f.skillName,
198
+ absPath: f.absPath
199
+ }))
200
+ });
201
+ } else divergences.push({
202
+ type: "divergence",
203
+ severity: "error",
204
+ scope,
205
+ relPath,
206
+ groups: [...byHash.entries()].map(([hash, hashFiles]) => ({
207
+ hash,
208
+ files: hashFiles.map((f) => ({
209
+ skillName: f.skillName,
210
+ absPath: f.absPath
211
+ }))
212
+ }))
213
+ });
214
+ }
215
+ if (options.nearDuplicates) {
216
+ const threshold = options.nearDuplicateThreshold ?? DEFAULT_NEAR_DUPLICATE_THRESHOLD;
217
+ const alreadyPaired = /* @__PURE__ */ new Set();
218
+ for (const finding of [...duplicates, ...divergences]) if (finding.scope === scope) alreadyPaired.add(finding.relPath);
219
+ for (let i = 0; i < files.length; i++) for (let j = i + 1; j < files.length; j++) {
220
+ const a = files[i];
221
+ const b = files[j];
222
+ if (a.skillName === b.skillName || a.relPath === b.relPath || alreadyPaired.has(a.relPath) || alreadyPaired.has(b.relPath)) continue;
223
+ const score = similarity(a.raw, b.raw);
224
+ if (score >= threshold) nearDuplicates.push({
225
+ type: "near-duplicate",
226
+ severity: "warn",
227
+ scope,
228
+ fileA: {
229
+ skillName: a.skillName,
230
+ absPath: a.absPath,
231
+ relPath: a.relPath
232
+ },
233
+ fileB: {
234
+ skillName: b.skillName,
235
+ absPath: b.absPath,
236
+ relPath: b.relPath
237
+ },
238
+ similarity: score
239
+ });
240
+ }
241
+ }
242
+ }
243
+ return {
244
+ duplicates,
245
+ divergences,
246
+ nearDuplicates
247
+ };
248
+ }
249
+ //#endregion
250
+ //#region src/dedup/report.ts
251
+ function formatReport(result) {
252
+ const lines = [];
253
+ for (const d of result.duplicates) lines.push(`warn duplicate references/${d.relPath} is byte-identical across: ${d.files.map((f) => f.skillName).join(", ")} — consider consolidating into a shared location.`);
254
+ for (const d of result.divergences) {
255
+ const groups = d.groups.map((g) => `[${g.files.map((f) => f.skillName).join(", ")}]`).join(" vs ");
256
+ lines.push(`error divergence references/${d.relPath} has DIFFERENT content across skills: ${groups} — reconcile or rename.`);
257
+ }
258
+ for (const n of result.nearDuplicates) lines.push(`warn near-dup ${n.fileA.skillName}/references/${n.fileA.relPath} ~ ${n.fileB.skillName}/references/${n.fileB.relPath} (${Math.round(n.similarity * 100)}% similar) — consider sharing instead of duplicating.`);
259
+ if (lines.length === 0) lines.push("No cross-skill reference duplication or drift found.");
260
+ const summary = `\n${result.duplicates.length} duplicate(s), ${result.divergences.length} divergence(s), ${result.nearDuplicates.length} near-duplicate(s).`;
261
+ return lines.join("\n") + summary;
262
+ }
263
+ //#endregion
264
+ export { analyzeReferences as n, analyzeSkills as r, formatReport as t };
265
+
266
+ //# sourceMappingURL=report-ClPKWYJy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-ClPKWYJy.js","names":[],"sources":["../src/dedup/hash.ts","../src/dedup/discover.ts","../src/dedup/analyze.ts","../src/dedup/report.ts"],"sourcesContent":["/**\n * Content normalization and hashing for cross-skill reference comparison.\n */\n\nimport { createHash } from \"node:crypto\";\n\n/**\n * Normalize markdown content so incidental formatting differences (line\n * endings, trailing whitespace, a stray blank line) don't defeat\n * identical-content detection.\n */\nexport function normalize(content: string): string {\n\treturn content\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.replace(/\\s+$/, \"\"))\n\t\t.join(\"\\n\")\n\t\t.replace(/\\n{3,}/g, \"\\n\\n\")\n\t\t.trim();\n}\n\n/** SHA-1 hash of the normalized content. */\nexport function hashContent(content: string): string {\n\treturn createHash(\"sha1\").update(normalize(content)).digest(\"hex\");\n}\n\n/**\n * Word-shingle Jaccard similarity between two texts, for near-duplicate\n * detection. Returns a value in [0, 1].\n */\nexport function similarity(a: string, b: string, shingleSize = 5): number {\n\tconst shinglesOf = (text: string): Set<string> => {\n\t\tconst words = normalize(text).toLowerCase().split(/\\s+/).filter(Boolean);\n\t\tconst shingles = new Set<string>();\n\t\tfor (let i = 0; i <= words.length - shingleSize; i++) {\n\t\t\tshingles.add(words.slice(i, i + shingleSize).join(\" \"));\n\t\t}\n\t\treturn shingles;\n\t};\n\n\tconst setA = shinglesOf(a);\n\tconst setB = shinglesOf(b);\n\tif (setA.size === 0 || setB.size === 0) {\n\t\treturn 0;\n\t}\n\tlet intersection = 0;\n\tfor (const shingle of setA) {\n\t\tif (setB.has(shingle)) {\n\t\t\tintersection++;\n\t\t}\n\t}\n\tconst union = setA.size + setB.size - intersection;\n\treturn union === 0 ? 0 : intersection / union;\n}\n","/**\n * Discover skills and their reference files under a root directory.\n */\n\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { basename, dirname, join, relative } from \"node:path\";\n\nimport { hashContent } from \"./hash\";\nimport type { ReferenceFile } from \"./types\";\n\nconst IGNORED_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \"coverage\"]);\n\n/** Recursively find every directory containing a `SKILL.md` file. */\nexport function findSkillDirs(root: string): string[] {\n\tconst out: string[] = [];\n\tconst walk = (dir: string): void => {\n\t\tlet entries;\n\t\ttry {\n\t\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (entries.some((e) => e.isFile() && e.name === \"SKILL.md\")) {\n\t\t\tout.push(dir);\n\t\t}\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.isDirectory() && !IGNORED_DIRS.has(entry.name)) {\n\t\t\t\twalk(join(dir, entry.name));\n\t\t\t}\n\t\t}\n\t};\n\twalk(root);\n\treturn out.sort();\n}\n\n/**\n * The comparison scope a skill belongs to under the \"plugin\" strategy: the\n * parent of the nearest ancestor directory literally named `skills` (i.e. the\n * plugin directory for sibling skills under `plugins/<x>/skills/*`). Skills\n * with no such ancestor fall back to `root`.\n */\nexport function scopeKeyFor(skillDir: string, root: string): string {\n\tconst parent = dirname(skillDir);\n\tif (basename(parent) === \"skills\") {\n\t\treturn dirname(parent);\n\t}\n\treturn root;\n}\n\n/** Recursively list `.md` files under a directory, relative-path keyed. */\nfunction markdownFilesRelative(dir: string, base = dir): Record<string, string> {\n\tconst out: Record<string, string> = {};\n\tlet entries;\n\ttry {\n\t\tentries = readdirSync(dir, { withFileTypes: true });\n\t} catch {\n\t\treturn out;\n\t}\n\tfor (const entry of entries) {\n\t\tconst full = join(dir, entry.name);\n\t\tif (entry.isDirectory()) {\n\t\t\tObject.assign(out, markdownFilesRelative(full, base));\n\t\t} else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n\t\t\tout[relative(base, full)] = full;\n\t\t}\n\t}\n\treturn out;\n}\n\n/** Gather every reference file for a skill, with content + hash loaded. */\nexport function referenceFilesFor(skillDir: string): ReferenceFile[] {\n\tconst skillName = basename(skillDir);\n\tconst referencesDir = join(skillDir, \"references\");\n\ttry {\n\t\tif (!statSync(referencesDir).isDirectory()) {\n\t\t\treturn [];\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\tconst files = markdownFilesRelative(referencesDir);\n\treturn Object.entries(files).map(([relPath, absPath]) => {\n\t\tconst raw = readFileSync(absPath, \"utf8\");\n\t\treturn { skillName, skillDir, relPath, absPath, hash: hashContent(raw), raw };\n\t});\n}\n","/**\n * Cross-skill reference de-duplication analysis.\n *\n * ESLint (and oxlint) rules see one file at a time, so they cannot reliably\n * detect that two DIFFERENT skills carry duplicated or drifted copies of the\n * same reference content — that is a repo/collection-level analysis. This\n * module implements it directly: group sibling skills into a comparison\n * scope, hash their `references/**​/*.md` files, and classify:\n *\n * - byte-identical content across skills → `duplicate` (maintenance burden)\n * - same filename, different content across skills → `divergence` (the\n * copies forked — a silent correctness hazard, since a fix to one will not\n * reach the other)\n * - (opt-in) different filenames, highly similar content → `near-duplicate`\n * (a possible copy that was renamed instead of shared)\n */\n\nimport { resolve } from \"node:path\";\n\nimport { findSkillDirs, referenceFilesFor, scopeKeyFor } from \"./discover\";\nimport { hashContent, similarity } from \"./hash\";\nimport type {\n\tAnalyzeOptions,\n\tAnalyzeResult,\n\tDivergenceFinding,\n\tDuplicateFinding,\n\tNearDuplicateFinding,\n\tReferenceFile,\n\tSkillReferences,\n} from \"./types\";\n\nconst DEFAULT_NEAR_DUPLICATE_THRESHOLD = 0.75;\nconst DEFAULT_MEMORY_SCOPE = \"skills\";\n\nexport function analyzeReferences(\n\troot: string,\n\toptions: AnalyzeOptions = {},\n): AnalyzeResult {\n\tconst absRoot = resolve(root);\n\tconst scopeStrategy = options.scope ?? \"plugin\";\n\n\tconst scopes = new Map<string, ReferenceFile[]>();\n\tfor (const skillDir of findSkillDirs(absRoot)) {\n\t\tconst scope = scopeStrategy === \"repo\" ? absRoot : scopeKeyFor(skillDir, absRoot);\n\t\taddToScope(scopes, scope, referenceFilesFor(skillDir));\n\t}\n\n\treturn classify(scopes, options);\n}\n\n/**\n * The same analysis over skills held in memory (a database, a remote store),\n * for hosts that never write their skills to disk. Each skill is compared\n * within its `scope` (default: one scope for every skill passed). A file's\n * `absPath` in the findings is `<name>/references/<relPath>`.\n */\nexport function analyzeSkills(\n\tskills: SkillReferences[],\n\toptions: Omit<AnalyzeOptions, \"scope\"> = {},\n): AnalyzeResult {\n\tconst scopes = new Map<string, ReferenceFile[]>();\n\tfor (const skill of skills) {\n\t\taddToScope(\n\t\t\tscopes,\n\t\t\tskill.scope ?? DEFAULT_MEMORY_SCOPE,\n\t\t\tskill.references.map(({ relPath, content }) => ({\n\t\t\t\tskillName: skill.name,\n\t\t\t\tskillDir: skill.name,\n\t\t\t\trelPath,\n\t\t\t\tabsPath: `${skill.name}/references/${relPath}`,\n\t\t\t\thash: hashContent(content),\n\t\t\t\traw: content,\n\t\t\t})),\n\t\t);\n\t}\n\n\treturn classify(scopes, options);\n}\n\nfunction addToScope(\n\tscopes: Map<string, ReferenceFile[]>,\n\tscope: string,\n\tfiles: ReferenceFile[],\n): void {\n\tif (files.length === 0) {\n\t\treturn;\n\t}\n\tconst bucket = scopes.get(scope) ?? [];\n\tbucket.push(...files);\n\tscopes.set(scope, bucket);\n}\n\n/** Classify every scope's reference files into duplicates, divergences and near-duplicates. */\nfunction classify(\n\tallScopes: Map<string, ReferenceFile[]>,\n\toptions: Omit<AnalyzeOptions, \"scope\">,\n): AnalyzeResult {\n\tconst ignore = new Set(options.ignoreRelPaths ?? []);\n\tconst scopes = new Map(\n\t\t[...allScopes].map(([scope, files]) => [\n\t\t\tscope,\n\t\t\tfiles.filter((f) => !ignore.has(f.relPath)),\n\t\t]),\n\t);\n\n\tconst duplicates: DuplicateFinding[] = [];\n\tconst divergences: DivergenceFinding[] = [];\n\tconst nearDuplicates: NearDuplicateFinding[] = [];\n\n\tfor (const [scope, files] of scopes) {\n\t\t// Group by relPath (filename within references/) to find same-name\n\t\t// files, then split each group by hash.\n\t\tconst byRelPath = new Map<string, ReferenceFile[]>();\n\t\tfor (const file of files) {\n\t\t\tconst bucket = byRelPath.get(file.relPath) ?? [];\n\t\t\tbucket.push(file);\n\t\t\tbyRelPath.set(file.relPath, bucket);\n\t\t}\n\n\t\tfor (const [relPath, group] of byRelPath) {\n\t\t\tconst distinctSkills = new Set(group.map((f) => f.skillName));\n\t\t\tif (distinctSkills.size < 2) {\n\t\t\t\tcontinue; // Only one skill has this filename — nothing to compare.\n\t\t\t}\n\n\t\t\tconst byHash = new Map<string, ReferenceFile[]>();\n\t\t\tfor (const file of group) {\n\t\t\t\tconst bucket = byHash.get(file.hash) ?? [];\n\t\t\t\tbucket.push(file);\n\t\t\t\tbyHash.set(file.hash, bucket);\n\t\t\t}\n\n\t\t\tif (byHash.size === 1) {\n\t\t\t\t// Every skill's copy of this filename is byte-identical.\n\t\t\t\tconst [hash] = byHash.keys();\n\t\t\t\tduplicates.push({\n\t\t\t\t\ttype: \"duplicate\",\n\t\t\t\t\tseverity: \"warn\",\n\t\t\t\t\tscope,\n\t\t\t\t\trelPath,\n\t\t\t\t\thash,\n\t\t\t\t\tfiles: group.map((f) => ({\n\t\t\t\t\t\tskillName: f.skillName,\n\t\t\t\t\t\tabsPath: f.absPath,\n\t\t\t\t\t})),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Same filename, but content diverged across skills.\n\t\t\t\tdivergences.push({\n\t\t\t\t\ttype: \"divergence\",\n\t\t\t\t\tseverity: \"error\",\n\t\t\t\t\tscope,\n\t\t\t\t\trelPath,\n\t\t\t\t\tgroups: [...byHash.entries()].map(([hash, hashFiles]) => ({\n\t\t\t\t\t\thash,\n\t\t\t\t\t\tfiles: hashFiles.map((f) => ({\n\t\t\t\t\t\t\tskillName: f.skillName,\n\t\t\t\t\t\t\tabsPath: f.absPath,\n\t\t\t\t\t\t})),\n\t\t\t\t\t})),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (options.nearDuplicates) {\n\t\t\tconst threshold =\n\t\t\t\toptions.nearDuplicateThreshold ?? DEFAULT_NEAR_DUPLICATE_THRESHOLD;\n\t\t\tconst alreadyPaired = new Set<string>();\n\t\t\tfor (const finding of [...duplicates, ...divergences]) {\n\t\t\t\tif (finding.scope === scope) {\n\t\t\t\t\talreadyPaired.add(finding.relPath);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfor (let i = 0; i < files.length; i++) {\n\t\t\t\tfor (let j = i + 1; j < files.length; j++) {\n\t\t\t\t\tconst a = files[i];\n\t\t\t\t\tconst b = files[j];\n\t\t\t\t\tif (\n\t\t\t\t\t\ta.skillName === b.skillName ||\n\t\t\t\t\t\ta.relPath === b.relPath ||\n\t\t\t\t\t\talreadyPaired.has(a.relPath) ||\n\t\t\t\t\t\talreadyPaired.has(b.relPath)\n\t\t\t\t\t) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst score = similarity(a.raw, b.raw);\n\t\t\t\t\tif (score >= threshold) {\n\t\t\t\t\t\tnearDuplicates.push({\n\t\t\t\t\t\t\ttype: \"near-duplicate\",\n\t\t\t\t\t\t\tseverity: \"warn\",\n\t\t\t\t\t\t\tscope,\n\t\t\t\t\t\t\tfileA: {\n\t\t\t\t\t\t\t\tskillName: a.skillName,\n\t\t\t\t\t\t\t\tabsPath: a.absPath,\n\t\t\t\t\t\t\t\trelPath: a.relPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tfileB: {\n\t\t\t\t\t\t\t\tskillName: b.skillName,\n\t\t\t\t\t\t\t\tabsPath: b.absPath,\n\t\t\t\t\t\t\t\trelPath: b.relPath,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tsimilarity: score,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { duplicates, divergences, nearDuplicates };\n}\n","/**\n * Human-readable rendering of an analysis result.\n */\n\nimport type { AnalyzeResult } from \"./types\";\n\nexport function formatReport(result: AnalyzeResult): string {\n\tconst lines: string[] = [];\n\n\tfor (const d of result.duplicates) {\n\t\tlines.push(\n\t\t\t`warn duplicate references/${d.relPath} is byte-identical across: ${d.files\n\t\t\t\t.map((f) => f.skillName)\n\t\t\t\t.join(\", \")} — consider consolidating into a shared location.`,\n\t\t);\n\t}\n\n\tfor (const d of result.divergences) {\n\t\tconst groups = d.groups\n\t\t\t.map((g) => `[${g.files.map((f) => f.skillName).join(\", \")}]`)\n\t\t\t.join(\" vs \");\n\t\tlines.push(\n\t\t\t`error divergence references/${d.relPath} has DIFFERENT content across skills: ${groups} — reconcile or rename.`,\n\t\t);\n\t}\n\n\tfor (const n of result.nearDuplicates) {\n\t\tlines.push(\n\t\t\t`warn near-dup ${n.fileA.skillName}/references/${n.fileA.relPath} ~ ${n.fileB.skillName}/references/${n.fileB.relPath} (${Math.round(n.similarity * 100)}% similar) — consider sharing instead of duplicating.`,\n\t\t);\n\t}\n\n\tif (lines.length === 0) {\n\t\tlines.push(\"No cross-skill reference duplication or drift found.\");\n\t}\n\n\tconst summary = `\\n${result.duplicates.length} duplicate(s), ${result.divergences.length} divergence(s), ${result.nearDuplicates.length} near-duplicate(s).`;\n\treturn lines.join(\"\\n\") + summary;\n}\n"],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Keep the skill body short to preserve progressive disclosure.
3
+ *
4
+ * A SKILL.md is an entry point, not a manual: heavy detail belongs in
5
+ * referenced files that are loaded on demand. This rule flags bodies that grow
6
+ * past a line budget (frontmatter excluded).
7
+ */
8
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
9
+ declare const rule: MarkdownRuleDefinition<{
10
+ RuleOptions: [{
11
+ maxLines?: number;
12
+ }?];
13
+ MessageIds: "tooLong";
14
+ }>;
15
+ export default rule;
16
+ //# sourceMappingURL=body-max-length.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"body-max-length.d.ts","sourceRoot":"","sources":["../../src/rules/body-max-length.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAO/D,QAAA,MAAM,IAAI,EAAE,sBAAsB,CAAC;IAClC,WAAW,EAAE,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtC,UAAU,EAAE,SAAS,CAAC;CACtB,CAiDA,CAAC;eAEa,IAAI"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Require the `description` to express when the skill applies.
3
+ *
4
+ * An agent routes to a skill purely from its description. A description that
5
+ * only says *what* the skill is, without signalling *when* to use it, routes
6
+ * poorly. This rule checks for a trigger cue (configurable).
7
+ */
8
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
9
+ declare const rule: MarkdownRuleDefinition<{
10
+ RuleOptions: [{
11
+ patterns?: string[];
12
+ }?];
13
+ MessageIds: "noTrigger";
14
+ }>;
15
+ export default rule;
16
+ //# sourceMappingURL=description-has-trigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-has-trigger.d.ts","sourceRoot":"","sources":["../../src/rules/description-has-trigger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAoB/D,QAAA,MAAM,IAAI,EAAE,sBAAsB,CAAC;IAClC,WAAW,EAAE,CAAC;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;IACxC,UAAU,EAAE,WAAW,CAAC;CACxB,CAkEA,CAAC;eAEa,IAAI"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @fileoverview Cap the length of the skill `description`.
3
+ *
4
+ * The description is the only text an agent sees when deciding whether to load
5
+ * a skill, so it must stay compact. The default ceiling mirrors the documented
6
+ * 1024-character frontmatter budget.
7
+ */
8
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
9
+ declare const rule: MarkdownRuleDefinition<{
10
+ RuleOptions: [{
11
+ max?: number;
12
+ }?];
13
+ MessageIds: "tooLong";
14
+ }>;
15
+ export default rule;
16
+ //# sourceMappingURL=description-length.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-length.d.ts","sourceRoot":"","sources":["../../src/rules/description-length.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAQ/D,QAAA,MAAM,IAAI,EAAE,sBAAsB,CAAC;IAClC,WAAW,EAAE,CAAC;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACjC,UAAU,EAAE,SAAS,CAAC;CACtB,CAkDA,CAAC;eAEa,IAAI"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview Disallow HTML/XML tags in the skill `description`.
3
+ *
4
+ * Descriptions are consumed as plain routing text. Embedded tags add noise and
5
+ * can interfere with how the description is parsed and displayed.
6
+ */
7
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
8
+ declare const rule: MarkdownRuleDefinition<{
9
+ RuleOptions: [];
10
+ MessageIds: "hasTag";
11
+ }>;
12
+ export default rule;
13
+ //# sourceMappingURL=description-no-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"description-no-html.d.ts","sourceRoot":"","sources":["../../src/rules/description-no-html.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAU/D,QAAA,MAAM,IAAI,EAAE,sBAAsB,CAAC;IAClC,WAAW,EAAE,EAAE,CAAC;IAChB,UAAU,EAAE,QAAQ,CAAC;CACrB,CAyCA,CAAC;eAEa,IAAI"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Require reference files to have descriptive names.
3
+ *
4
+ * Claude decides whether to open a reference file partly from its name, so
5
+ * generic names like `doc2.md` or `untitled.md` carry no signal. Official
6
+ * guidance: name files descriptively (`form_validation_rules.md`, not
7
+ * `doc2.md`).
8
+ */
9
+ import type { MarkdownRuleDefinition } from "@eslint/markdown";
10
+ declare const rule: MarkdownRuleDefinition<{
11
+ RuleOptions: [];
12
+ MessageIds: "generic";
13
+ }>;
14
+ export default rule;
15
+ //# sourceMappingURL=descriptive-reference-filenames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"descriptive-reference-filenames.d.ts","sourceRoot":"","sources":["../../src/rules/descriptive-reference-filenames.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAW/D,QAAA,MAAM,IAAI,EAAE,sBAAsB,CAAC;IAClC,WAAW,EAAE,EAAE,CAAC;IAChB,UAAU,EAAE,SAAS,CAAC;CACtB,CAkCA,CAAC;eAEa,IAAI"}