diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 4cd10caad..6cb9ad710 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -60,13 +60,14 @@ as Markdown links, including decoded attributes and fragment validation. URL attributes remove ASCII tabs/newlines throughout and discard surrounding ASCII control/space characters before resolution. Iframe/embed sources and object data attributes are document references and retain Markdown-target anchor checks. -Image and media references must resolve to files, not directories. +Image and media references require nonempty targets that resolve to files, not directories. Image references check file existence without interpreting image fragments as Markdown headings; document links keep anchor checks even when sharing a target. HTML video, audio, source and track `src` assets and video posters use the same existence checks as images. Entity decoding uses full HTML text/attribute rules, including references whose semicolon may be omitted. Inline guidance imports are rejected after punctuation as well as whitespace. +At-signs inside external URLs are excluded from the import scan. Extensionless inline candidates are also imports when they resolve to repository files, checking the full filename before prefixes at ASCII/Unicode prose separators. Declared scoped dependencies, scope wildcards and matching TypeScript path aliases diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index f7e4e0c27..6d4288654 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -41,6 +41,7 @@ async function validateReference( if (unresolvedReference !== undefined) return `${source}: unresolved Markdown reference "${unresolvedReference}"`; if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(target)) return; + if (image && !target) return `${source}: empty media target`; let path; let anchor; try { @@ -184,7 +185,10 @@ export async function validateAgentGuidance({ rootDir }) { diagnostics.push( `${source}: at most ${maxBytes} UTF-8 bytes allowed (received ${bytes})` ); - const prose = guidanceProse(markdown); + const prose = guidanceProse(markdown).replace( + /(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]+/giu, + ' ' + ); const imports = guidanceStandaloneImports(markdown); const inlineImports = []; for (const match of prose.matchAll( diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index 6aac2c98a..e5ca63685 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1381,3 +1381,21 @@ for (const entity of [' ', ' ', ' ']) { ); }); } + +for (const markup of ['![logo]()', '', '']) { + test(`empty media references are rejected: ${markup}`, async (t) => { + assert.ok((await diagnostics(t, { 'AGENTS.md': markup })).length > 0); + }); +} +for (const url of [ + 'https://example.com/@docs/guide', + 'https://example.com/user?next=@docs/guide', + '//example.com/@docs/guide', +]) { + test(`external URL at-sign is not an import: ${url}`, async (t) => { + assert.deepEqual( + await diagnostics(t, { 'AGENTS.md': 'Visit ' + url }), + [] + ); + }); +}