From f3a060fd3c3666bcb9300d577342dd173e25add8 Mon Sep 17 00:00:00 2001 From: Huzaifa Iftikhar Date: Fri, 28 Aug 2026 01:45:26 +0500 Subject: [PATCH] allow hyphen in command ref token names the token pattern was matching only A-Z 0-9 and underscore so a command name like speckit.agent-context.update can not be written as a token and the token stays in the output as plain text now the character class allows a hyphen also in both places that resolve the token --- src/specify_cli/extensions/__init__.py | 2 +- src/specify_cli/integrations/base.py | 6 +++- tests/integrations/test_base.py | 14 ++++++++ tests/test_extension_skills.py | 46 ++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/extensions/__init__.py b/src/specify_cli/extensions/__init__.py index 3968e4fcbe..a440b6da9b 100644 --- a/src/specify_cli/extensions/__init__.py +++ b/src/specify_cli/extensions/__init__.py @@ -1596,7 +1596,7 @@ def _replacement(match: re.Match[str]) -> str: ) return re.sub( - r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__", _replacement, body + r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_-]*)__", _replacement, body ) for cmd_info in manifest.commands: diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 27c43582b0..e58d231d36 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -636,11 +636,15 @@ def resolve_command_refs( * ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit`` * ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit`` + A hyphen belongs to the segment it sits in rather than separating + segments, so ``__SPECKIT_COMMAND_AGENT-CONTEXT_UPDATE__`` resolves to + ``/speckit.agent-context.update``. + *prefix* defaults to ``"/"`` but may be ``"$"`` for agents whose native skills invocation uses dollar-prefixed chat commands. """ return re.sub( - r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__", + r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_-]*)__", lambda m: prefix + "speckit" + separator diff --git a/tests/integrations/test_base.py b/tests/integrations/test_base.py index 5f99961804..713cf75523 100644 --- a/tests/integrations/test_base.py +++ b/tests/integrations/test_base.py @@ -353,6 +353,16 @@ def test_extension_command_hyphen(self): result = IntegrationBase.resolve_command_refs(text, "-") assert result == "Run /speckit-git-commit to commit." + def test_hyphenated_command_dot(self): + text = "Run __SPECKIT_COMMAND_AGENT-CONTEXT_UPDATE__ to refresh." + result = IntegrationBase.resolve_command_refs(text, ".") + assert result == "Run /speckit.agent-context.update to refresh." + + def test_hyphenated_command_hyphen(self): + text = "Run __SPECKIT_COMMAND_AGENT-CONTEXT_UPDATE__ to refresh." + result = IntegrationBase.resolve_command_refs(text, "-") + assert result == "Run /speckit-agent-context-update to refresh." + def test_no_placeholders_unchanged(self): text = "No placeholders here." assert IntegrationBase.resolve_command_refs(text, ".") == text @@ -400,6 +410,10 @@ def test_lowercase_placeholder_not_matched(self): text = "Run __SPECKIT_COMMAND_plan__ to plan." assert IntegrationBase.resolve_command_refs(text, ".") == text + def test_leading_hyphen_not_matched(self): + text = "Run __SPECKIT_COMMAND_-PLAN__ to plan." + assert IntegrationBase.resolve_command_refs(text, ".") == text + def test_placeholder_adjacent_to_text(self): text = "foo__SPECKIT_COMMAND_PLAN__bar" result = IntegrationBase.resolve_command_refs(text, ".") diff --git a/tests/test_extension_skills.py b/tests/test_extension_skills.py index 6eec5e7b47..a0a32f4d29 100644 --- a/tests/test_extension_skills.py +++ b/tests/test_extension_skills.py @@ -1166,6 +1166,52 @@ def test_skill_registration_resolves_command_ref_tokens( assert "__SPECKIT_COMMAND_PLAN__" not in content assert expected_invocation in content + def test_skill_registration_resolves_hyphenated_command_ref_tokens( + self, project_dir, temp_dir + ): + """Command names containing a hyphen resolve like any other name.""" + _create_init_options(project_dir, ai="claude", ai_skills=True) + skills_dir = _create_skills_dir(project_dir, ai="claude") + + ext_dir = temp_dir / "hyphen-ref-ext" + ext_dir.mkdir() + manifest_data = { + "schema_version": "1.0", + "extension": { + "id": "hyphen-ref-ext", + "name": "Hyphen Ref Extension", + "version": "1.0.0", + "description": "Test", + }, + "requires": {"speckit_version": ">=0.1.0"}, + "provides": { + "commands": [ + { + "name": "speckit.hyphen-ref-ext.run", + "file": "commands/run.md", + "description": "Run command", + } + ] + }, + } + with open(ext_dir / "extension.yml", "w") as f: + yaml.safe_dump(manifest_data, f) + + (ext_dir / "commands").mkdir() + (ext_dir / "commands" / "run.md").write_text( + "---\n" + "description: Run command\n" + "---\n\n" + "Use __SPECKIT_COMMAND_AGENT-CONTEXT_UPDATE__ before proceeding.\n" + ) + + manager = ExtensionManager(project_dir) + manager.install_from_directory(ext_dir, "0.1.0", register_commands=False) + + content = (skills_dir / "speckit-hyphen-ref-ext-run" / "SKILL.md").read_text() + assert "__SPECKIT_COMMAND_AGENT-CONTEXT_UPDATE__" not in content + assert "/speckit-agent-context-update" in content + def test_skill_registration_does_not_rewrite_literal_speckit_text( self, project_dir, temp_dir ):