From b5757e657f1594f4232afe7cceb48140647ae73b Mon Sep 17 00:00:00 2001 From: Manfred Riem <15701806+mnriem@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:35:18 -0500 Subject: [PATCH 1/4] fix: resolve command references per integration type (dot vs hyphen) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded /speckit. references in templates with __SPECKIT_COMMAND___ placeholders that are resolved at setup time based on the integration type: - Markdown/TOML/YAML agents: separator='.' → /speckit.plan - Skills agents: separator='-' → /speckit-plan Changes: - Add resolve_command_refs() static method to IntegrationBase - Add invoke_separator class attribute (. for base, - for skills) - Wire into process_template() as step 8 - Update _install_shared_infra() to process page templates - Replace /speckit.* in 5 command templates and 3 page templates - Add unit tests for resolve_command_refs (positive + negative) - Add integration tests verifying on-disk content for all agents - Add end-to-end CLI tests for Claude (skills) and Copilot (markdown) Fixes #2347 --- src/specify_cli/__init__.py | 20 +++- src/specify_cli/integrations/base.py | 29 +++++ templates/checklist-template.md | 4 +- templates/commands/analyze.md | 8 +- templates/commands/checklist.md | 2 +- templates/commands/clarify.md | 8 +- templates/commands/implement.md | 2 +- templates/commands/specify.md | 10 +- templates/plan-template.md | 14 +-- templates/tasks-template.md | 2 +- tests/integrations/test_base.py | 80 ++++++++++++++ tests/integrations/test_cli.py | 101 ++++++++++++++++++ .../test_integration_base_markdown.py | 1 + .../test_integration_base_skills.py | 16 +++ .../test_integration_base_toml.py | 1 + .../test_integration_base_yaml.py | 1 + tests/integrations/test_integration_claude.py | 2 + .../integrations/test_integration_copilot.py | 1 + tests/integrations/test_integration_forge.py | 1 + .../integrations/test_integration_generic.py | 1 + 20 files changed, 274 insertions(+), 30 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 77611128b5..8d593abefb 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -723,6 +723,7 @@ def _install_shared_infra( script_type: str, tracker: StepTracker | None = None, force: bool = False, + invoke_separator: str = ".", ) -> bool: """Install shared infrastructure files into *project_path*. @@ -730,12 +731,17 @@ def _install_shared_infra( bundled core_pack or source checkout. Tracks all installed files in ``speckit.manifest.json``. + Page templates are processed to resolve ``__SPECKIT_COMMAND___`` + placeholders using *invoke_separator* (``"."`` for markdown agents, + ``"-"`` for skills agents). + When *force* is ``True``, existing files are overwritten with the latest bundled versions. When ``False`` (default), only missing files are added and existing ones are skipped. Returns ``True`` on success. """ + from .integrations.base import IntegrationBase from .integrations.manifest import IntegrationManifest core = _locate_core_pack() @@ -786,7 +792,11 @@ def _install_shared_infra( if dst.exists() and not force: skipped_files.append(str(dst.relative_to(project_path))) else: - shutil.copy2(f, dst) + content = f.read_text(encoding="utf-8") + content = IntegrationBase.resolve_command_refs( + content, invoke_separator + ) + dst.write_text(content, encoding="utf-8") rel = dst.relative_to(project_path).as_posix() manifest.record_existing(rel) @@ -1295,7 +1305,7 @@ def init( # Install shared infrastructure (scripts, templates) tracker.start("shared-infra") - _install_shared_infra(project_path, selected_script, tracker=tracker, force=force) + _install_shared_infra(project_path, selected_script, tracker=tracker, force=force, invoke_separator=resolved_integration.invoke_separator) tracker.complete("shared-infra", f"scripts ({selected_script}) + templates") ensure_constitution_from_template(project_path, tracker=tracker) @@ -2074,7 +2084,7 @@ def integration_install( # Ensure shared infrastructure is present (safe to run unconditionally; # _install_shared_infra merges missing files without overwriting). - _install_shared_infra(project_root, selected_script) + _install_shared_infra(project_root, selected_script, invoke_separator=integration.invoke_separator) if os.name != "nt": ensure_executable_scripts(project_root) @@ -2358,7 +2368,7 @@ def integration_switch( # Ensure shared infrastructure is present (safe to run unconditionally; # _install_shared_infra merges missing files without overwriting). - _install_shared_infra(project_root, selected_script) + _install_shared_infra(project_root, selected_script, invoke_separator=target_integration.invoke_separator) if os.name != "nt": ensure_executable_scripts(project_root) @@ -2466,7 +2476,7 @@ def integration_upgrade( selected_script = _resolve_script_type(project_root, script) # Ensure shared infrastructure is up to date; --force overwrites existing files. - _install_shared_infra(project_root, selected_script, force=force) + _install_shared_infra(project_root, selected_script, force=force, invoke_separator=integration.invoke_separator) if os.name != "nt": ensure_executable_scripts(project_root) diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index a3d8a42aa2..16deb9647a 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -84,6 +84,9 @@ class IntegrationBase(ABC): context_file: str | None = None """Relative path to the agent context file (e.g. ``CLAUDE.md``).""" + invoke_separator: str = "." + """Separator used in slash-command invocations (``"."`` → ``/speckit.plan``).""" + # -- Markers for managed context section ------------------------------ CONTEXT_MARKER_START = "" @@ -597,6 +600,24 @@ def remove_context_section(self, project_root: Path) -> bool: return True + @staticmethod + def resolve_command_refs(content: str, separator: str = ".") -> str: + """Replace ``__SPECKIT_COMMAND___`` placeholders with invocations. + + Each placeholder encodes a command name in upper-case with + underscores (e.g. ``__SPECKIT_COMMAND_PLAN__``, + ``__SPECKIT_COMMAND_GIT_COMMIT__``). The replacement uses + *separator* to join the segments: + + * ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit`` + * ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit`` + """ + return re.sub( + r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__", + lambda m: "/speckit" + separator + m.group(1).lower().replace("_", separator), + content, + ) + @staticmethod def process_template( content: str, @@ -604,6 +625,7 @@ def process_template( script_type: str, arg_placeholder: str = "$ARGUMENTS", context_file: str = "", + invoke_separator: str = ".", ) -> str: """Process a raw command template into agent-ready content. @@ -615,6 +637,7 @@ def process_template( 5. Replace ``__AGENT__`` with *agent_name* 6. Replace ``__CONTEXT_FILE__`` with *context_file* 7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc. + 8. Replace ``__SPECKIT_COMMAND___`` with invocation strings """ # 1. Extract script command from frontmatter script_command = "" @@ -684,6 +707,9 @@ def process_template( content = CommandRegistrar.rewrite_project_relative_paths(content) + # 8. Replace __SPECKIT_COMMAND___ with invocation strings + content = IntegrationBase.resolve_command_refs(content, invoke_separator) + return content def setup( @@ -1274,6 +1300,8 @@ class SkillsIntegration(IntegrationBase): ``speckit-/SKILL.md`` file with skills-oriented frontmatter. """ + invoke_separator = "-" + def build_exec_args( self, prompt: str, @@ -1395,6 +1423,7 @@ def setup( processed_body = self.process_template( raw, self.key, script_type, arg_placeholder, context_file=self.context_file or "", + invoke_separator=self.invoke_separator, ) # Strip the processed frontmatter — we rebuild it for skills. # Preserve leading whitespace in the body to match release ZIP diff --git a/templates/checklist-template.md b/templates/checklist-template.md index 806657da09..9752c130ec 100644 --- a/templates/checklist-template.md +++ b/templates/checklist-template.md @@ -4,13 +4,13 @@ **Created**: [DATE] **Feature**: [Link to spec.md or relevant documentation] -**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. +**Note**: This checklist is generated by the `__SPECKIT_COMMAND_CHECKLIST__` command based on feature context and requirements.