Bug Description
specify preset add correctly registers type: "command" entries as SKILL.md files, but when a preset applies a composition strategy (prepend, append, or wrap) on top of another preset's command via a replace layer, the writer materializes only the effective replace base. The composing layers above the base are silently dropped from the on-disk artifact.
Composition semantics work correctly for type: "template" entries (bash/PowerShell resolvers apply prepend/append/wrap per scripts/*/common.sh|ps1, and presets/README.md documents that commands support all four strategies too). But the command write path in PresetManager / CommandRegistrar appears to skip the composition step for type: "command" entries.
Setup: Copilot in skills mode, two presets installed:
copilot-sub-agents — priority 100, strategy: "prepend", active
lean — priority 10, strategy: "replace" (effective base)
- core (canonical fallback)
Both presets declare speckit.plan (and 3 other overlapping commands) as type: "command" with matching name. Expected: .github/skills/speckit-plan/SKILL.md = copilot-sub-agents prepend content + blank line + lean body. Actual: lean body only, frontmatter source: preset:lean, no trace of the prepend content.
Steps to Reproduce
specify init myproj --integration copilot --integration-options="--skills" --script ps
- Install two presets that overlap on
speckit.plan (and any other commands), where one uses strategy: "replace" and one uses strategy: "prepend":
specify preset add lean --priority 10
specify preset add copilot-sub-agents --priority 100
- Verify both are enabled and priorities set correctly:
- Inspect the generated SKILL.md:
cat .github/skills/speckit-plan/SKILL.md
Expected Behavior
.github/skills/speckit-plan/SKILL.md should contain the composed body:
[copilot-sub-agents prepend content]
[lean replace content]
Frontmatter should reflect the composition chain (or at minimum, not silently claim lean is the sole source when a prepend layer above it is active).
This matches the documented behavior in presets/README.md:
"prepend — Places content before the resolved lower-priority template, separated by a blank line"
And the strategy support table in presets/README.md explicitly lists all four strategies as supported for type: "command".
Actual Behavior
.github/skills/speckit-plan/SKILL.md contains only the Lean body with frontmatter source: preset:lean. The 30+ lines of prepend content from copilot-sub-agents are silently dropped from the on-disk artifact.
This affects all 4 overlapping commands identically (speckit-specify, speckit-plan, speckit-tasks, speckit-implement) — not command-name specific.
Diagnostic checklist results
| Step |
Result |
Verdict |
1. specify preset resolve speckit.plan |
"not found" |
Resolver doesn't cover command-typed entries |
| 2. Both enabled, priorities set |
copilot-sub-agents=100, lean=10, both enabled |
✅ correct config |
| 3. On-disk SKILL.md |
Lean body only, frontmatter source: preset:lean |
❌ prepend dropped |
| 4. preset.yml declarations |
Both type: "command", matching name: speckit.plan, copilot-sub-agents has explicit strategy: "prepend" |
✅ correctly declared |
| 5. Source content files |
copilot-sub-agents file has 30+ lines of real prepend content |
✅ not empty |
| 6. init-options.json |
ai: copilot, ai_skills: true |
✅ skills mode |
7. specify preset disable && enable |
SKILL.md unchanged; CLI prints "Previously registered commands/skills remain active until preset removal" |
❌ enable/disable does not recompose |
| 8. Isolate command vs template |
All 4 overlapping commands affected identically |
✅ command-write path bug, not just plan |
Additional findings
specify preset resolve appears to be template-only — it returns "not found" for command entries, so users have no diagnostic to trace command composition.
specify preset disable && enable explicitly refuses to recompose ("previously registered commands/skills remain active"). There's no user-facing path to force reregistration short of remove + add.
- The frontmatter
source: preset:<single-preset> on generated SKILL.md is a strong tell: the writer is stamping a single-source header rather than a composed one, indicating a "resolve to file, copy it" shortcut instead of "resolve to content via the composing resolver."
Specify CLI Version
0.16.0
AI Agent
GitHub Copilot
Operating System
Windows 11
Python Version
Python 3.12.0
Error Logs
Additional Context
Root cause hypothesis
The write path in PresetManager.install_from_directory / CommandRegistrar for type: "command" entries appears to only materialize the effective replace base and never walks the composing layers above it to apply prepend/append/wrap. Templates compose correctly (per the bash/PowerShell resolvers in scripts/*/common.sh|ps1), but commands do not — despite presets/README.md documenting all four strategies as supported for commands.
Suggested fixes
PresetManager command materialization should walk composing layers above the effective replace base and apply prepend/append/wrap for type: "command" entries, matching the documented behavior and the template resolver semantics.
specify preset resolve should also cover type: "command" entries (or explicitly document that it's template-only).
- Consider a
specify preset reconcile command or --force-reregister flag so users can trigger recomposition without remove+add.
Impact
Any preset that layers command wrappers on top of another preset's commands via prepend/append/wrap is silently broken — the composition shows up correctly in tooling that reads preset.yml directly (e.g., wizards, specify preset info), but the on-disk SKILL.md never reflects it. Users get a false-positive install with the composed layer invisibly dropped, and the agent runs against the base body only.
Related
Bug Description
specify preset addcorrectly registerstype: "command"entries as SKILL.md files, but when a preset applies a composition strategy (prepend,append, orwrap) on top of another preset's command via areplacelayer, the writer materializes only the effectivereplacebase. The composing layers above the base are silently dropped from the on-disk artifact.Composition semantics work correctly for
type: "template"entries (bash/PowerShell resolvers apply prepend/append/wrap perscripts/*/common.sh|ps1, andpresets/README.mddocuments that commands support all four strategies too). But the command write path inPresetManager/CommandRegistrarappears to skip the composition step fortype: "command"entries.Setup: Copilot in skills mode, two presets installed:
copilot-sub-agents— priority 100,strategy: "prepend", activelean— priority 10,strategy: "replace"(effective base)Both presets declare
speckit.plan(and 3 other overlapping commands) astype: "command"with matchingname. Expected:.github/skills/speckit-plan/SKILL.md=copilot-sub-agentsprepend content + blank line +leanbody. Actual:leanbody only, frontmattersource: preset:lean, no trace of the prepend content.Steps to Reproduce
specify init myproj --integration copilot --integration-options="--skills" --script psspeckit.plan(and any other commands), where one usesstrategy: "replace"and one usesstrategy: "prepend":Expected Behavior
.github/skills/speckit-plan/SKILL.mdshould contain the composed body:Frontmatter should reflect the composition chain (or at minimum, not silently claim
leanis the sole source when aprependlayer above it is active).This matches the documented behavior in
presets/README.md:And the strategy support table in
presets/README.mdexplicitly lists all four strategies as supported fortype: "command".Actual Behavior
.github/skills/speckit-plan/SKILL.mdcontains only the Lean body with frontmattersource: preset:lean. The 30+ lines ofprependcontent fromcopilot-sub-agentsare silently dropped from the on-disk artifact.This affects all 4 overlapping commands identically (
speckit-specify,speckit-plan,speckit-tasks,speckit-implement) — not command-name specific.Diagnostic checklist results
specify preset resolve speckit.plansource: preset:leantype: "command", matchingname: speckit.plan, copilot-sub-agents has explicitstrategy: "prepend"ai: copilot,ai_skills: truespecify preset disable && enableAdditional findings
specify preset resolveappears to be template-only — it returns "not found" for command entries, so users have no diagnostic to trace command composition.specify preset disable && enableexplicitly refuses to recompose ("previously registered commands/skills remain active"). There's no user-facing path to force reregistration short ofremove+add.source: preset:<single-preset>on generated SKILL.md is a strong tell: the writer is stamping a single-source header rather than a composed one, indicating a "resolve to file, copy it" shortcut instead of "resolve to content via the composing resolver."Specify CLI Version
0.16.0
AI Agent
GitHub Copilot
Operating System
Windows 11
Python Version
Python 3.12.0
Error Logs
Additional Context
Root cause hypothesis
The write path in
PresetManager.install_from_directory/CommandRegistrarfortype: "command"entries appears to only materialize the effectivereplacebase and never walks the composing layers above it to applyprepend/append/wrap. Templates compose correctly (per the bash/PowerShell resolvers inscripts/*/common.sh|ps1), but commands do not — despitepresets/README.mddocumenting all four strategies as supported for commands.Suggested fixes
PresetManagercommand materialization should walk composing layers above the effectivereplacebase and applyprepend/append/wrapfortype: "command"entries, matching the documented behavior and the template resolver semantics.specify preset resolveshould also covertype: "command"entries (or explicitly document that it's template-only).specify preset reconcilecommand or--force-reregisterflag so users can trigger recomposition withoutremove+add.Impact
Any preset that layers command wrappers on top of another preset's commands via
prepend/append/wrapis silently broken — the composition shows up correctly in tooling that readspreset.ymldirectly (e.g., wizards,specify preset info), but the on-diskSKILL.mdnever reflects it. Users get a false-positive install with the composed layer invisibly dropped, and the agent runs against the base body only.Related
specify preset addno longer scaffolds commands — backward compatibility break for presets that previously created commands #4076 (originally "presets don't scaffold commands"; this is the sharper, provable version).fix(copilot): honor preset command template overrides— same subsystem, suggests known fragility in Copilot preset command handling.