diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md index 887fe102e..9ca91b9b4 100644 --- a/docs/development/agent-workflow.md +++ b/docs/development/agent-workflow.md @@ -37,7 +37,7 @@ Write generic filenames as prose; commands, templates, globs, URLs, package aliases and dotted code symbols are excluded. Bare dotted names with conventional file suffixes (such as .md, .json or .ts) are treated as filenames. Use a `./` prefix or Markdown link for other ambiguous filenames that resemble code symbols. -Explicit relative literals denote paths, including spaces and hyphenated words. +Explicit relative literals denote paths, including spaces, filesystem punctuation and hyphenated words. Put executable command examples in fenced code when their syntax also looks like a path. Multi-part dotfiles are path candidates too. Conventional extensionless filenames such as Dockerfile, Makefile and LICENSE @@ -73,7 +73,7 @@ removed before matching a declared package, as are straight/curly apostrophe pos Markdown destinations decode HTML entities before URI parsing, matching rendered links. Heading-anchor lookup is limited to Markdown targets. Source-file line fragments, PDF page fragments and other non-Markdown fragments retain file-existence checks. -The import scan includes visible HTML text and excludes code and non-rendered containers. +The import scan includes visible HTML text and literal backticks; it excludes parsed code nodes and non-rendered containers. Navigation uses the parsed rendered tree too. Temporary in-memory markers retain definition, unresolved-reference and literal-path metadata, so Markdown inside inert templates is excluded consistently with raw HTML navigation. diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs index 089b0ba53..40e8da99a 100644 --- a/tools/skills/agent-guidance-markdown.mjs +++ b/tools/skills/agent-guidance-markdown.mjs @@ -179,13 +179,7 @@ function isLiteralRepositoryPath(token) { // Exclude recognizable prose/code forms instead of allowlisting paths. if (/^(?:@|--|[a-z][a-z\d+.-]*:|\/\/)/iu.test(token)) return false; const explicitRelative = /^(?:\.\/|\.\.\/)/u.test(token); - if ( - (explicitRelative - ? /[^\p{L}\p{N}_./# -]/u - : /[^\p{L}\p{N}_./#-]/u - ).test(token) - ) - return false; + if (!explicitRelative && /[^\p{L}\p{N}_./#-]/u.test(token)) return false; if (token.includes('YYYY-MM-DD') || /(?:^|\/)\.\.\.(?:\/|$)/u.test(token)) return false; const path = token.split('#')[0]; diff --git a/tools/skills/validate-agent-guidance.mjs b/tools/skills/validate-agent-guidance.mjs index 7345e885a..db88f64c8 100644 --- a/tools/skills/validate-agent-guidance.mjs +++ b/tools/skills/validate-agent-guidance.mjs @@ -167,9 +167,8 @@ export async function validateAgentGuidance({ rootDir }) { diagnostics.push( `${source}: at most ${maxBytes} UTF-8 bytes allowed (received ${bytes})` ); - const unfenced = guidanceProse(markdown); + const prose = guidanceProse(markdown); const imports = guidanceStandaloneImports(markdown); - const prose = unfenced.replace(/`[^`\n]+`/gu, ''); const inlineImports = []; for (const match of prose.matchAll( /(?:^|[^\p{L}\p{N}_@])@([^\s]+)/gu diff --git a/tools/skills/validate-agent-guidance.test.mjs b/tools/skills/validate-agent-guidance.test.mjs index 3632d1926..9476b057a 100644 --- a/tools/skills/validate-agent-guidance.test.mjs +++ b/tools/skills/validate-agent-guidance.test.mjs @@ -1031,3 +1031,32 @@ test('ordinary unknown handle is not a file import', async (t) => { [] ); }); + +for (const wrapper of ['\\`', '`']) { + test(`visible literal backticks preserve imports: ${wrapper}`, async (t) => { + const result = await diagnostics(t, { + 'CLAUDE.md': + '@AGENTS.md\n\nRead ' + wrapper + '@docs/example.md' + wrapper, + }); + assert.ok( + result.some((message) => message.includes('additional or inline')) + ); + }); +} +for (const filename of [ + 'Guide (old).md', + "Author's guide.md", + 'Guide [draft].md', +]) { + test(`explicit path punctuation is validated: ${filename}`, async (t) => { + const guidance = '`./docs/' + filename + '`'; + assert.deepEqual( + await diagnostics(t, { + 'AGENTS.md': guidance, + ['docs/' + filename]: '# Guide', + }), + [] + ); + assert.ok((await diagnostics(t, { 'AGENTS.md': guidance })).length > 0); + }); +}