diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 6a627b4d0..42cb09109 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -66,7 +66,7 @@ The first active HTML base href sets reference resolution, including nested srcd Resolved file URLs use native filesystem conversion, including Windows drive paths. Explicit srcset attributes must contain at least one parsed candidate. Image and media references require a nonempty path that resolves to a file, not a directory. -Direct file URLs are rejected; use portable repository-relative paths. +Direct file URLs and file-scheme HTML bases are rejected; use portable repository-relative paths. 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 @@ -77,7 +77,7 @@ At-signs inside external URIs (including explicit opaque autolinks such as mailt per HTML text node, preserving adjacent imports. A colon directly before an import does not make that import a URI. Opaque schemes are excluded only in parsed links whose visible text equals their URI, so colon-labeled prose remains checked. -A closing bracket followed by punctuation and an at-sign terminates a bare URL exclusion. +A closing bracket or quote followed by punctuation and an at-sign terminates a bare URL exclusion. Extensionless inline candidates are also imports when they resolve to repository files, checking the full filename before prefixes at ASCII/Unicode prose separators, including opening parentheses, brackets and braces. Each at-sign candidate is diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index 82b9aa3aa..a903180c6 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -163,7 +163,7 @@ export function guidanceProse(markdown) { } if (node.nodeName === '#text') return node.value.replace( - /(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]*?(?=[)\]}>][.,;:!?]*@|\s|$)/giu, + /(?:\b[a-z][a-z\d+.-]*:\/\/|\/\/)[^\s]*?(?=[)\]}>"'”’][.,;:!?]*@|\s|$)/giu, ' ' ); const content = (node.childNodes ?? []).map(text).join(''); diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index 9f0683e47..f500c4e24 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -48,7 +48,11 @@ async function validateReference( if (bases?.length) { try { let base = pathToFileURL(resolve(rootDir, source)); - for (const href of bases) base = new URL(href, base); + for (const href of bases) { + if (/^file:/iu.test(href.replace(/[\t\n\r]/gu, '').trimStart())) + return `${source}: use a repository-relative HTML base instead of a file URL`; + base = new URL(href, base); + } const url = new URL(target, base); if (url.protocol !== 'file:') return; resolvedBasePath = fileURLToPath(url); diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index ad9d0e0be..84dfd50df 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1597,3 +1597,36 @@ for (const markup of [ assert.ok((await diagnostics(t, { 'AGENTS.md': markup })).length > 0); }); } + +for (const quote of ['"', "'", '”']) { + test(`quoted URL preserves adjacent import: ${quote}`, async (t) => { + assert.ok( + ( + await diagnostics(t, { + 'CLAUDE.md': + '@AGENTS.md\n\nVisit ' + + quote + + 'https://example.com/path' + + quote + + '.@docs/guide.md', + }) + ).some((message) => message.includes('additional or inline')) + ); + }); +} +for (const embedded of [false, true]) { + test(`file URL base is rejected: embedded=${embedded}`, async (t) => { + const rootDir = await fixture(t); + const { pathToFileURL } = await import('node:url'); + const { realpath } = await import('node:fs/promises'); + const base = pathToFileURL((await realpath(rootDir)) + '/').href; + const html = `Guide`; + await writeFile( + join(rootDir, 'AGENTS.md'), + embedded ? `` : html + ); + assert.ok( + (await validateAgentGuidance({ rootDir })).diagnostics.length > 0 + ); + }); +}