From 7bf5faec6f45e9a502a45b34c707d88ee5cd4c48 Mon Sep 17 00:00:00 2001 From: Abhishek Sharma Date: Tue, 25 Aug 2026 13:05:20 -0700 Subject: [PATCH] fix(validator): exempt fenced examples from the references/ link check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validate-reference-links.js` scans every line of a SKILL.md, so a `references/…md` path shown inside a fenced code block is treated as a link to resolve. The rule this validator enforces therefore cannot be documented: its own failure message tells authors to write `../../references/.md` rather than `references/.md`, and a skill showing that contrast as an example fails the check that is explaining it. Reproduced on main by appending a fenced markdown example to using-agent-skills/SKILL.md — exit 1, with the guidance text printed underneath the very link it had just rejected. Lines inside a fence are now skipped. The scan is line-by-line rather than one regex over the document so the three CommonMark fence forms all hold: tilde fences, up to three spaces of indent, and a closing fence longer than its opener — and a ``` line inside a ~~~ block does not close it. Line numbers are preserved by collecting fenced line indices and skipping them during the scan, rather than stripping the text first, so the L in an error still points at the real line. Deliberately local rather than shared with scripts/lib/skill-lint.js: that module's stripFencedCodeBlocks is the single regex with the gaps described in #437, and #444 is already rewriting it. Forking a rewrite in flight costs more than the duplication; once #444 lands the two should collapse into one helper under scripts/lib/. Three tests added. All three fail against the current validator; the third pins that a genuinely broken link outside a fence is still reported, so the exemption cannot silently swallow real breakage. --- scripts/validate-reference-links-test.js | 73 ++++++++++++++++++++++++ scripts/validate-reference-links.js | 45 +++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/scripts/validate-reference-links-test.js b/scripts/validate-reference-links-test.js index 8c8b147a..93c41cb3 100644 --- a/scripts/validate-reference-links-test.js +++ b/scripts/validate-reference-links-test.js @@ -151,3 +151,76 @@ test('reports every unresolvable link, not just the first per skill', () => { assert.equal(result.status, 1, result.stdout + result.stderr); assert.match(result.stdout, /1 skills checked — 2 error\(s\) — FAILED/); }); + +test('a link inside a fenced block is an example, not a link to resolve', () => { + const root = makeSandbox(); + writeFile(root, 'references/definition-of-done.md', '# DoD\n'); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + 'Shared checklists live two levels up:', + '', + 'See `../../references/definition-of-done.md`.', + '', + 'Do not write it this way:', + '', + '```markdown', + '[Definition of Done](references/definition-of-done.md)', + '```', + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/); +}); + +test('fence exemption follows CommonMark rather than a bare ``` match', () => { + const root = makeSandbox(); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + '~~~markdown', + '[a](references/missing-a.md)', + '```', // wrong marker: must not close the ~~~ block + '[b](references/missing-b.md)', + '~~~', + '', + ' ```markdown', // three-space indent is still a fence + '[c](references/missing-c.md)', + ' `````', // a longer closer is legal + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /0 error\(s\) — PASSED/); +}); + +test('a real broken link outside any fence is still reported', () => { + const root = makeSandbox(); + writeFile( + root, + 'skills/using-agent-skills/SKILL.md', + [ + '```markdown', + '[shown as an example](references/example-only.md)', + '```', + '', + 'See `references/definition-of-done.md`.', // genuinely wrong, outside the fence + '', + ].join('\n') + ); + + const result = run(root); + + assert.equal(result.status, 1, result.stdout + result.stderr); + assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/); + assert.match(result.stdout, /L5: references\/definition-of-done\.md/); +}); diff --git a/scripts/validate-reference-links.js b/scripts/validate-reference-links.js index f460bf98..ae92d8bd 100644 --- a/scripts/validate-reference-links.js +++ b/scripts/validate-reference-links.js @@ -25,6 +25,12 @@ * mention paths that do not exist yet (`tasks/todo.md`, `PERF.md`, * `docs/ideas/[idea-name].md`), and those must not fail the build. * + * Fenced code blocks are exempt for the same reason. Text inside a fence is + * an example, not a link an agent will follow — and this rule in particular + * has to be documentable: the failure message below tells authors to write + * `../../references/.md` rather than `references/.md`, which no + * skill could show as an example without failing the very check explaining it. + * * Exit codes: 0 = all clear, 1 = one or more unresolvable links. */ @@ -41,11 +47,50 @@ const SKILLS_DIR = path.join(ROOT, 'skills'); // non-path character so `myreferences/x.md` does not match. const REFERENCE_LINK_RE = /(? { + const marker = line.match(/^ {0,3}(`{3,}|~{3,})/); + if (fence) { + fenced.add(i); + if (marker && marker[1][0] === fence.marker && marker[1].length >= fence.length) { + fence = null; + } + return; + } + if (marker) { + fence = { marker: marker[1][0], length: marker[1].length }; + fenced.add(i); + } + }); + + return fenced; +} + function findViolations(skillDir, skillFile) { const violations = []; const lines = fs.readFileSync(skillFile, 'utf8').split(/\r?\n/); + const fenced = fencedLineNumbers(lines); lines.forEach((line, i) => { + if (fenced.has(i)) return; for (const match of line.matchAll(REFERENCE_LINK_RE)) { const link = match[1]; if (!fs.existsSync(path.resolve(skillDir, link))) {