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
- Initialize a project for Claude with the skill layer enabled (
--ai-skills).
- 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.
- 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.
What happened
When a preset overrides core commands and the project uses the agent skill layer, the generated
SKILL.mdfiles 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:It should read
/speckit-specify,/speckit-plan,/speckit-tasks.Notably,
{ARGS}→$ARGUMENTSis resolved correctly in the same file, so only the command-ref tokens leak. The.composed/*.mdintermediate 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-kitsrc/specify_cli/presets.pyhas zero calls toresolve_command_refsin 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:PresetManager._register_skills()(src/specify_cli/presets.py, ~L1201) — the path that mirrors preset command overrides into the agent skill layer — callsregistrar.resolve_skill_placeholders(...)(which only handles{SCRIPT},{ARGS}→$ARGUMENTS,__AGENT__,__CONTEXT_FILE__, path rewriting) but never callsresolve_command_refs.Claude's
post_process_skill_content()(src/specify_cli/integrations/claude/__init__.py) only injectsuser-invocable/disable-model-invocationflags 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
--ai-skills).prepend/wrapstrategy (so a.composed/<cmd>.mdis produced) — e.g. a preset overridingspeckit.specify..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 afterresolve_skill_placeholders():agent_configis already in scope in_register_skills(), andresolve_command_refsalready maps the separator correctly (-→/speckit-specify,.→/speckit.specify). The patch applies cleanly tov0.8.14andpy_compiles.