From 7473aa1ad77052f452adbff17d721bd6fe9bf339 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:38:40 +0900 Subject: [PATCH 1/3] fix(cline): keep .md extension on speckit.* file names in command bodies Cline's dot-to-hyphen rewrite matched `speckit.git.commit.md` as a command reference and produced `speckit-git-commit-md`, dropping the extension. Leave matches that end in `.md` untouched in both body and frontmatter. Fixes #5097 Assisted-by: Grok Bot (model: unknown, autonomous) Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com> --- src/specify_cli/agents.py | 24 +++++++++++++------- tests/integrations/test_integration_cline.py | 22 ++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 72966998a5..31981efbc0 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -92,6 +92,18 @@ def _ensure_configs(cls) -> None: except ImportError: pass # Circular import during module init; retry on next access + _SPECKIT_DOTTED_REF = re.compile(r"\bspeckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*\b") + + @staticmethod + def _hyphenate_ref_match(match: "re.Match[str]") -> str: + """Hyphenate one dotted speckit reference, leaving ``.md`` file names alone.""" + ref = match.group(0) + if ref.endswith(".md"): + # ``speckit.git.commit.md`` is a command file name, not a command + # reference; rewriting it would drop the extension. + return ref + return ref.replace(".", "-") + @staticmethod def _hyphenate_frontmatter_refs(val: Any) -> Any: """Recursively find any dotted references starting with speckit. and hyphenate them.""" @@ -103,20 +115,16 @@ def _hyphenate_frontmatter_refs(val: Any) -> Any: elif isinstance(val, list): return [CommandRegistrar._hyphenate_frontmatter_refs(x) for x in val] elif isinstance(val, str): - return re.sub( - r"\bspeckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*\b", - lambda m: m.group(0).replace(".", "-"), - val, + return CommandRegistrar._SPECKIT_DOTTED_REF.sub( + CommandRegistrar._hyphenate_ref_match, val ) return val @staticmethod def _hyphenate_body_refs(body: str) -> str: """Hyphenate dotted speckit references in command body text.""" - return re.sub( - r"\bspeckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*\b", - lambda m: m.group(0).replace(".", "-"), - body, + return CommandRegistrar._SPECKIT_DOTTED_REF.sub( + CommandRegistrar._hyphenate_ref_match, body ) @staticmethod diff --git a/tests/integrations/test_integration_cline.py b/tests/integrations/test_integration_cline.py index d7350a9633..81aceec87a 100644 --- a/tests/integrations/test_integration_cline.py +++ b/tests/integrations/test_integration_cline.py @@ -82,6 +82,28 @@ def test_cline_handoff_rewrite(self): rewritten = cline._rewrite_handoff_references(content) assert rewritten == "---\nagent: speckit-plan\n---\n" + def test_cline_body_refs_keep_md_file_names(self): + """Command references are hyphenated but ``speckit.*.md`` file names keep ``.md``.""" + from specify_cli.agents import CommandRegistrar + + body = ( + "Edit `.specify/extensions/git/commands/speckit.git.commit.md`, " + "then run /speckit.git.commit." + ) + assert CommandRegistrar._hyphenate_body_refs(body) == ( + "Edit `.specify/extensions/git/commands/speckit.git.commit.md`, " + "then run /speckit-git-commit." + ) + + def test_cline_frontmatter_refs_keep_md_file_names(self): + """Frontmatter string values get the same ``.md`` file name protection.""" + from specify_cli.agents import CommandRegistrar + + refs = CommandRegistrar._hyphenate_frontmatter_refs( + {"agent": "speckit.plan", "file": "commands/speckit.git.commit.md"} + ) + assert refs == {"agent": "speckit-plan", "file": "commands/speckit.git.commit.md"} + def test_cline_hook_instruction_injection(self): """Verify Cline injects the dot-to-hyphen note for hooks.""" cline = get_integration("cline") From c901e965742b97720088ea6eaf11034aeea0f7c4 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:20:55 +0900 Subject: [PATCH 2/3] fix(cline): still hyphenate a command named md outside a path A speckit..md token is a command reference unless it has more segments than a command can have or sits inside a path. Assisted-by: Grok Bot (model: unknown, autonomous) Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com> --- src/specify_cli/agents.py | 24 ++++++++++++++++---- tests/integrations/test_integration_cline.py | 17 ++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index 31981efbc0..f75fc9c207 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -96,12 +96,28 @@ def _ensure_configs(cls) -> None: @staticmethod def _hyphenate_ref_match(match: "re.Match[str]") -> str: - """Hyphenate one dotted speckit reference, leaving ``.md`` file names alone.""" + """Hyphenate one dotted speckit reference, leaving ``.md`` file names alone. + + ``speckit..`` may legitimately end in ``.md`` when the + command itself is named ``md`` (``/speckit.foo.md``), so the suffix alone + is not enough. A ``.md`` token is treated as a file name only when it has + more segments than any command reference can have + (``speckit.git.commit.md``) or sits inside a path + (``commands/speckit.plan.md``). + """ ref = match.group(0) if ref.endswith(".md"): - # ``speckit.git.commit.md`` is a command file name, not a command - # reference; rewriting it would drop the extension. - return ref + if ref.count(".") > 2: + return ref + start = match.start() + text = match.string + if ( + start >= 2 + and text[start - 1] in "/\\" + and not text[start - 2].isspace() + and text[start - 2] not in "`'\"(" + ): + return ref return ref.replace(".", "-") @staticmethod diff --git a/tests/integrations/test_integration_cline.py b/tests/integrations/test_integration_cline.py index 81aceec87a..850b8026c6 100644 --- a/tests/integrations/test_integration_cline.py +++ b/tests/integrations/test_integration_cline.py @@ -95,6 +95,23 @@ def test_cline_body_refs_keep_md_file_names(self): "then run /speckit-git-commit." ) + def test_cline_body_refs_hyphenate_a_command_named_md(self): + """An extension command whose name is ``md`` is still a command reference.""" + from specify_cli.agents import CommandRegistrar + + body = ( + "Run /speckit.foo.md, or open commands/speckit.plan.md " + "and agents/speckit.foo.md." + ) + assert CommandRegistrar._hyphenate_body_refs(body) == ( + "Run /speckit-foo-md, or open commands/speckit.plan.md " + "and agents/speckit.foo.md." + ) + refs = CommandRegistrar._hyphenate_frontmatter_refs( + {"agent": "speckit.foo.md", "next": "/speckit.foo.md"} + ) + assert refs == {"agent": "speckit-foo-md", "next": "/speckit-foo-md"} + def test_cline_frontmatter_refs_keep_md_file_names(self): """Frontmatter string values get the same ``.md`` file name protection.""" from specify_cli.agents import CommandRegistrar From a5afd73b516ee0282b320542c48f6c1125490166 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Fri, 30 Oct 2026 06:20:43 +0900 Subject: [PATCH 3/3] fix(cline): treat bare speckit.*.md tokens as command references Preset command names may contain dots, so speckit.foo.bar.md can be a valid command whose last segment is md. The segment count therefore cannot identify a file name. Keep a .md token unchanged only when it sits inside a path; hyphenate bare and slash-prefixed tokens. Assisted-by: Grok Bot (model: unknown, autonomous) Signed-off-by: kevin9327 <5299031+kevin9327@users.noreply.github.com> --- src/specify_cli/agents.py | 16 ++++++++-------- tests/integrations/test_integration_cline.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/specify_cli/agents.py b/src/specify_cli/agents.py index f75fc9c207..ec1e9d471f 100644 --- a/src/specify_cli/agents.py +++ b/src/specify_cli/agents.py @@ -98,17 +98,17 @@ def _ensure_configs(cls) -> None: def _hyphenate_ref_match(match: "re.Match[str]") -> str: """Hyphenate one dotted speckit reference, leaving ``.md`` file names alone. - ``speckit..`` may legitimately end in ``.md`` when the - command itself is named ``md`` (``/speckit.foo.md``), so the suffix alone - is not enough. A ``.md`` token is treated as a file name only when it has - more segments than any command reference can have - (``speckit.git.commit.md``) or sits inside a path - (``commands/speckit.plan.md``). + Command names may themselves end in an ``md`` segment: extension + commands are ``speckit..`` and preset command names + may contain further dots, so ``/speckit.foo.md`` and + ``speckit.foo.bar.md`` can both be valid command references. The + ``.md`` suffix and the segment count therefore cannot tell a file name + apart from a command. A ``.md`` token is kept as a file name only when + it sits inside a path (``commands/speckit.git.commit.md``); a bare or + slash-prefixed token is treated as a command reference. """ ref = match.group(0) if ref.endswith(".md"): - if ref.count(".") > 2: - return ref start = match.start() text = match.string if ( diff --git a/tests/integrations/test_integration_cline.py b/tests/integrations/test_integration_cline.py index 850b8026c6..556783b59a 100644 --- a/tests/integrations/test_integration_cline.py +++ b/tests/integrations/test_integration_cline.py @@ -112,6 +112,23 @@ def test_cline_body_refs_hyphenate_a_command_named_md(self): ) assert refs == {"agent": "speckit-foo-md", "next": "/speckit-foo-md"} + def test_cline_refs_hyphenate_dotted_preset_command_ending_in_md(self): + """A dotted preset command name ending in ``md`` is a command, not a file.""" + from specify_cli.agents import CommandRegistrar + + body = ( + "Run speckit.foo.bar.md or /speckit.foo.bar.md, " + "then edit presets/foo/commands/speckit.foo.bar.md." + ) + assert CommandRegistrar._hyphenate_body_refs(body) == ( + "Run speckit-foo-bar-md or /speckit-foo-bar-md, " + "then edit presets/foo/commands/speckit.foo.bar.md." + ) + refs = CommandRegistrar._hyphenate_frontmatter_refs( + {"agent": "speckit.foo.bar.md", "file": "commands/speckit.foo.bar.md"} + ) + assert refs == {"agent": "speckit-foo-bar-md", "file": "commands/speckit.foo.bar.md"} + def test_cline_frontmatter_refs_keep_md_file_names(self): """Frontmatter string values get the same ``.md`` file name protection.""" from specify_cli.agents import CommandRegistrar