You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If this is an intended behavior to freeze and lock the core commands description, please close this bug.
When a preset uses replaces: on one of the nine core speckit commands (taskstoissues), the propagated SKILL.mddescription: field is silently overwritten.
metadata.source: preset:<id> is set correctly, so propagation knows the preset is the winner; only description (and metadata.author) leak from the core.
Per docs/reference/presets.md, presets are explicitly the layer for "overriding templates, commands, and terminology", and the resolution stack places presets above extensions and core. The scaffold preset.yml even shows replaces: "speckit.specify" as a canonical example. So the override path here is the intended one — it just doesn't apply to the skill description field.
Steps to Reproduce
Initialize a Claude project with skills enabled:
specify init . --ai claude --ai-skills --force --ignore-agent-tools
Build a minimal preset overriding speckit.taskstoissues:
my-preset/preset.yml
schema_version: "1.0"preset:
id: "repro"name: "Repro Preset"version: "0.1.0"description: "Minimal repro"author: "repro"license: "MIT"requires:
speckit_version: ">=0.1.0"provides:
templates:
- type: "command"name: "speckit.taskstoissues"file: "commands/speckit.repro.taskstoissues.md"description: "MANIFEST-DESCRIPTION: this should appear if manifest is the source"replaces: "speckit.taskstoissues"strategy: "replace"tags: ["repro"]
my-preset/commands/speckit.repro.taskstoissues.md
---description: COMMAND-FRONTMATTER-DESCRIPTION: this should appear if command frontmatter is the source---# Repro command body
my-preset/skills/speckit-taskstoissues/SKILL.md
---name: speckit-taskstoissuesdescription: SKILL-FILE-DESCRIPTION: this should appear if preset's SKILL.md is the source---# Repro skill body
Install and inspect:
specify preset add --dev ./my-preset
head -10 .claude/skills/speckit-taskstoissues/SKILL.md
The result on my side is description: Convert tasks from tasks.md into GitHub issues.
Expected Behavior
description: should reflect one of the preset-supplied overrides — most naturally the command's frontmatter. This is consistent with the documented "presets override core" semantics.
Actual Behavior
---
name: speckit-taskstoissuesdescription: Convert tasks from tasks.md into GitHub issues.compatibility: Requires spec-kit project structure with .specify/ directorymetadata:
author: github-spec-kitsource: preset:reprouser-invocable: truedisable-model-invocation: false
---
description: is the hardcoded string from SKILL_DESCRIPTIONS["taskstoissues"] — none of MANIFEST-DESCRIPTION, COMMAND-FRONTMATTER-DESCRIPTION, or SKILL-FILE-DESCRIPTION appear anywhere.
Note also metadata.author: github-spec-kit — same shape of bug.
Specify CLI Version
0.8.8
AI Agent
Claude Code
Operating System
macOS 26.4.1 (Darwin 25.4.0)
Python Version
Python 3.14.4
Error Logs
No error or crash — the bug is silent. The propagated `SKILL.md` is written with the wrong `description:` field and no warning is emitted.
Additional Context
This section will include an AI-generated analysis of the specify-cli code, please ignore this section if you are not interested in this information.
Because dict.get(key, default) returns the dict value when the key is present, original_desc is unreachable for any of the nine hardcoded core short-names — including any preset that uses replaces: to override them.
Preset-shipped SKILL.md is silently ignored
The preset's own skills/<id>/SKILL.md is never read at all. grep confirms presets.py only references skills_dir as the destination (.claude/skills, etc.), never the source preset's skills/ subdirectory. Propagation always synthesizes SKILL.md from the command file's frontmatter + body via agents.py:build_skill_frontmatter(). It's worth clarifying whether shipping skills/<id>/SKILL.md in a preset is intended to be supported — the scaffold at presets/scaffold/preset.yml documents only type: template | command | script, so the current "silently ignored" behavior may be by design, but accepting the directory without a validation warning is surprising. Neither docs/reference/presets.md nor docs/reference/extensions.md mentions skill propagation at all, so the contract is currently implementation-defined.
What I've Verified
specify preset info repro correctly displays the manifest description (so the manifest is parsed).
metadata.source: preset:repro is set correctly on the propagated SKILL.md.
user-invocable: true and disable-model-invocation: false are set correctly.
Reproduces on a clean specify init — not interaction with prior state.
Extension-supplied descriptions DO propagate correctly (verified with the bundled git extension: speckit.git.feature's frontmatter description lands verbatim in .claude/skills/speckit-git-feature/SKILL.md). This is because non-core short-names aren't in SKILL_DESCRIPTIONS, so the fallback wins. So the bug is specific to overriding core commands via presets.
No existing issue covers this (searched: ai-skills description, SKILL_DESCRIPTIONS, skill description hardcoded, preset replaces skill, preset skill frontmatter, etc.). Closest tangential match is the closed tech-debt issue Tech Debt: Consolidate skill rendering/parsing paths and harden TOML safety #1976, which notes duplication across _register_skills / _unregister_skills but doesn't call out the precedence inversion.
Bug Description
If this is an intended behavior to freeze and lock the core commands description, please close this bug.
When a preset uses
replaces:on one of the nine core speckit commands (taskstoissues), the propagatedSKILL.mddescription:field is silently overwritten.metadata.source: preset:<id>is set correctly, so propagation knows the preset is the winner; onlydescription(andmetadata.author) leak from the core.Per
docs/reference/presets.md, presets are explicitly the layer for "overriding templates, commands, and terminology", and the resolution stack places presets above extensions and core. The scaffoldpreset.ymleven showsreplaces: "speckit.specify"as a canonical example. So the override path here is the intended one — it just doesn't apply to the skilldescriptionfield.Steps to Reproduce
Initialize a Claude project with skills enabled:
specify init . --ai claude --ai-skills --force --ignore-agent-toolsBuild a minimal preset overriding
speckit.taskstoissues:my-preset/preset.ymlmy-preset/commands/speckit.repro.taskstoissues.mdmy-preset/skills/speckit-taskstoissues/SKILL.mdInstall and inspect:
The result on my side is
description: Convert tasks from tasks.md into GitHub issues.Expected Behavior
description:should reflect one of the preset-supplied overrides — most naturally the command's frontmatter. This is consistent with the documented "presets override core" semantics.Actual Behavior
description:is the hardcoded string fromSKILL_DESCRIPTIONS["taskstoissues"]— none ofMANIFEST-DESCRIPTION,COMMAND-FRONTMATTER-DESCRIPTION, orSKILL-FILE-DESCRIPTIONappear anywhere.Note also
metadata.author: github-spec-kit— same shape of bug.Specify CLI Version
0.8.8
AI Agent
Claude Code
Operating System
macOS 26.4.1 (Darwin 25.4.0)
Python Version
Python 3.14.4
Error Logs
Additional Context
This section will include an AI-generated analysis of the specify-cli code, please ignore this section if you are not interested in this information.
Root Cause
src/specify_cli/__init__.py:975defines:Three call sites in
src/specify_cli/presets.pyapply it with inverted precedence — the dict is treated as an override rather than a fallback:presets.py:1051(project-override path,_register_skillsfor non-preset winners)presets.py:1317(preset-registration path,_register_skills)presets.py:1420(post-uninstall restore path,_unregister_skills)All three follow the same pattern, e.g. at line 1316–1322:
Because
dict.get(key, default)returns the dict value when the key is present,original_descis unreachable for any of the nine hardcoded core short-names — including any preset that usesreplaces:to override them.Preset-shipped SKILL.md is silently ignored
The preset's own
skills/<id>/SKILL.mdis never read at all.grepconfirmspresets.pyonly referencesskills_diras the destination (.claude/skills, etc.), never the source preset'sskills/subdirectory. Propagation always synthesizesSKILL.mdfrom the command file's frontmatter + body viaagents.py:build_skill_frontmatter(). It's worth clarifying whether shippingskills/<id>/SKILL.mdin a preset is intended to be supported — the scaffold atpresets/scaffold/preset.ymldocuments onlytype: template | command | script, so the current "silently ignored" behavior may be by design, but accepting the directory without a validation warning is surprising. Neitherdocs/reference/presets.mdnordocs/reference/extensions.mdmentions skill propagation at all, so the contract is currently implementation-defined.What I've Verified
specify preset info reprocorrectly displays the manifest description (so the manifest is parsed).metadata.source: preset:reprois set correctly on the propagated SKILL.md.user-invocable: trueanddisable-model-invocation: falseare set correctly.specify init— not interaction with prior state.gitextension:speckit.git.feature's frontmatter description lands verbatim in.claude/skills/speckit-git-feature/SKILL.md). This is because non-core short-names aren't inSKILL_DESCRIPTIONS, so the fallback wins. So the bug is specific to overriding core commands via presets.ai-skills description,SKILL_DESCRIPTIONS,skill description hardcoded,preset replaces skill,preset skill frontmatter, etc.). Closest tangential match is the closed tech-debt issue Tech Debt: Consolidate skill rendering/parsing paths and harden TOML safety #1976, which notes duplication across_register_skills/_unregister_skillsbut doesn't call out the precedence inversion.