Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,34 @@ 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.

Command names may themselves end in an ``md`` segment: extension
commands are ``speckit.<extension>.<command>`` 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"):
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
def _hyphenate_frontmatter_refs(val: Any) -> Any:
"""Recursively find any dotted references starting with speckit. and hyphenate them."""
Expand All @@ -103,20 +131,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
Expand Down
56 changes: 56 additions & 0 deletions tests/integrations/test_integration_cline.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,62 @@ 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_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_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

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")
Expand Down