Skip to content

[Bug]: Preset command composition (prepend/append/wrap) not applied — writer materializes only the effective replace base for type: "command" entries #4083

Description

@nicolehaugen

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

  1. specify init myproj --integration copilot --integration-options="--skills" --script ps
  2. 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
  3. Verify both are enabled and priorities set correctly:
    specify preset list
  4. 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

  1. 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.
  2. specify preset resolve should also cover type: "command" entries (or explicitly document that it's template-only).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions