diff --git a/docs/development/agent-workflow.md b/docs/development/agent-workflow.md
index ba00a0424..2353f1450 100644
--- a/docs/development/agent-workflow.md
+++ b/docs/development/agent-workflow.md
@@ -37,6 +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 literal paths may contain spaces; command-option snippets are excluded.
Multi-part dotfiles are path candidates too.
Conventional extensionless filenames such as Dockerfile, Makefile and LICENSE
are also path candidates; use an explicit `./` prefix for other extensionless files.
@@ -49,8 +50,9 @@ The parsed HTML tree also verifies that this paragraph is outside HTML container
including templates split across Markdown tokens. Generated HTML is inspected
in memory only; it is never executed or emitted.
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.
+for GitHub-compatible character filtering and duplicate suffixes.
+Only headings present outside inert HTML containers contribute slugs or duplicate counters.
+Explicit HTML anchors use `parse5`, excluding comments, scripts, styles and template contents.
Rendered HTML anchor hrefs and image sources use the same local-reference checks
as Markdown links, including decoded attributes and fragment validation.
Image references check file existence without interpreting image fragments as
diff --git a/tools/skills/agent-guidance-markdown.mjs b/tools/skills/agent-guidance-markdown.mjs
index 0768074d9..f6f3171b4 100644
--- a/tools/skills/agent-guidance-markdown.mjs
+++ b/tools/skills/agent-guidance-markdown.mjs
@@ -148,21 +148,37 @@ export function guidanceStandaloneImports(markdown) {
export function guidanceAnchors(markdown) {
const slugger = new GithubSlugger();
const found = new Set();
- const html = [];
- markdownLexer.walkTokens(markdownLexer.lexer(markdown), (token) => {
- if (token.type === 'heading') {
- found.add(slugger.slug(inlineText(token.tokens)));
- }
- if (token.type === 'html') html.push(token.raw);
+ const headings = [];
+ const marker = `data-guidance-${randomUUID()}`;
+ const renderer = new Marked({
+ renderer: {
+ heading(token) {
+ const index = headings.push(inlineText(token.tokens)) - 1;
+ return `${this.parser.parseInline(token.tokens)}\n`;
+ },
+ },
});
- return new Set([...found, ...htmlNavigation(html.join('\n')).anchors]);
+ const navigation = htmlNavigation(renderer.parse(markdown), (node) => {
+ const attribute = node.attrs?.find((attr) => attr.name === marker);
+ if (attribute)
+ found.add(slugger.slug(headings[Number(attribute.value)]));
+ });
+ return new Set([...found, ...navigation.anchors]);
}
function isLiteralRepositoryPath(token) {
// A typo in the directory or a new root filename must still be checked.
// Exclude recognizable prose/code forms instead of allowlisting paths.
if (/^(?:@|--|[a-z][a-z\d+.-]*:|\/\/)/iu.test(token)) return false;
- if (/[^\p{L}\p{N}_./#-]/u.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 (/\s+-{1,2}[\p{L}]/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.test.mjs b/tools/skills/validate-agent-guidance.test.mjs
index e7c9dde89..488219464 100644
--- a/tools/skills/validate-agent-guidance.test.mjs
+++ b/tools/skills/validate-agent-guidance.test.mjs
@@ -883,3 +883,41 @@ for (const content of [
);
});
}
+
+test('inert headings do not define or consume anchor slugs', async (t) => {
+ const body =
+ '\n\n# Hidden\n\n# Visible\n\n\n\n# Visible';
+ assert.match(
+ (
+ await diagnostics(t, {
+ 'AGENTS.md': '[Hidden](docs/example.md#hidden)',
+ 'docs/example.md': body,
+ })
+ ).join('\n'),
+ /missing anchor/
+ );
+ assert.deepEqual(
+ await diagnostics(t, {
+ 'AGENTS.md': '[Visible](docs/example.md#visible)',
+ 'docs/example.md': body,
+ }),
+ []
+ );
+});
+test('explicit relative literal paths support spaces', async (t) => {
+ assert.match(
+ (
+ await diagnostics(t, {
+ 'AGENTS.md': '`./docs/My Guide.md`',
+ })
+ ).join('\n'),
+ /does not exist/
+ );
+ assert.deepEqual(
+ await diagnostics(t, {
+ 'AGENTS.md': '`./docs/My Guide.md`',
+ 'docs/My Guide.md': '',
+ }),
+ []
+ );
+});