Skip to content

[Bug]: Preset _register_skills() leaks raw __SPECKIT_COMMAND_*__ placeholders into SKILL.md #2717

@huy1010

Description

@huy1010

What happened

When a preset overrides core commands and the project uses the agent skill layer, the generated SKILL.md files for command-backed agents (e.g. Claude → .claude/skills/) contain raw, unresolved __SPECKIT_COMMAND_*__ placeholders instead of the rendered slash-command invocation.

Example, in a generated .claude/skills/speckit-specify/SKILL.md:

The text the user typed after `__SPECKIT_COMMAND_SPECIFY__` in the triggering message **is** the feature description.
...
This allows downstream commands (`__SPECKIT_COMMAND_PLAN__`, `__SPECKIT_COMMAND_TASKS__`, etc.) to locate the feature directory...

It should read /speckit-specify, /speckit-plan, /speckit-tasks.

Notably, {ARGS}$ARGUMENTS is resolved correctly in the same file, so only the command-ref tokens leak. The .composed/*.md intermediate files correctly retain the raw tokens (resolution is per-agent), so the bug is purely in the skill-rendering step.

Affected versions

Reproduced on 0.8.11 and confirmed unchanged through 0.8.14 (latest). git/spec-kit src/specify_cli/presets.py has zero calls to resolve_command_refs in all of 0.8.12 / 0.8.13 / 0.8.14.

Root cause

There are two skill-rendering paths and they diverge:

  • CommandRegistrar.register_commands() (src/specify_cli/agents.py, ~L520) does resolve command refs:
    _sep = agent_config.get("invoke_separator", ".")
    body = IntegrationBase.resolve_command_refs(body, _sep)
  • PresetManager._register_skills() (src/specify_cli/presets.py, ~L1201) — the path that mirrors preset command overrides into the agent skill layer — calls registrar.resolve_skill_placeholders(...) (which only handles {SCRIPT}, {ARGS}$ARGUMENTS, __AGENT__, __CONTEXT_FILE__, path rewriting) but never calls resolve_command_refs.

Claude's post_process_skill_content() (src/specify_cli/integrations/claude/__init__.py) only injects user-invocable / disable-model-invocation flags and the hook-command note — it does not resolve the tokens either.

This is a concrete instance of the path divergence flagged in #1976 ("Consolidate skill rendering/parsing paths").

Steps to reproduce

  1. Initialize a project for Claude with the skill layer enabled (--ai-skills).
  2. Install a preset that overrides a core command with a prepend/wrap strategy (so a .composed/<cmd>.md is produced) — e.g. a preset overriding speckit.specify.
  3. Open the generated .claude/skills/speckit-specify/SKILL.md.

Expected: command references render as /speckit-specify, /speckit-plan, etc.
Actual: they appear as the raw __SPECKIT_COMMAND_SPECIFY__, __SPECKIT_COMMAND_PLAN__ tokens.

Proposed fix

Mirror the register_commands() behavior in _register_skills() — resolve command refs with the agent's invoke separator right after resolve_skill_placeholders():

--- a/src/specify_cli/presets.py
+++ b/src/specify_cli/presets.py
@@ -1322,6 +1322,14 @@
             frontmatter["description"] = enhanced_desc
             body = registrar.resolve_skill_placeholders(
                 selected_ai, frontmatter, body, self.project_root
+            )
+            # Resolve __SPECKIT_COMMAND_*__ tokens using the agent's invoke
+            # separator, mirroring CommandRegistrar.register_commands() so the
+            # mirrored skill layer renders /speckit-<cmd> (or /speckit.<cmd>)
+            # instead of leaking the raw placeholder into SKILL.md.
+            from specify_cli.integrations.base import IntegrationBase
+            body = IntegrationBase.resolve_command_refs(
+                body, agent_config.get("invoke_separator", ".")
             )
 
             for target_skill_name in target_skill_names:

agent_config is already in scope in _register_skills(), and resolve_command_refs already maps the separator correctly (-/speckit-specify, ./speckit.specify). The patch applies cleanly to v0.8.14 and py_compiles.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions