Skip to content

[Bug]: preset --ai-skills propagation ignores override description for core command taskstoissues #2528

@polmichel

Description

@polmichel

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 propagated SKILL.md description: 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

  1. Initialize a Claude project with skills enabled:

    specify init . --ai claude --ai-skills --force --ignore-agent-tools
  2. 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-taskstoissues
    description: SKILL-FILE-DESCRIPTION: this should appear if preset's SKILL.md is the source
    ---
    # Repro skill body
  3. 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-taskstoissues
description: Convert tasks from tasks.md into GitHub issues.
compatibility: Requires spec-kit project structure with .specify/ directory
metadata:
  author: github-spec-kit
  source: preset:repro
user-invocable: true
disable-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.

Root Cause

src/specify_cli/__init__.py:975 defines:

SKILL_DESCRIPTIONS = {
    "specify": "...",
    "plan": "...",
    "tasks": "...",
    "implement": "...",
    "analyze": "...",
    "clarify": "...",
    "constitution": "...",
    "checklist": "...",
    "taskstoissues": "Convert tasks from tasks.md into GitHub issues.",
}

Three call sites in src/specify_cli/presets.py apply it with inverted precedence — the dict is treated as an override rather than a fallback:

  • presets.py:1051 (project-override path, _register_skills for 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:

original_desc = frontmatter.get("description", "")
enhanced_desc = SKILL_DESCRIPTIONS.get(
    short_name,
    original_desc or f"Spec-kit workflow command: {short_name}",
)
frontmatter["description"] = enhanced_desc

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.

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