From 84596ecb41cce70a043d879299b37bb912900ded Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 20 Sep 2026 20:20:09 +0200 Subject: [PATCH] fix(agents): require standalone top-level Claude import --- docs/development/agent-workflow.md | 2 ++ tools/skills/agent-guidance-markdown.mjs | 10 ++++++++++ tools/skills/validate-agent-guidance.mjs | 5 ++--- tools/skills/validate-agent-guidance.test.mjs | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index a9779cfbd..fe10a1c71 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -40,6 +40,8 @@ prefix or Markdown link for other ambiguous filenames that resemble code symbols Multi-part dotfiles are path candidates too. Link paths and fragments are decoded separately so encoded filename delimiters stay in the filename. Fenced and indented examples do not count as root guidance imports or satisfy the required Claude import. +The required Claude import must be an unformatted standalone line in a top-level +paragraph; headings, quotes and list items do not satisfy it. Heading anchors decode HTML character references in text and use `github-slugger` for GitHub-compatible character filtering and duplicate suffixes. Explicit HTML anchors use `parse5`, excluding comments, scripts, styles and template contents. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index a1af4b278..afa90aa63 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -83,6 +83,16 @@ export function guidanceProse(markdown) { return markdownLexer.lexer(markdown).map(prose).join('\n'); } +export function guidanceStandaloneImports(markdown) { + return markdownLexer + .lexer(markdown) + .filter((token) => token.type === 'paragraph') + .flatMap((token) => [ + ...token.raw.matchAll(/^ {0,3}@([^\s]+)[\t ]*$/gmu), + ]) + .map((match) => match[1]); +} + export function guidanceAnchors(markdown) { const slugger = new GithubSlugger(); const found = new Set(); diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index aaf89f005..76232ae56 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'; import { guidanceAnchors as anchors, guidanceProse, + guidanceStandaloneImports, guidanceReferences as references, } from './agent-guidance-markdown.mjs'; @@ -90,9 +91,7 @@ export async function validateAgentGuidance({ rootDir }) { `${source}: at most ${maxBytes} UTF-8 bytes allowed (received ${bytes})` ); const unfenced = guidanceProse(markdown); - const imports = [...unfenced.matchAll(/^\s*@([^\s]+)\s*$/gmu)].map( - (match) => match[1] - ); + const imports = guidanceStandaloneImports(markdown); const prose = unfenced.replace(/`[^`\n]+`/gu, ''); const inlineImports = [...prose.matchAll(/(?:^|[\s(])@([^\s]+)/gu)] .map((match) => match[1]) diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index d02c7bcac..b53e55f75 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -558,3 +558,17 @@ test('GitHub heading slugs remove non-ASCII whitespace', async (t) => { ); } }); + +for (const body of [ + '> @AGENTS.md', + '- @AGENTS.md', + '# @AGENTS.md', + '**@AGENTS.md**', +]) { + test(`structured Markdown cannot satisfy Claude import: ${body}`, async (t) => { + assert.match( + (await diagnostics(t, { 'CLAUDE.md': body })).join('\n'), + /standalone @AGENTS.md/ + ); + }); +}