diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md
index 42cb09109..11b549f78 100644
--- a/docs/development/agent-workflow.md
+++ b/docs/development/agent-workflow.md
@@ -69,7 +69,7 @@ Image and media references require a nonempty path that resolves to a file, not
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
+HTML image-input, 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.
@@ -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 or quote followed by punctuation and an at-sign terminates a bare URL exclusion.
+A closing bracket or matching enclosing 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
@@ -88,7 +88,7 @@ rejected before those exemptions, including document paths with fragments or que
All recognized Markdown extensions share the document-import guard; reStructuredText
and AsciiDoc, PDF, Word, OpenDocument, RTF, Org and TeX documents are also excluded
from package exemptions. Recognized extensionless guidance names (including AGENTS,
-CLAUDE, INSTRUCTIONS and README) are excluded in package subpaths too. URL-encoded
+CLAUDE, INSTRUCTIONS and README, case-insensitively) are excluded in package subpaths too. URL-encoded
paths do not receive package exemptions. TypeScript configuration is parsed as JSONC.
Declared packages also permit safe subpaths; exact aliases stay exact.
Declared package mentions may include a version (including semver comparators and wildcard ranges) or dist-tag qualifier.
diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs
index a903180c6..5f697c1b9 100644
--- a/tools/skills/agent-guidance-markdown.mjs
+++ b/tools/skills/agent-guidance-markdown.mjs
@@ -90,7 +90,14 @@ function htmlNavigation(html, inspect = () => {}) {
].includes(node.tagName) &&
attribute.name === 'src') ||
(node.tagName === 'video' && attribute.name === 'poster') ||
- (node.tagName === 'object' && attribute.name === 'data')
+ (node.tagName === 'object' && attribute.name === 'data') ||
+ (node.tagName === 'input' &&
+ attribute.name === 'src' &&
+ node.attrs.some(
+ (attr) =>
+ attr.name === 'type' &&
+ attr.value.toLowerCase() === 'image'
+ ))
)
references.push({
target: attribute.value
@@ -141,7 +148,7 @@ function htmlNavigation(html, inspect = () => {}) {
}
export function guidanceProse(markdown) {
- function text(node) {
+ function text(node, preceding = '') {
if (
['script', 'style', 'template', 'pre', 'code'].includes(
node.tagName
@@ -163,10 +170,29 @@ 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,
+ (url, offset) => {
+ const opening = (
+ preceding + node.value.slice(0, offset)
+ ).at(-1);
+ const closing = {
+ '"': '"',
+ "'": "'",
+ '“': '”',
+ '”': '”',
+ '‘': '’',
+ '’': '’',
+ }[opening];
+ const boundary = closing ? url.indexOf(closing) : -1;
+ return boundary >= 0 &&
+ /^[.,;:!?]*@/u.test(url.slice(boundary + 1))
+ ? ' ' + url.slice(boundary)
+ : ' ';
+ }
);
- const content = (node.childNodes ?? []).map(text).join('');
+ let content = '';
+ for (const child of node.childNodes ?? [])
+ content += text(child, preceding + content);
return [
'address',
'article',
diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs
index f500c4e24..1af1f3a17 100644
--- a/tools/skills/validate-agent-guidance.mjs
+++ b/tools/skills/validate-agent-guidance.mjs
@@ -166,7 +166,7 @@ async function packageMentions(rootDir) {
if (
/%[\da-f]{2}/iu.test(token) ||
MARKDOWN_EXTENSION.test(path) ||
- /(?:^|\/)(?:AGENTS|CLAUDE|INSTRUCTIONS|README|LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CHANGELOG)$/u.test(
+ /(?:^|\/)(?:AGENTS|CLAUDE|INSTRUCTIONS|README|LICENSE|LICENCE|NOTICE|COPYING|AUTHORS|CONTRIBUTORS|CHANGELOG)$/iu.test(
path
) ||
DOCUMENT_EXTENSION.test(path)
diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs
index 84dfd50df..ea7364c92 100644
--- a/tools/skills/validate-agent-guidance.test.mjs
+++ b/tools/skills/validate-agent-guidance.test.mjs
@@ -1630,3 +1630,42 @@ for (const embedded of [false, true]) {
);
});
}
+
+for (const url of [
+ "https://example.com/don't@docs/guide.md",
+ "https://example.com/path'@docs/guide.md",
+]) {
+ test(`internal URL apostrophe remains URL prose: ${url}`, async (t) => {
+ assert.deepEqual(await diagnostics(t, { 'AGENTS.md': url }), []);
+ });
+}
+for (const name of ['readme', 'instructions', 'Agents']) {
+ test(`guidance basename matching ignores case: ${name}`, async (t) => {
+ assert.ok(
+ (
+ await diagnostics(t, {
+ 'package.json': JSON.stringify({
+ dependencies: { '@angular/core': '*' },
+ }),
+ 'CLAUDE.md':
+ '@AGENTS.md\n\nRead @angular/core/docs/' + name,
+ })
+ ).some((message) => message.includes('additional or inline'))
+ );
+ });
+}
+test('image input source is validated', async (t) => {
+ assert.ok(
+ (
+ await diagnostics(t, {
+ 'AGENTS.md': '',
+ })
+ ).length > 0
+ );
+ assert.deepEqual(
+ await diagnostics(t, {
+ 'AGENTS.md': '',
+ }),
+ []
+ );
+});